From c438acdd2724e5d27431c4d6331f6feb38264681 Mon Sep 17 00:00:00 2001 From: threethan Date: Sat, 29 Jul 2023 05:32:34 -0400 Subject: [PATCH] BG Audio is now a service, more reliable & efficient --- .idea/deploymentTargetDropDown.xml | 17 +++ .idea/misc.xml | 3 +- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 34 +++-- .../main/java/com/bos/oculess/AudioService.kt | 128 ++++++++++++++++++ .../main/java/com/bos/oculess/MainActivity.kt | 46 +++++-- .../bos/oculess/audio/BootDeviceReceiver.kt | 86 ------------ app/src/main/res/values-de-rDE/strings.xml | 7 +- app/src/main/res/values/strings.xml | 13 +- .../res/xml/accessibility_service_config.xml | 9 ++ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 12 files changed, 236 insertions(+), 117 deletions(-) create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 app/src/main/java/com/bos/oculess/AudioService.kt delete mode 100644 app/src/main/java/com/bos/oculess/audio/BootDeviceReceiver.kt create mode 100644 app/src/main/res/xml/accessibility_service_config.xml diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..a2a1d40 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 6a61e12..71b83d9 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - - + diff --git a/app/build.gradle b/app/build.gradle index 84f7b88..a05a0c3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.bos.oculess" minSdkVersion 23 targetSdkVersion 33 - versionCode 12 - versionName "1.4.0" + versionCode 13 + versionName "1.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6b23e74..1515c77 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,11 +5,9 @@ - - + > - - - - - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/bos/oculess/AudioService.kt b/app/src/main/java/com/bos/oculess/AudioService.kt new file mode 100644 index 0000000..e16293d --- /dev/null +++ b/app/src/main/java/com/bos/oculess/AudioService.kt @@ -0,0 +1,128 @@ +package com.bos.oculess + +import android.accessibilityservice.AccessibilityService +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Handler +import android.provider.Settings +import android.provider.Settings.SettingNotFoundException +import android.util.Log +import android.view.accessibility.AccessibilityEvent +import com.bos.oculess.util.AppOpsUtil +class AudioService : AccessibilityService() { + companion object { + private const val TAG = "OculessAudioService" + private var audioApps: Array? = null + private var canUpdateAppList = true; + fun isAccessibilityInitialized(context: Context): Boolean { + try { + Settings.Secure.getInt( + context.applicationContext.contentResolver, + Settings.Secure.ACCESSIBILITY_ENABLED + ) + val settingValue = Settings.Secure.getString( + context.applicationContext.contentResolver, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES + ) + if (settingValue != null) { + return settingValue.contains(context.packageName) + } + } catch (e: SettingNotFoundException) { + return false + } + return false + } + fun requestAccessibility(context: Context) { + val localIntent = Intent("android.settings.ACCESSIBILITY_SETTINGS") + localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + localIntent.setPackage("com.android.settings") + context.startActivity(localIntent) + } + } + override fun onAccessibilityEvent(e: AccessibilityEvent?) { + if (e?.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + Log.i(TAG, "Trigger: TYPE_WINDOW_STATE_CHANGED") + checkUpdateAppList() + audioFix() + } else if (e?.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + Log.i(TAG, "Trigger: TYPE_WINDOW_CONTENT_CHANGED") + audioFix() + } + } + + override fun onInterrupt() { + Log.w(TAG, "Service Interrupted!") + } + + private fun checkUpdateAppList() { + if (canUpdateAppList) { + updateAudioPackages() + val handler: Handler = Handler() + canUpdateAppList = false + handler.postDelayed(Runnable { + canUpdateAppList = true + Log.i(TAG, "Allowed to re-scan packages") + }, 5000) //1s timeout before we're allowed to look for new packages + } + } + + // Audio-specific + private fun audioFix() { + enableAudioPermission() + + val handler: Handler = Handler() + val r = Runnable { + enableAudioPermission() + } + handler.postDelayed(r, 250) + handler.postDelayed(r, 1500) + } + private fun updateAudioPackages() { + Log.i(TAG, "Start package scan...") + + if (applicationContext.packageManager == null) { + Log.w(TAG, "applicationContext.packageManager is null!") + return + } + /* Get All Installed Packages for Audio */ + // Requires query-all-packages permission + val packageInfoList = applicationContext.packageManager?.getInstalledPackages(0) + val packageNames = arrayListOf() + for (packageInfo in packageInfoList!!) { + packageNames.add(packageInfo.packageName) + } + // Remove system apps + val packageInfoListSystem = applicationContext.packageManager?.getInstalledPackages( + PackageManager.MATCH_SYSTEM_ONLY) + for (packageInfoSystem in packageInfoListSystem!!) { + packageNames.remove(packageInfoSystem.packageName) + } + // Commit to app list + audioApps = packageNames.toTypedArray() + Log.i(TAG, "...end package scan") + } + + private fun enableAudioPermission() { + Log.d(TAG, "Enabling permissions (This should run in bursts but not continuously)") + + if (audioApps == null) { + Log.i(TAG, "AudioApps is null!") + return + } + var first = true + for (app in audioApps!!) { + val info: ApplicationInfo?= applicationContext.packageManager?.getApplicationInfo(app, 0) + // https://cs.android.com/android/platform/superproject/+/master:frameworks/proto_logging/stats/enums/app/enums.proto;l=138?q=PLAY_AUDIO + try { + AppOpsUtil.allowOp(applicationContext, 28, info?.uid!!, app) // Play audio +// AppOpsUtil.allowOp(applicationContext, 27, info?.uid!!, app) // Record audio + } catch (e: java.lang.SecurityException) { + Log.w(TAG, "Audio service lacks permission to set appops. Is Oculess device owner?") + } + } + Log.d(TAG, "Enabled permissions (This should occur VERY shortly after enabling permissions)") + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bos/oculess/MainActivity.kt b/app/src/main/java/com/bos/oculess/MainActivity.kt index 99ee563..da53e97 100644 --- a/app/src/main/java/com/bos/oculess/MainActivity.kt +++ b/app/src/main/java/com/bos/oculess/MainActivity.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.bos.oculess import android.app.admin.DevicePolicyManager @@ -23,7 +25,7 @@ import kotlin.concurrent.fixedRateTimer class MainActivity : AppCompatActivity() { - @RequiresApi(Build.VERSION_CODES.N) +// @RequiresApi(Build.VERSION_CODES.N) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -255,11 +257,11 @@ class MainActivity : AppCompatActivity() { message.append("") .append(it) .append(" is ") - .append(if (dpm.isApplicationHidden(deviceAdminReceiverComponentName, it)) "disabled\r" else "enabled\r") + .append(if (dpm.isApplicationHidden(deviceAdminReceiverComponentName, it)) "disabled\r" else "ENABLED\r") } val builder1: AlertDialog.Builder = AlertDialog.Builder(this) builder1.setTitle(getString(R.string.title1)) - builder1.setMessage(Html.fromHtml(message.toString(), 0)) + builder1.setMessage(message.toString()) builder1.setPositiveButton( getString(R.string.ok) ) { dialog, _ -> @@ -299,11 +301,39 @@ class MainActivity : AppCompatActivity() { audioBtn.setOnClickListener { val builder: AlertDialog.Builder = AlertDialog.Builder(this) builder.setTitle(getString(R.string.title0)) - builder.setMessage(getString(R.string.audio_info)) - builder.setPositiveButton( - getString(R.string.ok) - ) { dialog, _ -> - dialog.dismiss() + if (!isOwner) { + builder.setMessage(getString(R.string.audio_info_none)) + builder.setPositiveButton( + getString(R.string.ok) + ) { dialog, _ -> + dialog.dismiss() + } + } else if (!AudioService.isAccessibilityInitialized(applicationContext)) { + builder.setMessage(getString(R.string.audio_info_serv)) + builder.setPositiveButton( + getString(R.string.ok) + ) { dialog, _ -> + AudioService.requestAccessibility(applicationContext) + dialog.dismiss() + } + builder.setNegativeButton( + getString(R.string.cancel) + ) { dialog, _ -> + dialog.dismiss() + } + } else { + builder.setMessage(getString(R.string.audio_info_done)) + builder.setPositiveButton( + getString(R.string.settings) + ) { dialog, _ -> + AudioService.requestAccessibility(applicationContext) + dialog.dismiss() + } + builder.setNegativeButton( + getString(R.string.cancel) + ) { dialog, _ -> + dialog.dismiss() + } } val alertDialog: AlertDialog = builder.create() alertDialog.show() diff --git a/app/src/main/java/com/bos/oculess/audio/BootDeviceReceiver.kt b/app/src/main/java/com/bos/oculess/audio/BootDeviceReceiver.kt deleted file mode 100644 index 167e622..0000000 --- a/app/src/main/java/com/bos/oculess/audio/BootDeviceReceiver.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.bos.oculess.audio - -import android.app.admin.DevicePolicyManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.util.Log -import android.widget.Toast -import com.bos.oculess.util.AppOpsUtil -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit - -class BootDeviceReceiver : BroadcastReceiver() { - companion object { - private const val TAG = "OculessBootReceiver" - private var audioApps: Array? = null - private var applicationContext: Context? = null - } - - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action - val message = "BootDeviceReceiver onReceive, action is $action" - Toast.makeText(context, message, Toast.LENGTH_LONG).show() - Log.d( - TAG, - action!! - ) - if (Intent.ACTION_BOOT_COMPLETED == action) { - applicationContext = context - startLoop() - } - } - - /* Start RunAfterBootService service directly and invoke the service every 10 seconds. */ - private fun startLoop() { - val dpm = Companion.applicationContext?.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - if (! dpm.isDeviceOwnerApp(applicationContext?.packageName!!)) { - Log.w(TAG, "No device owner - canceling background audio thread") - return - } - - updateAudioPackages() - - val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() - scheduler.scheduleAtFixedRate(Runnable { - enableAudioPermission() - - }, 5, 1, TimeUnit.SECONDS) - - val scheduler2: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() - scheduler2.scheduleAtFixedRate(Runnable { - updateAudioPackages() - - }, 1, 1, TimeUnit.MINUTES) - - - } - - private fun updateAudioPackages() { - /* Get All Installed Packages for Audio */ - // Requires query-all-packages permission - val packageInfoList = applicationContext?.packageManager?.getInstalledPackages(0) - val packageNames = arrayListOf() - for (packageInfo in packageInfoList!!) { - packageNames.add(packageInfo.packageName) - } - // Remove system apps - val packageInfoListSystem = applicationContext?.packageManager?.getInstalledPackages(PackageManager.MATCH_SYSTEM_ONLY) - for (packageInfoSystem in packageInfoListSystem!!) { - packageNames.remove(packageInfoSystem.packageName) - } - // Commit to app list - audioApps = packageNames.toTypedArray() - } - - private fun enableAudioPermission() { - for (app in audioApps!!) { - val info: ApplicationInfo ?= applicationContext?.packageManager?.getApplicationInfo(app, 0) - // https://cs.android.com/android/platform/superproject/+/master:frameworks/proto_logging/stats/enums/app/enums.proto;l=138?q=PLAY_AUDIO - AppOpsUtil.allowOp(applicationContext, 28, info?.uid!!, app) - } - } -} \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index fdb024a..ec52326 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -27,9 +27,14 @@ Hintergrundaudio alle Apps Gerätebesitzer ist aktiviert! Informationen zu den Funktionen des Gerätebesitzers finden Sie auf Github. - Wenn der Gerätebesitzer aktiviert ist, können alle Apps Audio im Hintergrund abspielen (ähnlich der experimentellen Einstellung, die auf einigen Geräten verfügbar ist).\nWenn Sie gerade den Gerätebesitzer aktiviert haben, halten Sie die Ein-/Aus-Taste gedrückt und starten Sie Ihr Headset neu. + Wenn der Gerätebesitzer aktiviert und Service sind, können alle Apps Audio im Hintergrund abspielen (ähnlich der experimentellen Einstellung, die auf einigen Geräten verfügbar ist) + Sie müssen „Oculess“ aktivieren in den Barrierefreiheitseinstellungen.\nWenn es bereits aktiviert zu sein scheint, schalten Sie es aus und wieder ein. + Aktiviert. Jetzt spielen alle Apps Audio im Hintergrund ab Entfernen Sie als Gerätebesitzer (Quest 1/2) Aktivieren Sie den Gerätebesitz Dies kann rückgängig gemacht werden, aber das Entfernen von Konten ist DAUERHAFT!\n1. Entfernen Sie alle Konten mit der Schaltfläche oben \n2. Mit ADB: adb shell dpm set-device-owner com.bos.oculess/.DevAdminReceiver\n(Mehr Details auf Github!) KOPIERE ES + Einstellungen + + Ermöglicht Oculess, Anwendungen Berechtigungen zum Abspielen von Audio im Hintergrund zu erteilen. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e14ed1..31e7640 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,7 +12,7 @@ Disable Oculus Companion Server Enable Oculus Companion Server - Remove Accounts (UNSAFE) + Remove/Check Accounts Disable System Updates Enable System Updates Enable/Disable Telemetry Apps @@ -20,7 +20,7 @@ Important Info Telemetry Status NOTE: Set light theme, or else text will be white on white\nPlease restart after the first time!\nRepeat this step after every restart! - WARNING: Removing a Meta account is most likely irreversible and will PERMANANTLY DISABLE SOME FUNCTIONALITY!\nIt is required to remove ALL accounts if you wish to activate device admin.\n(Check GitHub for more info) + WARNING: Removing a Meta account may be irreversible and can PERMANENTLY DISABLE SOME FUNCTIONALITY until factory reset.\nIt is required to remove ALL accounts if you wish to activate device admin.\n(Check GitHub for more info) Device Owner has not been set!\nPlease follow the tutorial on GitHub What would you like to do? Oculess will no longer be your device owner, and may be uninstalled normally.\nThis will disable some functions, and you will have to follow the guide if you wish to re-enable. @@ -30,9 +30,14 @@ Enable Telemetry Remove Device Ownership Enable Device Ownership - Background Audio for all Devices - With device owner enabled, all apps will be allowed to play audio in the background (similar to the experimental setting available on some devices)\nIf you just activated device owner, hold the power button and restart your headset. It should be automatic after that. + Background Audio for All + With device owner and accessibility enabled, all apps will be allowed to play audio in the background (similar to the experimental setting available on some devices) + You need to enable "Oculess" in accessibility settings.\nIf it appears to be enabled already, toggle it off and on.\nClick OK to open accessibility settings. + Setup finished. All apps will be allowed to play audio in the background!\nYou can disable the service in accessibility settings if you want. This can be undone, but REMOVING ACCOUNTS IS PERMANENT.\n1. Remove all accounts with the button above\n2. Using ADB: adb shell dpm set-device-owner com.bos.oculess/.DevAdminReceiver\n(More detail on github!) adb shell dpm set-device-owner com.bos.oculess/.DevAdminReceiver COPY COMMAND + SETTINGS + + Allows Oculess to give applications permissions to play audio in the background. \ No newline at end of file diff --git a/app/src/main/res/xml/accessibility_service_config.xml b/app/src/main/res/xml/accessibility_service_config.xml new file mode 100644 index 0000000..aac7356 --- /dev/null +++ b/app/src/main/res/xml/accessibility_service_config.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4d5c2eb..c866e43 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.0.1' + classpath 'com.android.tools.build:gradle:8.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 30122bc..b2a32d0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri May 19 12:06:25 EDT 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME +zipStoreBase=GRADLE_USER_HOME \ No newline at end of file