Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Build.PS1
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
$env:JAVA_HOME = "tools\jdk17\jdk-17.0.13+11";
$env:ANDROID_HOME = "tools\android-sdk";
.\gradlew.bat assembleRelease
pause
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<queries>
<package android:name="com.huami.watch.hmwatchmanager" />
<intent>
<action android:name="android.service.notification.NotificationListenerService" />
</intent>
</queries>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class MainActivity : AppCompatActivity() {
private fun scheduleJob() {
val componentName = ComponentName(this, NotificationAccessJobService::class.java)
var period = 60 * 1_000L // 60s
if (JobInfo.getMinPeriodMillis() > 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}