android 14: use work manager instead of foreground service as datasync
This commit is contained in:
parent
c94fc7245e
commit
01d2e21369
41 changed files with 214 additions and 377 deletions
|
@ -204,6 +204,7 @@ dependencies {
|
|||
implementation 'androidx.media:media:1.6.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'com.commonsware.cwac:document:0.5.0'
|
||||
|
|
|
@ -40,11 +40,6 @@ This change eventually prevents building the app with Flutter v3.7.11.
|
|||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<!-- to analyze media in a service -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!--
|
||||
TODO TLAD [Android 14 (API 34)] request FOREGROUND_SERVICE_DATA_SYNC permission
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
cf https://developer.android.com/about/versions/14/changes/fgs-types-required
|
||||
-->
|
||||
<!-- to fetch map tiles -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- from Android 12 (API 31), users can optionally grant access to the media management special permission -->
|
||||
|
@ -251,12 +246,6 @@ This change eventually prevents building the app with Flutter v3.7.11.
|
|||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".AnalysisService"
|
||||
android:description="@string/analysis_service_description"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".ScreenSaverService"
|
||||
android:exported="true"
|
||||
|
|
|
@ -1,258 +0,0 @@
|
|||
package deckers.thibault.aves
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import app.loup.streams_channel.StreamsChannel
|
||||
import deckers.thibault.aves.MainActivity.Companion.OPEN_FROM_ANALYSIS_SERVICE
|
||||
import deckers.thibault.aves.channel.calls.*
|
||||
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
||||
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class AnalysisService : Service() {
|
||||
private var flutterEngine: FlutterEngine? = null
|
||||
private var backgroundChannel: MethodChannel? = null
|
||||
private var serviceLooper: Looper? = null
|
||||
private var serviceHandler: ServiceHandler? = null
|
||||
private val analysisServiceBinder = AnalysisServiceBinder()
|
||||
|
||||
override fun onCreate() {
|
||||
Log.i(LOG_TAG, "Create analysis service")
|
||||
runBlocking {
|
||||
FlutterUtils.initFlutterEngine(this@AnalysisService, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
|
||||
flutterEngine = it
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
initChannels(this)
|
||||
|
||||
HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||
start()
|
||||
serviceLooper = looper
|
||||
serviceHandler = ServiceHandler(looper)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "failed to initialize service", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.i(LOG_TAG, "Destroy analysis service")
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = analysisServiceBinder
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ANALYSIS, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getText(R.string.analysis_channel_name))
|
||||
.setShowBadge(false)
|
||||
.build()
|
||||
NotificationManagerCompat.from(this).createNotificationChannel(channel)
|
||||
startForeground(NOTIFICATION_ID, buildNotification())
|
||||
|
||||
val msgData = Bundle()
|
||||
intent?.extras?.let {
|
||||
msgData.putAll(it)
|
||||
}
|
||||
serviceHandler?.obtainMessage()?.let { msg ->
|
||||
msg.arg1 = startId
|
||||
msg.data = msgData
|
||||
serviceHandler?.sendMessage(msg)
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun detachAndStop() {
|
||||
analysisServiceBinder.detach()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun initChannels(context: Context) {
|
||||
val engine = flutterEngine
|
||||
engine ?: throw Exception("Flutter engine is not initialized")
|
||||
|
||||
val messenger = engine.dartExecutor
|
||||
|
||||
// channels for analysis
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context))
|
||||
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context))
|
||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(context))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) }
|
||||
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) }
|
||||
|
||||
// channel for service management
|
||||
backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
|
||||
setMethodCallHandler { call, result -> onMethodCall(call, result) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"initialized" -> {
|
||||
Log.d(LOG_TAG, "background channel is ready")
|
||||
result.success(null)
|
||||
}
|
||||
"updateNotification" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val message = call.argument<String>("message")
|
||||
val notification = buildNotification(title, message)
|
||||
NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, notification)
|
||||
result.success(null)
|
||||
}
|
||||
"refreshApp" -> {
|
||||
analysisServiceBinder.refreshApp()
|
||||
result.success(null)
|
||||
}
|
||||
"stop" -> {
|
||||
detachAndStop()
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(title: String? = null, message: String? = null): Notification {
|
||||
val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
val stopServiceIntent = Intent(this, AnalysisService::class.java).let {
|
||||
it.putExtra(KEY_COMMAND, COMMAND_STOP)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PendingIntent.getForegroundService(this, STOP_SERVICE_REQUEST, it, pendingIntentFlags)
|
||||
} else {
|
||||
PendingIntent.getService(this, STOP_SERVICE_REQUEST, it, pendingIntentFlags)
|
||||
}
|
||||
}
|
||||
val openAppIntent = Intent(this, MainActivity::class.java).let {
|
||||
PendingIntent.getActivity(this, OPEN_FROM_ANALYSIS_SERVICE, it, pendingIntentFlags)
|
||||
}
|
||||
val stopAction = NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_outline_stop_24,
|
||||
getString(R.string.analysis_notification_action_stop),
|
||||
stopServiceIntent
|
||||
).build()
|
||||
val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) R.drawable.ic_notification else R.mipmap.ic_launcher_round
|
||||
return NotificationCompat.Builder(this, CHANNEL_ANALYSIS)
|
||||
.setContentTitle(title ?: getText(R.string.analysis_notification_default_title))
|
||||
.setContentText(message)
|
||||
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
|
||||
.setSmallIcon(icon)
|
||||
.setContentIntent(openAppIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.addAction(stopAction)
|
||||
.build()
|
||||
}
|
||||
|
||||
private inner class ServiceHandler(looper: Looper) : Handler(looper) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
val data = msg.data
|
||||
when (data.getString(KEY_COMMAND)) {
|
||||
COMMAND_START -> {
|
||||
runBlocking {
|
||||
FlutterUtils.runOnUiThread {
|
||||
val entryIds = data.getIntArray(KEY_ENTRY_IDS)?.toList()
|
||||
backgroundChannel?.invokeMethod(
|
||||
"start", hashMapOf(
|
||||
"entryIds" to entryIds,
|
||||
"force" to data.getBoolean(KEY_FORCE),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
COMMAND_STOP -> {
|
||||
// unconditionally stop the service
|
||||
runBlocking {
|
||||
FlutterUtils.runOnUiThread {
|
||||
backgroundChannel?.invokeMethod("stop", null)
|
||||
}
|
||||
}
|
||||
detachAndStop()
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<AnalysisService>()
|
||||
private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background"
|
||||
const val SHARED_PREFERENCES_KEY = "analysis_service"
|
||||
const val CALLBACK_HANDLE_KEY = "callback_handle"
|
||||
|
||||
const val NOTIFICATION_ID = 1
|
||||
const val STOP_SERVICE_REQUEST = 1
|
||||
const val CHANNEL_ANALYSIS = "analysis"
|
||||
|
||||
const val KEY_COMMAND = "command"
|
||||
const val COMMAND_START = "start"
|
||||
const val COMMAND_STOP = "stop"
|
||||
const val KEY_ENTRY_IDS = "entry_ids"
|
||||
const val KEY_FORCE = "force"
|
||||
}
|
||||
}
|
||||
|
||||
class AnalysisServiceBinder : Binder() {
|
||||
private val listeners = hashSetOf<AnalysisServiceListener>()
|
||||
|
||||
fun startListening(listener: AnalysisServiceListener) = listeners.add(listener)
|
||||
|
||||
fun stopListening(listener: AnalysisServiceListener) = listeners.remove(listener)
|
||||
|
||||
fun refreshApp() {
|
||||
val localListeners = listeners.toSet()
|
||||
for (listener in localListeners) {
|
||||
try {
|
||||
listener.refreshApp()
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "failed to notify listener=$listener", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
val localListeners = listeners.toSet()
|
||||
for (listener in localListeners) {
|
||||
try {
|
||||
listener.detachFromActivity()
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "failed to detach listener=$listener", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<AnalysisServiceBinder>()
|
||||
}
|
||||
}
|
||||
|
||||
interface AnalysisServiceListener {
|
||||
fun refreshApp()
|
||||
fun detachFromActivity()
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
package deckers.thibault.aves
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import app.loup.streams_channel.StreamsChannel
|
||||
import deckers.thibault.aves.channel.calls.DeviceHandler
|
||||
import deckers.thibault.aves.channel.calls.GeocodingHandler
|
||||
import deckers.thibault.aves.channel.calls.MediaStoreHandler
|
||||
import deckers.thibault.aves.channel.calls.MetadataFetchHandler
|
||||
import deckers.thibault.aves.channel.calls.StorageHandler
|
||||
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
||||
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class AnalysisWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) {
|
||||
private var workCont: Continuation<Any?>? = null
|
||||
private var flutterEngine: FlutterEngine? = null
|
||||
private var backgroundChannel: MethodChannel? = null
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
createNotificationChannel()
|
||||
setForeground(createForegroundInfo())
|
||||
suspendCoroutine { cont ->
|
||||
workCont = cont
|
||||
onStart()
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun onStart() {
|
||||
Log.i(LOG_TAG, "Start analysis worker")
|
||||
runBlocking {
|
||||
FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
|
||||
flutterEngine = it
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
initChannels(applicationContext)
|
||||
|
||||
runBlocking {
|
||||
FlutterUtils.runOnUiThread {
|
||||
backgroundChannel?.invokeMethod(
|
||||
"start", hashMapOf(
|
||||
"entryIds" to inputData.getIntArray(KEY_ENTRY_IDS)?.toList(),
|
||||
"force" to inputData.getBoolean(KEY_FORCE, false),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "failed to initialize worker", e)
|
||||
workCont?.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initChannels(context: Context) {
|
||||
val engine = flutterEngine
|
||||
engine ?: throw Exception("Flutter engine is not initialized")
|
||||
|
||||
val messenger = engine.dartExecutor
|
||||
|
||||
// channels for analysis
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context))
|
||||
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context))
|
||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(context))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) }
|
||||
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) }
|
||||
|
||||
// channel for service management
|
||||
backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
|
||||
setMethodCallHandler { call, result -> onMethodCall(call, result) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"initialized" -> {
|
||||
Log.d(LOG_TAG, "Analysis background channel is ready")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
"updateNotification" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val message = call.argument<String>("message")
|
||||
setForegroundAsync(createForegroundInfo(title, message))
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
"stop" -> {
|
||||
workCont?.resume(null)
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
val channel = NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(applicationContext.getText(R.string.analysis_channel_name))
|
||||
.setShowBadge(false)
|
||||
.build()
|
||||
NotificationManagerCompat.from(applicationContext).createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun createForegroundInfo(title: String? = null, message: String? = null): ForegroundInfo {
|
||||
val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
val openAppIntent = Intent(applicationContext, MainActivity::class.java).let {
|
||||
PendingIntent.getActivity(applicationContext, MainActivity.OPEN_FROM_ANALYSIS_SERVICE, it, pendingIntentFlags)
|
||||
}
|
||||
val stopAction = NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_outline_stop_24,
|
||||
applicationContext.getString(R.string.analysis_notification_action_stop),
|
||||
WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)
|
||||
).build()
|
||||
val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) R.drawable.ic_notification else R.mipmap.ic_launcher_round
|
||||
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL)
|
||||
.setContentTitle(title ?: applicationContext.getText(R.string.analysis_notification_default_title))
|
||||
.setContentText(message)
|
||||
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
|
||||
.setSmallIcon(icon)
|
||||
.setContentIntent(openAppIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.addAction(stopAction)
|
||||
.build()
|
||||
return ForegroundInfo(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<AnalysisWorker>()
|
||||
private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background"
|
||||
const val SHARED_PREFERENCES_KEY = "analysis_service"
|
||||
const val CALLBACK_HANDLE_KEY = "callback_handle"
|
||||
|
||||
const val NOTIFICATION_CHANNEL = "analysis"
|
||||
const val NOTIFICATION_ID = 1
|
||||
|
||||
const val KEY_ENTRY_IDS = "entry_ids"
|
||||
const val KEY_FORCE = "force"
|
||||
}
|
||||
}
|
|
@ -170,7 +170,6 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
|
||||
override fun onStop() {
|
||||
Log.i(LOG_TAG, "onStop")
|
||||
analysisHandler.detachFromActivity()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +1,30 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import deckers.thibault.aves.AnalysisService
|
||||
import deckers.thibault.aves.AnalysisServiceBinder
|
||||
import deckers.thibault.aves.AnalysisServiceListener
|
||||
import deckers.thibault.aves.utils.ContextUtils.isMyServiceRunning
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import androidx.core.app.ComponentActivity
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import deckers.thibault.aves.AnalysisWorker
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class AnalysisHandler(private val activity: Activity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler, AnalysisServiceListener {
|
||||
|
||||
class AnalysisHandler(private val activity: ComponentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"registerCallback" -> ioScope.launch { Coresult.safe(call, result, ::registerCallback) }
|
||||
"startService" -> Coresult.safe(call, result, ::startAnalysis)
|
||||
"startAnalysis" -> Coresult.safe(call, result, ::startAnalysis)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
@ -38,9 +36,9 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
|
|||
return
|
||||
}
|
||||
|
||||
activity.getSharedPreferences(AnalysisService.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||
activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putLong(AnalysisService.CALLBACK_HANDLE_KEY, callbackHandle)
|
||||
.putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle)
|
||||
.apply()
|
||||
result.success(true)
|
||||
}
|
||||
|
@ -55,20 +53,18 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
|
|||
// can be null or empty
|
||||
val entryIds = call.argument<List<Int>>("entryIds")
|
||||
|
||||
if (!activity.isMyServiceRunning(AnalysisService::class.java)) {
|
||||
val intent = Intent(activity, AnalysisService::class.java)
|
||||
.putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START)
|
||||
.putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray())
|
||||
.putExtra(AnalysisService.KEY_FORCE, force)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Foreground services cannot start from background, but the service here may start fine
|
||||
// while the current lifecycle state (via `ProcessLifecycleOwner.get().lifecycle.currentState`)
|
||||
// is only `INITIALIZED`, so we should not preemptively return when the state is below `STARTED`.
|
||||
activity.startForegroundService(intent)
|
||||
} else {
|
||||
activity.startService(intent)
|
||||
}
|
||||
}
|
||||
WorkManager.getInstance(activity).enqueueUniqueWork(
|
||||
ANALYSIS_WORK_NAME,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
OneTimeWorkRequestBuilder<AnalysisWorker>().apply {
|
||||
setInputData(
|
||||
workDataOf(
|
||||
AnalysisWorker.KEY_ENTRY_IDS to entryIds?.toIntArray(),
|
||||
AnalysisWorker.KEY_FORCE to force,
|
||||
)
|
||||
)
|
||||
}.build()
|
||||
)
|
||||
attachToActivity()
|
||||
result.success(null)
|
||||
}
|
||||
|
@ -76,44 +72,22 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
|
|||
private var attached = false
|
||||
|
||||
fun attachToActivity() {
|
||||
if (activity.isMyServiceRunning(AnalysisService::class.java)) {
|
||||
val intent = Intent(activity, AnalysisService::class.java)
|
||||
activity.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
if (!attached) {
|
||||
attached = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun detachFromActivity() {
|
||||
if (attached) {
|
||||
attached = false
|
||||
activity.unbindService(connection)
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshApp() {
|
||||
if (attached) {
|
||||
onAnalysisCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
var binder: AnalysisServiceBinder? = null
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
Log.i(LOG_TAG, "Analysis service connected")
|
||||
binder = service as AnalysisServiceBinder
|
||||
binder?.startListening(this@AnalysisHandler)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
Log.i(LOG_TAG, "Analysis service disconnected")
|
||||
binder?.stopListening(this@AnalysisHandler)
|
||||
binder = null
|
||||
WorkManager.getInstance(activity).getWorkInfosForUniqueWorkLiveData(ANALYSIS_WORK_NAME).observe(activity) { list ->
|
||||
if (list.any { it.state == WorkInfo.State.SUCCEEDED }) {
|
||||
runBlocking {
|
||||
FlutterUtils.runOnUiThread {
|
||||
onAnalysisCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<AnalysisHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/analysis"
|
||||
private const val ANALYSIS_WORK_NAME = "analysis_work"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
<string name="search_shortcut_short_label">البحث</string>
|
||||
<string name="videos_shortcut_short_label">الفيديوهات</string>
|
||||
<string name="analysis_channel_name">فحص الوسائط</string>
|
||||
<string name="analysis_service_description">فحص الصور والفيديوهات</string>
|
||||
<string name="analysis_notification_default_title">يتم فحص الوسائط</string>
|
||||
<string name="analysis_notification_action_stop">إيقاف</string>
|
||||
</resources>
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">گەڕان</string>
|
||||
<string name="videos_shortcut_short_label">ڤیدیۆ</string>
|
||||
<string name="analysis_channel_name">گەڕان بۆ فایل</string>
|
||||
<string name="analysis_service_description">گەڕان بۆ وێنە و ڤیدیۆ</string>
|
||||
<string name="analysis_notification_default_title">گەڕان بۆ فایلەکان</string>
|
||||
<string name="analysis_notification_action_stop">وەستاندن</string>
|
||||
</resources>
|
|
@ -5,7 +5,6 @@
|
|||
<string name="search_shortcut_short_label">Hledat</string>
|
||||
<string name="videos_shortcut_short_label">Videa</string>
|
||||
<string name="analysis_channel_name">Prohledat média</string>
|
||||
<string name="analysis_service_description">Prohledat obrázky a videa</string>
|
||||
<string name="analysis_notification_default_title">Prohledávání médií</string>
|
||||
<string name="analysis_notification_action_stop">Zastavit</string>
|
||||
<string name="app_widget_label">Fotorámeček</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Suche</string>
|
||||
<string name="videos_shortcut_short_label">Videos</string>
|
||||
<string name="analysis_channel_name">Analyse von Medien</string>
|
||||
<string name="analysis_service_description">Bilder & Videos scannen</string>
|
||||
<string name="analysis_notification_default_title">Medien scannen</string>
|
||||
<string name="analysis_notification_action_stop">Abbrechen</string>
|
||||
<string name="safe_mode_shortcut_short_label">Sicherer Modus</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Αναζήτηση</string>
|
||||
<string name="videos_shortcut_short_label">Βίντεο</string>
|
||||
<string name="analysis_channel_name">Σάρωση πολυμέσων</string>
|
||||
<string name="analysis_service_description">Σάρωση εικόνων & Βίντεο</string>
|
||||
<string name="analysis_notification_default_title">Σάρωση στοιχείων</string>
|
||||
<string name="analysis_notification_action_stop">Διακοπή</string>
|
||||
</resources>
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Búsqueda</string>
|
||||
<string name="videos_shortcut_short_label">Vídeos</string>
|
||||
<string name="analysis_channel_name">Explorar medios</string>
|
||||
<string name="analysis_service_description">Explorar imágenes & videos</string>
|
||||
<string name="analysis_notification_default_title">Explorando medios</string>
|
||||
<string name="analysis_notification_action_stop">Anular</string>
|
||||
<string name="safe_mode_shortcut_short_label">Modo seguro</string>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
<string name="search_shortcut_short_label">Bilatu</string>
|
||||
<string name="videos_shortcut_short_label">Bideoak</string>
|
||||
<string name="app_widget_label">Argazki-markoa</string>
|
||||
<string name="analysis_service_description">Irudiak eta bideoak eskaneatu</string>
|
||||
<string name="wallpaper">Horma-papera</string>
|
||||
<string name="analysis_channel_name">Media eskaneatu</string>
|
||||
<string name="analysis_notification_action_stop">Gelditu</string>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
<resources>
|
||||
<string name="videos_shortcut_short_label">ویدئو ها</string>
|
||||
<string name="analysis_channel_name">کنکاش رسانه</string>
|
||||
<string name="analysis_service_description">کنکاش تصاویر و ویدئو ها</string>
|
||||
<string name="search_shortcut_short_label">جستجو</string>
|
||||
<string name="wallpaper">کاغذدیواری</string>
|
||||
<string name="analysis_notification_default_title">در حال کنکاش رسانهها</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Recherche</string>
|
||||
<string name="videos_shortcut_short_label">Vidéos</string>
|
||||
<string name="analysis_channel_name">Analyse des images</string>
|
||||
<string name="analysis_service_description">Analyse des images & vidéos</string>
|
||||
<string name="analysis_notification_default_title">Analyse des images</string>
|
||||
<string name="analysis_notification_action_stop">Annuler</string>
|
||||
<string name="safe_mode_shortcut_short_label">Mode sans échec</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Procura</string>
|
||||
<string name="videos_shortcut_short_label">Vídeos</string>
|
||||
<string name="analysis_channel_name">Escaneo multimedia</string>
|
||||
<string name="analysis_service_description">Escanealas imaxes e os vídeos</string>
|
||||
<string name="analysis_notification_default_title">Escaneando medios</string>
|
||||
<string name="analysis_notification_action_stop">Pare</string>
|
||||
</resources>
|
|
@ -8,5 +8,4 @@
|
|||
<string name="analysis_channel_name">मीडिया जाँचे</string>
|
||||
<string name="app_name">ऐवीज</string>
|
||||
<string name="videos_shortcut_short_label">वीडियो</string>
|
||||
<string name="analysis_service_description">छवि & वीडियो जाँचे</string>
|
||||
</resources>
|
|
@ -8,6 +8,5 @@
|
|||
<string name="app_widget_label">Fotó keret</string>
|
||||
<string name="safe_mode_shortcut_short_label">Biztonsági üzemmód</string>
|
||||
<string name="analysis_channel_name">Tartalom keresése</string>
|
||||
<string name="analysis_service_description">Képek és videók keresése</string>
|
||||
<string name="analysis_notification_default_title">Média beolvasása</string>
|
||||
</resources>
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Cari</string>
|
||||
<string name="videos_shortcut_short_label">Video</string>
|
||||
<string name="analysis_channel_name">Pindai media</string>
|
||||
<string name="analysis_service_description">Pindai gambar & video</string>
|
||||
<string name="analysis_notification_default_title">Memindai media</string>
|
||||
<string name="analysis_notification_action_stop">Berhenti</string>
|
||||
<string name="safe_mode_shortcut_short_label">Mode aman</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Ricerca</string>
|
||||
<string name="videos_shortcut_short_label">Video</string>
|
||||
<string name="analysis_channel_name">Scansione media</string>
|
||||
<string name="analysis_service_description">Scansione immagini & videos</string>
|
||||
<string name="analysis_notification_default_title">Scansione in corso</string>
|
||||
<string name="analysis_notification_action_stop">Annulla</string>
|
||||
<string name="safe_mode_shortcut_short_label">Modalità provvisoria</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">חיפוש</string>
|
||||
<string name="videos_shortcut_short_label">סרטים</string>
|
||||
<string name="analysis_channel_name">סריקת מדיה</string>
|
||||
<string name="analysis_service_description">סרוק תמונות וסרטים</string>
|
||||
<string name="analysis_notification_default_title">סורק מדיה</string>
|
||||
<string name="analysis_notification_action_stop">הפסק</string>
|
||||
</resources>
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">検索</string>
|
||||
<string name="videos_shortcut_short_label">動画</string>
|
||||
<string name="analysis_channel_name">メディアスキャン</string>
|
||||
<string name="analysis_service_description">画像と動画をスキャン</string>
|
||||
<string name="analysis_notification_default_title">メディアをスキャン中</string>
|
||||
<string name="analysis_notification_action_stop">停止</string>
|
||||
<string name="safe_mode_shortcut_short_label">セーフモード</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">검색</string>
|
||||
<string name="videos_shortcut_short_label">동영상</string>
|
||||
<string name="analysis_channel_name">미디어 분석</string>
|
||||
<string name="analysis_service_description">사진과 동영상 분석</string>
|
||||
<string name="analysis_notification_default_title">미디어 분석</string>
|
||||
<string name="analysis_notification_action_stop">취소</string>
|
||||
<string name="safe_mode_shortcut_short_label">안전 모드</string>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="analysis_service_description">Nuskaityti paveikslėlius ir vaizdo įrašus</string>
|
||||
<string name="wallpaper">Ekrano paveikslėlis</string>
|
||||
<string name="videos_shortcut_short_label">Vaizdo įrašai</string>
|
||||
<string name="app_name">Aves</string>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
<string name="app_name">Aves</string>
|
||||
<string name="videos_shortcut_short_label">Videoer</string>
|
||||
<string name="analysis_channel_name">Mediaskanning</string>
|
||||
<string name="analysis_service_description">Skann bilder og videoer</string>
|
||||
<string name="analysis_notification_default_title">Skanning av media</string>
|
||||
<string name="app_widget_label">Bilderamme</string>
|
||||
<string name="wallpaper">Bakgrunnsbilde</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Zoeken</string>
|
||||
<string name="videos_shortcut_short_label">Video’s</string>
|
||||
<string name="analysis_channel_name">Media indexeren</string>
|
||||
<string name="analysis_service_description">Indexeren van afdbeeldingen & video’s</string>
|
||||
<string name="analysis_notification_default_title">Indexeren van media</string>
|
||||
<string name="analysis_notification_action_stop">Stop</string>
|
||||
</resources>
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Søk</string>
|
||||
<string name="videos_shortcut_short_label">Videoar</string>
|
||||
<string name="analysis_channel_name">Mediasøking</string>
|
||||
<string name="analysis_service_description">Søk igjennom bilete og videoar</string>
|
||||
<string name="analysis_notification_default_title">Søkjer igjennom media</string>
|
||||
<string name="analysis_notification_action_stop">Stogg</string>
|
||||
</resources>
|
|
@ -4,7 +4,6 @@
|
|||
<string name="search_shortcut_short_label">Szukaj</string>
|
||||
<string name="videos_shortcut_short_label">Wideo</string>
|
||||
<string name="analysis_channel_name">Przeskanuj multimedia</string>
|
||||
<string name="analysis_service_description">Przeskanuj obrazy oraz wideo</string>
|
||||
<string name="analysis_notification_default_title">Skanowanie multimediów</string>
|
||||
<string name="analysis_notification_action_stop">Zatrzymaj</string>
|
||||
<string name="app_name">Aves</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Procurar</string>
|
||||
<string name="videos_shortcut_short_label">Vídeos</string>
|
||||
<string name="analysis_channel_name">Digitalização de mídia</string>
|
||||
<string name="analysis_service_description">Digitalizar imagens & vídeos</string>
|
||||
<string name="analysis_notification_default_title">Digitalizando mídia</string>
|
||||
<string name="analysis_notification_action_stop">Pare</string>
|
||||
</resources>
|
|
@ -5,7 +5,6 @@
|
|||
<string name="wallpaper">Tapet</string>
|
||||
<string name="videos_shortcut_short_label">Videoclipuri</string>
|
||||
<string name="analysis_channel_name">Scanare media</string>
|
||||
<string name="analysis_service_description">Scanați imagini și videoclipuri</string>
|
||||
<string name="analysis_notification_default_title">Scanarea suporturilor</string>
|
||||
<string name="analysis_notification_action_stop">Stop</string>
|
||||
<string name="search_shortcut_short_label">Căutare</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Поиск</string>
|
||||
<string name="videos_shortcut_short_label">Видео</string>
|
||||
<string name="analysis_channel_name">Сканировать медия</string>
|
||||
<string name="analysis_service_description">Сканировать изображения и видео</string>
|
||||
<string name="analysis_notification_default_title">Сканирование медиа</string>
|
||||
<string name="analysis_notification_action_stop">Стоп</string>
|
||||
<string name="safe_mode_shortcut_short_label">Безопасный режим</string>
|
||||
|
|
|
@ -7,6 +7,5 @@
|
|||
<string name="videos_shortcut_short_label">Videá</string>
|
||||
<string name="analysis_notification_action_stop">Zastaviť</string>
|
||||
<string name="analysis_channel_name">Skenovanie médií</string>
|
||||
<string name="analysis_service_description">Skenovanie obrázkov & videí</string>
|
||||
<string name="analysis_notification_default_title">Skenovanie média</string>
|
||||
</resources>
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">ค้นหา</string>
|
||||
<string name="videos_shortcut_short_label">วิดีโอ</string>
|
||||
<string name="analysis_channel_name">สแกนสื่อบันเทิง</string>
|
||||
<string name="analysis_service_description">สแกนรูปภาพและวิดีโอ</string>
|
||||
<string name="analysis_notification_default_title">กำลังสแกนสื่อบันเทิง</string>
|
||||
<string name="analysis_notification_action_stop">หยุด</string>
|
||||
</resources>
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">Arama</string>
|
||||
<string name="videos_shortcut_short_label">Videolar</string>
|
||||
<string name="analysis_channel_name">Medya tarama</string>
|
||||
<string name="analysis_service_description">Görüntüleri ve videoları tarayın</string>
|
||||
<string name="analysis_notification_default_title">Medya taranıyor</string>
|
||||
<string name="analysis_notification_action_stop">Durdur</string>
|
||||
</resources>
|
|
@ -5,7 +5,6 @@
|
|||
<string name="search_shortcut_short_label">Пошук</string>
|
||||
<string name="videos_shortcut_short_label">Відео</string>
|
||||
<string name="analysis_channel_name">Сканувати медіа</string>
|
||||
<string name="analysis_service_description">Сканувати зображення та відео</string>
|
||||
<string name="analysis_notification_action_stop">Стоп</string>
|
||||
<string name="app_widget_label">Фоторамка</string>
|
||||
<string name="analysis_notification_default_title">Сканування медіа</string>
|
||||
|
|
|
@ -7,6 +7,5 @@
|
|||
<string name="app_widget_label">相框</string>
|
||||
<string name="search_shortcut_short_label">搜尋</string>
|
||||
<string name="analysis_channel_name">媒體掃描</string>
|
||||
<string name="analysis_service_description">掃描圖片和影片</string>
|
||||
<string name="analysis_notification_action_stop">停止</string>
|
||||
</resources>
|
|
@ -6,7 +6,6 @@
|
|||
<string name="search_shortcut_short_label">搜索</string>
|
||||
<string name="videos_shortcut_short_label">视频</string>
|
||||
<string name="analysis_channel_name">媒体扫描</string>
|
||||
<string name="analysis_service_description">扫描图像 & 视频</string>
|
||||
<string name="analysis_notification_default_title">正在扫描媒体库</string>
|
||||
<string name="analysis_notification_action_stop">停止</string>
|
||||
</resources>
|
|
@ -7,7 +7,6 @@
|
|||
<string name="search_shortcut_short_label">Search</string>
|
||||
<string name="videos_shortcut_short_label">Videos</string>
|
||||
<string name="analysis_channel_name">Media scan</string>
|
||||
<string name="analysis_service_description">Scan images & videos</string>
|
||||
<string name="analysis_notification_default_title">Scanning media</string>
|
||||
<string name="analysis_notification_action_stop">Stop</string>
|
||||
</resources>
|
|
@ -1,6 +1,6 @@
|
|||
buildscript {
|
||||
ext {
|
||||
kotlin_version = '1.8.0'
|
||||
kotlin_version = '1.8.21'
|
||||
agp_version = '7.4.2'
|
||||
glide_version = '4.15.1'
|
||||
huawei_agconnect_version = '1.8.0.300'
|
||||
|
|
|
@ -31,7 +31,7 @@ class AnalysisService {
|
|||
static Future<void> startService({required bool force, List<int>? entryIds}) async {
|
||||
await reportService.log('Start analysis service${entryIds != null ? ' for ${entryIds.length} items' : ''}');
|
||||
try {
|
||||
await _platform.invokeMethod('startService', <String, dynamic>{
|
||||
await _platform.invokeMethod('startAnalysis', <String, dynamic>{
|
||||
'entryIds': entryIds,
|
||||
'force': force,
|
||||
});
|
||||
|
@ -155,7 +155,6 @@ class Analyzer {
|
|||
|
||||
void _onSourceStateChanged() {
|
||||
if (_source.isReady) {
|
||||
_refreshApp();
|
||||
_serviceStateNotifier.value = AnalyzerState.stopping;
|
||||
}
|
||||
}
|
||||
|
@ -179,14 +178,6 @@ class Analyzer {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshApp() async {
|
||||
try {
|
||||
await _channel.invokeMethod('refreshApp');
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopPlatformService() async {
|
||||
try {
|
||||
await _channel.invokeMethod('stop');
|
||||
|
|
|
@ -2,7 +2,7 @@ group 'deckers.thibault.aves.aves_screen_state'
|
|||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.8.0'
|
||||
ext.kotlin_version = '1.8.21'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
|
Loading…
Reference in a new issue