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