diff --git a/Android/src/app/build.gradle.kts b/Android/src/app/build.gradle.kts
index 1e42bd6..9d63325 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 = "1.0.1"
+ versionName = "1.0.2"
// 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 acd3cb2..3d71053 100644
--- a/Android/src/app/src/main/AndroidManifest.xml
+++ b/Android/src/app/src/main/AndroidManifest.xml
@@ -18,8 +18,8 @@
-
-
+
+
@@ -83,11 +83,12 @@
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/data/Consts.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/data/Consts.kt
index 57a3282..4f6bb97 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
@@ -18,6 +18,7 @@ package com.google.ai.edge.gallery.data
// Keys used to send/receive data to Work.
const val KEY_MODEL_URL = "KEY_MODEL_URL"
+const val KEY_MODEL_NAME = "KEY_MODEL_NAME"
const val KEY_MODEL_VERSION = "KEY_MODEL_VERSION"
const val KEY_MODEL_DOWNLOAD_MODEL_DIR = "KEY_MODEL_DOWNLOAD_MODEL_DIR"
const val KEY_MODEL_DOWNLOAD_FILE_NAME = "KEY_MODEL_DOWNLOAD_FILE_NAME"
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 8dcd804..065c472 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
@@ -24,7 +24,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
-import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
@@ -92,7 +91,8 @@ class DefaultDownloadRepository(
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)
+ builder.putString(KEY_MODEL_NAME, model.name).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)
@@ -271,13 +271,11 @@ class DefaultDownloadRepository(
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val importance = NotificationManager.IMPORTANCE_HIGH
- val channel = NotificationChannel(channelId, channelName, importance)
- val notificationManager: NotificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.createNotificationChannel(channel)
- }
+ val importance = NotificationManager.IMPORTANCE_HIGH
+ val channel = NotificationChannel(channelId, channelName, importance)
+ val notificationManager: NotificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
// Create an Intent to open your app with a deep link.
val intent = Intent(
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatMessage.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatMessage.kt
index c33c462..50cae09 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatMessage.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatMessage.kt
@@ -44,7 +44,10 @@ data class Classification(val label: String, val score: Float, val color: Color)
/** Base class for a chat message. */
open class ChatMessage(
- open val type: ChatMessageType, open val side: ChatSide, open val latencyMs: Float = -1f
+ open val type: ChatMessageType,
+ open val side: ChatSide,
+ open val latencyMs: Float = -1f,
+ open val accelerator: String = "",
) {
open fun clone(): ChatMessage {
return ChatMessage(type = type, side = side, latencyMs = latencyMs)
@@ -52,7 +55,8 @@ open class ChatMessage(
}
/** Chat message for showing loading status. */
-class ChatMessageLoading : ChatMessage(type = ChatMessageType.LOADING, side = ChatSide.AGENT)
+class ChatMessageLoading(override val accelerator: String = "") :
+ ChatMessage(type = ChatMessageType.LOADING, side = ChatSide.AGENT, accelerator = accelerator)
/** Chat message for info (help). */
class ChatMessageInfo(val content: String) :
@@ -79,12 +83,19 @@ open class ChatMessageText(
// Benchmark result for LLM response.
var llmBenchmarkResult: ChatMessageBenchmarkLlmResult? = null,
-) : ChatMessage(type = ChatMessageType.TEXT, side = side, latencyMs = latencyMs) {
+ override val accelerator: String = "",
+) : ChatMessage(
+ type = ChatMessageType.TEXT,
+ side = side,
+ latencyMs = latencyMs,
+ accelerator = accelerator
+) {
override fun clone(): ChatMessageText {
return ChatMessageText(
content = content,
side = side,
latencyMs = latencyMs,
+ accelerator = accelerator,
isMarkdown = isMarkdown,
llmBenchmarkResult = llmBenchmarkResult,
)
@@ -168,10 +179,12 @@ class ChatMessageBenchmarkLlmResult(
val statValues: MutableMap,
val running: Boolean,
override val latencyMs: Float = 0f,
+ override val accelerator: String = "",
) : ChatMessage(
type = ChatMessageType.BENCHMARK_LLM_RESULT,
side = ChatSide.AGENT,
- latencyMs = latencyMs
+ latencyMs = latencyMs,
+ accelerator = accelerator,
)
data class Histogram(
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatPanel.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatPanel.kt
index 85a9df1..1e035eb 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatPanel.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatPanel.kt
@@ -80,7 +80,6 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.ai.edge.gallery.R
-import com.google.ai.edge.gallery.data.ConfigKey
import com.google.ai.edge.gallery.data.Model
import com.google.ai.edge.gallery.data.Task
import com.google.ai.edge.gallery.data.TaskType
@@ -266,9 +265,13 @@ fun ChatPanel(
horizontalAlignment = hAlign,
) messageColumn@{
// Sender row.
+ var agentName = stringResource(task.agentNameRes)
+ if (message.accelerator.isNotEmpty()) {
+ agentName = "$agentName on ${message.accelerator}"
+ }
MessageSender(
message = message,
- agentName = stringResource(task.agentNameRes),
+ agentName = agentName,
imageHistoryCurIndex = imageHistoryCurIndex.intValue
)
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatViewModel.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatViewModel.kt
index 285506c..7efdeef 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatViewModel.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/ChatViewModel.kt
@@ -141,6 +141,7 @@ open class ChatViewModel(val task: Task) : ViewModel() {
content = newContent,
side = lastMessage.side,
latencyMs = latencyMs,
+ accelerator = lastMessage.accelerator,
)
newMessages.removeAt(newMessages.size - 1)
newMessages.add(newLastMessage)
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageInputText.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageInputText.kt
index 45b0940..73b1713 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageInputText.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageInputText.kt
@@ -23,7 +23,6 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.net.Uri
-import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
@@ -31,9 +30,9 @@ import androidx.annotation.StringRes
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.lifecycle.awaitInstance
-import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -57,12 +56,12 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Send
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material.icons.rounded.FlipCameraAndroid
import androidx.compose.material.icons.rounded.History
import androidx.compose.material.icons.rounded.Photo
import androidx.compose.material.icons.rounded.PhotoCamera
import androidx.compose.material.icons.rounded.PostAdd
import androidx.compose.material.icons.rounded.Stop
-import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -81,13 +80,13 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.focus.focusModifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
@@ -96,13 +95,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
-import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.google.ai.edge.gallery.R
import com.google.ai.edge.gallery.ui.common.createTempPictureUri
import com.google.ai.edge.gallery.ui.modelmanager.ModelManagerViewModel
import com.google.ai.edge.gallery.ui.preview.PreviewModelManagerViewModel
import com.google.ai.edge.gallery.ui.theme.GalleryTheme
+import kotlinx.coroutines.launch
import java.util.concurrent.Executors
/**
@@ -131,6 +130,7 @@ fun MessageInputText(
showStopButtonWhenInProgress: Boolean = false,
) {
val context = LocalContext.current
+ val scope = rememberCoroutineScope()
val modelManagerUiState by modelManagerViewModel.uiState.collectAsState()
var showAddContentMenu by remember { mutableStateOf(false) }
var showTextInputHistorySheet by remember { mutableStateOf(false) }
@@ -144,6 +144,11 @@ fun MessageInputText(
newPickedImages.add(bitmap)
pickedImages = newPickedImages.toList()
}
+ var hasFrontCamera by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ checkFrontCamera(context = context, callback = { hasFrontCamera = it })
+ }
// launches camera
val cameraLauncher =
@@ -167,8 +172,8 @@ fun MessageInputText(
if (permissionGranted) {
showAddContentMenu = false
tempPhotoUri = context.createTempPictureUri()
-// showCameraCaptureBottomSheet = true
- cameraLauncher.launch(tempPhotoUri)
+ showCameraCaptureBottomSheet = true
+// cameraLauncher.launch(tempPhotoUri)
}
}
@@ -264,8 +269,8 @@ fun MessageInputText(
) -> {
showAddContentMenu = false
tempPhotoUri = context.createTempPictureUri()
-// showCameraCaptureBottomSheet = true
- cameraLauncher.launch(tempPhotoUri)
+ showCameraCaptureBottomSheet = true
+// cameraLauncher.launch(tempPhotoUri)
}
// Otherwise, ask for permission
@@ -408,12 +413,14 @@ fun MessageInputText(
ModalBottomSheet(
sheetState = cameraCaptureSheetState,
onDismissRequest = { showCameraCaptureBottomSheet = false }) {
+
val lifecycleOwner = LocalLifecycleOwner.current
val previewUseCase = remember { androidx.camera.core.Preview.Builder().build() }
val imageCaptureUseCase = remember { ImageCapture.Builder().build() }
var cameraProvider by remember { mutableStateOf(null) }
var cameraControl by remember { mutableStateOf(null) }
val localContext = LocalContext.current
+ var cameraSide by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) }
val executor = remember { Executors.newSingleThreadExecutor() }
val capturedImageUri = remember { mutableStateOf(null) }
@@ -421,7 +428,7 @@ fun MessageInputText(
fun rebindCameraProvider() {
cameraProvider?.let { cameraProvider ->
val cameraSelector = CameraSelector.Builder()
- .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+ .requireLensFacing(cameraSide)
.build()
cameraProvider.unbindAll()
val camera = cameraProvider.bindToLifecycle(
@@ -439,6 +446,10 @@ fun MessageInputText(
rebindCameraProvider()
}
+ LaunchedEffect(cameraSide) {
+ rebindCameraProvider()
+ }
+
// val cameraController = remember {
// LifecycleCameraController(context).apply {
// bindToLifecycle(lifecycleOwner)
@@ -465,25 +476,92 @@ fun MessageInputText(
// cameraController.unbind() // Unbinds the camera to free up resources
// }
)
+
+ // Close button.
+ IconButton(
+ onClick = {
+ scope.launch {
+ cameraCaptureSheetState.hide()
+ showCameraCaptureBottomSheet = false
+ }
+ }, colors = IconButtonDefaults.iconButtonColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant,
+ ), modifier = Modifier
+ .offset(x = (-8).dp, y = 8.dp)
+ .align(Alignment.TopEnd)
+ ) {
+ Icon(
+ Icons.Rounded.Close,
+ contentDescription = "",
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+
// Button that triggers the image capture process
IconButton(
colors = IconButtonDefaults.iconButtonColors(
- containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ containerColor = MaterialTheme.colorScheme.primary,
),
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 32.dp)
- .size(64.dp),
+ .size(64.dp)
+ .border(2.dp, MaterialTheme.colorScheme.onPrimary, CircleShape),
onClick = {
+ scope.launch {
+ val callback = object : ImageCapture.OnImageCapturedCallback() {
+ override fun onCaptureSuccess(image: ImageProxy) {
+ var bitmap = image.toBitmap()
+ val rotation = image.imageInfo.rotationDegrees
+ bitmap = if (rotation != 0) {
+ val matrix = Matrix().apply {
+ postRotate(rotation.toFloat())
+ }
+ Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ } else bitmap
+ updatePickedImages(bitmap)
+ image.close()
+ }
+ }
+ imageCaptureUseCase.takePicture(executor, callback)
+ cameraCaptureSheetState.hide()
+ showCameraCaptureBottomSheet = false
+ }
},
) {
Icon(
Icons.Rounded.PhotoCamera,
contentDescription = "",
- tint = MaterialTheme.colorScheme.onTertiaryContainer,
+ tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(36.dp)
)
}
+
+ // Button that toggles the front and back camera.
+ if (hasFrontCamera) {
+ IconButton(
+ colors = IconButtonDefaults.iconButtonColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer,
+ ),
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(bottom = 40.dp, end = 32.dp)
+ .size(48.dp),
+ onClick = {
+ cameraSide = when (cameraSide) {
+ CameraSelector.LENS_FACING_BACK -> CameraSelector.LENS_FACING_FRONT
+ else -> CameraSelector.LENS_FACING_BACK
+ }
+ },
+ ) {
+ Icon(
+ Icons.Rounded.FlipCameraAndroid,
+ contentDescription = "",
+ tint = MaterialTheme.colorScheme.onSecondaryContainer,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
}
}
}
@@ -501,13 +579,7 @@ private fun handleImageSelected(
val bitmap: Bitmap? = try {
val inputStream = context.contentResolver.openInputStream(uri)
val tmpBitmap = BitmapFactory.decodeStream(inputStream)
- if (rotateForPortrait && tmpBitmap.width > tmpBitmap.height) {
- val matrix = Matrix()
- matrix.postRotate(90f)
- Bitmap.createBitmap(tmpBitmap, 0, 0, tmpBitmap.width, tmpBitmap.height, matrix, true)
- } else {
- tmpBitmap
- }
+ rotateImageIfNecessary(bitmap = tmpBitmap, rotateForPortrait = rotateForPortrait)
} catch (e: Exception) {
e.printStackTrace()
null
@@ -517,6 +589,31 @@ private fun handleImageSelected(
}
}
+private fun rotateImageIfNecessary(bitmap: Bitmap, rotateForPortrait: Boolean = false): Bitmap {
+ return if (rotateForPortrait && bitmap.width > bitmap.height) {
+ val matrix = Matrix()
+ matrix.postRotate(90f)
+ Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ } else {
+ bitmap
+ }
+}
+
+private fun checkFrontCamera(context: Context, callback: (Boolean) -> Unit) {
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
+ cameraProviderFuture.addListener(Runnable {
+ val cameraProvider = cameraProviderFuture.get()
+ try {
+ // Attempt to select the default front camera
+ val hasFront = cameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)
+ callback(hasFront)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ callback(false)
+ }
+ }, ContextCompat.getMainExecutor(context))
+}
+
private fun createMessagesToSend(pickedImages: List, text: String): List {
val messages: MutableList = mutableListOf()
if (pickedImages.isNotEmpty()) {
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageSender.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageSender.kt
index 38d6860..ac1375c 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageSender.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageSender.kt
@@ -180,6 +180,9 @@ private fun getMessageLayoutConfig(
horizontalArrangement = Arrangement.SpaceBetween
modifier = modifier.fillMaxWidth()
userLabel = "Stats"
+ if (message.accelerator.isNotEmpty()) {
+ userLabel = "${userLabel} on ${message.accelerator}"
+ }
}
is ChatMessageImageWithHistory -> {
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/home/HomeScreen.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/home/HomeScreen.kt
index 37e48c3..5b124c1 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/home/HomeScreen.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/home/HomeScreen.kt
@@ -388,7 +388,7 @@ private fun TaskList(
introText,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold),
- modifier = Modifier.padding(bottom = 20.dp)
+ modifier = Modifier.padding(bottom = 20.dp).padding(horizontal = 16.dp)
)
}
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/llmchat/LlmChatViewModel.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/llmchat/LlmChatViewModel.kt
index 9eff00a..b99cdc8 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/llmchat/LlmChatViewModel.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/llmchat/LlmChatViewModel.kt
@@ -20,6 +20,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import androidx.lifecycle.viewModelScope
+import com.google.ai.edge.gallery.data.ConfigKey
import com.google.ai.edge.gallery.data.Model
import com.google.ai.edge.gallery.data.TASK_LLM_CHAT
import com.google.ai.edge.gallery.data.TASK_LLM_ASK_IMAGE
@@ -47,6 +48,7 @@ private val STATS = listOf(
open class LlmChatViewModel(curTask: Task = TASK_LLM_CHAT) : ChatViewModel(task = curTask) {
fun generateResponse(model: Model, input: String, image: Bitmap? = null, onError: () -> Unit) {
+ val accelerator = model.getStringConfigValue(key = ConfigKey.ACCELERATOR, defaultValue = "")
viewModelScope.launch(Dispatchers.Default) {
setInProgress(true)
setPreparing(true)
@@ -54,7 +56,7 @@ open class LlmChatViewModel(curTask: Task = TASK_LLM_CHAT) : ChatViewModel(task
// Loading.
addMessage(
model = model,
- message = ChatMessageLoading(),
+ message = ChatMessageLoading(accelerator = accelerator),
)
// Wait for instance to be initialized.
@@ -103,7 +105,12 @@ open class LlmChatViewModel(curTask: Task = TASK_LLM_CHAT) : ChatViewModel(task
// Add an empty message that will receive streaming results.
addMessage(
- model = model, message = ChatMessageText(content = "", side = ChatSide.AGENT)
+ model = model,
+ message = ChatMessageText(
+ content = "",
+ side = ChatSide.AGENT,
+ accelerator = accelerator
+ )
)
}
@@ -133,6 +140,7 @@ open class LlmChatViewModel(curTask: Task = TASK_LLM_CHAT) : ChatViewModel(task
),
running = false,
latencyMs = -1f,
+ accelerator = accelerator,
)
)
}
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/llmsingleturn/ResponsePanel.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/llmsingleturn/ResponsePanel.kt
index 43ece19..28116a9 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/llmsingleturn/ResponsePanel.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/llmsingleturn/ResponsePanel.kt
@@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AutoAwesome
@@ -56,6 +58,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.google.ai.edge.gallery.data.ConfigKey
import com.google.ai.edge.gallery.data.Model
import com.google.ai.edge.gallery.data.TASK_LLM_PROMPT_LAB
import com.google.ai.edge.gallery.ui.common.chat.MarkdownText
@@ -88,6 +92,7 @@ fun ResponsePanel(
val pagerState = rememberPagerState(
initialPage = task.models.indexOf(model),
pageCount = { task.models.size })
+ val accelerator = model.getStringConfigValue(key = ConfigKey.ACCELERATOR, defaultValue = "")
// Select the "response" tab when prompt template changes.
LaunchedEffect(selectedPromptTemplateType) {
@@ -191,7 +196,22 @@ fun ResponsePanel(
.size(16.dp)
.alpha(0.7f)
)
- Text(text = title)
+ var curTitle = title
+ if (accelerator.isNotEmpty()) {
+ curTitle = "$curTitle on $accelerator"
+ }
+ val titleColor = MaterialTheme.colorScheme.primary
+ BasicText(
+ text = curTitle,
+ maxLines = 1,
+ color = { titleColor },
+ style = MaterialTheme.typography.bodyMedium,
+ autoSize = TextAutoSize.StepBased(
+ minFontSize = 9.sp,
+ maxFontSize = 14.sp,
+ stepSize = 1.sp
+ )
+ )
}
})
}
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/modelmanager/ModelList.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/modelmanager/ModelList.kt
index ee8a295..806a6e6 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/modelmanager/ModelList.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/modelmanager/ModelList.kt
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Description
@@ -91,11 +92,14 @@ fun ModelList(
}
}
+ val listState = rememberLazyListState()
+
Box(contentAlignment = Alignment.BottomEnd) {
LazyColumn(
modifier = modifier.padding(top = 8.dp),
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(8.dp),
+ state = listState,
) {
// Headline.
item(key = "headline") {
@@ -103,7 +107,9 @@ fun ModelList(
task.description,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold),
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
)
}
diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/modelmanager/ModelManagerViewModel.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/modelmanager/ModelManagerViewModel.kt
index 2abad3c..63b5db7 100644
--- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/modelmanager/ModelManagerViewModel.kt
+++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/modelmanager/ModelManagerViewModel.kt
@@ -382,6 +382,8 @@ open class ModelManagerViewModel(
}
_uiState.update { _uiState.value.copy(textInputHistory = newHistory) }
dataStoreRepository.saveTextInputHistory(_uiState.value.textInputHistory)
+ } else {
+ promoteTextInputHistoryItem(text)
}
}
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 c294535..ed1a2d2 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
@@ -16,10 +16,16 @@
package com.google.ai.edge.gallery.worker
+import android.app.NotificationChannel
+import android.app.NotificationManager
import android.content.Context
+import android.content.pm.ServiceInfo
+import android.os.Build
import android.util.Log
+import androidx.core.app.NotificationCompat
import androidx.work.CoroutineWorker
import androidx.work.Data
+import androidx.work.ForegroundInfo
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
@@ -32,6 +38,7 @@ import com.google.ai.edge.gallery.data.KEY_MODEL_DOWNLOAD_REMAINING_MS
import com.google.ai.edge.gallery.data.KEY_MODEL_EXTRA_DATA_DOWNLOAD_FILE_NAMES
import com.google.ai.edge.gallery.data.KEY_MODEL_EXTRA_DATA_URLS
import com.google.ai.edge.gallery.data.KEY_MODEL_IS_ZIP
+import com.google.ai.edge.gallery.data.KEY_MODEL_NAME
import com.google.ai.edge.gallery.data.KEY_MODEL_START_UNZIPPING
import com.google.ai.edge.gallery.data.KEY_MODEL_TOTAL_BYTES
import com.google.ai.edge.gallery.data.KEY_MODEL_UNZIPPED_DIR
@@ -57,14 +64,40 @@ data class UrlAndFileName(
val fileName: String,
)
+private const val FOREGROUND_NOTIFICATION_CHANNEL_ID = "model_download_channel_foreground"
+private var channelCreated = false
+
class DownloadWorker(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
private val externalFilesDir = context.getExternalFilesDir(null)
+ private val notificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ // Unique notification id.
+ private val notificationId: Int = params.id.hashCode()
+
+ init {
+ if (!channelCreated) {
+ // Create a notification channel for showing notifications for model downloading progress.
+ val channel = NotificationChannel(
+ FOREGROUND_NOTIFICATION_CHANNEL_ID,
+ "Model Downloading",
+ // Make it silent.
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "Notifications for model downloading"
+ }
+ notificationManager.createNotificationChannel(channel)
+ channelCreated = true
+ }
+ }
+
override suspend fun doWork(): Result {
val appTs = readLaunchInfo(context = applicationContext)?.ts ?: 0
val fileUrl = inputData.getString(KEY_MODEL_URL)
+ val modelName = inputData.getString(KEY_MODEL_NAME) ?: "Model"
val version = inputData.getString(KEY_MODEL_VERSION)!!
val fileName = inputData.getString(KEY_MODEL_DOWNLOAD_FILE_NAME)
val modelDir = inputData.getString(KEY_MODEL_DOWNLOAD_MODEL_DIR)!!
@@ -87,6 +120,9 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
Result.failure()
} else {
return@withContext try {
+ // Set the worker as a foreground service immediately.
+ setForeground(createForegroundInfo(progress = 0, modelName = modelName))
+
// Collect data for all files.
val allFiles: MutableList = mutableListOf()
allFiles.add(UrlAndFileName(url = fileUrl, fileName = fileName))
@@ -206,6 +242,11 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
KEY_MODEL_DOWNLOAD_REMAINING_MS, remainingMs.toLong()
).build()
)
+ setForeground(
+ createForegroundInfo(
+ progress = (downloadedBytes * 100 / totalBytes).toInt(), modelName = modelName
+ )
+ )
Log.d(TAG, "downloadedBytes: $downloadedBytes")
lastSetProgressTs = curTs
}
@@ -221,11 +262,10 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
setProgress(Data.Builder().putBoolean(KEY_MODEL_START_UNZIPPING, true).build())
// Prepare target dir.
- val destDir =
- File(
- externalFilesDir,
- listOf(modelDir, version, unzippedDir).joinToString(File.separator)
- )
+ val destDir = File(
+ externalFilesDir,
+ listOf(modelDir, version, unzippedDir).joinToString(File.separator)
+ )
if (!destDir.exists()) {
destDir.mkdirs()
}
@@ -276,4 +316,42 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
}
}
}
+
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ // Initial progress is 0
+ return createForegroundInfo(0)
+ }
+
+ /**
+ * Creates a [ForegroundInfo] object for the download worker's ongoing notification.
+ * This notification is used to keep the worker running in the foreground, indicating
+ * to the user that an active download is in progress.
+ */
+ private fun createForegroundInfo(progress: Int, modelName: String? = null): ForegroundInfo {
+ // Create a notification for the foreground service
+ var title = "Downloading model"
+ if (modelName != null) {
+ title = "Downloading \"$modelName\""
+ }
+ val content = "Downloading in progress: $progress%"
+
+ val notification =
+ NotificationCompat.Builder(applicationContext, FOREGROUND_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(title).setContentText(content)
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
+ .setOngoing(true) // Makes the notification non-dismissable
+ .setProgress(100, progress, false) // Show progress
+ .build()
+
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ ForegroundInfo(
+ notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ )
+ } else {
+ ForegroundInfo(
+ notificationId,
+ notification,
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/Android/src/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Android/src/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 6942fb7..345888d 100644
--- a/Android/src/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/Android/src/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,20 +1,4 @@
-
-
diff --git a/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 8f6b3fb..44f0938 100644
Binary files a/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
index 13f178c..1966948 100644
Binary files a/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and b/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ
diff --git a/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
index ec172b7..43b53a3 100644
Binary files a/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
index ec172b7..43b53a3 100644
Binary files a/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png and b/Android/src/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ
diff --git a/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher.png
index b860434..4d0086b 100644
Binary files a/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
index 46e114d..75025cf 100644
Binary files a/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and b/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ
diff --git a/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
index 8b41ec7..26e24ba 100644
Binary files a/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
index 8b41ec7..26e24ba 100644
Binary files a/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png and b/Android/src/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 9981e9a..b9fe1e6 100644
Binary files a/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
index bf6bc62..9784f16 100644
Binary files a/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and b/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
index 5606e5f..b360465 100644
Binary files a/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
index 5606e5f..b360465 100644
Binary files a/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png and b/Android/src/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d16a726..7a25194 100644
Binary files a/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
index be8ef1d..04ef206 100644
Binary files a/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and b/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
index 760ef15..e0ea6e8 100644
Binary files a/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
index 760ef15..e0ea6e8 100644
Binary files a/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png and b/Android/src/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 92a5411..28c9f3d 100644
Binary files a/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
index e165902..66a5487 100644
Binary files a/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and b/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
index 0a45db8..9ed6922 100644
Binary files a/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
index 0a45db8..9ed6922 100644
Binary files a/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png and b/Android/src/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ