diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index 42d4d50218..2e0f6762b4 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -72,6 +72,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class InvariantDeviceProfile { @@ -577,6 +578,45 @@ public class InvariantDeviceProfile { return filteredProfiles; } + /** + * Returns the GridOption associated to the given file name or null if the fileName is not + * supported. + * Ej, launcher.db -> "normal grid", launcher_4_by_4.db -> "practical grid" + */ + public GridOption getGridOptionFromFileName(Context context, String fileName) { + return parseAllGridOptions(context).stream() + .filter(gridOption -> Objects.equals(gridOption.dbFile, fileName)) + .findFirst() + .orElse(null); + } + + /** + * Returns the name of the given size on the current device or empty string if the size is not + * supported. Ej. 4x4 -> normal, 5x4 -> practical, etc. + * (Note: the name of the grid can be different for the same grid size depending of + * the values of the InvariantDeviceProfile) + * + */ + public String getGridNameFromSize(Context context, Point size) { + return parseAllGridOptions(context).stream() + .filter(gridOption -> gridOption.numColumns == size.x + && gridOption.numRows == size.y) + .map(gridOption -> gridOption.name) + .findFirst() + .orElse(""); + } + + /** + * Returns the grid option for the given gridName on the current device (Note: the gridOption + * be different for the same gridName depending on the values of the InvariantDeviceProfile). + */ + public GridOption getGridOptionFromName(Context context, String gridName) { + return parseAllGridOptions(context).stream() + .filter(gridOption -> Objects.equals(gridOption.name, gridName)) + .findFirst() + .orElse(null); + } + /** * @return all the grid options that can be shown on the device */ diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java index 22bc13bb25..f2b7d18fed 100644 --- a/src/com/android/launcher3/provider/RestoreDbTask.java +++ b/src/com/android/launcher3/provider/RestoreDbTask.java @@ -50,8 +50,10 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; +import com.android.launcher3.Flags; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherFiles; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; @@ -121,7 +123,48 @@ public class RestoreDbTask { // executed again. LauncherPrefs.get(context).removeSync(RESTORE_DEVICE); - idp.reinitializeAfterRestore(context); + if (Flags.narrowGridRestore()) { + String oldPhoneFileName = idp.dbFile; + removeOldDBs(context, oldPhoneFileName); + trySettingPreviousGidAsCurrent(context, idp, oldPhoneFileName); + } else { + idp.reinitializeAfterRestore(context); + } + } + + /** + * Try setting the gird used in the previous phone to the new one. If the current device doesn't + * support the previous grid option it will not be set. + */ + private static void trySettingPreviousGidAsCurrent(Context context, InvariantDeviceProfile idp, + String oldPhoneDbFileName) { + InvariantDeviceProfile.GridOption gridOption = idp.getGridOptionFromFileName(context, + oldPhoneDbFileName); + if (gridOption != null) { + /* + * We do this because in some cases different devices have different names for grid + * options, in one device the grid option "normal" can be 4x4 while in other it + * could be "practical". Calling this changes the current device grid to the same + * we had in the other phone, in the case the current phone doesn't support the grid + * option we use the default and migrate the db to the default. Migration occurs on + * {@code GridSizeMigrationUtil#migrateGridIfNeeded} + */ + idp.setCurrentGrid(context, gridOption.name); + } + } + + /** + * Only keep the last database used on the previous device. + */ + private static void removeOldDBs(Context context, String oldPhoneDbFileName) { + // At this point idp.dbFile contains the name of the dbFile from the previous phone + LauncherFiles.GRID_DB_FILES.stream() + .filter(dbName -> !dbName.equals(oldPhoneDbFileName)) + .forEach(dbName -> { + if (context.getDatabasePath(dbName).delete()) { + FileLog.d(TAG, "Removed old grid db file: " + dbName); + } + }); } private static boolean performRestore(Context context, ModelDbController controller) { diff --git a/tests/assets/databases/BackupAndRestore/launcher.db b/tests/assets/databases/BackupAndRestore/launcher.db new file mode 100644 index 0000000000..126d166492 Binary files /dev/null and b/tests/assets/databases/BackupAndRestore/launcher.db differ diff --git a/tests/assets/databases/BackupAndRestore/launcher_3_by_3.db b/tests/assets/databases/BackupAndRestore/launcher_3_by_3.db new file mode 100644 index 0000000000..6d8cd735b7 Binary files /dev/null and b/tests/assets/databases/BackupAndRestore/launcher_3_by_3.db differ diff --git a/tests/assets/databases/BackupAndRestore/launcher_4_by_4.db b/tests/assets/databases/BackupAndRestore/launcher_4_by_4.db new file mode 100644 index 0000000000..00061dddf4 Binary files /dev/null and b/tests/assets/databases/BackupAndRestore/launcher_4_by_4.db differ diff --git a/tests/assets/databases/BackupAndRestore/launcher_4_by_5.db b/tests/assets/databases/BackupAndRestore/launcher_4_by_5.db new file mode 100644 index 0000000000..e2e65aaf07 Binary files /dev/null and b/tests/assets/databases/BackupAndRestore/launcher_4_by_5.db differ diff --git a/tests/multivalentTests/src/com/android/launcher3/util/rule/BackAndRestoreRule.kt b/tests/multivalentTests/src/com/android/launcher3/util/rule/BackAndRestoreRule.kt new file mode 100644 index 0000000000..cb3550a351 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/util/rule/BackAndRestoreRule.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util.rule + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherPrefs +import java.io.File +import java.nio.file.Paths +import kotlin.io.path.pathString +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Removes all launcher's DBs from the device and copies the dbs in + * assets/databases/BackupAndRestore to the device. It also set's the needed LauncherPrefs variables + * needed to kickstart a backup and restore. + */ +class BackAndRestoreRule : TestRule { + + private val phoneContext = getInstrumentation().targetContext + + private fun isWorkspaceDatabase(rawFileName: String): Boolean { + val fileName = Paths.get(rawFileName).fileName.pathString + return fileName.startsWith("launcher") && fileName.endsWith(".db") + } + + fun getDatabaseFiles() = + File(phoneContext.dataDir.path, "/databases").listFiles().filter { + isWorkspaceDatabase(it.name) + } + + private fun deleteDBs() = getDatabaseFiles().forEach { it.delete() } + + /** + * Setting RESTORE_DEVICE would trigger a restore next time the Launcher starts, and we remove + * the widgets and apps ids to prevent issues when loading the database. + */ + private fun setRestoreConstants() { + LauncherPrefs.get(phoneContext) + .put(LauncherPrefs.RESTORE_DEVICE.to(InvariantDeviceProfile.TYPE_MULTI_DISPLAY)) + LauncherPrefs.get(phoneContext) + .remove(LauncherPrefs.OLD_APP_WIDGET_IDS, LauncherPrefs.APP_WIDGET_IDS) + } + + private fun uploadDatabase(dbName: String) { + val file = File(File(getInstrumentation().targetContext.dataDir, "/databases"), dbName) + file.writeBytes( + InstrumentationRegistry.getInstrumentation() + .context + .assets + .open("databases/BackupAndRestore/$dbName") + .readBytes() + ) + file.setWritable(true, false) + } + + fun before() { + setRestoreConstants() + deleteDBs() + uploadDatabase("launcher.db") + uploadDatabase("launcher_4_by_4.db") + uploadDatabase("launcher_4_by_5.db") + uploadDatabase("launcher_3_by_3.db") + } + + fun after() { + deleteDBs() + } + + override fun apply(base: Statement?, description: Description?): Statement = + object : Statement() { + override fun evaluate() { + before() + base?.evaluate() + after() + } + } +} diff --git a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt new file mode 100644 index 0000000000..a1aede8802 --- /dev/null +++ b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.backuprestore + +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.Flags +import com.android.launcher3.model.ModelDbController +import com.android.launcher3.util.Executors.MODEL_EXECUTOR +import com.android.launcher3.util.TestUtil +import com.android.launcher3.util.rule.BackAndRestoreRule +import com.android.launcher3.util.rule.setFlags +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Makes sure to test {@code RestoreDbTask#removeOldDBs}, we need to remove all the dbs that are not + * the last one used when we restore the device. + */ +@RunWith(AndroidJUnit4::class) +@MediumTest +class BackupAndRestoreDBSelectionTest { + + @JvmField @Rule var backAndRestoreRule = BackAndRestoreRule() + + @JvmField + @Rule + val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + @Before + fun setUp() { + setFlagsRule.setFlags(true, Flags.FLAG_NARROW_GRID_RESTORE) + } + + @Test + fun oldDatabasesNotPresentAfterRestore() { + val dbController = ModelDbController(getInstrumentation().targetContext) + dbController.tryMigrateDB(null) + TestUtil.runOnExecutorSync(MODEL_EXECUTOR) { + assert(backAndRestoreRule.getDatabaseFiles().size == 1) { + "There should only be one database after restoring, the last one used. Actual databases ${backAndRestoreRule.getDatabaseFiles()}" + } + } + } +}