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