Add events notifications

This commit is contained in:
BinTianqi
2025-03-09 12:19:09 +08:00
parent ada12bf3dc
commit 10e34a4e96
10 changed files with 192 additions and 21 deletions

View File

@@ -397,6 +397,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) {
}
composable<AuthSettings> { AuthSettingsScreen(::navigateUp) }
composable<ApiSettings> { ApiSettings(::navigateUp) }
composable<Notifications> { NotificationsScreen(::navigateUp) }
composable<About> { AboutScreen(::navigateUp) }
composable<Authenticate>(

View File

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

View File

@@ -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())
}
}

View File

@@ -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

View File

@@ -1841,6 +1841,7 @@ fun InstallSystemUpdateScreen(onNavigateUp: () -> Unit) {
}
}
var uri by remember { mutableStateOf<Uri?>(null) }
var errorMessage by remember { mutableStateOf<String?>(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 }
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M640,440v-80h240v80L640,440ZM360,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM40,800v-112q0,-34 17.5,-62.5T104,582q62,-31 126,-46.5T360,520q66,0 130,15.5T616,582q29,15 46.5,43.5T680,688v112L40,800ZM120,720h480v-32q0,-11 -5.5,-20T580,654q-54,-27 -109,-40.5T360,600q-56,0 -111,13.5T140,654q-9,5 -14.5,14t-5.5,20v32ZM360,400q33,0 56.5,-23.5T440,320q0,-33 -23.5,-56.5T360,240q-33,0 -56.5,23.5T280,320q0,33 23.5,56.5T360,400ZM360,320ZM360,720Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -592,7 +592,8 @@
<string name="material_you_color">Цвет Material You</string>
<string name="dark_theme">Тёмная тема</string>
<string name="follow_system">Следовать за системой</string>
<string name="black_theme">Чёрная тема</string>
<string name="black_theme">Чёрная тема</string>
<string name="notifications">Notifications</string> <!--TODO-->
<string name="about">О приложении</string>
<string name="project_homepage">Домашняя страница проекта</string>
<string name="appearance">Оформление</string>
@@ -665,6 +666,8 @@
<string name="status_failure_storage">Операция не удалась из-за проблем с хранилищем. Например, на устройстве может быть недостаточно места или внешние носители могут быть недоступны. Вы можете попытаться освободить место или вставить другой внешний носитель.</string>
<string name="status_failure_incompatible">Операция не удалась, так как она принципиально несовместима с этим устройством. Например, приложению может потребоваться аппаратная функция, которой нет, в нем может отсутствовать собственный код для ABI, поддерживаемых устройством, или ему требуется более новая версия SDK и т.д.</string>
<!--TODO: events notification-->
<string name="info_dhizuku">Dhizuku это инструмент позволяющий делиться правами Владельца устройства с другими приложениями.</string>
<string name="info_device_id_attestation">Указывает, поддерживает ли устройство проверку идентификаторов устройств в дополнение к проверке ключей.</string>
<string name="info_unique_device_attestation">Да, если реализация StrongBox Keymaster на устройстве была обеспечена индивидуальным сертификатом аттестации и может использовать его для подписи записей аттестации (индивидуальный сертификат аттестации могут использовать только Keymaster с уровнем безопасности StrongBox).</string>

View File

@@ -597,6 +597,7 @@
<string name="dark_theme">Koyu Tema</string>
<string name="follow_system">Sistemi Takip Et</string>
<string name="black_theme">Siyah Tema</string>
<string name="notifications">Notifications</string> <!--TODO-->
<string name="about">Hakkında</string>
<string name="project_homepage">Proje Ana Sayfası</string>
<string name="appearance">Görünüm</string>
@@ -668,6 +669,8 @@
<string name="status_failure_storage">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.</string>
<string name="status_failure_incompatible">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.</string>
<!--TODO: events notification-->
<string name="info_dhizuku">Dhizuku, Cihaz Sahibi izinlerini diğer uygulamalarla paylaşabilen bir araçtır.</string>
<string name="info_device_id_attestation">Cihazın, anahtar doğrulamanın yanı sıra cihaz tanımlayıcılarının doğrulamasını destekleyip desteklemediğini belirtir.</string>
<string name="info_unique_device_attestation">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).</string>

View File

@@ -577,6 +577,7 @@
<string name="dark_theme">深色主题</string>
<string name="follow_system">跟随系统</string>
<string name="black_theme">黑色主题</string>
<string name="notifications">通知</string>
<string name="about">关于</string>
<string name="project_homepage">项目主页</string>
<string name="appearance">外观</string>
@@ -647,6 +648,21 @@
<string name="status_failure_storage">由于存储问题,操作失败。例如,设备空间不足,或外部媒体不可用。你可以尝试清理空间或插入不同的外部媒体。</string>
<string name="status_failure_incompatible">操作失败因为它与设备不兼容。例如这个app可能需要不存在的硬件功能它有可能缺少受此设备支持的ABI的本地代码或它需要高于此设备的SDK版本。</string>
<string name="events">事件</string>
<string name="password_changed">密码已更改</string>
<string name="user_added">用户已添加</string>
<string name="user_started">用户已启动</string>
<string name="user_switched">用户已切换</string>
<string name="user_stopped">用户已停止</string>
<string name="user_removed">用户已移除</string>
<string name="bug_report_shared">错误报告已分享</string>
<string name="bug_report_sharing_declined">错误报告已取消分享</string>
<string name="bug_report_failed">错误报告失败</string>
<string name="bug_report_failure_failed_completing">错误报告完成失败</string>
<string name="bug_report_failure_no_longer_available">错误报告不再可收集</string>
<string name="system_update_pending">系统更新等待中</string>
<string name="received_time">接收时间</string>
<string name="info_dhizuku">Dhizuku可以分享Device owner权限给其余应用</string>
<string name="info_device_id_attestation">指示设备是否除了密钥证明之外还支持设备标识符证明</string>
<string name="info_unique_device_attestation">如果设备上的StrongBox Keymaster可以配置单独的证明证书并且可以使用该证书签署证明记录则返回true只有StrongBox安全级别的Keymaster才能使用单独的证明证书进行证明</string>

View File

@@ -616,6 +616,7 @@
<string name="dark_theme">Dark theme</string>
<string name="follow_system">Follow system</string>
<string name="black_theme">Black theme</string>
<string name="notifications">Notifications</string>
<string name="about">About</string>
<string name="project_homepage">Project homepage</string>
<string name="appearance">Appearance</string>
@@ -687,6 +688,21 @@
<string name="status_failure_storage">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.</string>
<string name="status_failure_incompatible">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.</string>
<string name="events">Events</string>
<string name="password_changed">Password changed</string>
<string name="user_added">User added</string>
<string name="user_started">User started</string>
<string name="user_switched">User switched</string>
<string name="user_stopped">User stopped</string>
<string name="user_removed">User removed</string>
<string name="bug_report_shared">Bug report shared</string>
<string name="bug_report_sharing_declined">Bug report sharing declined</string>
<string name="bug_report_failed">Bug report failed</string>
<string name="bug_report_failure_failed_completing">Bug report completion process failed</string>
<string name="bug_report_failure_no_longer_available">Bug report is no longer available for collection</string>
<string name="system_update_pending">System update pending</string>
<string name="received_time">Received time</string>
<string name="info_dhizuku">Dhizuku is a tool that can share Device owner permissions to other application.</string>
<string name="info_device_id_attestation">Indicates if the device supports attestation of device identifiers in addition to key attestation.</string>
<string name="info_unique_device_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).</string>