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 + } + } + } +}