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();
+ }
}
}