diff --git a/build.gradle b/build.gradle index e4f20d0a..f2f0d523 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { ext { compileSdkVersion = 34 - minSdkVersion = 23 + minSdkVersion = 24 targetSdkVersion = 33 kotlin_version = '1.9.0' @@ -11,13 +11,13 @@ buildscript { desugar_version = '2.0.3' - firebase_version = '32.2.3' + firebase_version = '32.3.1' appcompat_version = '1.6.1' constraintlayout_version = '2.1.4' - core_version = '1.10.1' + core_version = '1.12.0' fragment_version = '1.6.1' - lifecycle_version = '2.6.1' + lifecycle_version = '2.6.2' preference_version = '1.2.1' recyclerview_version = '1.3.1' coresplash_version = '1.0.0' @@ -33,13 +33,17 @@ buildscript { material_version = '1.9.0' compose_compiler_version = '1.5.2' - compose_version = '1.5.0' + compose_version = '1.5.1' wear_compose_version = '1.2.0' horologist_version = '0.4.12' accompanist_version = '0.30.1' gson_version = '2.10.1' timber_version = '5.0.1' + + // Shizuku + shizuku_version = '13.1.5' + refine_version = '4.3.0' } repositories { diff --git a/hidden-api/.gitignore b/hidden-api/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/hidden-api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/hidden-api/build.gradle b/hidden-api/build.gradle new file mode 100644 index 00000000..8b841a2b --- /dev/null +++ b/hidden-api/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk rootProject.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + debug { + minifyEnabled false + } + release { + minifyEnabled true + } + } + + compileOptions { + // Sets Java compatibility to Java 8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlin { + jvmToolchain(17) + } + + namespace 'com.thewizrd.wearsettings.hidden_api' +} + +dependencies { + annotationProcessor 'dev.rikka.tools.refine:annotation-processor:4.3.0' + compileOnly 'dev.rikka.tools.refine:annotation:4.3.0' + implementation 'androidx.annotation:annotation:1.7.0' +} \ No newline at end of file diff --git a/hidden-api/consumer-rules.pro b/hidden-api/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/hidden-api/proguard-rules.pro b/hidden-api/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/hidden-api/proguard-rules.pro @@ -0,0 +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 \ No newline at end of file diff --git a/hidden-api/src/main/AndroidManifest.xml b/hidden-api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..568741e5 --- /dev/null +++ b/hidden-api/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/hidden-api/src/main/java/android/net/wifi/IWifiManager.java b/hidden-api/src/main/java/android/net/wifi/IWifiManager.java new file mode 100644 index 00000000..f7b2fd9d --- /dev/null +++ b/hidden-api/src/main/java/android/net/wifi/IWifiManager.java @@ -0,0 +1,23 @@ +package android.net.wifi; + +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.IInterface; + +import androidx.annotation.DeprecatedSinceApi; +import androidx.annotation.RequiresApi; + +public interface IWifiManager extends IInterface { + @DeprecatedSinceApi(api = Build.VERSION_CODES.N_MR1) + boolean setWifiEnabled(boolean enable); + + @RequiresApi(api = Build.VERSION_CODES.N_MR1) + boolean setWifiEnabled(String packageName, boolean enable); + + abstract class Stub extends Binder implements IWifiManager { + public static IWifiManager asInterface(IBinder obj) { + throw new RuntimeException("Stub!"); + } + } +} \ No newline at end of file diff --git a/hidden-api/src/main/java/com/android/internal/telephony/ITelephony.java b/hidden-api/src/main/java/com/android/internal/telephony/ITelephony.java new file mode 100644 index 00000000..92a74ec0 --- /dev/null +++ b/hidden-api/src/main/java/com/android/internal/telephony/ITelephony.java @@ -0,0 +1,53 @@ +package com.android.internal.telephony; + +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.IInterface; + +import androidx.annotation.DeprecatedSinceApi; +import androidx.annotation.RequiresApi; + +public interface ITelephony extends IInterface { + @DeprecatedSinceApi(api = Build.VERSION_CODES.P) + void setDataEnabled(int subId, boolean enable); + + @DeprecatedSinceApi(api = Build.VERSION_CODES.P) + boolean getDataEnabled(int subId); + + @RequiresApi(api = Build.VERSION_CODES.P) + @DeprecatedSinceApi(api = Build.VERSION_CODES.S) + void setUserDataEnabled(int subId, boolean enable); + + @RequiresApi(api = Build.VERSION_CODES.P) + @DeprecatedSinceApi(api = Build.VERSION_CODES.S) + boolean isUserDataEnabled(int subId); + + @RequiresApi(api = Build.VERSION_CODES.S) + @DeprecatedSinceApi(api = Build.VERSION_CODES.TIRAMISU) + void setDataEnabledForReason(int subId, int reason, boolean enable); + + @RequiresApi(api = Build.VERSION_CODES.S) + boolean isDataEnabledForReason(int subId, int reason); + + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + void setDataEnabledForReason(int subId, int reason, boolean enable, String callingPackage); + + @DeprecatedSinceApi(api = Build.VERSION_CODES.TIRAMISU) + boolean enableDataConnectivity(); + + @DeprecatedSinceApi(api = Build.VERSION_CODES.TIRAMISU) + boolean disableDataConnectivity(); + + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + boolean enableDataConnectivity(String callingPackage); + + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + boolean disableDataConnectivity(String callingPackage); + + abstract class Stub extends Binder implements ITelephony { + public static ITelephony asInterface(IBinder obj) { + throw new RuntimeException("Stub!"); + } + } +} diff --git a/mobile/build.gradle b/mobile/build.gradle index cd2315e9..64173f43 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -21,8 +21,8 @@ android { minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format [TargetSDK, Version Name, Build Number, Variant Code (Android: 00, WearOS: 01)] - versionCode 331913000 - versionName "1.13.0" + versionCode 331913100 + versionName "1.13.1" vectorDrawables.useSupportLibrary = true } @@ -89,7 +89,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.core:core-splashscreen:$coresplash_version" - implementation 'com.google.android.gms:play-services-wearable:18.0.0' + implementation 'com.google.android.gms:play-services-wearable:18.1.0' implementation platform("com.google.firebase:firebase-bom:$firebase_version") implementation 'com.google.firebase:firebase-analytics' diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 56170107..f5285cd3 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -9,14 +9,20 @@ - - + + + + + + + + + + + + + + + = Build.VERSION_CODES.R) { + telephonyManager.supportedModemCount + } else { + telephonyManager.phoneCount } - override fun onUnavailable() { - super.onUnavailable() - WearableWorker.sendActionUpdate(appContext, Actions.MOBILEDATA) + if (modemCount > 1) { + SubscriptionListener.registerListener(appContext) } } - ) - }.onFailure { - // SecurityException: Package android does not belong to xxxxx - // https://issuetracker.google.com/issues/175055271 - Logger.writeLine(Log.ERROR, it) + } } val oldHandler = Thread.getDefaultUncaughtExceptionHandler() diff --git a/mobile/src/main/java/com/thewizrd/simplewear/PermissionCheckFragment.kt b/mobile/src/main/java/com/thewizrd/simplewear/PermissionCheckFragment.kt index d4275c4c..9da7b1db 100644 --- a/mobile/src/main/java/com/thewizrd/simplewear/PermissionCheckFragment.kt +++ b/mobile/src/main/java/com/thewizrd/simplewear/PermissionCheckFragment.kt @@ -4,31 +4,36 @@ import android.Manifest import android.annotation.TargetApi import android.app.Activity import android.app.admin.DevicePolicyManager -import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothAdapter +import android.bluetooth.le.ScanFilter import android.companion.* import android.content.* -import android.content.IntentSender.SendIntentException import android.content.pm.PackageManager import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.CountDownTimer -import android.os.Parcelable import android.provider.Settings import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat -import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.thewizrd.shared_resources.helpers.WearSettingsHelper +import com.thewizrd.shared_resources.helpers.WearableHelper import com.thewizrd.shared_resources.lifecycle.LifecycleAwareFragment import com.thewizrd.shared_resources.tasks.delayLaunch import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.simplewear.databinding.FragmentPermcheckBinding import com.thewizrd.simplewear.helpers.PhoneStatusHelper +import com.thewizrd.simplewear.helpers.PhoneStatusHelper.deActivateDeviceAdmin import com.thewizrd.simplewear.helpers.PhoneStatusHelper.isCameraPermissionEnabled import com.thewizrd.simplewear.helpers.PhoneStatusHelper.isDeviceAdminEnabled import com.thewizrd.simplewear.helpers.PhoneStatusHelper.isNotificationAccessAllowed @@ -36,24 +41,132 @@ import com.thewizrd.simplewear.media.MediaControllerService import com.thewizrd.simplewear.services.CallControllerService import com.thewizrd.simplewear.services.InCallManagerService import com.thewizrd.simplewear.services.NotificationListener -import com.thewizrd.simplewear.wearable.WearableDataListenerService +import com.thewizrd.simplewear.telephony.SubscriptionListener +import com.thewizrd.simplewear.utils.associate +import com.thewizrd.simplewear.utils.disassociateAll +import com.thewizrd.simplewear.utils.hasAssociations import com.thewizrd.simplewear.wearable.WearableWorker import com.thewizrd.simplewear.wearable.WearableWorker.Companion.enqueueAction +import kotlinx.coroutines.launch import java.util.regex.Pattern class PermissionCheckFragment : LifecycleAwareFragment() { companion object { private const val TAG = "PermissionCheckFragment" - private const val CAMERA_REQCODE = 0 - private const val DEVADMIN_REQCODE = 1 - private const val MANAGECALLS_REQCODE = 2 - private const val BTCONNECT_REQCODE = 3 - private const val NOTIF_REQCODE = 4 - private const val SELECT_DEVICE_REQUEST_CODE = 42 } private lateinit var binding: FragmentPermcheckBinding - private var timer: CountDownTimer? = null + + private lateinit var permissionRequestLauncher: ActivityResultLauncher> + private lateinit var devAdminResultLauncher: ActivityResultLauncher + private lateinit var companionDeviceResultLauncher: ActivityResultLauncher + private lateinit var companionBTPermRequestLauncher: ActivityResultLauncher + private lateinit var requestBtResultLauncher: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + permissionRequestLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + permissions.entries.forEach { (permission, granted) -> + when (permission) { + Manifest.permission.CAMERA -> { + if (granted) { + updateCamPermText(true) + } else { + updateCamPermText(false) + Toast.makeText( + context, + R.string.error_permissiondenied, + Toast.LENGTH_SHORT + ).show() + } + } + + Manifest.permission.READ_PHONE_STATE -> { + if (granted) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !InCallManagerService.hasPermission( + requireContext() + ) + ) { + startDevicePairing() + } + + // Register listener for state changes + if (!SubscriptionListener.isRegistered) { + SubscriptionListener.registerListener(requireContext()) + } + } + + updateManageCallsText(granted) + } + + Manifest.permission.BLUETOOTH_CONNECT -> { + if (granted) { + updateBTPref(true) + } else { + updateBTPref(false) + Toast.makeText( + context, + R.string.error_permissiondenied, + Toast.LENGTH_SHORT + ) + .show() + } + } + + Manifest.permission.POST_NOTIFICATIONS -> { + if (granted) { + updateNotificationPref(true) + } else { + updateNotificationPref(false) + Toast.makeText( + context, + R.string.error_permissiondenied, + Toast.LENGTH_SHORT + ) + .show() + } + } + } + } + } + + devAdminResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + updateDeviceAdminText(it.resultCode == Activity.RESULT_OK) + } + + companionDeviceResultLauncher = + registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { + updatePermissions() + } + + companionBTPermRequestLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (granted) { + startDevicePairing() + } else { + Toast.makeText( + context, + R.string.error_permissiondenied, + Toast.LENGTH_SHORT + ) + .show() + + binding.companionPairProgress.visibility = View.GONE + } + } + + requestBtResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + pairDevice() + } else { + binding.companionPairProgress.visibility = View.GONE + } + } + } override fun onCreateView( inflater: LayoutInflater, @@ -64,19 +177,39 @@ class PermissionCheckFragment : LifecycleAwareFragment() { binding = FragmentPermcheckBinding.inflate(inflater, container, false) binding.torchPref.setOnClickListener { if (!isCameraPermissionEnabled(requireContext())) { - requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_REQCODE) + permissionRequestLauncher.launch(arrayOf(Manifest.permission.CAMERA)) } } binding.deviceadminPref.setOnClickListener { if (!isDeviceAdminEnabled(requireContext())) { - val mScreenLockAdmin = - ComponentName(requireContext(), ScreenLockAdminReceiver::class.java) + MaterialAlertDialogBuilder(it.context) + .setTitle(android.R.string.dialog_alert_title) + .setMessage(R.string.prompt_alert_message_device_admin) + .setPositiveButton(android.R.string.ok) { d, which -> + if (which == DialogInterface.BUTTON_POSITIVE) { + val mScreenLockAdmin = + ComponentName(it.context, ScreenLockAdminReceiver::class.java) + + runCatching { + // Launch the activity to have the user enable our admin. + val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN) + intent.putExtra( + DevicePolicyManager.EXTRA_DEVICE_ADMIN, + mScreenLockAdmin + ) + devAdminResultLauncher.launch(intent) + } + } - runCatching { - // Launch the activity to have the user enable our admin. - val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN) - intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, mScreenLockAdmin) - startActivityForResult(intent, DEVADMIN_REQCODE) + d.dismiss() + } + .setCancelable(true) + .setNegativeButton(android.R.string.cancel, null) + .show() + } else { + deActivateDeviceAdmin(requireContext()) + lifecycleScope.delayLaunch(timeMillis = 1000) { + updatePermissions() } } } @@ -122,10 +255,7 @@ class PermissionCheckFragment : LifecycleAwareFragment() { Manifest.permission.READ_PHONE_STATE ) == PackageManager.PERMISSION_DENIED ) { - requestPermissions( - arrayOf(Manifest.permission.READ_PHONE_STATE), - MANAGECALLS_REQCODE - ) + permissionRequestLauncher.launch(arrayOf(Manifest.permission.READ_PHONE_STATE)) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !InCallManagerService.hasPermission( requireContext() ) @@ -220,20 +350,24 @@ class PermissionCheckFragment : LifecycleAwareFragment() { Manifest.permission.POST_NOTIFICATIONS ) == PackageManager.PERMISSION_DENIED ) { - requestPermissions( - arrayOf(Manifest.permission.POST_NOTIFICATIONS), - NOTIF_REQCODE - ) + permissionRequestLauncher.launch(arrayOf(Manifest.permission.POST_NOTIFICATIONS)) } } binding.notifPref.visibility = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) View.VISIBLE else View.GONE + binding.btPref.setOnClickListener { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isBluetoothConnectPermGranted()) { + permissionRequestLauncher.launch(arrayOf(Manifest.permission.BLUETOOTH_CONNECT)) + } + } + binding.btPref.isVisible = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + return binding.root } override fun onPause() { - LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(mReceiver) super.onPause() } @@ -251,55 +385,14 @@ class PermissionCheckFragment : LifecycleAwareFragment() { private fun startDevicePairing() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (!isBluetoothConnectPermGranted()) { - requestPermissions( - arrayOf(Manifest.permission.BLUETOOTH_CONNECT), - BTCONNECT_REQCODE - ) + companionBTPermRequestLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT) return } } - LocalBroadcastManager.getInstance(requireContext()) - .registerReceiver( - mReceiver, - IntentFilter(WearableDataListenerService.ACTION_GETCONNECTEDNODE) - ) - if (timer == null) { - timer = object : CountDownTimer(5000, 1000) { - override fun onTick(millisUntilFinished: Long) {} - override fun onFinish() { - if (context != null) { - Toast.makeText( - context, - R.string.message_watchbttimeout, - Toast.LENGTH_LONG - ).show() - binding.companionPairProgress.visibility = View.GONE - Logger.writeLine(Log.INFO, "%s: BT Request Timeout", TAG) - // Device not found showing all - pairDevice() - } - } - } - } - timer?.start() binding.companionPairProgress.visibility = View.VISIBLE enqueueAction(requireContext(), WearableWorker.ACTION_REQUESTBTDISCOVERABLE) - Logger.writeLine(Log.INFO, "%s: ACTION_REQUESTBTDISCOVERABLE", TAG) - } - - // Android Q+ - private val mReceiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (WearableDataListenerService.ACTION_GETCONNECTEDNODE == intent.action) { - timer?.cancel() - binding.companionPairProgress.visibility = View.GONE - Logger.writeLine(Log.INFO, "%s: node received", TAG) - pairDevice() - LocalBroadcastManager.getInstance(context) - .unregisterReceiver(this) - } - } + pairDevice() } @TargetApi(Build.VERSION_CODES.Q) @@ -308,46 +401,32 @@ class PermissionCheckFragment : LifecycleAwareFragment() { val deviceManager = requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager - for (assoc in deviceManager.associations) { - if (assoc != null) { - runCatching { - deviceManager.disassociate(assoc) - }.onFailure { - Logger.writeLine(Log.ERROR, it) - } - } - } + deviceManager.disassociateAll() updatePairPermText(false) val request = AssociationRequest.Builder().apply { + addDeviceFilter( + BluetoothDeviceFilter.Builder() + .setNamePattern(Pattern.compile(".*", Pattern.DOTALL)) + .build() + ) if (BuildConfig.DEBUG) { - addDeviceFilter( - BluetoothDeviceFilter.Builder() - .setNamePattern(Pattern.compile(".*", Pattern.DOTALL)) - .build() - ) addDeviceFilter( WifiDeviceFilter.Builder() .setNamePattern(Pattern.compile(".*", Pattern.DOTALL)) .build() ) - addDeviceFilter( - BluetoothLeDeviceFilter.Builder() - .setNamePattern(Pattern.compile(".*", Pattern.DOTALL)) - .build() - ) - } else { - addDeviceFilter( - BluetoothDeviceFilter.Builder() - .setNamePattern(Pattern.compile(".*", Pattern.DOTALL)) - .build() - ) - addDeviceFilter( - BluetoothLeDeviceFilter.Builder() - .setNamePattern(Pattern.compile(".*", Pattern.DOTALL)) - .build() - ) } + addDeviceFilter( + BluetoothLeDeviceFilter.Builder() + .setNamePattern(Pattern.compile(".*", Pattern.DOTALL)) + .setScanFilter( + ScanFilter.Builder() + .setServiceUuid(WearableHelper.getBLEServiceUUID()) + .build() + ) + .build() + ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH) @@ -358,47 +437,55 @@ class PermissionCheckFragment : LifecycleAwareFragment() { // Verify bluetooth permissions if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isBluetoothConnectPermGranted()) { - requestPermissions( - arrayOf(Manifest.permission.BLUETOOTH_CONNECT), - BTCONNECT_REQCODE - ) + companionBTPermRequestLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT) + return@runWithView + } + + if (!PhoneStatusHelper.isBluetoothEnabled(requireContext())) { + runCatching { + requestBtResultLauncher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } return@runWithView } Toast.makeText(requireContext(), R.string.message_watchbtdiscover, Toast.LENGTH_LONG) .show() - delayLaunch(timeMillis = 5000) { + delayLaunch(timeMillis = 3500) { Logger.writeLine(Log.INFO, "%s: sending pair request", TAG) - // Enable Bluetooth to discover devices - context?.let { - if (!PhoneStatusHelper.isBluetoothEnabled(it)) { - PhoneStatusHelper.setBluetoothEnabled(it, true) - } - } - deviceManager.associate(request, object : CompanionDeviceManager.Callback() { - override fun onDeviceFound(chooserLauncher: IntentSender) { - if (context == null) return - try { - startIntentSenderForResult( - chooserLauncher, - SELECT_DEVICE_REQUEST_CODE, null, 0, 0, 0, null - ) - } catch (e: SendIntentException) { - Logger.writeLine(Log.ERROR, e) - } - } - override fun onFailure(error: CharSequence?) { + deviceManager.associate( + request, + onDeviceFound = { + context?.let { _ -> + runCatching { + lifecycleScope.launch { + binding.companionPairProgress.visibility = View.GONE + } + companionDeviceResultLauncher.launch( + IntentSenderRequest.Builder(it) + .build() + ) + }.onFailure { + Logger.writeLine(Log.ERROR, it) + } + } + }, + onFailure = { error -> Logger.writeLine(Log.ERROR, "%s: failed to find any devices; $error", TAG) - if (context == null) return - Toast.makeText( - context, - R.string.message_nodevices_found, - Toast.LENGTH_SHORT - ).show() + + context?.let { ctx -> + lifecycleScope.launch { + binding.companionPairProgress.visibility = View.GONE + Toast.makeText( + ctx, + R.string.message_nodevices_found, + Toast.LENGTH_SHORT + ).show() + } + } } - }, null) + ) } } } @@ -433,7 +520,7 @@ class PermissionCheckFragment : LifecycleAwareFragment() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val deviceManager = requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager - updatePairPermText(deviceManager.associations.isNotEmpty()) + updatePairPermText(deviceManager.hasAssociations()) } binding.bridgeMediaPref.isEnabled = notListenerEnabled @@ -511,90 +598,26 @@ class PermissionCheckFragment : LifecycleAwareFragment() { binding.notifPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - when (requestCode) { - DEVADMIN_REQCODE -> updateDeviceAdminText(resultCode == Activity.RESULT_OK) - SELECT_DEVICE_REQUEST_CODE -> if (data != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val parcel = - data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE) - if (parcel is BluetoothDevice) { - if (parcel.bondState != BluetoothDevice.BOND_BONDED) { - parcel.createBond() - } - } - - updatePermissions() - } - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - val permGranted = - grantResults.isNotEmpty() && !grantResults.contains(PackageManager.PERMISSION_DENIED) - - when (requestCode) { - CAMERA_REQCODE -> { - // If request is cancelled, the result arrays are empty. - if (permGranted) { - // permission was granted, yay! - // Do the task you need to do. - updateCamPermText(true) - } else { - // permission denied, boo! Disable the - // functionality that depends on this permission. - updateCamPermText(false) - Toast.makeText(context, R.string.error_permissiondenied, Toast.LENGTH_SHORT) - .show() - } - } - MANAGECALLS_REQCODE -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !InCallManagerService.hasPermission( - requireContext() - ) - ) { - startDevicePairing() - } else { - updateManageCallsText(permGranted) - } - } - - BTCONNECT_REQCODE -> { - if (permGranted) { - startDevicePairing() - } else { - Toast.makeText(context, R.string.error_permissiondenied, Toast.LENGTH_SHORT) - .show() - } - } - - NOTIF_REQCODE -> { - if (permGranted) { - updateNotificationPref(true) - } else { - updateNotificationPref(false) - Toast.makeText(context, R.string.error_permissiondenied, Toast.LENGTH_SHORT) - .show() - } - } - } + private fun updateBTPref(enabled: Boolean) { + binding.btPrefSummary.setText(if (enabled) R.string.permission_bt_enabled else R.string.permission_bt_disabled) + binding.btPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED) } + @Suppress("DEPRECATION") private fun requestUninstall() { val ctx = requireContext() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - try { + runCatching { startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = Uri.parse("package:${ctx.packageName}") flags = Intent.FLAG_ACTIVITY_NEW_TASK }) - } catch (e: ActivityNotFoundException) { } } else { - try { + runCatching { startActivity(Intent(Intent.ACTION_UNINSTALL_PACKAGE).apply { data = Uri.parse("package:${ctx.packageName}") }) - } catch (e: ActivityNotFoundException) { } } } diff --git a/mobile/src/main/java/com/thewizrd/simplewear/helpers/PhoneStatusHelper.kt b/mobile/src/main/java/com/thewizrd/simplewear/helpers/PhoneStatusHelper.kt index 4880d22a..acfc979d 100644 --- a/mobile/src/main/java/com/thewizrd/simplewear/helpers/PhoneStatusHelper.kt +++ b/mobile/src/main/java/com/thewizrd/simplewear/helpers/PhoneStatusHelper.kt @@ -1,6 +1,7 @@ package com.thewizrd.simplewear.helpers import android.Manifest +import android.annotation.SuppressLint import android.app.ActivityManager import android.app.NotificationManager import android.app.admin.DevicePolicyManager @@ -14,7 +15,6 @@ import android.content.pm.PackageManager import android.location.LocationManager import android.media.AudioManager import android.net.ConnectivityManager -import android.net.NetworkCapabilities import android.net.wifi.WifiConfiguration import android.net.wifi.WifiManager import android.os.BatteryManager @@ -22,8 +22,11 @@ import android.os.Build import android.os.Handler import android.os.PowerManager import android.provider.Settings +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager import android.util.Log import android.view.KeyEvent +import androidx.annotation.DeprecatedSinceApi import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.core.location.LocationManagerCompat @@ -33,6 +36,7 @@ import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.simplewear.ScreenLockAdminReceiver import com.thewizrd.simplewear.services.TorchService import com.thewizrd.simplewear.services.TorchService.Companion.enqueueWork +import com.thewizrd.simplewear.utils.hasAssociations import kotlinx.coroutines.delay import java.lang.reflect.Method import java.util.* @@ -72,6 +76,7 @@ object PhoneStatusHelper { return false } + @DeprecatedSinceApi(Build.VERSION_CODES.Q) fun setWifiEnabled(context: Context, enable: Boolean): ActionStatus { if (ContextCompat.checkSelfPermission( context, @@ -95,6 +100,7 @@ object PhoneStatusHelper { return btService.adapter?.isEnabled ?: false } + @DeprecatedSinceApi(Build.VERSION_CODES.TIRAMISU) fun setBluetoothEnabled(context: Context, enable: Boolean): ActionStatus { val btService = context.applicationContext.getSystemService(BluetoothManager::class.java) return btService.adapter?.let { @@ -112,13 +118,41 @@ object PhoneStatusHelper { } ?: ActionStatus.FAILURE } + @SuppressLint("MissingPermission") fun isMobileDataEnabled(context: Context): Boolean { return try { - val mobileDataSettingEnabled = - Settings.Global.getInt(context.contentResolver, "mobile_data", 0) == 1 - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val cap = cm.getNetworkCapabilities(cm.activeNetwork) - cap != null && cap.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || mobileDataSettingEnabled + var telephonyManager = context.getSystemService(TelephonyManager::class.java) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val activeSubId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + SubscriptionManager.getActiveDataSubscriptionId().takeUnless { + it == SubscriptionManager.INVALID_SUBSCRIPTION_ID + } ?: SubscriptionManager.getDefaultSubscriptionId() + } else { + SubscriptionManager.getDefaultSubscriptionId() + } + if (activeSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + telephonyManager = telephonyManager.createForSubscriptionId(activeSubId) + } + + telephonyManager.isDataEnabled + } else { + if (telephonyManager.phoneCount > 1) { + val activeSubId = SubscriptionManager.getDefaultSubscriptionId() + + if (activeSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + Settings.Global.getInt( + context.contentResolver, + "mobile_data${activeSubId}", + 0 + ) == 1 + } else { + Settings.Global.getInt(context.contentResolver, "mobile_data", 0) == 1 + } + } else { + Settings.Global.getInt(context.contentResolver, "mobile_data", 0) == 1 + } + } } catch (e: Exception) { Logger.writeLine(Log.ERROR, e) false @@ -231,15 +265,26 @@ object PhoneStatusHelper { } fun isDeviceAdminEnabled(context: Context): Boolean { - val mDPM = context.applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val mScreenLockAdmin = ComponentName(context.applicationContext, ScreenLockAdminReceiver::class.java) + val mDPM = + context.applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val mScreenLockAdmin = + ComponentName(context.applicationContext, ScreenLockAdminReceiver::class.java) return mDPM.isAdminActive(mScreenLockAdmin) } + fun deActivateDeviceAdmin(context: Context) { + val mDPM = + context.applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val mScreenLockAdmin = + ComponentName(context.applicationContext, ScreenLockAdminReceiver::class.java) + mDPM.removeActiveAdmin(mScreenLockAdmin) + } + fun lockScreen(context: Context): ActionStatus { if (!isDeviceAdminEnabled(context)) return ActionStatus.PERMISSION_DENIED return try { - val mDPM = context.applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val mDPM = + context.applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager mDPM.lockNow() ActionStatus.SUCCESS } catch (e: Exception) { @@ -513,8 +558,7 @@ object PhoneStatusHelper { fun companionDeviceAssociated(context: Context): Boolean { val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager - val associatedDevices = deviceManager.associations - return associatedDevices.isNotEmpty() + return deviceManager.hasAssociations() } fun callStatePermissionEnabled(context: Context): Boolean { @@ -572,6 +616,19 @@ object PhoneStatusHelper { } } + fun openBTSettings(context: Context): ActionStatus { + return try { + context.startActivity( + Intent(Settings.ACTION_BLUETOOTH_SETTINGS) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + ActionStatus.SUCCESS + } catch (e: Exception) { + Logger.writeLine(Log.ERROR, e) + ActionStatus.FAILURE + } + } + fun isWriteSystemSettingsPermissionEnabled(context: Context): Boolean { return Settings.System.canWrite(context) } diff --git a/mobile/src/main/java/com/thewizrd/simplewear/telephony/SubscriptionListener.kt b/mobile/src/main/java/com/thewizrd/simplewear/telephony/SubscriptionListener.kt new file mode 100644 index 00000000..33649b0e --- /dev/null +++ b/mobile/src/main/java/com/thewizrd/simplewear/telephony/SubscriptionListener.kt @@ -0,0 +1,138 @@ +package com.thewizrd.simplewear.telephony + +import android.Manifest +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.telephony.SubscriptionManager +import android.util.Log +import androidx.core.content.PermissionChecker +import com.thewizrd.shared_resources.actions.Actions +import com.thewizrd.shared_resources.utils.Logger +import com.thewizrd.simplewear.App +import com.thewizrd.simplewear.wearable.WearableWorker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.concurrent.Executors + +object SubscriptionListener { + private val subMap by lazy { + mutableMapOf() + } + + var isRegistered = false + private set + + private val listener = lazy { + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + updateActiveSubscriptions() + } + } + } + + fun registerListener(context: Context): Boolean { + return runCatching { + val appContext = context.applicationContext + val subMgr = appContext.getSystemService(SubscriptionManager::class.java) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + subMgr.addOnSubscriptionsChangedListener( + Executors.newSingleThreadExecutor(), + listener.value + ) + } else { + subMgr.addOnSubscriptionsChangedListener(listener.value) + } + + GlobalScope.launch(Dispatchers.Default) { + listener.value.onSubscriptionsChanged() + } + + true + }.getOrElse { + Logger.writeLine(Log.ERROR, it) + false + }.apply { + isRegistered = this + } + } + + private fun updateActiveSubscriptions() { + runCatching { + val appContext = App.instance.appContext + val subMgr = appContext.getSystemService(SubscriptionManager::class.java) + + if (PermissionChecker.checkSelfPermission( + appContext, + Manifest.permission.READ_PHONE_STATE + ) != PermissionChecker.PERMISSION_GRANTED + ) { + unregisterListener(appContext) + return + } + + // Get active SIMs (subscriptions) + val subList = subMgr.activeSubscriptionInfoList + val activeSubIds = subList.map { it.subscriptionId } + + // Remove any subs which are no longer active + val entriesToRemove = subMap.filterNot { + activeSubIds.contains(it.key) + } + + entriesToRemove.forEach { (id, obs) -> + subMap.remove(id) + appContext.applicationContext.contentResolver.unregisterContentObserver(obs) + } + + // Register any new subs + subMgr.activeSubscriptionInfoList.forEach { + if (it.subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + if (!subMap.containsKey(it.subscriptionId)) { + // Register listener for mobile data setting + val setting = Settings.Global.getUriFor("mobile_data${it.subscriptionId}") + val observer = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + if (uri.toString().contains("mobile_data")) { + WearableWorker.sendActionUpdate(appContext, Actions.MOBILEDATA) + } + } + } + appContext.contentResolver.registerContentObserver(setting, false, observer) + subMap[it.subscriptionId] = observer + } + } + } + } + } + + private fun unregisterLister() { + unregisterListener(App.instance.appContext) + } + + fun unregisterListener(context: Context) { + runCatching { + val appContext = context.applicationContext + + subMap.values.forEach { + appContext.contentResolver.unregisterContentObserver(it) + } + + subMap.clear() + + if (listener.isInitialized()) { + val subMgr = appContext.getSystemService(SubscriptionManager::class.java) + subMgr.removeOnSubscriptionsChangedListener(listener.value) + } + } + + isRegistered = false + } +} \ No newline at end of file diff --git a/mobile/src/main/java/com/thewizrd/simplewear/utils/CompanionDeviceManagerCompat.kt b/mobile/src/main/java/com/thewizrd/simplewear/utils/CompanionDeviceManagerCompat.kt new file mode 100644 index 00000000..a3578322 --- /dev/null +++ b/mobile/src/main/java/com/thewizrd/simplewear/utils/CompanionDeviceManagerCompat.kt @@ -0,0 +1,76 @@ +@file:RequiresApi(Build.VERSION_CODES.O) + +package com.thewizrd.simplewear.utils + +import android.companion.AssociationRequest +import android.companion.CompanionDeviceManager +import android.content.IntentSender +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.thewizrd.shared_resources.utils.Logger +import java.util.concurrent.Executors + +fun CompanionDeviceManager.disassociateAll() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + for (assoc in myAssociations) { + if (assoc != null) { + runCatching { + disassociate(assoc.id) + }.onFailure { + Logger.writeLine(Log.ERROR, it) + } + } + } + } else { + for (assoc in associations) { + if (assoc != null) { + runCatching { + disassociate(assoc) + }.onFailure { + Logger.writeLine(Log.ERROR, it) + } + } + } + } +} + +fun CompanionDeviceManager.hasAssociations(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + myAssociations.isNotEmpty() + } else { + associations.isNotEmpty() + } +} + +fun CompanionDeviceManager.associate( + request: AssociationRequest, + onDeviceFound: (IntentSender) -> Unit, + onFailure: (CharSequence?) -> Unit +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + associate( + request, + Executors.newSingleThreadExecutor(), + object : CompanionDeviceManager.Callback() { + override fun onAssociationPending(intentSender: IntentSender) { + onDeviceFound.invoke(intentSender) + } + + override fun onFailure(error: CharSequence?) { + onFailure.invoke(error) + } + }) + } else { + associate(request, object : CompanionDeviceManager.Callback() { + @Deprecated("Deprecated in Java", ReplaceWith("onAssociationPending")) + override fun onDeviceFound(intentSender: IntentSender) { + onDeviceFound.invoke(intentSender) + } + + override fun onFailure(error: CharSequence?) { + onFailure.invoke(error) + } + }, null) + } +} \ No newline at end of file diff --git a/mobile/src/main/java/com/thewizrd/simplewear/wearable/RemoteLaunchActivity.kt b/mobile/src/main/java/com/thewizrd/simplewear/wearable/RemoteLaunchActivity.kt new file mode 100644 index 00000000..8a18f8a7 --- /dev/null +++ b/mobile/src/main/java/com/thewizrd/simplewear/wearable/RemoteLaunchActivity.kt @@ -0,0 +1,32 @@ +package com.thewizrd.simplewear.wearable + +import android.annotation.SuppressLint +import android.util.Log +import androidx.activity.ComponentActivity +import com.thewizrd.shared_resources.helpers.WearableHelper +import com.thewizrd.shared_resources.helpers.WearableHelper.toLaunchIntent +import com.thewizrd.shared_resources.utils.Logger + +@SuppressLint("CustomSplashScreen") +class RemoteLaunchActivity : ComponentActivity() { + override fun onStart() { + super.onStart() + + intent?.data?.let { uri -> + if (WearableHelper.isRemoteLaunchUri(uri)) { + runCatching { + this.startActivity(uri.toLaunchIntent()) + }.onFailure { e -> + Logger.writeLine( + Log.ERROR, + e, + "%s: Unable to launch intent remotely - $uri", + this::class.java.simpleName + ) + } + } + } + + finishAffinity() + } +} \ No newline at end of file diff --git a/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableManager.kt b/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableManager.kt index 0c4c0ce9..25e4bd72 100644 --- a/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableManager.kt +++ b/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableManager.kt @@ -1,7 +1,6 @@ package com.thewizrd.simplewear.wearable import android.app.Activity -import android.companion.CompanionDeviceManager import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context @@ -164,13 +163,16 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen ) } } - } else { // Android Q+ Devices + } else { + // Android Q+ Devices // Android Q puts a limitation on starting activities from the background // We are allowed to bypass this if we have a device registered as companion, // which will be our WearOS device; Check if device is associated before we start - val deviceManager = mContext.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager - val associated_devices = deviceManager.associations - if (associated_devices.isEmpty()) { + // OR if SYSTEM_ALERT_WINDOW is granted + if (!PhoneStatusHelper.companionDeviceAssociated(mContext) && !android.provider.Settings.canDrawOverlays( + mContext + ) + ) { // No devices associated; send message to user sendMessage( nodeID, @@ -515,15 +517,22 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen Logger.writeLine(Log.ERROR, e) } } - } else { // Android Q+ Devices + } else { + // Android Q+ Devices // Android Q puts a limitation on starting activities from the background // We are allowed to bypass this if we have a device registered as companion, // which will be our WearOS device; Check if device is associated before we start - val deviceManager = mContext.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager - val associated_devices = deviceManager.associations - if (associated_devices.isEmpty()) { + // OR if SYSTEM_ALERT_WINDOW is granted + if (!PhoneStatusHelper.companionDeviceAssociated(mContext) && !android.provider.Settings.canDrawOverlays( + mContext + ) + ) { // No devices associated; send message to user - sendMessage(nodeID, WearableHelper.LaunchAppPath, ActionStatus.PERMISSION_DENIED.name.stringToBytes()) + sendMessage( + nodeID, + WearableHelper.LaunchAppPath, + ActionStatus.PERMISSION_DENIED.name.stringToBytes() + ) } else { try { mContext.startActivity(appIntent) @@ -778,8 +787,33 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen } Actions.BLUETOOTH -> { tA = action as ToggleAction - tA.setActionSuccessful(PhoneStatusHelper.setBluetoothEnabled(mContext, tA.isEnabled)) - sendMessage(nodeID, WearableHelper.ActionsPath, JSONParser.serializer(tA, Action::class.java).stringToBytes()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (WearSettingsHelper.isWearSettingsInstalled()) { + val status = performRemoteAction(action) + if (status == ActionStatus.REMOTE_FAILURE || + status == ActionStatus.REMOTE_PERMISSION_DENIED + ) { + tA.setActionSuccessful(status) + WearSettingsHelper.launchWearSettings() + } + } else { + /* BluetoothAdapter.enable/disable is no-op as of Android 13 */ + tA.setActionSuccessful(PhoneStatusHelper.openBTSettings(mContext)) + tA.isEnabled = PhoneStatusHelper.isBluetoothEnabled(mContext) + } + } else { + tA.setActionSuccessful( + PhoneStatusHelper.setBluetoothEnabled( + mContext, + tA.isEnabled + ) + ) + } + sendMessage( + nodeID, + WearableHelper.ActionsPath, + JSONParser.serializer(tA, Action::class.java).stringToBytes() + ) } Actions.MOBILEDATA -> { tA = action as ToggleAction diff --git a/mobile/src/main/res/layout/fragment_permcheck.xml b/mobile/src/main/res/layout/fragment_permcheck.xml index 3457b0ce..38f9b45c 100644 --- a/mobile/src/main/res/layout/fragment_permcheck.xml +++ b/mobile/src/main/res/layout/fragment_permcheck.xml @@ -44,7 +44,8 @@ android:minHeight="64dp" android:padding="16dp" android:background="?selectableItemBackground" - android:visibility="gone"> + android:visibility="gone" + tools:visibility="visible"> + android:textAppearance="?android:textAppearanceSmall" + tools:text="@string/permission_notifications_disabled" /> @@ -98,7 +100,8 @@ android:layout_below="@+id/torch_pref_title" android:layout_alignStart="@+id/torch_pref_title" android:maxLines="4" - android:textAppearance="?android:textAppearanceSmall" /> + android:textAppearance="?android:textAppearanceSmall" + tools:text="@string/permission_camera_disabled" /> @@ -130,7 +133,8 @@ android:layout_below="@+id/deviceadmin_title" android:layout_alignStart="@+id/deviceadmin_title" android:maxLines="4" - android:textAppearance="?android:textAppearanceSmall" /> + android:textAppearance="?android:textAppearanceSmall" + tools:text="@string/permission_admin_enabled" /> @@ -162,22 +166,21 @@ android:layout_below="@+id/dnd_title" android:layout_alignStart="@+id/dnd_title" android:maxLines="4" - android:textAppearance="?android:textAppearanceSmall" /> + android:textAppearance="?android:textAppearanceSmall" + tools:text="@string/permission_dnd_disabled" /> + android:background="?selectableItemBackground"> - - + android:textAppearance="?android:textAppearanceSmall" + tools:text="@string/prompt_notifservice_disabled" /> - + android:textAppearance="?android:textAppearanceSmall" + tools:text="@string/permission_systemsettings_disabled" /> - + + android:background="?selectableItemBackground" + android:visibility="gone" + tools:visibility="visible"> + android:textAppearance="?android:textAppearanceSmall" + tools:text="@string/permission_bt_disabled" /> @@ -300,12 +294,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="4" - android:textAppearance="?android:textAppearanceSmall" /> + android:textAppearance="?android:textAppearanceSmall" + tools:text="@string/preference_summary_wearsettings_notinstalled" /> + android:textAppearance="?android:textAppearanceSmall" + tools:text="@string/permission_callmanager_disabled" /> - + android:background="?selectableItemBackground" + android:visibility="gone" + tools:visibility="visible"> + tools:text="@string/permission_pairdevice_disabled" /> - + + + + + + + + + + + + + WiFi, Bluetooth, Location and Mobile Data + \ No newline at end of file diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index e859858f..73d642d9 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -3,16 +3,17 @@ Camera permission granted. Camera permission disabled. Please click to enable controlling flashlight from WearOS device - Allows you to turn of your screen and lock your device from your WearOS device - Device admin enabled + Allows you to turn off your screen and lock your device from your WearOS device + Device admin enabled.\nNote: To uninstall app please use \"Deactivate and uninstall\" option Not enabled as device admin. Please click to enable locking screen from your WearOS device + Please note that if you enable device admin access, you will need to deactivate the access prior to uninstalling. You can use the \"Deactivate and uninstall\" option in the app to do so. Do not Disturb access enabled Do not Disturb access disabled. Please click to enable changing Do not Disturb setting from your WearOS device Pair with WearOS device App is paired with WearOS device - WearOS device not paired with app. Please click to pair device. This is needed to start the media player, launch apps and/or manage calls + WearOS device not paired with app. Please click to pair device. This is needed to manage calls Please enable Bluetooth discovery on your WearOS device Please ensure Bluetooth is enabled on your WearOS device No devices found @@ -59,4 +60,8 @@ Notification permission enabled Notification permission disabled + Bluetooth + Bluetooth permission enabled + Bluetooth permission disabled + diff --git a/settings.gradle b/settings.gradle index 5704944e..ba200ee7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ include ':mobile', ':wear', ':shared_resources', ':unofficialtileapi' include ':wearsettings' +include ':hidden-api' diff --git a/shared_resources/build.gradle b/shared_resources/build.gradle index 7e97220d..fa7a1d78 100644 --- a/shared_resources/build.gradle +++ b/shared_resources/build.gradle @@ -65,7 +65,7 @@ dependencies { implementation "androidx.appcompat:appcompat:$appcompat_version" - implementation 'com.google.android.gms:play-services-wearable:18.0.0' + implementation 'com.google.android.gms:play-services-wearable:18.1.0' implementation platform("com.google.firebase:firebase-bom:$firebase_version") implementation 'com.google.firebase:firebase-analytics' diff --git a/shared_resources/consumer-rules.pro b/shared_resources/consumer-rules.pro index 9c57cd6c..533182e6 100644 --- a/shared_resources/consumer-rules.pro +++ b/shared_resources/consumer-rules.pro @@ -25,6 +25,7 @@ -keep class * extends com.thewizrd.shared_resources.actions.Action { *; } -keep public enum com.thewizrd.shared_resources.actions.* { ; } -keep public enum com.thewizrd.shared_resources.helpers.* { ; } +-keep class androidx.core.util.Pair { *; } ##---------------Begin: proguard configuration for Gson ---------- # Gson uses generic type information stored in a class file when working with fields. Proguard diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/MediaHelper.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/MediaHelper.kt index f5e23ff6..1705321c 100644 --- a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/MediaHelper.kt +++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/MediaHelper.kt @@ -1,5 +1,7 @@ package com.thewizrd.shared_resources.helpers +import android.content.Intent + object MediaHelper { const val MusicPlayersPath = "/music-players" const val PlayCommandPath = "/music/play" @@ -60,4 +62,19 @@ object MediaHelper { const val KEY_SUPPORTEDPLAYERS = "key_supported_players" const val KEY_VOLUME = "key_volume" + + // For Activity Launcher + const val URI_PARAM_MEDIAPLAYER = "media_player" + + fun createRemoteActivityIntent(packageName: String, activityName: String): Intent { + return Intent(Intent.ACTION_VIEW) + .addCategory(Intent.CATEGORY_DEFAULT) + .addCategory(Intent.CATEGORY_BROWSABLE) + .setData( + WearableHelper.getLaunchActivityUri(packageName, activityName) + .buildUpon() + .appendQueryParameter(URI_PARAM_MEDIAPLAYER, "true") + .build() + ) + } } \ No newline at end of file diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearableHelper.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearableHelper.kt index 97168394..bfccf218 100644 --- a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearableHelper.kt +++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearableHelper.kt @@ -1,9 +1,10 @@ package com.thewizrd.shared_resources.helpers +import android.content.ComponentName import android.content.Intent import android.net.Uri +import android.os.ParcelUuid import android.util.Log -import androidx.core.net.toUri import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.wearable.Node @@ -50,6 +51,12 @@ object WearableHelper { const val KEY_PKGNAME = "key_package_name" const val KEY_ACTIVITYNAME = "key_activity_name" + // For Activity Launcher + private const val SCHEME_APP = "simplewear" + private const val PATH_REMOTE_LAUNCH = "launch-activity" + const val URI_PARAM_PKGNAME = "package" + const val URI_PARAM_ACTIVITYNAME = "activity" + fun isGooglePlayServicesInstalled(): Boolean { val queryResult = GoogleApiAvailability.getInstance() .isGooglePlayServicesAvailable(SimpleLibrary.instance.app.appContext) @@ -83,11 +90,44 @@ object WearableHelper { .build() } - fun getRemoteIntentForPackage(packageName: String): Intent { - return Intent(Intent.ACTION_VIEW).apply { - data = "android-app://${packageName}".toUri() - addCategory(Intent.CATEGORY_BROWSABLE) - } + fun getLaunchActivityUri(packageName: String, activityName: String): Uri { + return Uri.Builder() + .scheme(SCHEME_APP) + .authority(PATH_REMOTE_LAUNCH) + .appendQueryParameter(URI_PARAM_PKGNAME, packageName) + .appendQueryParameter(URI_PARAM_ACTIVITYNAME, activityName) + .build() + } + + fun createRemoteActivityIntent(packageName: String, activityName: String): Intent { + return Intent(Intent.ACTION_VIEW) + .addCategory(Intent.CATEGORY_DEFAULT) + .addCategory(Intent.CATEGORY_BROWSABLE) + .setData(getLaunchActivityUri(packageName, activityName)) + } + + fun isRemoteLaunchUri(uri: Uri): Boolean { + return uri.scheme == SCHEME_APP && uri.host == PATH_REMOTE_LAUNCH && + !uri.getQueryParameter(URI_PARAM_PKGNAME).isNullOrEmpty() && + !uri.getQueryParameter(URI_PARAM_ACTIVITYNAME).isNullOrEmpty() + } + + fun Uri.toLaunchIntent(): Intent { + return Intent(Intent.ACTION_MAIN) + .apply { + if (getQueryParameter(MediaHelper.URI_PARAM_MEDIAPLAYER) == "true") { + addCategory(Intent.CATEGORY_APP_MUSIC) + } else { + addCategory(Intent.CATEGORY_LAUNCHER) + } + } + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .setComponent( + ComponentName( + this.getQueryParameter(URI_PARAM_PKGNAME)!!, + this.getQueryParameter(URI_PARAM_ACTIVITYNAME)!! + ) + ) } /* @@ -106,4 +146,7 @@ object WearableHelper { } return bestNode } + + fun getBLEServiceUUID(): ParcelUuid = + ParcelUuid.fromString("0000DA28-0000-1000-8000-00805F9B34FB") } \ No newline at end of file diff --git a/wear/build.gradle b/wear/build.gradle index 3b3dcffa..5cacd7fe 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -12,8 +12,8 @@ android { minSdkVersion 25 targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 00, WearOS: 01) - versionCode 331913001 - versionName "1.13.0" + versionCode 331913121 + versionName "1.13.1" vectorDrawables.useSupportLibrary = true } @@ -84,7 +84,7 @@ dependencies { implementation "com.google.android.material:material:$material_version" // WearOS - implementation 'com.google.android.gms:play-services-wearable:18.0.0' + implementation 'com.google.android.gms:play-services-wearable:18.1.0' compileOnly 'com.google.android.wearable:wearable:2.9.0' // Needed for Ambient Mode implementation 'androidx.wear:wear:1.3.0' diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index d60c33d0..20a091ba 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -3,7 +3,13 @@ xmlns:tools="http://schemas.android.com/tools"> - + + + @@ -12,6 +18,12 @@ + + diff --git a/wear/src/main/java/com/thewizrd/simplewear/AppLauncherActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/AppLauncherActivity.kt index 9dfe71f8..9fdcce48 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/AppLauncherActivity.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/AppLauncherActivity.kt @@ -9,10 +9,11 @@ import android.os.CountDownTimer import android.util.Log import android.view.View import androidx.core.content.ContextCompat -import androidx.core.util.Pair import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.wear.widget.WearableLinearLayoutManager +import androidx.wear.widget.drawer.WearableDrawerLayout +import androidx.wear.widget.drawer.WearableDrawerView import com.google.android.gms.wearable.ChannelClient import com.google.android.gms.wearable.DataClient.OnDataChangedListener import com.google.android.gms.wearable.DataEvent @@ -31,10 +32,8 @@ import com.thewizrd.shared_resources.helpers.WearConnectionStatus import com.thewizrd.shared_resources.helpers.WearableHelper import com.thewizrd.shared_resources.utils.ContextUtils.dpToPx import com.thewizrd.shared_resources.utils.ImageUtils.toBitmap -import com.thewizrd.shared_resources.utils.JSONParser import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.utils.bytesToString -import com.thewizrd.shared_resources.utils.stringToBytes import com.thewizrd.simplewear.adapters.AppsListAdapter import com.thewizrd.simplewear.adapters.ListHeaderAdapter import com.thewizrd.simplewear.adapters.SpacerAdapter @@ -132,6 +131,60 @@ class AppLauncherActivity : WearableListenerActivity(), OnDataChangedListener { } } + binding.drawerLayout.setDrawerStateCallback(object : + WearableDrawerLayout.DrawerStateCallback() { + override fun onDrawerOpened( + layout: WearableDrawerLayout, + drawerView: WearableDrawerView + ) { + super.onDrawerOpened(layout, drawerView) + drawerView.requestFocus() + } + + override fun onDrawerClosed( + layout: WearableDrawerLayout, + drawerView: WearableDrawerView + ) { + super.onDrawerClosed(layout, drawerView) + drawerView.clearFocus() + binding.appList.requestFocus() + } + + override fun onDrawerStateChanged(layout: WearableDrawerLayout, newState: Int) { + super.onDrawerStateChanged(layout, newState) + if (newState == WearableDrawerView.STATE_IDLE && binding.bottomActionDrawer.isPeeking) { + binding.bottomActionDrawer.clearFocus() + binding.appList.requestFocus() + } + } + }) + + binding.bottomActionDrawer.visibility = View.VISIBLE + binding.bottomActionDrawer.isPeekOnScrollDownEnabled = true + binding.bottomActionDrawer.setIsAutoPeekEnabled(true) + binding.bottomActionDrawer.setIsLocked(false) + + findViewById(R.id.icons_pref).also { iconsPref -> + iconsPref.setOnClickListener { + iconsPref.toggle() + Settings.setLoadAppIcons(iconsPref.isChecked) + lifecycleScope.launch(Dispatchers.IO) { + val dataRequest = PutDataMapRequest.create(WearableHelper.AppsIconSettingsPath) + dataRequest.dataMap.putBoolean(WearableHelper.KEY_ICON, iconsPref.isChecked) + dataRequest.setUrgent() + runCatching { + Wearable + .getDataClient(this@AppLauncherActivity) + .putDataItem(dataRequest.asPutDataRequest()) + .await() + }.onFailure { + Logger.writeLine(Log.ERROR, it) + } + } + } + iconsPref.isChecked = Settings.isLoadAppIcons() + } + binding.appList.setHasFixedSize(true) //binding.appList.isEdgeItemsCenteringEnabled = true binding.appList.addItemDecoration( @@ -147,16 +200,15 @@ class AppLauncherActivity : WearableListenerActivity(), OnDataChangedListener { mAdapter.setOnClickListener(object : ListAdapterOnClickInterface { override fun onClick(view: View, item: AppItemViewModel) { lifecycleScope.launch { - if (connect()) { - val nodeID = mPhoneNodeWithApp!!.id - sendMessage( - nodeID, WearableHelper.LaunchAppPath, - JSONParser.serializer( - Pair.create(item.packageName, item.activityName), - Pair::class.java - ).stringToBytes() + val success = runCatching { + val intent = WearableHelper.createRemoteActivityIntent( + item.packageName!!, + item.activityName!! ) - } + startRemoteActivity(intent) + }.getOrDefault(false) + + showConfirmationOverlay(success) } } }) @@ -173,27 +225,6 @@ class AppLauncherActivity : WearableListenerActivity(), OnDataChangedListener { } } - findViewById(R.id.icons_pref).also { iconsPref -> - iconsPref.setOnClickListener { - iconsPref.toggle() - Settings.setLoadAppIcons(iconsPref.isChecked) - lifecycleScope.launch(Dispatchers.IO) { - val dataRequest = PutDataMapRequest.create(WearableHelper.AppsIconSettingsPath) - dataRequest.dataMap.putBoolean(WearableHelper.KEY_ICON, iconsPref.isChecked) - dataRequest.setUrgent() - runCatching { - Wearable - .getDataClient(this@AppLauncherActivity) - .putDataItem(dataRequest.asPutDataRequest()) - .await() - }.onFailure { - Logger.writeLine(Log.ERROR, it) - } - } - } - iconsPref.isChecked = Settings.isLoadAppIcons() - } - intentFilter = IntentFilter() intentFilter.addAction(ACTION_UPDATECONNECTIONSTATUS) @@ -361,7 +392,7 @@ class AppLauncherActivity : WearableListenerActivity(), OnDataChangedListener { binding.noappsView.visibility = if (viewModels.isNotEmpty()) View.GONE else View.VISIBLE binding.appList.visibility = if (viewModels.isNotEmpty()) View.VISIBLE else View.GONE lifecycleScope.launch { - if (binding.appList.visibility == View.VISIBLE && !binding.appList.hasFocus()) { + if (!binding.bottomActionDrawer.isOpened && binding.appList.visibility == View.VISIBLE && !binding.appList.hasFocus()) { binding.appList.requestFocus() } } @@ -381,7 +412,11 @@ class AppLauncherActivity : WearableListenerActivity(), OnDataChangedListener { Wearable.getDataClient(this).addListener(this) Wearable.getChannelClient(this).registerChannelCallback(mChannelCallback) - binding.appList.requestFocus() + if (binding.bottomActionDrawer.isOpened) { + binding.bottomActionDrawer.requestFocus() + } else { + binding.appList.requestFocus() + } // Update statuses lifecycleScope.launch { diff --git a/wear/src/main/java/com/thewizrd/simplewear/DashboardActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/DashboardActivity.kt index 29c84fde..2710b830 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/DashboardActivity.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/DashboardActivity.kt @@ -352,14 +352,14 @@ class DashboardActivity : WearableListenerActivity(), OnSharedPreferenceChangeLi override fun onDrawerClosed(layout: WearableDrawerLayout, drawerView: WearableDrawerView) { super.onDrawerClosed(layout, drawerView) drawerView.clearFocus() + binding.scrollView.requestFocus() } override fun onDrawerStateChanged(layout: WearableDrawerLayout, newState: Int) { super.onDrawerStateChanged(layout, newState) - if (newState == WearableDrawerView.STATE_IDLE && - binding.bottomActionDrawer.isPeeking && binding.bottomActionDrawer.hasFocus() - ) { + if (newState == WearableDrawerView.STATE_IDLE && binding.bottomActionDrawer.isPeeking) { binding.bottomActionDrawer.clearFocus() + binding.scrollView.requestFocus() } } }) @@ -567,12 +567,26 @@ class DashboardActivity : WearableListenerActivity(), OnSharedPreferenceChangeLi requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0) } } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (PermissionChecker.checkSelfPermission( + this, + Manifest.permission.BLUETOOTH_ADVERTISE + ) != PermissionChecker.PERMISSION_GRANTED + ) { + requestPermissions(arrayOf(Manifest.permission.BLUETOOTH_ADVERTISE), 0) + } + } } override fun onResume() { super.onResume() - binding.scrollView.requestFocus() + if (binding.bottomActionDrawer.isOpened) { + binding.bottomActionDrawer.requestFocus() + } else { + binding.scrollView.requestFocus() + } // Update statuses binding.battStat.setText(R.string.state_syncing) diff --git a/wear/src/main/java/com/thewizrd/simplewear/MediaPlayerListActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/MediaPlayerListActivity.kt index b6ef0f8d..99358563 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/MediaPlayerListActivity.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/MediaPlayerListActivity.kt @@ -8,7 +8,6 @@ import android.util.Log import android.view.View import androidx.activity.viewModels import androidx.core.content.ContextCompat -import androidx.core.util.Pair import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.wear.widget.WearableLinearLayoutManager @@ -144,10 +143,9 @@ class MediaPlayerListActivity : WearableListenerActivity(), MessageClient.OnMess override fun onDrawerStateChanged(layout: WearableDrawerLayout, newState: Int) { super.onDrawerStateChanged(layout, newState) - if (newState == WearableDrawerView.STATE_IDLE && - binding.bottomActionDrawer.isPeeking && binding.bottomActionDrawer.hasFocus() - ) { + if (newState == WearableDrawerView.STATE_IDLE && binding.bottomActionDrawer.isPeeking) { binding.bottomActionDrawer.clearFocus() + binding.playerList.requestFocus() } } }) @@ -168,8 +166,19 @@ class MediaPlayerListActivity : WearableListenerActivity(), MessageClient.OnMess findViewById(R.id.filter_apps_btn).setOnClickListener { v -> val fragment = MediaPlayerFilterFragment() + supportFragmentManager.setFragmentResultListener( + fragment.javaClass.simpleName, + this + ) { _, _ -> + if (binding.bottomActionDrawer.isOpened) { + binding.bottomActionDrawer.requestFocus() + } else { + binding.playerList.requestFocus() + } + } + supportFragmentManager.beginTransaction() - .replace(android.R.id.content, fragment) + .replace(android.R.id.content, fragment, fragment.javaClass.simpleName) .commit() } @@ -188,23 +197,24 @@ class MediaPlayerListActivity : WearableListenerActivity(), MessageClient.OnMess mAdapter.setOnClickListener(object : ListAdapterOnClickInterface { override fun onClick(view: View, item: AppItemViewModel) { lifecycleScope.launch { - if (connect()) { - val nodeID = mPhoneNodeWithApp!!.id - sendMessage( - nodeID, - MediaHelper.OpenMusicPlayerPath, - JSONParser.serializer( - Pair.create(item.packageName, item.activityName), - Pair::class.java - ).stringToBytes() + val success = runCatching { + val intent = MediaHelper.createRemoteActivityIntent( + item.packageName!!, + item.activityName!! ) - } - startActivity( - MediaPlayerActivity.buildIntent( - this@MediaPlayerListActivity, - item + startRemoteActivity(intent) + }.getOrDefault(false) + + if (success) { + startActivity( + MediaPlayerActivity.buildIntent( + this@MediaPlayerListActivity, + item + ) ) - ) + } else { + showConfirmationOverlay(false) + } } } }) @@ -403,7 +413,7 @@ class MediaPlayerListActivity : WearableListenerActivity(), MessageClient.OnMess if (mMediaAppsList.size > 0) View.GONE else View.VISIBLE binding.playerList.visibility = if (mMediaAppsList.size > 0) View.VISIBLE else View.GONE lifecycleScope.launch { - if (binding.playerList.visibility == View.VISIBLE && !binding.playerList.hasFocus()) { + if (!binding.bottomActionDrawer.isOpened && binding.playerList.visibility == View.VISIBLE && !binding.playerList.hasFocus()) { binding.playerList.requestFocus() } } @@ -430,7 +440,11 @@ class MediaPlayerListActivity : WearableListenerActivity(), MessageClient.OnMess super.onResume() Wearable.getDataClient(this).addListener(this) - binding.playerList.requestFocus() + if (binding.bottomActionDrawer.isOpened) { + binding.bottomActionDrawer.requestFocus() + } else { + binding.playerList.requestFocus() + } // Update statuses lifecycleScope.launch { diff --git a/wear/src/main/java/com/thewizrd/simplewear/PhoneSyncActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/PhoneSyncActivity.kt index dbfc7cf5..597eeae7 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/PhoneSyncActivity.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/PhoneSyncActivity.kt @@ -1,5 +1,6 @@ package com.thewizrd.simplewear +import android.Manifest import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager import android.content.BroadcastReceiver @@ -8,11 +9,15 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.net.wifi.WifiManager +import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.View import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat +import androidx.core.content.PermissionChecker import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.lifecycleScope import androidx.wear.widget.ConfirmationOverlay @@ -26,10 +31,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch class PhoneSyncActivity : WearableListenerActivity() { - companion object { - private const val ENABLE_BT_REQUEST_CODE = 0 - } - override lateinit var broadcastReceiver: BroadcastReceiver private set override lateinit var intentFilter: IntentFilter @@ -37,6 +38,9 @@ class PhoneSyncActivity : WearableListenerActivity() { private lateinit var binding: ActivitySetupSyncBinding + private lateinit var bluetoothRequestLauncher: ActivityResultLauncher + private lateinit var permissionRequestLauncher: ActivityResultLauncher> + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -54,10 +58,15 @@ class PhoneSyncActivity : WearableListenerActivity() { binding.bluetoothButton.setOnClickListener { runCatching { - startActivityForResult( - Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), - ENABLE_BT_REQUEST_CODE - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && PermissionChecker.checkSelfPermission( + this, + Manifest.permission.BLUETOOTH_CONNECT + ) != PermissionChecker.PERMISSION_GRANTED + ) { + permissionRequestLauncher.launch(arrayOf(Manifest.permission.BLUETOOTH_CONNECT)) + } else { + bluetoothRequestLauncher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } } } @@ -180,6 +189,33 @@ class PhoneSyncActivity : WearableListenerActivity() { binding.message.setText(R.string.message_gettingstatus) intentFilter = IntentFilter(ACTION_UPDATECONNECTIONSTATUS) + + bluetoothRequestLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + lifecycleScope.launch(Dispatchers.Main) { + delay(2000) + startProgressBar() + delay(10000) + if (isActive) { + stopProgressBar() + } + } + } + } + + permissionRequestLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + permissions.entries.forEach { (permission, granted) -> + when (permission) { + Manifest.permission.BLUETOOTH_CONNECT -> { + if (granted) { + bluetoothRequestLauncher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + } + } } private fun stopProgressBar() { @@ -199,25 +235,6 @@ class PhoneSyncActivity : WearableListenerActivity() { } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - when (requestCode) { - ENABLE_BT_REQUEST_CODE -> { - if (resultCode == RESULT_OK) { - lifecycleScope.launch(Dispatchers.Main) { - delay(2000) - startProgressBar() - delay(10000) - if (isActive) { - stopProgressBar() - } - } - } - } - } - } - private fun checkNetworkStatus() { val btAdapter = getSystemService(BluetoothManager::class.java)?.adapter if (btAdapter != null) { diff --git a/wear/src/main/java/com/thewizrd/simplewear/WearableListenerActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/WearableListenerActivity.kt index 7808c9d2..e1f957f5 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/WearableListenerActivity.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/WearableListenerActivity.kt @@ -182,13 +182,24 @@ abstract class WearableListenerActivity : AppCompatLiteActivity(), OnMessageRece ) LocalBroadcastManager.getInstance(this@WearableListenerActivity) - .sendBroadcast(Intent(ACTION_OPENONPHONE) - .putExtra(EXTRA_SUCCESS, result != -1) - .putExtra(EXTRA_SHOWANIMATION, showAnimation)) + .sendBroadcast( + Intent(ACTION_OPENONPHONE) + .putExtra(EXTRA_SUCCESS, result != -1) + .putExtra(EXTRA_SHOWANIMATION, showAnimation) + ) } } } + protected suspend fun startRemoteActivity(intent: Intent): Boolean { + return runCatching { + remoteActivityHelper.startRemoteActivity(intent).await() + true + }.onFailure { + Logger.writeLine(Log.ERROR, it, "Error starting remote activity") + }.getOrDefault(false) + } + override fun onMessageReceived(messageEvent: MessageEvent) { lifecycleScope.launch { when { diff --git a/wear/src/main/java/com/thewizrd/simplewear/controls/WearChipButton.kt b/wear/src/main/java/com/thewizrd/simplewear/controls/WearChipButton.kt index 3a7b1e11..771a4531 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/controls/WearChipButton.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/controls/WearChipButton.kt @@ -97,9 +97,25 @@ class WearChipButton @JvmOverloads constructor( if (a.hasValue(R.styleable.WearChipButton_primaryText)) { setPrimaryText(a.getString(R.styleable.WearChipButton_primaryText)) } + if (a.hasValue(R.styleable.WearChipButton_primaryTextMaxLines)) { + setPrimaryTextMaxLines( + a.getInt( + R.styleable.WearChipButton_primaryTextMaxLines, + mPrimaryTextView.maxLines + ) + ) + } if (a.hasValue(R.styleable.WearChipButton_secondaryText)) { setSecondaryText(a.getString(R.styleable.WearChipButton_secondaryText)) } + if (a.hasValue(R.styleable.WearChipButton_secondaryTextMaxLines)) { + setSecondaryTextMaxLines( + a.getInt( + R.styleable.WearChipButton_secondaryTextMaxLines, + mSecondaryTextView.maxLines + ) + ) + } if (a.hasValue(R.styleable.WearChipButton_backgroundTint)) { val colorResId = a.getResourceId(R.styleable.WearChipButton_backgroundTint, 0) if (colorResId != 0) { @@ -196,6 +212,10 @@ class WearChipButton @JvmOverloads constructor( mPrimaryTextView.visibility = if (text == null) View.GONE else View.VISIBLE } + fun setPrimaryTextMaxLines(maxLines: Int) { + mPrimaryTextView.maxLines = maxLines + } + fun setSecondaryText(@StringRes resId: Int) { if (resId == 0) { setSecondaryText(null) @@ -220,6 +240,10 @@ class WearChipButton @JvmOverloads constructor( } } + fun setSecondaryTextMaxLines(maxLines: Int) { + mSecondaryTextView.maxLines = maxLines + } + fun setText(@StringRes primaryResId: Int, @StringRes secondaryResId: Int = 0) { setPrimaryText(primaryResId) setSecondaryText(secondaryResId) diff --git a/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerFilterFragment.kt b/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerFilterFragment.kt index 0f38fd37..27c2aed7 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerFilterFragment.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerFilterFragment.kt @@ -1,12 +1,17 @@ package com.thewizrd.simplewear.media import android.os.Bundle -import android.view.* +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup import androidx.core.view.InputDeviceCompat import androidx.core.view.MotionEventCompat import androidx.core.view.ViewConfigurationCompat import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.setFragmentResult import androidx.wear.widget.SwipeDismissFrameLayout import com.thewizrd.simplewear.adapters.MediaPlayerListAdapter import com.thewizrd.simplewear.databinding.MediaplayerfilterListBinding @@ -82,14 +87,17 @@ class MediaPlayerFilterFragment : DialogFragment() { Settings.setMusicPlayersFilter(mAdapter.getSelectedItems()) viewModel.filteredAppsList.postValue(mAdapter.getSelectedItems()) dismiss() + tag?.let { + setFragmentResult(it, Bundle.EMPTY) + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.scrollView.requestFocus() - viewModel.mediaAppsList.observe(viewLifecycleOwner, { + viewModel.mediaAppsList.observe(viewLifecycleOwner) { mAdapter.submitList(it.toList()) - }) + } } } \ No newline at end of file diff --git a/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardConfigActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardConfigActivity.kt index f0865d98..1e32d3df 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardConfigActivity.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardConfigActivity.kt @@ -4,6 +4,10 @@ import android.content.DialogInterface import android.graphics.Rect import android.os.Bundle import android.view.MotionEvent +import android.view.ViewConfiguration +import androidx.core.view.InputDeviceCompat +import androidx.core.view.MotionEventCompat +import androidx.core.view.ViewConfigurationCompat import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper @@ -15,6 +19,7 @@ import com.thewizrd.simplewear.adapters.TileActionAdapter import com.thewizrd.simplewear.databinding.LayoutDashboardConfigBinding import com.thewizrd.simplewear.helpers.AcceptDenyDialog import com.thewizrd.simplewear.helpers.TileActionsItemTouchCallback +import kotlin.math.roundToInt class DashboardConfigActivity : AppCompatLiteActivity() { companion object { @@ -122,4 +127,20 @@ class DashboardConfigActivity : AppCompatLiteActivity() { } return super.dispatchTouchEvent(ev) } + + override fun onGenericMotionEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_SCROLL && event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) { + // Don't forget the negation here + val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) * + ViewConfigurationCompat.getScaledVerticalScrollFactor( + ViewConfiguration.get(this), this + ) + + // Swap these axes if you want to do horizontal scrolling instead + binding.root.scrollBy(0, delta.roundToInt()) + + return true + } + return super.onGenericMotionEvent(event) + } } \ No newline at end of file diff --git a/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardTileConfigActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardTileConfigActivity.kt index f0354c92..33c3cfe4 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardTileConfigActivity.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardTileConfigActivity.kt @@ -4,6 +4,10 @@ import android.content.DialogInterface import android.graphics.Rect import android.os.Bundle import android.view.MotionEvent +import android.view.ViewConfiguration +import androidx.core.view.InputDeviceCompat +import androidx.core.view.MotionEventCompat +import androidx.core.view.ViewConfigurationCompat import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper @@ -18,6 +22,7 @@ import com.thewizrd.simplewear.helpers.TileActionsItemTouchCallback import com.thewizrd.simplewear.preferences.DashboardTileUtils.DEFAULT_TILES import com.thewizrd.simplewear.preferences.DashboardTileUtils.MAX_BUTTONS import com.thewizrd.simplewear.preferences.DashboardTileUtils.isActionAllowed +import kotlin.math.roundToInt class DashboardTileConfigActivity : AppCompatLiteActivity() { private lateinit var binding: LayoutTileDashboardConfigBinding @@ -122,4 +127,20 @@ class DashboardTileConfigActivity : AppCompatLiteActivity() { } return super.dispatchTouchEvent(ev) } + + override fun onGenericMotionEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_SCROLL && event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) { + // Don't forget the negation here + val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) * + ViewConfigurationCompat.getScaledVerticalScrollFactor( + ViewConfiguration.get(this), this + ) + + // Swap these axes if you want to do horizontal scrolling instead + binding.root.scrollBy(0, delta.roundToInt()) + + return true + } + return super.onGenericMotionEvent(event) + } } \ No newline at end of file diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt index e0ccce20..d6ce1986 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt @@ -4,6 +4,10 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.bluetooth.le.AdvertiseData +import android.bluetooth.le.AdvertisingSetCallback +import android.bluetooth.le.AdvertisingSetParameters import android.content.Intent import android.os.Build import android.util.Log @@ -29,7 +33,9 @@ import com.thewizrd.simplewear.media.MediaPlayerActivity import com.thewizrd.simplewear.preferences.Settings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.tasks.await class WearableDataListenerService : WearableListenerService() { @@ -59,11 +65,7 @@ class WearableDataListenerService : WearableListenerService() { val startIntent = Intent(this, PhoneSyncActivity::class.java) this.startActivity(startIntent) } else if (messageEvent.path == WearableHelper.BtDiscoverPath) { - this.startActivity( - Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 30) - ) + startBTDiscovery() GlobalScope.launch(Dispatchers.Default) { sendMessage( @@ -75,6 +77,57 @@ class WearableDataListenerService : WearableListenerService() { } } + private fun startBTDiscovery() { + val btService = applicationContext.getSystemService(BluetoothManager::class.java) + val adapter = btService.adapter + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && adapter.isMultipleAdvertisementSupported) { + val advertiser = adapter.bluetoothLeAdvertiser + + GlobalScope.launch(Dispatchers.Default) { + val params = AdvertisingSetParameters.Builder() + .setLegacyMode(true) + .setConnectable(false) + .setInterval(AdvertisingSetParameters.INTERVAL_HIGH) + .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_ULTRA_LOW) + .setScannable(true) + .build() + + val data = AdvertiseData.Builder() + .setIncludeDeviceName(true) + .addServiceUuid(WearableHelper.getBLEServiceUUID()) + .build() + + val callback = object : AdvertisingSetCallback() {} + + supervisorScope { + runCatching { + Logger.writeLine(Log.DEBUG, "${TAG}: starting BLE advertising") + + val startedAdv = runCatching { + advertiser.startAdvertisingSet(params, data, null, null, null, callback) + true + }.getOrDefault(false) + + if (startedAdv) { + delay(10000) + Logger.writeLine(Log.DEBUG, "${TAG}: stopping BLE advertising") + advertiser.stopAdvertisingSet(callback) + } + }.onFailure { + Logger.writeLine(Log.ERROR, it, "Error with BT discovery") + } + } + } + } else { + this.startActivity( + Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 20) + ) + } + } + protected suspend fun sendMessage(nodeID: String, path: String, data: ByteArray?) { try { Wearable.getMessageClient(this@WearableDataListenerService) diff --git a/wear/src/main/res/layout/activity_applauncher.xml b/wear/src/main/res/layout/activity_applauncher.xml index 611cedde..69a7f306 100644 --- a/wear/src/main/res/layout/activity_applauncher.xml +++ b/wear/src/main/res/layout/activity_applauncher.xml @@ -12,6 +12,7 @@ android:id="@+id/app_list" android:layout_width="match_parent" android:layout_height="match_parent" + android:scrollbars="vertical" android:visibility="gone" tools:listitem="@layout/app_item" tools:visibility="gone" /> diff --git a/wear/src/main/res/layout/activity_musicplayerlist.xml b/wear/src/main/res/layout/activity_musicplayerlist.xml index dbff8bcc..419d2ace 100644 --- a/wear/src/main/res/layout/activity_musicplayerlist.xml +++ b/wear/src/main/res/layout/activity_musicplayerlist.xml @@ -12,6 +12,7 @@ android:id="@+id/player_list" android:layout_width="match_parent" android:layout_height="match_parent" + android:scrollbars="vertical" android:visibility="gone" tools:listitem="@layout/app_item" tools:visibility="gone" /> diff --git a/wear/src/main/res/layout/applauncher_drawer_layout.xml b/wear/src/main/res/layout/applauncher_drawer_layout.xml index 1c189130..f53898c8 100644 --- a/wear/src/main/res/layout/applauncher_drawer_layout.xml +++ b/wear/src/main/res/layout/applauncher_drawer_layout.xml @@ -6,6 +6,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@null" + android:focusable="true" + android:focusableInTouchMode="true" android:orientation="vertical" android:paddingHorizontal="14dp" android:paddingBottom="48dp" @@ -35,6 +37,7 @@ android:layout_height="wrap_content" style="@style/Widget.Wear.WearChipButton.Surface.Checkable" app:primaryText="@string/pref_loadicons_title" + app:primaryTextMaxLines="10" app:controlType="toggle" /> diff --git a/wear/src/main/res/layout/dashboard_drawer_layout.xml b/wear/src/main/res/layout/dashboard_drawer_layout.xml index bb2318b4..ef128b54 100644 --- a/wear/src/main/res/layout/dashboard_drawer_layout.xml +++ b/wear/src/main/res/layout/dashboard_drawer_layout.xml @@ -9,6 +9,7 @@ android:fillViewport="true" android:focusable="true" android:focusableInTouchMode="true" + android:scrollbars="vertical" tools:background="?colorSurface" tools:context=".DashboardActivity" tools:deviceIds="wear"> @@ -79,6 +80,7 @@ android:layout_height="wrap_content" style="@style/Widget.Wear.WearChipButton.Surface.Checkable" app:primaryText="@string/pref_title_mediacontroller_launcher" + app:primaryTextMaxLines="10" app:controlType="toggle" /> diff --git a/wear/src/main/res/layout/fragment_browser_list.xml b/wear/src/main/res/layout/fragment_browser_list.xml index d595ba8d..8a7b31a2 100644 --- a/wear/src/main/res/layout/fragment_browser_list.xml +++ b/wear/src/main/res/layout/fragment_browser_list.xml @@ -21,6 +21,7 @@ android:id="@+id/list_view" android:layout_width="match_parent" android:layout_height="match_parent" + android:scrollbars="vertical" tools:listitem="@layout/app_item" tools:visibility="gone" /> diff --git a/wear/src/main/res/layout/layout_dashboard_config.xml b/wear/src/main/res/layout/layout_dashboard_config.xml index 6153657d..84e1578d 100644 --- a/wear/src/main/res/layout/layout_dashboard_config.xml +++ b/wear/src/main/res/layout/layout_dashboard_config.xml @@ -5,6 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" + android:scrollbars="vertical" tools:deviceIds="wear"> diff --git a/wear/src/main/res/values/attrs.xml b/wear/src/main/res/values/attrs.xml index 270e11f8..583cdd48 100644 --- a/wear/src/main/res/values/attrs.xml +++ b/wear/src/main/res/values/attrs.xml @@ -19,6 +19,8 @@ + + diff --git a/wearsettings/build.gradle b/wearsettings/build.gradle index 1238b574..81421384 100644 --- a/wearsettings/build.gradle +++ b/wearsettings/build.gradle @@ -1,5 +1,8 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' +plugins { + id('com.android.application') + id('kotlin-android') + id('dev.rikka.tools.refine') version "$refine_version" +} android { compileSdk rootProject.compileSdkVersion @@ -9,8 +12,8 @@ android { minSdk rootProject.minSdkVersion //noinspection ExpiredTargetSdkVersion targetSdk 28 - versionCode 1000100 - versionName "1.0.1" + versionCode 1010000 + versionName "1.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -48,6 +51,7 @@ android { dependencies { implementation project(":shared_resources") + compileOnly project(':hidden-api') // Unit Testing androidTestImplementation "androidx.test:core:$test_core_version" @@ -71,4 +75,10 @@ dependencies { // Root implementation 'com.github.topjohnwu.libsu:core:3.1.2' + + // Shizuku + implementation "dev.rikka.shizuku:api:$shizuku_version" + implementation "dev.rikka.shizuku:provider:$shizuku_version" + implementation "dev.rikka.tools.refine:runtime:$refine_version" + implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' } \ No newline at end of file diff --git a/wearsettings/src/main/AndroidManifest.xml b/wearsettings/src/main/AndroidManifest.xml index 63966163..c1db0ab9 100644 --- a/wearsettings/src/main/AndroidManifest.xml +++ b/wearsettings/src/main/AndroidManifest.xml @@ -6,10 +6,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/App.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/App.kt index 16a7353d..16a1cfff 100644 --- a/wearsettings/src/main/java/com/thewizrd/wearsettings/App.kt +++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/App.kt @@ -3,6 +3,7 @@ package com.thewizrd.wearsettings import android.app.Activity import android.app.Application import android.content.Context +import android.os.Build import android.os.Bundle import com.google.android.material.color.DynamicColors import com.thewizrd.shared_resources.ApplicationLib @@ -10,6 +11,7 @@ import com.thewizrd.shared_resources.SimpleLibrary import com.thewizrd.shared_resources.helpers.AppState import com.thewizrd.shared_resources.utils.FileLoggingTree import com.thewizrd.shared_resources.utils.Logger +import org.lsposed.hiddenapibypass.HiddenApiBypass class App : Application(), ApplicationLib, Application.ActivityLifecycleCallbacks { companion object { @@ -46,6 +48,13 @@ class App : Application(), ApplicationLib, Application.ActivityLifecycleCallback DynamicColors.applyToActivitiesIfAvailable(this) } + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + HiddenApiBypass.addHiddenApiExemptions("L"); + } + } + override fun onTerminate() { // Shutdown logger Logger.shutdown() diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/MainActivity.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/MainActivity.kt index 86add3e2..19fd9fdf 100644 --- a/wearsettings/src/main/java/com/thewizrd/wearsettings/MainActivity.kt +++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/MainActivity.kt @@ -1,5 +1,7 @@ package com.thewizrd.wearsettings +import android.Manifest +import android.annotation.SuppressLint import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager @@ -9,20 +11,34 @@ import android.os.Build import android.os.Bundle import android.os.PowerManager import android.provider.Settings +import android.util.Log import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.PermissionChecker import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.wearsettings.actions.checkSecureSettingsPermission import com.thewizrd.wearsettings.databinding.ActivityMainBinding import com.thewizrd.wearsettings.root.RootHelper +import com.thewizrd.wearsettings.shizuku.ShizukuState +import com.thewizrd.wearsettings.shizuku.ShizukuUtils import kotlinx.coroutines.launch +import rikka.shizuku.Shizuku import com.thewizrd.wearsettings.Settings as SettingsHelper -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListener { + companion object { + private const val BTCONNECT_REQCODE = 0 + private const val SHIZUKU_REQCODE = 1 + } + private lateinit var binding: ActivityMainBinding private lateinit var mPowerMgr: PowerManager + @SuppressLint("BatteryLife") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -86,6 +102,72 @@ class MainActivity : AppCompatActivity() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { binding.hidelauncherPref.isVisible = false } + + binding.btPref.setOnClickListener { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isBluetoothConnectPermGranted()) { + requestPermissions(arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 0) + } + } + + binding.btPref.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + + binding.shizukuPref.setOnClickListener { + runCatching { + val shizukuState = ShizukuUtils.getShizukuState(this) + + when (shizukuState) { + ShizukuState.RUNNING -> { /* no-op */ + } + + ShizukuState.NOT_INSTALLED -> { + showShizukuInstallDialog() + } + + ShizukuState.NOT_RUNNING -> { + ShizukuUtils.startShizukuActivity(this) + } + + ShizukuState.PERMISSION_DENIED -> { + if (Shizuku.shouldShowRequestPermissionRationale()) { + Snackbar.make( + binding.root, + R.string.message_shizuku_disabled, + Snackbar.LENGTH_LONG + ).apply { + setAction(R.string.title_settings) { + runCatching { + startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.parse("package:${it.context.packageName}") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }) + } + } + } + } else { + ShizukuUtils.requestPermission(this, SHIZUKU_REQCODE) + } + } + } + }.onFailure { + Logger.writeLine(Log.ERROR, it) + } + } + + Shizuku.addRequestPermissionResultListener(this) + } + + private fun showShizukuInstallDialog() { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.permission_title_shizuku) + .setMessage(R.string.message_shizuku_alert) + .setCancelable(true) + .setNegativeButton(android.R.string.cancel) { d, _ -> + d.dismiss() + } + .setPositiveButton(android.R.string.ok) { d, which -> + ShizukuUtils.openPlayStoreListing(this) + } + .show() } override fun onResume() { @@ -96,8 +178,22 @@ class MainActivity : AppCompatActivity() { updateHideLauncherPref(isLauncherIconEnabled()) val rootEnabled = SettingsHelper.isRootAccessEnabled() && RootHelper.isRootEnabled() - updateSecureSettingsPref(checkSecureSettingsPermission(this@MainActivity) || rootEnabled) + val shizukuState = ShizukuUtils.getShizukuState(this@MainActivity) + updateBTPref(isBluetoothConnectPermGranted() || rootEnabled || shizukuState == ShizukuState.RUNNING) + updateSecureSettingsPref(checkSecureSettingsPermission(this@MainActivity) || rootEnabled || shizukuState == ShizukuState.RUNNING) updateRootAccessPref(rootEnabled) + updateShizukuPref(shizukuState) + } + } + + private fun isBluetoothConnectPermGranted(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PermissionChecker.checkSelfPermission( + this, + Manifest.permission.BLUETOOTH_CONNECT + ) == PermissionChecker.PERMISSION_GRANTED + } else { + true } } @@ -120,6 +216,23 @@ class MainActivity : AppCompatActivity() { binding.hidelauncherPrefToggle.isChecked = enabled } + private fun updateBTPref(enabled: Boolean) { + binding.btPrefSummary.setText(if (enabled) R.string.permission_bt_enabled else R.string.permission_bt_disabled) + binding.btPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED) + } + + private fun updateShizukuPref(state: ShizukuState) { + binding.shizukuPrefSummary.setText( + when (state) { + ShizukuState.NOT_INSTALLED -> R.string.message_shizuku_not_installed + ShizukuState.NOT_RUNNING -> R.string.message_shizuku_not_running + ShizukuState.PERMISSION_DENIED -> R.string.message_shizuku_disabled + ShizukuState.RUNNING -> R.string.message_shizuku_running + } + ) + binding.shizukuPrefSummary.setTextColor(if (state == ShizukuState.RUNNING) Color.GREEN else Color.RED) + } + private fun isLauncherIconEnabled(): Boolean { val componentState = packageManager.getComponentEnabledSetting( ComponentName(this, LauncherActivity::class.java), @@ -138,4 +251,36 @@ class MainActivity : AppCompatActivity() { PackageManager.DONT_KILL_APP ) } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + val permGranted = + grantResults.isNotEmpty() && !grantResults.contains(PackageManager.PERMISSION_DENIED) + + when (requestCode) { + BTCONNECT_REQCODE -> { + updateBTPref(permGranted) + } + } + } + + // Shizuku + override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { + if (grantResult == PackageManager.PERMISSION_GRANTED) { + // granted!! + lifecycleScope.launch { + updateShizukuPref(ShizukuUtils.getShizukuState(this@MainActivity)) + } + } + } + + override fun onDestroy() { + Shizuku.removeRequestPermissionResultListener(this) + super.onDestroy() + } } \ No newline at end of file diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/ActionHelper.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/ActionHelper.kt index a2acfb76..96c315f3 100644 --- a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/ActionHelper.kt +++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/ActionHelper.kt @@ -21,12 +21,19 @@ object ActionHelper { Actions.WIFI -> { WifiAction.executeAction(context, action) } + Actions.LOCATION -> { LocationAction.executeAction(context, action) } + Actions.MOBILEDATA -> { MobileDataAction.executeAction(context, action) } + + Actions.BLUETOOTH -> { + BluetoothAction.executeAction(context, action) + } + else -> ActionStatus.FAILURE } } diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/BluetoothAction.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/BluetoothAction.kt new file mode 100644 index 00000000..da4bc25c --- /dev/null +++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/BluetoothAction.kt @@ -0,0 +1,71 @@ +package com.thewizrd.wearsettings.actions + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothManager +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.core.content.PermissionChecker +import com.thewizrd.shared_resources.actions.Action +import com.thewizrd.shared_resources.actions.ActionStatus +import com.thewizrd.shared_resources.actions.ToggleAction +import com.thewizrd.shared_resources.utils.Logger +import com.thewizrd.wearsettings.Settings +import com.thewizrd.wearsettings.root.RootHelper +import com.topjohnwu.superuser.Shell + +object BluetoothAction { + fun executeAction(context: Context, action: Action): ActionStatus { + if (action is ToggleAction) { + val status = setBTEnabled(context, action.isEnabled) + return if (status != ActionStatus.SUCCESS && Settings.isRootAccessEnabled() && RootHelper.isRootEnabled()) { + setBTEnabledRoot(action.isEnabled) + } else { + status + } + } + + return ActionStatus.UNKNOWN + } + + private fun isBluetoothPermissionGranted(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT + ) == PermissionChecker.PERMISSION_GRANTED + } else { + true + } + } + + @SuppressLint("MissingPermission") + private fun setBTEnabled(context: Context, enable: Boolean): ActionStatus { + if (isBluetoothPermissionGranted(context)) { + return try { + val btMan = + context.applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + val adapter = btMan.adapter + val success = if (enable) adapter.enable() else adapter.disable() + if (success) ActionStatus.SUCCESS else ActionStatus.REMOTE_FAILURE + } catch (e: Exception) { + Logger.writeLine(Log.ERROR, e) + ActionStatus.REMOTE_FAILURE + } + } + return ActionStatus.REMOTE_PERMISSION_DENIED + } + + private fun setBTEnabledRoot(enable: Boolean): ActionStatus { + val arg = if (enable) "enable" else "disable" + + val result = Shell.su("svc bluetooth $arg").exec() + + return if (result.isSuccess) { + ActionStatus.SUCCESS + } else { + ActionStatus.REMOTE_FAILURE + } + } +} \ No newline at end of file diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/MobileDataAction.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/MobileDataAction.kt index 6637efa4..52ae7c4a 100644 --- a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/MobileDataAction.kt +++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/MobileDataAction.kt @@ -1,30 +1,29 @@ package com.thewizrd.wearsettings.actions -import android.Manifest import android.content.Context -import android.content.pm.PackageManager -import android.provider.Settings -import androidx.core.content.ContextCompat +import android.os.Build +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.util.Log +import com.android.internal.telephony.ITelephony import com.thewizrd.shared_resources.actions.Action import com.thewizrd.shared_resources.actions.ActionStatus import com.thewizrd.shared_resources.actions.ToggleAction +import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.wearsettings.root.RootHelper +import com.topjohnwu.superuser.Shell +import rikka.shizuku.Shizuku +import rikka.shizuku.ShizukuBinderWrapper +import rikka.shizuku.SystemServiceHelper import com.thewizrd.wearsettings.Settings as SettingsHelper object MobileDataAction { fun executeAction(context: Context, action: Action): ActionStatus { if (action is ToggleAction) { - return if (checkSecureSettingsPermission(context)) { - setMobileDataEnabled(context, action.isEnabled) + return if (Shizuku.pingBinder()) { + setMobileDataEnabledShizuku(context, action.isEnabled) } else if (SettingsHelper.isRootAccessEnabled() && RootHelper.isRootEnabled()) { - GlobalSettingsAction.putSetting( - "mobile_data", - if (action.isEnabled) { - 1 - } else { - 0 - }.toString() - ) + setMobileDataEnabledRoot(action.isEnabled) } else { ActionStatus.REMOTE_PERMISSION_DENIED } @@ -33,23 +32,71 @@ object MobileDataAction { return ActionStatus.UNKNOWN } - private fun setMobileDataEnabled(context: Context, enabled: Boolean): ActionStatus { - return if (ContextCompat.checkSelfPermission( - context, - Manifest.permission.WRITE_SECURE_SETTINGS - ) == PackageManager.PERMISSION_GRANTED - ) { - val success = Settings.Global.putInt( - context.contentResolver, "mobile_data", - if (enabled) 1 else 0 - ) - if (success) { - ActionStatus.SUCCESS + private fun setMobileDataEnabledRoot(enable: Boolean): ActionStatus { + val arg = if (enable) "enable" else "disable" + + val result = Shell.su("svc data $arg").exec() + + return if (result.isSuccess) { + ActionStatus.SUCCESS + } else { + ActionStatus.REMOTE_FAILURE + } + } + + private fun setMobileDataEnabledShizuku(context: Context, enable: Boolean): ActionStatus { + return runCatching { + val telephony = SystemServiceHelper.getSystemService(Context.TELEPHONY_SERVICE) + .let(::ShizukuBinderWrapper) + .let(ITelephony.Stub::asInterface) + + val activeSubId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + SubscriptionManager.getActiveDataSubscriptionId().takeUnless { + it == SubscriptionManager.INVALID_SUBSCRIPTION_ID + } ?: SubscriptionManager.getDefaultSubscriptionId() } else { - ActionStatus.REMOTE_FAILURE + SubscriptionManager.getDefaultSubscriptionId() } - } else { - ActionStatus.REMOTE_PERMISSION_DENIED + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + telephony.setDataEnabledForReason( + activeSubId, + TelephonyManager.DATA_ENABLED_REASON_USER, + enable, + "" + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + telephony.setDataEnabledForReason( + activeSubId, + TelephonyManager.DATA_ENABLED_REASON_USER, + enable + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + telephony.setUserDataEnabled(activeSubId, enable) + } else { + telephony.setDataEnabled(activeSubId, enable) + } + + /* + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (enable) { + telephony.enableDataConnectivity("") + } else { + telephony.disableDataConnectivity("") + } + } else { + if (enable) { + telephony.enableDataConnectivity() + } else { + telephony.disableDataConnectivity() + } + } + */ + + ActionStatus.SUCCESS + }.getOrElse { + Logger.writeLine(Log.ERROR, it) + ActionStatus.REMOTE_FAILURE } } } \ No newline at end of file diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/WifiAction.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/WifiAction.kt index a3bf4187..e77984cc 100644 --- a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/WifiAction.kt +++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/WifiAction.kt @@ -3,7 +3,9 @@ package com.thewizrd.wearsettings.actions import android.Manifest import android.content.Context import android.content.pm.PackageManager +import android.net.wifi.IWifiManager import android.net.wifi.WifiManager +import android.os.Build import android.util.Log import androidx.core.content.ContextCompat import com.thewizrd.shared_resources.actions.Action @@ -13,13 +15,24 @@ import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.wearsettings.Settings import com.thewizrd.wearsettings.root.RootHelper import com.topjohnwu.superuser.Shell +import rikka.shizuku.Shizuku +import rikka.shizuku.ShizukuBinderWrapper +import rikka.shizuku.SystemServiceHelper object WifiAction { fun executeAction(context: Context, action: Action): ActionStatus { if (action is ToggleAction) { val status = setWifiEnabled(context, action.isEnabled) - return if (status != ActionStatus.SUCCESS && Settings.isRootAccessEnabled() && RootHelper.isRootEnabled()) { - setWifiEnabledRoot(action.isEnabled) + return if (status != ActionStatus.SUCCESS) { + // Note: could have failed due to Airplane mode restriction + // Try with root + if (Shizuku.pingBinder()) { + setWifiEnabledShizuku(context, action.isEnabled) + } else if (Settings.isRootAccessEnabled() && RootHelper.isRootEnabled()) { + setWifiEnabledRoot(action.isEnabled) + } else { + status + } } else { status } @@ -37,7 +50,10 @@ object WifiAction { return try { val wifiMan = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - if (wifiMan.setWifiEnabled(enable)) ActionStatus.SUCCESS else ActionStatus.REMOTE_FAILURE + if (wifiMan.setWifiEnabled(enable)) + ActionStatus.SUCCESS + else + ActionStatus.REMOTE_FAILURE } catch (e: Exception) { Logger.writeLine(Log.ERROR, e) ActionStatus.REMOTE_FAILURE @@ -57,4 +73,23 @@ object WifiAction { ActionStatus.REMOTE_FAILURE } } + + private fun setWifiEnabledShizuku(context: Context, enable: Boolean): ActionStatus { + return runCatching { + val wifiMgr = SystemServiceHelper.getSystemService(Context.WIFI_SERVICE) + .let(::ShizukuBinderWrapper) + .let(IWifiManager.Stub::asInterface) + + val ret = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + wifiMgr.setWifiEnabled("com.android.shell", enable) + } else { + wifiMgr.setWifiEnabled(enable) + } + + if (ret) ActionStatus.SUCCESS else ActionStatus.REMOTE_FAILURE + }.getOrElse { + Logger.writeLine(Log.ERROR, it) + ActionStatus.REMOTE_FAILURE + } + } } \ No newline at end of file diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/shizuku/ShizukuUtils.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/shizuku/ShizukuUtils.kt new file mode 100644 index 00000000..a358a3ce --- /dev/null +++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/shizuku/ShizukuUtils.kt @@ -0,0 +1,120 @@ +package com.thewizrd.wearsettings.shizuku + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import rikka.shizuku.Shizuku +import rikka.shizuku.ShizukuProvider + +object ShizukuUtils { + private const val PACKAGE_NAME = "moe.shizuku.privileged.api" + + // Link to Play Store listing + private const val PLAY_STORE_APP_URI = "market://details?id=${PACKAGE_NAME}" + private const val PLAY_STORE_APP_WEBURI = + "https://play.google.com/store/apps/details?id=${PACKAGE_NAME}" + + fun getShizukuState(context: Context): ShizukuState { + return if (Shizuku.pingBinder()) { + if (isPermissionGranted(context)) { + ShizukuState.RUNNING + } else { + ShizukuState.PERMISSION_DENIED + } + } else if (!isShizukuInstalled(context)) { + ShizukuState.NOT_INSTALLED + } else { + ShizukuState.NOT_RUNNING + } + } + + fun isShizukuInstalled(context: Context): Boolean = try { + context.packageManager.getApplicationInfo(PACKAGE_NAME, 0).enabled + } catch (e: PackageManager.NameNotFoundException) { + false + } + + fun isPermissionGranted(context: Context): Boolean { + return if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) { + ContextCompat.checkSelfPermission( + context, + ShizukuProvider.PERMISSION + ) == PackageManager.PERMISSION_GRANTED + } else { + Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED + } + } + + fun requestPermission(context: Activity, requestCode: Int) { + if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) { + ActivityCompat.requestPermissions( + context, + arrayOf(ShizukuProvider.PERMISSION), + requestCode + ) + } else { + Shizuku.requestPermission(requestCode) + } + } + + fun requestPermission(context: Fragment, requestCode: Int) { + if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) { + context.requestPermissions(arrayOf(ShizukuProvider.PERMISSION), requestCode) + } else { + Shizuku.requestPermission(requestCode) + } + } + + fun openPlayStoreListing(context: Context) { + try { + context.startActivity( + Intent(Intent.ACTION_VIEW) + .addCategory(Intent.CATEGORY_BROWSABLE) + .setData(getPlayStoreURI()) + ) + } catch (e: ActivityNotFoundException) { + val i = Intent(Intent.ACTION_VIEW) + .addCategory(Intent.CATEGORY_BROWSABLE) + .setData(getPlayStoreWebURI()) + + if (i.resolveActivity(context.packageManager) != null) { + context.startActivity(i) + } + } + } + + fun startShizukuActivity(context: Context) { + runCatching { + context.startActivity( + Intent(Intent.ACTION_MAIN).apply { + component = ComponentName( + PACKAGE_NAME, "moe.shizuku.manager.MainActivity" + ) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + } + } + + private fun getPlayStoreURI(): Uri { + return Uri.parse(PLAY_STORE_APP_URI) + } + + private fun getPlayStoreWebURI(): Uri { + return Uri.parse(PLAY_STORE_APP_WEBURI) + } +} + +enum class ShizukuState { + NOT_INSTALLED, + NOT_RUNNING, + PERMISSION_DENIED, + RUNNING +} \ No newline at end of file diff --git a/wearsettings/src/main/res/layout/activity_main.xml b/wearsettings/src/main/res/layout/activity_main.xml index 839a6ac6..efeba37a 100644 --- a/wearsettings/src/main/res/layout/activity_main.xml +++ b/wearsettings/src/main/res/layout/activity_main.xml @@ -12,7 +12,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true" - app:liftOnScroll="true"> + app:liftOnScroll="true" + tools:visibility="gone"> + + + + + + + + + + + + + + + + https://github.com/SimpleAppProjects/SimpleWear/wiki/Root-Access https://github.com/SimpleAppProjects/SimpleWear/wiki/Enable-WRITE_SECURE_SETTINGS-permission + + Bluetooth + Bluetooth permission enabled + Bluetooth permission disabled + + Shizuku + Shizuku permission disabled + Shizuku not installed + Shizuku service not running + Shizuku service enabled + Shizuku can be used as an option to provide system permissions to SimpleWear for non-root devices. As this is a third-party app, please use at your own discretion. + Please note that the Shizuku service will need to be manually restarted every time on boot. + \ No newline at end of file