1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-27 22:22:45 +01:00

chore(mobile): update target SDK version ()

* chore(mobile): update target SDK version

* background service

* remove print statements

* remove extra line

* format kotlin

* Correct permission
This commit is contained in:
Alex 2024-08-15 11:36:43 -05:00 committed by GitHub
parent a4506758aa
commit 49610de4b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 626 additions and 570 deletions

View file

@ -46,7 +46,7 @@ android {
defaultConfig {
applicationId "app.alextran.immich"
minSdkVersion 26
targetSdkVersion 33
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View file

@ -1,9 +1,38 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<!-- Foreground service permission -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"
android:largeHeap="true">
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:directBootAware="false"
android:enabled="@bool/enable_system_foreground_service_default"
android:exported="false"
android:foregroundServiceType="dataSync|shortService" />
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
@ -51,23 +80,13 @@
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove"></provider>
tools:node="remove" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<queries>
<intent>

View file

@ -7,7 +7,6 @@ import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.security.MessageDigest
import java.io.File
import java.io.FileInputStream
import kotlinx.coroutines.*
@ -21,7 +20,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private var methodChannel: MethodChannel? = null
private var context: Context? = null
private val sha1: MessageDigest = MessageDigest.getInstance("SHA-1")
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
@ -50,36 +48,47 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args[0] as Long)
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args[1] as String)
.apply()
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
ContentObserverWorker.enable(ctx, immediate = args[2] as Boolean)
result.success(true)
}
"configure" -> {
val args = call.arguments<ArrayList<*>>()!!
val requireUnmeteredNetwork = args.get(0) as Boolean
val requireCharging = args.get(1) as Boolean
val triggerUpdateDelay = (args.get(2) as Number).toLong()
val triggerMaxDelay = (args.get(3) as Number).toLong()
ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging, triggerUpdateDelay, triggerMaxDelay)
val requireUnmeteredNetwork = args[0] as Boolean
val requireCharging = args[1] as Boolean
val triggerUpdateDelay = (args[2] as Number).toLong()
val triggerMaxDelay = (args[3] as Number).toLong()
ContentObserverWorker.configureWork(
ctx,
requireUnmeteredNetwork,
requireCharging,
triggerUpdateDelay,
triggerMaxDelay
)
result.success(true)
}
"disable" -> {
ContentObserverWorker.disable(ctx)
BackupWorker.stopWork(ctx)
result.success(true)
}
"isEnabled" -> {
result.success(ContentObserverWorker.isEnabled(ctx))
}
"isIgnoringBatteryOptimizations" -> {
result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
}
"digestFiles" -> {
val args = call.arguments<ArrayList<String>>()!!
GlobalScope.launch(Dispatchers.IO) {
val buf = ByteArray(BUFSIZE)
val buf = ByteArray(BUFFER_SIZE)
val digest: MessageDigest = MessageDigest.getInstance("SHA-1")
val hashes = arrayOfNulls<ByteArray>(args.size)
for (i in args.indices) {
@ -87,14 +96,12 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
var len = 0
try {
val file = FileInputStream(path)
try {
file.use { assetFile ->
while (true) {
len = file.read(buf)
if (len != BUFSIZE) break
len = assetFile.read(buf)
if (len != BUFFER_SIZE) break
digest.update(buf)
}
} finally {
file.close()
}
digest.update(buf, 0, len)
hashes[i] = digest.digest()
@ -106,10 +113,11 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result.success(hashes.asList())
}
}
else -> result.notImplemented()
}
}
}
private const val TAG = "BackgroundServicePlugin"
private const val BUFSIZE = 2*1024*1024;
private const val BUFFER_SIZE = 2 * 1024 * 1024;

View file

@ -4,6 +4,7 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
import android.os.Build
import android.os.Handler
import android.os.Looper
@ -40,12 +41,14 @@ import java.util.concurrent.TimeUnit
* Called by Android WorkManager when all constraints for the work are met,
* i.e. battery is not low and optionally Wifi and charging are active.
*/
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler {
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params),
MethodChannel.MethodCallHandler {
private val resolvableFuture = ResolvableFuture.create<Result>()
private var engine: FlutterEngine? = null
private lateinit var backgroundChannel: MethodChannel
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val notificationManager =
ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
private var timeBackupStarted: Long = 0L
private var notificationBuilder: NotificationCompat.Builder? = null
@ -61,10 +64,11 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
if (!flutterLoader.initialized()) {
flutterLoader.startInitialization(ctx)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create a Notification channel if necessary
// Create a Notification channel
createChannel()
}
Log.d(TAG, "isIgnoringBatteryOptimizations $isIgnoringBatteryOptimizations")
if (isIgnoringBatteryOptimizations) {
// normal background services can only up to 10 minutes
// foreground services are allowed to run indefinitely
@ -74,6 +78,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
showInfo(getInfoBuilder(title, indeterminate = true).build())
}
engine = FlutterEngine(ctx)
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
@ -89,8 +94,10 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
*/
private fun runDart() {
val callbackDispatcherHandle = applicationContext.getSharedPreferences(
SHARED_PREF_NAME, Context.MODE_PRIVATE).getLong(SHARED_PREF_CALLBACK_KEY, 0L)
val callbackInformation = FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle)
SHARED_PREF_NAME, Context.MODE_PRIVATE
).getLong(SHARED_PREF_CALLBACK_KEY, 0L)
val callbackInformation =
FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle)
val appBundlePath = flutterLoader.findAppBundlePath()
engine?.let { engine ->
@ -123,11 +130,10 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private fun waitOnSetForegroundAsync() {
val fgFuture = this.fgFuture
if (fgFuture != null && !fgFuture.isCancelled() && !fgFuture.isDone()) {
if (fgFuture != null && !fgFuture.isCancelled && !fgFuture.isDone) {
try {
fgFuture.get(500, TimeUnit.MILLISECONDS)
}
catch (e: Exception) {
} catch (e: Exception) {
// ignored, there is nothing to be done
}
}
@ -144,6 +150,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
waitOnSetForegroundAsync()
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) {
when (call.method) {
"initialized" -> {
@ -167,26 +174,32 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
}
)
}
"updateNotification" -> {
val args = call.arguments<ArrayList<*>>()!!
val title = args.get(0) as String?
val content = args.get(1) as String?
val progress = args.get(2) as Int
val max = args.get(3) as Int
val indeterminate = args.get(4) as Boolean
val isDetail = args.get(5) as Boolean
val onlyIfFG = args.get(6) as Boolean
val title = args[0] as String?
val content = args[1] as String?
val progress = args[2] as Int
val max = args[3] as Int
val indeterminate = args[4] as Boolean
val isDetail = args[5] as Boolean
val onlyIfFG = args[6] as Boolean
if (!onlyIfFG || isIgnoringBatteryOptimizations) {
showInfo(getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), isDetail)
showInfo(
getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(),
isDetail
)
}
}
"showError" -> {
val args = call.arguments<ArrayList<*>>()!!
val title = args.get(0) as String
val content = args.get(1) as String?
val individualTag = args.get(2) as String?
val title = args[0] as String
val content = args[1] as String?
val individualTag = args[2] as String?
showError(title, content, individualTag)
}
"clearErrorNotifications" -> clearErrorNotifications()
"hasContentChanged" -> {
val lastChange = applicationContext
@ -196,6 +209,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
timeBackupStarted = SystemClock.uptimeMillis()
r.success(hasContentChanged)
}
else -> r.notImplemented()
}
}
@ -221,8 +235,13 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private fun showInfo(notification: Notification, isDetail: Boolean = false) {
val id = if (isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID
if (isIgnoringBatteryOptimizations && !isDetail) {
fgFuture = setForegroundAsync(ForegroundInfo(id, notification))
fgFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE))
} else {
setForegroundAsync(ForegroundInfo(id, notification))
}
} else {
notificationManager.notify(id, notification)
}
@ -257,11 +276,18 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
return builder.setProgress(max, progress, indeterminate)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannel() {
val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
val foreground = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_ID,
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(foreground)
val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_HIGH)
val error = NotificationChannel(
NOTIFICATION_CHANNEL_ERROR_ID,
NOTIFICATION_CHANNEL_ERROR_ID,
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(error)
}
@ -283,21 +309,26 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
/**
* Enqueues the BackupWorker to run once the constraints are met
*/
fun enqueueBackupWorker(context: Context,
fun enqueueBackupWorker(
context: Context,
requireWifi: Boolean = false,
requireCharging: Boolean = false,
delayMilliseconds: Long = 0L) {
delayMilliseconds: Long = 0L
) {
val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds)
WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest)
WorkManager.getInstance(context)
.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest)
Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued")
}
/**
* Updates the constraints of an already enqueued BackupWorker
*/
fun updateBackupWorker(context: Context,
fun updateBackupWorker(
context: Context,
requireWifi: Boolean = false,
requireCharging: Boolean = false) {
requireCharging: Boolean = false
) {
try {
val wm = WorkManager.getInstance(context)
val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP)
@ -314,7 +345,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
}
Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued")
} catch (e: Exception) {
Log.d(TAG, "updateBackupWorker failed: ${e}")
Log.d(TAG, "updateBackupWorker failed: $e")
}
}
@ -330,17 +361,15 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
* Returns `true` if the app is ignoring battery optimizations
*/
fun isIgnoringBatteryOptimizations(ctx: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val pwrm = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
val name = ctx.packageName
return pwrm.isIgnoringBatteryOptimizations(name)
}
return true
val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(ctx.packageName)
}
private fun buildWorkRequest(requireWifi: Boolean = false,
private fun buildWorkRequest(
requireWifi: Boolean = false,
requireCharging: Boolean = false,
delayMilliseconds: Long = 0L): OneTimeWorkRequest {
delayMilliseconds: Long = 0L
): OneTimeWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)

View file

@ -26,7 +26,7 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
if (!isEnabled(applicationContext)) {
return Result.failure()
}
if (getTriggeredContentUris().size > 0) {
if (triggeredContentUris.size > 0) {
startBackupWorker(applicationContext, delayMilliseconds = 0)
}
enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE)
@ -35,10 +35,10 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
companion object {
const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled"
const val SHARED_PREF_REQUIRE_WIFI = "requireWifi"
const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging"
const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay"
const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay"
private const val SHARED_PREF_REQUIRE_WIFI = "requireWifi"
private const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging"
private const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay"
private const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay"
private const val TASK_NAME_OBSERVER = "immich/ContentObserver"
@ -106,7 +106,7 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
WorkManager
.getInstance(context)
.enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work)
.getResult()
.result
.get()
Log.d(TAG, "workManagerAppClearedWorkaround")
}