diff --git a/build.gradle b/build.gradle index 232512a3..f364202f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ ext { def abi = project.properties['ABI'] - VERSION_CODE = 227 - VERSION_NAME = "1.9.4" + VERSION_CODE = 232 + VERSION_NAME = "1.9.5" SDK_MIN_VERSION = 23 SDK_TARGET_VERSION = 34 SDK_COMPILE_VERSION = 34 @@ -9,9 +9,9 @@ ext { ABI_FILTERS = (abi != null) ? abi.split(",") : ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'] localProps = gradle.ext.localProps - ANDROID_MATERIAL_VERSION = '1.9.0' + ANDROID_MATERIAL_VERSION = '1.10.0' ANDROID_PLAY_CORE_VERSION = '1.10.3' - ANDROIDX_CORE_VERSION = '1.3.2' + ANDROIDX_CORE_VERSION = '1.12.0' ANDROIDX_MEDIA_VERSION = '1.6.0' ANDROIDX_APPCOMPAT_VERSION = '1.6.1' ANDROIDX_CONSTRAINTLAYOUT_VERSION = '2.1.4' @@ -27,8 +27,8 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.1.1' - classpath 'com.google.gms:google-services:4.3.15' + classpath 'com.android.tools.build:gradle:8.1.2' + classpath 'com.google.gms:google-services:4.4.0' } } diff --git a/depends/utils b/depends/utils index eb5addbf..dddb6e89 160000 --- a/depends/utils +++ b/depends/utils @@ -1 +1 @@ -Subproject commit eb5addbff7841094102a947c794e4cf6c08de072 +Subproject commit dddb6e892a1ac01c0f445d056e65e067d27ef4dd diff --git a/fermata/src/main/AndroidManifest.xml b/fermata/src/main/AndroidManifest.xml index de124b83..237eb4ff 100644 --- a/fermata/src/main/AndroidManifest.xml +++ b/fermata/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ + @@ -61,8 +62,7 @@ android:requestLegacyExternalStorage="true" android:supportsRtl="true" android:theme="@style/AppTheme.Dark" - android:usesCleartextTraffic="true" - tools:targetApi="TIRAMISU"> + android:usesCleartextTraffic="true"> dblActionPref; private final PreferenceStore.Pref longActionPref; @Nullable - private Action.Handler clickHandler; + private Action clickAction; @Nullable - private Action.Handler dblClickHandler; + private Action dblClickAction; @Nullable - private Action.Handler longClickHandler; + private Action longClickAction; Key(int code, Action action) { - this(code, action, action, action); + this(code, action, Action.NONE, Action.NONE); } - Key(int code, Action action, Action dblAction, Action longAction) { + Key(int code, Action clickAction, Action dblClickAction, Action longClickAction) { this.code = code; var name = name(); media = name.startsWith("MEDIA_") || name.startsWith("VOLUME_"); actionPref = - PreferenceStore.Pref.i("KEY_ACTION_" + name, action.ordinal()).withInheritance(false); - dblActionPref = PreferenceStore.Pref.i("KEY_ACTION_DBL_" + name, dblAction.ordinal()) + PreferenceStore.Pref.i("KEY_ACTION_" + name, clickAction.ordinal()).withInheritance(false); + dblActionPref = PreferenceStore.Pref.i("KEY_ACTION_DBL_" + name, dblClickAction.ordinal()) .withInheritance(false); - longActionPref = PreferenceStore.Pref.i("KEY_ACTION_LONG_" + name, longAction.ordinal()) + longActionPref = PreferenceStore.Pref.i("KEY_ACTION_LONG_" + name, longClickAction.ordinal()) .withInheritance(false); } @@ -105,24 +106,25 @@ public PreferenceStore.Pref getLongActionPref() { } @Nullable - public Action.Handler getClickHandler() { - if (clickHandler != null) return clickHandler; - var a = Action.get(getPrefs().getIntPref(actionPref)); - return (a == null) ? null : (clickHandler = a.getHandler()); + public Action getClickAction() { + if (clickAction != null) return clickAction; + return Action.get(getPrefs().getIntPref(actionPref)); } @Nullable - public Action.Handler getDblClickHandler() { - if (dblClickHandler != null) return dblClickHandler; - var a = Action.get(getPrefs().getIntPref(dblActionPref)); - return (a == null) ? null : (dblClickHandler = a.getHandler()); + public Action getDblClickAction() { + if (dblClickAction != null) return dblClickAction; + return Action.get(getPrefs().getIntPref(dblActionPref)); } @Nullable - public Action.Handler getLongClickHandler() { - if (longClickHandler != null) return longClickHandler; - var a = Action.get(getPrefs().getIntPref(longActionPref)); - return (a == null) ? null : (longClickHandler = a.getHandler()); + public Action getLongClickAction() { + if (longClickAction != null) return longClickAction; + return Action.get(getPrefs().getIntPref(longActionPref)); + } + + public int getCode() { + return code; } public boolean isMedia() { @@ -136,9 +138,9 @@ public void onPreferenceChanged(PreferenceStore store, List defaultHandler) { + return handleKeyEvent(cb, null, event, defaultHandler); } - public KeyEventHandler(MainActivityDelegate activity) { - this.cb = activity.getMediaSessionCallback(); - this.activity = activity; - this.handler = activity.getHandler(); + public static boolean handleKeyEvent(MainActivityDelegate activity, KeyEvent event, + IntObjectFunction defaultHandler) { + return handleKeyEvent(activity.getMediaSessionCallback(), activity, event, defaultHandler); } - public boolean handle(KeyEvent event, IntObjectFunction defaultHandler) { + private static boolean handleKeyEvent(MediaSessionCallback cb, + @Nullable MainActivityDelegate activity, KeyEvent event, + IntObjectFunction defaultHandler) { Log.i((activity == null) ? "Media: " : "Activity: ", event); if (event.isCanceled()) { @@ -62,47 +58,61 @@ public boolean handle(KeyEvent event, IntObjectFunction defau return defaultHandler.apply(code, event); } - var dblClickHandler = k.getDblClickHandler(); - if (dblClickHandler == null) return defaultHandler.apply(code, event); + var dblClickAction = k.getDblClickAction(); + if (dblClickAction == null) return defaultHandler.apply(code, event); var action = event.getAction(); if (action == ACTION_MULTIPLE) { - dblClickHandler.handle(cb, activity, uptimeMillis()); + Log.i(k, " key double click"); + performAction(dblClickAction, cb, activity, uptimeMillis()); return true; } if (action != ACTION_DOWN) return defaultHandler.apply(code, event); - var clickHandler = k.getClickHandler(); - if (clickHandler == null) return defaultHandler.apply(code, event); - var longClickHandler = k.getLongClickHandler(); - if (longClickHandler == null) return defaultHandler.apply(code, event); + var clickAction = k.getClickAction(); + if (clickAction == null) return defaultHandler.apply(code, event); + var longClickAction = k.getLongClickAction(); + if (longClickAction == null) return defaultHandler.apply(code, event); - if ((dblClickHandler == Action.NONE.getHandler()) && - (longClickHandler == Action.NONE.getHandler())) { - clickHandler.handle(cb, activity, uptimeMillis()); + if (((clickAction == dblClickAction) && (clickAction == longClickAction)) || + ((dblClickAction == Action.NONE) && (longClickAction == Action.NONE))) { + Log.i(k, " key click"); + performAction(clickAction, cb, activity, uptimeMillis()); return true; } - worker = new Worker(event, clickHandler, dblClickHandler, longClickHandler); - return false; + worker = new Worker(cb, activity, k, clickAction, dblClickAction, longClickAction); + return true; + } + + private static void performAction(Action action, MediaSessionCallback cb, + @Nullable MainActivityDelegate activity, long timestamp) { + worker = null; + Log.i("Performing action ", action); + action.getHandler().handle(cb, activity, timestamp); } - private final class Worker implements Runnable { - private final KeyEvent event; - private final Action.Handler clickHandler; - private final Action.Handler dblClickHandler; - private final Action.Handler longClickHandler; + private static final class Worker implements Runnable { + private final MediaSessionCallback cb; + @Nullable + private final MainActivityDelegate activity; + private final Key key; + private final Action clickAction; + private final Action dblClickAction; + private final Action longClickAction; private final long time; private long longClickTime; private boolean up; - Worker(KeyEvent event, Action.Handler clickHandler, Action.Handler dblClickHandler, - Action.Handler longClickHandler) { - this.event = event; - this.clickHandler = clickHandler; - this.dblClickHandler = dblClickHandler; - this.longClickHandler = longClickHandler; + Worker(MediaSessionCallback cb, @Nullable MainActivityDelegate activity, Key key, + Action clickAction, Action dblClickAction, Action longClickAction) { + this.cb = cb; + this.activity = activity; + this.key = key; + this.clickAction = clickAction; + this.dblClickAction = dblClickAction; + this.longClickAction = longClickAction; time = longClickTime = uptimeMillis(); sched(DBL_CLICK_INTERVAL); } @@ -110,43 +120,38 @@ private final class Worker implements Runnable { @Override public void run() { if (worker != this) return; + if (up) { + Log.i(key, " key click"); + handle(clickAction); + return; + } long now = uptimeMillis(); - long diff = now - time; + long diff = now - longClickTime; if (diff < LONG_CLICK_INTERVAL) { - if (up) { - worker = null; - handle(clickHandler); - } else { - sched(LONG_CLICK_INTERVAL - (now - longClickTime)); - } + sched(LONG_CLICK_INTERVAL - diff); + } else if (diff > 15000) { // Key UP not received? + worker = null; } else { - diff = now - longClickTime; - - if (diff < LONG_CLICK_INTERVAL) { - sched(LONG_CLICK_INTERVAL - diff); - } else if (diff > 60000) { // Key UP not received? - worker = null; - } else { - longClickTime = time; - handle(longClickHandler); - sched(LONG_CLICK_INTERVAL); - } + longClickTime = time; + Log.i(key, " key long click"); + handle(longClickAction); + worker = this; + sched(LONG_CLICK_INTERVAL); } } boolean handle(KeyEvent e) { - if (e.getKeyCode() != event.getKeyCode()) return false; + if (e.getKeyCode() != key.getCode()) return false; switch (e.getAction()) { case ACTION_DOWN -> { - if (dblClickHandler == clickHandler) { - longClickTime = uptimeMillis(); - handle(longClickHandler); - } else { - worker = null; - handle(clickHandler); + if (!up) { + if ((longClickAction == clickAction) || (longClickAction == Action.NONE)) { + Log.i(key, " key click"); + handle(clickAction); + } } return true; } @@ -155,11 +160,11 @@ boolean handle(KeyEvent e) { if (holdTime <= DBL_CLICK_INTERVAL) { if (up) { - worker = null; - handle(dblClickHandler); - } else if (dblClickHandler == clickHandler) { - worker = null; - handle(clickHandler); + Log.i(key, " key double click"); + handle(dblClickAction); + } else if (dblClickAction == clickAction) { + Log.i(key, " key click"); + handle(clickAction); } else { up = true; } @@ -167,25 +172,29 @@ boolean handle(KeyEvent e) { worker = null; } else { worker = null; - if (longClickTime == time) handle(clickHandler); + if (longClickTime == time) { + Log.i(key, " key click"); + handle(clickAction); + } } return true; } case ACTION_MULTIPLE -> { - worker = null; - handle(dblClickHandler); + Log.i(key, " key double click"); + handle(dblClickAction); return true; } } return false; } - private void handle(Action.Handler h) { - h.handle(cb, activity, time); + private void handle(Action action) { + performAction(action, cb, activity, time); } private void sched(long delay) { + var handler = (activity == null) ? cb.getHandler() : activity.getHandler(); handler.postDelayed(this, delay); } } diff --git a/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngineBase.java b/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngineBase.java index 68112dd1..1ba1cb65 100644 --- a/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngineBase.java +++ b/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngineBase.java @@ -347,14 +347,12 @@ void stop(boolean pause) { } void sync(long position, float speed, boolean restart) { - if (sub != null) { - if (restart) { - sub.stop(false); - sub.start(position, getSubtitleDelay(), speed); - if (!isPlaying()) sub.stop(true); - } else { - sub.sync(position, getSubtitleDelay(), speed); - } + if (sub == null) return; + if (restart) { + sub.stop(false); + sub.start(position, getSubtitleDelay(), speed); + } else { + sub.sync(position, getSubtitleDelay(), speed); } } diff --git a/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngineManager.java b/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngineManager.java index b1f3a75e..cf39f39b 100644 --- a/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngineManager.java +++ b/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngineManager.java @@ -70,11 +70,15 @@ public MediaEngineManager(MediaLib lib) { setVlcPlayer(true); } - public void setEngineProvider(@NonNull MediaEngineProvider engineProvider) { + public boolean hasCustomEngineProvider() { + return engineProvider != null; + } + + public void setCustomEngineProvider(@NonNull MediaEngineProvider engineProvider) { this.engineProvider = engineProvider; } - public boolean removeEngineProvider(MediaEngineProvider engineProvider) { + public boolean removeCustomEngineProvider(MediaEngineProvider engineProvider) { if (this.engineProvider != engineProvider) return false; this.engineProvider = null; return true; diff --git a/fermata/src/main/java/me/aap/fermata/media/engine/MediaPlayerEngine.java b/fermata/src/main/java/me/aap/fermata/media/engine/MediaPlayerEngine.java index 4dcd74f6..193e8859 100644 --- a/fermata/src/main/java/me/aap/fermata/media/engine/MediaPlayerEngine.java +++ b/fermata/src/main/java/me/aap/fermata/media/engine/MediaPlayerEngine.java @@ -86,8 +86,8 @@ public void prepare(PlayableItem source) { @Override public void start() { player.start(); - listener.onEngineStarted(this); started(); + listener.onEngineStarted(this); } @Override diff --git a/fermata/src/main/java/me/aap/fermata/media/lib/IntentPlayable.java b/fermata/src/main/java/me/aap/fermata/media/lib/IntentPlayable.java index 6d41fa92..a0b0763a 100644 --- a/fermata/src/main/java/me/aap/fermata/media/lib/IntentPlayable.java +++ b/fermata/src/main/java/me/aap/fermata/media/lib/IntentPlayable.java @@ -16,7 +16,7 @@ * @author Andrey Pavlenko */ public class IntentPlayable extends PlayableItemBase { - private boolean video; + private final boolean video; public IntentPlayable(MainActivityDelegate a, Uri u) { super("intent://" + md5(u.toString()), new ExtRoot("intent_root", null) { @@ -27,7 +27,7 @@ public MediaLib getLib() { } }, GenericFileSystem.getInstance().create(Rid.create(u))); String mime = a.getContext().getContentResolver().getType(u); - video = (mime != null) && mime.startsWith("video/"); + video = (mime == null) || mime.startsWith("video/"); } @NonNull diff --git a/fermata/src/main/java/me/aap/fermata/media/service/FermataMediaService.java b/fermata/src/main/java/me/aap/fermata/media/service/FermataMediaService.java index 42d23e76..1eaac9f1 100644 --- a/fermata/src/main/java/me/aap/fermata/media/service/FermataMediaService.java +++ b/fermata/src/main/java/me/aap/fermata/media/service/FermataMediaService.java @@ -1,11 +1,14 @@ package me.aap.fermata.media.service; +import static android.Manifest.permission.POST_NOTIFICATIONS; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static java.util.Objects.requireNonNull; import static me.aap.fermata.media.service.ControlServiceConnection.ACTION_CONTROL_SERVICE; import static me.aap.utils.misc.MiscUtils.isPackageInstalled; +import android.Manifest; import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationChannel; @@ -15,6 +18,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; @@ -33,9 +37,11 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.Action; import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; import androidx.media.MediaBrowserServiceCompat; import androidx.media.app.NotificationCompat.MediaStyle; import androidx.media.session.MediaButtonReceiver; @@ -76,7 +82,8 @@ public class FermataMediaService extends MediaBrowserServiceCompat implements Sh private static final String INTENT_NEXT = "me.aap.fermata.action.next"; private static final String INTENT_FAVORITE_ADD = "me.aap.fermata.action.favorite.add"; private static final String INTENT_FAVORITE_REMOVE = "me.aap.fermata.action.favorite.remove"; - private static final String EXTRA_MEDIA_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED"; + private static final String EXTRA_MEDIA_SEARCH_SUPPORTED = + "android.media.browse.SEARCH_SUPPORTED"; private static final int NOTIF_ID = 1; private static final String NOTIF_CHANNEL_ID = "Fermata"; private DefaultMediaLib lib; @@ -115,9 +122,10 @@ public void onCreate() { FermataApplication.get().getHandler()); session.setCallback(callback); - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null, ctx, - MediaButtonReceiver.class); - session.setMediaButtonReceiver(PendingIntent.getBroadcast(ctx, 0, mediaButtonIntent, FLAG_IMMUTABLE)); + Intent mediaButtonIntent = + new Intent(Intent.ACTION_MEDIA_BUTTON, null, ctx, MediaButtonReceiver.class); + session.setMediaButtonReceiver( + PendingIntent.getBroadcast(ctx, 0, mediaButtonIntent, FLAG_IMMUTABLE)); notifColor = Color.parseColor(DEFAULT_NOTIF_COLOR); App.get().getScheduler().schedule(lib::cleanUpPrefs, 1, TimeUnit.HOURS); Log.d("FermataMediaService created"); @@ -181,7 +189,8 @@ private IBinder connectToControl() { } @Override - public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) { + public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, + Bundle rootHints) { Bundle extras = new Bundle(); extras.putBoolean(EXTRA_MEDIA_SEARCH_SUPPORTED, true); extras.putBoolean(CONTENT_STYLE_SUPPORTED, true); @@ -196,7 +205,8 @@ public void onConfigurationChanged(Configuration newConfig) { } @Override - public void onLoadChildren(@NonNull String parentMediaId, @NonNull Result> result) { + public void onLoadChildren(@NonNull String parentMediaId, + @NonNull Result> result) { getLib().getChildren(parentMediaId, result); } @@ -206,7 +216,8 @@ public void onLoadItem(String itemId, @NonNull Result result) { } @Override - public void onSearch(@NonNull String query, Bundle extras, @NonNull Result> result) { + public void onSearch(@NonNull String query, Bundle extras, + @NonNull Result> result) { getLib().search(query, result); } @@ -232,6 +243,9 @@ void updateNotification(int st, PlayableItem currentItem) { stopForeground(true); break; case PlaybackStateCompat.STATE_PAUSED: + if (ActivityCompat.checkSelfPermission(this, POST_NOTIFICATIONS) != PERMISSION_GRANTED) { + return; + } NotificationManagerCompat.from(this).notify(NOTIF_ID, createNotification(st, currentItem)); stopForeground(false); break; @@ -249,22 +263,17 @@ private Notification createNotification(int st, PlayableItem i) { Context ctx = this; MediaControllerCompat controller = session.getController(); MediaMetadataCompat mediaMetadata = controller.getMetadata(); - NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, NOTIF_CHANNEL_ID) - .setContentIntent(notifContentIntent) - .setDeleteIntent(pi(INTENT_STOP)) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setStyle(notifStyle) - .setSmallIcon(R.drawable.notification) - .setColor(notifColor) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setShowWhen(false) - .setOnlyAlertOnce(true); + NotificationCompat.Builder builder = + new NotificationCompat.Builder(ctx, NOTIF_CHANNEL_ID).setContentIntent(notifContentIntent) + .setDeleteIntent(pi(INTENT_STOP)).setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setStyle(notifStyle).setSmallIcon(R.drawable.notification).setColor(notifColor) + .setPriority(NotificationCompat.PRIORITY_HIGH).setShowWhen(false) + .setOnlyAlertOnce(true); if (mediaMetadata != null) { MediaDescriptionCompat description = mediaMetadata.getDescription(); Bitmap largeIcon = description.getIconBitmap(); - builder.setContentTitle(description.getTitle()) - .setContentText(description.getSubtitle()) + builder.setContentTitle(description.getTitle()).setContentText(description.getSubtitle()) .setSubText(description.getDescription()); if (callback.isDefaultImage(largeIcon)) { @@ -280,12 +289,9 @@ private Notification createNotification(int st, PlayableItem i) { builder.setLargeIcon(largeIcon); } - builder - .addAction(actionPrev) - .addAction(actionRw) + builder.addAction(actionPrev).addAction(actionRw) .addAction((st == PlaybackStateCompat.STATE_PLAYING) ? actionPause : actionPlay) - .addAction(actionFf) - .addAction(actionNext) + .addAction(actionFf).addAction(actionNext) .addAction(((i != null) && i.isFavoriteItem()) ? actionFavRm : actionFavAdd); return builder.build(); @@ -309,7 +315,8 @@ public void notificationInit() { try { Intent i = new Intent(this, Class.forName("me.aap.fermata.ui.activity.MainActivity")); - notifContentIntent = PendingIntent.getActivity(this, 0, i, FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT); + notifContentIntent = + PendingIntent.getActivity(this, 0, i, FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT); } catch (ClassNotFoundException ex) { Log.e(ex); notifContentIntent = session.getController().getSessionActivity(); @@ -321,20 +328,22 @@ public void notificationInit() { actionPlay = new Action(R.drawable.play, getString(R.string.play), pi(INTENT_PLAY)); actionFf = new Action(R.drawable.ff, getString(R.string.fast_forward), pi(INTENT_FF)); actionNext = new Action(R.drawable.next, getString(R.string.next), pi(INTENT_NEXT)); - actionFavAdd = new Action(R.drawable.favorite, getString(R.string.favorites_add), - pi(INTENT_FAVORITE_ADD)); + actionFavAdd = + new Action(R.drawable.favorite, getString(R.string.favorites_add), + pi(INTENT_FAVORITE_ADD)); actionFavRm = new Action(R.drawable.favorite_filled, getString(R.string.favorites_remove), pi(INTENT_FAVORITE_REMOVE)); notifStyle = new MediaStyle().setShowActionsInCompactView(0, 2, 4).setShowCancelButton(true) .setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this, - PlaybackStateCompat.ACTION_STOP)) - .setMediaSession(session.getSessionToken()); + PlaybackStateCompat.ACTION_STOP)).setMediaSession(session.getSessionToken()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel nc = new NotificationChannel(NOTIF_CHANNEL_ID, - getString(R.string.media_service_name), NotificationManager.IMPORTANCE_LOW); - NotificationManager nmgr = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + NotificationChannel nc = + new NotificationChannel(NOTIF_CHANNEL_ID, getString(R.string.media_service_name), + NotificationManager.IMPORTANCE_LOW); + NotificationManager nmgr = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); if (nmgr != null) nmgr.createNotificationChannel(nc); } @@ -387,7 +396,7 @@ public void onReceive(Context context, Intent intent) { filter.addAction(INTENT_FAVORITE_ADD); filter.addAction(INTENT_FAVORITE_REMOVE); - registerReceiver(intentReceiver, filter); + ContextCompat.registerReceiver(this, intentReceiver, filter, RECEIVER_NOT_EXPORTED); } private PendingIntent pi(String action) { diff --git a/fermata/src/main/java/me/aap/fermata/media/service/MediaSessionCallback.java b/fermata/src/main/java/me/aap/fermata/media/service/MediaSessionCallback.java index 459c13cf..a43c6461 100644 --- a/fermata/src/main/java/me/aap/fermata/media/service/MediaSessionCallback.java +++ b/fermata/src/main/java/me/aap/fermata/media/service/MediaSessionCallback.java @@ -33,6 +33,7 @@ import static android.support.v4.media.session.PlaybackStateCompat.STATE_REWINDING; import static android.support.v4.media.session.PlaybackStateCompat.STATE_SKIPPING_TO_NEXT; import static android.support.v4.media.session.PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS; +import static me.aap.fermata.action.KeyEventHandler.handleKeyEvent; import static me.aap.fermata.media.engine.MediaEngine.NO_SUBTITLES; import static me.aap.fermata.media.pref.MediaPrefs.AE_ENABLED; import static me.aap.fermata.media.pref.MediaPrefs.BASS_ENABLED; @@ -96,7 +97,6 @@ import me.aap.fermata.FermataApplication; import me.aap.fermata.R; -import me.aap.fermata.action.KeyEventHandler; import me.aap.fermata.media.engine.AudioEffects; import me.aap.fermata.media.engine.MediaEngine; import me.aap.fermata.media.engine.MediaEngineManager; @@ -144,7 +144,6 @@ public class MediaSessionCallback extends MediaSessionCompat.Callback private final MediaSessionCompat session; private final PlaybackControlPrefs playbackControlPrefs; private final Handler handler; - private final KeyEventHandler keyHandler; private final AudioManager audioManager; private final AudioFocusRequestCompat audioFocusReq; private final PlaybackStateCompat.CustomAction customRewind; @@ -175,7 +174,6 @@ public MediaSessionCallback(FermataMediaService service, MediaSessionCompat sess this.session = session; this.playbackControlPrefs = playbackControlPrefs; this.handler = handler; - keyHandler = new KeyEventHandler(this); Context ctx = lib.getContext(); customRewind = new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_RW, @@ -331,16 +329,20 @@ public MediaSessionCallbackAssistant getAssistant() { return (w == null) ? this : w.obj; } - public void setEngineProvider(@NonNull MediaEngineProvider engineProvider) { - getEngineManager().setEngineProvider(engineProvider); + public boolean hasCustomEngineProvider() { + return getEngineManager().hasCustomEngineProvider(); + } + + public void setCustomEngineProvider(@NonNull MediaEngineProvider engineProvider) { + getEngineManager().setCustomEngineProvider(engineProvider); if (getEngine() != null) { if (isPlaying()) onStop(true).onSuccess(v -> handler.post(this::play)); else onStop(); } } - public void removeEngineProvider(MediaEngineProvider engineProvider) { - if (getEngineManager().removeEngineProvider(engineProvider)) { + public void removeCustomEngineProvider(MediaEngineProvider engineProvider) { + if (getEngineManager().removeCustomEngineProvider(engineProvider)) { if (isPlaying()) onStop(true).onSuccess(v -> handler.post(this::play)); else onStop(); } @@ -377,7 +379,7 @@ private static boolean removeFromQueue(Queue> q, T t) { public boolean onMediaButtonEvent(Intent mediaButtonEvent) { KeyEvent e = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); if (e == null) return super.onMediaButtonEvent(mediaButtonEvent); - return keyHandler.handle(e, (i, ke) -> super.onMediaButtonEvent(mediaButtonEvent)); + return handleKeyEvent(this, e, (i, ke) -> super.onMediaButtonEvent(mediaButtonEvent)); } public void close() { @@ -959,10 +961,16 @@ public void onEngineEnded(MediaEngine engine) { playerTask = engineEnded(engine); } - private FutureSupplier engineEnded(MediaEngine engine) { + private FutureSupplier engineEnded(MediaEngine engine) { PlayableItem i = engine.getSource(); if (i != null) { + if (i instanceof StreamItem) { + Log.w("Failed to play stream? Retrying ", i); + playItem(i, 0); + return playerTask; + } + if (i.isVideo()) i.getPrefs().setWatchedPref(true); if (!i.getParent().getPrefs().getPlayNextPref()) { diff --git a/fermata/src/main/java/me/aap/fermata/media/sub/SubScheduler.java b/fermata/src/main/java/me/aap/fermata/media/sub/SubScheduler.java index f1fbc8d0..554caf53 100644 --- a/fermata/src/main/java/me/aap/fermata/media/sub/SubScheduler.java +++ b/fermata/src/main/java/me/aap/fermata/media/sub/SubScheduler.java @@ -1,5 +1,7 @@ package me.aap.fermata.media.sub; +import static me.aap.utils.function.Cancellable.CANCELED; + import java.util.ArrayList; import me.aap.fermata.media.sub.SubGrid.Position; @@ -42,12 +44,13 @@ public void start(long time, int delay, float speed) { if (started) return; started = true; sync(time, delay, speed); - for (var w : workers) w.run(); + assert started; } public void stop(boolean pause) { started = false; for (var w : workers) w.stop(pause); + assert !started; } public boolean isStarted() { @@ -55,15 +58,19 @@ public boolean isStarted() { } public void sync(long time, int delay, float speed) { + if (!started) return; this.time = time + delay; this.speed = speed; syncTime = System.currentTimeMillis(); + for (var w : workers) { + if (!w.isStarted()) w.start(); + } } private final class Worker implements Runnable { private final Position pos; private final Subtitles subtitles; - private Cancellable sched = Cancellable.CANCELED; + private Cancellable sched = CANCELED; Worker(Position pos, Subtitles subtitles) { @@ -73,6 +80,7 @@ private final class Worker implements Runnable { @Override public void run() { + assert started; assert !sched.cancel(); long time = time(); Subtitles.Text text = subtitles.getNext(time); @@ -86,25 +94,38 @@ public void run() { if (delay > 500) { consumer.accept(pos, null); - sched = executor.schedule(this, delay(delay)); + sched(delay); } else { consumer.accept(pos, text); - sched = executor.schedule(this, delay(text.getDuration() + delay)); + sched(text.getDuration() + delay); } } + void start() { + assert !sched.cancel(); + sched = executor.submit(this); + } + void stop(boolean pause) { if (!pause) consumer.accept(pos, null); sched.cancel(); - sched = Cancellable.CANCELED; + sched = CANCELED; + } + + boolean isStarted() { + return sched != CANCELED; } private long time() { return time + (long) (speed * (System.currentTimeMillis() - syncTime)); } - private long delay(long delay) { - return (long) (delay / speed); + + private void sched(long delay) { + if (!started) return; + delay /= speed; + if (delay > 0) sched = executor.schedule(this, delay); + else sched = CANCELED; } } } diff --git a/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityDelegate.java b/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityDelegate.java index 392434fe..628ff54b 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityDelegate.java +++ b/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityDelegate.java @@ -12,6 +12,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; import static java.nio.charset.StandardCharsets.US_ASCII; import static me.aap.fermata.BuildConfig.AUTO; +import static me.aap.fermata.action.KeyEventHandler.handleKeyEvent; import static me.aap.fermata.ui.activity.MainActivityPrefs.BRIGHTNESS; import static me.aap.fermata.ui.activity.MainActivityPrefs.CHANGE_BRIGHTNESS; import static me.aap.fermata.ui.activity.MainActivityPrefs.CLOCK_POS; @@ -71,6 +72,7 @@ import com.google.android.material.textview.MaterialTextView; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; @@ -81,7 +83,6 @@ import me.aap.fermata.R; import me.aap.fermata.action.Action; import me.aap.fermata.action.Key; -import me.aap.fermata.action.KeyEventHandler; import me.aap.fermata.addon.AddonManager; import me.aap.fermata.addon.FermataActivityAddon; import me.aap.fermata.addon.FermataAddon; @@ -149,7 +150,6 @@ public class MainActivityDelegate extends ActivityDelegate private final HandlerExecutor handler = new HandlerExecutor(App.get().getHandler().getLooper()); private final NavBarMediator navBarMediator = new NavBarMediator(); private final FermataServiceUiBinder mediaServiceBinder; - private final KeyEventHandler keyHandler; private ToolBarView toolBar; private NavBarView navBar; private BodyLayout body; @@ -166,7 +166,6 @@ public class MainActivityDelegate extends ActivityDelegate public MainActivityDelegate(AppActivity activity, FermataServiceUiBinder binder) { super(activity); mediaServiceBinder = binder; - keyHandler = new KeyEventHandler(this); } @NonNull @@ -956,18 +955,23 @@ private int getLayout() { } private static String[] getRequiredPermissions() { + List perms = new ArrayList<>(); + perms.add(permission.READ_EXTERNAL_STORAGE); + if (VERSION.SDK_INT >= VERSION_CODES.P) { + perms.add(permission.FOREGROUND_SERVICE); + } + if (VERSION.SDK_INT >= VERSION_CODES.Q) { + perms.add(permission.ACCESS_MEDIA_LOCATION); + perms.add(permission.USE_FULL_SCREEN_INTENT); + } if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { - return new String[]{permission.READ_MEDIA_AUDIO, permission.READ_MEDIA_VIDEO, - permission.FOREGROUND_SERVICE, permission.ACCESS_MEDIA_LOCATION, - permission.USE_FULL_SCREEN_INTENT}; - } else if (VERSION.SDK_INT >= VERSION_CODES.Q) { - return new String[]{permission.READ_EXTERNAL_STORAGE, permission.FOREGROUND_SERVICE, - permission.ACCESS_MEDIA_LOCATION, permission.USE_FULL_SCREEN_INTENT}; - } else if (VERSION.SDK_INT == VERSION_CODES.P) { - return new String[]{permission.READ_EXTERNAL_STORAGE, permission.FOREGROUND_SERVICE}; - } else { - return new String[]{permission.READ_EXTERNAL_STORAGE}; + perms.add(permission.USE_FULL_SCREEN_INTENT); + perms.add(permission.POST_NOTIFICATIONS); } + if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) { + perms.add(permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK); + } + return perms.toArray(new String[0]); } @Override @@ -1016,18 +1020,18 @@ public void onPreferenceChanged(PreferenceStore store, List next) { - return keyHandler.handle(event, next); + return handleKeyEvent(this, event, next); } @Override public boolean onKeyUp(int code, KeyEvent event, IntObjectFunction next) { - return keyHandler.handle(event, next); + return handleKeyEvent(this, event, next); } @Override public boolean onKeyLongPress(int code, KeyEvent event, IntObjectFunction next) { - return keyHandler.handle(event, next); + return handleKeyEvent(this, event, next); } public HandlerExecutor getHandler() { @@ -1063,6 +1067,10 @@ static final class Prefs implements MainActivityPrefs { private final SharedPreferences prefs = FermataApplication.get().getDefaultSharedPreferences(); private Prefs() { + App.get().getHandler().post(this::migratePrefs); + } + + private void migratePrefs() { // Rename old prefs var oldTheme = Pref.i("THEME", THEME_DARK); var oldScale = Pref.f("MEDIA_ITEM_SCALE", 1f); diff --git a/fermata/src/main/java/me/aap/fermata/ui/fragment/FoldersFragment.java b/fermata/src/main/java/me/aap/fermata/ui/fragment/FoldersFragment.java index 0ec627c7..888a3ad4 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/fragment/FoldersFragment.java +++ b/fermata/src/main/java/me/aap/fermata/ui/fragment/FoldersFragment.java @@ -1,6 +1,7 @@ package me.aap.fermata.ui.fragment; import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; +import static android.os.Build.VERSION.SDK_INT; import static me.aap.fermata.BuildConfig.ENABLE_GS; import static me.aap.fermata.util.Utils.isSafSupported; import static me.aap.fermata.vfs.FermataVfsManager.GDRIVE_ID; @@ -10,9 +11,11 @@ import static me.aap.utils.async.Completed.completed; import static me.aap.utils.function.ResultConsumer.Cancel.isCancellation; +import android.Manifest; import android.content.ActivityNotFoundException; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.view.View; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -127,8 +130,15 @@ public void addFolder() { menu.show(b -> { b.setTitle(R.string.add_folder); b.setSelectionHandler(this::addFolder); - b.addItem(R.id.vfs_file_system, R.string.vfs_file_system); - if (isSafSupported(a)) b.addItem(R.id.vfs_content, R.string.vfs_content); + if (isSafSupported(a)) { + if ((SDK_INT < Build.VERSION_CODES.TIRAMISU) || + App.get().hasManifestPermission(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + b.addItem(R.id.vfs_file_system, R.string.vfs_file_system); + } + b.addItem(R.id.vfs_content, R.string.vfs_content); + } else { + b.addItem(R.id.vfs_file_system, R.string.vfs_file_system); + } b.addItem(R.id.vfs_sftp, R.string.vfs_sftp); b.addItem(R.id.vfs_smb, R.string.vfs_smb); if (ENABLE_GS) b.addItem(R.id.vfs_gdrive, R.string.vfs_gdrive); @@ -168,8 +178,8 @@ private void addFolderIntent() { .onSuccess(this::addFolderResult); } catch (ActivityNotFoundException ex) { String msg = ex.getLocalizedMessage(); - UiUtils.showAlert(getContext(), getString(R.string.err_failed_add_folder, - (msg != null) ? msg : ex.toString())); + UiUtils.showAlert(getContext(), + getString(R.string.err_failed_add_folder, (msg != null) ? msg : ex.toString())); } } @@ -191,11 +201,8 @@ public boolean canScrollUp() { private void addFolderVfs(String provId, @StringRes int name) { FermataVfsManager mgr = getLib().getVfsManager(); - mgr.getProvider(provId) - .then(p -> p.select(getMainActivity(), mgr.getFileSystems(provId))) - .main() - .onFailure(fail -> failedToLoadModule(name, fail)) - .onSuccess(this::addFolderResult); + mgr.getProvider(provId).then(p -> p.select(getMainActivity(), mgr.getFileSystems(provId))) + .main().onFailure(fail -> failedToLoadModule(name, fail)).onSuccess(this::addFolderResult); } private void failedToLoadModule(@StringRes int name, Throwable ex) { @@ -210,8 +217,8 @@ private void failedToLoadModule(@StringRes int name, Throwable ex) { UiUtils.showAlert(getContext(), getString(R.string.err_failed_install_module, n)); } else { String msg = ex.getLocalizedMessage(); - UiUtils.showAlert(getContext(), getString(R.string.err_failed_add_folder, - (msg != null) ? msg : ex.toString())); + UiUtils.showAlert(getContext(), + getString(R.string.err_failed_add_folder, (msg != null) ? msg : ex.toString())); } }); } @@ -301,7 +308,8 @@ private void animateAddButton(BrowsableItem parent) { FloatingButton fb = getMainActivity().getFloatingButton(); fb.requestFocus(); - Animation shake = AnimationUtils.loadAnimation(getContext(), me.aap.utils.R.anim.shake_y_20); + Animation shake = + AnimationUtils.loadAnimation(getContext(), me.aap.utils.R.anim.shake_y_20); fb.startAnimation(shake); }); } diff --git a/fermata/src/main/java/me/aap/fermata/ui/fragment/MediaLibFragment.java b/fermata/src/main/java/me/aap/fermata/ui/fragment/MediaLibFragment.java index 7a25034d..26c85a51 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/fragment/MediaLibFragment.java +++ b/fermata/src/main/java/me/aap/fermata/ui/fragment/MediaLibFragment.java @@ -625,6 +625,14 @@ public void openEpg(StreamItem i) { private void onClick(PlayableItem i) { MainActivityDelegate a = getMainActivity(); + + if (i.isVideo() && !a.getBody().getVideoView().isSurfaceCreated() && + !a.getMediaSessionCallback().hasCustomEngineProvider()) { + a.getBody().setMode(BodyLayout.Mode.VIDEO); + a.getBody().getVideoView().onSurfaceCreated(() -> onClick(i)); + return; + } + FermataServiceUiBinder b = a.getMediaServiceBinder(); PlayableItem cur = b.getCurrentItem(); b.playItem(i); diff --git a/fermata/src/main/java/me/aap/fermata/ui/view/VideoView.java b/fermata/src/main/java/me/aap/fermata/ui/view/VideoView.java index d98e268d..92cb1269 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/view/VideoView.java +++ b/fermata/src/main/java/me/aap/fermata/ui/view/VideoView.java @@ -18,6 +18,7 @@ import static me.aap.fermata.media.pref.MediaPrefs.SCALE_FILL; import static me.aap.fermata.media.pref.MediaPrefs.SCALE_ORIGINAL; import static me.aap.fermata.media.sub.SubGrid.Position.BOTTOM_LEFT; +import static me.aap.utils.async.Completed.completedNull; import static me.aap.utils.ui.UiUtils.isVisible; import static me.aap.utils.ui.UiUtils.toIntPx; @@ -70,6 +71,7 @@ import me.aap.fermata.ui.activity.MainActivityListener; import me.aap.fermata.ui.activity.MainActivityPrefs; import me.aap.utils.async.FutureSupplier; +import me.aap.utils.async.Promise; import me.aap.utils.function.BiConsumer; import me.aap.utils.pref.PreferenceStore; import me.aap.utils.ui.view.NavBarView; @@ -83,7 +85,7 @@ public class VideoView extends FrameLayout private final Set> prefChange = new HashSet<>( Arrays.asList(MediaPrefs.VIDEO_SCALE, MediaPrefs.AUDIO_DELAY, MediaPrefs.SUB_DELAY)); private SubDrawer subDrawer; - private boolean surfaceCreated; + private FutureSupplier createSurface = new Promise<>(); public VideoView(Context context) { this(context, null); @@ -183,22 +185,15 @@ public VideoInfoView getVideoInfoView() { } public void showVideo(boolean hideTitle) { - if (surfaceCreated) { + createSurface.onSuccess(v -> { MainActivityDelegate a = getActivity().peek(); if (a == null) return; MediaSessionCallback cb = a.getMediaSessionCallback(); MediaEngine eng = cb.getEngine(); - if (eng == null) return; - - PlayableItem i = eng.getSource(); - if ((i == null) || !i.isVideo()) return; - - setSurfaceSize(eng); - cb.addVideoView(this, a.isCarActivity() ? 0 : 1); - + if (eng != null) setSurfaceSize(eng); VideoInfoView info = getVideoInfoView(); if (hideTitle && (info != null)) info.setVisibility(GONE); - } + }); } public void prepareSubDrawer(boolean dbl) { @@ -221,18 +216,19 @@ public void releaseSubDrawer() { public void accept(SubGrid.Position position, @Nullable Subtitles.Text text) { if (subDrawer == null) return; if (!subDrawer.setText(position, text)) return; - if (!surfaceCreated) return; - SurfaceView sv = getSubtitleSurface(); - if (sv == null) return; - - var h = sv.getHolder(); - var c = h.lockCanvas(); - try { - subDrawer.clr(c); - subDrawer.draw(c); - } finally { - h.unlockCanvasAndPost(c); - } + createSurface.onSuccess(v -> { + SurfaceView sv = getSubtitleSurface(); + if (sv == null) return; + + var h = sv.getHolder(); + var c = h.lockCanvas(); + try { + subDrawer.clr(c); + subDrawer.draw(c); + } finally { + h.unlockCanvasAndPost(c); + } + }); } public void setSurfaceSize(MediaEngine eng) { @@ -298,9 +294,7 @@ public void setSurfaceSize(MediaEngine eng) { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { - FermataApplication.get().getHandler().post(() -> { - if (!surfaceCreated) return; - + FermataApplication.get().getHandler().post(() -> createSurface.onSuccess(s -> { MainActivityDelegate a = getActivity().peek(); if (a == null) return; MediaEngine eng = a.getMediaServiceBinder().getCurrentEngine(); @@ -308,7 +302,7 @@ public void onLayoutChange(View v, int left, int top, int right, int bottom, int PlayableItem i = eng.getSource(); if ((i != null) && i.isVideo()) setSurfaceSize(eng); - }); + })); } @Override @@ -316,16 +310,28 @@ public void surfaceCreated(@NonNull SurfaceHolder holder) { if (!getVideoSurface().getHolder().getSurface().isValid()) return; SurfaceView s = getSubtitleSurface(); if ((s != null) && !s.getHolder().getSurface().isValid()) return; - surfaceCreated = true; - showVideo(true); + getActivity().onSuccess( + a -> a.getMediaSessionCallback().addVideoView(this, a.isCarActivity() ? 0 : 1)); + if (createSurface instanceof Promise p) { + createSurface = completedNull(); + p.complete(null); + } } @Override public void surfaceDestroyed(@NonNull SurfaceHolder holder) { - surfaceCreated = false; + createSurface = new Promise<>(); getActivity().onSuccess(a -> a.getMediaSessionCallback().removeVideoView(this)); } + public boolean isSurfaceCreated() { + return createSurface.isDone(); + } + + public void onSurfaceCreated(Runnable run) { + createSurface.onSuccess(v -> run.run()); + } + @Override public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { } @@ -403,7 +409,7 @@ private boolean onTouch(@NonNull MotionEvent e) { @Override public void onPreferenceChanged(PreferenceStore store, List> prefs) { - if (surfaceCreated && !Collections.disjoint(prefChange, prefs)) { + if (createSurface.isDone() && !Collections.disjoint(prefChange, prefs)) { MainActivityDelegate a = getActivity().peek(); if (a == null) return; MediaEngine eng = a.getMediaSessionCallback().getEngine(); diff --git a/fermata/src/main/java/me/aap/fermata/util/Utils.java b/fermata/src/main/java/me/aap/fermata/util/Utils.java index 6d83dfb1..0e7baad6 100644 --- a/fermata/src/main/java/me/aap/fermata/util/Utils.java +++ b/fermata/src/main/java/me/aap/fermata/util/Utils.java @@ -1,6 +1,5 @@ package me.aap.fermata.util; -import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.os.Build.VERSION.SDK_INT; import android.content.ActivityNotFoundException; @@ -25,6 +24,7 @@ import me.aap.utils.app.App; import me.aap.utils.io.FileUtils; import me.aap.utils.log.Log; +import me.aap.utils.misc.MiscUtils; import me.aap.utils.net.http.HttpFileDownloader; import me.aap.utils.ui.activity.ActivityDelegate; import me.aap.utils.ui.notif.HttpDownloadStatusListener; @@ -70,10 +70,7 @@ public static Context dynCtx(Context ctx) { public static boolean isSafSupported(@Nullable MainActivityDelegate a) { if ((a != null) && a.isCarActivity()) return false; - Context ctx = (a == null) ? App.get() : a.getContext(); - if (ctx.getPackageManager().hasSystemFeature(FEATURE_LEANBACK)) return false; - Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - return i.resolveActivity(ctx.getPackageManager()) != null; + return MiscUtils.isSafSupported(); } public static boolean isExternalStorageManager() { diff --git a/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFile.java b/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFile.java index 00eff67a..b5afb95c 100644 --- a/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFile.java +++ b/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFile.java @@ -11,6 +11,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -135,6 +136,12 @@ public FutureSupplier getLength() { @Override public AsyncInputStream getInputStream() throws IOException { + String url = getUrl(); + + if (url.startsWith("content://")) { + return AsyncInputStream.from(App.get().getContentResolver().openInputStream(Uri.parse(url))); + } + return LocalFileSystem.getInstance().getFile(getLocalFile()).getInputStream(); } @@ -167,12 +174,8 @@ public int hashCode() { @NonNull @Override public String toString() { - return getClass().getSimpleName() + " {" + - "rid=" + rid + - ", name='" + getName() + '\'' + - ", url='" + getUrl() + '\'' + - ", localFile=" + getLocalFile() + - '}'; + return getClass().getSimpleName() + " {" + "rid=" + rid + ", name='" + getName() + '\'' + + ", url='" + getUrl() + '\'' + ", localFile=" + getLocalFile() + '}'; } @NonNull diff --git a/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFileSystem.java b/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFileSystem.java index 5fb8ded2..50e525b3 100644 --- a/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFileSystem.java +++ b/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFileSystem.java @@ -82,7 +82,7 @@ protected FutureSupplier load(M3uFile file) { return completedNull(); } - if (url.startsWith("/")) { + if (url.startsWith("/") || url.startsWith("content://")) { p.complete(file); return p; } diff --git a/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFileSystemProvider.java b/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFileSystemProvider.java index e11ef5c8..145a23ab 100644 --- a/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFileSystemProvider.java +++ b/fermata/src/main/java/me/aap/fermata/vfs/m3u/M3uFileSystemProvider.java @@ -88,6 +88,7 @@ protected boolean validate(PreferenceStore ps) { String u = ps.getStringPref(URL); if (u.startsWith("http://")) return u.length() > 7; if (u.startsWith("https://")) return u.length() > 8; + if (u.startsWith("content://")) return u.length() > 10; if (u.startsWith("/")) return new File(u).isFile(); return false; } diff --git a/fermata/src/main/res/layout/video_info_layout.xml b/fermata/src/main/res/layout/video_info_layout.xml index 1ade60c7..713e1d48 100644 --- a/fermata/src/main/res/layout/video_info_layout.xml +++ b/fermata/src/main/res/layout/video_info_layout.xml @@ -9,13 +9,12 @@ android:layout_height="0dp" android:layout_marginLeft="5dp" android:layout_marginRight="5dp" - android:minWidth="60dp" - android:minHeight="60dp" android:scaleType="fitCenter" - app:layout_constraintBottom_toBottomOf="@id/media_item_dsc" + app:layout_constraintBottom_toTopOf="@id/media_item_dsc" app:layout_constraintDimensionRatio="1:1" + app:layout_constraintEnd_toStartOf="@id/vinfo_guideline" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@id/media_item_title" /> + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintStart_toEndOf="@id/vinfo_guideline" + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintStart_toEndOf="@id/vinfo_guideline" + app:layout_constraintTop_toBottomOf="@id/media_item_title" /> + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/vinfo_guideline" /> - \ No newline at end of file + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa3f74c3..f2124f8d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Jul 08 00:52:13 CEST 2023 +#Fri Oct 06 22:55:19 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/modules/cast/src/main/java/me/aap/fermata/addon/cast/CastAddon.java b/modules/cast/src/main/java/me/aap/fermata/addon/cast/CastAddon.java index ae598587..72e6978f 100644 --- a/modules/cast/src/main/java/me/aap/fermata/addon/cast/CastAddon.java +++ b/modules/cast/src/main/java/me/aap/fermata/addon/cast/CastAddon.java @@ -138,7 +138,7 @@ public void onSessionResuming(@NonNull CastSession session, @NonNull String s) { @Override public void onSessionEnding(@NonNull CastSession castSession) { if ((cb == null) || (engProvider == null)) return; - cb.removeEngineProvider(engProvider); + cb.removeCustomEngineProvider(engProvider); } @Override @@ -172,13 +172,13 @@ private void connected(CastSession session) { if (client == null) return; IoUtils.close(engProvider); engProvider = new CastMediaEngineProvider(session, client, cb.getMediaLib()); - cb.setEngineProvider(engProvider); + cb.setCustomEngineProvider(engProvider); } private void disconnected() { IoUtils.close(engProvider); if ((cb == null) || (engProvider == null)) return; - cb.removeEngineProvider(engProvider); + cb.removeCustomEngineProvider(engProvider); this.engProvider = null; } } diff --git a/modules/exoplayer/build.gradle b/modules/exoplayer/build.gradle index 10778c2c..e9c3e385 100644 --- a/modules/exoplayer/build.gradle +++ b/modules/exoplayer/build.gradle @@ -9,7 +9,6 @@ android { dependencies { implementation project(':utils') implementation project(':fermata') - implementation 'androidx.core:core:' + ANDROIDX_CORE_VERSION implementation project(':exoplayer-library-core') implementation project(':exoplayer-library-hls') implementation project(':exoplayer-extension-ffmpeg') diff --git a/modules/exoplayer/src/main/java/me/aap/fermata/engine/exoplayer/ExoPlayerEngine.java b/modules/exoplayer/src/main/java/me/aap/fermata/engine/exoplayer/ExoPlayerEngine.java index 0ca6b223..4bac918e 100644 --- a/modules/exoplayer/src/main/java/me/aap/fermata/engine/exoplayer/ExoPlayerEngine.java +++ b/modules/exoplayer/src/main/java/me/aap/fermata/engine/exoplayer/ExoPlayerEngine.java @@ -7,8 +7,6 @@ import android.content.Context; import android.net.Uri; -import androidx.annotation.NonNull; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayer; @@ -222,7 +220,7 @@ public void onVideoSizeChanged(VideoSize videoSize) { } @Override - public void onPlayerError(@NonNull PlaybackException error) { + public void onPlayerError(PlaybackException error) { listener.onEngineError(this, error); } } diff --git a/modules/felex/src/main/assets/Example.fxd b/modules/felex/src/main/assets/Example.fxd index ada5d3c0..e3fc8f4b 100644 --- a/modules/felex/src/main/assets/Example.fxd +++ b/modules/felex/src/main/assets/Example.fxd @@ -1,7 +1,6 @@ #Name Example #SourceLang de #TargetLang en -#Count 4 [das] Geld Money diff --git a/modules/felex/src/main/java/me/aap/fermata/addon/felex/dict/DictInfo.java b/modules/felex/src/main/java/me/aap/fermata/addon/felex/dict/DictInfo.java index 6aa1cb3a..be1df116 100644 --- a/modules/felex/src/main/java/me/aap/fermata/addon/felex/dict/DictInfo.java +++ b/modules/felex/src/main/java/me/aap/fermata/addon/felex/dict/DictInfo.java @@ -39,7 +39,7 @@ public static DictInfo read(InputStream in) throws IOException { String srcLang = null; String targetLang = null; - for (int i = r.readLine(sb); i != -1; sb.setLength(1024), i = r.readLine(sb)) { + for (int i = r.readLine(sb, 1024); i != -1; sb.setLength(0), i = r.readLine(sb)) { if ((sb.length() == 0) || sb.charAt(0) != '#') break; if (TextUtils.startsWith(sb, TAG_NAME)) name = sb.substring(TAG_NAME.length()).trim(); diff --git a/modules/vlc/build.gradle b/modules/vlc/build.gradle index 276a2923..755c9f58 100644 --- a/modules/vlc/build.gradle +++ b/modules/vlc/build.gradle @@ -9,7 +9,6 @@ android { dependencies { implementation project(':utils') implementation project(':fermata') - implementation 'androidx.core:core:' + ANDROIDX_CORE_VERSION implementation 'androidx.appcompat:appcompat:' + ANDROIDX_APPCOMPAT_VERSION - implementation 'org.videolan.android:libvlc-all:3.6.0-eap9' + implementation 'org.videolan.android:libvlc-all:3.6.0-eap10' } diff --git a/modules/vlc/src/main/java/me/aap/fermata/engine/vlc/VlcEngineProvider.java b/modules/vlc/src/main/java/me/aap/fermata/engine/vlc/VlcEngineProvider.java index 67f2177e..3ce4edaa 100644 --- a/modules/vlc/src/main/java/me/aap/fermata/engine/vlc/VlcEngineProvider.java +++ b/modules/vlc/src/main/java/me/aap/fermata/engine/vlc/VlcEngineProvider.java @@ -47,18 +47,15 @@ public void init(Context ctx) { audioSessionId = (am != null) ? am.generateAudioSessionId() : AudioManager.ERROR; if (BuildConfig.D) opts.add("-vvv"); - if (audioSessionId != AudioManager.ERROR) - opts.add("--audiotrack-session-id=" + audioSessionId); - - opts.add("--avcodec-skiploopfilter"); - opts.add("1"); - opts.add("--avcodec-skip-frame"); - opts.add("0"); + if (audioSessionId != AudioManager.ERROR) opts.add("--audiotrack-session-id=" + audioSessionId); opts.add("--avcodec-skip-idct"); opts.add("0"); - opts.add("--no-stats"); + opts.add("--avcodec-skip-frame"); + opts.add("0"); + opts.add("--avcodec-skiploopfilter"); + opts.add("1"); opts.add("--android-display-chroma"); - opts.add("RV16"); + opts.add("RV24"); opts.add("--sout-keep"); opts.add("--audio-time-stretch"); opts.add("--audio-resampler"); @@ -66,16 +63,18 @@ public void init(Context ctx) { opts.add("--subsdec-encoding=UTF8"); opts.add("--freetype-rel-fontsize=16"); opts.add("--freetype-color=16777215"); + opts.add("--freetype-opacity=255"); opts.add("--freetype-background-opacity=0"); - opts.add("--no-sout-chromecast-audio-passthrough"); - opts.add("--sout-chromecast-conversion-quality=2"); + opts.add("--freetype-shadow-color=0"); + opts.add("--freetype-shadow-opacity=128"); + opts.add("--freetype-outline-thickness=4"); + opts.add("--freetype-outline-color=0"); + opts.add("--freetype-outline-opacity=255"); + opts.add("--freetype-outline-opacity=255"); + opts.add("--freetype-rel-fontsize=16"); opts.add("--network-caching=60000"); - opts.add("--android-display-chroma"); - opts.add("--audio-resampler"); - opts.add("soxr"); -// opts.add("--aout=opensles,android_audiotrack"); -// opts.add("--vout=android_display"); -// opts.add("--vout=opengles2"); + opts.add("--no-lua"); + opts.add("--no-stats"); vlc = new LibVLC(ctx, opts); } @@ -96,7 +95,8 @@ public boolean getMediaMetadata(MetadataBuilder meta, PlayableItem item) { if ("content".equals(uri.getScheme())) { ContentResolver cr = getVlc().getAppContext().getContentResolver(); fd = cr.openFileDescriptor(uri, "r"); - media = (fd != null) ? new Media(getVlc(), fd.getFileDescriptor()) : new Media(getVlc(), uri); + media = + (fd != null) ? new Media(getVlc(), fd.getFileDescriptor()) : new Media(getVlc(), uri); } else { media = new Media(getVlc(), uri); } diff --git a/modules/web/build.gradle b/modules/web/build.gradle index 23dc7661..a0ac6f10 100644 --- a/modules/web/build.gradle +++ b/modules/web/build.gradle @@ -32,5 +32,5 @@ dependencies { implementation "com.google.android.material:material:${ANDROID_MATERIAL_VERSION}" implementation "androidx.constraintlayout:constraintlayout:${ANDROIDX_CONSTRAINTLAYOUT_VERSION}" implementation "androidx.swiperefreshlayout:swiperefreshlayout:${ANDROIDX_SWIPEREFRESHLAYOUT_VERSION}" - implementation 'androidx.webkit:webkit:1.7.0' + implementation 'androidx.webkit:webkit:1.8.0' } diff --git a/modules/web/src/main/java/me/aap/fermata/addon/web/WebBrowserFragment.java b/modules/web/src/main/java/me/aap/fermata/addon/web/WebBrowserFragment.java index dec70423..60763a8d 100644 --- a/modules/web/src/main/java/me/aap/fermata/addon/web/WebBrowserFragment.java +++ b/modules/web/src/main/java/me/aap/fermata/addon/web/WebBrowserFragment.java @@ -85,8 +85,11 @@ public void onDestroyView() { public void onRefresh(BooleanConsumer refreshing) { FermataWebView v = getWebView(); if (v != null) { - v.getWebViewClient().loading = refreshing; - v.reload(); + FermataWebClient c = v.getWebViewClient(); + if (c != null) { + c.loading = refreshing; + v.reload(); + } } }