9cbc478574
Splitting allows easily making a fake that doesn't have to override the implementation details of LauncherPrefs. The fake should not deal with SharedPreferences. LauncherPrefs could be an interface, but then the companion object has more limitations. The solution there is to have a dedicated object class, e.g. `LauncherPrefItems`. I went with an abs class to make the refactor simpler. Flag: EXEMPT refactor Bug: 369641781 Test: go/testedequals Change-Id: I97a2d495d3b5fa892fa53a11fb3f7a7dfb98515b
418 lines
17 KiB
Java
418 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2019 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.provider;
|
|
|
|
import static android.os.Process.myUserHandle;
|
|
|
|
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
|
|
|
import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS;
|
|
import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS;
|
|
import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE;
|
|
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
|
|
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
|
|
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
|
|
import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
|
|
|
|
import static com.google.common.truth.Truth.assertThat;
|
|
|
|
import static org.junit.Assert.assertArrayEquals;
|
|
import static org.junit.Assert.assertEquals;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.eq;
|
|
import static org.mockito.Mockito.doReturn;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.spy;
|
|
import static org.mockito.Mockito.times;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
import android.app.backup.BackupManager;
|
|
import android.appwidget.AppWidgetHost;
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.database.Cursor;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.os.UserHandle;
|
|
import android.os.UserManager;
|
|
import android.util.LongSparseArray;
|
|
|
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
import androidx.test.filters.SmallTest;
|
|
|
|
import com.android.launcher3.LauncherAppState;
|
|
import com.android.launcher3.LauncherPrefs;
|
|
import com.android.launcher3.LauncherSettings;
|
|
import com.android.launcher3.LauncherSettings.Favorites;
|
|
import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
|
|
import com.android.launcher3.model.ModelDbController;
|
|
import com.android.launcher3.util.IntArray;
|
|
import com.android.launcher3.util.LauncherModelHelper;
|
|
|
|
import org.junit.After;
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.mockito.Mockito;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.stream.IntStream;
|
|
|
|
/**
|
|
* Tests for {@link RestoreDbTask}
|
|
*/
|
|
@SmallTest
|
|
@RunWith(AndroidJUnit4.class)
|
|
public class RestoreDbTaskTest {
|
|
|
|
private static final int PER_USER_RANGE = 200000;
|
|
|
|
private final UserHandle mWorkUser = UserHandle.getUserHandleForUid(PER_USER_RANGE);
|
|
|
|
private LauncherModelHelper mModelHelper;
|
|
private Context mContext;
|
|
private RestoreDbTask mTask;
|
|
private ModelDbController mMockController;
|
|
private SQLiteDatabase mMockDb;
|
|
private Cursor mMockCursor;
|
|
private LauncherPrefs mPrefs;
|
|
private LauncherRestoreEventLogger mMockRestoreEventLogger;
|
|
|
|
@Before
|
|
public void setup() {
|
|
mModelHelper = new LauncherModelHelper();
|
|
mContext = mModelHelper.sandboxContext;
|
|
mTask = new RestoreDbTask();
|
|
mMockController = Mockito.mock(ModelDbController.class);
|
|
mMockDb = mock(SQLiteDatabase.class);
|
|
mMockCursor = mock(Cursor.class);
|
|
mPrefs = LauncherPrefs.get(mContext);
|
|
mMockRestoreEventLogger = mock(LauncherRestoreEventLogger.class);
|
|
}
|
|
|
|
@After
|
|
public void teardown() {
|
|
mModelHelper.destroy();
|
|
LauncherPrefs.get(mContext).removeSync(RESTORE_DEVICE);
|
|
}
|
|
|
|
@Test
|
|
public void testGetProfileId() throws Exception {
|
|
SQLiteDatabase db = new MyModelDbController(23).getDb();
|
|
assertEquals(23, new RestoreDbTask().getDefaultProfileId(db));
|
|
}
|
|
|
|
@Test
|
|
public void testMigrateProfileId() throws Exception {
|
|
SQLiteDatabase db = new MyModelDbController(42).getDb();
|
|
// Add some mock data
|
|
for (int i = 0; i < 5; i++) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(Favorites._ID, i);
|
|
values.put(Favorites.TITLE, "item " + i);
|
|
db.insert(Favorites.TABLE_NAME, null, values);
|
|
}
|
|
// Verify item add
|
|
assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
|
|
|
|
new RestoreDbTask().migrateProfileId(db, 42, 33);
|
|
|
|
// verify data migrated
|
|
assertEquals(0, getCount(db, "select * from favorites where profileId = 42"));
|
|
assertEquals(5, getCount(db, "select * from favorites where profileId = 33"));
|
|
}
|
|
|
|
@Test
|
|
public void testChangeDefaultColumn() throws Exception {
|
|
SQLiteDatabase db = new MyModelDbController(42).getDb();
|
|
// Add some mock data
|
|
for (int i = 0; i < 5; i++) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(Favorites._ID, i);
|
|
values.put(Favorites.TITLE, "item " + i);
|
|
db.insert(Favorites.TABLE_NAME, null, values);
|
|
}
|
|
// Verify default column is 42
|
|
assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
|
|
|
|
new RestoreDbTask().changeDefaultColumn(db, 33);
|
|
|
|
// Verify default value changed
|
|
ContentValues values = new ContentValues();
|
|
values.put(Favorites._ID, 100);
|
|
values.put(Favorites.TITLE, "item 100");
|
|
db.insert(Favorites.TABLE_NAME, null, values);
|
|
assertEquals(1, getCount(db, "select * from favorites where profileId = 33"));
|
|
}
|
|
|
|
@Test
|
|
public void testSanitizeDB_bothProfiles() throws Exception {
|
|
UserHandle myUser = myUserHandle();
|
|
long myProfileId = mContext.getSystemService(UserManager.class)
|
|
.getSerialNumberForUser(myUser);
|
|
long myProfileId_old = myProfileId + 1;
|
|
long workProfileId = myProfileId + 2;
|
|
long workProfileId_old = myProfileId + 3;
|
|
|
|
MyModelDbController controller = new MyModelDbController(myProfileId);
|
|
SQLiteDatabase db = controller.getDb();
|
|
BackupManager bm = spy(new BackupManager(mContext));
|
|
doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old));
|
|
doReturn(mWorkUser).when(bm).getUserForAncestralSerialNumber(eq(workProfileId_old));
|
|
controller.users.put(workProfileId, mWorkUser);
|
|
|
|
addIconsBulk(controller, 10, 1, myProfileId_old);
|
|
addIconsBulk(controller, 6, 2, workProfileId_old);
|
|
assertEquals(10, getItemCountForProfile(db, myProfileId_old));
|
|
assertEquals(6, getItemCountForProfile(db, workProfileId_old));
|
|
|
|
mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger);
|
|
|
|
// All the data has been migrated to the new user ids
|
|
assertEquals(0, getItemCountForProfile(db, myProfileId_old));
|
|
assertEquals(0, getItemCountForProfile(db, workProfileId_old));
|
|
assertEquals(10, getItemCountForProfile(db, myProfileId));
|
|
assertEquals(6, getItemCountForProfile(db, workProfileId));
|
|
}
|
|
|
|
@Test
|
|
public void testSanitizeDB_workItemsRemoved() throws Exception {
|
|
UserHandle myUser = myUserHandle();
|
|
long myProfileId = mContext.getSystemService(UserManager.class)
|
|
.getSerialNumberForUser(myUser);
|
|
long myProfileId_old = myProfileId + 1;
|
|
long workProfileId_old = myProfileId + 3;
|
|
|
|
MyModelDbController controller = new MyModelDbController(myProfileId);
|
|
SQLiteDatabase db = controller.getDb();
|
|
BackupManager bm = spy(new BackupManager(mContext));
|
|
doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old));
|
|
// Work profile is not migrated
|
|
doReturn(null).when(bm).getUserForAncestralSerialNumber(eq(workProfileId_old));
|
|
|
|
addIconsBulk(controller, 10, 1, myProfileId_old);
|
|
addIconsBulk(controller, 6, 2, workProfileId_old);
|
|
assertEquals(10, getItemCountForProfile(db, myProfileId_old));
|
|
assertEquals(6, getItemCountForProfile(db, workProfileId_old));
|
|
|
|
mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger);
|
|
|
|
// All the data has been migrated to the new user ids
|
|
assertEquals(0, getItemCountForProfile(db, myProfileId_old));
|
|
assertEquals(0, getItemCountForProfile(db, workProfileId_old));
|
|
assertEquals(10, getItemCountForProfile(db, myProfileId));
|
|
assertEquals(10, getCount(db, "select * from favorites"));
|
|
}
|
|
|
|
@Test
|
|
public void givenLauncherPrefsHasNoIds_whenRestoreAppWidgetIdsIfExists_thenIdsAreRemoved() {
|
|
// When
|
|
mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger);
|
|
// Then
|
|
assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse();
|
|
}
|
|
|
|
@Test
|
|
public void givenNoPendingRestore_WhenRestoreAppWidgetIds_ThenRemoveNewWidgetIds() {
|
|
// Given
|
|
AppWidgetHost expectedHost = new AppWidgetHost(mContext, APPWIDGET_HOST_ID);
|
|
int[] expectedOldIds = generateOldWidgetIds(expectedHost);
|
|
int[] expectedNewIds = generateNewWidgetIds(expectedHost, expectedOldIds);
|
|
when(mMockController.getDb()).thenReturn(mMockDb);
|
|
mPrefs.remove(RESTORE_DEVICE);
|
|
|
|
// When
|
|
setRestoredAppWidgetIds(mContext, expectedOldIds, expectedNewIds);
|
|
mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger);
|
|
|
|
// Then
|
|
assertThat(expectedHost.getAppWidgetIds()).isEqualTo(expectedOldIds);
|
|
assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse();
|
|
// b/343530737
|
|
verifyNoMoreInteractions(mMockController);
|
|
}
|
|
|
|
@Test
|
|
public void givenRestoreWithNonExistingWidgets_WhenRestoreAppWidgetIds_ThenRemoveNewIds() {
|
|
// Given
|
|
AppWidgetHost expectedHost = new AppWidgetHost(mContext, APPWIDGET_HOST_ID);
|
|
int[] expectedOldIds = generateOldWidgetIds(expectedHost);
|
|
int[] expectedNewIds = generateNewWidgetIds(expectedHost, expectedOldIds);
|
|
when(mMockController.getDb()).thenReturn(mMockDb);
|
|
when(mMockDb.query(any(), any(), any(), any(), any(), any(), any())).thenReturn(
|
|
mMockCursor);
|
|
when(mMockCursor.moveToFirst()).thenReturn(false);
|
|
RestoreDbTask.setPending(mContext);
|
|
|
|
// When
|
|
setRestoredAppWidgetIds(mContext, expectedOldIds, expectedNewIds);
|
|
mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger);
|
|
|
|
// Then
|
|
assertThat(expectedHost.getAppWidgetIds()).isEqualTo(expectedOldIds);
|
|
assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse();
|
|
verify(mMockController, times(expectedOldIds.length)).update(any(), any(), any(), any());
|
|
}
|
|
|
|
@Test
|
|
public void givenRestore_WhenRestoreAppWidgetIds_ThenAddNewIds() {
|
|
// Given
|
|
AppWidgetHost expectedHost = new AppWidgetHost(mContext, APPWIDGET_HOST_ID);
|
|
int[] expectedOldIds = generateOldWidgetIds(expectedHost);
|
|
int[] expectedNewIds = generateNewWidgetIds(expectedHost, expectedOldIds);
|
|
int[] allExpectedIds = IntStream.concat(
|
|
Arrays.stream(expectedOldIds),
|
|
Arrays.stream(expectedNewIds)
|
|
).toArray();
|
|
|
|
when(mMockController.getDb()).thenReturn(mMockDb);
|
|
when(mMockDb.query(any(), any(), any(), any(), any(), any(), any()))
|
|
.thenReturn(mMockCursor);
|
|
when(mMockCursor.moveToFirst()).thenReturn(true);
|
|
when(mMockCursor.getColumnNames()).thenReturn(new String[] {});
|
|
when(mMockCursor.isAfterLast()).thenReturn(true);
|
|
RestoreDbTask.setPending(mContext);
|
|
|
|
// When
|
|
setRestoredAppWidgetIds(mContext, expectedOldIds, expectedNewIds);
|
|
mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger);
|
|
|
|
// Then
|
|
assertThat(expectedHost.getAppWidgetIds()).isEqualTo(allExpectedIds);
|
|
assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse();
|
|
verify(mMockController, times(expectedOldIds.length)).update(any(), any(), any(), any());
|
|
}
|
|
|
|
private void addIconsBulk(MyModelDbController controller,
|
|
int count, int screen, long profileId) {
|
|
int columns = LauncherAppState.getIDP(mContext).numColumns;
|
|
String packageName = getInstrumentation().getContext().getPackageName();
|
|
for (int i = 0; i < count; i++) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(LauncherSettings.Favorites._ID, controller.generateNewItemId());
|
|
values.put(LauncherSettings.Favorites.CONTAINER, CONTAINER_DESKTOP);
|
|
values.put(LauncherSettings.Favorites.SCREEN, screen);
|
|
values.put(LauncherSettings.Favorites.CELLX, i % columns);
|
|
values.put(LauncherSettings.Favorites.CELLY, i / columns);
|
|
values.put(LauncherSettings.Favorites.SPANX, 1);
|
|
values.put(LauncherSettings.Favorites.SPANY, 1);
|
|
values.put(LauncherSettings.Favorites.PROFILE_ID, profileId);
|
|
values.put(LauncherSettings.Favorites.ITEM_TYPE, ITEM_TYPE_APPLICATION);
|
|
values.put(LauncherSettings.Favorites.INTENT,
|
|
new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0));
|
|
|
|
controller.insert(TABLE_NAME, values);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void testRemoveScreenIdGaps_firstScreenEmpty() {
|
|
runRemoveScreenIdGapsTest(
|
|
new int[]{1, 2, 5, 6, 6, 7, 9, 9},
|
|
new int[]{1, 2, 3, 4, 4, 5, 6, 6});
|
|
}
|
|
|
|
@Test
|
|
public void testRemoveScreenIdGaps_firstScreenOccupied() {
|
|
runRemoveScreenIdGapsTest(
|
|
new int[]{0, 2, 5, 6, 6, 7, 9, 9},
|
|
new int[]{0, 1, 2, 3, 3, 4, 5, 5});
|
|
}
|
|
|
|
@Test
|
|
public void testRemoveScreenIdGaps_noGap() {
|
|
runRemoveScreenIdGapsTest(
|
|
new int[]{0, 1, 1, 2, 3, 3, 4, 5},
|
|
new int[]{0, 1, 1, 2, 3, 3, 4, 5});
|
|
}
|
|
|
|
private void runRemoveScreenIdGapsTest(int[] screenIds, int[] expectedScreenIds) {
|
|
SQLiteDatabase db = new MyModelDbController(42).getDb();
|
|
// Add some mock data
|
|
for (int i = 0; i < screenIds.length; i++) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(Favorites._ID, i);
|
|
values.put(Favorites.SCREEN, screenIds[i]);
|
|
values.put(Favorites.CONTAINER, CONTAINER_DESKTOP);
|
|
db.insert(Favorites.TABLE_NAME, null, values);
|
|
}
|
|
// Verify items are added
|
|
assertEquals(screenIds.length,
|
|
getCount(db, "select * from favorites where container = -100"));
|
|
|
|
new RestoreDbTask().removeScreenIdGaps(db);
|
|
|
|
// verify screenId gaps removed
|
|
int[] resultScreenIds = new int[screenIds.length];
|
|
try (Cursor c = db.rawQuery(
|
|
"select screen from favorites where container = -100 order by screen", null)) {
|
|
int i = 0;
|
|
while (c.moveToNext()) {
|
|
resultScreenIds[i++] = c.getInt(0);
|
|
}
|
|
}
|
|
|
|
assertArrayEquals(expectedScreenIds, resultScreenIds);
|
|
}
|
|
|
|
public int getItemCountForProfile(SQLiteDatabase db, long profileId) {
|
|
return getCount(db, "select * from favorites where profileId = " + profileId);
|
|
}
|
|
|
|
private int getCount(SQLiteDatabase db, String sql) {
|
|
try (Cursor c = db.rawQuery(sql, null)) {
|
|
return c.getCount();
|
|
}
|
|
}
|
|
|
|
private int[] generateOldWidgetIds(AppWidgetHost host) {
|
|
// generate some widget ids in case there are none
|
|
host.allocateAppWidgetId();
|
|
host.allocateAppWidgetId();
|
|
return host.getAppWidgetIds();
|
|
}
|
|
|
|
private int[] generateNewWidgetIds(AppWidgetHost host, int[] oldWidgetIds) {
|
|
// map as many new ids as old ids
|
|
return Arrays.stream(oldWidgetIds)
|
|
.map(id -> host.allocateAppWidgetId()).toArray();
|
|
}
|
|
|
|
private class MyModelDbController extends ModelDbController {
|
|
|
|
public final LongSparseArray<UserHandle> users = new LongSparseArray<>();
|
|
|
|
MyModelDbController(long profileId) {
|
|
super(mContext);
|
|
users.put(profileId, myUserHandle());
|
|
}
|
|
|
|
@Override
|
|
public long getSerialNumberForUser(UserHandle user) {
|
|
int index = users.indexOfValue(user);
|
|
return index >= 0 ? users.keyAt(index) : -1;
|
|
}
|
|
}
|
|
|
|
private void setRestoredAppWidgetIds(Context context, int[] oldIds, int[] newIds) {
|
|
LauncherPrefs.get(context).putSync(
|
|
OLD_APP_WIDGET_IDS.to(IntArray.wrap(oldIds).toConcatString()),
|
|
APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString()));
|
|
}
|
|
}
|