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