From 3d166e6d4b49ebe298aa55b2e944713e7d3c60ff Mon Sep 17 00:00:00 2001 From: Albert Vaca Cintora Date: Thu, 2 May 2024 16:14:45 +0200 Subject: [PATCH] Offer to "continue playing" media on this device after pausing Based on the MR !249 Co-authored-by: Alex Gravenor --- res/values/strings.xml | 7 +++ res/xml/mprisplugin_preferences.xml | 7 +++ .../Helpers/NotificationHelper.java | 19 +++++-- .../Plugins/MprisPlugin/MprisPlugin.kt | 49 ++++++++++++++++++- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index dd581970..23d4e2df 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -569,4 +569,11 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted The notifications permission is needed so the phone can ring when the app is in the background Notifications are disabled, you won\'t receive incoming pair notifications. + + Continue playing + mpris_keepwatching_enabled + Continue playing + Show a silent notification to continue playing on this device after closing media + Continue playing + diff --git a/res/xml/mprisplugin_preferences.xml b/res/xml/mprisplugin_preferences.xml index e29d7a06..2b522f49 100644 --- a/res/xml/mprisplugin_preferences.xml +++ b/res/xml/mprisplugin_preferences.xml @@ -28,4 +28,11 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted android:summary="@string/mpris_notification_settings_summary" android:title="@string/mpris_notification_settings_title" /> + + diff --git a/src/org/kde/kdeconnect/Helpers/NotificationHelper.java b/src/org/kde/kdeconnect/Helpers/NotificationHelper.java index 5ef6512d..aff10eb6 100644 --- a/src/org/kde/kdeconnect/Helpers/NotificationHelper.java +++ b/src/org/kde/kdeconnect/Helpers/NotificationHelper.java @@ -35,6 +35,8 @@ public class NotificationHelper { public final static String RECEIVENOTIFICATION = "receive"; public final static String HIGHPRIORITY = "highpriority"; + public final static String CONTINUEWATCHING = "continuewatching"; + } public static void notifyCompat(NotificationManager notificationManager, int notificationId, Notification notification) { @@ -87,14 +89,23 @@ public class NotificationHelper { .Builder(Channels.RECEIVENOTIFICATION, NotificationManagerCompat.IMPORTANCE_DEFAULT) .setName(context.getString(R.string.notification_channel_receivenotification)) .build(); - final NotificationChannelCompat highPriorityChannel = new NotificationChannelCompat + final NotificationChannelCompat continueWatchingChannel = new NotificationChannelCompat .Builder(Channels.HIGHPRIORITY, NotificationManagerCompat.IMPORTANCE_HIGH) .setName(context.getString(R.string.notification_channel_high_priority)) .build(); - + /* This notification should be highly visible *only* if the user looks at their phone */ + /* It should not be a distraction. It should be a convenient button to press */ + final NotificationChannelCompat highPriorityChannel = new NotificationChannelCompat + .Builder(Channels.CONTINUEWATCHING, NotificationManagerCompat.IMPORTANCE_HIGH) + .setName(context.getString(R.string.notification_channel_keepwatching)) + .setVibrationEnabled(false) + .setLightsEnabled(false) + .setSound(null, null) + .build(); final List channels = Arrays.asList(persistentChannel, defaultChannel, mediaChannel, fileTransferDownloadChannel, fileTransferUploadChannel, - fileTransferErrorChannel, receiveNotificationChannel, highPriorityChannel); + fileTransferErrorChannel, receiveNotificationChannel, highPriorityChannel, + continueWatchingChannel); NotificationManagerCompat.from(context).createNotificationChannelsCompat(channels); @@ -102,7 +113,7 @@ public class NotificationHelper { // Use this to deprecate old channels. NotificationManagerCompat.from(context).deleteUnlistedNotificationChannels( channels.stream() - .map(notificationChannelCompat -> notificationChannelCompat.getId()) + .map(NotificationChannelCompat::getId) .collect(Collectors.toList())); } diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.kt b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.kt index e6ef070b..ca485bd6 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.kt +++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.kt @@ -7,12 +7,20 @@ package org.kde.kdeconnect.Plugins.MprisPlugin import android.Manifest import android.app.Activity +import android.app.NotificationManager +import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.Bitmap +import android.net.Uri import android.os.Build import android.util.Log import androidx.annotation.DrawableRes +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager +import org.kde.kdeconnect.Helpers.NotificationHelper +import org.kde.kdeconnect.Helpers.VideoUrlsHelper import org.kde.kdeconnect.NetworkPacket import org.kde.kdeconnect.Plugins.MprisPlugin.AlbumArtCache.deregisterPlugin import org.kde.kdeconnect.Plugins.MprisPlugin.AlbumArtCache.getAlbumArt @@ -243,6 +251,7 @@ class MprisPlugin : Plugin() { if (np.has("player")) { val playerStatus = players[np.getString("player")] if (playerStatus != null) { + val wasPlaying = playerStatus.isPlaying //Note: title, artist and album will not be available for all desktop clients playerStatus.title = np.getString("title", playerStatus.title) playerStatus.artist = np.getString("artist", playerStatus.artist) @@ -286,6 +295,11 @@ class MprisPlugin : Plugin() { playerStatusUpdated.remove(key) } } + + // Check to see if a stream has stopped playing and we should deliver a notification + if (np.has("isPlaying") && !playerStatus.isPlaying && wasPlaying) { + showContinueWatchingNotification(playerStatus) + } } } @@ -309,8 +323,13 @@ class MprisPlugin : Plugin() { val oldPlayer = it.key val found = newPlayerList.stream().anyMatch { newPlayer -> newPlayer == oldPlayer } if (!found) { - iter.remove() + // Player got removed equals = false + iter.remove() + val playerStatus = it.value + if (playerStatus.isPlaying) { + showContinueWatchingNotification(playerStatus) + } } } if (!equals) { @@ -328,6 +347,34 @@ class MprisPlugin : Plugin() { return true } + private fun showContinueWatchingNotification(playerStatus: MprisPlayer) { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + if (prefs.getBoolean(context.getString(R.string.mpris_keepwatching_key), true) && + (playerStatus.url.startsWith("http://") || playerStatus.url.startsWith("https://")) + ) { + try { + val url = VideoUrlsHelper.formatUriWithSeek(playerStatus.url, playerStatus.position).toString() + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + val pendingIntent = PendingIntent.getActivity(device.context, 0, browserIntent, PendingIntent.FLAG_IMMUTABLE) + + val notificationManager = ContextCompat.getSystemService(device.context, NotificationManager::class.java) + val builder = NotificationCompat.Builder(device.context, NotificationHelper.Channels.CONTINUEWATCHING) + .setContentTitle(context.resources.getString(R.string.kde_connect)) + .setSmallIcon(R.drawable.ic_play_white) + .setTimeoutAfter(3000) + .setContentIntent(pendingIntent) + .setContentText(context.resources.getString(R.string.mpris_keepwatching) + " " + playerStatus.title) + NotificationHelper.notifyCompat( + notificationManager, + System.currentTimeMillis().toInt(), + builder.build() + ) + } catch (e: MalformedURLException) { + e.printStackTrace(); + } + } + } + override val supportedPacketTypes: Array = arrayOf(PACKET_TYPE_MPRIS) override val outgoingPacketTypes: Array = arrayOf(PACKET_TYPE_MPRIS_REQUEST)