mirror of
https://github.com/google-ai-edge/gallery.git
synced 2025-07-06 06:30:30 -04:00
Fix a download resume bug.
This commit is contained in:
parent
6785ad881a
commit
0b67ccce1a
9 changed files with 86 additions and 19 deletions
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue