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
*/