From 10e34a4e9699a003676e77e1666b2a31615445ad Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 9 Mar 2025 12:19:09 +0800 Subject: [PATCH] Add events notifications --- .../com/bintianqi/owndroid/MainActivity.kt | 1 + .../bintianqi/owndroid/NotificationUtils.kt | 38 ++++++-- .../java/com/bintianqi/owndroid/Receiver.kt | 97 +++++++++++++++++-- .../java/com/bintianqi/owndroid/Settings.kt | 20 ++++ .../java/com/bintianqi/owndroid/dpm/System.kt | 8 +- .../main/res/drawable/person_remove_fill0.xml | 9 ++ app/src/main/res/values-ru/strings.xml | 5 +- app/src/main/res/values-tr/strings.xml | 3 + app/src/main/res/values-zh-rCN/strings.xml | 16 +++ app/src/main/res/values/strings.xml | 16 +++ 10 files changed, 192 insertions(+), 21 deletions(-) create mode 100644 app/src/main/res/drawable/person_remove_fill0.xml diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 072e282..a5b5d8e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -397,6 +397,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { } composable { AuthSettingsScreen(::navigateUp) } composable { ApiSettings(::navigateUp) } + composable { NotificationsScreen(::navigateUp) } composable { AboutScreen(::navigateUp) } composable( diff --git a/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt b/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt index 13a2033..9cc55d8 100644 --- a/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt @@ -1,19 +1,13 @@ package com.bintianqi.owndroid import android.Manifest +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.content.pm.PackageManager import android.os.Build -/** - * ### Notification channels - * - LockTaskMode - * - * ### Notification IDs - * - 1: Stop lock task mode - */ object NotificationUtils { fun checkPermission(context: Context): Boolean { return if(Build.VERSION.SDK_INT >= 33) @@ -23,7 +17,33 @@ object NotificationUtils { fun registerChannels(context: Context) { if(Build.VERSION.SDK_INT < 26) return val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val channel = NotificationChannel("LockTaskMode", context.getString(R.string.lock_task_mode), NotificationManager.IMPORTANCE_HIGH) - nm.createNotificationChannel(channel) + val lockTaskMode = NotificationChannel(Channel.LOCK_TASK_MODE, context.getString(R.string.lock_task_mode), NotificationManager.IMPORTANCE_HIGH) + val events = NotificationChannel(Channel.EVENTS, context.getString(R.string.events), NotificationManager.IMPORTANCE_HIGH) + nm.createNotificationChannels(listOf(lockTaskMode, events)) + } + fun notify(context: Context, id: Int, notification: Notification) { + val sp = context.getSharedPreferences("data", Context.MODE_PRIVATE) + if(sp.getBoolean("n_$id", true) && checkPermission(context)) { + registerChannels(context) + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + nm.notify(id, notification) + } + } + object Channel { + const val LOCK_TASK_MODE = "LockTaskMode" + const val EVENTS = "Events" + } + object ID { + const val LOCK_TASK_MODE = 1 + const val PASSWORD_CHANGED = 2 + const val USER_ADDED = 3 + const val USER_STARTED = 4 + const val USER_SWITCHED = 5 + const val USER_STOPPED = 6 + const val USER_REMOVED = 7 + const val BUG_REPORT_SHARED = 8 + const val BUG_REPORT_SHARING_DECLINED = 9 + const val BUG_REPORT_FAILED = 10 + const val SYSTEM_UPDATE_PENDING = 11 } } \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index 2a3b249..48d7a7b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -8,6 +8,8 @@ import android.content.Context import android.content.Intent import android.os.Build.VERSION import android.os.PersistableBundle +import android.os.UserHandle +import android.os.UserManager import android.widget.Toast import androidx.core.app.NotificationCompat import com.bintianqi.owndroid.dpm.handleNetworkLogs @@ -18,6 +20,9 @@ import com.bintianqi.owndroid.dpm.setDefaultAffiliationID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale class Receiver : DeviceAdminReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -82,21 +87,101 @@ class Receiver : DeviceAdminReceiver() { override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) { super.onLockTaskModeEntering(context, intent, pkg) + if(!NotificationUtils.checkPermission(context)) return NotificationUtils.registerChannels(context) - val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val intent = Intent(context, this::class.java).apply { action = "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE" } + val intent = Intent(context, this::class.java).setAction("com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) - val builder = NotificationCompat.Builder(context, "LockTaskMode") + val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.LOCK_TASK_MODE) .setContentTitle(context.getText(R.string.lock_task_mode)) .setSmallIcon(R.drawable.lock_fill0) .addAction(NotificationCompat.Action.Builder(null, context.getString(R.string.stop), pendingIntent).build()) - .setPriority(NotificationCompat.PRIORITY_HIGH) - nm.notify(1, builder.build()) + NotificationUtils.notify(context, NotificationUtils.ID.LOCK_TASK_MODE, builder.build()) } override fun onLockTaskModeExiting(context: Context, intent: Intent) { super.onLockTaskModeExiting(context, intent) val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - nm.cancel(1) + nm.cancel(NotificationUtils.ID.LOCK_TASK_MODE) + } + + override fun onPasswordChanged(context: Context, intent: Intent, userHandle: UserHandle) { + super.onPasswordChanged(context, intent, userHandle) + sendUserRelatedNotification(context, userHandle, NotificationUtils.ID.PASSWORD_CHANGED, R.string.password_changed, R.drawable.password_fill0) + } + + override fun onUserAdded(context: Context, intent: Intent, addedUser: UserHandle) { + super.onUserAdded(context, intent, addedUser) + sendUserRelatedNotification(context, addedUser, NotificationUtils.ID.USER_ADDED, R.string.user_added, R.drawable.person_add_fill0) + } + + override fun onUserStarted(context: Context, intent: Intent, startedUser: UserHandle) { + super.onUserStarted(context, intent, startedUser) + sendUserRelatedNotification(context, startedUser, NotificationUtils.ID.USER_STARTED, R.string.user_started, R.drawable.person_fill0) + } + + override fun onUserSwitched(context: Context, intent: Intent, switchedUser: UserHandle) { + super.onUserSwitched(context, intent, switchedUser) + sendUserRelatedNotification(context, switchedUser, NotificationUtils.ID.USER_SWITCHED, R.string.user_switched, R.drawable.person_fill0) + } + + override fun onUserStopped(context: Context, intent: Intent, stoppedUser: UserHandle) { + super.onUserStopped(context, intent, stoppedUser) + sendUserRelatedNotification(context, stoppedUser, NotificationUtils.ID.USER_STOPPED, R.string.user_stopped, R.drawable.person_fill0) + } + + override fun onUserRemoved(context: Context, intent: Intent, removedUser: UserHandle) { + super.onUserRemoved(context, intent, removedUser) + sendUserRelatedNotification(context, removedUser, NotificationUtils.ID.USER_REMOVED, R.string.user_removed, R.drawable.person_remove_fill0) + } + + override fun onBugreportShared(context: Context, intent: Intent, hash: String) { + super.onBugreportShared(context, intent, hash) + val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) + .setContentTitle(context.getString(R.string.bug_report_shared)) + .setContentText("SHA-256 hash: $hash") + .setSmallIcon(R.drawable.bug_report_fill0) + NotificationUtils.notify(context, NotificationUtils.ID.BUG_REPORT_SHARED, builder.build()) + } + + override fun onBugreportSharingDeclined(context: Context, intent: Intent) { + super.onBugreportSharingDeclined(context, intent) + val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) + .setContentTitle(context.getString(R.string.bug_report_sharing_declined)) + .setSmallIcon(R.drawable.bug_report_fill0) + NotificationUtils.notify(context, NotificationUtils.ID.BUG_REPORT_SHARING_DECLINED, builder.build()) + } + + override fun onBugreportFailed(context: Context, intent: Intent, failureCode: Int) { + super.onBugreportFailed(context, intent, failureCode) + val message = when(failureCode) { + BUGREPORT_FAILURE_FAILED_COMPLETING -> R.string.bug_report_failure_failed_completing + BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE -> R.string.bug_report_failure_no_longer_available + else -> R.string.place_holder + } + val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) + .setContentTitle(context.getString(R.string.bug_report_failed)) + .setContentText(context.getString(message)) + .setSmallIcon(R.drawable.bug_report_fill0) + NotificationUtils.notify(context, NotificationUtils.ID.BUG_REPORT_FAILED, builder.build()) + } + + override fun onSystemUpdatePending(context: Context, intent: Intent, receivedTime: Long) { + super.onSystemUpdatePending(context, intent, receivedTime) + val time = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(receivedTime)) + val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) + .setContentTitle(context.getString(R.string.system_update_pending)) + .setContentText(context.getString(R.string.received_time) + ": $time") + .setSmallIcon(R.drawable.system_update_fill0) + NotificationUtils.notify(context, NotificationUtils.ID.SYSTEM_UPDATE_PENDING, builder.build()) + } + + private fun sendUserRelatedNotification(context: Context, userHandle: UserHandle, id: Int, title: Int, icon: Int) { + val um = context.getSystemService(Context.USER_SERVICE) as UserManager + val serial = um.getSerialNumberForUser(userHandle) + val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) + .setContentTitle(context.getString(title)) + .setContentText(context.getString(R.string.serial_number) + ": $serial") + .setSmallIcon(icon) + NotificationUtils.notify(context, id, builder.build()) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index d5620d6..ed19e1f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -57,6 +57,7 @@ fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { onNavigate(Appearance) } FunctionItem(title = R.string.security, icon = R.drawable.lock_fill0) { onNavigate(AuthSettings) } FunctionItem(title = R.string.api, icon = R.drawable.apps_fill0) { onNavigate(ApiSettings) } + FunctionItem(R.string.notifications, icon = R.drawable.notifications_fill0) { onNavigate(Notifications) } FunctionItem(title = R.string.export_logs, icon = R.drawable.description_fill0) { val time = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date(System.currentTimeMillis())) exportLogsLauncher.launch("owndroid_log_$time") @@ -219,6 +220,25 @@ fun ApiSettings(onNavigateUp: () -> Unit) { } } +@Serializable object Notifications + +@Composable +fun NotificationsScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.notifications, 0.dp, onNavigateUp) { + val sp = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) + val map = mapOf( + NotificationUtils.ID.PASSWORD_CHANGED to R.string.password_changed, NotificationUtils.ID.USER_ADDED to R.string.user_added, + NotificationUtils.ID.USER_STARTED to R.string.user_started, NotificationUtils.ID.USER_SWITCHED to R.string.user_switched, + NotificationUtils.ID.USER_STOPPED to R.string.user_stopped, NotificationUtils.ID.USER_REMOVED to R.string.user_removed, + NotificationUtils.ID.BUG_REPORT_SHARED to R.string.bug_report_shared, + NotificationUtils.ID.BUG_REPORT_SHARING_DECLINED to R.string.bug_report_sharing_declined, + NotificationUtils.ID.BUG_REPORT_FAILED to R.string.bug_report_failed, + NotificationUtils.ID.SYSTEM_UPDATE_PENDING to R.string.system_update_pending + ) + map.forEach { (k, v) -> + SwitchItem(v, getState = { sp.getBoolean("n_$k", true) }, onCheckedChange = { sp.edit { putBoolean("n_$k", it) } }) + } +} + @Serializable object About @Composable diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index faafe9d..a645c5f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -1841,6 +1841,7 @@ fun InstallSystemUpdateScreen(onNavigateUp: () -> Unit) { } } var uri by remember { mutableStateOf(null) } + var errorMessage by remember { mutableStateOf(null) } val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri = it } MyScaffold(R.string.install_system_update, 8.dp, onNavigateUp) { Button( @@ -1859,11 +1860,7 @@ fun InstallSystemUpdateScreen(onNavigateUp: () -> Unit) { dpm.installSystemUpdate(receiver, uri!!, executor, callback) Toast.makeText(context, R.string.start_install_system_update, Toast.LENGTH_SHORT).show() } catch(e: Exception) { - Toast.makeText( - context, - context.getString(R.string.install_system_update_failed) + e.cause.toString(), - Toast.LENGTH_SHORT - ).show() + errorMessage = e.message } }, modifier = Modifier.fillMaxWidth() @@ -1874,4 +1871,5 @@ fun InstallSystemUpdateScreen(onNavigateUp: () -> Unit) { Spacer(Modifier.padding(vertical = 10.dp)) Notes(R.string.auto_reboot_after_install_succeed) } + ErrorDialog(errorMessage) { errorMessage = null } } diff --git a/app/src/main/res/drawable/person_remove_fill0.xml b/app/src/main/res/drawable/person_remove_fill0.xml new file mode 100644 index 0000000..b92b1eb --- /dev/null +++ b/app/src/main/res/drawable/person_remove_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4f61472..e183863 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -592,7 +592,8 @@ Цвет Material You Тёмная тема Следовать за системой - Чёрная тема + Чёрная тема + Notifications О приложении Домашняя страница проекта Оформление @@ -665,6 +666,8 @@ Операция не удалась из-за проблем с хранилищем. Например, на устройстве может быть недостаточно места или внешние носители могут быть недоступны. Вы можете попытаться освободить место или вставить другой внешний носитель. Операция не удалась, так как она принципиально несовместима с этим устройством. Например, приложению может потребоваться аппаратная функция, которой нет, в нем может отсутствовать собственный код для ABI, поддерживаемых устройством, или ему требуется более новая версия SDK и т.д. + + Dhizuku это инструмент позволяющий делиться правами Владельца устройства с другими приложениями. Указывает, поддерживает ли устройство проверку идентификаторов устройств в дополнение к проверке ключей. Да, если реализация StrongBox Keymaster на устройстве была обеспечена индивидуальным сертификатом аттестации и может использовать его для подписи записей аттестации (индивидуальный сертификат аттестации могут использовать только Keymaster с уровнем безопасности StrongBox). diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 6c6fabc..27d3b72 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -597,6 +597,7 @@ Koyu Tema Sistemi Takip Et Siyah Tema + Notifications Hakkında Proje Ana Sayfası Görünüm @@ -668,6 +669,8 @@ Depolama sorunları nedeniyle işlem başarısız oldu. Örneğin, cihazda yer azalmış olabilir veya harici medya kullanılamıyor olabilir. Yer açmayı veya farklı bir harici medya takmayı deneyebilirsiniz. Bu cihazla temel olarak uyumsuz olduğu için işlem başarısız oldu. Örneğin, uygulama mevcut olmayan bir donanım özelliği gerektirebilir, cihazın desteklediği ABI\'lar için yerel kod eksik olabilir veya daha yeni bir SDK sürümü gerektirebilir vb. + + Dhizuku, Cihaz Sahibi izinlerini diğer uygulamalarla paylaşabilen bir araçtır. Cihazın, anahtar doğrulamanın yanı sıra cihaz tanımlayıcılarının doğrulamasını destekleyip desteklemediğini belirtir. Evet, eğer cihaz üzerindeki StrongBox Keymaster uygulaması bireysel bir doğrulama sertifikası ile sağlanmışsa ve bunu kullanarak doğrulama kayıtlarını imzalayabiliyorsa (yalnızca StrongBox güvenlik seviyesine sahip Keymaster bireysel doğrulama sertifikası kullanabilir). diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ddfd296..5b51fb5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -577,6 +577,7 @@ 深色主题 跟随系统 黑色主题 + 通知 关于 项目主页 外观 @@ -647,6 +648,21 @@ 由于存储问题,操作失败。例如,设备空间不足,或外部媒体不可用。你可以尝试清理空间或插入不同的外部媒体。 操作失败,因为它与设备不兼容。例如,这个app可能需要不存在的硬件功能,它有可能缺少受此设备支持的ABI的本地代码,或它需要高于此设备的SDK版本。 + 事件 + 密码已更改 + 用户已添加 + 用户已启动 + 用户已切换 + 用户已停止 + 用户已移除 + 错误报告已分享 + 错误报告已取消分享 + 错误报告失败 + 错误报告完成失败 + 错误报告不再可收集 + 系统更新等待中 + 接收时间 + Dhizuku可以分享Device owner权限给其余应用 指示设备是否除了密钥证明之外还支持设备标识符证明 如果设备上的StrongBox Keymaster可以配置单独的证明证书并且可以使用该证书签署证明记录,则返回true(只有StrongBox安全级别的Keymaster才能使用单独的证明证书进行证明) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 212f18c..571f1bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -616,6 +616,7 @@ Dark theme Follow system Black theme + Notifications About Project homepage Appearance @@ -687,6 +688,21 @@ The operation failed because of storage issues. For example, the device may be running low on space, or external media may be unavailable. You may try to help free space or insert different external media. The operation failed because it is fundamentally incompatible with this device. For example, the app may require a hardware feature that doesn\'t exist, it may be missing native code for the ABIs supported by the device, or it requires a newer SDK version, etc. + Events + Password changed + User added + User started + User switched + User stopped + User removed + Bug report shared + Bug report sharing declined + Bug report failed + Bug report completion process failed + Bug report is no longer available for collection + System update pending + Received time + Dhizuku is a tool that can share Device owner permissions to other application. Indicates if the device supports attestation of device identifiers in addition to key attestation. Yes if the StrongBox Keymaster implementation on the device was provisioned with an individual attestation certificate and can sign attestation records using it (only Keymaster with StrongBox security level can use an individual attestation certificate).