Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,43 +1,44 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.plugin.compose")
}

android {
compileSdk = 36
compileSdk = 37
buildToolsVersion = "37.0.0"

defaultConfig {
applicationId = "ru.hepolise.volumekeymusicmanagermodule"
minSdk = 27
targetSdk = 36
targetSdk = 37
versionCode = rootProject.ext["appVersionCode"].toString().toInt()
versionName = rootProject.ext["appVersionName"].toString()
}

buildTypes {
getByName("release") {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
isShrinkResources = true
proguardFiles("proguard-rules.pro")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}

namespace = "ru.hepolise.volumekeytrackcontrolmodule"
namespace = "ru.hepolise.volumekeytrackcontrol"
buildFeatures {
buildConfig = true
}

packaging {
resources {
merges += "META-INF/xposed/*"
excludes += "**"
}
}
}

dependencies {
Expand Down Expand Up @@ -66,7 +67,8 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-tooling")

// Xposed Framework API dependencies
compileOnly("de.robv.android.xposed:api:82")
compileOnly("io.github.libxposed:api:102.0.0")
implementation("io.github.libxposed:service:102.0.0")

// RemotePreferences
implementation("com.crossbowffs.remotepreferences:remotepreferences:0.8")
Expand Down
42 changes: 21 additions & 21 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn io.github.libxposed.annotation.**
-adaptresourcefilecontents META-INF/xposed/java_init.list
-keep,allowoptimization,allowobfuscation public class * extends io.github.libxposed.api.XposedModule {
public <init>();
}
-dontwarn androidx.window.extensions.WindowExtensions
-dontwarn androidx.window.extensions.WindowExtensionsProvider
-dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation
-dontwarn androidx.window.extensions.core.util.function.Consumer
-dontwarn androidx.window.extensions.core.util.function.Function
-dontwarn androidx.window.extensions.core.util.function.Predicate
-dontwarn androidx.window.extensions.layout.DisplayFeature
-dontwarn androidx.window.extensions.layout.FoldingFeature
-dontwarn androidx.window.extensions.layout.WindowLayoutComponent
-dontwarn androidx.window.extensions.layout.WindowLayoutInfo
-dontwarn androidx.window.sidecar.SidecarDeviceState
-dontwarn androidx.window.sidecar.SidecarDisplayFeature
-dontwarn androidx.window.sidecar.SidecarInterface$SidecarCallback
-dontwarn androidx.window.sidecar.SidecarInterface
-dontwarn androidx.window.sidecar.SidecarProvider
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo
32 changes: 5 additions & 27 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<application
android:name="App"
android:description="@string/module_description"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
Expand All @@ -19,7 +20,7 @@

<!-- Main activity -->
<activity
android:name="ru.hepolise.volumekeytrackcontrol.ui.SettingsActivity"
android:name=".ui.SettingsActivity"
android:exported="true"
android:theme="@style/Theme.App.Starting">
<intent-filter>
Expand All @@ -29,23 +30,14 @@
</activity>

<provider
android:name="ru.hepolise.volumekeytrackcontrol.provider.RemotePrefProvider"
android:name=".provider.RemotePrefProvider"
android:authorities="${applicationId}"
android:directBootAware="true"
android:exported="true"
tools:ignore="ExportedContentProvider" />

<receiver
android:name="ru.hepolise.volumekeytrackcontrol.receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>

<service
android:name="ru.hepolise.volumekeytrackcontrol.service.RewindActionTileService"
android:name=".service.RewindActionTileService"
android:exported="true"
android:label="@string/long_press_action"
android:icon="@drawable/ic_skip_next_48dp"
Expand All @@ -54,20 +46,6 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>

<!-- metadata -->
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposedminversion"
android:value="93" />
<meta-data
android:name="xposeddescription"
android:value="Skip and pause track with long press volume keys" />
<meta-data
android:name="xposedscope"
android:resource="@array/module_scope" />
</application>

</manifest>
82 changes: 82 additions & 0 deletions app/src/main/java/ru/hepolise/volumekeytrackcontrol/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package ru.hepolise.volumekeytrackcontrol

import android.app.Application
import androidx.core.content.edit
import io.github.libxposed.service.XposedService
import io.github.libxposed.service.XposedServiceHelper
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getStatusSharedPreferences
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.concurrent.Volatile

class App : Application(), XposedServiceHelper.OnServiceListener {

companion object {
@Volatile
var xposedService: XposedService? = null
private set
private val serviceStateListeners =
CopyOnWriteArraySet<ServiceStateListener>()

private fun dispatchServiceState(
listener: ServiceStateListener,
service: XposedService?
) {
if (serviceStateListeners.contains(listener)) {
listener.onServiceStateChanged(service)
}
}

fun addServiceStateListener(
listener: ServiceStateListener,
notifyImmediately: Boolean
) {
serviceStateListeners.add(listener)
if (notifyImmediately) {
dispatchServiceState(listener, xposedService)
}
}

fun removeServiceStateListener(listener: ServiceStateListener) {
serviceStateListeners.remove(listener)
}
}

private fun notifyServiceStateChanged(service: XposedService?) {
for (listener in serviceStateListeners) {
dispatchServiceState(listener, service)
}
}

override fun onCreate() {
super.onCreate()
migratePreferences()
XposedServiceHelper.registerListener(this)
}

interface ServiceStateListener {
fun onServiceStateChanged(service: XposedService?)
}

override fun onServiceBind(service: XposedService) {
xposedService = service
notifyServiceStateChanged(xposedService)
}

override fun onServiceDied(service: XposedService) {
xposedService = null
notifyServiceStateChanged(xposedService)
}

private fun migratePreferences() {
val prefs = getStatusSharedPreferences()
val currentVersion = BuildConfig.VERSION_CODE
val savedVersion = prefs.getInt("app_version", 0)

if (savedVersion < 19) {
prefs.edit {
clear()
putInt("app_version", currentVersion)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package ru.hepolise.volumekeytrackcontrol.module

import android.content.SharedPreferences
import android.media.MediaMetadata
import android.media.session.MediaController
import ru.hepolise.volumekeytrackcontrol.module.util.StateManager
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getRewindDuration
import kotlin.math.max
import kotlin.math.min

sealed class MediaEvent {
abstract fun execute(context: ExecutionContext): Boolean

abstract fun canHandle(context: ExecutionContext): Boolean

object PlayPause : MediaEvent() {
override fun canHandle(context: ExecutionContext): Boolean {
val state = context.stateManager
return state.isUpPressed && state.isDownPressed
}

override fun execute(context: ExecutionContext): Boolean {
if (!canHandle(context)) {
context.logger("PlayPause canHandle = false, not sending")
return false
}
context.logger("Sending PlayPause")
if (context.controller.isMusicActive()) {
context.controls.pause()
} else {
context.controls.play()
}
return true
}
}

abstract class BaseEvent : MediaEvent() {
abstract val isUpBtnEvent: Boolean

override fun canHandle(context: ExecutionContext): Boolean {
context.logger("canHandle: isPrimary=${context.isPrimary}, event=${if (isUpBtnEvent) "UP" else "DOWN"}, state=${context.stateManager}")
if (!context.isPrimary) return true
val state = context.stateManager
val isPressed = if (isUpBtnEvent) state.isUpPressed else state.isDownPressed
return !isPressed
}
}

object Next : BaseEvent() {
override val isUpBtnEvent = true

override fun execute(context: ExecutionContext): Boolean {
if (!canHandle(context)) {
context.logger("Next canHandle = false, not sending")
return false
}
context.logger("Sending Next")
context.controls.skipToNext()
return true
}
}

object Prev : BaseEvent() {
override val isUpBtnEvent = false

override fun execute(context: ExecutionContext): Boolean {
if (!canHandle(context)) {
context.logger("Prev canHandle = false, not sending")
return false
}
context.logger("Sending Prev")
context.controls.skipToPrevious()
return true
}
}

object FastForward : BaseEvent() {
override val isUpBtnEvent = true

override fun execute(context: ExecutionContext): Boolean {
if (!canHandle(context)) {
context.logger("FastForward canHandle = false, not sending")
return false
}
context.logger("Sending FastForward")
val current = context.controller.playbackState?.position ?: 0L
val duration = context.controller.metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)
?: Long.MAX_VALUE
val newPos = min(current + context.prefs.getRewindDuration() * 1000L, duration)
context.controls.seekTo(newPos)
return true
}
}

object Rewind : BaseEvent() {
override val isUpBtnEvent = false

override fun execute(context: ExecutionContext): Boolean {
if (!canHandle(context)) {
context.logger("Rewind canHandle = false, not sending")
return false
}
context.logger("Sending Rewind")
val current = context.controller.playbackState?.position ?: 0L
val newPos = max(current - context.prefs.getRewindDuration() * 1000L, 0L)
context.controls.seekTo(newPos)
return true
}
}
}

data class ExecutionContext(
val controller: MediaController,
val controls: MediaController.TransportControls,
val prefs: SharedPreferences,
val logger: (String) -> Unit,
val stateManager: StateManager,
val isPrimary: Boolean
)

fun MediaController.isMusicActive(): Boolean {
return when (playbackState?.state) {
android.media.session.PlaybackState.STATE_PLAYING,
android.media.session.PlaybackState.STATE_FAST_FORWARDING,
android.media.session.PlaybackState.STATE_REWINDING,
android.media.session.PlaybackState.STATE_BUFFERING -> true

else -> false
}
}
Loading
Loading