From 5842af6d0fd9318d267607be39f8618946b7c3e3 Mon Sep 17 00:00:00 2001 From: Andrey Pavlenko Date: Sun, 16 Jul 2023 20:25:52 +0200 Subject: [PATCH] Key binding settings --- build.gradle | 2 +- .../java/me/aap/fermata/action/Action.java | 142 ++++++ .../main/java/me/aap/fermata/action/Key.java | 148 ++++++ .../aap/fermata/action/KeyEventHandler.java | 180 ++++++++ .../aap/fermata/media/engine/MediaEngine.java | 3 +- .../fermata/media/engine/MediaEngineBase.java | 2 +- .../fermata/media/lib/DefaultMediaLib.java | 72 +-- .../me/aap/fermata/media/lib/MediaLib.java | 2 - .../media/pref/PlaybackControlPrefs.java | 51 ++- .../media/service/MediaSessionCallback.java | 421 ++++++++---------- .../ui/activity/MainActivityDelegate.java | 118 ++--- .../ui/activity/MainActivityPrefs.java | 56 +-- .../fermata/ui/fragment/SettingsFragment.java | 79 ++-- .../me/aap/fermata/ui/view/BodyLayout.java | 15 +- .../aap/fermata/ui/view/ControlPanelView.java | 9 +- .../me/aap/fermata/ui/view/SplitLayout.java | 3 +- fermata/src/main/res/values-de/strings.xml | 4 - fermata/src/main/res/values-it/strings.xml | 4 - fermata/src/main/res/values-ru/strings.xml | 26 +- fermata/src/main/res/values-tr/strings.xml | 4 - fermata/src/main/res/values/strings.xml | 27 +- .../fermata/addon/cast/CastMediaEngine.java | 35 +- 22 files changed, 878 insertions(+), 525 deletions(-) create mode 100644 fermata/src/main/java/me/aap/fermata/action/Action.java create mode 100644 fermata/src/main/java/me/aap/fermata/action/Key.java create mode 100644 fermata/src/main/java/me/aap/fermata/action/KeyEventHandler.java diff --git a/build.gradle b/build.gradle index 281451a1..bf437582 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ ext { def abi = project.properties['ABI'] - VERSION_CODE = 221 + VERSION_CODE = 225 VERSION_NAME = "1.9.3" SDK_MIN_VERSION = 23 SDK_TARGET_VERSION = 33 diff --git a/fermata/src/main/java/me/aap/fermata/action/Action.java b/fermata/src/main/java/me/aap/fermata/action/Action.java new file mode 100644 index 00000000..cae8df4e --- /dev/null +++ b/fermata/src/main/java/me/aap/fermata/action/Action.java @@ -0,0 +1,142 @@ +package me.aap.fermata.action; + +import static android.media.AudioManager.ADJUST_LOWER; +import static android.media.AudioManager.ADJUST_RAISE; +import static android.media.AudioManager.ADJUST_TOGGLE_MUTE; +import static android.media.AudioManager.FLAG_SHOW_UI; +import static android.media.AudioManager.STREAM_MUSIC; +import static android.os.SystemClock.uptimeMillis; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import android.content.Context; +import android.media.AudioManager; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import java.util.List; + +import me.aap.fermata.R; +import me.aap.fermata.media.service.MediaSessionCallback; +import me.aap.fermata.ui.activity.MainActivityDelegate; +import me.aap.utils.app.App; +import me.aap.utils.ui.activity.ActivityDelegate; + +/** + * @author Andrey Pavlenko + */ +public enum Action { + STOP(R.string.action_stop, m(MediaSessionCallback::onStop)), + PLAY(R.string.action_play, m(MediaSessionCallback::onPlay)), + PAUSE(R.string.action_pause, m(MediaSessionCallback::onPause)), + PLAY_PAUSE(R.string.action_play_pause, m(cb -> { + if (cb.isPlaying()) cb.onPause(); + else cb.onPlay(); + })), + PREV(R.string.action_prev, m(MediaSessionCallback::onSkipToPrevious)), + NEXT(R.string.action_next, m(MediaSessionCallback::onSkipToNext)), + RW(R.string.action_rw, new RwFfHandler(false)), + FF(R.string.action_ff, new RwFfHandler(true)), + VOLUME_UP(R.string.action_vol_up, new VolumeHandler(ADJUST_RAISE)), + VOLUME_DOWN(R.string.action_vol_down, new VolumeHandler(ADJUST_LOWER)), + VOLUME_MUTE_UNMUTE(R.string.action_vol_mute_unmute, new VolumeHandler(ADJUST_TOGGLE_MUTE)), + ACTIVATE_VOICE_CTRL(R.string.action_activate_voice_ctrl, + m(cb -> cb.getAssistant().startVoiceAssistant())), + MENU(R.string.action_menu, a(a -> a.getNavBarMediator().showMenu(a))), + CP_MENU(R.string.action_cp_menu, a(a -> { + var cp = a.getControlPanel(); + if (cp.isActive()) cp.showMenu(); + })), + BACK_OR_EXIT(R.string.action_back_or_exit, a(ActivityDelegate::onBackPressed)), + EXIT(R.string.action_exit, a(ActivityDelegate::finish)), + NONE(R.string.action_none, m(cb -> {})), + ; + + private static final List all = unmodifiableList(asList(values())); + + @StringRes + private final int name; + private final Action.Handler handler; + + Action(int name, Action.Handler handler) { + this.name = name; + this.handler = handler; + } + + @Nullable + public static Action get(int ordinal) { + return (ordinal >= 0) && (ordinal < all.size()) ? all.get(ordinal) : null; + } + + public static List getAll() { + return all; + } + + @StringRes + public int getName() { + return name; + } + + public Handler getHandler() { + return handler; + } + + private static Handler m(MediaHandler h) { + return h; + } + + private static Handler a(ActivityHandler h) { + return h; + } + + public interface Handler { + void handle(MediaSessionCallback cb, @Nullable MainActivityDelegate a, long timestamp); + } + + private interface MediaHandler extends Handler { + void handle(MediaSessionCallback cb); + + @Override + default void handle(MediaSessionCallback cb, @Nullable MainActivityDelegate a, + long timestamp) { + handle(cb); + } + } + + private interface ActivityHandler extends Handler { + void handle(MainActivityDelegate a); + + @Override + default void handle(MediaSessionCallback cb, @Nullable MainActivityDelegate a, + long timestamp) { + if (a != null) handle(a); + } + } + + private static final class VolumeHandler implements Handler { + private final int direction; + + VolumeHandler(int direction) {this.direction = direction;} + + @Override + public void handle(MediaSessionCallback cb, @Nullable MainActivityDelegate a, long timestamp) { + var eng = cb.getEngine(); + if ((eng != null) && eng.adjustVolume(direction)) return; + var ctx = (a == null) ? App.get() : a.getContext(); + var amgr = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE); + if (amgr != null) amgr.adjustStreamVolume(STREAM_MUSIC, direction, FLAG_SHOW_UI); + } + } + + private static final class RwFfHandler implements Handler { + private final boolean ff; + + private RwFfHandler(boolean ff) {this.ff = ff;} + + @Override + public void handle(MediaSessionCallback cb, @Nullable MainActivityDelegate a, long timestamp) { + cb.rewindFastForward(ff, (int) ((uptimeMillis() - timestamp) / 1000)); + } + } +} diff --git a/fermata/src/main/java/me/aap/fermata/action/Key.java b/fermata/src/main/java/me/aap/fermata/action/Key.java new file mode 100644 index 00000000..f4440791 --- /dev/null +++ b/fermata/src/main/java/me/aap/fermata/action/Key.java @@ -0,0 +1,148 @@ +package me.aap.fermata.action; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import android.view.KeyEvent; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import me.aap.fermata.ui.activity.MainActivityPrefs; +import me.aap.utils.function.IntSupplier; +import me.aap.utils.pref.PreferenceStore; + +/** + * @author Andrey Pavlenko + */ +public enum Key { + MEDIA_STOP(KeyEvent.KEYCODE_MEDIA_STOP, Action.STOP), + MEDIA_PLAY(KeyEvent.KEYCODE_MEDIA_PLAY, Action.PLAY), + MEDIA_PAUSE(KeyEvent.KEYCODE_MEDIA_PAUSE, Action.PAUSE), + MEDIA_PLAY_PAUSE(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, Action.PLAY_PAUSE), + MEDIA_PREVIOUS(KeyEvent.KEYCODE_MEDIA_PREVIOUS, Action.PREV), + MEDIA_NEXT(KeyEvent.KEYCODE_MEDIA_NEXT, Action.NEXT), + MEDIA_REWIND(KeyEvent.KEYCODE_MEDIA_REWIND, Action.RW, Action.RW, Action.RW), + MEDIA_FAST_FORWARD(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, Action.FF, Action.FF, Action.FF), + VOLUME_UP(KeyEvent.KEYCODE_VOLUME_UP, Action.VOLUME_UP), + VOLUME_DOWN(KeyEvent.KEYCODE_VOLUME_DOWN, Action.VOLUME_DOWN), + HEADSETHOOK(KeyEvent.KEYCODE_HEADSETHOOK, Action.PLAY_PAUSE, Action.NEXT, + Action.ACTIVATE_VOICE_CTRL), + SEARCH(KeyEvent.KEYCODE_SEARCH, Action.ACTIVATE_VOICE_CTRL), + BACK(KeyEvent.KEYCODE_BACK, Action.BACK_OR_EXIT), + ESCAPE(KeyEvent.KEYCODE_ESCAPE, Action.BACK_OR_EXIT), + DEL(KeyEvent.KEYCODE_DEL, Action.STOP), + MENU(KeyEvent.KEYCODE_MENU, Action.MENU, Action.CP_MENU, Action.CP_MENU), + M(KeyEvent.KEYCODE_M, Action.MENU, Action.CP_MENU, Action.CP_MENU), + P(KeyEvent.KEYCODE_P, Action.PLAY_PAUSE), + S(KeyEvent.KEYCODE_S, Action.STOP), + X(KeyEvent.KEYCODE_X, Action.EXIT); + + private static final Map keys = new HashMap<>(); + + private static final List all = unmodifiableList(asList(values())); + private static final PrefsListener listener = new PrefsListener(); + + static { + for (var k : all) keys.put(k.code, k); + MainActivityPrefs.get().addBroadcastListener(listener); + } + + private final int code; + private final boolean media; + private final PreferenceStore.Pref actionPref; + private final PreferenceStore.Pref dblActionPref; + private final PreferenceStore.Pref longActionPref; + @Nullable + private Action.Handler clickHandler; + @Nullable + private Action.Handler dblClickHandler; + @Nullable + private Action.Handler longClickHandler; + + Key(int code, Action action) { + this(code, action, action, action); + } + + Key(int code, Action action, Action dblAction, Action longAction) { + 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()) + .withInheritance(false); + longActionPref = PreferenceStore.Pref.i("KEY_ACTION_LONG_" + name, longAction.ordinal()) + .withInheritance(false); + } + + @Nullable + public static Key get(int code) { + return keys.get(code); + } + + public static List getAll() { + return all; + } + + public static PreferenceStore getPrefs() { + return MainActivityPrefs.get(); + } + + public PreferenceStore.Pref getActionPref() { + return actionPref; + } + + public PreferenceStore.Pref getDblActionPref() { + return dblActionPref; + } + + public PreferenceStore.Pref getLongActionPref() { + return longActionPref; + } + + @Nullable + public Action.Handler getClickHandler() { + if (clickHandler != null) return clickHandler; + var a = Action.get(getPrefs().getIntPref(actionPref)); + return (a == null) ? null : (clickHandler = a.getHandler()); + } + + @Nullable + public Action.Handler getDblClickHandler() { + if (dblClickHandler != null) return dblClickHandler; + var a = Action.get(getPrefs().getIntPref(dblActionPref)); + return (a == null) ? null : (dblClickHandler = a.getHandler()); + } + + @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 boolean isMedia() { + return media; + } + + public static final class PrefsListener implements PreferenceStore.Listener { + + @Override + public void onPreferenceChanged(PreferenceStore store, List> prefs) { + for (var p : prefs) { + if (p.getName().startsWith("KEY_ACTION_")) { + for (var k : Key.all) { + k.clickHandler = null; + k.dblClickHandler = null; + k.longClickHandler = null; + } + return; + } + } + } + } +} diff --git a/fermata/src/main/java/me/aap/fermata/action/KeyEventHandler.java b/fermata/src/main/java/me/aap/fermata/action/KeyEventHandler.java new file mode 100644 index 00000000..4ac529f7 --- /dev/null +++ b/fermata/src/main/java/me/aap/fermata/action/KeyEventHandler.java @@ -0,0 +1,180 @@ +package me.aap.fermata.action; + +import static android.os.SystemClock.uptimeMillis; +import static android.view.KeyEvent.ACTION_DOWN; +import static android.view.KeyEvent.ACTION_MULTIPLE; +import static android.view.KeyEvent.ACTION_UP; + +import android.os.Handler; +import android.view.KeyEvent; +import android.widget.EditText; + +import androidx.annotation.Nullable; + +import me.aap.fermata.media.service.MediaSessionCallback; +import me.aap.fermata.ui.activity.MainActivityDelegate; +import me.aap.utils.function.IntObjectFunction; +import me.aap.utils.log.Log; + +/** + * @author Andrey Pavlenko + */ +public class KeyEventHandler { + private static final int DBL_CLICK_INTERVAL = 500; + private static final int LONG_CLICK_INTERVAL = 1000; + private final Handler handler; + private final MediaSessionCallback cb; + @Nullable + private final MainActivityDelegate activity; + private Worker worker; + + public KeyEventHandler(MediaSessionCallback cb) { + this.cb = cb; + this.activity = null; + this.handler = cb.getHandler(); + } + + public KeyEventHandler(MainActivityDelegate activity) { + this.cb = activity.getMediaSessionCallback(); + this.activity = activity; + this.handler = activity.getHandler(); + } + + public boolean handle(KeyEvent event, IntObjectFunction defaultHandler) { + Log.i((activity == null) ? "Media: " : "Activity: ", event); + + if (event.isCanceled()) { + worker = null; + return defaultHandler.apply(event.getKeyCode(), event); + } + + if (worker != null) { + if (worker.handle(event)) return true; + worker = null; + return false; + } + + var code = event.getKeyCode(); + var k = Key.get(code); + if (k == null) return defaultHandler.apply(code, event); + + if (!k.isMedia() && (activity != null) && (activity.getCurrentFocus() instanceof EditText)) { + return defaultHandler.apply(code, event); + } + + var dblClickHandler = k.getDblClickHandler(); + if (dblClickHandler == null) return defaultHandler.apply(code, event); + + var action = event.getAction(); + if (action == ACTION_MULTIPLE) { + dblClickHandler.handle(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); + worker = new Worker(event, clickHandler, dblClickHandler, longClickHandler); + return false; + } + + 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 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; + time = longClickTime = uptimeMillis(); + sched(DBL_CLICK_INTERVAL); + } + + @Override + public void run() { + if (worker != this) return; + + long now = uptimeMillis(); + long diff = now - time; + + if (diff < LONG_CLICK_INTERVAL) { + if (up) { + worker = null; + handle(clickHandler); + } else { + sched(LONG_CLICK_INTERVAL - (now - longClickTime)); + } + } else { + diff = now - longClickTime; + + if (diff < LONG_CLICK_INTERVAL) { + sched(LONG_CLICK_INTERVAL - diff); + } else { + longClickTime = time; + handle(longClickHandler); + sched(LONG_CLICK_INTERVAL); + } + } + } + + boolean handle(KeyEvent e) { + if (e.getKeyCode() != event.getKeyCode()) return false; + + switch (e.getAction()) { + case ACTION_DOWN -> { + if (dblClickHandler == clickHandler) { + longClickTime = uptimeMillis(); + handle(longClickHandler); + } + return true; + } + case ACTION_UP -> { + long holdTime = uptimeMillis() - time; + + if (holdTime <= DBL_CLICK_INTERVAL) { + if (up) { + worker = null; + handle(dblClickHandler); + } else if (dblClickHandler == clickHandler) { + worker = null; + handle(clickHandler); + } else { + up = true; + } + } else if (holdTime >= LONG_CLICK_INTERVAL) { + worker = null; + } else { + worker = null; + if (longClickTime == time) handle(clickHandler); + } + + return true; + } + case ACTION_MULTIPLE -> { + worker = null; + handle(dblClickHandler); + return true; + } + } + return false; + } + + private void handle(Action.Handler h) { + h.handle(cb, activity, time); + } + + private void sched(long delay) { + handler.postDelayed(this, delay); + } + } +} diff --git a/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngine.java b/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngine.java index 9e226bf0..308a74e3 100644 --- a/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngine.java +++ b/fermata/src/main/java/me/aap/fermata/media/engine/MediaEngine.java @@ -7,7 +7,6 @@ import static me.aap.utils.text.TextUtils.isBlank; import android.media.AudioManager; -import android.view.KeyEvent; import androidx.annotation.Nullable; import androidx.media.AudioFocusRequestCompat; @@ -250,7 +249,7 @@ default boolean hasVideoMenu() { default void contributeToMenu(OverlayMenu.Builder b) {} - default boolean adjustVolume(KeyEvent event) { + default boolean adjustVolume(int direction) { return false; } 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 92931363..68112dd1 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 @@ -351,7 +351,7 @@ void sync(long position, float speed, boolean restart) { if (restart) { sub.stop(false); sub.start(position, getSubtitleDelay(), speed); - if (state == STATE_PAUSED) sub.stop(true); + if (!isPlaying()) sub.stop(true); } else { sub.sync(position, getSubtitleDelay(), speed); } diff --git a/fermata/src/main/java/me/aap/fermata/media/lib/DefaultMediaLib.java b/fermata/src/main/java/me/aap/fermata/media/lib/DefaultMediaLib.java index 802b51a7..0ccf58c8 100644 --- a/fermata/src/main/java/me/aap/fermata/media/lib/DefaultMediaLib.java +++ b/fermata/src/main/java/me/aap/fermata/media/lib/DefaultMediaLib.java @@ -182,8 +182,7 @@ public void getChildren(String parentMediaId, MediaLibResult> re getLastPlayedItem().then(i -> (i == null) ? completedNull() : i.asMediaItem()) .onSuccess(i -> { if (i != null) items.add(i); - }) - .then(v -> getFolders().asMediaItem()).onSuccess(items::add) + }).then(v -> getFolders().asMediaItem()).onSuccess(items::add) .then(v -> getFavorites().asMediaItem()).onSuccess(items::add) .then(v -> getPlaylists().asMediaItem()).onSuccess(items::add) .onCompletion((r, f) -> result.sendResult(items, null)).onFailure(this::log); @@ -205,9 +204,7 @@ public void getChildren(String parentMediaId, MediaLibResult> re @Override public void getItem(String itemId, MediaLibResult result) { result.detach(); - getItem(itemId) - .then(Item::asMediaItem) - .onFailure(this::log) + getItem(itemId).then(Item::asMediaItem).onFailure(this::log) .onCompletion((i, f) -> result.sendResult(i, null)); } @@ -217,9 +214,8 @@ public void search(String query, MediaLibResult> result) { if (id != null) { getItem(id).onCompletion((i, err1) -> { if (i == null) result.sendResult(Collections.emptyList(), null); - else i.asMediaItem().onCompletion((mi, err2) -> - result.sendResult((mi == null) ? Collections.emptyList() - : Collections.singletonList(mi), null)); + else i.asMediaItem().onCompletion((mi, err2) -> result.sendResult( + (mi == null) ? Collections.emptyList() : Collections.singletonList(mi), null)); }); } else { result.sendResult(Collections.emptyList(), null); @@ -241,64 +237,13 @@ public FutureSupplier getLastPlayedItem() { @Override public long getLastPlayedPosition(PlayableItem i) { - if (i.isVideo()) return i.getPrefs().getPositionPref(); + long pos = i.getPrefs().getPositionPref(); + if ((pos != 0) || i.isVideo()) return pos; BrowsableItemPrefs p = i.getParent().getPrefs(); String id = p.getLastPlayedItemPref(); return ((id != null) && id.equals(i.getId())) ? p.getLastPlayedPosPref() : 0; } - @Override - public void setLastPlayed(PlayableItem i, long position) { - if ((position < 0) || i.isExternal()) return; - - i.getDuration().main().onSuccess(dur -> { - String id; - BrowsableItemPrefs p; - - if (i.isStream() || (dur <= 0)) { - id = i.getId(); - p = i.getParent().getPrefs(); - setLastPlayedItemPref(id); - setLastPlayedPosPref(0); - p.setLastPlayedItemPref(id); - p.setLastPlayedPosPref(0); - return; - } - - if ((dur - position) <= 1000) { - i.getNextPlayable().onCompletion((next, fail) -> { - if (next == null) next = i; - - String nextId = next.getId(); - BrowsableItemPrefs nextPrefs = next.getParent().getPrefs(); - setLastPlayedItemPref(nextId); - setLastPlayedPosPref(0); - nextPrefs.setLastPlayedItemPref(nextId); - nextPrefs.setLastPlayedPosPref(0); - }); - - return; - } else { - id = i.getId(); - p = i.getParent().getPrefs(); - } - - if (i.isVideo()) { - PlayableItemPrefs prefs = i.getPrefs(); - float th = prefs.getWatchedThresholdPref() / 100F; - if (th > 0) { - if (position > (dur * th)) prefs.setWatchedPref(true); - else prefs.setPositionPref(position); - } - } - - setLastPlayedItemPref(id); - setLastPlayedPosPref(position); - p.setLastPlayedItemPref(id); - p.setLastPlayedPosPref(position); - }); - } - @NonNull @Override public DefaultFolders getFolders() { @@ -362,8 +307,9 @@ void addToCache(Item i) { clearRefs(itemCache, itemRefQueue); String id = i.getId(); if (BuildConfig.D && itemCache.containsKey(id)) { - throw new AssertionError("Unable to add item " + i + - ". Item with id=" + id + "already exists: " + itemCache.get(id)); + throw new AssertionError( + "Unable to add item " + i + ". Item with id=" + id + "already exists: " + + itemCache.get(id)); } itemCache.put(id, new WeakRef<>(id, i, itemRefQueue)); } diff --git a/fermata/src/main/java/me/aap/fermata/media/lib/MediaLib.java b/fermata/src/main/java/me/aap/fermata/media/lib/MediaLib.java index 60cf66b8..6a4b811e 100644 --- a/fermata/src/main/java/me/aap/fermata/media/lib/MediaLib.java +++ b/fermata/src/main/java/me/aap/fermata/media/lib/MediaLib.java @@ -117,8 +117,6 @@ default FutureSupplier getBitmap(String uri) { long getLastPlayedPosition(PlayableItem i); - void setLastPlayed(PlayableItem i, long position); - void getChildren(String parentMediaId, MediaLibResult> result); default void getChildren(String parentMediaId, MediaBrowserServiceCompat.Result> result) { diff --git a/fermata/src/main/java/me/aap/fermata/media/pref/PlaybackControlPrefs.java b/fermata/src/main/java/me/aap/fermata/media/pref/PlaybackControlPrefs.java index a438ee37..8972e66e 100644 --- a/fermata/src/main/java/me/aap/fermata/media/pref/PlaybackControlPrefs.java +++ b/fermata/src/main/java/me/aap/fermata/media/pref/PlaybackControlPrefs.java @@ -4,6 +4,8 @@ import androidx.annotation.NonNull; +import me.aap.fermata.action.Action; +import me.aap.fermata.action.Key; import me.aap.utils.event.BasicEventBroadcaster; import me.aap.utils.function.BooleanSupplier; import me.aap.utils.function.IntSupplier; @@ -21,14 +23,13 @@ public interface PlaybackControlPrefs extends SharedPreferenceStore { Pref RW_FF_LONG_TIME = Pref.i("RW_FF_LONG_TIME", 20); Pref RW_FF_LONG_TIME_UNIT = Pref.i("RW_FF_LONG_TIME_UNIT", TIME_UNIT_SECOND); Pref PREV_NEXT_LONG_TIME = Pref.i("PREV_NEXT_LONG_TIME", 5); - Pref PREV_NEXT_LONG_TIME_UNIT = Pref.i("PREV_NEXT_LONG_TIME_UNIT", TIME_UNIT_PERCENT); + Pref PREV_NEXT_LONG_TIME_UNIT = + Pref.i("PREV_NEXT_LONG_TIME_UNIT", TIME_UNIT_PERCENT); Pref PLAY_PAUSE_STOP = Pref.b("PLAY_PAUSE_STOP", true); Pref VIDEO_CONTROL_START_DELAY = Pref.i("VIDEO_CONTROL_START_DELAY", 0); Pref VIDEO_CONTROL_TOUCH_DELAY = Pref.i("VIDEO_CONTROL_TOUCH_DELAY", 5); Pref VIDEO_CONTROL_SEEK_DELAY = Pref.i("VIDEO_CONTROL_SEEK_DELAY", 3); Pref VIDEO_AA_SHOW_STATUS = Pref.b("VIDEO_AA_SHOW_STATUS", false); - Pref PREV_VOICE_CONTROl = Pref.b("PREV_VOICE_CONTROl", false); - Pref NEXT_VOICE_CONTROl = Pref.b("NEXT_VOICE_CONTROl", false); default int getRwFfTimePref() { return getIntPref(RW_FF_TIME); @@ -74,28 +75,36 @@ default boolean getVideoAaShowStatusPref() { return getBooleanPref(VIDEO_AA_SHOW_STATUS); } - default boolean getPrevVoiceControlPref() { - return getBooleanPref(PREV_VOICE_CONTROl); - } - - default boolean getNextVoiceControlPref() { - return getBooleanPref(NEXT_VOICE_CONTROl); - } - static long getTimeMillis(long dur, int time, int unit) { - switch (unit) { - case PlaybackControlPrefs.TIME_UNIT_SECOND: - return time * 1000; - case PlaybackControlPrefs.TIME_UNIT_MINUTE: - return time * 60000; - default: - return (long) (dur * ((float) time / 100)); - } + return switch (unit) { + case PlaybackControlPrefs.TIME_UNIT_SECOND -> time * 1000L; + case PlaybackControlPrefs.TIME_UNIT_MINUTE -> time * 60000L; + default -> (long) (dur * ((float) time / 100)); + }; } static PlaybackControlPrefs create(SharedPreferences prefs) { - class ControlPrefs extends BasicEventBroadcaster - implements PlaybackControlPrefs { + // Old prefs migration + var prevVoiceCtrl = "PREV_VOICE_CONTROl"; + var nextVoiceCtrl = "NEXT_VOICE_CONTROl"; + + if (prefs.contains(prevVoiceCtrl)) { + if (prefs.getBoolean(prevVoiceCtrl, false)) { + Key.getPrefs().applyIntPref(Key.MEDIA_PREVIOUS.getDblActionPref(), + Action.ACTIVATE_VOICE_CTRL.ordinal()); + } + prefs.edit().remove(prevVoiceCtrl).apply(); + } + if (prefs.contains(nextVoiceCtrl)) { + if (prefs.getBoolean(nextVoiceCtrl, false)) { + Key.getPrefs() + .applyIntPref(Key.MEDIA_NEXT.getDblActionPref(), Action.ACTIVATE_VOICE_CTRL.ordinal()); + } + prefs.edit().remove(nextVoiceCtrl).apply(); + } + + + class ControlPrefs extends BasicEventBroadcaster implements PlaybackControlPrefs { @NonNull @Override 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 a2eee76f..459c13cf 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,10 +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 android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD; -import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT; -import static android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS; -import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND; +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; import static me.aap.fermata.media.pref.MediaPrefs.BASS_STRENGTH; @@ -44,11 +41,11 @@ import static me.aap.fermata.media.pref.MediaPrefs.EQ_ENABLED; import static me.aap.fermata.media.pref.MediaPrefs.EQ_PRESET; import static me.aap.fermata.media.pref.MediaPrefs.EQ_USER_PRESETS; -import static me.aap.fermata.media.pref.MediaPrefs.VOL_BOOST_ENABLED; -import static me.aap.fermata.media.pref.MediaPrefs.VOL_BOOST_STRENGTH; import static me.aap.fermata.media.pref.MediaPrefs.VIRT_ENABLED; import static me.aap.fermata.media.pref.MediaPrefs.VIRT_MODE; import static me.aap.fermata.media.pref.MediaPrefs.VIRT_STRENGTH; +import static me.aap.fermata.media.pref.MediaPrefs.VOL_BOOST_ENABLED; +import static me.aap.fermata.media.pref.MediaPrefs.VOL_BOOST_STRENGTH; import static me.aap.fermata.media.pref.MediaPrefs.getUserPresetBands; import static me.aap.fermata.media.pref.PlaybackControlPrefs.getTimeMillis; import static me.aap.utils.async.Completed.completed; @@ -99,6 +96,7 @@ 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; @@ -111,6 +109,7 @@ import me.aap.fermata.media.lib.MediaLib.PlayableItem; import me.aap.fermata.media.lib.MediaLib.StreamItem; import me.aap.fermata.media.pref.BrowsableItemPrefs; +import me.aap.fermata.media.pref.MediaLibPrefs; import me.aap.fermata.media.pref.MediaPrefs; import me.aap.fermata.media.pref.PlayableItemPrefs; import me.aap.fermata.media.pref.PlaybackControlPrefs; @@ -128,21 +127,24 @@ /** * @author Andrey Pavlenko */ -public class MediaSessionCallback extends MediaSessionCompat.Callback implements SharedConstants, - MediaSessionCallbackAssistant, MediaEngine.Listener, AudioManager.OnAudioFocusChangeListener, - EventBroadcaster, Closeable { +public class MediaSessionCallback extends MediaSessionCompat.Callback + implements SharedConstants, MediaSessionCallbackAssistant, MediaEngine.Listener, + AudioManager.OnAudioFocusChangeListener, EventBroadcaster, + Closeable { public static final String EXTRA_POS = "me.aap.fermata.extra.pos"; - private static final long SUPPORTED_ACTIONS = ACTION_PLAY | ACTION_STOP | ACTION_PAUSE | ACTION_PLAY_PAUSE - | ACTION_PLAY_FROM_MEDIA_ID | ACTION_PLAY_FROM_SEARCH | ACTION_PLAY_FROM_URI - | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_QUEUE_ITEM - | ACTION_REWIND | ACTION_FAST_FORWARD | ACTION_SEEK_TO - | ACTION_SET_REPEAT_MODE | ACTION_SET_SHUFFLE_MODE; - private final Collection> listeners = new LinkedList<>(); + private static final long SUPPORTED_ACTIONS = + ACTION_PLAY | ACTION_STOP | ACTION_PAUSE | ACTION_PLAY_PAUSE | ACTION_PLAY_FROM_MEDIA_ID | + ACTION_PLAY_FROM_SEARCH | ACTION_PLAY_FROM_URI | ACTION_SKIP_TO_PREVIOUS | + ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_QUEUE_ITEM | ACTION_REWIND | ACTION_FAST_FORWARD | + ACTION_SEEK_TO | ACTION_SET_REPEAT_MODE | ACTION_SET_SHUFFLE_MODE; + private final Collection> listeners = + new LinkedList<>(); private final MediaLib lib; private final FermataMediaService service; 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; @@ -158,7 +160,6 @@ public class MediaSessionCallback extends MediaSessionCompat.Callback implements private boolean playOnPrepared; private boolean playOnAudioFocus; private boolean tryAnotherEngine; - private MediaKeyHandler mediaKeyHandler; @NonNull private PlaybackStateCompat currentState; private MediaMetadataCompat currentMetadata; @@ -166,32 +167,37 @@ public class MediaSessionCallback extends MediaSessionCompat.Callback implements private Queue> assistants; private FutureSupplier playerTask = completedVoid(); - public MediaSessionCallback(FermataMediaService service, MediaSessionCompat session, MediaLib lib, + public MediaSessionCallback(FermataMediaService service, MediaSessionCompat session, + MediaLib lib, PlaybackControlPrefs playbackControlPrefs, Handler handler) { this.lib = lib; this.service = service; 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, ctx.getString(R.string.rewind), R.drawable.rw).build(); customFastForward = new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_FF, ctx.getString(R.string.fast_forward), R.drawable.ff).build(); customRepeatEnable = new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_REPEAT_ENABLE, ctx.getString(R.string.repeat), R.drawable.repeat).build(); - customRepeatDisable = new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_REPEAT_DISABLE, - ctx.getString(R.string.repeat_disable), R.drawable.repeat_filled).build(); - customShuffleEnable = new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SHUFFLE_ENABLE, - ctx.getString(R.string.shuffle), R.drawable.shuffle).build(); - customShuffleDisable = new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SHUFFLE_DISABLE, - ctx.getString(R.string.shuffle_disable), R.drawable.shuffle_filled).build(); + customRepeatDisable = + new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_REPEAT_DISABLE, + ctx.getString(R.string.repeat_disable), R.drawable.repeat_filled).build(); + customShuffleEnable = + new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SHUFFLE_ENABLE, + ctx.getString(R.string.shuffle), R.drawable.shuffle).build(); + customShuffleDisable = + new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SHUFFLE_DISABLE, + ctx.getString(R.string.shuffle_disable), R.drawable.shuffle_filled).build(); customFavoritesAdd = new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_FAVORITES_ADD, ctx.getString(R.string.favorites_add), R.drawable.favorite).build(); - customFavoritesRemove = new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_FAVORITES_REMOVE, - ctx.getString(R.string.favorites_remove), R.drawable.favorite_filled).build(); + customFavoritesRemove = + new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_FAVORITES_REMOVE, + ctx.getString(R.string.favorites_remove), R.drawable.favorite_filled).build(); currentState = new PlaybackStateCompat.Builder().setActions(SUPPORTED_ACTIONS).build(); setPlaybackState(currentState); @@ -200,15 +206,12 @@ public MediaSessionCallback(FermataMediaService service, MediaSessionCompat sess audioManager = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE); if (audioManager != null) { - AudioAttributesCompat focusAttrs = new AudioAttributesCompat.Builder() - .setUsage(AudioAttributesCompat.USAGE_MEDIA) - .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) - .build(); - audioFocusReq = new AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) - .setAudioAttributes(focusAttrs) - .setWillPauseWhenDucked(false) - .setOnAudioFocusChangeListener(this) - .build(); + AudioAttributesCompat focusAttrs = + new AudioAttributesCompat.Builder().setUsage(AudioAttributesCompat.USAGE_MEDIA) + .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC).build(); + audioFocusReq = new AudioFocusRequestCompat.Builder( + AudioManagerCompat.AUDIOFOCUS_GAIN).setAudioAttributes(focusAttrs) + .setWillPauseWhenDucked(false).setOnAudioFocusChangeListener(this).build(); } else { audioFocusReq = null; } @@ -225,6 +228,10 @@ public void onReceive(Context context, Intent intent) { ctx.registerReceiver(onNoisy, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); } + public Context getContext() { + return getMediaLib().getContext(); + } + public MediaLib getMediaLib() { return lib; } @@ -312,6 +319,11 @@ public void removeAssistant(MediaSessionCallbackAssistant a) { removeFromQueue(assistants, a); } + @NonNull + public Handler getHandler() { + return handler; + } + @NonNull public MediaSessionCallbackAssistant getAssistant() { if (assistants == null) return this; @@ -338,16 +350,16 @@ public void removeEngineProvider(MediaEngineProvider engineProvider) { @Override public FutureSupplier getPrevPlayable(Item i) { MediaSessionCallbackAssistant a = getAssistant(); - return (a == this) ? MediaSessionCallbackAssistant.super.getPrevPlayable(i) - : a.getPrevPlayable(i); + return (a == this) ? MediaSessionCallbackAssistant.super.getPrevPlayable(i) : + a.getPrevPlayable(i); } @NonNull @Override public FutureSupplier getNextPlayable(Item i) { MediaSessionCallbackAssistant a = getAssistant(); - return (a == this) ? MediaSessionCallbackAssistant.super.getNextPlayable(i) - : a.getNextPlayable(i); + return (a == this) ? MediaSessionCallbackAssistant.super.getNextPlayable(i) : + a.getNextPlayable(i); } private static boolean removeFromQueue(Queue> q, T t) { @@ -365,53 +377,14 @@ 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); - if ((mediaKeyHandler != null) && (mediaKeyHandler.handle(e))) return true; - - int action = e.getAction(); - int code = e.getKeyCode(); - - if (action == KeyEvent.ACTION_DOWN) { - switch (e.getKeyCode()) { - case KEYCODE_MEDIA_NEXT: - case KEYCODE_MEDIA_PREVIOUS: - case KEYCODE_MEDIA_REWIND: - case KEYCODE_MEDIA_FAST_FORWARD: - mediaKeyHandler = new ProgressiveSeekHandler(code); - return true; - } - } else if (action == KeyEvent.ACTION_UP) { - switch (code) { - case KEYCODE_MEDIA_NEXT: - case KEYCODE_MEDIA_PREVIOUS: - if (getAssistant() != this) { - PlaybackControlPrefs prefs = getPlaybackControlPrefs(); - if ((code == KEYCODE_MEDIA_NEXT) && (prefs.getNextVoiceControlPref()) || - (code == KEYCODE_MEDIA_PREVIOUS) && (prefs.getPrevVoiceControlPref())) { - mediaKeyHandler = new VoiceSearchHandler(code); - return true; - } - } - - if (code == KEYCODE_MEDIA_NEXT) onSkipToNext(); - else onSkipToPrevious(); - return true; - case KEYCODE_MEDIA_REWIND: - onRewind(); - return true; - case KEYCODE_MEDIA_FAST_FORWARD: - onFastForward(); - return true; - } - } - - return super.onMediaButtonEvent(mediaButtonEvent); + return keyHandler.handle(e, (i, ke) -> super.onMediaButtonEvent(mediaButtonEvent)); } public void close() { onStop(); session.setActive(false); lib.getContext().unregisterReceiver(onNoisy); - listeners.clear(); + removeBroadcastListeners(); } @Override @@ -473,8 +446,9 @@ public FutureSupplier play() { long pos = state.getPosition(); float speed = getSpeed(engine.getSource()); - state = new PlaybackStateCompat.Builder(state) - .setState(PlaybackStateCompat.STATE_PLAYING, pos, speed).build(); + state = + new PlaybackStateCompat.Builder(state).setState(PlaybackStateCompat.STATE_PLAYING, pos, + speed).build(); setPlaybackState(state); eng.setPosition(pos); start(eng, speed); @@ -506,7 +480,8 @@ private FutureSupplier playFromMediaId(String mediaId, Bundle extras) { long pos = (extras == null) ? 0 : extras.getLong(EXTRA_POS, 0); playPreparedItem(pi, pos); } else { - String msg = lib.getContext().getResources().getString(R.string.err_failed_to_play, mediaId); + String msg = + lib.getContext().getResources().getString(R.string.err_failed_to_play, mediaId); Log.w(msg); PlaybackStateCompat state = new PlaybackStateCompat.Builder().setActions(SUPPORTED_ACTIONS) .setState(PlaybackStateCompat.STATE_ERROR, 0, 1.0f) @@ -546,7 +521,7 @@ public void onPause() { eng.getPosition().and(eng.getSpeed()).main().onSuccess(h -> { if (eng != getEngine()) return; long qid = currentState.getActiveQueueItemId(); - lib.setLastPlayed(i, h.value1); + setLastPlayed(i, h.value1); PlaybackStateCompat state = createPlayingState(i, true, qid, h.value1, h.value2); setPlaybackState(state); }); @@ -573,7 +548,7 @@ private FutureSupplier onStop(MediaEngine eng, long pos) { if (eng != null) { if (pos != -1) { PlayableItem i = eng.getSource(); - if (i != null) lib.setLastPlayed(i, pos); + if (i != null) setLastPlayed(i, pos); } eng.stop(); @@ -590,7 +565,8 @@ private void stopped() { if (getPlaybackState().getState() != PlaybackStateCompat.STATE_STOPPED) { PlaybackStateCompat state = new PlaybackStateCompat.Builder().setActions(SUPPORTED_ACTIONS) .setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f).build(); - setPlaybackState(state, null, Collections.emptyList(), REPEAT_MODE_INVALID, SHUFFLE_MODE_INVALID); + setPlaybackState(state, null, Collections.emptyList(), REPEAT_MODE_INVALID, + SHUFFLE_MODE_INVALID); } session.setQueue(null); @@ -636,24 +612,25 @@ private FutureSupplier skipTo(boolean next) { private void skipTo(boolean next, PlayableItem i) { PlaybackStateCompat state = getPlaybackState(); - long pos = i.isVideo() ? i.getPrefs().getPositionPref() : 0; + long pos = i.getPrefs().getPositionPref(); PlaybackStateCompat.Builder b = new PlaybackStateCompat.Builder(state); - b.setState(next ? STATE_SKIPPING_TO_NEXT : STATE_SKIPPING_TO_PREVIOUS, pos, state.getPlaybackSpeed()); + b.setState(next ? STATE_SKIPPING_TO_NEXT : STATE_SKIPPING_TO_PREVIOUS, pos, + state.getPlaybackSpeed()); setPlaybackState(b.build()); playPreparedItem(i, pos); } @Override public void onRewind() { - onRwFf(false, 1); + rewindFastForward(false, 1); } @Override public void onFastForward() { - onRwFf(true, 1); + rewindFastForward(true, 1); } - private void onRwFf(boolean ff, int multiply) { + public void rewindFastForward(boolean ff, int multiply) { PlaybackControlPrefs pp = getPlaybackControlPrefs(); rewindFastForward(ff, pp.getRwFfTimePref(), pp.getRwFfTimeUnitPref(), multiply); } @@ -664,13 +641,13 @@ public boolean rewindFastForward(boolean ff, int time, int timeUnit, int multipl MediaEngine eng = getEngine(); if ((eng == null) || ((i = eng.getSource()) == null)) return false; - playerTask = eng.getDuration().and(eng.getPosition()).main().onSuccess(h -> - rewindFastForward(eng, i, h.value2, h.value1, ff, time, timeUnit, multiply)); + playerTask = eng.getDuration().and(eng.getPosition()).main().onSuccess( + h -> rewindFastForward(eng, i, h.value2, h.value1, ff, time, timeUnit, multiply)); return true; } - private void rewindFastForward(MediaEngine eng, PlayableItem i, long pos, long dur, - boolean ff, int time, int timeUnit, int multiply) { + private void rewindFastForward(MediaEngine eng, PlayableItem i, long pos, long dur, boolean ff, + int time, int timeUnit, int multiply) { if (getCurrentItem() != i) return; PlaybackStateCompat state = getPlaybackState(); @@ -678,7 +655,7 @@ private void rewindFastForward(MediaEngine eng, PlayableItem i, long pos, long d b.setState(ff ? STATE_FAST_FORWARDING : STATE_REWINDING, state.getPosition(), state.getPlaybackSpeed()); setPlaybackState(b.build()); - long timeShift = getTimeMillis(dur, time, timeUnit) * multiply; + long timeShift = getTimeMillis(dur, time, timeUnit) * Math.max(1, multiply); if (ff) { dur -= 1000; @@ -696,30 +673,14 @@ private void rewindFastForward(MediaEngine eng, PlayableItem i, long pos, long d @Override public void onCustomAction(String action, Bundle extras) { switch (action) { - case CUSTOM_ACTION_RW: - onRewind(); - break; - case CUSTOM_ACTION_FF: - onFastForward(); - break; - case CUSTOM_ACTION_REPEAT_ENABLE: - repeatEnableDisable(true); - break; - case CUSTOM_ACTION_REPEAT_DISABLE: - repeatEnableDisable(false); - break; - case CUSTOM_ACTION_SHUFFLE_ENABLE: - shuffleEnableDisable(true); - break; - case CUSTOM_ACTION_SHUFFLE_DISABLE: - shuffleEnableDisable(false); - break; - case CUSTOM_ACTION_FAVORITES_ADD: - favoriteAddRemove(true); - break; - case CUSTOM_ACTION_FAVORITES_REMOVE: - favoriteAddRemove(false); - break; + case CUSTOM_ACTION_RW -> onRewind(); + case CUSTOM_ACTION_FF -> onFastForward(); + case CUSTOM_ACTION_REPEAT_ENABLE -> repeatEnableDisable(true); + case CUSTOM_ACTION_REPEAT_DISABLE -> repeatEnableDisable(false); + case CUSTOM_ACTION_SHUFFLE_ENABLE -> shuffleEnableDisable(true); + case CUSTOM_ACTION_SHUFFLE_DISABLE -> shuffleEnableDisable(false); + case CUSTOM_ACTION_FAVORITES_ADD -> favoriteAddRemove(true); + case CUSTOM_ACTION_FAVORITES_REMOVE -> favoriteAddRemove(false); } } @@ -833,20 +794,18 @@ public void onSetRepeatMode(int repeatMode) { BrowsableItemPrefs p = i.getParent().getPrefs(); switch (repeatMode) { - case PlaybackStateCompat.REPEAT_MODE_INVALID: - case REPEAT_MODE_NONE: + case PlaybackStateCompat.REPEAT_MODE_INVALID, REPEAT_MODE_NONE -> { p.setRepeatItemPref(null); p.setRepeatPref(false); - break; - case REPEAT_MODE_ONE: + } + case REPEAT_MODE_ONE -> { p.setRepeatItemPref(i.getId()); p.setRepeatPref(false); - break; - case REPEAT_MODE_ALL: - case REPEAT_MODE_GROUP: + } + case REPEAT_MODE_ALL, REPEAT_MODE_GROUP -> { p.setRepeatItemPref(null); p.setRepeatPref(true); - break; + } } } @@ -886,7 +845,7 @@ private void onEnginePrepared(MediaEngine engine, PlayableItem i) { runWithRetry(() -> setAudiEffects(engine, prefs, parentPrefs, playbackPrefs)); if (playOnPrepared) { - lib.setLastPlayed(i, pos); + setLastPlayed(i, pos); start(engine, speed); } else { setPlayingState(engine, false, pos, speed); @@ -895,8 +854,8 @@ private void onEnginePrepared(MediaEngine engine, PlayableItem i) { @Override public void onEngineStarted(MediaEngine engine) { - engine.getPosition().and(engine.getSpeed()).main().onSuccess( - h -> setPlayingState(engine, true, h.value1, h.value2)); + engine.getPosition().and(engine.getSpeed()).main() + .onSuccess(h -> setPlayingState(engine, true, h.value1, h.value2)); } private void setPlayingState(MediaEngine engine, boolean playing, long pos, float speed) { @@ -924,22 +883,21 @@ private void setPlayingState(MediaEngine engine, boolean playing, long pos, floa update.get().accept(md1); return getQid.then(qid -> i.getMediaDescription().main().then(dsc -> { - if (getCurrentItem() != i) return completedVoid(); - MediaMetadataCompat.Builder b = new MediaMetadataCompat.Builder(md1); - FutureSupplier md2 = buildMetadata(b, md1, dsc); + if (getCurrentItem() != i) return completedVoid(); + MediaMetadataCompat.Builder b = new MediaMetadataCompat.Builder(md1); + FutureSupplier md2 = buildMetadata(b, md1, dsc); - if (md2.isDone()) { - update.get().accept(md2.get(b::build)); - return completedVoid(); - } else { - update.get().accept(b.build()); - return md2.main().then(md3 -> { - update.get().accept(md3); - return completedVoid(); - }); - } - }) - ); + if (md2.isDone()) { + update.get().accept(md2.get(b::build)); + return completedVoid(); + } else { + update.get().accept(b.build()); + return md2.main().then(md3 -> { + update.get().accept(md3); + return completedVoid(); + }); + } + })); }); MediaMetadataCompat md; @@ -953,7 +911,8 @@ private void setPlayingState(MediaEngine engine, boolean playing, long pos, floa md = b.build(); update.set(m -> engine.getPosition().main().onSuccess(position -> { if (getCurrentItem() != i) return; - PlaybackStateCompat s = createPlayingState(i, !isPlaying(), getQid.peek(0L), position, speed); + PlaybackStateCompat s = + createPlayingState(i, !isPlaying(), getQid.peek(0L), position, speed); session.setMetadata(m); setPlaybackState(s, m, null, repeat, shuffle); })); @@ -1016,7 +975,7 @@ private FutureSupplier engineEnded(MediaEngine engine) { skipTo(true, next); } else { onStop(false); - lib.setLastPlayed(i, 0); + setLastPlayed(i, 0); } return completedVoid(); @@ -1046,8 +1005,8 @@ public void onEngineError(MediaEngine engine, Throwable ex) { if (TextUtils.isEmpty(ex.getLocalizedMessage())) { msg = lib.getContext().getResources().getString(R.string.err_failed_to_play, i); } else { - msg = lib.getContext().getResources().getString(R.string.err_failed_to_play_cause, - i, ex.getLocalizedMessage()); + msg = lib.getContext().getResources() + .getString(R.string.err_failed_to_play_cause, i, ex.getLocalizedMessage()); } Log.w(ex, msg); @@ -1111,8 +1070,8 @@ private void playPreparedItem(PlayableItem i, long pos) { if (current instanceof StreamItem) { playPreparedItem(eng, i, pos, current, 0); } else { - eng.getPosition().main().onSuccess(currentPos - -> playPreparedItem(eng, i, pos, current, currentPos)); + eng.getPosition().main() + .onSuccess(currentPos -> playPreparedItem(eng, i, pos, current, currentPos)); } return; } @@ -1126,10 +1085,10 @@ private void playPreparedItem(MediaEngine eng, PlayableItem i, long pos, Playabl engine = eng = getEngineManager().createEngine(eng, i, this); if (eng == null) { - if (current != null) lib.setLastPlayed(current, currentPos); - String msg = lib.getContext().getResources().getString(R.string.err_unsupported_source_type, i); - PlaybackStateCompat state = new PlaybackStateCompat.Builder() - .setActions(SUPPORTED_ACTIONS) + if (current != null) setLastPlayed(current, currentPos); + String msg = + lib.getContext().getResources().getString(R.string.err_unsupported_source_type, i); + PlaybackStateCompat state = new PlaybackStateCompat.Builder().setActions(SUPPORTED_ACTIONS) .setState(PlaybackStateCompat.STATE_ERROR, 0, 1.0f) .setErrorMessage(PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR, msg).build(); setPlaybackState(state, Collections.emptyList()); @@ -1140,16 +1099,16 @@ private void playPreparedItem(MediaEngine eng, PlayableItem i, long pos, Playabl boolean updateQueue = false; if (current != null) { - lib.setLastPlayed(current, currentPos); + setLastPlayed(current, currentPos); if (current.equals(i)) { if (pos != -1) eng.setPosition(pos); } else { - lib.setLastPlayed(i, pos); + setLastPlayed(i, pos); if (!p.equals(current.getParent())) updateQueue = true; } } else { updateQueue = true; - lib.setLastPlayed(i, pos); + setLastPlayed(i, pos); } if (i.isVideo() && (videoView != null)) engine.setVideoView(getVideoView()); @@ -1180,9 +1139,8 @@ private PlaybackStateCompat createPlayingState(PlayableItem i, boolean pause, lo boolean repeat = p.getRepeatPref(); boolean shuffle = p.getShufflePref(); return new PlaybackStateCompat.Builder().setActions(SUPPORTED_ACTIONS) - .setState(state, position, speed) - .setActiveQueueItemId(qid) - .addCustomAction(customRewind).addCustomAction(customFastForward) + .setState(state, position, speed).setActiveQueueItemId(qid).addCustomAction(customRewind) + .addCustomAction(customFastForward) .addCustomAction(repeat ? customRepeatDisable : customRepeatEnable) .addCustomAction(shuffle ? customShuffleDisable : customShuffleEnable) .addCustomAction(i.isFavoriteItem() ? customFavoritesRemove : customFavoritesAdd).build(); @@ -1363,6 +1321,68 @@ private FutureSupplier prepareItem(PlayableItem i) { return completed(i).main(); } + private void setLastPlayed(PlayableItem i, long position) { + if ((position < 0) || i.isExternal()) return; + + i.getDuration().main().onSuccess(dur -> { + String id; + MediaLibPrefs libPrefs = lib.getPrefs(); + BrowsableItemPrefs p; + + if (i.isStream() || (dur <= 0)) { + id = i.getId(); + p = i.getParent().getPrefs(); + libPrefs.setLastPlayedItemPref(id); + libPrefs.setLastPlayedPosPref(0); + p.setLastPlayedItemPref(id); + p.setLastPlayedPosPref(0); + return; + } + + if ((dur - position) <= 1000) { + i.getNextPlayable().onCompletion((next, fail) -> { + if (next == null) next = i; + + String nextId = next.getId(); + BrowsableItemPrefs nextPrefs = next.getParent().getPrefs(); + libPrefs.setLastPlayedItemPref(nextId); + libPrefs.setLastPlayedPosPref(0); + nextPrefs.setLastPlayedItemPref(nextId); + nextPrefs.setLastPlayedPosPref(0); + i.getPrefs().setPositionPref(0); + }); + + return; + } else { + id = i.getId(); + p = i.getParent().getPrefs(); + } + + if (i.isVideo()) { + PlayableItemPrefs prefs = i.getPrefs(); + float th = prefs.getWatchedThresholdPref() / 100F; + if (th > 0) { + if (position > (dur * th)) prefs.setWatchedPref(true); + else prefs.setPositionPref(position); + } + } else if (position == 0) { + i.getPrefs().setPositionPref(0); + } else { + MediaEngine eng = getEngine(); + + if ((eng != null) && (eng.getSource() == i) && + (eng.getCurrentSubtitles() != NO_SUBTITLES)) { + i.getPrefs().setPositionPref(position); + } + } + + libPrefs.setLastPlayedItemPref(id); + libPrefs.setLastPlayedPosPref(position); + p.setLastPlayedItemPref(id); + p.setLastPlayedPosPref(position); + }); + } + private Runnable timer; private void startTimer(PlayableItem i, long pos, float speed) { @@ -1438,8 +1458,8 @@ public int hashCode() { private PlaybackTimer playbackTimer; public int getPlaybackTimer() { - return (playbackTimer == null) ? 0 - : Math.max((int) (playbackTimer.time - System.currentTimeMillis()) / 1000, 0); + return (playbackTimer == null) ? 0 : + Math.max((int) (playbackTimer.time - System.currentTimeMillis()) / 1000, 0); } public void setPlaybackTimer(int time) { @@ -1447,7 +1467,8 @@ public void setPlaybackTimer(int time) { playbackTimer = null; } else { int delay = time * 1000; - PlaybackTimer timer = this.playbackTimer = new PlaybackTimer(delay + System.currentTimeMillis()); + PlaybackTimer timer = + this.playbackTimer = new PlaybackTimer(delay + System.currentTimeMillis()); handler.postDelayed(timer, delay); } } @@ -1464,74 +1485,4 @@ public void run() { if (playbackTimer == this) onStop(); } } - - private interface MediaKeyHandler { - boolean handle(KeyEvent e); - } - - private final class ProgressiveSeekHandler implements MediaKeyHandler, Runnable { - private final long time = System.currentTimeMillis(); - private final boolean ff; - - ProgressiveSeekHandler(int code) { - ff = (code == KEYCODE_MEDIA_NEXT) || (code == KEYCODE_MEDIA_FAST_FORWARD); - handler.postDelayed(this, 1000); - } - - @Override - public void run() { - if (mediaKeyHandler != this) return; - long holdTime = System.currentTimeMillis() - time; - onRwFf(ff, (int) (holdTime / 1000)); - handler.postDelayed(this, 1000); - } - - @Override - public boolean handle(KeyEvent e) { - if (e.getAction() == KeyEvent.ACTION_UP) { - if (mediaKeyHandler != this) return false; - mediaKeyHandler = null; - return (System.currentTimeMillis() - time) >= 1000; - } - - mediaKeyHandler = null; - return false; - } - } - - private final class VoiceSearchHandler implements MediaKeyHandler, Runnable { - private final long time = System.currentTimeMillis(); - private final int code; - - VoiceSearchHandler(int code) { - this.code = code; - handler.postDelayed(this, 500); - } - - @Override - public void run() { - if (mediaKeyHandler != this) return; - mediaKeyHandler = null; - if (code == KEYCODE_MEDIA_NEXT) onSkipToNext(); - else onSkipToPrevious(); - } - - @Override - public boolean handle(KeyEvent e) { - if ((e.getKeyCode() == code) && (System.currentTimeMillis() - time) <= 500) { - int a = e.getAction(); - - if (a == KeyEvent.ACTION_DOWN) { - return true; - } else if (a == KeyEvent.ACTION_UP) { - mediaKeyHandler = null; - getAssistant().startVoiceAssistant(); - return true; - } - } - - mediaKeyHandler = null; - return false; - } - } } \ No newline at end of file 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 47662d59..f1b407f6 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,8 +12,6 @@ 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.media.pref.PlaybackControlPrefs.NEXT_VOICE_CONTROl; -import static me.aap.fermata.media.pref.PlaybackControlPrefs.PREV_VOICE_CONTROl; 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; @@ -21,7 +19,6 @@ import static me.aap.fermata.ui.activity.MainActivityPrefs.VOICE_CONTROL_SUBST; import static me.aap.fermata.ui.activity.MainActivityPrefs.VOICE_CONTROl_ENABLED; import static me.aap.fermata.ui.activity.MainActivityPrefs.VOICE_CONTROl_FB; -import static me.aap.fermata.ui.activity.MainActivityPrefs.VOICE_CONTROl_M; import static me.aap.utils.async.Completed.completed; import static me.aap.utils.async.Completed.completedVoid; import static me.aap.utils.async.Completed.failed; @@ -60,7 +57,6 @@ import android.view.MotionEvent; import android.view.View; import android.widget.EditText; -import android.widget.Toast; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; @@ -83,12 +79,14 @@ import me.aap.fermata.FermataApplication; 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; import me.aap.fermata.addon.FermataFragmentAddon; import me.aap.fermata.addon.MediaLibAddon; -import me.aap.fermata.media.engine.MediaEngine; import me.aap.fermata.media.engine.MediaEngineManager; import me.aap.fermata.media.lib.AtvInterface; import me.aap.fermata.media.lib.DefaultMediaLib; @@ -122,12 +120,9 @@ import me.aap.utils.async.Promise; import me.aap.utils.concurrent.HandlerExecutor; import me.aap.utils.event.ListenerLeakDetector; -import me.aap.utils.function.BooleanSupplier; import me.aap.utils.function.Cancellable; -import me.aap.utils.function.DoubleSupplier; import me.aap.utils.function.Function; import me.aap.utils.function.IntObjectFunction; -import me.aap.utils.function.IntSupplier; import me.aap.utils.function.Supplier; import me.aap.utils.log.Log; import me.aap.utils.misc.MiscUtils; @@ -154,6 +149,7 @@ 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; @@ -163,7 +159,6 @@ public class MainActivityDelegate extends ActivityDelegate private FutureSupplier contentLoading; private boolean barsHidden; private boolean videoMode; - private boolean exitPressed; private int brightness = 255; private SpeechListener speechListener; private VoiceCommandHandler voiceCommandHandler; @@ -171,6 +166,7 @@ public class MainActivityDelegate extends ActivityDelegate public MainActivityDelegate(AppActivity activity, FermataServiceUiBinder binder) { super(activity); mediaServiceBinder = binder; + keyHandler = new KeyEventHandler(this); } @NonNull @@ -404,6 +400,7 @@ public void onActivityDestroy() { boolean leaks = ListenerLeakDetector.hasLeaks((b, l) -> { if (l instanceof ExportedItem.ListenerWrapper) l = ((ExportedItem.ListenerWrapper) l).getListener(); + if (l instanceof Key.PrefsListener) return false; if (l instanceof FermataAddon) return false; if (l instanceof AtvInterface) return false; if ((l instanceof DefaultMediaLib) && (b instanceof DefaultMediaLib)) return false; @@ -994,29 +991,14 @@ public void onPreferenceChanged(PreferenceStore store, List { if ((err == null) && (r[0] == PERMISSION_GRANTED)) return; if (err != null) Log.e(err, "Failed to request RECORD_AUDIO permission"); showAlert(getContext(), R.string.err_no_audio_record_perm); - try (PreferenceStore.Edit e = getPrefs().editPreferenceStore()) { - e.setBooleanPref(VOICE_CONTROl_ENABLED, false); - e.setBooleanPref(VOICE_CONTROl_FB, false); - e.setBooleanPref(VOICE_CONTROl_M, false); - } - try (PreferenceStore.Edit e = getPlaybackControlPrefs().editPreferenceStore()) { - e.setBooleanPref(NEXT_VOICE_CONTROl, false); - e.setBooleanPref(PREV_VOICE_CONTROl, false); - } + getPrefs().applyBooleanPref(VOICE_CONTROl_FB, false); }); } else if (prefs.contains(VOICE_CONTROL_SUBST)) { if (voiceCommandHandler != null) voiceCommandHandler.updateWordSubst(); @@ -1029,73 +1011,18 @@ public void onPreferenceChanged(PreferenceStore store, List next) { - switch (code) { - case KeyEvent.KEYCODE_BACK: - case KeyEvent.KEYCODE_ESCAPE: - onBackPressed(); - return true; - case KeyEvent.KEYCODE_M: - if (getCurrentFocus() instanceof EditText) return super.onKeyDown(code, event, next); - case KeyEvent.KEYCODE_MENU: - if (getPrefs().getVoiceControlMenuPref()) event.startTracking(); - return true; - case KeyEvent.KEYCODE_P: - getMediaServiceBinder().onPlayPauseButtonClick(); - if (isVideoMode()) getControlPanel().onVideoSeek(); - return true; - case KeyEvent.KEYCODE_S: - case KeyEvent.KEYCODE_DEL: - getMediaServiceBinder().getMediaSessionCallback().onStop(); - return true; - case KeyEvent.KEYCODE_X: - if (exitPressed) { - finish(); - } else { - exitPressed = true; - Toast.makeText(getContext(), R.string.press_x_again, Toast.LENGTH_SHORT).show(); - FermataApplication.get().getHandler().postDelayed(() -> exitPressed = false, 2000); - } - return true; - case KeyEvent.KEYCODE_VOLUME_UP: - case KeyEvent.KEYCODE_VOLUME_DOWN: - MediaEngine eng = getMediaServiceBinder().getCurrentEngine(); - if ((eng != null) && eng.adjustVolume(event)) return true; - } - - return super.onKeyDown(code, event, next); + return keyHandler.handle(event, next); } @Override public boolean onKeyUp(int code, KeyEvent event, IntObjectFunction next) { - if ((event.getFlags() & KeyEvent.FLAG_CANCELED_LONG_PRESS) == 0) { - switch (code) { - case KeyEvent.KEYCODE_M: - if (getCurrentFocus() instanceof EditText) return super.onKeyUp(code, event, next); - case KeyEvent.KEYCODE_MENU: - if (event.isShiftPressed()) { - getNavBarMediator().showMenu(this); - } else { - ControlPanelView cp = getControlPanel(); - if (cp.isActive()) cp.showMenu(); - else getNavBarMediator().showMenu(this); - } - return true; - } - } - - return super.onKeyUp(code, event, next); + return keyHandler.handle(event, next); } @Override public boolean onKeyLongPress(int code, KeyEvent event, IntObjectFunction next) { - switch (code) { - case KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_MENU -> { - if (getPrefs().getVoiceControlMenuPref()) startVoiceAssistant(); - return true; - } - } - return super.onKeyLongPress(code, event, next); + return keyHandler.handle(event, next); } public HandlerExecutor getHandler() { @@ -1132,13 +1059,14 @@ static final class Prefs implements MainActivityPrefs { private Prefs() { // Rename old prefs - Pref oldTheme = Pref.i("THEME", THEME_DARK); - Pref oldScale = Pref.f("MEDIA_ITEM_SCALE", 1f); - Pref fbLongPress = Pref.i("FB_LONG_PRESS", 0); - Pref fbLongPressAA = Pref.i("FB_LONG_PRESS_AA", 0); - Pref showClock = Pref.b("SHOW_CLOCK", false); - int theme = getIntPref(oldTheme); - float scale = getFloatPref(oldScale); + var oldTheme = Pref.i("THEME", THEME_DARK); + var oldScale = Pref.f("MEDIA_ITEM_SCALE", 1f); + var fbLongPress = Pref.i("FB_LONG_PRESS", 0); + var fbLongPressAA = Pref.i("FB_LONG_PRESS_AA", 0); + var showClock = Pref.b("SHOW_CLOCK", false); + var voiceCtrlM = Pref.b("VOICE_CONTROl_M", false); + var theme = getIntPref(oldTheme); + var scale = getFloatPref(oldScale); if ((theme != THEME_DARK) || (scale != 1f)) { try (PreferenceStore.Edit e = editPreferenceStore()) { @@ -1168,6 +1096,14 @@ private Prefs() { e.setIntPref(CLOCK_POS, CLOCK_POS_RIGHT); } } + + if (getBooleanPref(voiceCtrlM)) { + removePref(voiceCtrlM); + var kp = Key.getPrefs(); + var o = Action.ACTIVATE_VOICE_CTRL.ordinal(); + kp.applyIntPref(Key.M.getLongActionPref(), o); + kp.applyIntPref(Key.MENU.getLongActionPref(), o); + } } @NonNull diff --git a/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityPrefs.java b/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityPrefs.java index c7750368..5d016e22 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityPrefs.java +++ b/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityPrefs.java @@ -2,9 +2,6 @@ import static me.aap.fermata.BuildConfig.AUTO; -import android.content.Context; -import android.content.res.Configuration; - import androidx.annotation.Nullable; import java.util.List; @@ -22,7 +19,8 @@ /** * @author Andrey Pavlenko */ -public interface MainActivityPrefs extends SharedPreferenceStore, EventBroadcaster { +public interface MainActivityPrefs + extends SharedPreferenceStore, EventBroadcaster { int THEME_DARK = 0; int THEME_LIGHT = 1; int THEME_DAY_NIGHT = 2; @@ -59,31 +57,24 @@ public interface MainActivityPrefs extends SharedPreferenceStore, EventBroadcast Pref BRIGHTNESS = Pref.i("BRIGHTNESS", 255); Pref VOICE_CONTROl_ENABLED = Pref.b("VOICE_CONTROl_ENABLED", false); Pref VOICE_CONTROl_FB = Pref.b("VOICE_CONTROl_FB", false); - Pref VOICE_CONTROl_M = Pref.b("VOICE_CONTROl_M", false); Pref> VOICE_CONTROL_SUBST = Pref.s("VOICE_CONTROL_SUBST", ""); - Pref> VOICE_CONTROL_LANG = Pref.s("VOICE_CONTROL_LANG", - () -> Locale.getDefault().toLanguageTag()); + Pref> VOICE_CONTROL_LANG = + Pref.s("VOICE_CONTROL_LANG", () -> Locale.getDefault().toLanguageTag()); Pref CLOCK_POS = Pref.i("CLOCK_POS", CLOCK_POS_NONE); - Pref LOCALE = Pref.i("LOCALE", () -> { - switch (Locale.getDefault().getLanguage()) { - case "ru": - return LOCALE_RU; - case "it": - return LOCALE_IT; - case "tr": - return LOCALE_TR; - case "de": - return LOCALE_DE; - default: - return LOCALE_EN; - } + Pref LOCALE = Pref.i("LOCALE", () -> switch (Locale.getDefault().getLanguage()) { + case "ru" -> LOCALE_RU; + case "it" -> LOCALE_IT; + case "tr" -> LOCALE_TR; + case "de" -> LOCALE_DE; + default -> LOCALE_EN; }); Pref THEME_AA = Pref.i("THEME_AA", THEME_DARK); Pref HIDE_BARS_AA = AUTO ? Pref.b("HIDE_BARS_AA", false) : null; Pref FULLSCREEN_AA = AUTO ? Pref.b("FULLSCREEN_AA", false) : null; Pref SHOW_PG_UP_DOWN_AA = AUTO ? Pref.b("SHOW_PG_UP_DOWN_AA", true) : null; - Pref NAV_BAR_POS_AA = AUTO ? Pref.i("NAV_BAR_POS_AA", NavBarView.POSITION_BOTTOM) : null; + Pref NAV_BAR_POS_AA = + AUTO ? Pref.i("NAV_BAR_POS_AA", NavBarView.POSITION_BOTTOM) : null; Pref NAV_BAR_SIZE_AA = AUTO ? Pref.f("NAV_BAR_SIZE_AA", 1f) : null; Pref TOOL_BAR_SIZE_AA = AUTO ? Pref.f("TOOL_BAR_SIZE_AA", 1f) : null; Pref CONTROL_PANEL_SIZE_AA = AUTO ? Pref.f("CONTROL_PANEL_SIZE_AA", 1f) : null; @@ -232,10 +223,6 @@ default boolean getVoiceControlFBPref() { return getBooleanPref(VOICE_CONTROl_FB); } - default boolean getVoiceControlMenuPref() { - return getBooleanPref(VOICE_CONTROl_M); - } - default String getVoiceControlLang(MainActivityDelegate a) { return a.getPrefs().getStringPref(VOICE_CONTROL_LANG); } @@ -245,17 +232,12 @@ default int getClockPosPref() { } default Locale getLocalePref() { - switch (getIntPref(LOCALE)) { - case LOCALE_RU: - return new Locale("ru"); - case LOCALE_IT: - return Locale.ITALIAN; - case LOCALE_TR: - return new Locale("tr"); - case LOCALE_DE: - return new Locale("de"); - default: - return Locale.ENGLISH; - } + return switch (getIntPref(LOCALE)) { + case LOCALE_RU -> new Locale("ru"); + case LOCALE_IT -> Locale.ITALIAN; + case LOCALE_TR -> new Locale("tr"); + case LOCALE_DE -> new Locale("de"); + default -> Locale.ENGLISH; + }; } } diff --git a/fermata/src/main/java/me/aap/fermata/ui/fragment/SettingsFragment.java b/fermata/src/main/java/me/aap/fermata/ui/fragment/SettingsFragment.java index b5b418b7..26808f04 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/fragment/SettingsFragment.java +++ b/fermata/src/main/java/me/aap/fermata/ui/fragment/SettingsFragment.java @@ -9,8 +9,6 @@ import static me.aap.fermata.media.pref.MediaPrefs.MEDIA_SCANNER_DEFAULT; import static me.aap.fermata.media.pref.MediaPrefs.MEDIA_SCANNER_SYSTEM; import static me.aap.fermata.media.pref.MediaPrefs.MEDIA_SCANNER_VLC; -import static me.aap.fermata.media.pref.PlaybackControlPrefs.NEXT_VOICE_CONTROl; -import static me.aap.fermata.media.pref.PlaybackControlPrefs.PREV_VOICE_CONTROl; import static me.aap.fermata.ui.activity.MainActivityPrefs.LOCALE_DE; import static me.aap.fermata.ui.activity.MainActivityPrefs.LOCALE_EN; import static me.aap.fermata.ui.activity.MainActivityPrefs.LOCALE_IT; @@ -20,7 +18,6 @@ import static me.aap.fermata.ui.activity.MainActivityPrefs.VOICE_CONTROL_SUBST; import static me.aap.fermata.ui.activity.MainActivityPrefs.VOICE_CONTROl_ENABLED; import static me.aap.fermata.ui.activity.MainActivityPrefs.VOICE_CONTROl_FB; -import static me.aap.fermata.ui.activity.MainActivityPrefs.VOICE_CONTROl_M; import static me.aap.utils.ui.UiUtils.ID_NULL; import android.app.Activity; @@ -53,6 +50,8 @@ import me.aap.fermata.BuildConfig; import me.aap.fermata.FermataApplication; import me.aap.fermata.R; +import me.aap.fermata.action.Action; +import me.aap.fermata.action.Key; import me.aap.fermata.addon.AddonInfo; import me.aap.fermata.addon.AddonManager; import me.aap.fermata.addon.FermataAddon; @@ -97,8 +96,11 @@ public int getFragmentId() { @Override public CharSequence getTitle() { if (adapter != null) { - PreferenceSet set = adapter.getPreferenceSet(); - if (set.getParent() != null) return getResources().getString(set.get().title); + var set = adapter.getPreferenceSet(); + if (set.getParent() != null) { + var o = set.get(); + return (o.ctitle != null) ? o.ctitle : getResources().getString(o.title); + } } return getResources().getString(R.string.settings); } @@ -283,6 +285,47 @@ private PreferenceViewAdapter createAdapter(MainActivityDelegate a) { o.removeDefault = false; }); + sub1 = set.subSet(o -> o.title = R.string.key_bindings); + var actions = Action.getAll(); + var actionNames = new int[actions.size()]; + var actionOrdinals = new int[actions.size()]; + for (var action : actions) { + actionNames[action.ordinal()] = action.getName(); + actionOrdinals[action.ordinal()] = action.ordinal(); + } + + for (var k : Key.getAll()) { + sub2 = sub1.subSet(o -> o.ctitle = k.name()); + sub2.addListPref(o -> { + o.store = Key.getPrefs(); + o.pref = k.getActionPref(); + o.title = R.string.key_on_click; + o.subtitle = R.string.string_format; + o.values = actionNames; + o.valuesMap = actionOrdinals; + o.formatSubtitle = true; + }); + sub2.addListPref(o -> { + o.store = Key.getPrefs(); + o.pref = k.getLongActionPref(); + o.title = R.string.key_on_long_click; + o.subtitle = R.string.string_format; + o.values = actionNames; + o.valuesMap = actionOrdinals; + o.formatSubtitle = true; + }); + sub2.addListPref(o -> { + o.store = Key.getPrefs(); + o.pref = k.getDblActionPref(); + o.title = R.string.key_on_dbl_click; + o.subtitle = R.string.string_format; + o.values = actionNames; + o.valuesMap = actionOrdinals; + o.formatSubtitle = true; + }); + } + + sub1 = set.subSet(o -> o.title = R.string.playback_settings); sub1.addBooleanPref(o -> { o.store = mediaPrefs; @@ -388,27 +431,6 @@ private PreferenceViewAdapter createAdapter(MainActivityDelegate a) { o.store = a.getPrefs(); o.visibility = PrefCondition.create(a.getPrefs(), VOICE_CONTROl_ENABLED); }); - sub1.addBooleanPref(o -> { - o.title = R.string.voice_control_menu; - o.subtitle = R.string.voice_control_sub_long; - o.pref = VOICE_CONTROl_M; - o.store = a.getPrefs(); - o.visibility = PrefCondition.create(a.getPrefs(), VOICE_CONTROl_ENABLED); - }); - sub1.addBooleanPref(o -> { - o.title = R.string.voice_control_next; - o.subtitle = R.string.voice_control_sub_double; - o.pref = NEXT_VOICE_CONTROl; - o.store = a.getPlaybackControlPrefs(); - o.visibility = PrefCondition.create(a.getPrefs(), VOICE_CONTROl_ENABLED); - }); - sub1.addBooleanPref(o -> { - o.title = R.string.voice_control_prev; - o.subtitle = R.string.voice_control_sub_double; - o.pref = PREV_VOICE_CONTROl; - o.store = a.getPlaybackControlPrefs(); - o.visibility = PrefCondition.create(a.getPrefs(), VOICE_CONTROl_ENABLED); - }); sub1.addStringPref(o -> { o.title = R.string.voice_control_subst; o.subtitle = R.string.voice_control_subst_sub; @@ -549,10 +571,7 @@ private PreferenceViewAdapter createAdapter(MainActivityDelegate a) { o.seekScale = 5; }); - sub1 = set.subSet(o -> { - o.title = R.string.subtitles; - o.visibility = PrefCondition.create(mediaPrefs, MediaLibPrefs.VLC_ENABLED); - }); + sub1 = set.subSet(o -> o.title = R.string.subtitles); addSubtitlePrefs(sub1, mediaPrefs, isCar); addAddons(set); diff --git a/fermata/src/main/java/me/aap/fermata/ui/view/BodyLayout.java b/fermata/src/main/java/me/aap/fermata/ui/view/BodyLayout.java index 1c0ca89e..2f6a231b 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/view/BodyLayout.java +++ b/fermata/src/main/java/me/aap/fermata/ui/view/BodyLayout.java @@ -42,18 +42,19 @@ public class BodyLayout extends SplitLayout public BodyLayout(@NonNull Context ctx, @Nullable AttributeSet attrs) { super(ctx, attrs); - MainActivityDelegate a = getActivity(); SwipeRefreshLayout srl = getSwipeRefresh(); srl.setId(R.id.swiperefresh); srl.setOnRefreshListener(this); srl.setOnChildScrollUpCallback(this); setMode(Mode.FRAME); - FermataServiceUiBinder b = a.getMediaServiceBinder(); - b.addBroadcastListener(this); - a.addBroadcastListener(this, FRAGMENT_CHANGED | ACTIVITY_DESTROY); - b.getMediaSessionCallback().addBroadcastListener(this); - onPlayableChanged(null, b.getCurrentItem()); + MainActivityDelegate.getActivityDelegate(ctx).onSuccess(a -> { + FermataServiceUiBinder b = a.getMediaServiceBinder(); + b.addBroadcastListener(this); + a.addBroadcastListener(this, FRAGMENT_CHANGED | ACTIVITY_DESTROY); + b.getMediaSessionCallback().addBroadcastListener(this); + onPlayableChanged(null, b.getCurrentItem()); + }); } @Override @@ -143,7 +144,7 @@ public void onActivityEvent(MainActivityDelegate a, long e) { if (handleActivityDestroyEvent(a, e)) { FermataServiceUiBinder b = a.getMediaServiceBinder(); b.removeBroadcastListener(this); - b.getMediaSessionCallback().addBroadcastListener(this); + b.getMediaSessionCallback().removeBroadcastListener(this); } else if (e == FRAGMENT_CHANGED) { if (a.getActiveMediaLibFragment() == null) { setMode(Mode.FRAME); diff --git a/fermata/src/main/java/me/aap/fermata/ui/view/ControlPanelView.java b/fermata/src/main/java/me/aap/fermata/ui/view/ControlPanelView.java index 081f0841..0fe29bea 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/view/ControlPanelView.java +++ b/fermata/src/main/java/me/aap/fermata/ui/view/ControlPanelView.java @@ -2,8 +2,6 @@ import static android.media.AudioManager.ADJUST_LOWER; import static android.media.AudioManager.ADJUST_RAISE; -import static android.media.AudioManager.FLAG_SHOW_UI; -import static android.media.AudioManager.STREAM_MUSIC; import static android.util.TypedValue.COMPLEX_UNIT_PX; import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID; import static me.aap.utils.ui.UiUtils.getTextAppearanceSize; @@ -12,7 +10,6 @@ import android.content.Context; import android.content.res.TypedArray; -import android.media.AudioManager; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; @@ -356,10 +353,8 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d br = (distanceY > 0) ? Math.min(255, br + 10) : Math.max(0, br - 10); a.setBrightness(br); } else { - AudioManager amgr = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); - if (amgr == null) return false; - amgr.adjustStreamVolume(STREAM_MUSIC, (distanceY > 0) ? ADJUST_RAISE : ADJUST_LOWER, - FLAG_SHOW_UI); + MediaEngine eng = getActivity().getMediaServiceBinder().getCurrentEngine(); + return (eng != null) && eng.adjustVolume((distanceY > 0) ? ADJUST_RAISE : ADJUST_LOWER); } return true; diff --git a/fermata/src/main/java/me/aap/fermata/ui/view/SplitLayout.java b/fermata/src/main/java/me/aap/fermata/ui/view/SplitLayout.java index 93753591..836ad274 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/view/SplitLayout.java +++ b/fermata/src/main/java/me/aap/fermata/ui/view/SplitLayout.java @@ -30,7 +30,8 @@ protected SplitLayout(@NonNull Context ctx, @Nullable AttributeSet attrs) { inflate(ctx, getLayout(portrait), this); getSplitLine().setOnTouchListener(this); getSplitHandle().setOnTouchListener(this); - setSplitPercent(getActivity().getPrefs().getFloatPref(getSplitPercentPref(portrait))); + MainActivityDelegate.getActivityDelegate(ctx) + .onSuccess(a -> a.getPrefs().getFloatPref(getSplitPercentPref(portrait))); } @LayoutRes diff --git a/fermata/src/main/res/values-de/strings.xml b/fermata/src/main/res/values-de/strings.xml index b9d46762..4122c1b3 100644 --- a/fermata/src/main/res/values-de/strings.xml +++ b/fermata/src/main/res/values-de/strings.xml @@ -68,11 +68,7 @@ Sprachsuche Sprachsteuerung Schwebender Button - \'Menu\' oder \'M\' Button - \'zum nächsten Titel\' Button - \'zum vorhergehenden Titel\' Button Aktivieren der Sprachsteuerung durch langes Drücken - Aktivieren der Sprachsteuerung durch Doppel-Drücken Wortersetzung Ersetzen Sie ein Wort durch ein anderes Wort oder einen Satz diff --git a/fermata/src/main/res/values-it/strings.xml b/fermata/src/main/res/values-it/strings.xml index ce8f708b..029b3888 100644 --- a/fermata/src/main/res/values-it/strings.xml +++ b/fermata/src/main/res/values-it/strings.xml @@ -68,11 +68,7 @@ Ricerca vocale Controllo vocale Pulsante fluttuante - Pulsante \'Menù\' o \'M\' - Pulsante multimediale \'Successivo\' - Pulsante multimediale \'Precedente\' Attiva controllo vocale con clic lungo - Attiva controllo vocale con doppio clic Sostituzione comandi Sostituisci un comando con un\'altra parola o con una frase diff --git a/fermata/src/main/res/values-ru/strings.xml b/fermata/src/main/res/values-ru/strings.xml index 92c9ca94..b929b1a6 100644 --- a/fermata/src/main/res/values-ru/strings.xml +++ b/fermata/src/main/res/values-ru/strings.xml @@ -35,6 +35,28 @@ Размер панели управления Размер текста и иконок + Привязки клавиш + При нажатии + При двойном нажатии + При долгом нажатии + Стоп + Воспроизведение + Пауза + Воспроизведение/Пауза + Воспроизвести предыдущий + Воспроизвести следующий + Перемотка назад + Перемотка вперед + Увелечить громкость + Уменьшить громкость + Включить/Выключить звук + Активировать голосовое управление + Меню + Панель управления + Назад или выход + Выход + Ничего + Воспроизводить следующий трек Назад/Вперед - нажатие @@ -69,11 +91,7 @@ Голосовой поиск Голосовое управление Плавающая кнопка - Кнопка \'Меню\' или \'M\' Активировать голосовое управление, при долгом нажатии - Медиа кнопка \'Вперед\' - Медиа кнопка \'Назад\' - Активировать голосовое управление, при двойном нажатии Замена слов Заменять слово другим словом или фразой включить: играть\nвыключить: пауза\nскучно: найти в браузере ближайши бар diff --git a/fermata/src/main/res/values-tr/strings.xml b/fermata/src/main/res/values-tr/strings.xml index 96b2190c..1e8af5b7 100644 --- a/fermata/src/main/res/values-tr/strings.xml +++ b/fermata/src/main/res/values-tr/strings.xml @@ -252,10 +252,6 @@ Sanallaştırıcı Sesli kontrol Kayan düğme - "'Menü' veya 'M' butonu" - "'Skip to next' media button" - "'Skip to previous' media button" - Çift tıklamayla ses kontrolünü etkinleştir Uzun dokununca ses kontrolünü etkinleştir Kelime değiştirme on: play\noff: pause\nmerde: find in browser nearest bar diff --git a/fermata/src/main/res/values/strings.xml b/fermata/src/main/res/values/strings.xml index 43a5f05f..9fd1f2cf 100644 --- a/fermata/src/main/res/values/strings.xml +++ b/fermata/src/main/res/values/strings.xml @@ -57,6 +57,29 @@ Control panel size Text and icons size + Key bindings + On click + On long click + On double click + Stop + Play + Pause + Play/Pause + Play previous + Play next + Rewind + Fast forward + Volume up + Volume down + Mute/Unmute + Activate voice control + Menu + Control panel menu + Back or exit + Exit + Default + None + Play next on completion RW/FF - click @@ -91,11 +114,7 @@ Voice search Voice control Floating button - \'Menu\' or \'M\' button - \'Skip to next\' media button - \'Skip to previous\' media button Activate voice control on long press - Activate voice control on double click Word substitution Replace a word with an another word or a phrase on: play\noff: pause\nmerde: find in browser nearest bar diff --git a/modules/cast/src/main/java/me/aap/fermata/addon/cast/CastMediaEngine.java b/modules/cast/src/main/java/me/aap/fermata/addon/cast/CastMediaEngine.java index a8a565f2..b65aab0d 100644 --- a/modules/cast/src/main/java/me/aap/fermata/addon/cast/CastMediaEngine.java +++ b/modules/cast/src/main/java/me/aap/fermata/addon/cast/CastMediaEngine.java @@ -1,5 +1,8 @@ package me.aap.fermata.addon.cast; +import static android.media.AudioManager.ADJUST_LOWER; +import static android.media.AudioManager.ADJUST_RAISE; +import static android.media.AudioManager.ADJUST_TOGGLE_MUTE; import static com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED; import static com.google.android.gms.cast.MediaMetadata.KEY_SUBTITLE; import static com.google.android.gms.cast.MediaMetadata.KEY_TITLE; @@ -16,7 +19,6 @@ import android.annotation.SuppressLint; import android.media.AudioManager; import android.net.Uri; -import android.view.KeyEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -62,6 +64,7 @@ public class CastMediaEngine extends RemoteMediaClient.Callback implements Media MediaEngine.Listener listener; private PlayableItem source; private float speed; + private double volume; public CastMediaEngine(CastServer server, CastSession session, RemoteMediaClient client, Listener listener) { @@ -82,8 +85,7 @@ public void prepare(PlayableItem source) { this.source = source; VirtualResource src; - if (source instanceof MediaLib.ArchiveItem) { - MediaLib.ArchiveItem a = (MediaLib.ArchiveItem) source; + if (source instanceof MediaLib.ArchiveItem a) { long start = a.getStartTime(); Uri uri = a.getParent().getLocation(start, a.getEndTime() - start); src = GenericFileSystem.getInstance().getResource(Rid.create(uri)).getOrThrow(); @@ -274,14 +276,33 @@ public void releaseAudioFocus(@Nullable AudioManager audioManager, } @Override - public boolean adjustVolume(KeyEvent event) { + public boolean adjustVolume(int direction) { try { - double diff = (event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP) ? 0.025 : -0.025; - session.setVolume(session.getVolume() + diff); + double vol = session.getVolume(); + switch (direction) { + case ADJUST_LOWER: + vol -= 0.025; + break; + case ADJUST_RAISE: + vol += 0.025; + break; + case ADJUST_TOGGLE_MUTE: + if (vol > 0) { + volume = vol; + vol = 0; + } else { + vol = volume; + } + break; + default: + return false; + } + session.setVolume(vol); + return true; } catch (IOException ex) { Log.e(ex); } - return true; + return false; } @Override