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