Skip to content
This repository has been archived by the owner on Dec 28, 2022. It is now read-only.

Commit

Permalink
Added a dummy notification helper..
Browse files Browse the repository at this point in the history
When the MusicService is started, the a dummy notification saying "Shuttle service running..." is shown, and the app is moved into the foreground state. This ensures that the system does not cause an ANR if we don't otherwise call through to stopService() (for example when we've decided not to foreground the service, because it's about to be shut down).

The dummy notification is silent, and is removed 12.5 seconds after it is created (unless removed earlier). According to the source on github, the Android system will ANR your app if you don't call startForeground 10 seconds after starting the foreground service. So, we wait an extra 2.5 seconds before we shut down.

If Shuttle is then moved into the foreground because it actually needs to be, the dummy notification is removed, as it's no longer required.

When the MusicService is destroyed, the dummy notification is removed. It seems that it's OK for this to happen within the ANR delay, as long as we have called startForeground() at some point.

Thia seems like the least annoying way to resolve this issue.
  • Loading branch information
timusus committed Feb 15, 2019
1 parent c1be138 commit 90f545b
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public void onLoadFailed(Exception e, Drawable errorDrawable) {
}));
}

public void startForeground(
public boolean startForeground(
Service service,
@NonNull Repository.PlaylistsRepository playlistsRepository,
@NonNull Repository.SongsRepository songsRepository,
Expand All @@ -179,9 +179,11 @@ public void startForeground(
analyticsManager.dropBreadcrumb(TAG, "startForeground() called");
Log.w(TAG, "service.startForeground called");
service.startForeground(NOTIFICATION_ID, notification);
return true;
} catch (RuntimeException e) {
Log.e(TAG, "startForeground not called, error: " + e);
LogUtils.logException(TAG, "Error starting foreground notification", e);
return false;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.simplecity.amp_library.playback;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.os.Build;
import android.support.annotation.Nullable;
import com.simplecity.amp_library.R;
import io.reactivex.Completable;
import io.reactivex.disposables.Disposable;
import java.util.concurrent.TimeUnit;

class DummyNotificationHelper {

private static int NOTIFICATION_ID_DUMMY = 5;

private boolean isShowingDummyNotification;
private boolean isForegroundedByApp = false;

private static String CHANNEL_ID = "channel_dummy";

// Must be greater than 10000
// See https://github.com/aosp-mirror/platform_frameworks_base/blob/e80b45506501815061b079dcb10bf87443bd385d/services/core/java/com/android/server/am/ActiveServices.java
// (SERVICE_START_FOREGROUND_TIMEOUT = 10*1000)
//
private static int NOTIFICATION_STOP_DELAY = 1250;

@Nullable
private Disposable dummyNotificationDisposable = null;

void setForegroundedByApp(boolean foregroundedByApp) {
isForegroundedByApp = foregroundedByApp;
}

void showDummyNotification(Service service) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isShowingDummyNotification) {
NotificationManager notificationManager = service.getSystemService(NotificationManager.class);
NotificationChannel channel = notificationManager.getNotificationChannel(CHANNEL_ID);
if (channel == null) {
channel = new NotificationChannel(CHANNEL_ID, service.getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(false);
channel.enableVibration(false);
channel.setSound(null, null);
channel.setShowBadge(false);
channel.setImportance(NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
}

Notification notification = new Notification.Builder(service, CHANNEL_ID)
.setContentTitle(service.getString(R.string.app_name))
.setContentText(service.getString(R.string.notification_text_shuttle_running))
.setSmallIcon(R.drawable.ic_stat_notification)
.build();

notificationManager.notify(NOTIFICATION_ID_DUMMY, notification);

if (!isForegroundedByApp) {
service.startForeground(NOTIFICATION_ID_DUMMY, notification);
}

isShowingDummyNotification = true;
}
}

if (dummyNotificationDisposable != null) {
dummyNotificationDisposable.dispose();
}
dummyNotificationDisposable = Completable.timer(NOTIFICATION_STOP_DELAY, TimeUnit.MILLISECONDS).doOnComplete(() -> removeDummyNotification(service)).subscribe();
}

void teardown(Service service) {

removeDummyNotification(service);

if (dummyNotificationDisposable != null) {
dummyNotificationDisposable.dispose();
}
}

private void removeDummyNotification(Service service) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (isShowingDummyNotification) {

if (dummyNotificationDisposable != null) {
dummyNotificationDisposable.dispose();
}

if (!isForegroundedByApp) {
service.stopForeground(true);
}

NotificationManager notificationManager = service.getSystemService(NotificationManager.class);
notificationManager.cancel(NOTIFICATION_ID_DUMMY);

isShowingDummyNotification = false;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@

import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
Expand Down Expand Up @@ -105,6 +101,8 @@ public class MusicService extends MediaBrowserServiceCompat {

private PlaybackManager playbackManager;

private DummyNotificationHelper dummyNotificationHelper = new DummyNotificationHelper();

@Inject
Repository.SongsRepository songsRepository;

Expand Down Expand Up @@ -337,28 +335,13 @@ public void onDestroy() {

playbackManager.destroy();

dummyNotificationHelper.teardown(this);

disposables.clear();

super.onDestroy();
}

void fakeForegroundNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
String channelId = "channel_1";
NotificationManager notificationManager = getSystemService(NotificationManager.class);
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
if (channel == null) {
channel = new NotificationChannel(channelId, getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(false);
channel.enableVibration(false);
notificationManager.createNotificationChannel(channel);
}

Notification notification = new Notification.Builder(this, channelId).build();
startForeground(45, notification);
}
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
serviceStartId = startId;
Expand All @@ -374,7 +357,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
if (action != null) {
switch (action) {
case ServiceCommand.NEXT:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is not already started:
// - With queue: Force to next song, start playback, update notification, no ANR
Expand All @@ -387,7 +370,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {

break;
case ServiceCommand.PREV:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is not already started:
// - With queue: ANR
Expand All @@ -399,7 +382,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {

break;
case ServiceCommand.PAUSE:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is not already started:
// - With queue: ANR
Expand All @@ -411,7 +394,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
break;
case ServiceCommand.PLAY:
case ShortcutCommands.PLAY:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is not already started:
// - No queue: ANR
Expand All @@ -422,7 +405,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
play();
break;
case ServiceCommand.TOGGLE_PLAYBACK:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

if (isPlaying()) {

Expand All @@ -437,7 +420,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
}
break;
case ServiceCommand.STOP:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is already started & we're not playing: ANR
// If the service is not already started: ANR
Expand All @@ -449,43 +432,43 @@ public int onStartCommand(Intent intent, int flags, int startId) {
new Handler().postDelayed(() -> stopForegroundImpl(true, false), 150);
break;
case ServiceCommand.SHUFFLE:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is not already started: ANR

toggleShuffleMode();
break;
case ServiceCommand.REPEAT:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is not already started: ANR

toggleRepeat();
break;
case ServiceCommand.TOGGLE_FAVORITE:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is not already started: ANR

toggleFavorite();
break;
case ExternalIntents.PLAY_STATUS_REQUEST:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is not already started: ANR

notifyChange(ExternalIntents.PLAY_STATUS_RESPONSE);
break;
case ServiceCommand.SHUTDOWN:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If the service is not already started: ANR

shutdownScheduled = false;
releaseServiceUiAndStop();
return START_NOT_STICKY;
case ShortcutCommands.SHUFFLE_ALL:
fakeForegroundNotification();
dummyNotificationHelper.showDummyNotification(this);

// If service is not already started: ANR

Expand Down Expand Up @@ -1013,8 +996,18 @@ private void startForegroundImpl() {
Song song = queueManager.getCurrentSong();
if (song != null) {
Log.i(TAG, "startForeground called");
notificationHelper.startForeground(this, playlistsRepository, songsRepository, queueManager.getCurrentSong(), isPlaying(), playbackManager.getMediaSessionToken(), settingsManager,
favoritesPlaylistManager);
if (notificationHelper.startForeground(
this,
playlistsRepository,
songsRepository,
queueManager.getCurrentSong(),
isPlaying(),
playbackManager.getMediaSessionToken(),
settingsManager,
favoritesPlaylistManager
)) {
dummyNotificationHelper.setForegroundedByApp(true);
}
} else {
Log.e(TAG, "startForeground should have been called, but song is null");
}
Expand All @@ -1034,6 +1027,7 @@ void stopForegroundImpl(boolean removeNotification, boolean withDelay) {
notificationStateHandler.sendEmptyMessageDelayed(NotificationStateHandler.STOP_FOREGROUND, 1500);
} else {
stopForeground(removeNotification);
dummyNotificationHelper.setForegroundedByApp(false);
}
}

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -839,4 +839,8 @@
<string name="saf_access_required_message">Shuttle requires permission to access these files. Please pick the directory containing your music.</string>
<string name="saf_show_files_button">Show files</string>



<string name="notification_text_shuttle_running">Shuttle service running…</string>

</resources>

0 comments on commit 90f545b

Please sign in to comment.