Files
podlite/platform/api16/com/miniabs/Platform.java
T
XoséandClaude Opus 5.5 666a7eb8cd Podlite 1.12: a build for Android 4.1 (Xperia J) with ExoPlayer, speed and skip silences
- One build per Android level, as in Tenda and Plexamp Lite: build/Podlite.apk (Android 2.2+, versionCode 1308) and
  build/Podlite-api16.apk (Android 4.1+, 1316), from shared src/ plus platform/api8 or platform/api16; build.sh now
  runs on the local toolchain (aapt2, dx or R8, signapk.py; the JKS key converted for the run).
- Android 4.1 build: ExoPlayer 2.18.7 behind an Engine interface, with playback speed (player header) and skipping
  silences (Settings), a 3-minute buffer and MediaPlayer as fallback for audio it can't decode; a notification and
  lock screen with the cover and buttons; Holo; a menu button on phones without a Menu key; metered networks for
  "download on Wi-Fi only".
- Both builds: tlskit's Http.abort fix (a finished response's pooled connection is no longer closed under a later
  request); the lock screen's and newer headsets' play/pause keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 21:56:18 +01:00

158 lines
7.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.miniabs;
import android.app.Notification;
import android.app.PendingIntent;
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.StrictMode;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.RemoteViews;
/**
* What differs between Podlite's builds, for Android 4.1 and later (Xperia J, Galaxy S II...): ExoPlayer (speed,
* skipping silences), a notification with the cover and buttons, lock-screen controls, metered networks, and the
* ⋮ button on phones without a Menu key. 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 boolean SPEED = true;
static final int COVER_PX = 320; // the lock screen shows it large
private static boolean initDone;
static void init(Context c) {
if (initDone) return;
initDone = true;
// Shared code sometimes closes a connection 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 unmetered(Context c) {
try {
ConnectivityManager cm = (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo ni = cm == null ? null : cm.getActiveNetworkInfo();
return ni != null && ni.isConnected() && !cm.isActiveNetworkMetered();
} catch (Exception e) {
return false;
}
}
static boolean needsMenuButton(Context c) {
return !ViewConfiguration.get(c).hasPermanentMenuKey();
}
/** Cover, title and show with −15 ⏯ +30 and ✕ (pulled down, a larger one). Swiping it away while paused stops. */
static Notification playerNotification(Context c, Episode ep, String what, Bitmap cover, int state, boolean ongoing) {
Notification n = new Notification.Builder(c)
.setSmallIcon(R.drawable.ic_stat)
.setContent(views(c, R.layout.notif_player, ep, what, cover, state))
.setContentIntent(openPlayer(c))
.setDeleteIntent(service(c, PlayerService.A_STOP, 14))
.setOngoing(ongoing)
.setWhen(0)
.setPriority(state == PlayerService.PLAYING ? Notification.PRIORITY_HIGH : Notification.PRIORITY_DEFAULT)
.build();
n.bigContentView = views(c, R.layout.notif_player_big, ep, what, cover, state);
return n;
}
private static Bitmap small, smallOf;
private static RemoteViews views(Context c, int layout, Episode ep, String what, Bitmap cover, int state) {
RemoteViews v = new RemoteViews(c.getPackageName(), layout);
v.setTextViewText(R.id.n_title, ep.title);
v.setTextViewText(R.id.n_sub, Util.join(what, ep.show));
if (cover != null) {
// the size the notification shows it at: a full-size bitmap would be copied to the status bar each time
if (cover != smallOf) {
int px = (int) (112 * c.getResources().getDisplayMetrics().density);
smallOf = cover;
small = cover.getWidth() <= px ? cover : Bitmap.createScaledBitmap(cover, px, px * cover.getHeight() / cover.getWidth(), true);
}
v.setImageViewBitmap(R.id.n_cover, small);
}
Prefs p = Prefs.get(c);
v.setTextViewText(R.id.n_back, "−" + p.skipBack());
v.setTextViewText(R.id.n_fwd, "+" + p.skipFwd());
boolean playing = state == PlayerService.PLAYING || state == PlayerService.LOADING;
v.setImageViewResource(R.id.n_play, playing ? R.drawable.ic_pause_small : R.drawable.ic_play_small);
v.setOnClickPendingIntent(R.id.n_back, service(c, PlayerService.A_BACK, 11));
v.setOnClickPendingIntent(R.id.n_play, service(c, PlayerService.A_TOGGLE, 12));
v.setOnClickPendingIntent(R.id.n_fwd, service(c, PlayerService.A_FWD, 13));
v.setOnClickPendingIntent(R.id.n_close, service(c, PlayerService.A_STOP, 14));
v.setOnClickPendingIntent(R.id.n_cover, openPlayer(c));
v.setOnClickPendingIntent(R.id.n_text, openPlayer(c));
v.setViewVisibility(R.id.n_close, View.VISIBLE);
return v;
}
static PendingIntent openPlayer(Context c) {
Intent i = new Intent(c, PlayerActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return PendingIntent.getActivity(c, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
}
private static PendingIntent service(Context c, String action, int request) {
return PendingIntent.getService(c, request, new Intent(c, PlayerService.class).setAction(action), 0);
}
private static RemoteControlClient remote;
private static String remoteFor;
private static Bitmap remoteCover;
/** The lock screen's player: cover, title, show and buttons (⏮ ⏭ skip back and forward: media keys). */
static void lockScreen(Context c, Episode ep, Bitmap cover, int state) {
AudioManager am = (AudioManager) c.getSystemService(Context.AUDIO_SERVICE);
if (ep == null || state == PlayerService.IDLE) {
if (remote != null) {
remote.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
am.unregisterRemoteControlClient(remote);
remote = null;
remoteFor = null;
remoteCover = 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.LOADING ? RemoteControlClient.PLAYSTATE_BUFFERING
: state == PlayerService.ERROR ? RemoteControlClient.PLAYSTATE_ERROR : RemoteControlClient.PLAYSTATE_PAUSED);
if (ep.id.equals(remoteFor) && cover == remoteCover) return;
remoteFor = ep.id;
remoteCover = cover;
RemoteControlClient.MetadataEditor ed = remote.editMetadata(true);
ed.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, ep.title);
ed.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, ep.show);
ed.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, (long) (ep.duration * 1000));
// a copy: Android recycles the artwork it's given when the next one comes, and this one is in the cover cache
if (cover != null) ed.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, cover.copy(Bitmap.Config.ARGB_8888, false));
ed.apply();
}
}