owenclave / app/src/main/java/io/nekohasekai/sagernet/bg/ServiceNotification.kt

github - dev - back to tree - upstream
/****************************************************************************** * * * Copyright (C) 2021 by nekohasekai * * Copyright (C) 2021 by Max Lv * * Copyright (C) 2021 by Mygod Studio * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ******************************************************************************/ package io.nekohasekai.sagernet.bg import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import android.os.PowerManager import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.ServiceCompat import androidx.core.content.getSystemService import io.nekohasekai.sagernet.Action import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.aidl.AppStatsList import io.nekohasekai.sagernet.aidl.ISagerNetServiceCallback import io.nekohasekai.sagernet.aidl.TrafficStats import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.ktx.app import io.nekohasekai.sagernet.ktx.getColorAttr import io.nekohasekai.sagernet.ui.SwitchActivity import io.nekohasekai.sagernet.utils.FormatFileSizeCompat import io.nekohasekai.sagernet.utils.Theme /** * User can customize visibility of notification since Android 8. * The default visibility: * * Android 8.x: always visible due to system limitations * VPN: always invisible because of VPN notification/icon * Other: always visible * * See also: https://github.com/aosp-mirror/platform_frameworks_base/commit/070d142993403cc2c42eca808ff3fafcee220ac4 */ class ServiceNotification( private val service: BaseService.Interface, profileName: String, channel: String, visible: Boolean = false, ) : BroadcastReceiver() { companion object { const val notificationId = 1 val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 } val trafficStatistics = DataStore.profileTrafficStatistics val showDirectSpeed = DataStore.showDirectSpeed private val callback: ISagerNetServiceCallback by lazy { object : ISagerNetServiceCallback.Stub() { override fun stateChanged(state: Int, profileName: String?, msg: String?) {} // ignore override fun trafficUpdated(profileId: Long, stats: TrafficStats, isCurrent: Boolean) { if (!trafficStatistics || profileId == 0L || !isCurrent) return builder.apply { if (showDirectSpeed) { val speedDetail = (service as Context).getString( R.string.speed_detail, service.getString( R.string.speed, FormatFileSizeCompat.formatFileSize(service, stats.txRateProxy, DataStore.useIECUnit) ), service.getString( R.string.speed, FormatFileSizeCompat.formatFileSize(service, stats.rxRateProxy, DataStore.useIECUnit) ), service.getString( R.string.speed, FormatFileSizeCompat.formatFileSize(service, stats.txRateDirect, DataStore.useIECUnit) ), service.getString( R.string.speed, FormatFileSizeCompat.formatFileSize(service, stats.rxRateDirect, DataStore.useIECUnit) ) ) setStyle(NotificationCompat.BigTextStyle().bigText(speedDetail)) setContentText(speedDetail) } else { val speedSimple = (service as Context).getString( R.string.traffic, service.getString( R.string.speed, FormatFileSizeCompat.formatFileSize(service, stats.txRateProxy, DataStore.useIECUnit) ), service.getString( R.string.speed, FormatFileSizeCompat.formatFileSize(service, stats.rxRateProxy, DataStore.useIECUnit) ) ) setContentText(speedSimple) } setSubText( service.getString( R.string.traffic, FormatFileSizeCompat.formatFileSize(service, stats.txTotal, DataStore.useIECUnit), FormatFileSizeCompat.formatFileSize(service, stats.rxTotal, DataStore.useIECUnit) ) ) } update() } override fun statsUpdated(statsList: AppStatsList?) { } override fun observatoryResultsUpdated(groupId: Long) { } override fun profilePersisted(profileId: Long) { } override fun missingPlugin(profileName: String?, pluginName: String?) { } override fun routeAlert(type: Int, routeName: String?) { } } } private var callbackRegistered = false private val builder = NotificationCompat.Builder(service as Context, channel) .setWhen(0) .setTicker(service.getString(R.string.forward_success)) .setContentTitle(profileName) .setOnlyAlertOnce(true) .setContentIntent(SagerNet.configureIntent(service)) .setSmallIcon(R.drawable.ic_service_active) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setPriority(if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN) init { service as Context updateActions() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // https://developer.android.com/design/ui/mobile/guides/home-screen/notifications // Starting in Android 12 (API level 31), the system derives the icon color from the // notification color you set in the app. If the app doesn't set the color, it uses // the system theme color. Previously, the color was gray. Theme.apply(app) Theme.apply(service) builder.color = service.getColorAttr(androidx.appcompat.R.attr.colorPrimary) } updateCallback(service.getSystemService()?.isInteractive != false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { service.registerReceiver(this, IntentFilter().apply { addAction(Intent.ACTION_SCREEN_ON) addAction(Intent.ACTION_SCREEN_OFF) addAction(Action.THEME_CHANGED) }, Context.RECEIVER_EXPORTED) } else { service.registerReceiver(this, IntentFilter().apply { addAction(Intent.ACTION_SCREEN_ON) addAction(Intent.ACTION_SCREEN_OFF) addAction(Action.THEME_CHANGED) }) } show() } fun updateActions() { service as Context builder.clearActions() val closeAction = NotificationCompat.Action.Builder( 0, service.getText(R.string.stop), PendingIntent.getBroadcast( service, 0, Intent(Action.CLOSE).setPackage(service.packageName), flags ) ).apply { setShowsUserInterface(false) }.build() builder.addAction(closeAction) val switchAction = NotificationCompat.Action.Builder( 0, service.getString(R.string.quick_toggle), PendingIntent.getActivity( service, 0, Intent(service, SwitchActivity::class.java), flags ) ).apply { setShowsUserInterface(false) }.build() builder.addAction(switchAction) } override fun onReceive(context: Context, intent: Intent) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && intent.action == Action.THEME_CHANGED) { service as Context Theme.apply(app) Theme.apply(service) builder.color = service.getColorAttr(androidx.appcompat.R.attr.colorPrimary) update() } if (service.data.state == BaseService.State.Connected) { updateCallback(intent.action == Intent.ACTION_SCREEN_ON) } } private fun updateCallback(screenOn: Boolean) { if (!trafficStatistics) return if (screenOn) { service.data.binder.registerCallback(callback) service.data.binder.startListeningForBandwidth( callback, DataStore.speedInterval.toLong() ) callbackRegistered = true } else if (callbackRegistered) { // unregister callback to save battery service.data.binder.unregisterCallback(callback) callbackRegistered = false } } private fun show() = (service as Service).startForeground(notificationId, builder.build()) private fun update() = NotificationManagerCompat.from(service as Service).notify(notificationId, builder.build()) fun destroy() { (service as Service).unregisterReceiver(this) updateCallback(false) ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE) } }