1 /*
2  * Copyright (C) 2016 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 
17 package com.android.launcher3.provider;
18 
19 import android.content.ContentProviderOperation;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.SharedPreferences;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ProviderInfo;
27 import android.database.Cursor;
28 import android.database.sqlite.SQLiteDatabase;
29 import android.net.Uri;
30 import android.os.Process;
31 import android.text.TextUtils;
32 import android.util.LongSparseArray;
33 import android.util.SparseBooleanArray;
34 
35 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
36 import com.android.launcher3.DefaultLayoutParser;
37 import com.android.launcher3.LauncherAppState;
38 import com.android.launcher3.LauncherAppWidgetInfo;
39 import com.android.launcher3.LauncherFiles;
40 import com.android.launcher3.LauncherSettings;
41 import com.android.launcher3.LauncherSettings.Favorites;
42 import com.android.launcher3.LauncherSettings.Settings;
43 import com.android.launcher3.LauncherSettings.WorkspaceScreens;
44 import com.android.launcher3.R;
45 import com.android.launcher3.Utilities;
46 import com.android.launcher3.Workspace;
47 import com.android.launcher3.compat.UserManagerCompat;
48 import com.android.launcher3.config.FeatureFlags;
49 import com.android.launcher3.config.ProviderConfig;
50 import com.android.launcher3.logging.FileLog;
51 import com.android.launcher3.model.GridSizeMigrationTask;
52 import com.android.launcher3.util.LongArrayMap;
53 
54 import java.net.URISyntaxException;
55 import java.util.ArrayList;
56 import java.util.HashMap;
57 import java.util.HashSet;
58 
59 /**
60  * Utility class to import data from another Launcher which is based on Launcher3 schema.
61  */
62 public class ImportDataTask {
63 
64     public static final String KEY_DATA_IMPORT_SRC_PKG = "data_import_src_pkg";
65     public static final String KEY_DATA_IMPORT_SRC_AUTHORITY = "data_import_src_authority";
66 
67     private static final String TAG = "ImportDataTask";
68     private static final int MIN_ITEM_COUNT_FOR_SUCCESSFUL_MIGRATION = 6;
69     // Insert items progressively to avoid OOM exception when loading icons.
70     private static final int BATCH_INSERT_SIZE = 15;
71 
72     private final Context mContext;
73 
74     private final Uri mOtherScreensUri;
75     private final Uri mOtherFavoritesUri;
76 
77     private int mHotseatSize;
78     private int mMaxGridSizeX;
79     private int mMaxGridSizeY;
80 
ImportDataTask(Context context, String sourceAuthority)81     private ImportDataTask(Context context, String sourceAuthority) {
82         mContext = context;
83         mOtherScreensUri = Uri.parse("content://" +
84                 sourceAuthority + "/" + WorkspaceScreens.TABLE_NAME);
85         mOtherFavoritesUri = Uri.parse("content://" + sourceAuthority + "/" + Favorites.TABLE_NAME);
86     }
87 
importWorkspace()88     public boolean importWorkspace() throws Exception {
89         ArrayList<Long> allScreens = LauncherDbUtils.getScreenIdsFromCursor(
90                 mContext.getContentResolver().query(mOtherScreensUri, null, null, null,
91                         LauncherSettings.WorkspaceScreens.SCREEN_RANK));
92         FileLog.d(TAG, "Importing DB from " + mOtherFavoritesUri);
93 
94         // During import we reset the screen IDs to 0-indexed values.
95         if (allScreens.isEmpty()) {
96             // No thing to migrate
97             FileLog.e(TAG, "No data found to import");
98             return false;
99         }
100 
101         mHotseatSize = mMaxGridSizeX = mMaxGridSizeY = 0;
102 
103         // Build screen update
104         ArrayList<ContentProviderOperation> screenOps = new ArrayList<>();
105         int count = allScreens.size();
106         LongSparseArray<Long> screenIdMap = new LongSparseArray<>(count);
107         for (int i = 0; i < count; i++) {
108             ContentValues v = new ContentValues();
109             v.put(LauncherSettings.WorkspaceScreens._ID, i);
110             v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
111             screenIdMap.put(allScreens.get(i), (long) i);
112             screenOps.add(ContentProviderOperation.newInsert(
113                     LauncherSettings.WorkspaceScreens.CONTENT_URI).withValues(v).build());
114         }
115         mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY, screenOps);
116         importWorkspaceItems(allScreens.get(0), screenIdMap);
117 
118         GridSizeMigrationTask.markForMigration(mContext, mMaxGridSizeX, mMaxGridSizeY, mHotseatSize);
119 
120         // Create empty DB flag.
121         LauncherSettings.Settings.call(mContext.getContentResolver(),
122                 LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
123         return true;
124     }
125 
126     /**
127      * 1) Imports all the workspace entries from the source provider.
128      * 2) For home screen entries, maps the screen id based on {@param screenIdMap}
129      * 3) In the end fills any holes in hotseat with items from default hotseat layout.
130      */
importWorkspaceItems( long firsetScreenId, LongSparseArray<Long> screenIdMap)131     private void importWorkspaceItems(
132             long firsetScreenId, LongSparseArray<Long> screenIdMap) throws Exception {
133         String profileId = Long.toString(UserManagerCompat.getInstance(mContext)
134                 .getSerialNumberForUser(Process.myUserHandle()));
135 
136         boolean createEmptyRowOnFirstScreen = false;
137         if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
138             try (Cursor c = mContext.getContentResolver().query(mOtherFavoritesUri, null,
139                     // get items on the first row of the first screen
140                     "profileId = ? AND container = -100 AND screen = ? AND cellY = 0",
141                     new String[]{profileId, Long.toString(firsetScreenId)},
142                     null)) {
143                 // First row of first screen is not empty
144                 createEmptyRowOnFirstScreen = c.moveToNext();
145             }
146         }
147 
148         ArrayList<ContentProviderOperation> insertOperations = new ArrayList<>(BATCH_INSERT_SIZE);
149 
150         // Set of package names present in hotseat
151         final HashSet<String> hotseatTargetApps = new HashSet<>();
152         int maxId = 0;
153 
154         // Number of imported items on workspace and hotseat
155         int totalItemsOnWorkspace = 0;
156 
157         try (Cursor c = mContext.getContentResolver()
158                 .query(mOtherFavoritesUri, null,
159                         // Only migrate the primary user
160                         Favorites.PROFILE_ID + " = ?", new String[]{profileId},
161                         // Get the items sorted by container, so that the folders are loaded
162                         // before the corresponding items.
163                         Favorites.CONTAINER)) {
164 
165             // various columns we expect to exist.
166             final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
167             final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
168             final int titleIndex = c.getColumnIndexOrThrow(Favorites.TITLE);
169             final int containerIndex = c.getColumnIndexOrThrow(Favorites.CONTAINER);
170             final int itemTypeIndex = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
171             final int widgetProviderIndex = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER);
172             final int screenIndex = c.getColumnIndexOrThrow(Favorites.SCREEN);
173             final int cellXIndex = c.getColumnIndexOrThrow(Favorites.CELLX);
174             final int cellYIndex = c.getColumnIndexOrThrow(Favorites.CELLY);
175             final int spanXIndex = c.getColumnIndexOrThrow(Favorites.SPANX);
176             final int spanYIndex = c.getColumnIndexOrThrow(Favorites.SPANY);
177             final int rankIndex = c.getColumnIndexOrThrow(Favorites.RANK);
178             final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON);
179             final int iconPackageIndex = c.getColumnIndexOrThrow(Favorites.ICON_PACKAGE);
180             final int iconResourceIndex = c.getColumnIndexOrThrow(Favorites.ICON_RESOURCE);
181 
182             SparseBooleanArray mValidFolders = new SparseBooleanArray();
183             ContentValues values = new ContentValues();
184 
185             while (c.moveToNext()) {
186                 values.clear();
187                 int id = c.getInt(idIndex);
188                 maxId = Math.max(maxId, id);
189                 int type = c.getInt(itemTypeIndex);
190                 int container = c.getInt(containerIndex);
191 
192                 long screen = c.getLong(screenIndex);
193 
194                 int cellX = c.getInt(cellXIndex);
195                 int cellY = c.getInt(cellYIndex);
196                 int spanX = c.getInt(spanXIndex);
197                 int spanY = c.getInt(spanYIndex);
198 
199                 switch (container) {
200                     case Favorites.CONTAINER_DESKTOP: {
201                         Long newScreenId = screenIdMap.get(screen);
202                         if (newScreenId == null) {
203                             FileLog.d(TAG, String.format("Skipping item %d, type %d not on a valid screen %d", id, type, screen));
204                             continue;
205                         }
206                         // Reset the screen to 0-index value
207                         screen = newScreenId;
208                         if (createEmptyRowOnFirstScreen && screen == Workspace.FIRST_SCREEN_ID) {
209                             // Shift items by 1.
210                             cellY++;
211                         }
212 
213                         mMaxGridSizeX = Math.max(mMaxGridSizeX, cellX + spanX);
214                         mMaxGridSizeY = Math.max(mMaxGridSizeY, cellY + spanY);
215                         break;
216                     }
217                     case Favorites.CONTAINER_HOTSEAT: {
218                         mHotseatSize = Math.max(mHotseatSize, (int) screen + 1);
219                         break;
220                     }
221                     default:
222                         if (!mValidFolders.get(container)) {
223                             FileLog.d(TAG, String.format("Skipping item %d, type %d not in a valid folder %d", id, type, container));
224                             continue;
225                         }
226                 }
227 
228                 Intent intent = null;
229                 switch (type) {
230                     case Favorites.ITEM_TYPE_FOLDER: {
231                         mValidFolders.put(id, true);
232                         // Use a empty intent to indicate a folder.
233                         intent = new Intent();
234                         break;
235                     }
236                     case Favorites.ITEM_TYPE_APPWIDGET: {
237                         values.put(Favorites.RESTORED,
238                                 LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
239                                         LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
240                                         LauncherAppWidgetInfo.FLAG_UI_NOT_READY);
241                         values.put(Favorites.APPWIDGET_PROVIDER, c.getString(widgetProviderIndex));
242                         break;
243                     }
244                     case Favorites.ITEM_TYPE_SHORTCUT:
245                     case Favorites.ITEM_TYPE_APPLICATION: {
246                         intent = Intent.parseUri(c.getString(intentIndex), 0);
247                         if (Utilities.isLauncherAppTarget(intent)) {
248                             type = Favorites.ITEM_TYPE_APPLICATION;
249                         } else {
250                             values.put(Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
251                             values.put(Favorites.ICON_RESOURCE, c.getString(iconResourceIndex));
252                         }
253                         values.put(Favorites.ICON,  c.getBlob(iconIndex));
254                         values.put(Favorites.INTENT, intent.toUri(0));
255                         values.put(Favorites.RANK, c.getInt(rankIndex));
256 
257                         values.put(Favorites.RESTORED, 1);
258                         break;
259                     }
260                     default:
261                         FileLog.d(TAG, String.format("Skipping item %d, not a valid type %d", id, type));
262                         continue;
263                 }
264 
265                 if (container == Favorites.CONTAINER_HOTSEAT) {
266                     if (intent == null) {
267                         FileLog.d(TAG, String.format("Skipping item %d, null intent on hotseat", id));
268                         continue;
269                     }
270                     if (intent.getComponent() != null) {
271                         intent.setPackage(intent.getComponent().getPackageName());
272                     }
273                     hotseatTargetApps.add(getPackage(intent));
274                 }
275 
276                 values.put(Favorites._ID, id);
277                 values.put(Favorites.ITEM_TYPE, type);
278                 values.put(Favorites.CONTAINER, container);
279                 values.put(Favorites.SCREEN, screen);
280                 values.put(Favorites.CELLX, cellX);
281                 values.put(Favorites.CELLY, cellY);
282                 values.put(Favorites.SPANX, spanX);
283                 values.put(Favorites.SPANY, spanY);
284                 values.put(Favorites.TITLE, c.getString(titleIndex));
285                 insertOperations.add(ContentProviderOperation
286                         .newInsert(Favorites.CONTENT_URI).withValues(values).build());
287                 if (container < 0) {
288                     totalItemsOnWorkspace++;
289                 }
290 
291                 if (insertOperations.size() >= BATCH_INSERT_SIZE) {
292                     mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY,
293                             insertOperations);
294                     insertOperations.clear();
295                 }
296             }
297         }
298         FileLog.d(TAG, totalItemsOnWorkspace + " items imported from external source");
299         if (totalItemsOnWorkspace < MIN_ITEM_COUNT_FOR_SUCCESSFUL_MIGRATION) {
300             throw new Exception("Insufficient data");
301         }
302         if (!insertOperations.isEmpty()) {
303             mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY,
304                     insertOperations);
305             insertOperations.clear();
306         }
307 
308         LongArrayMap<Object> hotseatItems = GridSizeMigrationTask.removeBrokenHotseatItems(mContext);
309         int myHotseatCount = LauncherAppState.getIDP(mContext).numHotseatIcons;
310         if (!FeatureFlags.NO_ALL_APPS_ICON) {
311             myHotseatCount--;
312         }
313         if (hotseatItems.size() < myHotseatCount) {
314             // Insufficient hotseat items. Add a few more.
315             HotseatParserCallback parserCallback = new HotseatParserCallback(
316                     hotseatTargetApps, hotseatItems, insertOperations, maxId + 1, myHotseatCount);
317             new HotseatLayoutParser(mContext,
318                     parserCallback).loadLayout(null, new ArrayList<Long>());
319             mHotseatSize = (int) hotseatItems.keyAt(hotseatItems.size() - 1) + 1;
320 
321             if (!insertOperations.isEmpty()) {
322                 mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY,
323                         insertOperations);
324             }
325         }
326     }
327 
getPackage(Intent intent)328     private static final String getPackage(Intent intent) {
329         return intent.getComponent() != null ? intent.getComponent().getPackageName()
330                 : intent.getPackage();
331     }
332 
333     /**
334      * Performs data import if possible.
335      * @return true on successful data import, false if it was not available
336      * @throws Exception if the import failed
337      */
performImportIfPossible(Context context)338     public static boolean performImportIfPossible(Context context) throws Exception {
339         SharedPreferences devicePrefs = getDevicePrefs(context);
340         String sourcePackage = devicePrefs.getString(KEY_DATA_IMPORT_SRC_PKG, "");
341         String sourceAuthority = devicePrefs.getString(KEY_DATA_IMPORT_SRC_AUTHORITY, "");
342 
343         if (TextUtils.isEmpty(sourcePackage) || TextUtils.isEmpty(sourceAuthority)) {
344             return false;
345         }
346 
347         // Synchronously clear the migration flags. This ensures that we do not try migration
348         // again and thus prevents potential crash loops due to migration failure.
349         devicePrefs.edit().remove(KEY_DATA_IMPORT_SRC_PKG).remove(KEY_DATA_IMPORT_SRC_AUTHORITY).commit();
350 
351         if (!Settings.call(context.getContentResolver(), Settings.METHOD_WAS_EMPTY_DB_CREATED)
352                 .getBoolean(Settings.EXTRA_VALUE, false)) {
353             // Only migration if a new DB was created.
354             return false;
355         }
356 
357         for (ProviderInfo info : context.getPackageManager().queryContentProviders(
358                 null, context.getApplicationInfo().uid, 0)) {
359 
360             if (sourcePackage.equals(info.packageName)) {
361                 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
362                     // Only migrate if the source launcher is also on system image.
363                     return false;
364                 }
365 
366                 // Wait until we found a provider with matching authority.
367                 if (sourceAuthority.equals(info.authority)) {
368                     if (TextUtils.isEmpty(info.readPermission) ||
369                             context.checkPermission(info.readPermission, Process.myPid(),
370                                     Process.myUid()) == PackageManager.PERMISSION_GRANTED) {
371                         // All checks passed, run the import task.
372                         return new ImportDataTask(context, sourceAuthority).importWorkspace();
373                     }
374                 }
375             }
376         }
377         return false;
378     }
379 
getDevicePrefs(Context c)380     private static SharedPreferences getDevicePrefs(Context c) {
381         return c.getSharedPreferences(LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE);
382     }
383 
getMyHotseatLayoutId(Context context)384     private static final int getMyHotseatLayoutId(Context context) {
385         return LauncherAppState.getIDP(context).numHotseatIcons <= 5
386                 ? R.xml.dw_phone_hotseat
387                 : R.xml.dw_tablet_hotseat;
388     }
389 
390     /**
391      * Extension of {@link DefaultLayoutParser} which only allows icons and shortcuts.
392      */
393     private static class HotseatLayoutParser extends DefaultLayoutParser {
HotseatLayoutParser(Context context, LayoutParserCallback callback)394         public HotseatLayoutParser(Context context, LayoutParserCallback callback) {
395             super(context, null, callback, context.getResources(), getMyHotseatLayoutId(context));
396         }
397 
398         @Override
getLayoutElementsMap()399         protected HashMap<String, TagParser> getLayoutElementsMap() {
400             // Only allow shortcut parsers
401             HashMap<String, TagParser> parsers = new HashMap<String, TagParser>();
402             parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser());
403             parsers.put(TAG_SHORTCUT, new UriShortcutParser(mSourceRes));
404             parsers.put(TAG_RESOLVE, new ResolveParser());
405             return parsers;
406         }
407     }
408 
409     /**
410      * {@link LayoutParserCallback} which adds items in empty hotseat spots.
411      */
412     private static class HotseatParserCallback implements LayoutParserCallback {
413         private final HashSet<String> mExisitingApps;
414         private final LongArrayMap<Object> mExistingItems;
415         private final ArrayList<ContentProviderOperation> mOutOps;
416         private final int mRequiredSize;
417         private int mStartItemId;
418 
HotseatParserCallback( HashSet<String> existingApps, LongArrayMap<Object> existingItems, ArrayList<ContentProviderOperation> outOps, int startItemId, int requiredSize)419         HotseatParserCallback(
420                 HashSet<String> existingApps, LongArrayMap<Object> existingItems,
421                 ArrayList<ContentProviderOperation> outOps, int startItemId, int requiredSize) {
422             mExisitingApps = existingApps;
423             mExistingItems = existingItems;
424             mOutOps = outOps;
425             mRequiredSize = requiredSize;
426             mStartItemId = startItemId;
427         }
428 
429         @Override
generateNewItemId()430         public long generateNewItemId() {
431             return mStartItemId++;
432         }
433 
434         @Override
insertAndCheck(SQLiteDatabase db, ContentValues values)435         public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
436             if (mExistingItems.size() >= mRequiredSize) {
437                 // No need to add more items.
438                 return 0;
439             }
440             Intent intent;
441             try {
442                 intent = Intent.parseUri(values.getAsString(Favorites.INTENT), 0);
443             } catch (URISyntaxException e) {
444                 return 0;
445             }
446             String pkg = getPackage(intent);
447             if (pkg == null || mExisitingApps.contains(pkg)) {
448                 // The item does not target an app or is already in hotseat.
449                 return 0;
450             }
451             mExisitingApps.add(pkg);
452 
453             // find next vacant spot.
454             long screen = 0;
455             while (mExistingItems.get(screen) != null) {
456                 screen++;
457             }
458             mExistingItems.put(screen, intent);
459             values.put(Favorites.SCREEN, screen);
460             mOutOps.add(ContentProviderOperation.newInsert(Favorites.CONTENT_URI).withValues(values).build());
461             return 0;
462         }
463     }
464 }
465