Private
Public Access
Plexamp Lite 1.14: Android 4.1 build with ExoPlayer, lyrics, home-screen widget
- One build per Android level, as in Tenda: build/PlexampLite.apk (Android 2.2+, versionCode 1508) and build/PlexampLite-api16.apk (Android 4.1+, 1516), from shared src/ plus platform/api8 or platform/api16. - Android 4.1 build: ExoPlayer 2.18.7 behind a small Engine interface (gapless hand-over to read-ahead and downloaded files, a longer buffer, MediaPlayer fallback for tracks it can't decode), a notification and lock screen with art and buttons, a resizable widget, Holo, a menu button without a Menu key, metered networks. R8 shrinks ExoPlayer and Guava (tools/fetch_exoplayer.sh, tools/r8-rules.pro). - Both builds: lyrics in Now Playing as in the Mac app (timed lyrics follow and seek), a home-screen widget, media play/pause keys, the moon icon drawn without clipPath. - PlexampLite-noreport on the SD card stops play reports, for tests against a real server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e7476669e
commit
13f29e26c4
39 files changed
+1685
-119
No files matched your search
@@ -0,0 +1,122 @@
|
||||
package com.minamp;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
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.RemoteControlClient;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Bundle;
|
||||
import android.os.StrictMode;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.minamp.plex.Item;
|
||||
|
||||
/**
|
||||
* What differs between Plexamp Lite's builds, for Android 4.1 and later (Xperia J, Galaxy S II...): ExoPlayer
|
||||
* (gapless, steadier buffering), a notification with art and buttons, lock-screen controls, a resizable widget,
|
||||
* and metered networks. platform/api8 has the same methods for Android 2.2; keep the two in step.
|
||||
*/
|
||||
final class Platform {
|
||||
private Platform() {}
|
||||
|
||||
static final int API = 16;
|
||||
|
||||
static final int ART_PX = 320; // the lock screen shows it large
|
||||
|
||||
static void init(App app) {
|
||||
// The shared code sometimes closes a connection or cuts a download short on the main thread (always brief).
|
||||
// Android 2.x allows that; Android 3+ kills apps that target it. Allow it here too.
|
||||
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder(StrictMode.getThreadPolicy()).permitNetwork().build());
|
||||
}
|
||||
|
||||
static Engine newEngine(Context c, boolean fallback) {
|
||||
return fallback ? new MediaPlayerEngine(c) : new ExoEngine(c);
|
||||
}
|
||||
|
||||
/** Android 4.1 knows which networks cost: mobile data, and a Wi-Fi that is a phone's hotspot. */
|
||||
static boolean metered(Context c) {
|
||||
try {
|
||||
ConnectivityManager cm = (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo ni = cm.getActiveNetworkInfo();
|
||||
return ni != null && ni.isConnected() && cm.isActiveNetworkMetered();
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean needsMenuButton(Context c) {
|
||||
return !ViewConfiguration.get(c).hasPermanentMenuKey();
|
||||
}
|
||||
|
||||
/** Art, title and artist with ⏮ ⏯ ⏭ and ×; pulled down, a larger view. Swiping it away while paused stops. */
|
||||
static Notification playerNotification(Context c, Item it, Bitmap art, int state) {
|
||||
boolean playing = state == PlayerService.PLAYING || state == PlayerService.BUFFERING;
|
||||
Notification n = new Notification.Builder(c)
|
||||
.setSmallIcon(R.drawable.ic_stat)
|
||||
.setContent(PlayerViews.build(c, R.layout.widget_player, it, art, state, true))
|
||||
.setContentIntent(PlayerViews.open(c))
|
||||
.setDeleteIntent(PlayerViews.service(c, PlayerService.ACTION_STOP, 14))
|
||||
.setOngoing(playing)
|
||||
.setWhen(0)
|
||||
.setPriority(playing ? Notification.PRIORITY_HIGH : Notification.PRIORITY_DEFAULT)
|
||||
.build();
|
||||
n.bigContentView = PlayerViews.build(c, R.layout.widget_player_large, it, art, state, true);
|
||||
return n;
|
||||
}
|
||||
|
||||
private static RemoteControlClient remote;
|
||||
private static String remoteKey;
|
||||
private static Bitmap remoteArt;
|
||||
|
||||
/** The lock screen's player: art, title, artist and buttons (they arrive as media buttons). */
|
||||
static void lockScreen(Context c, Item it, Bitmap art, int state) {
|
||||
AudioManager am = (AudioManager) c.getSystemService(Context.AUDIO_SERVICE);
|
||||
if (it == null || state == PlayerService.IDLE) {
|
||||
if (remote != null) {
|
||||
remote.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
|
||||
am.unregisterRemoteControlClient(remote);
|
||||
remote = null;
|
||||
remoteKey = null;
|
||||
remoteArt = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (remote == null) {
|
||||
ComponentName buttons = new ComponentName(c, MediaButtonReceiver.class);
|
||||
am.registerMediaButtonEventReceiver(buttons);
|
||||
Intent i = new Intent(Intent.ACTION_MEDIA_BUTTON).setComponent(buttons);
|
||||
remote = new RemoteControlClient(PendingIntent.getBroadcast(c.getApplicationContext(), 0, i, 0));
|
||||
remote.setTransportControlFlags(RemoteControlClient.FLAG_KEY_MEDIA_PLAY | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
|
||||
| RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
|
||||
| RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS);
|
||||
am.registerRemoteControlClient(remote);
|
||||
}
|
||||
remote.setPlaybackState(state == PlayerService.PLAYING ? RemoteControlClient.PLAYSTATE_PLAYING
|
||||
: state == PlayerService.PAUSED ? RemoteControlClient.PLAYSTATE_PAUSED : RemoteControlClient.PLAYSTATE_BUFFERING);
|
||||
if (it.ratingKey.equals(remoteKey) && art == remoteArt) return;
|
||||
remoteKey = it.ratingKey;
|
||||
remoteArt = art;
|
||||
RemoteControlClient.MetadataEditor ed = remote.editMetadata(true);
|
||||
ed.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, it.title);
|
||||
ed.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, it.artist);
|
||||
ed.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, it.album);
|
||||
ed.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, it.duration);
|
||||
// a copy: Android recycles the artwork it's given when the next one comes, and this one is in the art cache
|
||||
if (art != null) ed.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, art.copy(Bitmap.Config.ARGB_8888, false));
|
||||
ed.apply();
|
||||
}
|
||||
|
||||
/** Two rows tall or more: the larger layout. */
|
||||
static int widgetLayout(AppWidgetManager m, int id) {
|
||||
Bundle o = m.getAppWidgetOptions(id);
|
||||
int h = o == null ? 0 : o.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT);
|
||||
return h >= 100 ? R.layout.widget_player_large : R.layout.widget_player;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user