You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MagiskModuleManager/app/src/main/kotlin/com/fox2code/mmm/background/BackgroundUpdateChecker.kt

438 lines
22 KiB
Kotlin

/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.background
import android.Manifest
import android.annotation.SuppressLint
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.fox2code.mmm.AppUpdateManager
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainActivity
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.R
import com.fox2code.mmm.UpdateActivity
import com.fox2code.mmm.manager.ModuleManager
import com.fox2code.mmm.repo.RepoManager
import com.fox2code.mmm.utils.io.PropUtils
import timber.log.Timber
import java.util.Objects
import java.util.concurrent.TimeUnit
class BackgroundUpdateChecker(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
override fun doWork(): Result {
if (!NotificationManagerCompat.from(this.applicationContext)
.areNotificationsEnabled() || !MainApplication.isBackgroundUpdateCheckEnabled
) return Result.success()
synchronized(lock) { doCheck(this.applicationContext) }
return Result.success()
}
companion object {
private const val NOTIFICATION_CHANNEL_ID = "background_update"
private const val NOTIFICATION_ID = 1
private const val NOTFIICATION_GROUP = "updates"
private const val NOTIFICATION_CHANNEL_ID_APP = "background_update_app"
val lock =
Any() // Avoid concurrency issues
private const val NOTIFICATION_ID_ONGOING = 2
private const val NOTIFICATION_CHANNEL_ID_ONGOING = "mmm_background_update"
private const val NOTIFICATION_ID_APP = 3
@SuppressLint("RestrictedApi")
private fun postNotificationForAppUpdate(context: Context) {
// create the notification channel if not already created
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.createNotificationChannel(
NotificationChannelCompat.Builder(
NOTIFICATION_CHANNEL_ID_APP, NotificationManagerCompat.IMPORTANCE_HIGH
).setName(
context.getString(
R.string.notification_channel_category_app_update
)
).setDescription(
context.getString(
R.string.notification_channel_category_app_update_description
)
).setGroup(
NOTFIICATION_GROUP
).build()
)
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_APP)
builder.setSmallIcon(R.drawable.baseline_system_update_24)
builder.priority = NotificationCompat.PRIORITY_HIGH
builder.setCategory(NotificationCompat.CATEGORY_RECOMMENDATION)
builder.setShowWhen(false)
builder.setOnlyAlertOnce(true)
builder.setOngoing(false)
builder.setAutoCancel(true)
builder.setGroup(NOTFIICATION_GROUP)
// open app on click
val intent = Intent(context, UpdateActivity::class.java)
// set action to ACTIONS.DOWNLOAD
intent.action = UpdateActivity.ACTIONS.DOWNLOAD.toString()
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
builder.setContentIntent(
PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
)
// set summary to Found X
builder.setContentTitle(context.getString(R.string.notification_channel_background_update_app))
builder.setContentText(context.getString(R.string.notification_channel_background_update_app_description))
if (ContextCompat.checkSelfPermission(
MainApplication.INSTANCE!!.applicationContext,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
notificationManager.notify(NOTIFICATION_ID_APP, builder.build())
}
}
@Suppress("NAME_SHADOWING", "KotlinConstantConditions")
fun doCheck(context: Context) {
// first, check if the user has enabled background update checking
if (!MainApplication.getPreferences("mmm")!!
.getBoolean("pref_background_update_check", false)
) {
return
}
if (MainApplication.INSTANCE!!.isInForeground) {
// don't check if app is in foreground, this is a background check
return
}
// next, check if user requires wifi
if (MainApplication.getPreferences("mmm")!!
.getBoolean("pref_background_update_check_wifi", true)
) {
// check if wifi is connected
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkInfo = connectivityManager.activeNetwork
if (networkInfo == null || !connectivityManager.getNetworkCapabilities(
networkInfo
)?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)!!
) {
Timber.w("Background update check: wifi not connected but required")
return
}
}
synchronized(lock) {
// post checking notification if notifications are enabled
if (ContextCompat.checkSelfPermission(
MainApplication.INSTANCE!!.applicationContext,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.createNotificationChannel(
NotificationChannelCompat.Builder(
NOTIFICATION_CHANNEL_ID_ONGOING,
NotificationManagerCompat.IMPORTANCE_MIN
).setName(
context.getString(
R.string.notification_channel_category_background_update
)
).setDescription(
context.getString(
R.string.notification_channel_category_background_update_description
)
).setGroup(
NOTFIICATION_GROUP
).build()
)
val builder =
NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_ONGOING)
builder.setSmallIcon(R.drawable.ic_baseline_update_24)
builder.priority = NotificationCompat.PRIORITY_MIN
builder.setCategory(NotificationCompat.CATEGORY_SERVICE)
builder.setShowWhen(false)
builder.setOnlyAlertOnce(true)
builder.setOngoing(true)
builder.setAutoCancel(false)
builder.setGroup("update_bg")
builder.setContentTitle(context.getString(R.string.notification_channel_background_update))
builder.setContentText(context.getString(R.string.notification_channel_background_update_description))
notificationManager.notify(NOTIFICATION_ID_ONGOING, builder.build())
} else {
if (MainApplication.forceDebugLogging) Timber.d("Not posting notification because of missing permission")
}
ModuleManager.instance!!.scanAsync()
RepoManager.getINSTANCE()!!.update(null)
ModuleManager.instance!!.runAfterScan {
var moduleUpdateCount = 0
val repoModules = RepoManager.getINSTANCE()!!.modules
// hashmap of updateable modules names
val updateableModules = HashMap<String, String>()
for (localModuleInfo in ModuleManager.instance!!.modules.values) {
if ("twrp-keep" == localModuleInfo.id) continue
// exclude all modules with id's stored in the pref pref_background_update_check_excludes
try {
if (Objects.requireNonNull(
MainApplication.getPreferences("mmm")!!.getStringSet(
"pref_background_update_check_excludes",
HashSet()
)
).contains(localModuleInfo.id)
) continue
} catch (ignored: Exception) {
}
// now, we just had to make it more fucking complicated, didn't we?
// we now have pref_background_update_check_excludes_version, which is a id:version stringset of versions the user may want to "skip"
// oh, and because i hate myself, i made ^ at the beginning match that version and newer, and $ at the end match that version and older
val stringSet = MainApplication.getPreferences("mmm")!!.getStringSet(
"pref_background_update_check_excludes_version",
HashSet()
)
var version = ""
for (s in stringSet!!) {
if (s.startsWith(localModuleInfo.id)) {
version = s
if (MainApplication.forceDebugLogging) Timber.d("igV: %s", version)
break
}
}
val repoModule = repoModules[localModuleInfo.id]
localModuleInfo.checkModuleUpdate()
var remoteVersionCode = localModuleInfo.updateVersionCode.toString()
if (repoModule != null) {
remoteVersionCode = repoModule.moduleInfo.versionCode.toString()
}
if (version.isNotEmpty()) {
if (MainApplication.forceDebugLogging) Timber.d("igV found: %s", version)
val remoteVersionCodeInt = remoteVersionCode.toInt()
val wantsVersion =
version.split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()[1].replace("[^0-9]".toRegex(), "").toInt()
// now find out if user wants up to and including this version, or this version and newer
// if it starts with ^, it's this version and newer, if it ends with $, it's this version and older
version = version.split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()[1]
if (version.startsWith("^")) {
if (MainApplication.forceDebugLogging) Timber.d("igV: newer")
// the wantsversion and newer
if (remoteVersionCodeInt >= wantsVersion) {
if (MainApplication.forceDebugLogging) Timber.d("igV: skipping")
// if it is, we skip it
continue
}
} else if (version.endsWith("$")) {
if (MainApplication.forceDebugLogging) Timber.d("igV: older")
// this wantsversion and older
if (remoteVersionCodeInt <= wantsVersion) {
if (MainApplication.forceDebugLogging) Timber.d("igV: skipping")
// if it is, we skip it
continue
}
} else if (wantsVersion == remoteVersionCodeInt) {
if (MainApplication.forceDebugLogging) Timber.d("igV: equal")
// if it is, we skip it
continue
}
}
if (localModuleInfo.updateVersionCode > localModuleInfo.versionCode && !PropUtils.isNullString(
localModuleInfo.updateVersion
)
) {
moduleUpdateCount++
val version: String = localModuleInfo.version!!
val name: String = localModuleInfo.name!!
updateableModules[name] = version
} else if (repoModule != null && repoModule.moduleInfo.versionCode > localModuleInfo.versionCode && !PropUtils.isNullString(
repoModule.moduleInfo.version
)
) {
val version: String = localModuleInfo.version!!
val name: String = localModuleInfo.name!!
moduleUpdateCount++
updateableModules[name] = version
}
}
if (moduleUpdateCount != 0) {
if (MainApplication.forceDebugLogging) Timber.d("Found %d updates", moduleUpdateCount)
postNotification(context, updateableModules, moduleUpdateCount, false)
}
}
// check for app updates
if (MainApplication.getPreferences("mmm")!!
.getBoolean("pref_background_update_check_app", false)
) {
// don't check if app is from play store or fdroid
if (BuildConfig.FLAVOR != "play" || BuildConfig.FLAVOR != "fdroid") {
try {
val shouldUpdate = AppUpdateManager.appUpdateManager.checkUpdate(true)
if (shouldUpdate) {
if (MainApplication.forceDebugLogging) Timber.d("Found app update")
postNotificationForAppUpdate(context)
} else {
if (MainApplication.forceDebugLogging) Timber.d("No app update found")
}
} catch (e: Exception) {
Timber.e("Failed to check for app update")
}
}
}
// remove checking notification
if (ContextCompat.checkSelfPermission(
MainApplication.INSTANCE!!.applicationContext,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
if (MainApplication.forceDebugLogging) Timber.d("Removing notification")
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.cancel(NOTIFICATION_ID_ONGOING)
}
}
// increment or create counter in shared preferences
MainApplication.getPreferences("mmm")!!.edit().putInt(
"pref_background_update_counter",
MainApplication.getPreferences("mmm")!!
.getInt("pref_background_update_counter", 0) + 1
).apply()
}
fun postNotification(
context: Context,
updateable: HashMap<String, String>,
updateCount: Int,
test: Boolean
) {
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
builder.setSmallIcon(R.drawable.baseline_system_update_24)
builder.priority = NotificationCompat.PRIORITY_HIGH
builder.setCategory(NotificationCompat.CATEGORY_RECOMMENDATION)
builder.setShowWhen(false)
builder.setOnlyAlertOnce(true)
builder.setOngoing(false)
builder.setAutoCancel(true)
builder.setGroup(NOTFIICATION_GROUP)
// open app on click
val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
builder.setContentIntent(
PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
)
// set summary to Found X updates: <module name> <module version> <module name> <module version> ...
val summary = StringBuilder()
summary.append(context.getString(R.string.notification_update_summary))
// use notification_update_module_template string to set name and version
for ((key, value) in updateable) {
summary.append("\n").append(
context.getString(
R.string.notification_update_module_template,
key,
value
)
)
}
builder.setContentTitle(
context.getString(
R.string.notification_update_title,
updateCount
)
)
builder.setContentText(summary)
// set long text to summary so it doesn't get cut off
builder.setStyle(NotificationCompat.BigTextStyle().bigText(summary))
if (ContextCompat.checkSelfPermission(
MainApplication.INSTANCE!!.applicationContext,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
// check if app is in foreground. if so, don't show notification
if (MainApplication.INSTANCE!!.isInForeground && !test) return
NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build())
}
fun onMainActivityCreate(context: Context) {
// Refuse to run if first_launch pref is not false
if (MainApplication.getPreferences("mmm")!!
.getString("last_shown_setup", null) != "v6"
) return
// create notification channel group
val groupName: CharSequence = context.getString(R.string.notification_group_updates)
val mNotificationManager =
ContextCompat.getSystemService(context, NotificationManager::class.java)
mNotificationManager?.createNotificationChannelGroup(
NotificationChannelGroup(
NOTFIICATION_GROUP, groupName
)
)
val notificationManagerCompat = NotificationManagerCompat.from(context)
notificationManagerCompat.createNotificationChannel(
NotificationChannelCompat.Builder(
NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH
).setShowBadge(true).setName(
context.getString(
R.string.notification_update_pref
)
).setDescription(context.getString(R.string.auto_updates_notifs)).setGroup(
NOTFIICATION_GROUP
).build()
)
notificationManagerCompat.cancel(NOTIFICATION_ID)
// now for the ongoing notification
notificationManagerCompat.createNotificationChannel(
NotificationChannelCompat.Builder(
NOTIFICATION_CHANNEL_ID_ONGOING, NotificationManagerCompat.IMPORTANCE_MIN
).setShowBadge(true).setName(
context.getString(
R.string.notification_update_pref
)
).setDescription(context.getString(R.string.auto_updates_notifs)).setGroup(
NOTFIICATION_GROUP
).build()
)
notificationManagerCompat.cancel(NOTIFICATION_ID_ONGOING)
if (MainApplication.forceDebugLogging) Timber.d("Scheduling periodic background check")
// use pref_background_update_check_frequency to set frequency. value is in minutes
val frequency = MainApplication.getPreferences("mmm")!!
.getInt("pref_background_update_check_frequency", 60).toLong()
if (MainApplication.forceDebugLogging) Timber.d("Frequency: $frequency minutes")
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"background_checker",
ExistingPeriodicWorkPolicy.UPDATE,
PeriodicWorkRequest.Builder(
BackgroundUpdateChecker::class.java, frequency, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder().setRequiresBatteryNotLow(true).build()
).build()
)
}
fun onMainActivityResume(context: Context?) {
NotificationManagerCompat.from(context!!).cancel(NOTIFICATION_ID)
}
}
}