BG Audio is now a service, more reliable & efficient

This commit is contained in:
threethan 2023-07-29 05:32:34 -04:00
parent 2ad14e7287
commit c438acdd27
12 changed files with 236 additions and 117 deletions

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\ethan\.android\avd\Pixel_3a_API_34_extension_level_7_x86_64.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-07-29T09:28:03.346931100Z" />
</component>
</project>

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
@ -34,7 +33,7 @@
</option>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -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"
}

View File

@ -5,11 +5,9 @@
<uses-feature android:glEsVersion="0x00030001" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- the following is required for background audio to query 3rd party apps -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:banner="@mipmap/ic_banner"
@ -30,14 +28,14 @@
android:resource="@xml/device_admin" />
></receiver>
<receiver
android:name=".audio.BootDeviceReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<!-- <receiver-->
<!-- android:name=".audio.BootDeviceReceiver"-->
<!-- android:enabled="true"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.BOOT_COMPLETED"/>-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<activity
android:name=".MainActivity"
@ -49,6 +47,20 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".AudioService"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>

View File

@ -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<String>? = 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<String>()
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)")
}
}

View File

@ -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("<b>")
.append(it)
.append("</b> is ")
.append(if (dpm.isApplicationHidden(deviceAdminReceiverComponentName, it)) "disabled\r" else "<b>enabled</b>\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()

View File

@ -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<String>? = 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<String>()
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)
}
}
}

View File

@ -27,9 +27,14 @@
<string name="audio_button">Hintergrundaudio alle Apps</string>
<string name="owner_enabled">Gerätebesitzer ist aktiviert!</string>
<string name="owner_disabled">Informationen zu den Funktionen des Gerätebesitzers finden Sie auf Github.</string>
<string name="audio_info">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.</string>
<string name="audio_info_none">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)</string>
<string name="audio_info_serv">Sie müssen „Oculess“ aktivieren in den Barrierefreiheitseinstellungen.\nWenn es bereits aktiviert zu sein scheint, schalten Sie es aus und wieder ein.</string>
<string name="audio_info_done">Aktiviert. Jetzt spielen alle Apps Audio im Hintergrund ab</string>
<string name="disable_ownership">Entfernen Sie als Gerätebesitzer (Quest 1/2)</string>
<string name="enable_ownership">Aktivieren Sie den Gerätebesitz</string>
<string name="owner_info">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!)</string>
<string name="copy">KOPIERE ES</string>
<string name="settings">Einstellungen</string>
<string name="service_desc">Ermöglicht Oculess, Anwendungen Berechtigungen zum Abspielen von Audio im Hintergrund zu erteilen.</string>
</resources>

View File

@ -12,7 +12,7 @@
<string name="disable_companion">Disable Oculus Companion Server</string>
<string name="enable_companion">Enable Oculus Companion Server</string>
<string name="remove_accounts">Remove Accounts (UNSAFE)</string>
<string name="remove_accounts">Remove/Check Accounts</string>
<string name="disable_ota">Disable System Updates</string>
<string name="enable_ota">Enable System Updates</string>
<string name="telemetry">Enable/Disable Telemetry Apps</string>
@ -20,7 +20,7 @@
<string name="title0">Important Info</string>
<string name="title1">Telemetry Status</string>
<string name="message0">NOTE: Set light theme, or else text will be white on white\nPlease restart after the first time!\nRepeat this step after every restart!</string>
<string name="message1">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)</string>
<string name="message1">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)</string>
<string name="message2">Device Owner has not been set!\nPlease follow the tutorial on GitHub</string>
<string name="message3">What would you like to do?</string>
<string name="message4">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.</string>
@ -30,9 +30,14 @@
<string name="enable_telemetry">Enable Telemetry</string>
<string name="disable_ownership">Remove Device Ownership</string>
<string name="enable_ownership">Enable Device Ownership</string>
<string name="audio_button">Background Audio for all Devices</string>
<string name="audio_info">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.</string>
<string name="audio_button">Background Audio for All</string>
<string name="audio_info_none">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)</string>
<string name="audio_info_serv">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.</string>
<string name="audio_info_done">Setup finished. All apps will be allowed to play audio in the background!\nYou can disable the service in accessibility settings if you want.</string>
<string name="owner_info">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!)</string>
<string name="adb" translatable="false">adb shell dpm set-device-owner com.bos.oculess/.DevAdminReceiver</string>
<string name="copy">COPY COMMAND</string>
<string name="settings">SETTINGS</string>
<string name="service_desc">Allows Oculess to give applications permissions to play audio in the background.</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/service_desc"
android:settingsActivity=""
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFlags="flagDefault"
android:notificationTimeout="250"
android:canRetrieveWindowContent="true"
android:canRequestFilterKeyEvents="true" />

View File

@ -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

View File

@ -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