Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions DEVDOC.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
## Local migration regression tests

With JDK 21 and the Android SDK configured (`ANDROID_HOME` or `local.properties`), run:

```sh
./gradlew testAospOmegaDebugUnitTest --tests '*GridSizeMigrationRegressionTest'
```

These tests live in `tests/unit/src`, outside the production `src` source root. They use
Robolectric API 35 and native, file-backed SQLite with the production migration/controller
implementation. Coverage includes SQLite rollback, retry/reopen, widget and folder preservation,
fresh database initialization, invalid/no-op migrations, and loader cancellation before defaults,
cleanup, binding, or loader-transaction commit. Application DI, logging, and the smartspace setting
are mocked; no device or launcher application startup is required. This suite does not replace
the existing device/Soong tests under `tests/src`.

## Development Progress

- [ ] Rebase on A13
Expand Down
15 changes: 15 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ extensions.configure<ApplicationExtension> {
}

sourceSets {
named("test") {
java.directories.add("tests/unit/src")
kotlin.directories.add("tests/unit/src")
}
named("main") {
java.directories.addAll(
listOf(
Expand Down Expand Up @@ -229,6 +233,15 @@ extensions.configure<ApplicationExtension> {
checkReleaseBuilds = false
disable += listOf("MissingTranslation", "ExtraTranslation")
}

testOptions {
unitTests.isIncludeAndroidResources = true
unitTests.all {
val scratchDir = layout.buildDirectory.dir("unitTestScratch").get().asFile
it.systemProperty("java.io.tmpdir", scratchDir.absolutePath)
it.doFirst { scratchDir.mkdirs() }
}
}
}
protobuf {
protoc {
Expand Down Expand Up @@ -356,6 +369,8 @@ dependencies {
protobuf(files("protos_overrides/"))

testImplementation(libs.junit)
testImplementation(libs.mockito.core)
testImplementation(libs.robolectric)
androidTestImplementation(libs.dexmaker.mockito)
androidTestImplementation(libs.junit.jupiter)
androidTestImplementation(libs.mockito.core)
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ junit = "4.13.2"
junit5 = "6.1.1"
truth = "1.4.5"
mockito = "5.23.0"
robolectric = "4.16.1"
dexmaker = "1.2"
androidx-annotation = "1.10.0"
androidx-test = "1.3.0"
Expand Down Expand Up @@ -137,6 +138,7 @@ dexmaker-mockito = { module = "com.google.dexmaker:dexmaker-mockito", version.re
junit = { module = "junit:junit", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
runner = { module = "com.android.support.test:runner", version.ref = "support-runner" }
rules = { module = "com.android.support.test:rules", version.ref = "support-rules" }
test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test" }
Expand Down
3 changes: 3 additions & 0 deletions res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@
<string name="permdesc_write_settings">Allows the app to change the settings and
shortcuts in home.</string>

<!-- Shown when a grid migration fails and loading stops to preserve the saved layout. -->
<string name="workspace_migration_failed">Couldn\'t migrate the home screen. Your saved layout has not been reset.</string>

<!-- Widgets: -->
<skip />

Expand Down
30 changes: 16 additions & 14 deletions src/com/android/launcher3/model/GridSizeMigrationDBController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ object GridSizeMigrationDBController {
idsInUse: List<Int>,
): Int {
var newId = -1
val c =
helper.writableDatabase.query(
helper.writableDatabase
.query(
srcTableName,
null,
LauncherSettings.Favorites._ID + " = '" + (entry?.id ?: id) + "'",
Expand All @@ -125,18 +125,20 @@ object GridSizeMigrationDBController {
null,
null,
)
while (c.moveToNext()) {
val values = ContentValues()
DatabaseUtils.cursorRowToContentValues(c, values)
entry?.updateContentValues(values)
?: values.put(LauncherSettings.Favorites.CONTAINER, folderId)
do {
newId = helper.generateNewItemId()
} while (idsInUse.contains(newId))
values.put(LauncherSettings.Favorites._ID, newId)
helper.writableDatabase.insert(destTableName, null, values)
}
c.close()
.use { c ->
check(c.moveToFirst()) {
"Missing migration entry ${entry?.id ?: id} in $srcTableName"
}
val values = ContentValues()
DatabaseUtils.cursorRowToContentValues(c, values)
entry?.updateContentValues(values)
?: values.put(LauncherSettings.Favorites.CONTAINER, folderId)
do {
newId = helper.generateNewItemId()
} while (idsInUse.contains(newId))
values.put(LauncherSettings.Favorites._ID, newId)
helper.writableDatabase.insertOrThrow(destTableName, null, values)
}
return newId
}

Expand Down
99 changes: 41 additions & 58 deletions src/com/android/launcher3/model/GridSizeMigrationLogic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ constructor(
target: DatabaseHelper,
source: SQLiteDatabase,
isDestNewDb: Boolean,
modelDelegate: ModelDelegate,
) {

if (!GridSizeMigrationDBController.needsToMigrate(srcDeviceState, destDeviceState)) {
Expand All @@ -82,86 +81,70 @@ constructor(
"$srcDeviceState\ndestDeviceState: $destDeviceState\nisDestNewDb: $isDestNewDb",
)

val shouldMigrateToStrtictlyTallerGrid =
val shouldMigrateToStrictlyTallerGrid =
shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)
if (shouldMigrateToStrtictlyTallerGrid) {
copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context)
} else {
copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context)
}
// Stage the source before the transaction: copyTable attaches and detaches its database.
// Never replace favorites outside the transaction, even for the taller-grid shortcut.
copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context)

val migrationStartTime = System.currentTimeMillis()
try {
SQLiteTransaction(target.writableDatabase).use { t ->
// We want to add the extra row(s) to the top of the screen, so we shift the grid
// down.
if (shouldMigrateToStrtictlyTallerGrid) {
if (shouldMigrateToStrictlyTallerGrid) {
Log.d(TAG, "Migrating to strictly taller grid")
copyTable(t.db, TMP_TABLE, t.db, TABLE_NAME, context)
if (Flags.oneGridSpecs()) {
shiftWorkspaceByXCells(
target.writableDatabase,
(destDeviceState.rows - srcDeviceState.rows),
TABLE_NAME,
)
}
// Save current configuration, so that the migration does not run again.
destDeviceState.writeToPrefs(context)
t.commit()

if (isOneGridMigration(srcDeviceState, destDeviceState)) {
statsLogManager.logger().log(LAUNCHER_ROW_SHIFT_ONE_GRID_MIGRATION)
}
statsLogManager.logger().log(LAUNCHER_ROW_SHIFT_GRID_MIGRATION)

return
} else {
val srcReader = DbReader(t.db, TMP_TABLE, context)
val destReader = DbReader(t.db, TABLE_NAME, context)

val targetSize = Point(destDeviceState.columns, destDeviceState.rows)

// Keep destination IDs so newly added items cannot collide with them.
val idsInUse = mutableListOf<Int>()

migrateHotseat(
srcDeviceState.numHotseat,
destDeviceState.numHotseat,
srcReader,
destReader,
target,
idsInUse,
)
migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse)
}

val srcReader = DbReader(t.db, TMP_TABLE, context)
val destReader = DbReader(t.db, TABLE_NAME, context)

val targetSize = Point(destDeviceState.columns, destDeviceState.rows)

// Here we keep all the DB ids we have in the destination DB such that we don't
// assign
// an item that we want to add to the destination DB the same id as an already
// existing
// item.
val idsInUse = mutableListOf<Int>()

// Migrate hotseat.
migrateHotseat(
srcDeviceState.numHotseat,
destDeviceState.numHotseat,
srcReader,
destReader,
target,
idsInUse,
)
// Migrate workspace.
migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse)

dropTable(t.db, TMP_TABLE)
t.commit()

if (isOneGridMigration(srcDeviceState, destDeviceState)) {
statsLogManager.logger().log(LAUNCHER_STANDARD_ONE_GRID_MIGRATION)
}
statsLogManager.logger().log(LAUNCHER_STANDARD_GRID_MIGRATION)
}
} catch (e: Exception) {
FileLog.e(TAG, "Error during grid migration", e)
} finally {
Log.v(
TAG,
"Workspace migration completed in " +
(System.currentTimeMillis() - migrationStartTime),
)

// Save current configuration, so that the migration does not run again.
destDeviceState.writeToPrefs(context)
throw e
}

// Notify if we've migrated successfully
modelDelegate.gridMigrationComplete(srcDeviceState, destDeviceState)
Log.v(
TAG,
"Workspace migration completed in " +
(System.currentTimeMillis() - migrationStartTime),
)
if (shouldMigrateToStrictlyTallerGrid) {
if (isOneGridMigration(srcDeviceState, destDeviceState)) {
statsLogManager.logger().log(LAUNCHER_ROW_SHIFT_ONE_GRID_MIGRATION)
}
statsLogManager.logger().log(LAUNCHER_ROW_SHIFT_GRID_MIGRATION)
} else {
if (isOneGridMigration(srcDeviceState, destDeviceState)) {
statsLogManager.logger().log(LAUNCHER_STANDARD_ONE_GRID_MIGRATION)
}
statsLogManager.logger().log(LAUNCHER_STANDARD_GRID_MIGRATION)
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/com/android/launcher3/model/LoaderTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static com.android.launcher3.model.data.AppsListData.FLAG_QUIET_MODE_ENABLED;
import static com.android.launcher3.model.data.AppsListData.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.LooperExecutor.CALLER_LOADER_TASK;
import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
Expand All @@ -48,6 +49,7 @@
import android.os.UserHandle;
import android.util.Log;
import android.util.LongSparseArray;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -58,6 +60,7 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
import com.android.launcher3.dagger.ApplicationContext;
Expand Down Expand Up @@ -450,6 +453,12 @@ private void loadWorkspaceImpl(
dbController.attemptMigrateDb(restoreEventLogger, mModelDelegate);
} catch (Exception e) {
FileLog.e(TAG, "Failed to migrate grid", e);
MAIN_EXECUTOR.execute(() -> Toast.makeText(
mContext, R.string.workspace_migration_failed, Toast.LENGTH_LONG).show());
CancellationException cancelled =
new CancellationException("Workspace loading cancelled after migration failure");
cancelled.initCause(e);
throw cancelled;
}

Log.d(TAG, "loadWorkspace: loading default favorites if necessary");
Expand Down
Loading