Files
plexamp-lite/platform/api16/com/minamp/Platform.java
T
XoséandClaude Opus 5.5 13f29e26c4 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>
2026-11-01 18:41:26 +01:00

123 lines
5.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}