From 2038c6722c1ebd977290492881023675e1cddecd Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong Date: Wed, 18 May 2022 18:31:18 +0700 Subject: [PATCH] Implement backup creation --- build.gradle | 3 + lawnchair/protos/lawnchair.proto | 21 +++ lawnchair/res/values/strings.xml | 8 + .../app/lawnchair/backup/LawnchairBackup.kt | 148 ++++++++++++++++ .../lawnchair/backup/ui/CreateBackupScreen.kt | 166 ++++++++++++++++++ .../backup/ui/CreateBackupViewModel.kt | 87 +++++++++ .../lawnchair/ui/preferences/Preferences.kt | 32 ++-- .../ui/preferences/PreferencesDashboard.kt | 11 ++ .../components/FlagSwitchPreference.kt | 25 +++ .../components/PreferenceDivider.kt | 3 +- lawnchair/src/app/lawnchair/util/FlagUtils.kt | 25 +++ .../src/app/lawnchair/util/LawnchairUtils.kt | 4 - .../lawnchair/views/LauncherPreviewView.kt | 47 +++-- .../graphics/LauncherPreviewRenderer.java | 16 +- .../launcher3/model/DeviceGridState.java | 18 ++ 15 files changed, 582 insertions(+), 32 deletions(-) create mode 100644 lawnchair/protos/lawnchair.proto create mode 100644 lawnchair/src/app/lawnchair/backup/LawnchairBackup.kt create mode 100644 lawnchair/src/app/lawnchair/backup/ui/CreateBackupScreen.kt create mode 100644 lawnchair/src/app/lawnchair/backup/ui/CreateBackupViewModel.kt create mode 100644 lawnchair/src/app/lawnchair/ui/preferences/components/FlagSwitchPreference.kt create mode 100644 lawnchair/src/app/lawnchair/util/FlagUtils.kt diff --git a/build.gradle b/build.gradle index 2f8b11a3a6..e29b58b321 100644 --- a/build.gradle +++ b/build.gradle @@ -284,6 +284,9 @@ android { assets { srcDirs 'lawnchair/assets' } + proto { + srcDirs = ['lawnchair/protos/'] + } } lawnWithoutQuickstep { diff --git a/lawnchair/protos/lawnchair.proto b/lawnchair/protos/lawnchair.proto new file mode 100644 index 0000000000..a3afd6829a --- /dev/null +++ b/lawnchair/protos/lawnchair.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +option java_package = "app.lawnchair"; +option java_outer_classname = "LawnchairProto"; + +import "google/protobuf/timestamp.proto"; + +message BackupInfo { + int32 lawnchair_version = 1; + int32 backup_version = 2; + google.protobuf.Timestamp created_at = 3; + + int32 contents = 4; + GridState grid_state = 5; +} + +message GridState { + string grid_size = 1; + int32 hotseat_count = 2; + int32 device_type = 3; +} diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index d507bf0bb8..02d012d347 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -258,4 +258,12 @@ Off Home Screen Home Screen & App Drawer + + Create backup + Create + What to Backup + Layout and Settings + Wallpaper + Backup created + Failed to create backup diff --git a/lawnchair/src/app/lawnchair/backup/LawnchairBackup.kt b/lawnchair/src/app/lawnchair/backup/LawnchairBackup.kt new file mode 100644 index 0000000000..eb77c89fc7 --- /dev/null +++ b/lawnchair/src/app/lawnchair/backup/LawnchairBackup.kt @@ -0,0 +1,148 @@ +package app.lawnchair.backup + +import android.annotation.SuppressLint +import android.app.WallpaperManager +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import androidx.core.graphics.drawable.toBitmap +import app.lawnchair.LawnchairProto.BackupInfo +import app.lawnchair.util.hasFlag +import app.lawnchair.util.scaleDownTo +import com.android.launcher3.BuildConfig +import com.android.launcher3.LauncherAppState +import com.android.launcher3.LauncherFiles +import com.android.launcher3.R +import com.android.launcher3.model.DeviceGridState +import com.google.protobuf.Timestamp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream +import java.text.SimpleDateFormat +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +class LawnchairBackup( + private val context: Context, + private val uri: Uri +) { + lateinit var info: BackupInfo + var screenshot: Bitmap? = null + var wallpaper: Bitmap? = null + + private suspend fun readInfoAndPreview() { + readZip(mapOf( + INFO_FILE_NAME to { info = BackupInfo.newBuilder().mergeFrom(it).build() }, + SCREENSHOT_FILE_NAME to { screenshot = BitmapFactory.decodeStream(it)?.scaleDownTo(1000, false) }, + WALLPAPER_FILE_NAME to { wallpaper = BitmapFactory.decodeStream(it)?.scaleDownTo(1000, false) }, + )) + } + + private suspend fun readZip(handlers: Map Unit>) { + withContext(Dispatchers.IO) { + val pfd = context.contentResolver.openFileDescriptor(uri, "r")!! + pfd.use { + FileInputStream(it.fileDescriptor).use { inStream -> + ZipInputStream(inStream).use { zipIs -> + var entry: ZipEntry? + while (true) { + entry = zipIs.nextEntry + if (entry == null) break + handlers[entry.name]?.invoke(zipIs) + } + } + } + } + } + } + + companion object { + private const val BACKUP_VERSION = 1 + + const val INFO_FILE_NAME = "info.pb" + const val WALLPAPER_FILE_NAME = "wallpaper.png" + const val SCREENSHOT_FILE_NAME = "screenshot.png" + + const val INCLUDE_LAYOUT_AND_SETTINGS = 1 shl 0 + const val INCLUDE_WALLPAPER = 1 shl 1 + + val contentOptions = listOf( + INCLUDE_LAYOUT_AND_SETTINGS to R.string.backup_content_layout_and_settings, + INCLUDE_WALLPAPER to R.string.backup_content_wallpaper, + ) + + fun generateBackupFileName(): String { + val fileName = "Lawnchair Backup ${SimpleDateFormat.getDateTimeInstance().format(Date())}" + return "$fileName.lawnchairbackup" + } + + @SuppressLint("MissingPermission") + suspend fun create(context: Context, contents: Int, screenshotBitmap: Bitmap, fileUri: Uri) { + val files: MutableList = ArrayList() + if (contents.hasFlag(INCLUDE_LAYOUT_AND_SETTINGS)) { + files.add(prefsFile(context)) + files.add(prefsDataStoreFile(context)) + } + val idp = LauncherAppState.getIDP(context) + val createdAt = Timestamp.newBuilder() + .setSeconds(System.currentTimeMillis() / 1000) + val info = BackupInfo.newBuilder() + .setLawnchairVersion(BuildConfig.VERSION_CODE) + .setBackupVersion(BACKUP_VERSION) + .setCreatedAt(createdAt) + .setContents(contents) + .setGridState(DeviceGridState(idp).toProtoMessage()) + .build() + + val pfd = context.contentResolver.openFileDescriptor(fileUri, "w")!! + withContext(Dispatchers.IO) { + pfd.use { + ZipOutputStream(FileOutputStream(pfd.fileDescriptor).buffered()).use { out -> + out.putNextEntry(ZipEntry(INFO_FILE_NAME)) + info.writeTo(out) + + if (contents.hasFlag(INCLUDE_WALLPAPER)) { + val wallpaperManager = WallpaperManager.getInstance(context) + val wallpaperBitmap = wallpaperManager.drawable?.toBitmap() + if (wallpaperBitmap != null) { + out.putNextEntry(ZipEntry(WALLPAPER_FILE_NAME)) + wallpaperBitmap.compress(Bitmap.CompressFormat.PNG, 100, out) + } + } + if (contents.hasFlag(INCLUDE_LAYOUT_AND_SETTINGS)) { + out.putNextEntry(ZipEntry(SCREENSHOT_FILE_NAME)) + screenshotBitmap.compress(Bitmap.CompressFormat.PNG, 85, out) + + out.putNextEntry(ZipEntry("launcher.db")) + gridDbFile(context).inputStream().copyTo(out) + } + + files.forEach { file -> + out.putNextEntry(ZipEntry(file.name)) + file.inputStream().copyTo(out) + } + } + } + } + } + + private fun gridDbFile(context: Context): File { + return context.getDatabasePath(LauncherAppState.getIDP(context).dbFile) + } + + private fun prefsFile(context: Context): File { + val dir = context.cacheDir.parent + return File(dir, "shared_prefs/" + LauncherFiles.SHARED_PREFERENCES_KEY + ".xml") + } + + private fun prefsDataStoreFile(context: Context): File { + return File(context.filesDir, "datastore/preferences.preferences_pb") + } + } +} diff --git a/lawnchair/src/app/lawnchair/backup/ui/CreateBackupScreen.kt b/lawnchair/src/app/lawnchair/backup/ui/CreateBackupScreen.kt new file mode 100644 index 0000000000..cbedbd7c52 --- /dev/null +++ b/lawnchair/src/app/lawnchair/backup/ui/CreateBackupScreen.kt @@ -0,0 +1,166 @@ +package app.lawnchair.backup.ui + +import android.Manifest +import android.app.Activity +import android.app.WallpaperManager +import android.content.Intent +import android.content.res.Configuration +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavGraphBuilder +import app.lawnchair.backup.LawnchairBackup +import app.lawnchair.ui.preferences.components.* +import app.lawnchair.ui.preferences.preferenceGraph +import app.lawnchair.util.hasFlag +import app.lawnchair.util.removeFlag +import com.android.launcher3.R +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.coroutines.launch + +fun NavGraphBuilder.createBackupGraph(route: String) { + preferenceGraph(route, { CreateBackupScreen(viewModel()) }) +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun CreateBackupScreen(viewModel: CreateBackupViewModel) { + val contents by viewModel.backupContents.collectAsState() + val screenshot by viewModel.screenshot.collectAsState() + val screenshotDone by viewModel.screenshotDone.collectAsState() + + val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT + val scrollState = rememberScrollState() + + val context = LocalContext.current + val hasLiveWallpaper = remember { WallpaperManager.getInstance(context).wallpaperInfo != null } + val permissionState = rememberPermissionState(Manifest.permission.READ_EXTERNAL_STORAGE) + val hasStoragePermission = permissionState.status.isGranted + + val scope = rememberCoroutineScope() + var creatingBackup by remember { mutableStateOf(false) } + + val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + val request = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult + val uri = it.data?.data ?: return@rememberLauncherForActivityResult + if (creatingBackup) return@rememberLauncherForActivityResult + scope.launch { + creatingBackup = true + try { + LawnchairBackup.create(context, contents, screenshot, uri) + backDispatcher?.onBackPressed() + Toast.makeText(context, R.string.backup_create_success, Toast.LENGTH_SHORT).show() + } catch (t: Throwable) { + Log.d("CreateBackupScreen", "failed to create backup", t) + Toast.makeText(context, R.string.backup_create_error, Toast.LENGTH_SHORT).show() + } + creatingBackup = true + } + } + + fun launchPicker() { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + intent.putExtra(Intent.EXTRA_TITLE, LawnchairBackup.generateBackupFileName()) + request.launch(intent) + } + + PreferenceLayout( + label = stringResource(id = R.string.create_backup), + scrollState = if (isPortrait) null else scrollState + ) { + DisposableEffect(contents, hasLiveWallpaper, hasStoragePermission) { + val canBackupWallpaper = hasLiveWallpaper || !hasStoragePermission + if (contents.hasFlag(LawnchairBackup.INCLUDE_WALLPAPER) && canBackupWallpaper) { + viewModel.setBackupContents(contents.removeFlag(LawnchairBackup.INCLUDE_WALLPAPER)) + } + onDispose { } + } + + if (isPortrait) { + DummyLauncherBox( + modifier = Modifier + .padding(top = 8.dp) + .weight(1f) + .align(Alignment.CenterHorizontally) + .clip(MaterialTheme.shapes.large) + ) { + if (contents.hasFlag(LawnchairBackup.INCLUDE_WALLPAPER)) { + WallpaperPreview(modifier = Modifier.fillMaxSize()) + } + if (contents.hasFlag(LawnchairBackup.INCLUDE_LAYOUT_AND_SETTINGS)) { + Image( + bitmap = screenshot.asImageBitmap(), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.FillHeight + ) + } + } + } + + PreferenceGroup(heading = stringResource(id = R.string.what_to_backup)) { + FlagSwitchPreference( + flags = contents, + setFlags = viewModel::setBackupContents, + mask = LawnchairBackup.INCLUDE_LAYOUT_AND_SETTINGS, + label = stringResource(id = R.string.backup_content_layout_and_settings) + ) + FlagSwitchPreference( + flags = contents, + setFlags = { + if (it.hasFlag(LawnchairBackup.INCLUDE_WALLPAPER) && !hasStoragePermission) { + permissionState.launchPermissionRequest() + } else { + viewModel.setBackupContents(it) + } + }, + mask = LawnchairBackup.INCLUDE_WALLPAPER, + label = stringResource(id = R.string.backup_content_wallpaper), + enabled = !hasLiveWallpaper, + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .padding(horizontal = 16.dp) + ) { + Button( + onClick = { launchPicker() }, + modifier = Modifier + .align(Alignment.CenterEnd) + .fillMaxWidth(), + enabled = contents != 0 && screenshotDone && !creatingBackup + ) { + Text(text = stringResource(id = R.string.create_backup_action)) + } + } + } +} diff --git a/lawnchair/src/app/lawnchair/backup/ui/CreateBackupViewModel.kt b/lawnchair/src/app/lawnchair/backup/ui/CreateBackupViewModel.kt new file mode 100644 index 0000000000..56c69e15c0 --- /dev/null +++ b/lawnchair/src/app/lawnchair/backup/ui/CreateBackupViewModel.kt @@ -0,0 +1,87 @@ +package app.lawnchair.backup.ui + +import android.app.Application +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Canvas +import android.view.ContextThemeWrapper +import android.view.View.MeasureSpec +import android.view.View.MeasureSpec.EXACTLY +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import app.lawnchair.backup.LawnchairBackup +import app.lawnchair.views.LauncherPreviewView +import com.android.launcher3.LauncherAppState +import com.android.launcher3.R +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import java.lang.Integer.max +import java.lang.Integer.min +import kotlin.coroutines.resume + +class CreateBackupViewModel( + application: Application, + private val savedStateHandle: SavedStateHandle +) : AndroidViewModel(application) { + val screenshot = MutableStateFlow(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + val screenshotDone = MutableStateFlow(false) + + val backupContents = savedStateHandle.getStateFlow( + "contents", + LawnchairBackup.INCLUDE_LAYOUT_AND_SETTINGS or LawnchairBackup.INCLUDE_WALLPAPER + ) + + init { + viewModelScope.launch { + captureScreenshot()?.let { screenshot.value = it } + screenshotDone.value = true + } + } + + private suspend fun captureScreenshot(): Bitmap? { + return suspendCancellableCoroutine { continuation -> + val app = getApplication() + + val config = Configuration(app.resources.configuration).apply { + orientation = Configuration.ORIENTATION_PORTRAIT + val width = screenWidthDp + val height = screenHeightDp + screenWidthDp = min(width, height) + screenHeightDp = max(width, height) + } + val context = app.createConfigurationContext(config) + + val idp = LauncherAppState.getIDP(context) + val themedContext = ContextThemeWrapper(context, R.style.Theme_Lawnchair) + val previewView = LauncherPreviewView( + context = themedContext, + idp = idp, + dummyInsets = true, + dummySmartspace = true, + appContext = themedContext + ) + continuation.invokeOnCancellation { previewView.destroy() } + previewView.addOnReadyCallback { + val dp = idp.getDeviceProfile(context) + val width = dp.widthPx + val height = dp.heightPx + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + previewView.measure( + MeasureSpec.makeMeasureSpec(width, EXACTLY), + MeasureSpec.makeMeasureSpec(height, EXACTLY), + ) + previewView.layout(0, 0, width, height) + previewView.draw(canvas) + continuation.resume(bitmap) + previewView.destroy() + } + } + } + + fun setBackupContents(contents: Int) { + savedStateHandle["contents"] = contents + } +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt index 9e629c3d67..c55d0c5ebc 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import app.lawnchair.backup.ui.createBackupGraph import app.lawnchair.ui.preferences.about.aboutGraph import app.lawnchair.ui.preferences.components.SystemUi import app.lawnchair.ui.util.ProvideBottomSheetHandler @@ -36,20 +37,22 @@ import com.google.accompanist.navigation.animation.rememberAnimatedNavController import soup.compose.material.motion.materialSharedAxisX object Routes { - const val GENERAL: String = "general" - const val ABOUT: String = "about" - const val HOME_SCREEN: String = "homeScreen" - const val DOCK: String = "dock" - const val APP_DRAWER: String = "appDrawer" - const val FOLDERS: String = "folders" - const val QUICKSTEP: String = "quickstep" - const val FONT_SELECTION: String = "fontSelection" - const val DEBUG_MENU: String = "debugMenu" - const val SELECT_ICON: String = "selectIcon" - const val ICON_PICKER: String = "iconPicker" - const val EXPERIMENTAL_FEATURES: String = "experimentalFeatures" - const val SMARTSPACE: String = "smartspace" - const val SMARTSPACE_WIDGET: String = "smartspaceWidget" + const val GENERAL = "general" + const val ABOUT = "about" + const val HOME_SCREEN = "homeScreen" + const val DOCK = "dock" + const val APP_DRAWER = "appDrawer" + const val FOLDERS = "folders" + const val QUICKSTEP = "quickstep" + const val FONT_SELECTION = "fontSelection" + const val DEBUG_MENU = "debugMenu" + const val SELECT_ICON = "selectIcon" + const val ICON_PICKER = "iconPicker" + const val EXPERIMENTAL_FEATURES = "experimentalFeatures" + const val SMARTSPACE = "smartspace" + const val SMARTSPACE_WIDGET = "smartspaceWidget" + const val CREATE_BACKUP = "createBackup" + const val RESTORE_BACKUP = "restoreBackup" } val LocalNavController = staticCompositionLocalOf { @@ -98,6 +101,7 @@ fun Preferences(interactor: PreferenceInteractor = viewModel Unit, + mask: Int, + label: String, + description: String? = null, + onClick: (() -> Unit)? = null, + enabled: Boolean = true, +) { + SwitchPreference( + checked = flags.hasFlag(mask), + onCheckedChange = { setFlags(flags.setFlag(mask, it)) }, + label = label, + description = description, + onClick = onClick, + enabled = enabled, + ) +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceDivider.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceDivider.kt index fdff981349..42418be904 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceDivider.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceDivider.kt @@ -10,11 +10,12 @@ import app.lawnchair.ui.theme.dividerColor @Composable fun PreferenceDivider( + modifier: Modifier = Modifier, startIndent: Dp = 0.dp, endIndent: Dp = 0.dp, ) { Divider( - modifier = Modifier + modifier = modifier .padding(start = startIndent + 16.dp, end = endIndent + 16.dp), color = dividerColor() ) diff --git a/lawnchair/src/app/lawnchair/util/FlagUtils.kt b/lawnchair/src/app/lawnchair/util/FlagUtils.kt new file mode 100644 index 0000000000..7c3a3a9b17 --- /dev/null +++ b/lawnchair/src/app/lawnchair/util/FlagUtils.kt @@ -0,0 +1,25 @@ +package app.lawnchair.util + +fun Int.hasFlag(flag: Int): Boolean { + return (this and flag) == flag +} + +fun Int.addFlag(flag: Int): Int { + return this or flag +} + +fun Int.removeFlag(flag: Int): Int { + return this and flag.inv() +} + +fun Int.toggleFlag(flag: Int): Int { + return if (hasFlag(flag)) removeFlag(flag) else addFlag(flag) +} + +fun Int.setFlag(flag: Int, value: Boolean): Int { + return if (value) { + addFlag(flag) + } else { + removeFlag(flag) + } +} diff --git a/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt b/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt index b138ac933b..7b98ba635f 100644 --- a/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt +++ b/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt @@ -161,10 +161,6 @@ fun getAllAppsScrimColor(context: Context): Int { return ColorUtils.setAlphaComponent(scrimColor, alpha) } -fun Int.hasFlag(flag: Int): Boolean { - return (this and flag) != 0 -} - fun Context.checkPackagePermission(packageName: String, permissionName: String): Boolean { try { val info = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS) diff --git a/lawnchair/src/app/lawnchair/views/LauncherPreviewView.kt b/lawnchair/src/app/lawnchair/views/LauncherPreviewView.kt index 13e3ef40b3..f7209289d3 100644 --- a/lawnchair/src/app/lawnchair/views/LauncherPreviewView.kt +++ b/lawnchair/src/app/lawnchair/views/LauncherPreviewView.kt @@ -3,15 +3,14 @@ package app.lawnchair.views import android.annotation.SuppressLint import android.appwidget.AppWidgetProviderInfo import android.content.Context -import android.content.res.ColorStateList import android.util.Log import android.view.ContextThemeWrapper import android.view.Gravity +import android.view.View import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout import androidx.annotation.UiThread import androidx.annotation.WorkerThread -import app.lawnchair.DeviceProfileOverrides import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.LauncherAppState import com.android.launcher3.LauncherSettings.Favorites.* @@ -32,12 +31,18 @@ import kotlin.math.min @SuppressLint("ViewConstructor") class LauncherPreviewView( context: Context, - private val idp: InvariantDeviceProfile + private val idp: InvariantDeviceProfile, + private val dummySmartspace: Boolean = false, + private val dummyInsets: Boolean = false, + private val appContext: Context = context.applicationContext ) : FrameLayout(context) { + private val onReadyCallbacks = RunnableList() private val onDestroyCallbacks = RunnableList() private var destroyed = false + private var rendererView: View? = null + private val spinner = CircularProgressIndicator(context).apply { val themedContext = ContextThemeWrapper(context, Themes.getActivityThemeRes(context)) val textColor = Themes.getAttrColor(themedContext, R.attr.workspaceTextColor) @@ -58,6 +63,10 @@ class LauncherPreviewView( loadAsync() } + fun addOnReadyCallback(runnable: Runnable) { + onReadyCallbacks.add(runnable) + } + @UiThread fun destroy() { destroyed = true @@ -73,7 +82,7 @@ class LauncherPreviewView( private fun loadModelData() { val migrated = doGridMigrationIfNecessary() - val inflationContext = ContextThemeWrapper(context.applicationContext, Themes.getActivityThemeRes(context)) + val inflationContext = ContextThemeWrapper(appContext, Themes.getActivityThemeRes(context)) if (migrated) { val previewContext = LauncherPreviewRenderer.PreviewContext(inflationContext, idp) object : LoaderTask( @@ -100,6 +109,7 @@ class LauncherPreviewView( renderView(inflationContext, dataModel, null) } } else { + onReadyCallbacks.executeAllAndDestroy() Log.e("LauncherPreviewView", "Model loading failed") } } @@ -125,8 +135,28 @@ class LauncherPreviewView( return } - val view = LauncherPreviewRenderer(inflationContext, idp, null) - .getRenderedView(dataModel, widgetProviderInfoMap) + val renderer = LauncherPreviewRenderer(inflationContext, idp, null, dummyInsets) + if (dummySmartspace) { + renderer.setWorkspaceSearchContainer(R.layout.smartspace_widget_placeholder) + } + + val view = renderer.getRenderedView(dataModel, widgetProviderInfoMap) + updateScale(view) + view.pivotX = if (layoutDirection == LAYOUT_DIRECTION_RTL) view.measuredWidth.toFloat() else 0f + view.pivotY = 0f + view.layoutParams = LayoutParams(view.measuredWidth, view.measuredHeight) + removeView(spinner) + rendererView = view + addView(view) + onReadyCallbacks.executeAllAndDestroy() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + rendererView?.let { updateScale(it) } + } + + private fun updateScale(view: View) { // This aspect scales the view to fit in the surface and centers it val scale: Float = min( measuredWidth / view.measuredWidth.toFloat(), @@ -134,10 +164,5 @@ class LauncherPreviewView( ) view.scaleX = scale view.scaleY = scale - view.pivotX = if (layoutDirection == LAYOUT_DIRECTION_RTL) view.measuredWidth.toFloat() else 0f - view.pivotY = 0f - view.layoutParams = LayoutParams(view.measuredWidth, view.measuredHeight) - removeView(spinner) - addView(view) } } diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index 286384502a..869cf54f4a 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -201,10 +201,18 @@ public class LauncherPreviewRenderer extends ContextWrapper private final Map mWorkspaceScreens = new HashMap<>(); private final AppWidgetHost mAppWidgetHost; private final SparseIntArray mWallpaperColorResources; + private int mWorkspaceSearchContainer = R.layout.search_container_workspace; public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, WallpaperColors wallpaperColorsOverride) { + this(context, idp, wallpaperColorsOverride, false); + } + + public LauncherPreviewRenderer(Context context, + InvariantDeviceProfile idp, + WallpaperColors wallpaperColorsOverride, + boolean dummyInsets) { super(context); mUiHandler = new Handler(Looper.getMainLooper()); @@ -212,7 +220,7 @@ public class LauncherPreviewRenderer extends ContextWrapper mIdp = idp; mDp = idp.getDeviceProfile(context).copy(context); - if (Utilities.ATLEAST_R) { + if (!dummyInsets && Utilities.ATLEAST_R) { WindowInsets currentWindowInsets = context.getSystemService(WindowManager.class) .getCurrentWindowMetrics().getWindowInsets(); mInsets = new Rect( @@ -504,7 +512,7 @@ public class LauncherPreviewRenderer extends ContextWrapper // Add first page QSB if (FeatureFlags.topQsbOnFirstScreenEnabled(mContext)) { CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID); - View qsb = mHomeElementInflater.inflate(R.layout.search_container_workspace, firstScreen, + View qsb = mHomeElementInflater.inflate(mWorkspaceSearchContainer, firstScreen, false); CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, firstScreen.getCountX(), 1); @@ -519,6 +527,10 @@ public class LauncherPreviewRenderer extends ContextWrapper measureView(mRootView, mDp.widthPx, mDp.heightPx); } + public void setWorkspaceSearchContainer(int resId) { + mWorkspaceSearchContainer = resId; + } + private static void measureView(View view, int width, int height) { view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY)); view.layout(0, 0, width, height); diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java index fa11d4e395..95a43f37c5 100644 --- a/src/com/android/launcher3/model/DeviceGridState.java +++ b/src/com/android/launcher3/model/DeviceGridState.java @@ -23,6 +23,7 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_4; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_5; +import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.text.TextUtils; @@ -34,6 +35,8 @@ import com.android.launcher3.logging.StatsLogManager.LauncherEvent; import java.util.Locale; import java.util.Objects; +import app.lawnchair.LawnchairProto; + /** * Utility class representing persisted grid properties. */ @@ -60,6 +63,13 @@ public class DeviceGridState { mDeviceType = prefs.getInt(KEY_DEVICE_TYPE, TYPE_PHONE); } + @SuppressLint("WrongConstant") + public DeviceGridState(LawnchairProto.GridState protoGridState) { + mGridSizeString = protoGridState.getGridSize(); + mNumHotseat = protoGridState.getHotseatCount(); + mDeviceType = protoGridState.getDeviceType(); + } + /** * Returns the device type for the grid */ @@ -78,6 +88,14 @@ public class DeviceGridState { .apply(); } + public LawnchairProto.GridState toProtoMessage() { + return LawnchairProto.GridState.newBuilder() + .setGridSize(mGridSizeString) + .setHotseatCount(mNumHotseat) + .setDeviceType(mDeviceType) + .build(); + } + /** * Returns the logging event corresponding to the grid state */