Fix a download resume bug.

This commit is contained in:
Jing Jin 2025-05-19 20:23:05 -07:00
parent 6785ad881a
commit 0b67ccce1a
9 changed files with 86 additions and 19 deletions

View file

@ -31,7 +31,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "0.9.5" versionName = "0.9.6"
// Needed for HuggingFace auth workflows. // Needed for HuggingFace auth workflows.
manifestPlaceholders["appAuthRedirectScheme"] = "com.google.ai.edge.gallery.oauth" manifestPlaceholders["appAuthRedirectScheme"] = "com.google.ai.edge.gallery.oauth"

View file

@ -18,6 +18,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>-->
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>-->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
@ -80,6 +82,12 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<!-- <service-->
<!-- android:name=".GalleryService"-->
<!-- android:foregroundServiceType="dataSync"-->
<!-- android:exported="false">-->
<!-- </service>-->
</application> </application>
</manifest> </manifest>

View file

@ -49,7 +49,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController 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.AppBarAction
import com.google.ai.edge.gallery.data.AppBarActionType import com.google.ai.edge.gallery.data.AppBarActionType
import com.google.ai.edge.gallery.ui.navigation.GalleryNavHost import com.google.ai.edge.gallery.ui.navigation.GalleryNavHost
@ -65,6 +64,7 @@ fun GalleryApp(navController: NavHostController = rememberNavController()) {
/** /**
* The top app bar. * The top app bar.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun GalleryTopAppBar( fun GalleryTopAppBar(
title: String, title: String,

View file

@ -23,6 +23,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.google.ai.edge.gallery.data.AppContainer import com.google.ai.edge.gallery.data.AppContainer
import com.google.ai.edge.gallery.data.DefaultAppContainer 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 import com.google.ai.edge.gallery.ui.theme.ThemeSettings
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "app_gallery_preferences") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "app_gallery_preferences")
@ -34,6 +35,8 @@ class GalleryApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
writeLaunchInfo(context = this)
container = DefaultAppContainer(this, dataStore) container = DefaultAppContainer(this, dataStore)
// Load theme. // Load theme.

View file

@ -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
}
}

View file

@ -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_REMAINING_MS = "KEY_MODEL_DOWNLOAD_REMAINING_SECONDS"
const val KEY_MODEL_DOWNLOAD_ERROR_MESSAGE = "KEY_MODEL_DOWNLOAD_ERROR_MESSAGE" 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_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_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_EXTRA_DATA_DOWNLOAD_FILE_NAMES = "KEY_MODEL_EXTRA_DATA_DOWNLOAD_FILE_NAMES"
const val KEY_MODEL_IS_ZIP = "KEY_MODEL_IS_ZIP" const val KEY_MODEL_IS_ZIP = "KEY_MODEL_IS_ZIP"

View file

@ -39,6 +39,7 @@ import androidx.work.WorkManager
import androidx.work.WorkQuery import androidx.work.WorkQuery
import com.google.ai.edge.gallery.AppLifecycleProvider import com.google.ai.edge.gallery.AppLifecycleProvider
import com.google.ai.edge.gallery.R 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.ai.edge.gallery.worker.DownloadWorker
import com.google.common.util.concurrent.FutureCallback import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
@ -85,24 +86,23 @@ class DefaultDownloadRepository(
override fun downloadModel( override fun downloadModel(
model: Model, onStatusUpdated: (model: Model, status: ModelDownloadStatus) -> Unit model: Model, onStatusUpdated: (model: Model, status: ModelDownloadStatus) -> Unit
) { ) {
val appTs = readLaunchInfo(context = context)?.ts ?: 0
// Create input data. // Create input data.
val builder = Data.Builder() val builder = Data.Builder()
val totalBytes = model.totalBytes + model.extraDataFiles.sumOf { it.sizeInBytes } val totalBytes = model.totalBytes + model.extraDataFiles.sumOf { it.sizeInBytes }
val inputDataBuilder = builder.putString(KEY_MODEL_URL, model.url) val inputDataBuilder =
.putString(KEY_MODEL_VERSION, model.version) 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_MODEL_DIR, model.normalizedName)
.putString(KEY_MODEL_DOWNLOAD_FILE_NAME, model.downloadFileName) .putString(KEY_MODEL_DOWNLOAD_FILE_NAME, model.downloadFileName)
.putBoolean(KEY_MODEL_IS_ZIP, model.isZip).putString(KEY_MODEL_UNZIPPED_DIR, model.unzipDir) .putBoolean(KEY_MODEL_IS_ZIP, model.isZip).putString(KEY_MODEL_UNZIPPED_DIR, model.unzipDir)
.putLong( .putLong(KEY_MODEL_TOTAL_BYTES, totalBytes).putLong(KEY_MODEL_DOWNLOAD_APP_TS, appTs)
KEY_MODEL_TOTAL_BYTES, totalBytes
)
if (model.extraDataFiles.isNotEmpty()) { if (model.extraDataFiles.isNotEmpty()) {
inputDataBuilder.putString( inputDataBuilder.putString(KEY_MODEL_EXTRA_DATA_URLS,
KEY_MODEL_EXTRA_DATA_URLS, model.extraDataFiles.joinToString(",") { it.url } model.extraDataFiles.joinToString(",") { it.url }).putString(
).putString(
KEY_MODEL_EXTRA_DATA_DOWNLOAD_FILE_NAMES, KEY_MODEL_EXTRA_DATA_DOWNLOAD_FILE_NAMES,
model.extraDataFiles.joinToString(",") { it.downloadFileName } model.extraDataFiles.joinToString(",") { it.downloadFileName })
)
} }
if (model.accessToken != null) { if (model.accessToken != null) {
inputDataBuilder.putString(KEY_MODEL_DOWNLOAD_ACCESS_TOKEN, model.accessToken) 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. // Create an Intent to open your app with a deep link.
val intent = Intent( val intent = Intent(
Intent.ACTION_VIEW, Intent.ACTION_VIEW, Uri.parse("com.google.ai.edge.gallery://model/${modelName}")
Uri.parse("com.google.ai.edge.gallery://model/${modelName}")
).apply { ).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }

View file

@ -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.common.chat.Stat
import com.google.ai.edge.gallery.ui.modelmanager.ModelManagerViewModel import com.google.ai.edge.gallery.ui.modelmanager.ModelManagerViewModel
import com.google.ai.edge.gallery.ui.theme.customColors 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.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
@ -52,6 +54,9 @@ import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt
private const val TAG = "AGUtils"
private const val LAUNCH_INFO_FILE_NAME = "launch_info"
private val STATS = listOf( private val STATS = listOf(
Stat(id = "min", label = "Min", unit = "ms"), Stat(id = "min", label = "Min", unit = "ms"),
Stat(id = "max", label = "Max", unit = "ms"), Stat(id = "max", label = "Max", unit = "ms"),
@ -70,6 +75,10 @@ data class JsonObjAndTextContent<T>(
val jsonObj: T, val textContent: String, val jsonObj: T, val textContent: String,
) )
data class LaunchInfo(
val ts: Long
)
/** Format the bytes into a human-readable format. */ /** Format the bytes into a human-readable format. */
fun Long.humanReadableSize(si: Boolean = true, extraDecimalForGbAndAbove: Boolean = false): String { fun Long.humanReadableSize(si: Boolean = true, extraDecimalForGbAndAbove: Boolean = false): String {
val bytes = this val bytes = this
@ -531,3 +540,28 @@ inline fun <reified T> getJsonResponse(url: String): JsonObjAndTextContent<T>? {
return null 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<LaunchInfo>() {}.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
}
}

View file

@ -22,6 +22,7 @@ import androidx.work.CoroutineWorker
import androidx.work.Data import androidx.work.Data
import androidx.work.WorkerParameters 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_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_ERROR_MESSAGE
import com.google.ai.edge.gallery.data.KEY_MODEL_DOWNLOAD_FILE_NAME import com.google.ai.edge.gallery.data.KEY_MODEL_DOWNLOAD_FILE_NAME
import com.google.ai.edge.gallery.data.KEY_MODEL_DOWNLOAD_MODEL_DIR 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_UNZIPPED_DIR
import com.google.ai.edge.gallery.data.KEY_MODEL_URL 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.data.KEY_MODEL_VERSION
import com.google.ai.edge.gallery.ui.common.readLaunchInfo
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -60,6 +62,8 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
private val externalFilesDir = context.getExternalFilesDir(null) private val externalFilesDir = context.getExternalFilesDir(null)
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val appTs = readLaunchInfo(context = applicationContext)?.ts ?: 0
val fileUrl = inputData.getString(KEY_MODEL_URL) val fileUrl = inputData.getString(KEY_MODEL_URL)
val version = inputData.getString(KEY_MODEL_VERSION)!! val version = inputData.getString(KEY_MODEL_VERSION)!!
val fileName = inputData.getString(KEY_MODEL_DOWNLOAD_FILE_NAME) 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() inputData.getString(KEY_MODEL_EXTRA_DATA_DOWNLOAD_FILE_NAMES)?.split(",") ?: listOf()
val totalBytes = inputData.getLong(KEY_MODEL_TOTAL_BYTES, 0L) val totalBytes = inputData.getLong(KEY_MODEL_TOTAL_BYTES, 0L)
val accessToken = inputData.getString(KEY_MODEL_DOWNLOAD_ACCESS_TOKEN) 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) { return withContext(Dispatchers.IO) {
if (fileUrl == null || fileName == null) { if (fileUrl == null || fileName == null) {
@ -172,11 +182,11 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
var bytesPerMs = 0f var bytesPerMs = 0f
if (lastSetProgressTs != 0L) { if (lastSetProgressTs != 0L) {
if (bytesReadSizeBuffer.size == 5) { if (bytesReadSizeBuffer.size == 5) {
bytesReadSizeBuffer.removeAt(bytesReadLatencyBuffer.lastIndex) bytesReadSizeBuffer.removeAt(0)
} }
bytesReadSizeBuffer.add(deltaBytes) bytesReadSizeBuffer.add(deltaBytes)
if (bytesReadLatencyBuffer.size == 5) { if (bytesReadLatencyBuffer.size == 5) {
bytesReadLatencyBuffer.removeAt(bytesReadLatencyBuffer.lastIndex) bytesReadLatencyBuffer.removeAt(0)
} }
bytesReadLatencyBuffer.add(curTs - lastSetProgressTs) bytesReadLatencyBuffer.add(curTs - lastSetProgressTs)
deltaBytes = 0L deltaBytes = 0L