From 0b67ccce1ab901414e8f5f624601ccef70ff635c Mon Sep 17 00:00:00 2001 From: Jing Jin <8752427+jinjingforever@users.noreply.github.com> Date: Mon, 19 May 2025 20:23:05 -0700 Subject: [PATCH] Fix a download resume bug. --- Android/src/app/build.gradle.kts | 2 +- Android/src/app/src/main/AndroidManifest.xml | 8 +++++ .../com/google/ai/edge/gallery/GalleryApp.kt | 2 +- .../ai/edge/gallery/GalleryApplication.kt | 3 ++ .../google/ai/edge/gallery/GalleryService.kt | 12 +++++++ .../com/google/ai/edge/gallery/data/Consts.kt | 1 + .../edge/gallery/data/DownloadRepository.kt | 29 ++++++++-------- .../google/ai/edge/gallery/ui/common/Utils.kt | 34 +++++++++++++++++++ .../ai/edge/gallery/worker/DownloadWorker.kt | 14 ++++++-- 9 files changed, 86 insertions(+), 19 deletions(-) create mode 100644 Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryService.kt diff --git a/Android/src/app/build.gradle.kts b/Android/src/app/build.gradle.kts index c6571f0..98ecf34 100644 --- a/Android/src/app/build.gradle.kts +++ b/Android/src/app/build.gradle.kts @@ -31,7 +31,7 @@ android { minSdk = 26 targetSdk = 35 versionCode = 1 - versionName = "0.9.5" + versionName = "0.9.6" // Needed for HuggingFace auth workflows. manifestPlaceholders["appAuthRedirectScheme"] = "com.google.ai.edge.gallery.oauth" diff --git a/Android/src/app/src/main/AndroidManifest.xml b/Android/src/app/src/main/AndroidManifest.xml index 62b91d1..acd3cb2 100644 --- a/Android/src/app/src/main/AndroidManifest.xml +++ b/Android/src/app/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ + + @@ -80,6 +82,12 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> + + + + + + \ No newline at end of file diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApp.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApp.kt index 53e5ec9..19f53a0 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApp.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApp.kt @@ -49,7 +49,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.google.ai.edge.gallery.R import com.google.ai.edge.gallery.data.AppBarAction import com.google.ai.edge.gallery.data.AppBarActionType import com.google.ai.edge.gallery.ui.navigation.GalleryNavHost @@ -65,6 +64,7 @@ fun GalleryApp(navController: NavHostController = rememberNavController()) { /** * The top app bar. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun GalleryTopAppBar( title: String, diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt index 942bf5e..d6c02a3 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt @@ -23,6 +23,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore import com.google.ai.edge.gallery.data.AppContainer import com.google.ai.edge.gallery.data.DefaultAppContainer +import com.google.ai.edge.gallery.ui.common.writeLaunchInfo import com.google.ai.edge.gallery.ui.theme.ThemeSettings private val Context.dataStore: DataStore by preferencesDataStore(name = "app_gallery_preferences") @@ -34,6 +35,8 @@ class GalleryApplication : Application() { override fun onCreate() { super.onCreate() + + writeLaunchInfo(context = this) container = DefaultAppContainer(this, dataStore) // Load theme. diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryService.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryService.kt new file mode 100644 index 0000000..f245943 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryService.kt @@ -0,0 +1,12 @@ +package com.google.ai.edge.gallery + +import android.app.Service +import android.content.Intent +import android.os.IBinder + +// TODO(jingjin): implement foreground service. +class GalleryService : Service() { + override fun onBind(p0: Intent?): IBinder? { + return null + } +} \ No newline at end of file diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/data/Consts.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/data/Consts.kt index 75eb4e4..57a3282 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/data/Consts.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/data/Consts.kt @@ -27,6 +27,7 @@ const val KEY_MODEL_DOWNLOAD_RATE = "KEY_MODEL_DOWNLOAD_RATE" const val KEY_MODEL_DOWNLOAD_REMAINING_MS = "KEY_MODEL_DOWNLOAD_REMAINING_SECONDS" const val KEY_MODEL_DOWNLOAD_ERROR_MESSAGE = "KEY_MODEL_DOWNLOAD_ERROR_MESSAGE" const val KEY_MODEL_DOWNLOAD_ACCESS_TOKEN = "KEY_MODEL_DOWNLOAD_ACCESS_TOKEN" +const val KEY_MODEL_DOWNLOAD_APP_TS = "KEY_MODEL_DOWNLOAD_APP_TS" const val KEY_MODEL_EXTRA_DATA_URLS = "KEY_MODEL_EXTRA_DATA_URLS" const val KEY_MODEL_EXTRA_DATA_DOWNLOAD_FILE_NAMES = "KEY_MODEL_EXTRA_DATA_DOWNLOAD_FILE_NAMES" const val KEY_MODEL_IS_ZIP = "KEY_MODEL_IS_ZIP" diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/data/DownloadRepository.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/data/DownloadRepository.kt index 131f36d..8dcd804 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/data/DownloadRepository.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/data/DownloadRepository.kt @@ -39,6 +39,7 @@ import androidx.work.WorkManager import androidx.work.WorkQuery import com.google.ai.edge.gallery.AppLifecycleProvider import com.google.ai.edge.gallery.R +import com.google.ai.edge.gallery.ui.common.readLaunchInfo import com.google.ai.edge.gallery.worker.DownloadWorker import com.google.common.util.concurrent.FutureCallback import com.google.common.util.concurrent.Futures @@ -85,24 +86,23 @@ class DefaultDownloadRepository( override fun downloadModel( model: Model, onStatusUpdated: (model: Model, status: ModelDownloadStatus) -> Unit ) { + val appTs = readLaunchInfo(context = context)?.ts ?: 0 + // Create input data. val builder = Data.Builder() val totalBytes = model.totalBytes + model.extraDataFiles.sumOf { it.sizeInBytes } - val inputDataBuilder = builder.putString(KEY_MODEL_URL, model.url) - .putString(KEY_MODEL_VERSION, model.version) - .putString(KEY_MODEL_DOWNLOAD_MODEL_DIR, model.normalizedName) - .putString(KEY_MODEL_DOWNLOAD_FILE_NAME, model.downloadFileName) - .putBoolean(KEY_MODEL_IS_ZIP, model.isZip).putString(KEY_MODEL_UNZIPPED_DIR, model.unzipDir) - .putLong( - KEY_MODEL_TOTAL_BYTES, totalBytes - ) + val inputDataBuilder = + builder.putString(KEY_MODEL_URL, model.url).putString(KEY_MODEL_VERSION, model.version) + .putString(KEY_MODEL_DOWNLOAD_MODEL_DIR, model.normalizedName) + .putString(KEY_MODEL_DOWNLOAD_FILE_NAME, model.downloadFileName) + .putBoolean(KEY_MODEL_IS_ZIP, model.isZip).putString(KEY_MODEL_UNZIPPED_DIR, model.unzipDir) + .putLong(KEY_MODEL_TOTAL_BYTES, totalBytes).putLong(KEY_MODEL_DOWNLOAD_APP_TS, appTs) + if (model.extraDataFiles.isNotEmpty()) { - inputDataBuilder.putString( - KEY_MODEL_EXTRA_DATA_URLS, model.extraDataFiles.joinToString(",") { it.url } - ).putString( + inputDataBuilder.putString(KEY_MODEL_EXTRA_DATA_URLS, + model.extraDataFiles.joinToString(",") { it.url }).putString( KEY_MODEL_EXTRA_DATA_DOWNLOAD_FILE_NAMES, - model.extraDataFiles.joinToString(",") { it.downloadFileName } - ) + model.extraDataFiles.joinToString(",") { it.downloadFileName }) } if (model.accessToken != null) { inputDataBuilder.putString(KEY_MODEL_DOWNLOAD_ACCESS_TOKEN, model.accessToken) @@ -281,8 +281,7 @@ class DefaultDownloadRepository( // Create an Intent to open your app with a deep link. val intent = Intent( - Intent.ACTION_VIEW, - Uri.parse("com.google.ai.edge.gallery://model/${modelName}") + Intent.ACTION_VIEW, Uri.parse("com.google.ai.edge.gallery://model/${modelName}") ).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/Utils.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/Utils.kt index 64df7ec..5b72d17 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/Utils.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/Utils.kt @@ -40,6 +40,8 @@ import com.google.ai.edge.gallery.ui.common.chat.Histogram import com.google.ai.edge.gallery.ui.common.chat.Stat import com.google.ai.edge.gallery.ui.modelmanager.ModelManagerViewModel import com.google.ai.edge.gallery.ui.theme.customColors +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import java.io.File @@ -52,6 +54,9 @@ import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt +private const val TAG = "AGUtils" +private const val LAUNCH_INFO_FILE_NAME = "launch_info" + private val STATS = listOf( Stat(id = "min", label = "Min", unit = "ms"), Stat(id = "max", label = "Max", unit = "ms"), @@ -70,6 +75,10 @@ data class JsonObjAndTextContent( val jsonObj: T, val textContent: String, ) +data class LaunchInfo( + val ts: Long +) + /** Format the bytes into a human-readable format. */ fun Long.humanReadableSize(si: Boolean = true, extraDecimalForGbAndAbove: Boolean = false): String { val bytes = this @@ -531,3 +540,28 @@ inline fun getJsonResponse(url: String): JsonObjAndTextContent? { return null } + +fun writeLaunchInfo(context: Context) { + try { + val gson = Gson() + val launchInfo = LaunchInfo(ts = System.currentTimeMillis()) + val jsonString = gson.toJson(launchInfo) + val file = File(context.getExternalFilesDir(null), LAUNCH_INFO_FILE_NAME) + file.writeText(jsonString) + } catch (e: Exception) { + Log.e(TAG, "Failed to write launch info", e) + } +} + +fun readLaunchInfo(context: Context): LaunchInfo? { + try { + val gson = Gson() + val type = object : TypeToken() {}.type + val file = File(context.getExternalFilesDir(null), LAUNCH_INFO_FILE_NAME) + val content = file.readText() + return gson.fromJson(content, type) + } catch (e: Exception) { + Log.e(TAG, "Failed to read launch info", e) + return null + } +} \ No newline at end of file diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/worker/DownloadWorker.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/worker/DownloadWorker.kt index 377741b..c294535 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/worker/DownloadWorker.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/worker/DownloadWorker.kt @@ -22,6 +22,7 @@ import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.WorkerParameters import com.google.ai.edge.gallery.data.KEY_MODEL_DOWNLOAD_ACCESS_TOKEN +import com.google.ai.edge.gallery.data.KEY_MODEL_DOWNLOAD_APP_TS import com.google.ai.edge.gallery.data.KEY_MODEL_DOWNLOAD_ERROR_MESSAGE import com.google.ai.edge.gallery.data.KEY_MODEL_DOWNLOAD_FILE_NAME import com.google.ai.edge.gallery.data.KEY_MODEL_DOWNLOAD_MODEL_DIR @@ -36,6 +37,7 @@ import com.google.ai.edge.gallery.data.KEY_MODEL_TOTAL_BYTES import com.google.ai.edge.gallery.data.KEY_MODEL_UNZIPPED_DIR import com.google.ai.edge.gallery.data.KEY_MODEL_URL import com.google.ai.edge.gallery.data.KEY_MODEL_VERSION +import com.google.ai.edge.gallery.ui.common.readLaunchInfo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.BufferedInputStream @@ -60,6 +62,8 @@ class DownloadWorker(context: Context, params: WorkerParameters) : private val externalFilesDir = context.getExternalFilesDir(null) override suspend fun doWork(): Result { + val appTs = readLaunchInfo(context = applicationContext)?.ts ?: 0 + val fileUrl = inputData.getString(KEY_MODEL_URL) val version = inputData.getString(KEY_MODEL_VERSION)!! val fileName = inputData.getString(KEY_MODEL_DOWNLOAD_FILE_NAME) @@ -71,6 +75,12 @@ class DownloadWorker(context: Context, params: WorkerParameters) : inputData.getString(KEY_MODEL_EXTRA_DATA_DOWNLOAD_FILE_NAMES)?.split(",") ?: listOf() val totalBytes = inputData.getLong(KEY_MODEL_TOTAL_BYTES, 0L) val accessToken = inputData.getString(KEY_MODEL_DOWNLOAD_ACCESS_TOKEN) + val workerAppTs = inputData.getLong(KEY_MODEL_DOWNLOAD_APP_TS, 0L) + + if (workerAppTs > 0 && appTs > 0 && workerAppTs != appTs) { + Log.d(TAG, "Worker is from previous launch. Ignoring...") + return Result.success() + } return withContext(Dispatchers.IO) { if (fileUrl == null || fileName == null) { @@ -172,11 +182,11 @@ class DownloadWorker(context: Context, params: WorkerParameters) : var bytesPerMs = 0f if (lastSetProgressTs != 0L) { if (bytesReadSizeBuffer.size == 5) { - bytesReadSizeBuffer.removeAt(bytesReadLatencyBuffer.lastIndex) + bytesReadSizeBuffer.removeAt(0) } bytesReadSizeBuffer.add(deltaBytes) if (bytesReadLatencyBuffer.size == 5) { - bytesReadLatencyBuffer.removeAt(bytesReadLatencyBuffer.lastIndex) + bytesReadLatencyBuffer.removeAt(0) } bytesReadLatencyBuffer.add(curTs - lastSetProgressTs) deltaBytes = 0L