diff --git a/Build.PS1 b/Build.PS1
new file mode 100644
index 0000000..3cc4bc6
--- /dev/null
+++ b/Build.PS1
@@ -0,0 +1,4 @@
+$env:JAVA_HOME = "tools\jdk17\jdk-17.0.13+11";
+$env:ANDROID_HOME = "tools\android-sdk";
+.\gradlew.bat assembleRelease
+pause
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6f935d1..cae7360 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
period) period = JobInfo.getMinPeriodMillis() // I read that minimum is 15m
+ if (JobInfo.getMinPeriodMillis() > period) period = JobInfo.getMinPeriodMillis()
val jobInfo = JobInfo.Builder(123, componentName)
.setPeriodic(period)
.setPersisted(true)
diff --git a/app/src/main/java/nodomain/watcher/checkzeppnotificationJob/NotificationAccessJobService.kt b/app/src/main/java/nodomain/watcher/checkzeppnotificationJob/NotificationAccessJobService.kt
index 41f58a4..1fbee9c 100755
--- a/app/src/main/java/nodomain/watcher/checkzeppnotificationJob/NotificationAccessJobService.kt
+++ b/app/src/main/java/nodomain/watcher/checkzeppnotificationJob/NotificationAccessJobService.kt
@@ -1,20 +1,23 @@
package nodomain.watcher.checkzeppnotificationJob
-import android.app.job.JobParameters
-import android.app.job.JobService
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
+import android.app.job.JobParameters
+import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.os.Build
import android.provider.Settings
+import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
+import android.os.Build
import androidx.work.Configuration
+
class NotificationAccessJobService : JobService() {
+
companion object {
private val PACKAGE = "com.huami.watch.hmwatchmanager"
private val channelId = "notification_access_alert"
@@ -27,25 +30,51 @@ class NotificationAccessJobService : JobService() {
}
override fun onStartJob(params: JobParameters?): Boolean {
+ Log.d("NotificationAccessJS", "Job started")
+
if (!isNotificationListenerEnabled(PACKAGE)) {
- showToast()
- showNotification()
+ Log.d("NotificationAccessJS", "Permission missing")
+
+ // Try auto-grant if root is available
+ if (RootUtils.isRootAvailable()) {
+ showToast()
+ Log.d("NotificationAccessJS", "Root available, attempting auto-grant")
+ val granted = RootUtils.grantNotificationListener(this, PACKAGE)
+ if (granted) {
+ Log.d("NotificationAccessJS", "Auto-grant successful")
+ showFixedNotification()
+ return false
+ } else {
+ Log.e("NotificationAccessJS", "Auto-grant failed")
+ }
+ } else {
+ Log.d("NotificationAccessJS", "Root not available")
+ }
+
+ // Fallback to manual warning
+ try {
+ showToast()
+ showNotification()
+ } catch (ignored: Exception) {
+ Log.d("NotificationAccessJS", "toast failed", ignored)
+ }
+
+ } else {
+ Log.d("NotificationAccessJS", "Permission already granted")
}
- jobFinished(params, false)
- return true
+
+ return false
}
override fun onStopJob(params: JobParameters?): Boolean {
- // Return true to reschedule if job is interrupted
return true
}
private fun isNotificationListenerEnabled(packageName: String): Boolean {
- val enabledListeners = Settings.Secure.getString(
- contentResolver,
- "enabled_notification_listeners"
- ) ?: return false
-
+ val enabledListeners = Settings.Secure.getString(contentResolver, "enabled_notification_listeners")
+ if (enabledListeners.isNullOrEmpty()) {
+ return false
+ }
val names = enabledListeners.split(":")
for (name in names) {
val component = ComponentName.unflattenFromString(name)
@@ -57,16 +86,11 @@ class NotificationAccessJobService : JobService() {
}
private fun showToast() {
- Toast.makeText(
- applicationContext,
- getString(R.string.toast, PACKAGE),
- Toast.LENGTH_LONG
- ).show()
+ Toast.makeText(applicationContext, getString(R.string.toast, "Zepp App"), Toast.LENGTH_LONG).show()
}
private fun showNotification() {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
@@ -93,4 +117,21 @@ class NotificationAccessJobService : JobService() {
manager.notify(1001, notification)
}
+
+ private fun showFixedNotification() {
+ val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val channelId = "permission_fixed_channel"
+ val channelName = "Permission Fixed"
+ val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
+ manager.createNotificationChannel(channel)
+
+ val notification = NotificationCompat.Builder(this, channelId)
+ .setContentTitle("Permission Granted")
+ .setContentText("Zepp Notification Access was automatically enabled.")
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setAutoCancel(true)
+ .build()
+
+ manager.notify(2, notification)
+ }
}
diff --git a/app/src/main/java/nodomain/watcher/checkzeppnotificationJob/RootUtils.kt b/app/src/main/java/nodomain/watcher/checkzeppnotificationJob/RootUtils.kt
new file mode 100644
index 0000000..caba2da
--- /dev/null
+++ b/app/src/main/java/nodomain/watcher/checkzeppnotificationJob/RootUtils.kt
@@ -0,0 +1,94 @@
+package nodomain.watcher.checkzeppnotificationJob
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.provider.Settings
+import java.io.DataOutputStream
+import java.io.IOException
+
+object RootUtils {
+
+ fun isRootAvailable(): Boolean {
+ return try {
+ val process = Runtime.getRuntime().exec("su")
+ val os = DataOutputStream(process.outputStream)
+ os.writeBytes("exit\n")
+ os.flush()
+ val exitValue = process.waitFor()
+ exitValue == 0
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ fun grantNotificationListener(context: Context, targetPackage: String): Boolean {
+ android.util.Log.d("RootUtils", "Attempting to grant notification listener for $targetPackage")
+ val componentName = getNotificationListenerComponent(context, targetPackage)
+ if (componentName == null) {
+ android.util.Log.e("RootUtils", "NotificationListenerService component not found for $targetPackage")
+ return false
+ }
+ android.util.Log.d("RootUtils", "Found component: $componentName")
+
+ // Command to allow notification listener
+ val cmd = "cmd notification allow_listener $componentName"
+ android.util.Log.d("RootUtils", "Executing command: $cmd")
+
+ return try {
+ val success = executeRootCommand(cmd)
+ android.util.Log.d("RootUtils", "Command execution result: $success")
+ if (!success) {
+ android.util.Log.e("RootUtils", "Failed to execute allow_listener command via root")
+ false
+ } else {
+ true
+ }
+ } catch (e: Exception) {
+ android.util.Log.e("RootUtils", "Exception executing root command", e)
+ false
+ }
+ }
+
+ fun getNotificationListenerComponent(context: Context, targetPackage: String): String? {
+ val packageManager = context.packageManager
+ val intent = android.content.Intent("android.service.notification.NotificationListenerService")
+ intent.setPackage(targetPackage)
+
+ val services = packageManager.queryIntentServices(intent, PackageManager.GET_META_DATA)
+ if (services.isEmpty()) {
+ android.util.Log.w("RootUtils", "No services found for intent with package $targetPackage")
+ return null
+ }
+
+ val serviceInfo = services[0].serviceInfo
+ return "${serviceInfo.packageName}/${serviceInfo.name}"
+ }
+
+ private fun executeRootCommand(command: String): Boolean {
+ var process: Process? = null
+ var os: DataOutputStream? = null
+ return try {
+ process = Runtime.getRuntime().exec("su")
+ os = DataOutputStream(process.outputStream)
+ os.writeBytes("$command\n")
+ os.writeBytes("exit\n")
+ os.flush()
+ val exitValue = process.waitFor()
+ android.util.Log.d("RootUtils", "Root command exit value: $exitValue")
+ exitValue == 0
+ } catch (e: IOException) {
+ android.util.Log.e("RootUtils", "IOException in executeRootCommand", e)
+ false
+ } catch (e: InterruptedException) {
+ android.util.Log.e("RootUtils", "InterruptedException in executeRootCommand", e)
+ false
+ } finally {
+ try {
+ os?.close()
+ process?.destroy()
+ } catch (e: Exception) {
+ // Ignore
+ }
+ }
+ }
+}