1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.provider; 17 18 import static android.os.Process.myUserHandle; 19 20 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 21 22 import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS; 23 import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS; 24 import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE; 25 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; 26 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 27 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; 28 import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID; 29 30 import static com.google.common.truth.Truth.assertThat; 31 32 import static org.junit.Assert.assertArrayEquals; 33 import static org.junit.Assert.assertEquals; 34 import static org.mockito.ArgumentMatchers.any; 35 import static org.mockito.ArgumentMatchers.eq; 36 import static org.mockito.Mockito.doReturn; 37 import static org.mockito.Mockito.mock; 38 import static org.mockito.Mockito.spy; 39 import static org.mockito.Mockito.times; 40 import static org.mockito.Mockito.verify; 41 import static org.mockito.Mockito.verifyNoMoreInteractions; 42 import static org.mockito.Mockito.when; 43 44 import android.app.backup.BackupManager; 45 import android.appwidget.AppWidgetHost; 46 import android.content.ContentValues; 47 import android.content.Context; 48 import android.content.Intent; 49 import android.database.Cursor; 50 import android.database.sqlite.SQLiteDatabase; 51 import android.os.UserHandle; 52 import android.os.UserManager; 53 import android.util.LongSparseArray; 54 55 import androidx.test.ext.junit.runners.AndroidJUnit4; 56 import androidx.test.filters.SmallTest; 57 58 import com.android.launcher3.LauncherAppState; 59 import com.android.launcher3.LauncherPrefs; 60 import com.android.launcher3.LauncherSettings; 61 import com.android.launcher3.LauncherSettings.Favorites; 62 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger; 63 import com.android.launcher3.model.ModelDbController; 64 import com.android.launcher3.util.IntArray; 65 import com.android.launcher3.util.LauncherModelHelper; 66 67 import org.junit.After; 68 import org.junit.Before; 69 import org.junit.Test; 70 import org.junit.runner.RunWith; 71 import org.mockito.Mockito; 72 73 import java.util.Arrays; 74 import java.util.stream.IntStream; 75 76 /** 77 * Tests for {@link RestoreDbTask} 78 */ 79 @SmallTest 80 @RunWith(AndroidJUnit4.class) 81 public class RestoreDbTaskTest { 82 83 private static final int PER_USER_RANGE = 200000; 84 85 private final UserHandle mWorkUser = UserHandle.getUserHandleForUid(PER_USER_RANGE); 86 87 private LauncherModelHelper mModelHelper; 88 private Context mContext; 89 private RestoreDbTask mTask; 90 private ModelDbController mMockController; 91 private SQLiteDatabase mMockDb; 92 private Cursor mMockCursor; 93 private LauncherPrefs mPrefs; 94 private LauncherRestoreEventLogger mMockRestoreEventLogger; 95 96 @Before setup()97 public void setup() { 98 mModelHelper = new LauncherModelHelper(); 99 mContext = mModelHelper.sandboxContext; 100 mTask = new RestoreDbTask(); 101 mMockController = Mockito.mock(ModelDbController.class); 102 mMockDb = mock(SQLiteDatabase.class); 103 mMockCursor = mock(Cursor.class); 104 mPrefs = new LauncherPrefs(mContext); 105 mMockRestoreEventLogger = mock(LauncherRestoreEventLogger.class); 106 } 107 108 @After teardown()109 public void teardown() { 110 mModelHelper.destroy(); 111 LauncherPrefs.get(mContext).removeSync(RESTORE_DEVICE); 112 } 113 114 @Test testGetProfileId()115 public void testGetProfileId() throws Exception { 116 SQLiteDatabase db = new MyModelDbController(23).getDb(); 117 assertEquals(23, new RestoreDbTask().getDefaultProfileId(db)); 118 } 119 120 @Test testMigrateProfileId()121 public void testMigrateProfileId() throws Exception { 122 SQLiteDatabase db = new MyModelDbController(42).getDb(); 123 // Add some mock data 124 for (int i = 0; i < 5; i++) { 125 ContentValues values = new ContentValues(); 126 values.put(Favorites._ID, i); 127 values.put(Favorites.TITLE, "item " + i); 128 db.insert(Favorites.TABLE_NAME, null, values); 129 } 130 // Verify item add 131 assertEquals(5, getCount(db, "select * from favorites where profileId = 42")); 132 133 new RestoreDbTask().migrateProfileId(db, 42, 33); 134 135 // verify data migrated 136 assertEquals(0, getCount(db, "select * from favorites where profileId = 42")); 137 assertEquals(5, getCount(db, "select * from favorites where profileId = 33")); 138 } 139 140 @Test testChangeDefaultColumn()141 public void testChangeDefaultColumn() throws Exception { 142 SQLiteDatabase db = new MyModelDbController(42).getDb(); 143 // Add some mock data 144 for (int i = 0; i < 5; i++) { 145 ContentValues values = new ContentValues(); 146 values.put(Favorites._ID, i); 147 values.put(Favorites.TITLE, "item " + i); 148 db.insert(Favorites.TABLE_NAME, null, values); 149 } 150 // Verify default column is 42 151 assertEquals(5, getCount(db, "select * from favorites where profileId = 42")); 152 153 new RestoreDbTask().changeDefaultColumn(db, 33); 154 155 // Verify default value changed 156 ContentValues values = new ContentValues(); 157 values.put(Favorites._ID, 100); 158 values.put(Favorites.TITLE, "item 100"); 159 db.insert(Favorites.TABLE_NAME, null, values); 160 assertEquals(1, getCount(db, "select * from favorites where profileId = 33")); 161 } 162 163 @Test testSanitizeDB_bothProfiles()164 public void testSanitizeDB_bothProfiles() throws Exception { 165 UserHandle myUser = myUserHandle(); 166 long myProfileId = mContext.getSystemService(UserManager.class) 167 .getSerialNumberForUser(myUser); 168 long myProfileId_old = myProfileId + 1; 169 long workProfileId = myProfileId + 2; 170 long workProfileId_old = myProfileId + 3; 171 172 MyModelDbController controller = new MyModelDbController(myProfileId); 173 SQLiteDatabase db = controller.getDb(); 174 BackupManager bm = spy(new BackupManager(mContext)); 175 doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old)); 176 doReturn(mWorkUser).when(bm).getUserForAncestralSerialNumber(eq(workProfileId_old)); 177 controller.users.put(workProfileId, mWorkUser); 178 179 addIconsBulk(controller, 10, 1, myProfileId_old); 180 addIconsBulk(controller, 6, 2, workProfileId_old); 181 assertEquals(10, getItemCountForProfile(db, myProfileId_old)); 182 assertEquals(6, getItemCountForProfile(db, workProfileId_old)); 183 184 mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger); 185 186 // All the data has been migrated to the new user ids 187 assertEquals(0, getItemCountForProfile(db, myProfileId_old)); 188 assertEquals(0, getItemCountForProfile(db, workProfileId_old)); 189 assertEquals(10, getItemCountForProfile(db, myProfileId)); 190 assertEquals(6, getItemCountForProfile(db, workProfileId)); 191 } 192 193 @Test testSanitizeDB_workItemsRemoved()194 public void testSanitizeDB_workItemsRemoved() throws Exception { 195 UserHandle myUser = myUserHandle(); 196 long myProfileId = mContext.getSystemService(UserManager.class) 197 .getSerialNumberForUser(myUser); 198 long myProfileId_old = myProfileId + 1; 199 long workProfileId_old = myProfileId + 3; 200 201 MyModelDbController controller = new MyModelDbController(myProfileId); 202 SQLiteDatabase db = controller.getDb(); 203 BackupManager bm = spy(new BackupManager(mContext)); 204 doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old)); 205 // Work profile is not migrated 206 doReturn(null).when(bm).getUserForAncestralSerialNumber(eq(workProfileId_old)); 207 208 addIconsBulk(controller, 10, 1, myProfileId_old); 209 addIconsBulk(controller, 6, 2, workProfileId_old); 210 assertEquals(10, getItemCountForProfile(db, myProfileId_old)); 211 assertEquals(6, getItemCountForProfile(db, workProfileId_old)); 212 213 mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger); 214 215 // All the data has been migrated to the new user ids 216 assertEquals(0, getItemCountForProfile(db, myProfileId_old)); 217 assertEquals(0, getItemCountForProfile(db, workProfileId_old)); 218 assertEquals(10, getItemCountForProfile(db, myProfileId)); 219 assertEquals(10, getCount(db, "select * from favorites")); 220 } 221 222 @Test givenLauncherPrefsHasNoIds_whenRestoreAppWidgetIdsIfExists_thenIdsAreRemoved()223 public void givenLauncherPrefsHasNoIds_whenRestoreAppWidgetIdsIfExists_thenIdsAreRemoved() { 224 // When 225 mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger); 226 // Then 227 assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse(); 228 } 229 230 @Test givenNoPendingRestore_WhenRestoreAppWidgetIds_ThenRemoveNewWidgetIds()231 public void givenNoPendingRestore_WhenRestoreAppWidgetIds_ThenRemoveNewWidgetIds() { 232 // Given 233 AppWidgetHost expectedHost = new AppWidgetHost(mContext, APPWIDGET_HOST_ID); 234 int[] expectedOldIds = generateOldWidgetIds(expectedHost); 235 int[] expectedNewIds = generateNewWidgetIds(expectedHost, expectedOldIds); 236 when(mMockController.getDb()).thenReturn(mMockDb); 237 mPrefs.remove(RESTORE_DEVICE); 238 239 // When 240 setRestoredAppWidgetIds(mContext, expectedOldIds, expectedNewIds); 241 mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger); 242 243 // Then 244 assertThat(expectedHost.getAppWidgetIds()).isEqualTo(expectedOldIds); 245 assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse(); 246 // b/343530737 247 verifyNoMoreInteractions(mMockController); 248 } 249 250 @Test givenRestoreWithNonExistingWidgets_WhenRestoreAppWidgetIds_ThenRemoveNewIds()251 public void givenRestoreWithNonExistingWidgets_WhenRestoreAppWidgetIds_ThenRemoveNewIds() { 252 // Given 253 AppWidgetHost expectedHost = new AppWidgetHost(mContext, APPWIDGET_HOST_ID); 254 int[] expectedOldIds = generateOldWidgetIds(expectedHost); 255 int[] expectedNewIds = generateNewWidgetIds(expectedHost, expectedOldIds); 256 when(mMockController.getDb()).thenReturn(mMockDb); 257 when(mMockDb.query(any(), any(), any(), any(), any(), any(), any())).thenReturn( 258 mMockCursor); 259 when(mMockCursor.moveToFirst()).thenReturn(false); 260 RestoreDbTask.setPending(mContext); 261 262 // When 263 setRestoredAppWidgetIds(mContext, expectedOldIds, expectedNewIds); 264 mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger); 265 266 // Then 267 assertThat(expectedHost.getAppWidgetIds()).isEqualTo(expectedOldIds); 268 assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse(); 269 verify(mMockController, times(expectedOldIds.length)).update(any(), any(), any(), any()); 270 } 271 272 @Test givenRestore_WhenRestoreAppWidgetIds_ThenAddNewIds()273 public void givenRestore_WhenRestoreAppWidgetIds_ThenAddNewIds() { 274 // Given 275 AppWidgetHost expectedHost = new AppWidgetHost(mContext, APPWIDGET_HOST_ID); 276 int[] expectedOldIds = generateOldWidgetIds(expectedHost); 277 int[] expectedNewIds = generateNewWidgetIds(expectedHost, expectedOldIds); 278 int[] allExpectedIds = IntStream.concat( 279 Arrays.stream(expectedOldIds), 280 Arrays.stream(expectedNewIds) 281 ).toArray(); 282 283 when(mMockController.getDb()).thenReturn(mMockDb); 284 when(mMockDb.query(any(), any(), any(), any(), any(), any(), any())) 285 .thenReturn(mMockCursor); 286 when(mMockCursor.moveToFirst()).thenReturn(true); 287 when(mMockCursor.getColumnNames()).thenReturn(new String[] {}); 288 when(mMockCursor.isAfterLast()).thenReturn(true); 289 RestoreDbTask.setPending(mContext); 290 291 // When 292 setRestoredAppWidgetIds(mContext, expectedOldIds, expectedNewIds); 293 mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger); 294 295 // Then 296 assertThat(expectedHost.getAppWidgetIds()).isEqualTo(allExpectedIds); 297 assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse(); 298 verify(mMockController, times(expectedOldIds.length)).update(any(), any(), any(), any()); 299 } 300 addIconsBulk(MyModelDbController controller, int count, int screen, long profileId)301 private void addIconsBulk(MyModelDbController controller, 302 int count, int screen, long profileId) { 303 int columns = LauncherAppState.getIDP(mContext).numColumns; 304 String packageName = getInstrumentation().getContext().getPackageName(); 305 for (int i = 0; i < count; i++) { 306 ContentValues values = new ContentValues(); 307 values.put(LauncherSettings.Favorites._ID, controller.generateNewItemId()); 308 values.put(LauncherSettings.Favorites.CONTAINER, CONTAINER_DESKTOP); 309 values.put(LauncherSettings.Favorites.SCREEN, screen); 310 values.put(LauncherSettings.Favorites.CELLX, i % columns); 311 values.put(LauncherSettings.Favorites.CELLY, i / columns); 312 values.put(LauncherSettings.Favorites.SPANX, 1); 313 values.put(LauncherSettings.Favorites.SPANY, 1); 314 values.put(LauncherSettings.Favorites.PROFILE_ID, profileId); 315 values.put(LauncherSettings.Favorites.ITEM_TYPE, ITEM_TYPE_APPLICATION); 316 values.put(LauncherSettings.Favorites.INTENT, 317 new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0)); 318 319 controller.insert(TABLE_NAME, values); 320 } 321 } 322 323 @Test testRemoveScreenIdGaps_firstScreenEmpty()324 public void testRemoveScreenIdGaps_firstScreenEmpty() { 325 runRemoveScreenIdGapsTest( 326 new int[]{1, 2, 5, 6, 6, 7, 9, 9}, 327 new int[]{1, 2, 3, 4, 4, 5, 6, 6}); 328 } 329 330 @Test testRemoveScreenIdGaps_firstScreenOccupied()331 public void testRemoveScreenIdGaps_firstScreenOccupied() { 332 runRemoveScreenIdGapsTest( 333 new int[]{0, 2, 5, 6, 6, 7, 9, 9}, 334 new int[]{0, 1, 2, 3, 3, 4, 5, 5}); 335 } 336 337 @Test testRemoveScreenIdGaps_noGap()338 public void testRemoveScreenIdGaps_noGap() { 339 runRemoveScreenIdGapsTest( 340 new int[]{0, 1, 1, 2, 3, 3, 4, 5}, 341 new int[]{0, 1, 1, 2, 3, 3, 4, 5}); 342 } 343 runRemoveScreenIdGapsTest(int[] screenIds, int[] expectedScreenIds)344 private void runRemoveScreenIdGapsTest(int[] screenIds, int[] expectedScreenIds) { 345 SQLiteDatabase db = new MyModelDbController(42).getDb(); 346 // Add some mock data 347 for (int i = 0; i < screenIds.length; i++) { 348 ContentValues values = new ContentValues(); 349 values.put(Favorites._ID, i); 350 values.put(Favorites.SCREEN, screenIds[i]); 351 values.put(Favorites.CONTAINER, CONTAINER_DESKTOP); 352 db.insert(Favorites.TABLE_NAME, null, values); 353 } 354 // Verify items are added 355 assertEquals(screenIds.length, 356 getCount(db, "select * from favorites where container = -100")); 357 358 new RestoreDbTask().removeScreenIdGaps(db); 359 360 // verify screenId gaps removed 361 int[] resultScreenIds = new int[screenIds.length]; 362 try (Cursor c = db.rawQuery( 363 "select screen from favorites where container = -100 order by screen", null)) { 364 int i = 0; 365 while (c.moveToNext()) { 366 resultScreenIds[i++] = c.getInt(0); 367 } 368 } 369 370 assertArrayEquals(expectedScreenIds, resultScreenIds); 371 } 372 getItemCountForProfile(SQLiteDatabase db, long profileId)373 public int getItemCountForProfile(SQLiteDatabase db, long profileId) { 374 return getCount(db, "select * from favorites where profileId = " + profileId); 375 } 376 getCount(SQLiteDatabase db, String sql)377 private int getCount(SQLiteDatabase db, String sql) { 378 try (Cursor c = db.rawQuery(sql, null)) { 379 return c.getCount(); 380 } 381 } 382 generateOldWidgetIds(AppWidgetHost host)383 private int[] generateOldWidgetIds(AppWidgetHost host) { 384 // generate some widget ids in case there are none 385 host.allocateAppWidgetId(); 386 host.allocateAppWidgetId(); 387 return host.getAppWidgetIds(); 388 } 389 generateNewWidgetIds(AppWidgetHost host, int[] oldWidgetIds)390 private int[] generateNewWidgetIds(AppWidgetHost host, int[] oldWidgetIds) { 391 // map as many new ids as old ids 392 return Arrays.stream(oldWidgetIds) 393 .map(id -> host.allocateAppWidgetId()).toArray(); 394 } 395 396 private class MyModelDbController extends ModelDbController { 397 398 public final LongSparseArray<UserHandle> users = new LongSparseArray<>(); 399 MyModelDbController(long profileId)400 MyModelDbController(long profileId) { 401 super(mContext); 402 users.put(profileId, myUserHandle()); 403 } 404 405 @Override getSerialNumberForUser(UserHandle user)406 public long getSerialNumberForUser(UserHandle user) { 407 int index = users.indexOfValue(user); 408 return index >= 0 ? users.keyAt(index) : -1; 409 } 410 } 411 setRestoredAppWidgetIds(Context context, int[] oldIds, int[] newIds)412 private void setRestoredAppWidgetIds(Context context, int[] oldIds, int[] newIds) { 413 LauncherPrefs.get(context).putSync( 414 OLD_APP_WIDGET_IDS.to(IntArray.wrap(oldIds).toConcatString()), 415 APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString())); 416 } 417 } 418