Preparation to music streaming - Picking from Gelli

This commit is contained in:
Antonio Cappiello 2020-12-08 20:30:21 +01:00
parent a28ad27288
commit 820f783d01
18 changed files with 1921 additions and 87 deletions

View file

@ -0,0 +1,219 @@
/*
* Copyright (C) 2007 The Android Open Source Project Licensed under the Apache
* License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
* or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Modified for Phonograph by Karim Abou Zeid (kabouzeid).
package com.cappielloantonio.play.service;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.view.KeyEvent;
import androidx.core.content.ContextCompat;
import com.cappielloantonio.play.BuildConfig;
/**
* Used to control headset playback.
* Single press: pause/resume
* Double press: next track
* Triple press: previous track
*/
public class MediaButtonIntentReceiver extends BroadcastReceiver {
private static final boolean DEBUG = BuildConfig.DEBUG;
public static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();
private static final int MSG_HEADSET_DOUBLE_CLICK_TIMEOUT = 2;
private static final int DOUBLE_CLICK = 400;
private static WakeLock mWakeLock = null;
private static int mClickCounter = 0;
private static long mLastClickTime = 0;
@SuppressLint("HandlerLeak")
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(final Message msg) {
switch (msg.what) {
case MSG_HEADSET_DOUBLE_CLICK_TIMEOUT:
final int clickCount = msg.arg1;
final String command;
if (DEBUG) Log.v(TAG, "Handling headset click, count = " + clickCount);
switch (clickCount) {
case 1:
command = MusicService.ACTION_TOGGLE;
break;
case 2:
command = MusicService.ACTION_SKIP;
break;
case 3:
command = MusicService.ACTION_REWIND;
break;
default:
command = null;
break;
}
if (command != null) {
final Context context = (Context) msg.obj;
startService(context, command);
}
break;
}
releaseWakeLockIfHandlerIdle();
}
};
@Override
public void onReceive(final Context context, final Intent intent) {
if (DEBUG) Log.v(TAG, "Received intent: " + intent);
if (handleIntent(context, intent) && isOrderedBroadcast()) {
abortBroadcast();
}
}
public static boolean handleIntent(final Context context, final Intent intent) {
final String intentAction = intent.getAction();
if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {
final KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (event == null) {
return false;
}
final int keycode = event.getKeyCode();
final int action = event.getAction();
// fallback to system time if event time is not available
final long eventTime = event.getEventTime() != 0
? event.getEventTime()
: System.currentTimeMillis();
String command = null;
switch (keycode) {
case KeyEvent.KEYCODE_MEDIA_STOP:
command = MusicService.ACTION_STOP;
break;
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
command = MusicService.ACTION_TOGGLE;
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
command = MusicService.ACTION_SKIP;
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
command = MusicService.ACTION_REWIND;
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
command = MusicService.ACTION_PAUSE;
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
command = MusicService.ACTION_PLAY;
break;
}
if (command != null) {
if (action == KeyEvent.ACTION_DOWN) {
if (event.getRepeatCount() == 0) {
// Only consider the first event in a sequence, not the repeat events,
// so that we don't trigger in cases where the first event went to
// a different app (e.g. when the user ends a phone call by
// long pressing the headset button)
// The service may or may not be running, but we need to send it
// a command.
if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
if (eventTime - mLastClickTime >= DOUBLE_CLICK) {
mClickCounter = 0;
}
mClickCounter++;
if (DEBUG) Log.v(TAG, "Got headset click, count = " + mClickCounter);
mHandler.removeMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT);
Message msg = mHandler.obtainMessage(
MSG_HEADSET_DOUBLE_CLICK_TIMEOUT, mClickCounter, 0, context);
long delay = mClickCounter < 3 ? DOUBLE_CLICK : 0;
if (mClickCounter >= 3) {
mClickCounter = 0;
}
mLastClickTime = eventTime;
acquireWakeLockAndSendMessage(context, msg, delay);
} else {
startService(context, command);
}
return true;
}
}
}
}
return false;
}
private static void startService(Context context, String command) {
final Intent intent = new Intent(context, MusicService.class);
intent.setAction(command);
try {
// IMPORTANT NOTE: (kind of a hack)
// on Android O and above the following crashes when the app is not running
// there is no good way to check whether the app is running so we catch the exception
// we do not always want to use startForegroundService() because then one gets an ANR
// if no notification is displayed via startForeground()
// according to Play analytics this happens a lot, I suppose for example if command = PAUSE
context.startService(intent);
} catch (IllegalStateException ignored) {
ContextCompat.startForegroundService(context, intent);
}
}
private static void acquireWakeLockAndSendMessage(Context context, Message msg, long delay) {
if (mWakeLock == null) {
Context appContext = context.getApplicationContext();
PowerManager pm = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, context.getClass().getName());
mWakeLock.setReferenceCounted(false);
}
if (DEBUG) Log.v(TAG, "Acquiring wake lock and sending " + msg.what);
mWakeLock.acquire(10000);
mHandler.sendMessageDelayed(msg, delay);
}
private static void releaseWakeLockIfHandlerIdle() {
if (mHandler.hasMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT)) {
if (DEBUG) Log.v(TAG, "Handler still has messages pending, not releasing wake lock");
return;
}
if (mWakeLock != null) {
if (DEBUG) Log.v(TAG, "Releasing wake lock");
mWakeLock.release();
mWakeLock = null;
}
}
}

View file

@ -0,0 +1,290 @@
package com.cappielloantonio.play.service;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.model.Song;
import com.cappielloantonio.play.service.playback.Playback;
import com.cappielloantonio.play.util.MusicUtil;
import com.cappielloantonio.play.util.PreferenceUtil;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import java.io.File;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class MultiPlayer implements Playback {
public static final String TAG = MultiPlayer.class.getSimpleName();
private final Context context;
private final OkHttpClient httpClient;
private SimpleExoPlayer exoPlayer;
private ConcatenatingMediaSource mediaSource;
private final SimpleCache simpleCache;
private final DataSource.Factory dataSource;
private PlaybackCallbacks callbacks;
private boolean isReady = false;
private boolean isPlaying = false;
private boolean requestPlay = false;
private int requestProgress = 0;
private final ExoPlayer.EventListener eventListener = new ExoPlayer.EventListener() {
@Override
public void onTracksChanged(@NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) {
Log.i(TAG, "onTracksChanged");
}
@Override
public void onLoadingChanged(boolean isLoading) {
Log.i(TAG, "onLoadingChanged: " + isLoading);
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Log.i(TAG, "onPlayerStateChanged playWhenReady: " + playWhenReady);
Log.i(TAG, "onPlayerStateChanged playbackState: " + playbackState);
if (callbacks == null) return;
if (requestProgress != 0 && playbackState == Player.STATE_READY) {
exoPlayer.seekTo(requestProgress);
requestProgress = 0;
}
if (exoPlayer.isPlaying() || requestPlay && playbackState == ExoPlayer.STATE_READY) {
requestPlay = false;
isPlaying = true;
exoPlayer.setPlayWhenReady(true);
callbacks.onTrackStarted();
}
}
@Override
public void onPositionDiscontinuity(int reason) {
Log.i(TAG, "onPositionDiscontinuity: " + reason);
int windowIndex = exoPlayer.getCurrentWindowIndex();
if (windowIndex == 1) {
mediaSource.removeMediaSource(0);
if (exoPlayer.isPlaying()) {
// there are still songs left in the queue
callbacks.onTrackWentToNext();
} else {
callbacks.onTrackEnded();
}
}
}
@Override
public void onPlayerError(ExoPlaybackException error) {
Log.i(TAG, "onPlayerError: " + error.getMessage());
if (context == null) {
return;
}
Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
exoPlayer.release();
exoPlayer = new SimpleExoPlayer.Builder(context).build();
isReady = false;
}
};
public MultiPlayer(Context context) {
this.context = context;
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(1);
httpClient = new OkHttpClient.Builder().dispatcher(dispatcher).build();
exoPlayer = new SimpleExoPlayer.Builder(context).build();
mediaSource = new ConcatenatingMediaSource();
exoPlayer.addListener(eventListener);
exoPlayer.prepare(mediaSource);
exoPlayer.setRepeatMode(Player.REPEAT_MODE_OFF);
long cacheSize = PreferenceUtil.getInstance(context).getMediaCacheSize();
LeastRecentlyUsedCacheEvictor recentlyUsedCache = new LeastRecentlyUsedCacheEvictor(cacheSize);
ExoDatabaseProvider databaseProvider = new ExoDatabaseProvider(context);
File cacheDirectory = new File(context.getCacheDir(), "exoplayer");
simpleCache = new SimpleCache(cacheDirectory, recentlyUsedCache, databaseProvider);
dataSource = buildDataSourceFactory();
}
@Override
public void setDataSource(Song song) {
isReady = false;
mediaSource = new ConcatenatingMediaSource();
exoPlayer.addListener(eventListener);
exoPlayer.prepare(mediaSource);
// queue and other information is currently handled outside exoplayer
exoPlayer.setRepeatMode(Player.REPEAT_MODE_OFF);
appendDataSource(MusicUtil.getSongFileUri(song));
}
@Override
public void queueDataSource(Song song) {
String path = MusicUtil.getSongFileUri(song);
if (mediaSource.getSize() == 2 && mediaSource.getMediaSource(1).getTag() != path) {
mediaSource.removeMediaSource(1);
}
if (mediaSource.getSize() != 2) {
appendDataSource(path);
}
}
private void appendDataSource(String path) {
Uri uri = Uri.parse(path);
httpClient.newCall(new Request.Builder().url(path).head().build()).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
MediaSource source;
if (response.header("Content-Type").equals("application/x-mpegURL")) {
source = new HlsMediaSource.Factory(dataSource)
.setTag(path)
.setAllowChunklessPreparation(true)
.createMediaSource(uri);
} else {
source = new ProgressiveMediaSource.Factory(dataSource)
.setTag(path)
.createMediaSource(uri);
}
mediaSource.addMediaSource(source);
isReady = true;
}
});
}
private DataSource.Factory buildDataSourceFactory() {
return () -> new CacheDataSource(
simpleCache,
new DefaultDataSourceFactory(context, context.getPackageName(), null).createDataSource(),
new FileDataSource(),
new CacheDataSink(simpleCache, 10 * 1024 * 1024),
CacheDataSource.FLAG_BLOCK_ON_CACHE,
null
);
}
@Override
public void setCallbacks(Playback.PlaybackCallbacks callbacks) {
this.callbacks = callbacks;
}
@Override
public boolean isReady() {
return isReady;
}
@Override
public boolean isPlaying() {
return isReady && isPlaying;
}
@Override
public void start() {
if (!isReady) {
requestPlay = true;
return;
}
isPlaying = true;
exoPlayer.setPlayWhenReady(true);
}
@Override
public void pause() {
isPlaying = false;
exoPlayer.setPlayWhenReady(false);
}
@Override
public void stop() {
simpleCache.release();
exoPlayer.release();
exoPlayer = null;
isReady = false;
}
@Override
public int getProgress() {
if (!isReady) return -1;
return (int) exoPlayer.getCurrentPosition();
}
@Override
public int getDuration() {
if (!isReady) return -1;
return (int) exoPlayer.getDuration();
}
@Override
public void setProgress(int progress) {
if (!isReady) {
requestProgress = progress;
return;
}
exoPlayer.seekTo(progress);
}
@Override
public void setVolume(int volume) {
exoPlayer.setVolume(volume / 100f);
}
@Override
public int getVolume() {
return (int) (exoPlayer.getVolume() * 100);
}
}

View file

@ -0,0 +1,181 @@
package com.cappielloantonio.play.service.notification;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.palette.graphics.Palette;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.glide.palette.BitmapPaletteWrapper;
import com.cappielloantonio.play.model.Song;
import com.cappielloantonio.play.service.MusicService;
import com.cappielloantonio.play.ui.activities.MainActivity;
import static android.content.Context.NOTIFICATION_SERVICE;
import static com.cappielloantonio.play.service.MusicService.ACTION_REWIND;
import static com.cappielloantonio.play.service.MusicService.ACTION_SKIP;
import static com.cappielloantonio.play.service.MusicService.ACTION_TOGGLE;
public class PlayingNotification {
private static final int NOTIFICATION_ID = 1;
static final String NOTIFICATION_CHANNEL_ID = "playing_notification";
private static final int NOTIFY_MODE_FOREGROUND = 1;
private static final int NOTIFY_MODE_BACKGROUND = 0;
private int notifyMode = NOTIFY_MODE_BACKGROUND;
private NotificationManager notificationManager;
protected MusicService service;
boolean stopped;
public synchronized void init(MusicService service) {
this.service = service;
notificationManager = (NotificationManager) service.getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel();
}
}
public synchronized void update() {
stopped = false;
final Song song = service.getCurrentSong();
final boolean isPlaying = service.isPlaying();
final int playButtonResId = isPlaying ? R.drawable.ic_pause_white_24dp : R.drawable.ic_play_arrow_white_24dp;
Intent action = new Intent(service, MainActivity.class);
action.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
final PendingIntent clickIntent = PendingIntent.getActivity(service, 0, action, 0);
final ComponentName serviceName = new ComponentName(service, MusicService.class);
Intent intent = new Intent(MusicService.ACTION_QUIT);
intent.setComponent(serviceName);
final PendingIntent deleteIntent = PendingIntent.getService(service, 0, intent, 0);
final int bigNotificationImageSize = service.getResources().getDimensionPixelSize(R.dimen.notification_big_image_size);
service.runOnUiThread(() -> CustomGlideRequest.Builder
.from(service, song.getPrimary(), song.getBlurHash(), CustomGlideRequest.PRIMARY, CustomGlideRequest.TOP_QUALITY)
.build()
.into(new CustomTarget<BitmapPaletteWrapper>(bigNotificationImageSize, bigNotificationImageSize) {
@Override
public void onResourceReady(@NonNull BitmapPaletteWrapper resource, Transition<? super BitmapPaletteWrapper> glideAnimation) {
Palette palette = resource.getPalette();
update(resource.getBitmap(), palette.getVibrantColor(palette.getMutedColor(Color.TRANSPARENT)));
}
@Override
public void onLoadFailed(Drawable drawable) {
update(null, Color.TRANSPARENT);
}
@Override
public void onLoadCleared(Drawable drawable) {
update(null, Color.TRANSPARENT);
}
void update(Bitmap bitmap, int color) {
if (bitmap == null)
bitmap = BitmapFactory.decodeResource(service.getResources(), R.drawable.default_album_art);
NotificationCompat.Action playPauseAction = new NotificationCompat.Action(playButtonResId,
service.getString(R.string.action_play_pause),
retrievePlaybackAction(ACTION_TOGGLE));
NotificationCompat.Action previousAction = new NotificationCompat.Action(R.drawable.ic_skip_previous_white_24dp,
service.getString(R.string.action_previous),
retrievePlaybackAction(ACTION_REWIND));
NotificationCompat.Action nextAction = new NotificationCompat.Action(R.drawable.ic_skip_next_white_24dp,
service.getString(R.string.action_next),
retrievePlaybackAction(ACTION_SKIP));
NotificationCompat.Builder builder = new NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setSubText(song.getAlbumName())
.setLargeIcon(bitmap)
.setContentIntent(clickIntent)
.setDeleteIntent(deleteIntent)
.setContentTitle(song.getTitle())
.setContentText(song.getArtistName())
.setOngoing(isPlaying)
.setShowWhen(false)
.addAction(previousAction)
.addAction(playPauseAction)
.addAction(nextAction);
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle().setMediaSession(service.getMediaSession().getSessionToken())
.setShowActionsInCompactView(0, 1, 2))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setColor(color);
// notification has been stopped before loading was finished
if (stopped) return;
updateNotifyModeAndPostNotification(builder.build());
}
}));
}
public synchronized void stop() {
stopped = true;
service.stopForeground(true);
notificationManager.cancel(NOTIFICATION_ID);
}
private PendingIntent retrievePlaybackAction(final String action) {
final ComponentName serviceName = new ComponentName(service, MusicService.class);
Intent intent = new Intent(action);
intent.setComponent(serviceName);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
return PendingIntent.getService(service, 0, intent, 0);
}
void updateNotifyModeAndPostNotification(Notification notification) {
int newNotifyMode;
if (service.isPlaying()) {
newNotifyMode = NOTIFY_MODE_FOREGROUND;
} else {
newNotifyMode = NOTIFY_MODE_BACKGROUND;
}
if (notifyMode != newNotifyMode && newNotifyMode == NOTIFY_MODE_BACKGROUND) {
service.stopForeground(false);
}
if (newNotifyMode == NOTIFY_MODE_FOREGROUND) {
service.startForeground(NOTIFICATION_ID, notification);
} else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) {
notificationManager.notify(NOTIFICATION_ID, notification);
}
notifyMode = newNotifyMode;
}
@RequiresApi(26)
private void createNotificationChannel() {
NotificationChannel notificationChannel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID);
if (notificationChannel == null) {
notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, service.getString(R.string.playing_notification_name), NotificationManager.IMPORTANCE_LOW);
notificationChannel.setDescription(service.getString(R.string.playing_notification_description));
notificationChannel.enableLights(false);
notificationChannel.enableVibration(false);
notificationManager.createNotificationChannel(notificationChannel);
}
}
}

View file

@ -0,0 +1,39 @@
package com.cappielloantonio.play.service.playback;
import com.cappielloantonio.play.model.Song;
public interface Playback {
void setDataSource(Song song);
void queueDataSource(Song song);
void setCallbacks(PlaybackCallbacks callbacks);
boolean isReady();
boolean isPlaying();
void start();
void pause();
void stop();
int getProgress();
int getDuration();
void setProgress(int progress);
void setVolume(int volume);
int getVolume();
interface PlaybackCallbacks {
void onTrackStarted();
void onTrackWentToNext();
void onTrackEnded();
}
}