diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java index 411dbada..a7b7a52d 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java @@ -206,6 +206,7 @@ public class BookmarkActivity extends SubsonicTabActivity if (!getSelectedSongs(albumListView).isEmpty()) { int position = songs.get(0).getBookmarkPosition(); + if (getDownloadService() == null) return; getDownloadService().restore(songs, 0, position, true, true); selectAll(false, false); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java index 7f5e2222..a3e7954e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -97,11 +97,8 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen applyTheme(); super.onCreate(bundle); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(new Intent(this, DownloadServiceImpl.class)); - } else { - startService(new Intent(this, DownloadServiceImpl.class)); - } + // This should always succeed as it is called when Ultrasonic is in the foreground + startService(new Intent(this, DownloadServiceImpl.class)); setVolumeControlStream(AudioManager.STREAM_MUSIC); if (bundle != null) @@ -778,11 +775,16 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen } Log.w(TAG, "DownloadService not running. Attempting to start it."); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(new Intent(this, DownloadServiceImpl.class)); - } else { + + try + { startService(new Intent(this, DownloadServiceImpl.class)); } + catch (IllegalStateException exception) + { + Log.w(TAG, "getDownloadService couldn't start DownloadServiceImpl because the application was in the background."); + return null; + } Util.sleepQuietly(50L); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index cff36151..864d8a14 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -108,6 +108,13 @@ public class SettingsFragment extends PreferenceFragment setupClearSearchPreference(); setupGaplessControlSettingsV14(); setupFeatureFlagsPreferences(); + + // After API26 foreground services must be used for music playback, and they must have a notification + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PreferenceCategory notificationsCategory = (PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_CATEGORY_NOTIFICATIONS); + notificationsCategory.removePreference(findPreference(Constants.PREFERENCES_KEY_SHOW_NOTIFICATION)); + notificationsCategory.removePreference(findPreference(Constants.PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION)); + } } @Override diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java index 944c6481..171fe2ff 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java @@ -25,6 +25,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.util.Log; +import android.view.KeyEvent; import org.moire.ultrasonic.service.DownloadServiceImpl; import org.moire.ultrasonic.util.Util; @@ -58,11 +59,32 @@ public class MediaButtonIntentReceiver extends BroadcastReceiver Intent serviceIntent = new Intent(context, DownloadServiceImpl.class); serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(serviceIntent); - } else { + + try + { context.startService(serviceIntent); } + catch (IllegalStateException exception) + { + Log.i(TAG, "MediaButtonIntentReceiver couldn't start DownloadServiceImpl because the application was in the background."); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + KeyEvent keyEvent = (KeyEvent) event; + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.getRepeatCount() == 0) + { + int keyCode = keyEvent.getKeyCode(); + if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || + keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) + { + // TODO: The only time it is OK to start DownloadServiceImpl as a foreground service is when we now it will display its notification. + // When DownloadServiceImpl is refactored to a proper foreground service, this can be removed. + context.startForegroundService(serviceIntent); + Log.i(TAG, "MediaButtonIntentReceiver started DownloadServiceImpl as foreground service"); + } + } + } + } try { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java index 5558ed92..309eb8e4 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java @@ -154,6 +154,7 @@ public class DownloadServiceImpl extends Service implements DownloadService private final static int lockScreenBitmapSize = 500; private boolean isInForeground = false; + private NotificationCompat.Builder notificationBuilder; static { @@ -262,13 +263,30 @@ public class DownloadServiceImpl extends Service implements DownloadService instance = this; lifecycleSupport.onCreate(); + + // Create Notification Channel + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + //The suggested importance of a startForeground service notification is IMPORTANCE_LOW + NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); + channel.setLightColor(android.R.color.holo_blue_dark); + channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + manager.createNotificationChannel(channel); + } + + // We should use a single notification builder, otherwise the notification may not be updated + notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); + + Log.i(TAG, "DownloadServiceImpl created"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); + lifecycleSupport.onStart(intent); + Log.i(TAG, "DownloadServiceImpl started with intent"); return START_NOT_STICKY; } @@ -324,6 +342,8 @@ public class DownloadServiceImpl extends Service implements DownloadService catch (Throwable ignored) { } + + Log.i(TAG, "DownloadServiceImpl stopped"); } public static DownloadService getInstance() @@ -723,10 +743,7 @@ public class DownloadServiceImpl extends Service implements DownloadService if (currentPlaying != null) { if (tabInstance != null) { - if (Util.isNotificationEnabled(this)) { - startForeground(NOTIFICATION_ID, buildForegroundNotification()); - isInForeground = true; - } + updateNotification(); tabInstance.showNowPlaying(); } } @@ -735,6 +752,7 @@ public class DownloadServiceImpl extends Service implements DownloadService if (tabInstance != null) { stopForeground(true); + clearRemoteControl(); isInForeground = false; tabInstance.hideNowPlaying(); } @@ -1260,6 +1278,7 @@ public class DownloadServiceImpl extends Service implements DownloadService if (tabInstance != null) { stopForeground(true); + clearRemoteControl(); isInForeground = false; tabInstance.hideNowPlaying(); } @@ -2085,9 +2104,15 @@ public class DownloadServiceImpl extends Service implements DownloadService { if (Util.isNotificationEnabled(this)) { if (isInForeground == true) { - final NotificationManagerCompat notificationManager = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification()); + } + else { + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification()); + notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification()); + } Log.w(TAG, "--- Updated notification"); } else { @@ -2100,31 +2125,24 @@ public class DownloadServiceImpl extends Service implements DownloadService @SuppressWarnings("IconColors") private Notification buildForegroundNotification() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_NONE); - channel.setLightColor(android.R.color.holo_blue_dark); - channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.createNotificationChannel(channel); + notificationBuilder.setSmallIcon(R.drawable.ic_stat_ultrasonic); - } - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); - builder.setSmallIcon(R.drawable.ic_stat_ultrasonic); - - builder.setAutoCancel(false); - builder.setOngoing(true); - builder.setWhen(System.currentTimeMillis()); - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + notificationBuilder.setAutoCancel(false); + notificationBuilder.setOngoing(true); + notificationBuilder.setOnlyAlertOnce(true); + notificationBuilder.setWhen(System.currentTimeMillis()); + notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + notificationBuilder.setPriority(NotificationCompat.PRIORITY_LOW); RemoteViews contentView = new RemoteViews(this.getPackageName(), R.layout.notification); Util.linkButtons(this, contentView, false); RemoteViews bigView = new RemoteViews(this.getPackageName(), R.layout.notification_large); Util.linkButtons(this, bigView, false); - builder.setContent(contentView); + notificationBuilder.setContent(contentView); Intent notificationIntent = new Intent(this, DownloadActivity.class); - builder.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, 0)); + notificationBuilder.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, 0)); if (playerState == PlayerState.PAUSED || playerState == PlayerState.IDLE) { contentView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); @@ -2134,47 +2152,50 @@ public class DownloadServiceImpl extends Service implements DownloadService bigView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); } - final Entry song = currentPlaying.getSong(); - final String title = song.getTitle(); - final String text = song.getArtist(); - final String album = song.getAlbum(); - final int rating = song.getUserRating() == null ? 0 : song.getUserRating(); - final int imageSize = Util.getNotificationImageSize(this); + if (currentPlaying != null) { + final Entry song = currentPlaying.getSong(); + final String title = song.getTitle(); + final String text = song.getArtist(); + final String album = song.getAlbum(); + final int rating = song.getUserRating() == null ? 0 : song.getUserRating(); + final int imageSize = Util.getNotificationImageSize(this); - try { - final Bitmap nowPlayingImage = FileUtil.getAlbumArtBitmap(this, currentPlaying.getSong(), imageSize, true); - if (nowPlayingImage == null) { - contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - } else { - contentView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); - bigView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); - } - } catch (Exception x) { - Log.w(TAG, "Failed to get notification cover art", x); - contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - } + try { + final Bitmap nowPlayingImage = FileUtil.getAlbumArtBitmap(this, currentPlaying.getSong(), imageSize, true); + if (nowPlayingImage == null) { + contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } else { + contentView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); + bigView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); + } + } catch (Exception x) { + Log.w(TAG, "Failed to get notification cover art", x); + contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } - contentView.setTextViewText(R.id.trackname, title); - bigView.setTextViewText(R.id.trackname, title); - contentView.setTextViewText(R.id.artist, text); - bigView.setTextViewText(R.id.artist, text); - contentView.setTextViewText(R.id.album, album); - bigView.setTextViewText(R.id.album, album); - boolean useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING); - if (!useFiveStarRating) bigView.setViewVisibility(R.id.notification_rating, View.INVISIBLE); - else - { - bigView.setImageViewResource(R.id.notification_five_star_1, rating > 0 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_2, rating > 1 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_3, rating > 2 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_4, rating > 3 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_5, rating > 4 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + contentView.setTextViewText(R.id.trackname, title); + bigView.setTextViewText(R.id.trackname, title); + contentView.setTextViewText(R.id.artist, text); + bigView.setTextViewText(R.id.artist, text); + contentView.setTextViewText(R.id.album, album); + bigView.setTextViewText(R.id.album, album); + + boolean useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING); + if (!useFiveStarRating) + bigView.setViewVisibility(R.id.notification_rating, View.INVISIBLE); + else { + bigView.setImageViewResource(R.id.notification_five_star_1, rating > 0 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + bigView.setImageViewResource(R.id.notification_five_star_2, rating > 1 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + bigView.setImageViewResource(R.id.notification_five_star_3, rating > 2 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + bigView.setImageViewResource(R.id.notification_five_star_4, rating > 3 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + bigView.setImageViewResource(R.id.notification_five_star_5, rating > 4 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + } } - Notification notification = builder.build(); + Notification notification = notificationBuilder.build(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { notification.bigContentView = bigView; } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java index 1ae1eaeb..265bce86 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java @@ -289,6 +289,7 @@ public class DownloadServiceLifecycleSupport return; } Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); + // TODO: here the autoPlay = false creates problems when Ultrasonic is started by a Play MediaButton as the player won't start this way. downloadService.restore(state.songs, state.currentPlayingIndex, state.currentPlayingPosition, false, false); // Work-around: Serialize again, as the restore() method creates a serialization without current playing info. @@ -321,7 +322,11 @@ public class DownloadServiceLifecycleSupport downloadService.stop(); break; case KeyEvent.KEYCODE_MEDIA_PLAY: - if (downloadService.getPlayerState() != PlayerState.STARTED) + if (downloadService.getPlayerState() == PlayerState.IDLE) + { + downloadService.play(); + } + else if (downloadService.getPlayerState() != PlayerState.STARTED) { downloadService.start(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java index d0ca72b7..827891fb 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java @@ -132,6 +132,7 @@ public final class Constants public static final String PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY = "imageLoaderConcurrency"; public static final String PREFERENCES_KEY_FF_IMAGE_LOADER = "ff_new_image_loader"; public static final String PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating"; + public static final String PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory"; // Number of free trial days for non-licensed servers. public static final int FREE_TRIAL_DAYS = 30; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java index 578ae72e..4c455236 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -36,6 +36,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.net.wifi.WifiManager; +import android.os.Build; import android.os.Environment; import android.os.Parcelable; import android.preference.PreferenceManager; @@ -155,12 +156,16 @@ public class Util extends DownloadActivity public static boolean isNotificationEnabled(Context context) { + // After API26 foreground services must be used for music playback, and they must have a notification + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) return true; SharedPreferences preferences = getPreferences(context); return preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_NOTIFICATION, false); } public static boolean isNotificationAlwaysEnabled(Context context) { + // After API26 foreground services must be used for music playback, and they must have a notification + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) return true; SharedPreferences preferences = getPreferences(context); return preferences.getBoolean(Constants.PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION, false); } diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 27fcff8a..eca14974 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -107,7 +107,9 @@ a:summary="@string/settings.playback.resume_play_on_headphones_plug.summary" /> - +