From 13b987791e164169191460188e12a11b372d2962 Mon Sep 17 00:00:00 2001 From: Nite Date: Sun, 21 Jun 2020 09:31:38 +0200 Subject: [PATCH] Started DownloadServiceImpl refactor --- ultrasonic/src/main/AndroidManifest.xml | 2 +- .../ultrasonic/activity/MainActivity.java | 3 +- .../activity/SubsonicTabActivity.java | 32 +- .../provider/UltraSonicAppWidgetProvider.java | 26 +- .../receiver/MediaButtonIntentReceiver.java | 35 +- .../ultrasonic/service/DownloadFile.java | 5 +- .../service/DownloadServiceImpl.java | 1912 ++--------------- .../DownloadServiceLifecycleSupport.java | 37 +- .../ultrasonic/service/JukeboxService.java | 24 +- .../service/MediaPlayerService.java | 1792 +++++++++++++++ .../moire/ultrasonic/service/Supplier.java | 6 + .../moire/ultrasonic/util/StreamProxy.java | 10 +- .../java/org/moire/ultrasonic/util/Util.java | 48 +- 13 files changed, 2056 insertions(+), 1876 deletions(-) create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index f34f2342..93d6ce3e 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -118,7 +118,7 @@ diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java index d9665a79..c5a4c53a 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java @@ -36,6 +36,7 @@ import android.widget.TextView; import org.moire.ultrasonic.R; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.MediaPlayerService; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.util.Constants; @@ -476,7 +477,7 @@ public class MainActivity extends SubsonicTabActivity private void exit() { - stopService(new Intent(this, DownloadServiceImpl.class)); + DownloadServiceImpl.getInstance().onCommand(new Intent(this, MediaPlayerService.class)); Util.unregisterMediaButtonEventReceiver(this); finish(); } 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 a3e7954e..43712579 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -97,8 +97,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen applyTheme(); super.onCreate(bundle); - // This should always succeed as it is called when Ultrasonic is in the foreground - startService(new Intent(this, DownloadServiceImpl.class)); + if (DownloadServiceImpl.getInstance() == null) new DownloadServiceImpl(getApplicationContext()); setVolumeControlStream(AudioManager.STREAM_MUSIC); if (bundle != null) @@ -764,30 +763,17 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen public DownloadService getDownloadService() { - // If service is not available, request it to start and wait for it. - for (int i = 0; i < 5; i++) + DownloadService downloadService = DownloadServiceImpl.getInstance(); + + if (downloadService != null) { - DownloadService downloadService = DownloadServiceImpl.getInstance(); - - if (downloadService != null) - { - return downloadService; - } - - Log.w(TAG, "DownloadService not running. Attempting to start it."); - - 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); + return downloadService; } + Log.w(TAG, "DownloadService not running. Attempting to start it."); + + new DownloadServiceImpl(getApplicationContext()); + return DownloadServiceImpl.getInstance(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java index d4e986c6..8a834026 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java @@ -19,6 +19,7 @@ import org.moire.ultrasonic.activity.MainActivity; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.MediaPlayerService; import org.moire.ultrasonic.util.FileUtil; public class UltraSonicAppWidgetProvider extends AppWidgetProvider @@ -69,11 +70,11 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider /** * Handle a change notification coming over from {@link DownloadService} */ - public void notifyChange(Context context, DownloadService service, boolean playing, boolean setAlbum) + public void notifyChange(Context context, MusicDirectory.Entry currentSong, boolean playing, boolean setAlbum) { if (hasInstances(context)) { - performUpdate(context, service, null, playing, setAlbum); + performUpdate(context, currentSong, null, playing, setAlbum); } } @@ -96,15 +97,14 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider /** * Update all active widget instances by pushing changes */ - private void performUpdate(Context context, DownloadService service, int[] appWidgetIds, boolean playing, boolean setAlbum) + private void performUpdate(Context context, MusicDirectory.Entry currentSong, int[] appWidgetIds, boolean playing, boolean setAlbum) { final Resources res = context.getResources(); final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId); - MusicDirectory.Entry currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong(); - String title = currentPlaying == null ? null : currentPlaying.getTitle(); - String artist = currentPlaying == null ? null : currentPlaying.getArtist(); - String album = currentPlaying == null ? null : currentPlaying.getAlbum(); + String title = currentSong == null ? null : currentSong.getTitle(); + String artist = currentSong == null ? null : currentSong.getArtist(); + String album = currentSong == null ? null : currentSong.getAlbum(); CharSequence errorState = null; // Show error message? @@ -117,7 +117,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider { errorState = res.getText(R.string.widget_sdcard_missing); } - else if (currentPlaying == null) + else if (currentSong == null) { errorState = res.getText(R.string.widget_initial_text); } @@ -157,7 +157,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider // Set the cover art try { - Bitmap bitmap = currentPlaying == null ? null : FileUtil.getAlbumArtBitmap(context, currentPlaying, 240, true); + Bitmap bitmap = currentSong == null ? null : FileUtil.getAlbumArtBitmap(context, currentSong, 240, true); if (bitmap == null) { @@ -176,7 +176,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider } // Link actions buttons to intents - linkButtons(context, views, currentPlaying != null); + linkButtons(context, views, currentSong != null); pushUpdate(context, appWidgetIds, views); } @@ -200,19 +200,19 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider // Emulate media button clicks. intent = new Intent("1"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.control_play, pendingIntent); intent = new Intent("2"); // Use a unique action name to ensure a different PendingIntent to be created. - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.control_next, pendingIntent); intent = new Intent("3"); // Use a unique action name to ensure a different PendingIntent to be created. - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); 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 171fe2ff..09593d9e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java @@ -25,9 +25,9 @@ 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.service.MediaPlayerService; import org.moire.ultrasonic.util.Util; /** @@ -57,42 +57,23 @@ public class MediaButtonIntentReceiver extends BroadcastReceiver Parcelable event = (Parcelable) extras.get(Intent.EXTRA_KEY_EVENT); Log.i(TAG, "Got MEDIA_BUTTON key event: " + event); - Intent serviceIntent = new Intent(context, DownloadServiceImpl.class); + Intent serviceIntent = new Intent(context, MediaPlayerService.class); serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); 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"); - } - } - } - } + if (DownloadServiceImpl.getInstance() == null) new DownloadServiceImpl(context); + DownloadServiceImpl.getInstance().onCommand(serviceIntent); - try - { if (isOrderedBroadcast()) { abortBroadcast(); } } + catch (IllegalStateException exception) + { + Log.w(TAG, "MediaButtonIntentReceiver couldn't start DownloadServiceImpl because the application was in the background."); + } catch (Exception x) { // Ignored. diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java index 3c7d9ef4..22e25e15 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java @@ -441,10 +441,7 @@ public class DownloadFile new CacheCleaner(context, DownloadServiceImpl.getInstance()).cleanSpace(); - if (DownloadServiceImpl.getInstance() != null) - { - ((DownloadServiceImpl) DownloadServiceImpl.getInstance()).checkDownloads(); - } + MediaPlayerService.checkDownloads(context); } } 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 309eb8e4..47b37406 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java @@ -18,37 +18,12 @@ */ package org.moire.ultrasonic.service; -import android.annotation.SuppressLint; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.graphics.Bitmap; -import android.media.AudioManager; -import android.media.MediaMetadataRetriever; -import android.media.MediaPlayer; -import android.media.RemoteControlClient; -import android.media.audiofx.AudioEffect; -import android.os.Build; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; import android.os.PowerManager; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; import android.util.Log; -import android.view.View; -import android.widget.RemoteViews; -import android.widget.SeekBar; import org.koin.java.standalone.KoinJavaComponent; -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.activity.DownloadActivity; -import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.audiofx.EqualizerController; import org.moire.ultrasonic.audiofx.VisualizerController; import org.moire.ultrasonic.domain.MusicDirectory; @@ -58,41 +33,22 @@ import org.moire.ultrasonic.domain.RepeatMode; import org.moire.ultrasonic.domain.UserInfo; import org.moire.ultrasonic.featureflags.Feature; import org.moire.ultrasonic.featureflags.FeatureStorage; -import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x1; -import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x2; -import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x3; -import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x4; -import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; -import org.moire.ultrasonic.util.CancellableTask; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.FileUtil; import org.moire.ultrasonic.util.LRUCache; import org.moire.ultrasonic.util.ShufflePlayBuffer; -import org.moire.ultrasonic.util.SimpleServiceBinder; -import org.moire.ultrasonic.util.StreamProxy; import org.moire.ultrasonic.util.Util; -import java.io.File; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; -import static org.moire.ultrasonic.domain.PlayerState.COMPLETED; -import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; -import static org.moire.ultrasonic.domain.PlayerState.IDLE; -import static org.moire.ultrasonic.domain.PlayerState.PAUSED; -import static org.moire.ultrasonic.domain.PlayerState.PREPARED; -import static org.moire.ultrasonic.domain.PlayerState.PREPARING; -import static org.moire.ultrasonic.domain.PlayerState.STARTED; -import static org.moire.ultrasonic.domain.PlayerState.STOPPED; +import static org.moire.ultrasonic.service.MediaPlayerService.playerState; /** * @author Sindre Mehus, Joshua Bahnsen * @version $Id$ */ -public class DownloadServiceImpl extends Service implements DownloadService +public class DownloadServiceImpl implements DownloadService { private static final String TAG = DownloadServiceImpl.class.getSimpleName(); @@ -103,264 +59,51 @@ public class DownloadServiceImpl extends Service implements DownloadService public static final String CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS"; public static final String CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT"; - private static final String NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"; - private static final String NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"; - private static final int NOTIFICATION_ID = 3033; - - private final IBinder binder = new SimpleServiceBinder(this); - private Looper mediaPlayerLooper; - private MediaPlayer mediaPlayer; - private MediaPlayer nextMediaPlayer; - private boolean nextSetup; - private final List downloadList = new ArrayList(); - private final List backgroundDownloadList = new ArrayList(); - private final Handler handler = new Handler(); - private Handler mediaPlayerHandler; - private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this); - private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this); + private DownloadServiceLifecycleSupport lifecycleSupport; private final LRUCache downloadFileCache = new LRUCache(100); - private final List cleanupCandidates = new ArrayList(); - private final Scrobbler scrobbler = new Scrobbler(); - private final JukeboxService jukeboxService = new JukeboxService(this); - private DownloadFile currentPlaying; - private DownloadFile nextPlaying; - private DownloadFile currentDownloading; - private CancellableTask bufferTask; - private CancellableTask nextPlayingTask; - private PlayerState playerState = IDLE; - private PlayerState nextPlayerState = IDLE; - private boolean shufflePlay; - private long revision; - private static DownloadService instance; + private static DownloadServiceImpl instance; private String suggestedPlaylistName; - private PowerManager.WakeLock wakeLock; private boolean keepScreenOn; - private int cachedPosition; - private static boolean equalizerAvailable; - private static boolean visualizerAvailable; - private EqualizerController equalizerController; - private VisualizerController visualizerController; private boolean showVisualization; private boolean jukeboxEnabled; - private PositionCache positionCache; - private StreamProxy proxy; - public RemoteControlClient remoteControlClient; - private AudioManager audioManager; - private int secondaryProgress = -1; private boolean autoPlayStart; - private final static int lockScreenBitmapSize = 500; - private boolean isInForeground = false; - private NotificationCompat.Builder notificationBuilder; + private Context context; - static + public DownloadServiceImpl(Context context) { - try - { - EqualizerController.checkAvailable(); - equalizerAvailable = true; - } - catch (Throwable t) - { - equalizerAvailable = false; - } - } + this.context = context; - static - { - try - { - VisualizerController.checkAvailable(); - visualizerAvailable = true; - } - catch (Throwable t) - { - visualizerAvailable = false; - } - } - - @SuppressLint("NewApi") - @Override - public void onCreate() - { - super.onCreate(); - - new Thread(new Runnable() - { - @Override - public void run() - { - Thread.currentThread().setName("DownloadServiceImpl"); - - Looper.prepare(); - - if (mediaPlayer != null) - { - mediaPlayer.release(); - } - - mediaPlayer = new MediaPlayer(); - mediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK); - - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() - { - @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int more) - { - handleError(new Exception(String.format("MediaPlayer error: %d (%d)", what, more))); - return false; - } - }); - - try - { - Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); - i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); - sendBroadcast(i); - } - catch (Throwable e) - { - // Froyo or lower - } - - mediaPlayerLooper = Looper.myLooper(); - mediaPlayerHandler = new Handler(mediaPlayerLooper); - Looper.loop(); - } - }).start(); - - audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); - setUpRemoteControlClient(); - - if (equalizerAvailable) - { - equalizerController = new EqualizerController(this, mediaPlayer); - if (!equalizerController.isAvailable()) - { - equalizerController = null; - } - else - { - equalizerController.loadSettings(); - } - } - if (visualizerAvailable) - { - visualizerController = new VisualizerController(mediaPlayer); - if (!visualizerController.isAvailable()) - { - visualizerController = null; - } - } - - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); - wakeLock.setReferenceCounted(false); + // TODO: refactor + MediaPlayerService.shufflePlayBuffer = new ShufflePlayBuffer(context); + MediaPlayerService.jukeboxService = new JukeboxService(context, this); instance = this; + lifecycleSupport = new DownloadServiceLifecycleSupport(context,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); + MediaPlayerService.lifecycleSupport = lifecycleSupport; Log.i(TAG, "DownloadServiceImpl created"); } - @Override - public int onStartCommand(Intent intent, int flags, int startId) + public void onCommand(Intent intent) { - super.onStartCommand(intent, flags, startId); - lifecycleSupport.onStart(intent); - Log.i(TAG, "DownloadServiceImpl started with intent"); - return START_NOT_STICKY; + Log.i(TAG, "DownloadServiceImpl received intent"); } - @Override - public void onDestroy() - { - super.onDestroy(); - - try - { - instance = null; - lifecycleSupport.onDestroy(); - mediaPlayer.release(); - - if (nextMediaPlayer != null) - { - nextMediaPlayer.release(); - } - - mediaPlayerLooper.quit(); - shufflePlayBuffer.shutdown(); - - if (equalizerController != null) - { - equalizerController.release(); - } - - if (visualizerController != null) - { - visualizerController.release(); - } - - if (bufferTask != null) - { - bufferTask.cancel(); - } - - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - } - - Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); - i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); - sendBroadcast(i); - - audioManager.unregisterRemoteControlClient(remoteControlClient); - clearRemoteControl(); - - wakeLock.release(); - } - catch (Throwable ignored) - { - } - - Log.i(TAG, "DownloadServiceImpl stopped"); - } - - public static DownloadService getInstance() + public static DownloadServiceImpl getInstance() { return instance; } - @Override - public IBinder onBind(Intent intent) - { - return binder; - } - @Override public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, boolean newPlaylist) { - shufflePlay = false; + MediaPlayerService.shufflePlay = false; int offset = 1; if (songs.isEmpty()) @@ -370,7 +113,7 @@ public class DownloadServiceImpl extends Service implements DownloadService if (newPlaylist) { - downloadList.clear(); + MediaPlayerService.downloadList.clear(); } if (playNext) @@ -382,12 +125,12 @@ public class DownloadServiceImpl extends Service implements DownloadService for (MusicDirectory.Entry song : songs) { - DownloadFile downloadFile = new DownloadFile(this, song, save); - downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); + DownloadFile downloadFile = new DownloadFile(context, song, save); + MediaPlayerService.downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); offset++; } - revision++; + MediaPlayerService.revision++; } else { @@ -396,19 +139,20 @@ public class DownloadServiceImpl extends Service implements DownloadService for (MusicDirectory.Entry song : songs) { - DownloadFile downloadFile = new DownloadFile(this, song, save); - downloadList.add(downloadFile); + DownloadFile downloadFile = new DownloadFile(context, song, save); + MediaPlayerService.downloadList.add(downloadFile); } if (!autoplay && (size - 1) == index) { - setNextPlaying(); + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); } - revision++; + MediaPlayerService.revision++; } - updateJukeboxPlaylist(); + MediaPlayerService.updateJukeboxPlaylist(); if (shuffle) shuffle(); @@ -418,13 +162,13 @@ public class DownloadServiceImpl extends Service implements DownloadService } else { - if (currentPlaying == null) + if (MediaPlayerService.currentPlaying == null) { - currentPlaying = downloadList.get(0); - currentPlaying.setPlaying(true); + MediaPlayerService.currentPlaying = MediaPlayerService.downloadList.get(0); + MediaPlayerService.currentPlaying.setPlaying(true); } - checkDownloads(); + MediaPlayerService.checkDownloads(context); } lifecycleSupport.serializeDownloadQueue(); @@ -435,24 +179,16 @@ public class DownloadServiceImpl extends Service implements DownloadService { for (MusicDirectory.Entry song : songs) { - DownloadFile downloadFile = new DownloadFile(this, song, save); - backgroundDownloadList.add(downloadFile); + DownloadFile downloadFile = new DownloadFile(context, song, save); + MediaPlayerService.backgroundDownloadList.add(downloadFile); } - revision++; + MediaPlayerService.revision++; - checkDownloads(); + MediaPlayerService.checkDownloads(context); lifecycleSupport.serializeDownloadQueue(); } - private void updateJukeboxPlaylist() - { - if (jukeboxEnabled) - { - jukeboxService.updatePlaylist(); - } - } - @Override public void restore(List songs, int currentPlayingIndex, int currentPlayingPosition, boolean autoPlay, boolean newPlaylist) { @@ -460,24 +196,19 @@ public class DownloadServiceImpl extends Service implements DownloadService if (currentPlayingIndex != -1) { - while (mediaPlayer == null) - { - Util.sleepQuietly(50L); - } + MediaPlayerService.getInstance(context).play(currentPlayingIndex, autoPlayStart); - play(currentPlayingIndex, autoPlayStart); - - if (currentPlaying != null) + if (MediaPlayerService.currentPlaying != null) { if (autoPlay && jukeboxEnabled) { - jukeboxService.skip(getCurrentPlayingIndex(), currentPlayingPosition / 1000); + MediaPlayerService.jukeboxService.skip(getCurrentPlayingIndex(), currentPlayingPosition / 1000); } else { - if (currentPlaying.isCompleteFileAvailable()) + if (MediaPlayerService.currentPlaying.isCompleteFileAvailable()) { - doPlay(currentPlaying, currentPlayingPosition, autoPlay); + MediaPlayerService.getInstance(context).doPlay(MediaPlayerService.currentPlaying, currentPlayingPosition, autoPlay); } } } @@ -486,61 +217,82 @@ public class DownloadServiceImpl extends Service implements DownloadService } } + public synchronized void setCurrentPlaying(DownloadFile currentPlaying) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setCurrentPlaying(currentPlaying); + } + + public synchronized void setCurrentPlaying(int index) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setCurrentPlaying(index); + } + + public synchronized void setPlayerState(PlayerState state) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setPlayerState(state); + } + @Override public void stopJukeboxService() { - jukeboxService.stopJukeboxService(); + MediaPlayerService.jukeboxService.stopJukeboxService(); } @Override public void startJukeboxService() { - jukeboxService.startJukeboxService(); + MediaPlayerService.jukeboxService.startJukeboxService(); } @Override public synchronized void setShufflePlayEnabled(boolean enabled) { - shufflePlay = enabled; - if (shufflePlay) + MediaPlayerService.shufflePlay = enabled; + if (MediaPlayerService.shufflePlay) { clear(); - checkDownloads(); + MediaPlayerService.checkDownloads(context); } } @Override public boolean isShufflePlayEnabled() { - return shufflePlay; + return MediaPlayerService.shufflePlay; } @Override public synchronized void shuffle() { - Collections.shuffle(downloadList); - if (currentPlaying != null) + Collections.shuffle(MediaPlayerService.downloadList); + if (MediaPlayerService.currentPlaying != null) { - downloadList.remove(getCurrentPlayingIndex()); - downloadList.add(0, currentPlaying); + MediaPlayerService.downloadList.remove(getCurrentPlayingIndex()); + MediaPlayerService.downloadList.add(0, MediaPlayerService.currentPlaying); } - revision++; + MediaPlayerService.revision++; lifecycleSupport.serializeDownloadQueue(); - updateJukeboxPlaylist(); - setNextPlaying(); + MediaPlayerService.updateJukeboxPlaylist(); + + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); } @Override public RepeatMode getRepeatMode() { - return Util.getRepeatMode(this); + return Util.getRepeatMode(context); } @Override public void setRepeatMode(RepeatMode repeatMode) { - Util.setRepeatMode(this, repeatMode); - setNextPlaying(); + Util.setRepeatMode(context, repeatMode); + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); } @Override @@ -570,14 +322,14 @@ public class DownloadServiceImpl extends Service implements DownloadService @Override public synchronized DownloadFile forSong(MusicDirectory.Entry song) { - for (DownloadFile downloadFile : downloadList) + for (DownloadFile downloadFile : MediaPlayerService.downloadList) { if (downloadFile.getSong().equals(song) && ((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) || downloadFile.isWorkDone())) { return downloadFile; } } - for (DownloadFile downloadFile : backgroundDownloadList) + for (DownloadFile downloadFile : MediaPlayerService.backgroundDownloadList) { if (downloadFile.getSong().equals(song)) { @@ -588,7 +340,7 @@ public class DownloadServiceImpl extends Service implements DownloadService DownloadFile downloadFile = downloadFileCache.get(song); if (downloadFile == null) { - downloadFile = new DownloadFile(this, song, false); + downloadFile = new DownloadFile(context, song, false); downloadFileCache.put(song, downloadFile); } return downloadFile; @@ -597,25 +349,30 @@ public class DownloadServiceImpl extends Service implements DownloadService @Override public synchronized void clear() { - clear(true); + MediaPlayerService.clear(true); + } + + public synchronized void clear(boolean serialize) + { + MediaPlayerService.clear(serialize); } @Override public synchronized void clearBackground() { - if (currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) + if (MediaPlayerService.currentDownloading != null && MediaPlayerService.backgroundDownloadList.contains(MediaPlayerService.currentDownloading)) { - currentDownloading.cancelDownload(); - currentDownloading = null; + MediaPlayerService.currentDownloading.cancelDownload(); + MediaPlayerService.currentDownloading = null; } - backgroundDownloadList.clear(); + MediaPlayerService.backgroundDownloadList.clear(); } @Override public synchronized void clearIncomplete() { reset(); - Iterator iterator = downloadList.iterator(); + Iterator iterator = MediaPlayerService.downloadList.iterator(); while (iterator.hasNext()) { @@ -627,62 +384,43 @@ public class DownloadServiceImpl extends Service implements DownloadService } lifecycleSupport.serializeDownloadQueue(); - updateJukeboxPlaylist(); + MediaPlayerService.updateJukeboxPlaylist(); } @Override public synchronized int size() { - return downloadList.size(); - } - - public synchronized void clear(boolean serialize) - { - reset(); - downloadList.clear(); - revision++; - if (currentDownloading != null) - { - currentDownloading.cancelDownload(); - currentDownloading = null; - } - setCurrentPlaying(null); - - if (serialize) - { - lifecycleSupport.serializeDownloadQueue(); - } - updateJukeboxPlaylist(); - setNextPlaying(); + return MediaPlayerService.downloadList.size(); } @Override public synchronized void remove(int which) { - downloadList.remove(which); + MediaPlayerService.downloadList.remove(which); } @Override public synchronized void remove(DownloadFile downloadFile) { - if (downloadFile == currentDownloading) + if (downloadFile == MediaPlayerService.currentDownloading) { - currentDownloading.cancelDownload(); - currentDownloading = null; + MediaPlayerService.currentDownloading.cancelDownload(); + MediaPlayerService.currentDownloading = null; } - if (downloadFile == currentPlaying) + if (downloadFile == MediaPlayerService.currentPlaying) { reset(); setCurrentPlaying(null); } - downloadList.remove(downloadFile); - backgroundDownloadList.remove(downloadFile); - revision++; + MediaPlayerService.downloadList.remove(downloadFile); + MediaPlayerService.backgroundDownloadList.remove(downloadFile); + MediaPlayerService.revision++; lifecycleSupport.serializeDownloadQueue(); - updateJukeboxPlaylist(); - if (downloadFile == nextPlaying) + MediaPlayerService.updateJukeboxPlaylist(); + if (downloadFile == MediaPlayerService.nextPlaying) { - setNextPlaying(); + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); } } @@ -704,133 +442,28 @@ public class DownloadServiceImpl extends Service implements DownloadService } } - synchronized void setCurrentPlaying(int currentPlayingIndex) - { - try - { - setCurrentPlaying(downloadList.get(currentPlayingIndex)); - } - catch (IndexOutOfBoundsException x) - { - // Ignored - } - } - - synchronized void setCurrentPlaying(DownloadFile currentPlaying) - { - this.currentPlaying = currentPlaying; - - if (currentPlaying != null) - { - Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); - Util.broadcastA2dpMetaDataChange(this, instance); - } - else - { - Util.broadcastNewTrackInfo(this, null); - Util.broadcastA2dpMetaDataChange(this, null); - } - - updateRemoteControl(); - - // Update widget - UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED, true); - UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED, false); - SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); - - if (currentPlaying != null) - { - if (tabInstance != null) { - updateNotification(); - tabInstance.showNowPlaying(); - } - } - else - { - if (tabInstance != null) - { - stopForeground(true); - clearRemoteControl(); - isInForeground = false; - tabInstance.hideNowPlaying(); - } - } - } - - synchronized void setNextPlaying() - { - boolean gaplessPlayback = Util.getGaplessPlaybackPreference(DownloadServiceImpl.this); - - if (!gaplessPlayback) - { - nextPlaying = null; - nextPlayerState = IDLE; - return; - } - - int index = getCurrentPlayingIndex(); - - if (index != -1) - { - switch (getRepeatMode()) - { - case OFF: - index += 1; - break; - case ALL: - index = (index + 1) % size(); - break; - case SINGLE: - break; - default: - break; - } - } - - nextSetup = false; - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - nextPlayingTask = null; - } - - if (index < size() && index != -1) - { - nextPlaying = downloadList.get(index); - nextPlayingTask = new CheckCompletionTask(nextPlaying); - nextPlayingTask.start(); - } - else - { - nextPlaying = null; - setNextPlayerState(IDLE); - } - } - @Override public synchronized int getCurrentPlayingIndex() { - return downloadList.indexOf(currentPlaying); + return MediaPlayerService.downloadList.indexOf(MediaPlayerService.currentPlaying); } @Override public DownloadFile getCurrentPlaying() { - return currentPlaying; + return MediaPlayerService.currentPlaying; } @Override public DownloadFile getCurrentDownloading() { - return currentDownloading; + return MediaPlayerService.currentDownloading; } @Override public List getSongs() { - return downloadList; + return MediaPlayerService.downloadList; } @Override @@ -838,7 +471,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { long totalDuration = 0; - for (DownloadFile downloadFile : downloadList) + for (DownloadFile downloadFile : MediaPlayerService.downloadList) { Entry entry = downloadFile.getSong(); @@ -863,98 +496,26 @@ public class DownloadServiceImpl extends Service implements DownloadService public synchronized List getDownloads() { List temp = new ArrayList(); - temp.addAll(downloadList); - temp.addAll(backgroundDownloadList); + temp.addAll(MediaPlayerService.downloadList); + temp.addAll(MediaPlayerService.backgroundDownloadList); return temp; } @Override public List getBackgroundDownloads() { - return backgroundDownloadList; - } - - /** - * Plays either the current song (resume) or the first/next one in queue. - */ - public synchronized void play() - { - int current = getCurrentPlayingIndex(); - if (current == -1) - { - play(0); - } - else - { - play(current); - } + return MediaPlayerService.backgroundDownloadList; } @Override public synchronized void play(int index) { - play(index, true); + MediaPlayerService.getInstance(context).play(index, true); } - private synchronized void play(int index, boolean start) + public synchronized void play() { - updateRemoteControl(); - - if (index < 0 || index >= size()) - { - resetPlayback(); - } - else - { - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - nextPlayingTask = null; - } - - setCurrentPlaying(index); - - if (start) - { - if (jukeboxEnabled) - { - jukeboxService.skip(getCurrentPlayingIndex(), 0); - setPlayerState(STARTED); - } - else - { - bufferAndPlay(); - } - } - - checkDownloads(); - setNextPlaying(); - } - } - - private synchronized void resetPlayback() - { - reset(); - setCurrentPlaying(null); - lifecycleSupport.serializeDownloadQueue(); - } - - private synchronized void playNext() - { - MediaPlayer tmp = mediaPlayer; - mediaPlayer = nextMediaPlayer; - nextMediaPlayer = tmp; - setCurrentPlaying(nextPlaying); - setPlayerState(PlayerState.STARTED); - setupHandlers(currentPlaying, false); - setNextPlaying(); - - // Proxy should not be being used here since the next player was already setup to play - if (proxy != null) - { - proxy.stop(); - proxy = null; - } + MediaPlayerService.getInstance(context).play(); } /** @@ -963,42 +524,14 @@ public class DownloadServiceImpl extends Service implements DownloadService @Override public synchronized void togglePlayPause() { - if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) - { - start(); - } - else if (playerState == IDLE) - { - autoPlayStart = true; - play(); - } - else if (playerState == STARTED) - { - pause(); - } + if (playerState == PlayerState.IDLE) autoPlayStart = true; + MediaPlayerService.getInstance(context).togglePlayPause(); } @Override public synchronized void seekTo(int position) { - try - { - if (jukeboxEnabled) - { - jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); - } - else - { - mediaPlayer.seekTo(position); - cachedPosition = position; - - updateRemoteControl(); - } - } - catch (Exception x) - { - handleError(x); - } + MediaPlayerService.getInstance(context).seekTo(position); } @Override @@ -1031,193 +564,53 @@ public class DownloadServiceImpl extends Service implements DownloadService } } - private void onSongCompleted() - { - int index = getCurrentPlayingIndex(); - - if (currentPlaying != null) - { - final Entry song = currentPlaying.getSong(); - - if (song != null && song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(this)) - { - MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this); - try - { - musicService.deleteBookmark(song.getId(), DownloadServiceImpl.this, null); - } - catch (Exception ignored) - { - - } - } - } - - if (index != -1) - { - switch (getRepeatMode()) - { - case OFF: - if (index + 1 < 0 || index + 1 >= size()) - { - if (Util.getShouldClearPlaylist(this)) - { - clear(); - } - - resetPlayback(); - break; - } - - play(index + 1); - break; - case ALL: - play((index + 1) % size()); - break; - case SINGLE: - play(index); - break; - default: - break; - } - } - } - @Override public synchronized void pause() { - try - { - if (playerState == STARTED) - { - if (jukeboxEnabled) - { - jukeboxService.stop(); - } - else - { - mediaPlayer.pause(); - } - setPlayerState(PAUSED); - } - } - catch (Exception x) - { - handleError(x); - } + MediaPlayerService.getInstance(context).pause(); } @Override public synchronized void stop() { - try - { - if (playerState == STARTED) - { - if (jukeboxEnabled) - { - jukeboxService.stop(); - } - else - { - mediaPlayer.pause(); - } - setPlayerState(STOPPED); - } - else - { - setPlayerState(STOPPED); - } - } - catch (Exception x) - { - handleError(x); - } + MediaPlayerService.getInstance(context).stop(); } @Override public synchronized void start() { - try - { - if (jukeboxEnabled) - { - jukeboxService.start(); - } - else - { - mediaPlayer.start(); - } - setPlayerState(STARTED); - } - catch (Exception x) - { - handleError(x); - } + MediaPlayerService.getInstance(context).start(); } @Override public synchronized void reset() { - if (bufferTask != null) - { - bufferTask.cancel(); - } - try - { - setPlayerState(IDLE); - mediaPlayer.setOnErrorListener(null); - mediaPlayer.setOnCompletionListener(null); - mediaPlayer.reset(); - } - catch (Exception x) - { - handleError(x); - } + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.reset(); } @Override public synchronized int getPlayerPosition() { - try - { - if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) - { - return 0; - } - - return jukeboxEnabled ? jukeboxService.getPositionSeconds() * 1000 : cachedPosition; - } - catch (Exception x) - { - handleError(x); - return 0; - } + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService == null) return 0; + return mediaPlayerService.getPlayerPosition(); } @Override public synchronized int getPlayerDuration() { - if (currentPlaying != null) + if (MediaPlayerService.currentPlaying != null) { - Integer duration = currentPlaying.getSong().getDuration(); + Integer duration = MediaPlayerService.currentPlaying.getSong().getDuration(); if (duration != null) { return duration * 1000; } } - if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) - { - try - { - return mediaPlayer.getDuration(); - } - catch (Exception x) - { - handleError(x); - } - } - return 0; + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService == null) return 0; + return mediaPlayerService.getPlayerDuration(); } @Override @@ -1226,106 +619,6 @@ public class DownloadServiceImpl extends Service implements DownloadService return playerState; } - synchronized void setPlayerState(PlayerState playerState) - { - Log.i(TAG, String.format("%s -> %s (%s)", this.playerState.name(), playerState.name(), currentPlaying)); - - this.playerState = playerState; - - if (this.playerState == PAUSED) - { - lifecycleSupport.serializeDownloadQueue(); - } - - if (this.playerState == PlayerState.STARTED) - { - Util.requestAudioFocus(this); - } - - boolean showWhenPaused = (this.playerState != PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(this)); - boolean show = this.playerState == PlayerState.STARTED || showWhenPaused; - - Util.broadcastPlaybackStatusChange(this, this.playerState); - Util.broadcastA2dpPlayStatusChange(this, this.playerState, instance); - - if (this.playerState == PlayerState.STARTED || this.playerState == PlayerState.PAUSED) - { - // Set remote control - updateRemoteControl(); - } - - // Update widget - UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, this, this.playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, this, this.playerState == PlayerState.STARTED, true); - UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, this, this.playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, this, this.playerState == PlayerState.STARTED, false); - SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); - - if (show) - { - if (tabInstance != null) - { - // Only update notification is player state is one that will change the icon - if (this.playerState == PlayerState.STARTED || this.playerState == PlayerState.PAUSED) - { - updateNotification(); - tabInstance.showNowPlaying(); - } - } - } - else - { - if (tabInstance != null) - { - stopForeground(true); - clearRemoteControl(); - isInForeground = false; - tabInstance.hideNowPlaying(); - } - } - - if (this.playerState == STARTED) - { - scrobbler.scrobble(this, currentPlaying, false); - } - else if (this.playerState == COMPLETED) - { - scrobbler.scrobble(this, currentPlaying, true); - } - - if (playerState == STARTED && positionCache == null) - { - positionCache = new PositionCache(); - Thread thread = new Thread(positionCache); - thread.start(); - } - else if (playerState != STARTED && positionCache != null) - { - positionCache.stop(); - positionCache = null; - } - } - - private void setPlayerStateCompleted() - { - Log.i(TAG, String.format("%s -> %s (%s)", this.playerState.name(), PlayerState.COMPLETED, currentPlaying)); - this.playerState = PlayerState.COMPLETED; - - if (positionCache != null) - { - positionCache.stop(); - positionCache = null; - } - - scrobbler.scrobble(this, currentPlaying, true); - } - - private synchronized void setNextPlayerState(PlayerState playerState) - { - Log.i(TAG, String.format("Next: %s -> %s (%s)", this.nextPlayerState.name(), playerState.name(), nextPlaying)); - this.nextPlayerState = playerState; - } - @Override public void setSuggestedPlaylistName(String name) { @@ -1339,47 +632,28 @@ public class DownloadServiceImpl extends Service implements DownloadService } @Override - public boolean getEqualizerAvailable() - { - return equalizerAvailable; - } + public boolean getEqualizerAvailable() { return MediaPlayerService.equalizerAvailable; } @Override public boolean getVisualizerAvailable() { - return visualizerAvailable; + return MediaPlayerService.visualizerAvailable; } @Override public EqualizerController getEqualizerController() { - if (equalizerAvailable && equalizerController == null) - { - equalizerController = new EqualizerController(this, mediaPlayer); - if (!equalizerController.isAvailable()) - { - equalizerController = null; - } - else - { - equalizerController.loadSettings(); - } - } - return equalizerController; + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService == null) return null; + return mediaPlayerService.getEqualizerController(); } @Override public VisualizerController getVisualizerController() { - if (visualizerAvailable && visualizerController == null) - { - visualizerController = new VisualizerController(mediaPlayer); - if (!visualizerController.isAvailable()) - { - visualizerController = null; - } - } - return visualizerController; + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService == null) return null; + return mediaPlayerService.getVisualizerController(); } @Override @@ -1391,12 +665,12 @@ public class DownloadServiceImpl extends Service implements DownloadService @Override public boolean isJukeboxAvailable() { - MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this); + MusicService musicService = MusicServiceFactory.getMusicService(context); try { - String username = Util.getUserName(DownloadServiceImpl.this, Util.getActiveServer(DownloadServiceImpl.this)); - UserInfo user = musicService.getUser(username, DownloadServiceImpl.this, null); + String username = Util.getUserName(context, Util.getActiveServer(context)); + UserInfo user = musicService.getUser(username, context, null); return user.getJukeboxRole(); } catch (Exception e) @@ -1410,12 +684,12 @@ public class DownloadServiceImpl extends Service implements DownloadService @Override public boolean isSharingAvailable() { - MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this); + MusicService musicService = MusicServiceFactory.getMusicService(context); try { - String username = Util.getUserName(DownloadServiceImpl.this, Util.getActiveServer(DownloadServiceImpl.this)); - UserInfo user = musicService.getUser(username, DownloadServiceImpl.this, null); + String username = Util.getUserName(context, Util.getActiveServer(context)); + UserInfo user = musicService.getUser(username, context, null); return user.getShareRole(); } catch (Exception e) @@ -1430,445 +704,43 @@ public class DownloadServiceImpl extends Service implements DownloadService public void setJukeboxEnabled(boolean jukeboxEnabled) { this.jukeboxEnabled = jukeboxEnabled; - jukeboxService.setEnabled(jukeboxEnabled); + MediaPlayerService.jukeboxService.setEnabled(jukeboxEnabled); if (jukeboxEnabled) { - jukeboxService.startJukeboxService(); + MediaPlayerService.jukeboxService.startJukeboxService(); reset(); // Cancel current download, if necessary. - if (currentDownloading != null) + if (MediaPlayerService.currentDownloading != null) { - currentDownloading.cancelDownload(); + MediaPlayerService.currentDownloading.cancelDownload(); } } else { - jukeboxService.stopJukeboxService(); + MediaPlayerService.jukeboxService.stopJukeboxService(); } } @Override public void adjustJukeboxVolume(boolean up) { - jukeboxService.adjustVolume(up); - } - - @SuppressLint("NewApi") - public void setUpRemoteControlClient() - { - if (!Util.isLockScreenEnabled(this)) return; - - ComponentName componentName = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()); - - if (remoteControlClient == null) - { - final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(componentName); - PendingIntent broadcast = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); - remoteControlClient = new RemoteControlClient(broadcast); - audioManager.registerRemoteControlClient(remoteControlClient); - - // Flags for the media transport control that this client supports. - int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | - RemoteControlClient.FLAG_KEY_MEDIA_NEXT | - RemoteControlClient.FLAG_KEY_MEDIA_PLAY | - RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_STOP; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) - { - flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE; - - remoteControlClient.setOnGetPlaybackPositionListener(new RemoteControlClient.OnGetPlaybackPositionListener() - { - @Override - public long onGetPlaybackPosition() - { - return mediaPlayer.getCurrentPosition(); - } - }); - - remoteControlClient.setPlaybackPositionUpdateListener(new RemoteControlClient.OnPlaybackPositionUpdateListener() - { - @Override - public void onPlaybackPositionUpdate(long newPositionMs) - { - seekTo((int) newPositionMs); - } - }); - } - - remoteControlClient.setTransportControlFlags(flags); - } - } - - private void clearRemoteControl() - { - if (remoteControlClient != null) - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); - audioManager.unregisterRemoteControlClient(remoteControlClient); - remoteControlClient = null; - } - } - - private void updateRemoteControl() - { - if (!Util.isLockScreenEnabled(this)) - { - clearRemoteControl(); - return; - } - - if (remoteControlClient != null) - { - audioManager.unregisterRemoteControlClient(remoteControlClient); - audioManager.registerRemoteControlClient(remoteControlClient); - } - else - { - setUpRemoteControlClient(); - } - - Log.i(TAG, String.format("In updateRemoteControl, playerState: %s [%d]", playerState, getPlayerPosition())); - - switch (playerState) - { - case STARTED: - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); - } - else - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, getPlayerPosition(), 1.0f); - } - break; - default: - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); - } - else - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED, getPlayerPosition(), 1.0f); - } - break; - } - - if (currentPlaying != null) - { - MusicDirectory.Entry currentSong = currentPlaying.getSong(); - - Bitmap lockScreenBitmap = FileUtil.getAlbumArtBitmap(this, currentSong, Util.getMinDisplayMetric(this), true); - - String artist = currentSong.getArtist(); - String album = currentSong.getAlbum(); - String title = currentSong.getTitle(); - Integer currentSongDuration = currentSong.getDuration(); - Long duration = 0L; - - if (currentSongDuration != null) duration = (long) currentSongDuration * 1000; - - remoteControlClient.editMetadata(true).putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist).putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist).putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album).putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title).putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) - .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap).apply(); - } - } - - private synchronized void bufferAndPlay() - { - if (playerState != PREPARED) - { - reset(); - - bufferTask = new BufferTask(currentPlaying, 0); - bufferTask.start(); - } - else - { - doPlay(currentPlaying, 0, true); - } - } - - private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) - { - try - { - downloadFile.setPlaying(false); - //downloadFile.setPlaying(true); - final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); - boolean partial = file.equals(downloadFile.getPartialFile()); - downloadFile.updateModificationDate(); - - mediaPlayer.setOnCompletionListener(null); - secondaryProgress = -1; // Ensure seeking in non StreamProxy playback works - mediaPlayer.reset(); - setPlayerState(IDLE); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - String dataSource = file.getPath(); - - if (partial) - { - if (proxy == null) - { - proxy = new StreamProxy(this); - proxy.start(); - } - - dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8)); - Log.i(TAG, String.format("Data Source: %s", dataSource)); - } - else if (proxy != null) - { - proxy.stop(); - proxy = null; - } - - Log.i(TAG, "Preparing media player"); - mediaPlayer.setDataSource(dataSource); - setPlayerState(PREPARING); - - mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() - { - @Override - public void onBufferingUpdate(MediaPlayer mp, int percent) - { - SeekBar progressBar = DownloadActivity.getProgressBar(); - MusicDirectory.Entry song = downloadFile.getSong(); - - if (percent == 100) - { - if (progressBar != null) - { - progressBar.setSecondaryProgress(100 * progressBar.getMax()); - } - - mp.setOnBufferingUpdateListener(null); - } - else if (progressBar != null && song.getTranscodedContentType() == null && Util.getMaxBitRate(DownloadServiceImpl.this) == 0) - { - secondaryProgress = (int) (((double) percent / (double) 100) * progressBar.getMax()); - progressBar.setSecondaryProgress(secondaryProgress); - } - } - }); - - mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() - { - @Override - public void onPrepared(MediaPlayer mp) - { - Log.i(TAG, "Media player prepared"); - - setPlayerState(PREPARED); - - SeekBar progressBar = DownloadActivity.getProgressBar(); - - if (progressBar != null && downloadFile.isWorkDone()) - { - // Populate seek bar secondary progress if we have a complete file for consistency - DownloadActivity.getProgressBar().setSecondaryProgress(100 * progressBar.getMax()); - } - - synchronized (DownloadServiceImpl.this) - { - if (position != 0) - { - Log.i(TAG, String.format("Restarting player from position %d", position)); - seekTo(position); - } - cachedPosition = position; - - if (start) - { - mediaPlayer.start(); - setPlayerState(STARTED); - } - else - { - setPlayerState(PAUSED); - } - } - - lifecycleSupport.serializeDownloadQueue(); - } - }); - - setupHandlers(downloadFile, partial); - - mediaPlayer.prepareAsync(); - } - catch (Exception x) - { - handleError(x); - } - } - - private synchronized void setupNext(final DownloadFile downloadFile) - { - try - { - final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); - - if (nextMediaPlayer != null) - { - nextMediaPlayer.setOnCompletionListener(null); - nextMediaPlayer.release(); - nextMediaPlayer = null; - } - - nextMediaPlayer = new MediaPlayer(); - nextMediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK); - - try - { - nextMediaPlayer.setAudioSessionId(mediaPlayer.getAudioSessionId()); - } - catch (Throwable e) - { - nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - } - - nextMediaPlayer.setDataSource(file.getPath()); - setNextPlayerState(PREPARING); - - nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() - { - @Override - @SuppressLint("NewApi") - public void onPrepared(MediaPlayer mp) - { - try - { - setNextPlayerState(PREPARED); - - if (Util.getGaplessPlaybackPreference(DownloadServiceImpl.this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) - { - mediaPlayer.setNextMediaPlayer(nextMediaPlayer); - nextSetup = true; - } - } - catch (Exception x) - { - handleErrorNext(x); - } - } - }); - - nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() - { - @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int extra) - { - Log.w(TAG, String.format("Error on playing next (%d, %d): %s", what, extra, downloadFile)); - return true; - } - }); - - nextMediaPlayer.prepareAsync(); - } - catch (Exception x) - { - handleErrorNext(x); - } - } - - private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) - { - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() - { - @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int extra) - { - Log.w(TAG, String.format("Error on playing file (%d, %d): %s", what, extra, downloadFile)); - int pos = cachedPosition; - reset(); - downloadFile.setPlaying(false); - doPlay(downloadFile, pos, true); - downloadFile.setPlaying(true); - return true; - } - }); - - final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; - - mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() - { - @Override - public void onCompletion(MediaPlayer mediaPlayer) - { - // Acquire a temporary wakelock, since when we return from - // this callback the MediaPlayer will release its wakelock - // and allow the device to go to sleep. - wakeLock.acquire(60000); - - int pos = cachedPosition; - Log.i(TAG, String.format("Ending position %d of %d", pos, duration)); - - if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 1000))) - { - setPlayerStateCompleted(); - - if (Util.getGaplessPlaybackPreference(DownloadServiceImpl.this) && nextPlaying != null && nextPlayerState == PlayerState.PREPARED) - { - if (!nextSetup) - { - playNext(); - } - else - { - nextSetup = false; - playNext(); - } - } - else - { - onSongCompleted(); - } - - return; - } - - synchronized (DownloadServiceImpl.this) - { - if (downloadFile.isWorkDone()) - { - // Complete was called early even though file is fully buffered - Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration)); - reset(); - downloadFile.setPlaying(false); - doPlay(downloadFile, pos, true); - downloadFile.setPlaying(true); - } - else - { - Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration)); - reset(); - bufferTask = new BufferTask(downloadFile, pos); - bufferTask.start(); - } - } - } - }); + MediaPlayerService.jukeboxService.adjustVolume(up); } @Override public void setVolume(float volume) { - if (mediaPlayer != null) - { - mediaPlayer.setVolume(volume, volume); - } + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setVolume(volume); } @Override public synchronized void swap(boolean mainList, int from, int to) { - List list = mainList ? downloadList : backgroundDownloadList; + List list = mainList ? MediaPlayerService.downloadList : MediaPlayerService.backgroundDownloadList; int max = list.size(); if (to >= max) @@ -1886,332 +758,38 @@ public class DownloadServiceImpl extends Service implements DownloadService if (jukeboxEnabled && mainList) { - updateJukeboxPlaylist(); + MediaPlayerService.updateJukeboxPlaylist(); } - else if (mainList && (movedSong == nextPlaying || (currentPlayingIndex + 1) == to)) + else if (mainList && (movedSong == MediaPlayerService.nextPlaying || (currentPlayingIndex + 1) == to)) { // Moving next playing or moving a song to be next playing - setNextPlaying(); - } - } - - private void handleError(Exception x) - { - Log.w(TAG, String.format("Media player error: %s", x), x); - - try - { - mediaPlayer.reset(); - } - catch (Exception ex) - { - Log.w(TAG, String.format("Exception encountered when resetting media player: %s", ex), ex); - } - - setPlayerState(IDLE); - } - - private void handleErrorNext(Exception x) - { - Log.w(TAG, String.format("Next Media player error: %s", x), x); - nextMediaPlayer.reset(); - setNextPlayerState(IDLE); - } - - protected synchronized void checkDownloads() - { - if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) - { - return; - } - - if (shufflePlay) - { - checkShufflePlay(); - } - - if (jukeboxEnabled || !Util.isNetworkConnected(this)) - { - return; - } - - if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) - { - return; - } - - // Need to download current playing? - if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) - { - // Cancel current download, if necessary. - if (currentDownloading != null) - { - currentDownloading.cancelDownload(); - } - - currentDownloading = currentPlaying; - currentDownloading.download(); - cleanupCandidates.add(currentDownloading); - } - - // Find a suitable target for download. - else - { - if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && (!downloadList.isEmpty() || !backgroundDownloadList.isEmpty())) - { - currentDownloading = null; - int n = size(); - - int preloaded = 0; - - if (n != 0) - { - int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); - if (start == -1) - { - start = 0; - } - int i = start; - do - { - DownloadFile downloadFile = downloadList.get(i); - if (!downloadFile.isWorkDone()) - { - if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) - { - currentDownloading = downloadFile; - currentDownloading.download(); - cleanupCandidates.add(currentDownloading); - if (i == (start + 1)) - { - setNextPlayerState(DOWNLOADING); - } - break; - } - } - else if (currentPlaying != downloadFile) - { - preloaded++; - } - - i = (i + 1) % n; - } while (i != start); - } - - if ((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty()) - { - for (int i = 0; i < backgroundDownloadList.size(); i++) - { - DownloadFile downloadFile = backgroundDownloadList.get(i); - if (downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved())) - { - if (Util.getShouldScanMedia(this)) - { - Util.scanMedia(this, downloadFile.getCompleteFile()); - } - - // Don't need to keep list like active song list - backgroundDownloadList.remove(i); - revision++; - i--; - } - else - { - currentDownloading = downloadFile; - currentDownloading.download(); - cleanupCandidates.add(currentDownloading); - break; - } - } - } - } - } - - // Delete obsolete .partial and .complete files. - cleanup(); - } - - private synchronized void checkShufflePlay() - { - // Get users desired random playlist size - int listSize = Util.getMaxSongs(this); - boolean wasEmpty = downloadList.isEmpty(); - - long revisionBefore = revision; - - // First, ensure that list is at least 20 songs long. - int size = size(); - if (size < listSize) - { - for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) - { - DownloadFile downloadFile = new DownloadFile(this, song, false); - downloadList.add(downloadFile); - revision++; - } - } - - int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex(); - - // Only shift playlist if playing song #5 or later. - if (currIndex > 4) - { - int songsToShift = currIndex - 2; - for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) - { - downloadList.add(new DownloadFile(this, song, false)); - downloadList.get(0).cancelDownload(); - downloadList.remove(0); - revision++; - } - } - - if (revisionBefore != revision) - { - updateJukeboxPlaylist(); - } - - if (wasEmpty && !downloadList.isEmpty()) - { - play(0); + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); } } @Override public long getDownloadListUpdateRevision() { - return revision; - } - - private synchronized void cleanup() - { - Iterator iterator = cleanupCandidates.iterator(); - while (iterator.hasNext()) - { - DownloadFile downloadFile = iterator.next(); - if (downloadFile != currentPlaying && downloadFile != currentDownloading) - { - if (downloadFile.cleanup()) - { - iterator.remove(); - } - } - } + return MediaPlayerService.revision; } @Override public void updateNotification() { - if (Util.isNotificationEnabled(this)) { - if (isInForeground == true) { - 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()); - } - Log.w(TAG, "--- Updated notification"); - } - else { - startForeground(NOTIFICATION_ID, buildForegroundNotification()); - isInForeground = true; - Log.w(TAG, "--- Created Foreground notification"); - } - } + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.updateNotification(); } - @SuppressWarnings("IconColors") - private Notification buildForegroundNotification() { - notificationBuilder.setSmallIcon(R.drawable.ic_stat_ultrasonic); - - 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); - - notificationBuilder.setContent(contentView); - - Intent notificationIntent = new Intent(this, DownloadActivity.class); - 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); - bigView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); - } else if (playerState == PlayerState.STARTED) { - contentView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); - bigView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); - } - - 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); - } - - - 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 = notificationBuilder.build(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - notification.bigContentView = bigView; - } - - return notification; - } - public void setSongRating(final int rating) { if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING)) return; - if (currentPlaying == null) + if (MediaPlayerService.currentPlaying == null) return; - final Entry song = currentPlaying.getSong(); + final Entry song = MediaPlayerService.currentPlaying.getSong(); song.setUserRating(rating); new Thread(new Runnable() @@ -2219,11 +797,11 @@ public class DownloadServiceImpl extends Service implements DownloadService @Override public void run() { - final MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this); + final MusicService musicService = MusicServiceFactory.getMusicService(context); try { - musicService.setRating(song.getId(), rating, DownloadServiceImpl.this, null); + musicService.setRating(song.getId(), rating, context, null); } catch (Exception e) { @@ -2235,164 +813,8 @@ public class DownloadServiceImpl extends Service implements DownloadService updateNotification(); } - private class BufferTask extends CancellableTask + private void handleError(Exception x) { - private final DownloadFile downloadFile; - private final int position; - private final long expectedFileSize; - private final File partialFile; - - public BufferTask(DownloadFile downloadFile, int position) - { - this.downloadFile = downloadFile; - this.position = position; - partialFile = downloadFile.getPartialFile(); - - long bufferLength = Util.getBufferLength(DownloadServiceImpl.this); - - if (bufferLength == 0) - { - // Set to seconds in a day, basically infinity - bufferLength = 86400L; - } - - // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. - int bitRate = downloadFile.getBitRate(); - long byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength); - - // Find out how large the file should grow before resuming playback. - Log.i(TAG, String.format("Buffering from position %d and bitrate %d", position, bitRate)); - expectedFileSize = (position * bitRate / 8) + byteCount; - } - - @Override - public void execute() - { - setPlayerState(DOWNLOADING); - - while (!bufferComplete() && !Util.isOffline(DownloadServiceImpl.this)) - { - Util.sleepQuietly(1000L); - if (isCancelled()) - { - return; - } - } - doPlay(downloadFile, position, true); - } - - private boolean bufferComplete() - { - boolean completeFileAvailable = downloadFile.isWorkDone(); - long size = partialFile.length(); - - Log.i(TAG, String.format("Buffering %s (%d/%d, %s)", partialFile, size, expectedFileSize, completeFileAvailable)); - return completeFileAvailable || size >= expectedFileSize; - } - - @Override - public String toString() - { - return String.format("BufferTask (%s)", downloadFile); - } - } - - private class PositionCache implements Runnable - { - boolean isRunning = true; - - public void stop() - { - isRunning = false; - } - - @Override - public void run() - { - Thread.currentThread().setName("PositionCache"); - - // Stop checking position before the song reaches completion - while (isRunning) - { - try - { - if (mediaPlayer != null && playerState == STARTED) - { - cachedPosition = mediaPlayer.getCurrentPosition(); - } - - Util.sleepQuietly(25L); - } - catch (Exception e) - { - Log.w(TAG, "Crashed getting current position", e); - isRunning = false; - positionCache = null; - } - } - } - } - - private class CheckCompletionTask extends CancellableTask - { - private final DownloadFile downloadFile; - private final File partialFile; - - public CheckCompletionTask(DownloadFile downloadFile) - { - super(); - setNextPlayerState(PlayerState.IDLE); - - this.downloadFile = downloadFile; - - partialFile = downloadFile != null ? downloadFile.getPartialFile() : null; - } - - @Override - public void execute() - { - Thread.currentThread().setName("CheckCompletionTask"); - - if (downloadFile == null) - { - return; - } - - // Do an initial sleep so this prepare can't compete with main prepare - Util.sleepQuietly(5000L); - - while (!bufferComplete()) - { - Util.sleepQuietly(5000L); - - if (isCancelled()) - { - return; - } - } - - // Start the setup of the next media player - mediaPlayerHandler.post(new Runnable() - { - @Override - public void run() - { - setupNext(downloadFile); - } - }); - } - - private boolean bufferComplete() - { - boolean completeFileAvailable = downloadFile.isWorkDone(); - Log.i(TAG, String.format("Buffering next %s (%d)", partialFile, partialFile.length())); - return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED); - } - - @Override - public String toString() - { - return String.format("CheckCompletionTask (%s)", downloadFile); - } + Log.w(TAG, String.format("Media player error: %s", x), x); } } \ No newline at end of file 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 265bce86..4cbcef74 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java @@ -67,6 +67,7 @@ public class DownloadServiceLifecycleSupport private boolean externalStorageAvailable = true; private Lock lock = new ReentrantLock(); private final AtomicBoolean setup = new AtomicBoolean(false); + private Context context; /** * This receiver manages the intent that could come from other applications. @@ -107,9 +108,10 @@ public class DownloadServiceLifecycleSupport }; - public DownloadServiceLifecycleSupport(DownloadServiceImpl downloadService) + public DownloadServiceLifecycleSupport(Context context, DownloadServiceImpl downloadService) { this.downloadService = downloadService; + this.context = context; } public void onCreate() @@ -121,7 +123,7 @@ public class DownloadServiceLifecycleSupport { try { - downloadService.checkDownloads(); + MediaPlayerService.checkDownloads(context); } catch (Throwable x) { @@ -156,10 +158,10 @@ public class DownloadServiceLifecycleSupport IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); ejectFilter.addDataScheme("file"); - downloadService.registerReceiver(ejectEventReceiver, ejectFilter); + context.registerReceiver(ejectEventReceiver, ejectFilter); // React to media buttons. - Util.registerMediaButtonEventReceiver(downloadService); + Util.registerMediaButtonEventReceiver(context); // Pause temporarily on incoming phone calls. //phoneStateListener = new MyPhoneStateListener(); @@ -174,20 +176,20 @@ public class DownloadServiceLifecycleSupport commandFilter.addAction(DownloadServiceImpl.CMD_STOP); commandFilter.addAction(DownloadServiceImpl.CMD_PREVIOUS); commandFilter.addAction(DownloadServiceImpl.CMD_NEXT); - downloadService.registerReceiver(intentReceiver, commandFilter); + context.registerReceiver(intentReceiver, commandFilter); - int instance = Util.getActiveServer(downloadService); - downloadService.setJukeboxEnabled(Util.getJukeboxEnabled(downloadService, instance)); + int instance = Util.getActiveServer(context); + downloadService.setJukeboxEnabled(Util.getJukeboxEnabled(context, instance)); deserializeDownloadQueue(); - new CacheCleaner(downloadService, downloadService).clean(); + new CacheCleaner(context, downloadService).clean(); } private void registerHeadsetReceiver() { // Pause when headset is unplugged. - final SharedPreferences sp = Util.getPreferences(downloadService); - final String spKey = downloadService + final SharedPreferences sp = Util.getPreferences(context); + final String spKey = context .getString(R.string.settings_playback_resume_play_on_headphones_plug); headsetEventReceiver = new BroadcastReceiver() { @@ -218,7 +220,7 @@ public class DownloadServiceLifecycleSupport IntentFilter headsetIntentFilter = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) ? new IntentFilter(AudioManager.ACTION_HEADSET_PLUG) : new IntentFilter(Intent.ACTION_HEADSET_PLUG); - downloadService.registerReceiver(headsetEventReceiver, headsetIntentFilter); + context.registerReceiver(headsetEventReceiver, headsetIntentFilter); } public void onStart(Intent intent) @@ -238,12 +240,9 @@ public class DownloadServiceLifecycleSupport executorService.shutdown(); serializeDownloadQueueNow(); downloadService.clear(false); - downloadService.unregisterReceiver(ejectEventReceiver); - downloadService.unregisterReceiver(headsetEventReceiver); - downloadService.unregisterReceiver(intentReceiver); - - //TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); - //telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); + context.unregisterReceiver(ejectEventReceiver); + context.unregisterReceiver(headsetEventReceiver); + context.unregisterReceiver(intentReceiver); } public boolean isExternalStorageAvailable() @@ -273,7 +272,7 @@ public class DownloadServiceLifecycleSupport state.currentPlayingPosition = downloadService.getPlayerPosition(); Log.i(TAG, String.format("Serialized currentPlayingIndex: %d, currentPlayingPosition: %d", state.currentPlayingIndex, state.currentPlayingPosition)); - FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER); + FileUtil.serialize(context, state, FILENAME_DOWNLOADS_SER); } private void deserializeDownloadQueue() @@ -283,7 +282,7 @@ public class DownloadServiceLifecycleSupport private void deserializeDownloadQueueNow() { - State state = FileUtil.deserialize(downloadService, FILENAME_DOWNLOADS_SER); + State state = FileUtil.deserialize(context, FILENAME_DOWNLOADS_SER); if (state == null) { return; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java index 4d85b0b6..cc321cb3 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java @@ -67,6 +67,7 @@ public class JukeboxService private VolumeToast volumeToast; private AtomicBoolean running = new AtomicBoolean(); private Thread serviceThread; + private Context context; // TODO: Report warning if queue fills up. // TODO: Create shutdown method? @@ -74,8 +75,9 @@ public class JukeboxService // TODO: Persist RC state? // TODO: Minimize status updates. - public JukeboxService(DownloadServiceImpl downloadService) + public JukeboxService(Context context, DownloadServiceImpl downloadService) { + this.context = context; this.downloadService = downloadService; } @@ -149,7 +151,7 @@ public class JukeboxService try { - if (!Util.isOffline(downloadService)) + if (!Util.isOffline(context)) { task = tasks.take(); JukeboxStatus status = task.execute(); @@ -212,7 +214,7 @@ public class JukeboxService @Override public void run() { - Util.toast(downloadService, resourceId, false); + Util.toast(context, resourceId, false); } }); @@ -282,14 +284,14 @@ public class JukeboxService if (volumeToast == null) { - volumeToast = new VolumeToast(downloadService); + volumeToast = new VolumeToast(context); } volumeToast.setVolume(gain); } private MusicService getMusicService() { - return MusicServiceFactory.getMusicService(downloadService); + return MusicServiceFactory.getMusicService(context); } public int getPositionSeconds() @@ -380,7 +382,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().getJukeboxStatus(downloadService, null); + return getMusicService().getJukeboxStatus(context, null); } } @@ -396,7 +398,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().updateJukeboxPlaylist(ids, downloadService, null); + return getMusicService().updateJukeboxPlaylist(ids, context, null); } } @@ -414,7 +416,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().skipJukebox(index, offsetSeconds, downloadService, null); + return getMusicService().skipJukebox(index, offsetSeconds, context, null); } } @@ -423,7 +425,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().stopJukebox(downloadService, null); + return getMusicService().stopJukebox(context, null); } } @@ -432,7 +434,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().startJukebox(downloadService, null); + return getMusicService().startJukebox(context, null); } } @@ -449,7 +451,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().setJukeboxGain(gain, downloadService, null); + return getMusicService().setJukeboxGain(gain, context, null); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java new file mode 100644 index 00000000..0d44ab69 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -0,0 +1,1792 @@ +package org.moire.ultrasonic.service; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.media.RemoteControlClient; +import android.media.audiofx.AudioEffect; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.PowerManager; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; +import android.widget.SeekBar; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import org.koin.java.standalone.KoinJavaComponent; +import org.moire.ultrasonic.R; +import org.moire.ultrasonic.activity.DownloadActivity; +import org.moire.ultrasonic.activity.SubsonicTabActivity; +import org.moire.ultrasonic.audiofx.EqualizerController; +import org.moire.ultrasonic.audiofx.VisualizerController; +import org.moire.ultrasonic.domain.MusicDirectory; +import org.moire.ultrasonic.domain.PlayerState; +import org.moire.ultrasonic.domain.RepeatMode; +import org.moire.ultrasonic.featureflags.Feature; +import org.moire.ultrasonic.featureflags.FeatureStorage; +import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x1; +import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x2; +import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x3; +import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x4; +import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; +import org.moire.ultrasonic.util.CancellableTask; +import org.moire.ultrasonic.util.Constants; +import org.moire.ultrasonic.util.FileUtil; +import org.moire.ultrasonic.util.ShufflePlayBuffer; +import org.moire.ultrasonic.util.SimpleServiceBinder; +import org.moire.ultrasonic.util.StreamProxy; +import org.moire.ultrasonic.util.Util; + +import java.io.File; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.moire.ultrasonic.domain.PlayerState.COMPLETED; +import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; +import static org.moire.ultrasonic.domain.PlayerState.IDLE; +import static org.moire.ultrasonic.domain.PlayerState.PAUSED; +import static org.moire.ultrasonic.domain.PlayerState.PREPARED; +import static org.moire.ultrasonic.domain.PlayerState.PREPARING; +import static org.moire.ultrasonic.domain.PlayerState.STARTED; +import static org.moire.ultrasonic.domain.PlayerState.STOPPED; + +public class MediaPlayerService extends Service +{ + private static final String TAG = MediaPlayerService.class.getSimpleName(); + private static final String NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"; + private static final String NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"; + private static final int NOTIFICATION_ID = 3033; + + private static MediaPlayerService instance = null; + + public static boolean equalizerAvailable; + public static boolean visualizerAvailable; + public static final List downloadList = new ArrayList(); + public static final List backgroundDownloadList = new ArrayList(); + + private final IBinder binder = new SimpleServiceBinder<>(this); + private PowerManager.WakeLock wakeLock; + + private Looper mediaPlayerLooper; + private MediaPlayer mediaPlayer; + private MediaPlayer nextMediaPlayer; + private Handler mediaPlayerHandler; + private AudioManager audioManager; + + public RemoteControlClient remoteControlClient; + private EqualizerController equalizerController; + private VisualizerController visualizerController; + public static ShufflePlayBuffer shufflePlayBuffer; + private final Scrobbler scrobbler = new Scrobbler(); + private CancellableTask bufferTask; + public static DownloadFile currentPlaying; + public static DownloadFile currentDownloading; + public static DownloadFile nextPlaying; + + public static boolean jukeboxEnabled; + public static JukeboxService jukeboxService; + public static DownloadServiceLifecycleSupport lifecycleSupport; + + public static int cachedPosition; + private PositionCache positionCache; + private StreamProxy proxy; + + private static boolean nextSetup; + private static CancellableTask nextPlayingTask; + public static PlayerState playerState = IDLE; + public static PlayerState nextPlayerState = IDLE; + private boolean isInForeground = false; + private static final List cleanupCandidates = new ArrayList(); + public static boolean shufflePlay; + public static long revision; + private int secondaryProgress = -1; + + private NotificationCompat.Builder notificationBuilder; + + static + { + try + { + EqualizerController.checkAvailable(); + equalizerAvailable = true; + } + catch (Throwable t) + { + equalizerAvailable = false; + } + } + + static + { + try + { + VisualizerController.checkAvailable(); + visualizerAvailable = true; + } + catch (Throwable t) + { + visualizerAvailable = false; + } + } + + public static synchronized int size() { return downloadList.size(); } + public RepeatMode getRepeatMode() { return Util.getRepeatMode(this); } + + public static MediaPlayerService getInstance(Context context) + { + for (int i = 0; i < 5; i++) + { + if (instance != null) return instance; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + context.startForegroundService(new Intent(context, MediaPlayerService.class)); + } + else + { + context.startService(new Intent(context, MediaPlayerService.class)); + } + + Util.sleepQuietly(50L); + } + + return instance; + } + + public static MediaPlayerService getRunningInstance() + { + return instance; + } + + @Nullable + @Override + public IBinder onBind(Intent intent) + { + return binder; + } + + @Override + public void onCreate() + { + super.onCreate(); + + new Thread(new Runnable() + { + @Override + public void run() + { + Thread.currentThread().setName("MediaPlayerService"); + + Looper.prepare(); + + if (mediaPlayer != null) + { + mediaPlayer.release(); + } + + mediaPlayer = new MediaPlayer(); + mediaPlayer.setWakeMode(MediaPlayerService.this, PowerManager.PARTIAL_WAKE_LOCK); + + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() + { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int more) + { + handleError(new Exception(String.format("MediaPlayer error: %d (%d)", what, more))); + return false; + } + }); + + try + { + Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(i); + } + catch (Throwable e) + { + // Froyo or lower + } + + mediaPlayerLooper = Looper.myLooper(); + mediaPlayerHandler = new Handler(mediaPlayerLooper); + Looper.loop(); + } + }).start(); + + audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); + setUpRemoteControlClient(); + + if (equalizerAvailable) + { + equalizerController = new EqualizerController(this, mediaPlayer); + if (!equalizerController.isAvailable()) + { + equalizerController = null; + } + else + { + equalizerController.loadSettings(); + } + } + + if (visualizerAvailable) + { + visualizerController = new VisualizerController(mediaPlayer); + if (!visualizerController.isAvailable()) + { + visualizerController = null; + } + } + + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); + wakeLock.setReferenceCounted(false); + + // 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); + + instance = this; + + Log.i(TAG, "MediaPlayerService created"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) + { + super.onStartCommand(intent, flags, startId); + lifecycleSupport.onStart(intent); + return START_NOT_STICKY; + } + + @Override + public void onDestroy() + { + super.onDestroy(); + + instance = null; + + reset(); + try + { + mediaPlayer.release(); + + if (nextMediaPlayer != null) + { + nextMediaPlayer.release(); + } + + mediaPlayerLooper.quit(); + shufflePlayBuffer.shutdown(); + + if (equalizerController != null) + { + equalizerController.release(); + } + + if (visualizerController != null) + { + visualizerController.release(); + } + + if (bufferTask != null) + { + bufferTask.cancel(); + } + + if (nextPlayingTask != null) + { + nextPlayingTask.cancel(); + } + + Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(i); + + audioManager.unregisterRemoteControlClient(remoteControlClient); + clearRemoteControl(); + + wakeLock.release(); + } + catch (Throwable ignored) + { + } + + Log.i(TAG, "MediaPlayerService stopped"); + } + + public EqualizerController getEqualizerController() + { + if (equalizerAvailable && equalizerController == null) + { + equalizerController = new EqualizerController(this, mediaPlayer); + if (!equalizerController.isAvailable()) + { + equalizerController = null; + } + else + { + equalizerController.loadSettings(); + } + } + return equalizerController; + } + + public VisualizerController getVisualizerController() + { + if (visualizerAvailable && visualizerController == null) + { + visualizerController = new VisualizerController(mediaPlayer); + if (!visualizerController.isAvailable()) + { + visualizerController = null; + } + } + return visualizerController; + } + + public void setUpRemoteControlClient() + { + if (!Util.isLockScreenEnabled(this)) return; + + ComponentName componentName = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()); + + if (remoteControlClient == null) + { + final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(componentName); + PendingIntent broadcast = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); + remoteControlClient = new RemoteControlClient(broadcast); + audioManager.registerRemoteControlClient(remoteControlClient); + + // Flags for the media transport control that this client supports. + int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | + RemoteControlClient.FLAG_KEY_MEDIA_NEXT | + RemoteControlClient.FLAG_KEY_MEDIA_PLAY | + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_STOP; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) + { + flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE; + + remoteControlClient.setOnGetPlaybackPositionListener(new RemoteControlClient.OnGetPlaybackPositionListener() + { + @Override + public long onGetPlaybackPosition() + { + return mediaPlayer.getCurrentPosition(); + } + }); + + remoteControlClient.setPlaybackPositionUpdateListener(new RemoteControlClient.OnPlaybackPositionUpdateListener() + { + @Override + public void onPlaybackPositionUpdate(long newPositionMs) + { + seekTo((int) newPositionMs); + } + }); + } + + remoteControlClient.setTransportControlFlags(flags); + } + } + + private void clearRemoteControl() + { + if (remoteControlClient != null) + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + audioManager.unregisterRemoteControlClient(remoteControlClient); + remoteControlClient = null; + } + } + + private void updateRemoteControl() + { + if (!Util.isLockScreenEnabled(this)) + { + clearRemoteControl(); + return; + } + + if (remoteControlClient != null) + { + audioManager.unregisterRemoteControlClient(remoteControlClient); + audioManager.registerRemoteControlClient(remoteControlClient); + } + else + { + setUpRemoteControlClient(); + } + + Log.i(TAG, String.format("In updateRemoteControl, playerState: %s [%d]", playerState, getPlayerPosition())); + + switch (playerState) + { + case STARTED: + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + } + else + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, getPlayerPosition(), 1.0f); + } + break; + default: + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + } + else + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED, getPlayerPosition(), 1.0f); + } + break; + } + + if (currentPlaying != null) + { + MusicDirectory.Entry currentSong = currentPlaying.getSong(); + + Bitmap lockScreenBitmap = FileUtil.getAlbumArtBitmap(this, currentSong, Util.getMinDisplayMetric(this), true); + + String artist = currentSong.getArtist(); + String album = currentSong.getAlbum(); + String title = currentSong.getTitle(); + Integer currentSongDuration = currentSong.getDuration(); + Long duration = 0L; + + if (currentSongDuration != null) duration = (long) currentSongDuration * 1000; + + remoteControlClient.editMetadata(true).putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist).putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist).putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album).putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title).putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) + .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap).apply(); + } + } + + public synchronized void seekTo(int position) + { + try + { + if (jukeboxEnabled) + { + jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); + } + else + { + mediaPlayer.seekTo(position); + cachedPosition = position; + + updateRemoteControl(); + } + } + catch (Exception x) + { + handleError(x); + } + } + + public synchronized int getPlayerPosition() + { + try + { + if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) + { + return 0; + } + + return jukeboxEnabled ? jukeboxService.getPositionSeconds() * 1000 : cachedPosition; + } + catch (Exception x) + { + handleError(x); + return 0; + } + } + + public synchronized int getPlayerDuration() + { + if (MediaPlayerService.currentPlaying != null) + { + Integer duration = MediaPlayerService.currentPlaying.getSong().getDuration(); + if (duration != null) + { + return duration * 1000; + } + } + if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) + { + try + { + return mediaPlayer.getDuration(); + } + catch (Exception x) + { + handleError(x); + } + } + return 0; + } + + public static synchronized int getCurrentPlayingIndex() + { + return downloadList.indexOf(currentPlaying); + } + + public synchronized void setCurrentPlaying(int currentPlayingIndex) + { + try + { + setCurrentPlaying(downloadList.get(currentPlayingIndex)); + } + catch (IndexOutOfBoundsException x) + { + // Ignored + } + } + + public synchronized void setCurrentPlaying(DownloadFile currentPlaying) + { + MediaPlayerService.currentPlaying = currentPlaying; + + if (currentPlaying != null) + { + Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); + Util.broadcastA2dpPlayStatusChange(this, playerState, currentPlaying.getSong(), downloadList.size() + backgroundDownloadList.size(), downloadList.indexOf(currentPlaying) + 1, getPlayerPosition()); + } + else + { + Util.broadcastNewTrackInfo(this, null); + Util.broadcastA2dpMetaDataChange(this, null); + } + + // Update widget + UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); + UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, true); + UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); + UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); + + updateRemoteControl(); + SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); + + if (currentPlaying != null) + { + if (tabInstance != null) { + updateNotification(); + tabInstance.showNowPlaying(); + } + } + else + { + if (tabInstance != null) + { + tabInstance.hideNowPlaying(); + stopForeground(true); + isInForeground = false; + stopSelf(); + } + } + } + + public synchronized void setNextPlaying() + { + boolean gaplessPlayback = Util.getGaplessPlaybackPreference(this); + + if (!gaplessPlayback) + { + nextPlaying = null; + nextPlayerState = IDLE; + return; + } + + int index = getCurrentPlayingIndex(); + + if (index != -1) + { + switch (getRepeatMode()) + { + case OFF: + index += 1; + break; + case ALL: + index = (index + 1) % size(); + break; + case SINGLE: + break; + default: + break; + } + } + + nextSetup = false; + if (nextPlayingTask != null) + { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + + if (index < size() && index != -1) + { + nextPlaying = downloadList.get(index); + nextPlayingTask = new CheckCompletionTask(nextPlaying); + nextPlayingTask.start(); + } + else + { + nextPlaying = null; + setNextPlayerState(IDLE); + } + } + + public synchronized void togglePlayPause() + { + if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) + { + start(); + } + else if (playerState == IDLE) + { + play(); + } + else if (playerState == STARTED) + { + pause(); + } + } + + public void setVolume(float volume) + { + if (mediaPlayer != null) + { + mediaPlayer.setVolume(volume, volume); + } + } + + /** + * Plays either the current song (resume) or the first/next one in queue. + */ + public synchronized void play() + { + int current = getCurrentPlayingIndex(); + if (current == -1) + { + play(0); + } + else + { + play(current); + } + } + + public synchronized void play(int index) + { + play(index, true); + } + + public synchronized void play(int index, boolean start) + { + updateRemoteControl(); + + if (index < 0 || index >= size()) + { + resetPlayback(); + } + else + { + if (nextPlayingTask != null) + { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + + setCurrentPlaying(index); + + if (start) + { + if (jukeboxEnabled) + { + jukeboxService.skip(getCurrentPlayingIndex(), 0); + setPlayerState(STARTED); + } + else + { + bufferAndPlay(); + } + } + + checkDownloads(this); + setNextPlaying(); + } + } + + private synchronized void resetPlayback() + { + reset(); + setCurrentPlaying(null); + lifecycleSupport.serializeDownloadQueue(); + } + + public synchronized void reset() + { + if (bufferTask != null) + { + bufferTask.cancel(); + } + try + { + setPlayerState(IDLE); + mediaPlayer.setOnErrorListener(null); + mediaPlayer.setOnCompletionListener(null); + mediaPlayer.reset(); + } + catch (Exception x) + { + handleError(x); + } + } + + private synchronized void playNext() + { + MediaPlayer tmp = mediaPlayer; + mediaPlayer = nextMediaPlayer; + nextMediaPlayer = tmp; + setCurrentPlaying(nextPlaying); + setPlayerState(PlayerState.STARTED); + setupHandlers(currentPlaying, false); + setNextPlaying(); + + // Proxy should not be being used here since the next player was already setup to play + if (proxy != null) + { + proxy.stop(); + proxy = null; + } + } + + public synchronized void pause() + { + try + { + if (playerState == STARTED) + { + if (jukeboxEnabled) + { + jukeboxService.stop(); + } + else + { + mediaPlayer.pause(); + } + setPlayerState(PAUSED); + } + } + catch (Exception x) + { + handleError(x); + } + } + + public synchronized void stop() + { + try + { + if (playerState == STARTED) + { + if (jukeboxEnabled) + { + jukeboxService.stop(); + } + else + { + mediaPlayer.pause(); + } + } + setPlayerState(STOPPED); + } + catch (Exception x) + { + handleError(x); + } + } + + public synchronized void start() + { + try + { + if (jukeboxEnabled) + { + jukeboxService.start(); + } + else + { + mediaPlayer.start(); + } + setPlayerState(STARTED); + } + catch (Exception x) + { + handleError(x); + } + } + + private synchronized void bufferAndPlay() + { + if (playerState != PREPARED) + { + reset(); + + bufferTask = new BufferTask(currentPlaying, 0); + bufferTask.start(); + } + else + { + doPlay(currentPlaying, 0, true); + } + } + + public synchronized void setPlayerState(PlayerState playerState) + { + Log.i(TAG, String.format("%s -> %s (%s)", playerState.name(), playerState.name(), currentPlaying)); + + MediaPlayerService.playerState = playerState; + + if (playerState == PAUSED) + { + lifecycleSupport.serializeDownloadQueue(); + } + + if (playerState == PlayerState.STARTED) + { + Util.requestAudioFocus(this); + } + + boolean showWhenPaused = (playerState != PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(this)); + boolean show = playerState == PlayerState.STARTED || showWhenPaused; + + Util.broadcastPlaybackStatusChange(this, playerState); + Util.broadcastA2dpPlayStatusChange(this, playerState, currentPlaying.getSong(), downloadList.size() + backgroundDownloadList.size(), downloadList.indexOf(currentPlaying) + 1, getPlayerPosition()); + + if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) + { + // Set remote control + updateRemoteControl(); + } + + // Update widget + UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); + UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, true); + UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); + UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); + SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); + + if (show) + { + if (tabInstance != null) + { + // Only update notification is player state is one that will change the icon + if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) + { + updateNotification(); + tabInstance.showNowPlaying(); + } + } + } + else + { + if (tabInstance != null) + { + stopForeground(true); + isInForeground = false; + tabInstance.hideNowPlaying(); + stopSelf(); + } + } + + if (playerState == STARTED) + { + scrobbler.scrobble(this, currentPlaying, false); + } + else if (playerState == COMPLETED) + { + scrobbler.scrobble(this, currentPlaying, true); + } + + if (playerState == STARTED && positionCache == null) + { + positionCache = new PositionCache(); + Thread thread = new Thread(positionCache); + thread.start(); + } + else if (playerState != STARTED && positionCache != null) + { + positionCache.stop(); + positionCache = null; + } + } + + private void setPlayerStateCompleted() + { + Log.i(TAG, String.format("%s -> %s (%s)", playerState.name(), PlayerState.COMPLETED, currentPlaying)); + playerState = PlayerState.COMPLETED; + + if (positionCache != null) + { + positionCache.stop(); + positionCache = null; + } + + scrobbler.scrobble(this, currentPlaying, true); + } + + private static synchronized void setNextPlayerState(PlayerState playerState) + { + Log.i(TAG, String.format("Next: %s -> %s (%s)", nextPlayerState.name(), playerState.name(), nextPlaying)); + nextPlayerState = playerState; + } + + public synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) + { + try + { + downloadFile.setPlaying(false); + //downloadFile.setPlaying(true); + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + boolean partial = file.equals(downloadFile.getPartialFile()); + downloadFile.updateModificationDate(); + + mediaPlayer.setOnCompletionListener(null); + secondaryProgress = -1; // Ensure seeking in non StreamProxy playback works + mediaPlayer.reset(); + setPlayerState(IDLE); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + String dataSource = file.getPath(); + + if (partial) + { + if (proxy == null) + { + proxy = new StreamProxy(new Supplier() { + @Override + public DownloadFile get() { return currentPlaying; } + }); + proxy.start(); + } + + dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8)); + Log.i(TAG, String.format("Data Source: %s", dataSource)); + } + else if (proxy != null) + { + proxy.stop(); + proxy = null; + } + + Log.i(TAG, "Preparing media player"); + mediaPlayer.setDataSource(dataSource); + setPlayerState(PREPARING); + + mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() + { + @Override + public void onBufferingUpdate(MediaPlayer mp, int percent) + { + SeekBar progressBar = DownloadActivity.getProgressBar(); + MusicDirectory.Entry song = downloadFile.getSong(); + + if (percent == 100) + { + if (progressBar != null) + { + progressBar.setSecondaryProgress(100 * progressBar.getMax()); + } + + mp.setOnBufferingUpdateListener(null); + } + else if (progressBar != null && song.getTranscodedContentType() == null && Util.getMaxBitRate(MediaPlayerService.this) == 0) + { + secondaryProgress = (int) (((double) percent / (double) 100) * progressBar.getMax()); + progressBar.setSecondaryProgress(secondaryProgress); + } + } + }); + + mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() + { + @Override + public void onPrepared(MediaPlayer mp) + { + Log.i(TAG, "Media player prepared"); + + setPlayerState(PREPARED); + + SeekBar progressBar = DownloadActivity.getProgressBar(); + + if (progressBar != null && downloadFile.isWorkDone()) + { + // Populate seek bar secondary progress if we have a complete file for consistency + DownloadActivity.getProgressBar().setSecondaryProgress(100 * progressBar.getMax()); + } + + synchronized (MediaPlayerService.this) + { + if (position != 0) + { + Log.i(TAG, String.format("Restarting player from position %d", position)); + seekTo(position); + } + cachedPosition = position; + + if (start) + { + mediaPlayer.start(); + setPlayerState(STARTED); + } + else + { + setPlayerState(PAUSED); + } + } + + lifecycleSupport.serializeDownloadQueue(); + } + }); + + setupHandlers(downloadFile, partial); + + mediaPlayer.prepareAsync(); + } + catch (Exception x) + { + handleError(x); + } + } + + private synchronized void setupNext(final DownloadFile downloadFile) + { + try + { + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + + if (nextMediaPlayer != null) + { + nextMediaPlayer.setOnCompletionListener(null); + nextMediaPlayer.release(); + nextMediaPlayer = null; + } + + nextMediaPlayer = new MediaPlayer(); + nextMediaPlayer.setWakeMode(MediaPlayerService.this, PowerManager.PARTIAL_WAKE_LOCK); + + try + { + nextMediaPlayer.setAudioSessionId(mediaPlayer.getAudioSessionId()); + } + catch (Throwable e) + { + nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + } + + nextMediaPlayer.setDataSource(file.getPath()); + setNextPlayerState(PREPARING); + + nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() + { + @Override + @SuppressLint("NewApi") + public void onPrepared(MediaPlayer mp) + { + try + { + setNextPlayerState(PREPARED); + + if (Util.getGaplessPlaybackPreference(MediaPlayerService.this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) + { + mediaPlayer.setNextMediaPlayer(nextMediaPlayer); + nextSetup = true; + } + } + catch (Exception x) + { + handleErrorNext(x); + } + } + }); + + nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() + { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) + { + Log.w(TAG, String.format("Error on playing next (%d, %d): %s", what, extra, downloadFile)); + return true; + } + }); + + nextMediaPlayer.prepareAsync(); + } + catch (Exception x) + { + handleErrorNext(x); + } + } + + private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) + { + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() + { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) + { + Log.w(TAG, String.format("Error on playing file (%d, %d): %s", what, extra, downloadFile)); + int pos = cachedPosition; + reset(); + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + return true; + } + }); + + final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; + + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() + { + @Override + public void onCompletion(MediaPlayer mediaPlayer) + { + // Acquire a temporary wakelock, since when we return from + // this callback the MediaPlayer will release its wakelock + // and allow the device to go to sleep. + wakeLock.acquire(60000); + + int pos = cachedPosition; + Log.i(TAG, String.format("Ending position %d of %d", pos, duration)); + + if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 1000))) + { + setPlayerStateCompleted(); + + if (Util.getGaplessPlaybackPreference(MediaPlayerService.this) && nextPlaying != null && nextPlayerState == PlayerState.PREPARED) + { + if (!nextSetup) + { + playNext(); + } + else + { + nextSetup = false; + playNext(); + } + } + else + { + onSongCompleted(); + } + + return; + } + + synchronized (this) + { + if (downloadFile.isWorkDone()) + { + // Complete was called early even though file is fully buffered + Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration)); + reset(); + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + } + else + { + Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration)); + reset(); + bufferTask = new BufferTask(downloadFile, pos); + bufferTask.start(); + } + } + } + }); + } + + private void onSongCompleted() + { + int index = getCurrentPlayingIndex(); + + if (currentPlaying != null) + { + final MusicDirectory.Entry song = currentPlaying.getSong(); + + if (song != null && song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(this)) + { + MusicService musicService = MusicServiceFactory.getMusicService(this); + try + { + musicService.deleteBookmark(song.getId(), this, null); + } + catch (Exception ignored) + { + + } + } + } + + if (index != -1) + { + switch (getRepeatMode()) + { + case OFF: + if (index + 1 < 0 || index + 1 >= size()) + { + if (Util.getShouldClearPlaylist(this)) + { + clear(true); + } + + resetPlayback(); + break; + } + + play(index + 1); + break; + case ALL: + play((index + 1) % size()); + break; + case SINGLE: + play(index); + break; + default: + break; + } + } + } + + public static synchronized void clear(boolean serialize) + { + MediaPlayerService mediaPlayerService = getRunningInstance(); + + if (mediaPlayerService != null) mediaPlayerService.reset(); + downloadList.clear(); + revision++; + if (currentDownloading != null) + { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + if (mediaPlayerService != null) + { + mediaPlayerService.setCurrentPlaying(null); + updateJukeboxPlaylist(); + mediaPlayerService.setNextPlaying(); + } + + if (serialize) + { + lifecycleSupport.serializeDownloadQueue(); + } + } + + protected static synchronized void checkDownloads(Context context) + { + if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) + { + return; + } + + if (shufflePlay) + { + checkShufflePlay(context); + } + + if (jukeboxEnabled || !Util.isNetworkConnected(context)) + { + return; + } + + if (MediaPlayerService.downloadList.isEmpty() && MediaPlayerService.backgroundDownloadList.isEmpty()) + { + return; + } + + // Need to download current playing? + if (MediaPlayerService.currentPlaying != null && MediaPlayerService.currentPlaying != MediaPlayerService.currentDownloading && !MediaPlayerService.currentPlaying.isWorkDone()) + { + // Cancel current download, if necessary. + if (MediaPlayerService.currentDownloading != null) + { + MediaPlayerService.currentDownloading.cancelDownload(); + } + + MediaPlayerService.currentDownloading = MediaPlayerService.currentPlaying; + MediaPlayerService.currentDownloading.download(); + cleanupCandidates.add(MediaPlayerService.currentDownloading); + } + + // Find a suitable target for download. + else + { + if (MediaPlayerService.currentDownloading == null || + MediaPlayerService.currentDownloading.isWorkDone() || + MediaPlayerService.currentDownloading.isFailed() && + (!MediaPlayerService.downloadList.isEmpty() || !MediaPlayerService.backgroundDownloadList.isEmpty())) + { + MediaPlayerService.currentDownloading = null; + int n = size(); + + int preloaded = 0; + + if (n != 0) + { + int start = MediaPlayerService.currentPlaying == null ? 0 : getCurrentPlayingIndex(); + if (start == -1) + { + start = 0; + } + int i = start; + do + { + DownloadFile downloadFile = MediaPlayerService.downloadList.get(i); + if (!downloadFile.isWorkDone()) + { + if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(context)) + { + MediaPlayerService.currentDownloading = downloadFile; + MediaPlayerService.currentDownloading.download(); + cleanupCandidates.add(MediaPlayerService.currentDownloading); + if (i == (start + 1)) + { + setNextPlayerState(DOWNLOADING); + } + break; + } + } + else if (MediaPlayerService.currentPlaying != downloadFile) + { + preloaded++; + } + + i = (i + 1) % n; + } while (i != start); + } + + if ((preloaded + 1 == n || preloaded >= Util.getPreloadCount(context) || MediaPlayerService.downloadList.isEmpty()) && !MediaPlayerService.backgroundDownloadList.isEmpty()) + { + for (int i = 0; i < MediaPlayerService.backgroundDownloadList.size(); i++) + { + DownloadFile downloadFile = MediaPlayerService.backgroundDownloadList.get(i); + if (downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved())) + { + if (Util.getShouldScanMedia(context)) + { + Util.scanMedia(context, downloadFile.getCompleteFile()); + } + + // Don't need to keep list like active song list + MediaPlayerService.backgroundDownloadList.remove(i); + revision++; + i--; + } + else + { + MediaPlayerService.currentDownloading = downloadFile; + MediaPlayerService.currentDownloading.download(); + cleanupCandidates.add(MediaPlayerService.currentDownloading); + break; + } + } + } + } + } + + // Delete obsolete .partial and .complete files. + cleanup(context); + } + + private static synchronized void checkShufflePlay(Context context) + { + // Get users desired random playlist size + int listSize = Util.getMaxSongs(context); + boolean wasEmpty = MediaPlayerService.downloadList.isEmpty(); + + long revisionBefore = revision; + + // First, ensure that list is at least 20 songs long. + int size = size(); + if (size < listSize) + { + for (MusicDirectory.Entry song : MediaPlayerService.shufflePlayBuffer.get(listSize - size)) + { + DownloadFile downloadFile = new DownloadFile(context, song, false); + MediaPlayerService.downloadList.add(downloadFile); + revision++; + } + } + + int currIndex = MediaPlayerService.currentPlaying == null ? 0 : getCurrentPlayingIndex(); + + // Only shift playlist if playing song #5 or later. + if (currIndex > 4) + { + int songsToShift = currIndex - 2; + for (MusicDirectory.Entry song : MediaPlayerService.shufflePlayBuffer.get(songsToShift)) + { + MediaPlayerService.downloadList.add(new DownloadFile(context, song, false)); + MediaPlayerService.downloadList.get(0).cancelDownload(); + MediaPlayerService.downloadList.remove(0); + revision++; + } + } + + if (revisionBefore != revision) + { + getInstance(context).updateJukeboxPlaylist(); + } + + if (wasEmpty && !MediaPlayerService.downloadList.isEmpty()) + { + getInstance(context).play(0); + } + } + + public static void updateJukeboxPlaylist() + { + if (jukeboxEnabled) + { + jukeboxService.updatePlaylist(); + } + } + + private static synchronized void cleanup(Context context) + { + Iterator iterator = cleanupCandidates.iterator(); + while (iterator.hasNext()) + { + DownloadFile downloadFile = iterator.next(); + if (downloadFile != MediaPlayerService.currentPlaying && downloadFile != MediaPlayerService.currentDownloading) + { + if (downloadFile.cleanup()) + { + iterator.remove(); + } + } + } + } + + public void updateNotification() + { + if (Util.isNotificationEnabled(this)) { + if (isInForeground == true) { + 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()); + } + Log.w(TAG, "--- Updated notification"); + } + else { + startForeground(NOTIFICATION_ID, buildForegroundNotification()); + isInForeground = true; + Log.w(TAG, "--- Created Foreground notification"); + } + } + } + + @SuppressWarnings("IconColors") + private Notification buildForegroundNotification() { + notificationBuilder.setSmallIcon(R.drawable.ic_stat_ultrasonic); + + 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); + + notificationBuilder.setContent(contentView); + + Intent notificationIntent = new Intent(this, DownloadActivity.class); + 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); + bigView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); + } else if (playerState == PlayerState.STARTED) { + contentView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); + bigView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); + } + + if (currentPlaying != null) { + final MusicDirectory.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); + } + + + 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 = notificationBuilder.build(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + notification.bigContentView = bigView; + } + + return notification; + } + + private class PositionCache implements Runnable + { + boolean isRunning = true; + + public void stop() + { + isRunning = false; + } + + @Override + public void run() + { + Thread.currentThread().setName("PositionCache"); + + // Stop checking position before the song reaches completion + while (isRunning) + { + try + { + if (mediaPlayer != null && playerState == STARTED) + { + cachedPosition = mediaPlayer.getCurrentPosition(); + } + + Util.sleepQuietly(25L); + } + catch (Exception e) + { + Log.w(TAG, "Crashed getting current position", e); + isRunning = false; + positionCache = null; + } + } + } + } + + private void handleError(Exception x) + { + Log.w(TAG, String.format("Media player error: %s", x), x); + + try + { + mediaPlayer.reset(); + } + catch (Exception ex) + { + Log.w(TAG, String.format("Exception encountered when resetting media player: %s", ex), ex); + } + } + + private void handleErrorNext(Exception x) + { + Log.w(TAG, String.format("Next Media player error: %s", x), x); + nextMediaPlayer.reset(); + } + + private class BufferTask extends CancellableTask + { + private final DownloadFile downloadFile; + private final int position; + private final long expectedFileSize; + private final File partialFile; + + public BufferTask(DownloadFile downloadFile, int position) + { + this.downloadFile = downloadFile; + this.position = position; + partialFile = downloadFile.getPartialFile(); + + long bufferLength = Util.getBufferLength(MediaPlayerService.this); + + if (bufferLength == 0) + { + // Set to seconds in a day, basically infinity + bufferLength = 86400L; + } + + // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. + int bitRate = downloadFile.getBitRate(); + long byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength); + + // Find out how large the file should grow before resuming playback. + Log.i(TAG, String.format("Buffering from position %d and bitrate %d", position, bitRate)); + expectedFileSize = (position * bitRate / 8) + byteCount; + } + + @Override + public void execute() + { + setPlayerState(DOWNLOADING); + + while (!bufferComplete() && !Util.isOffline(MediaPlayerService.this)) + { + Util.sleepQuietly(1000L); + if (isCancelled()) + { + return; + } + } + doPlay(downloadFile, position, true); + } + + private boolean bufferComplete() + { + boolean completeFileAvailable = downloadFile.isWorkDone(); + long size = partialFile.length(); + + Log.i(TAG, String.format("Buffering %s (%d/%d, %s)", partialFile, size, expectedFileSize, completeFileAvailable)); + return completeFileAvailable || size >= expectedFileSize; + } + + @Override + public String toString() + { + return String.format("BufferTask (%s)", downloadFile); + } + } + + private class CheckCompletionTask extends CancellableTask + { + private final DownloadFile downloadFile; + private final File partialFile; + + public CheckCompletionTask(DownloadFile downloadFile) + { + super(); + setNextPlayerState(PlayerState.IDLE); + + this.downloadFile = downloadFile; + + partialFile = downloadFile != null ? downloadFile.getPartialFile() : null; + } + + @Override + public void execute() + { + Thread.currentThread().setName("CheckCompletionTask"); + + if (downloadFile == null) + { + return; + } + + // Do an initial sleep so this prepare can't compete with main prepare + Util.sleepQuietly(5000L); + + while (!bufferComplete()) + { + Util.sleepQuietly(5000L); + + if (isCancelled()) + { + return; + } + } + + // Start the setup of the next media player + mediaPlayerHandler.post(new Runnable() + { + @Override + public void run() + { + setupNext(downloadFile); + } + }); + } + + private boolean bufferComplete() + { + boolean completeFileAvailable = downloadFile.isWorkDone(); + Log.i(TAG, String.format("Buffering next %s (%d)", partialFile, partialFile.length())); + return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED); + } + + @Override + public String toString() + { + return String.format("CheckCompletionTask (%s)", downloadFile); + } + + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java new file mode 100644 index 00000000..9a4a072c --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java @@ -0,0 +1,6 @@ +package org.moire.ultrasonic.service; + +public abstract class Supplier +{ + public abstract T get(); +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java index 5e8c5d4a..34df0285 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java @@ -4,7 +4,7 @@ import android.util.Log; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.DownloadService; +import org.moire.ultrasonic.service.Supplier; import java.io.BufferedOutputStream; import java.io.BufferedReader; @@ -32,9 +32,9 @@ public class StreamProxy implements Runnable private boolean isRunning; private ServerSocket socket; private int port; - private DownloadService downloadService; + private Supplier currentPlaying; - public StreamProxy(DownloadService downloadService) + public StreamProxy(Supplier currentPlaying) { // Create listening socket @@ -43,7 +43,7 @@ public class StreamProxy implements Runnable socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1})); socket.setSoTimeout(5000); port = socket.getLocalPort(); - this.downloadService = downloadService; + this.currentPlaying = currentPlaying; } catch (UnknownHostException e) { // impossible @@ -170,7 +170,7 @@ public class StreamProxy implements Runnable public void run() { Log.i(TAG, "Streaming song in background"); - DownloadFile downloadFile = downloadService.getCurrentPlaying(); + DownloadFile downloadFile = currentPlaying.get(); MusicDirectory.Entry song = downloadFile.getSong(); long fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8; Log.i(TAG, String.format("Streaming fileSize: %d", fileSize)); 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 4c455236..26a2870a 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -56,6 +56,7 @@ import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.MediaPlayerService; import org.moire.ultrasonic.service.MusicServiceFactory; import java.io.*; @@ -1045,38 +1046,31 @@ public class Util extends DownloadActivity context.sendBroadcast(avrcpIntent); } - public static void broadcastA2dpPlayStatusChange(Context context, PlayerState state, DownloadService downloadService) + public static void broadcastA2dpPlayStatusChange(Context context, PlayerState state, Entry currentSong, Integer listSize, Integer id, Integer playerPosition) { - if (!Util.getShouldSendBluetoothNotifications(context) || downloadService == null) + if (!Util.getShouldSendBluetoothNotifications(context)) { return; } - DownloadFile currentPlaying = downloadService.getCurrentPlaying(); - - if (currentPlaying != null) + if (currentSong != null) { Intent avrcpIntent = new Intent(CM_AVRCP_PLAYSTATE_CHANGED); - Entry song = currentPlaying.getSong(); - - if (song == null) + if (currentSong == null) { return; } - if (song != currentSong) + if (currentSong != currentSong) { - currentSong = song; + Util.currentSong = currentSong; } - String title = song.getTitle(); - String artist = song.getArtist(); - String album = song.getAlbum(); - Integer duration = song.getDuration(); - Integer listSize = downloadService.getDownloads().size(); - Integer id = downloadService.getCurrentPlayingIndex() + 1; - Integer playerPosition = downloadService.getPlayerPosition(); + String title = currentSong.getTitle(); + String artist = currentSong.getArtist(); + String album = currentSong.getAlbum(); + Integer duration = currentSong.getDuration(); avrcpIntent.putExtra("track", title); avrcpIntent.putExtra("track_name", title); @@ -1089,7 +1083,7 @@ public class Util extends DownloadActivity if (Util.getShouldSendBluetoothAlbumArt(context)) { - File albumArtFile = FileUtil.getAlbumArtFile(context, song); + File albumArtFile = FileUtil.getAlbumArtFile(context, currentSong); avrcpIntent.putExtra("coverart", albumArtFile.getAbsolutePath()); avrcpIntent.putExtra("cover", albumArtFile.getAbsolutePath()); } @@ -1290,55 +1284,55 @@ public class Util extends DownloadActivity // Emulate media button clicks. intent = new Intent("1"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.control_play, pendingIntent); intent = new Intent("2"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.control_next, pendingIntent); intent = new Intent("3"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); intent = new Intent("4"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.control_stop, pendingIntent); intent = new Intent("RATE_1"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_1)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_1, pendingIntent); intent = new Intent("RATE_2"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_2)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_2, pendingIntent); intent = new Intent("RATE_3"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_3)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_3, pendingIntent); intent = new Intent("RATE_4"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_4)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_4, pendingIntent); intent = new Intent("RATE_5"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.setComponent(new ComponentName(context, MediaPlayerService.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_5)); pendingIntent = PendingIntent.getService(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_5, pendingIntent);