From 3077653dd9f815052161565766f4a6a5a418d01a Mon Sep 17 00:00:00 2001 From: Synced Synapse Date: Sun, 24 Dec 2017 17:49:40 +0000 Subject: [PATCH] Fix notifications to work with Android Oreo Whan the background service is launcher, it is launched on the foreground and a notification is always shown. The service stays on while something is playing. It stops itself on a connection error, quitting Kodi or 5 seconds after stopping Make the Pause Phone Calls preference dependent on the Show Notification preference, as we always need to show a notification when the service is running --- .../ConnectionObserversManagerService.java | 62 ++++++++++--- .../kore/service/NotificationObserver.java | 91 +++++++++++++++---- .../ui/sections/remote/RemoteActivity.java | 41 +++------ .../sections/settings/SettingsFragment.java | 18 +++- .../main/java/org/xbmc/kore/utils/Utils.java | 4 + app/src/main/res/xml/preferences.xml | 11 ++- 6 files changed, 163 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/org/xbmc/kore/service/ConnectionObserversManagerService.java b/app/src/main/java/org/xbmc/kore/service/ConnectionObserversManagerService.java index 72a5d70..ad4c34c 100644 --- a/app/src/main/java/org/xbmc/kore/service/ConnectionObserversManagerService.java +++ b/app/src/main/java/org/xbmc/kore/service/ConnectionObserversManagerService.java @@ -19,6 +19,7 @@ import android.Manifest; import android.app.Service; import android.content.Intent; import android.content.pm.PackageManager; +import android.os.Handler; import android.os.IBinder; import android.preference.PreferenceManager; import android.support.v4.content.ContextCompat; @@ -51,6 +52,10 @@ public class ConnectionObserversManagerService extends Service private HostConnectionObserver mHostConnectionObserver = null; private List mConnectionObservers = new ArrayList<>(); + private NotificationObserver mNotificationObserver; + + private boolean somethingPlaying = false; + private Handler mStopHandler = new Handler(); @Override public void onCreate() { @@ -69,6 +74,7 @@ public class ConnectionObserversManagerService extends Service if (mConnectionObservers.isEmpty()) { LogUtils.LOGD(TAG, "No observers, stopping observer service."); stopSelf(); + return START_NOT_STICKY; } // Get the connection observer here, not on create to check if @@ -81,6 +87,16 @@ public class ConnectionObserversManagerService extends Service return START_STICKY; } + // Create the observers we are managing + createObservers(); + if (mConnectionObservers.isEmpty()) { + stopForeground(true); + stopSelf(); + return START_NOT_STICKY; + } + + startForeground(NotificationObserver.NOTIFICATION_ID, mNotificationObserver.getNothingPlayingNotification()); + // If there's a change in hosts, unregister from the previous one if (mHostConnectionObserver != null) { mHostConnectionObserver.unregisterPlayerObserver(this); @@ -97,14 +113,13 @@ public class ConnectionObserversManagerService extends Service private void createObservers() { mConnectionObservers = new ArrayList<>(); - // Check whether we should show a notification - boolean showNotification = PreferenceManager - .getDefaultSharedPreferences(this) - .getBoolean(Settings.KEY_PREF_SHOW_NOTIFICATION, - Settings.DEFAULT_PREF_SHOW_NOTIFICATION); - if (showNotification) { - mConnectionObservers.add(new NotificationObserver(this)); - } + // Always show a notification +// boolean showNotification = PreferenceManager +// .getDefaultSharedPreferences(this) +// .getBoolean(Settings.KEY_PREF_SHOW_NOTIFICATION, +// Settings.DEFAULT_PREF_SHOW_NOTIFICATION); + mNotificationObserver = new NotificationObserver(this); + mConnectionObservers.add(mNotificationObserver); // Check whether we should react to phone state changes and wether // we have permissions to do so @@ -136,6 +151,7 @@ public class ConnectionObserversManagerService extends Service if (mHostConnectionObserver != null) { mHostConnectionObserver.unregisterPlayerObserver(this); } + stopForeground(true); stopSelf(); } @@ -165,6 +181,7 @@ public class ConnectionObserversManagerService extends Service for (HostConnectionObserver.PlayerEventsObserver observer : mConnectionObservers) { observer.playerOnPlay(getActivePlayerResult, getPropertiesResult, getItemResult); } + somethingPlaying = true; } public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult, @@ -173,6 +190,7 @@ public class ConnectionObserversManagerService extends Service for (HostConnectionObserver.PlayerEventsObserver observer : mConnectionObservers) { observer.playerOnPause(getActivePlayerResult, getPropertiesResult, getItemResult); } + somethingPlaying = true; } public void playerOnStop() { @@ -180,30 +198,43 @@ public class ConnectionObserversManagerService extends Service observer.playerOnStop(); } - // Stop service - LogUtils.LOGD(TAG, "Player stopped"); -// if (mHostConnectionObserver != null) { -// mHostConnectionObserver.unregisterPlayerObserver(this); -// } -// stopSelf(); + somethingPlaying = false; + + // Stop service if nothing starts in a couple of seconds + mStopHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!somethingPlaying) { + LogUtils.LOGD(TAG, "Stopping service"); + if (mHostConnectionObserver != null) { + mHostConnectionObserver.unregisterPlayerObserver(ConnectionObserversManagerService.this); + } + stopForeground(true); + stopSelf(); + } + } + }, 5000); } public void playerNoResultsYet() { for (HostConnectionObserver.PlayerEventsObserver observer : mConnectionObservers) { observer.playerNoResultsYet(); } + somethingPlaying = false; } public void playerOnConnectionError(int errorCode, String description) { for (HostConnectionObserver.PlayerEventsObserver observer : mConnectionObservers) { observer.playerOnConnectionError(errorCode, description); } + somethingPlaying = false; // Stop service LogUtils.LOGD(TAG, "Shutting down observer service - Connection error"); if (mHostConnectionObserver != null) { mHostConnectionObserver.unregisterPlayerObserver(this); } + stopForeground(true); stopSelf(); } @@ -211,12 +242,14 @@ public class ConnectionObserversManagerService extends Service for (HostConnectionObserver.PlayerEventsObserver observer : mConnectionObservers) { observer.systemOnQuit(); } + somethingPlaying = false; // Stop service LogUtils.LOGD(TAG, "Shutting down observer service - System quit"); if (mHostConnectionObserver != null) { mHostConnectionObserver.unregisterPlayerObserver(this); } + stopForeground(true); stopSelf(); } @@ -233,6 +266,7 @@ public class ConnectionObserversManagerService extends Service } // Called when the user changes host LogUtils.LOGD(TAG, "Shutting down observer service - Stop observing"); + stopForeground(true); stopSelf(); } } diff --git a/app/src/main/java/org/xbmc/kore/service/NotificationObserver.java b/app/src/main/java/org/xbmc/kore/service/NotificationObserver.java index 34612be..5080baa 100644 --- a/app/src/main/java/org/xbmc/kore/service/NotificationObserver.java +++ b/app/src/main/java/org/xbmc/kore/service/NotificationObserver.java @@ -17,8 +17,11 @@ package org.xbmc.kore.service; import android.annotation.TargetApi; import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; @@ -52,11 +55,15 @@ public class NotificationObserver implements HostConnectionObserver.PlayerEventsObserver { public static final String TAG = LogUtils.makeLogTag(NotificationObserver.class); - private static final int NOTIFICATION_ID = 1; + public static final int NOTIFICATION_ID = 1; + public static final String NOTIFICATION_CHANNEL = "KORE"; private PendingIntent mRemoteStartPendingIntent; private Service mService; + private Notification mNothingPlayingNotification; + + public NotificationObserver(Service service) { this.mService = service; @@ -65,6 +72,12 @@ public class NotificationObserver stackBuilder.addParentStack(RemoteActivity.class); stackBuilder.addNextIntent(new Intent(mService, RemoteActivity.class)); mRemoteStartPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + + // Create the notification channel + if (Utils.isOreoOrLater()) { + buildNotificationChannel(); + } + mNothingPlayingNotification = buildNothingPlayingNotification(); } @Override @@ -78,21 +91,21 @@ public class NotificationObserver public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult, PlayerType.PropertyValue getPropertiesResult, ListType.ItemsAll getItemResult) { - buildNotification(getActivePlayerResult, getPropertiesResult, getItemResult); + notifyPlaying(getActivePlayerResult, getPropertiesResult, getItemResult); } public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult, PlayerType.PropertyValue getPropertiesResult, ListType.ItemsAll getItemResult) { - buildNotification(getActivePlayerResult, getPropertiesResult, getItemResult); + notifyPlaying(getActivePlayerResult, getPropertiesResult, getItemResult); } public void playerOnStop() { - removeNotification(); + notifyNothingPlaying(); } public void playerNoResultsYet() { - removeNotification(); + notifyNothingPlaying(); } public void playerOnConnectionError(int errorCode, String description) { @@ -111,13 +124,53 @@ public class NotificationObserver removeNotification(); } + @TargetApi(Build.VERSION_CODES.O) + private void buildNotificationChannel() { + + NotificationChannel channel = + new NotificationChannel(NOTIFICATION_CHANNEL, + mService.getString(R.string.app_name), + NotificationManager.IMPORTANCE_LOW); + channel.enableLights(false); + channel.enableVibration(false); + channel.setShowBadge(false); + + NotificationManager notificationManager = + (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.createNotificationChannel(channel); + } + // Picasso target that will be used to load images private static Target picassoTarget = null; + private Notification buildNothingPlayingNotification() { + int smallIcon = R.drawable.ic_devices_white_24dp; + + NotificationCompat.Builder builder = new NotificationCompat.Builder(mService, NOTIFICATION_CHANNEL); + return builder + .setSmallIcon(smallIcon) + .setShowWhen(false) + .setOngoing(true) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCategory(NotificationCompat.CATEGORY_TRANSPORT) + .setContentIntent(mRemoteStartPendingIntent) + .setContentTitle(String.format(mService.getString(R.string.connected_to), + HostManager.getInstance(mService).getHostInfo().getName())) + .setContentText(mService.getString(R.string.nothing_playing)) + .build(); + } + + public Notification getNothingPlayingNotification() { + if (mNothingPlayingNotification == null) { + mNothingPlayingNotification = buildNothingPlayingNotification(); + } + return mNothingPlayingNotification; + } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private void buildNotification(PlayerType.GetActivePlayersReturnType getActivePlayerResult, - PlayerType.PropertyValue getPropertiesResult, - ListType.ItemsAll getItemResult) { + private void notifyPlaying(PlayerType.GetActivePlayersReturnType getActivePlayerResult, + PlayerType.PropertyValue getPropertiesResult, + ListType.ItemsAll getItemResult) { final String title, underTitle, poster; int smallIcon, playPauseIcon, rewindIcon, ffIcon; @@ -212,7 +265,7 @@ public class NotificationObserver } // Build the notification - NotificationCompat.Builder builder = new NotificationCompat.Builder(mService); + NotificationCompat.Builder builder = new NotificationCompat.Builder(mService, NOTIFICATION_CHANNEL); final Notification notification = builder .setSmallIcon(smallIcon) .setShowWhen(false) @@ -270,10 +323,9 @@ public class NotificationObserver expandedRV.setImageViewBitmap(expandedIconResId, bitmap); } -// NotificationManager notificationManager = -// (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); -// notificationManager.notify(NOTIFICATION_ID, notification); - mService.startForeground(NOTIFICATION_ID, notification); + NotificationManager notificationManager = + (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_ID, notification); picassoTarget = null; } }; @@ -296,9 +348,14 @@ public class NotificationObserver } private void removeNotification() { -// NotificationManager notificationManager = -// (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); -// notificationManager.cancel(NOTIFICATION_ID); - mService.stopForeground(true); + NotificationManager notificationManager = + (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(NOTIFICATION_ID); + } + + private void notifyNothingPlaying() { + NotificationManager notificationManager = + (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_ID, mNothingPlayingNotification); } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java index bc5428b..1b9f487 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java @@ -64,6 +64,7 @@ import org.xbmc.kore.ui.volumecontrollers.VolumeKeyActionHandler; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.TabsAdapter; import org.xbmc.kore.utils.UIUtils; +import org.xbmc.kore.utils.Utils; import java.net.MalformedURLException; import java.net.URL; @@ -623,32 +624,20 @@ public class RemoteActivity extends BaseActivity } lastImageUrl = imageUrl; - // Start service that manages connection observers - LogUtils.LOGD(TAG, "Starting observer service"); - startService(new Intent(this, ConnectionObserversManagerService.class)); - - -// // Check whether we should show a notification -// boolean showNotification = PreferenceManager -// .getDefaultSharedPreferences(this) -// .getBoolean(Settings.KEY_PREF_SHOW_NOTIFICATION, -// Settings.DEFAULT_PREF_SHOW_NOTIFICATION); -// if (showNotification) { -// // Let's start the notification service -// LogUtils.LOGD(TAG, "Starting notification service"); -// startService(new Intent(this, NotificationObserver.class)); -// } -// -// // Check whether we should react to phone state changes -// boolean shouldPause = PreferenceManager -// .getDefaultSharedPreferences(this) -// .getBoolean(Settings.KEY_PREF_USE_HARDWARE_VOLUME_KEYS, -// Settings.DEFAULT_PREF_USE_HARDWARE_VOLUME_KEYS); -// if (shouldPause) { -// // Let's start the listening service -// LogUtils.LOGD(TAG, "Starting phone state listener"); -// startService(new Intent(this, PauseCallObserver.class)); -// } + // Check whether we should show a notification + boolean showNotification = PreferenceManager + .getDefaultSharedPreferences(this) + .getBoolean(Settings.KEY_PREF_SHOW_NOTIFICATION, + Settings.DEFAULT_PREF_SHOW_NOTIFICATION); + if (showNotification) { + // Start service that manages connection observers + LogUtils.LOGD(TAG, "Starting observer service"); + if (Utils.isOreoOrLater()) { + startForegroundService(new Intent(this, ConnectionObserversManagerService.class)); + } else { + startService(new Intent(this, ConnectionObserversManagerService.class)); + } + } } public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult, diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsFragment.java index 07c837f..1d03b79 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsFragment.java @@ -16,6 +16,8 @@ package org.xbmc.kore.ui.sections.settings; import android.Manifest; +import android.app.NotificationManager; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -40,6 +42,8 @@ import org.xbmc.kore.utils.Utils; import java.lang.reflect.Method; +import static org.xbmc.kore.service.NotificationObserver.NOTIFICATION_ID; + /** * Simple fragment to display preferences screen */ @@ -144,12 +148,22 @@ public class SettingsFragment extends PreferenceFragmentCompat } } - // If one of the settings that use the observer service are modified, restart it + // If one of the settings that use the observer service are modified, restart it if (key.equals(Settings.KEY_PREF_SHOW_NOTIFICATION) || key.equals(Settings.KEY_PREF_PAUSE_DURING_CALLS)) { LogUtils.LOGD(TAG, "Stoping connection observer service"); Intent intent = new Intent(getActivity(), ConnectionObserversManagerService.class); getActivity().stopService(intent); - getActivity().startService(intent); + if (sharedPreferences.getBoolean(Settings.KEY_PREF_SHOW_NOTIFICATION, Settings.DEFAULT_PREF_SHOW_NOTIFICATION)) { + if (Utils.isOreoOrLater()) { + getActivity().startForegroundService(intent); + } else { + getActivity().startService(intent); + } + } else { + NotificationManager notificationManager = + (NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(NOTIFICATION_ID); + } } } diff --git a/app/src/main/java/org/xbmc/kore/utils/Utils.java b/app/src/main/java/org/xbmc/kore/utils/Utils.java index b401dea..ed42b8d 100644 --- a/app/src/main/java/org/xbmc/kore/utils/Utils.java +++ b/app/src/main/java/org/xbmc/kore/utils/Utils.java @@ -69,6 +69,10 @@ public class Utils { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } + public static boolean isOreoOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + } + public static boolean isLollipopAndPreOreo() { return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) && (Build.VERSION.SDK_INT < 27); diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 7a04942..7bab420 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -64,17 +64,18 @@ android:title="@string/show_notification" android:defaultValue="false"/> + + - -