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