1 /*
2  * Copyright (C) 2017 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.model;
18 
19 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
20 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
21 
22 import android.content.ComponentName;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.LauncherActivityInfo;
27 import android.content.pm.LauncherApps;
28 import android.database.Cursor;
29 import android.database.CursorWrapper;
30 import android.os.UserHandle;
31 import android.provider.BaseColumns;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.util.LongSparseArray;
35 
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 
39 import com.android.launcher3.InvariantDeviceProfile;
40 import com.android.launcher3.LauncherAppState;
41 import com.android.launcher3.LauncherSettings.Favorites;
42 import com.android.launcher3.Utilities;
43 import com.android.launcher3.Workspace;
44 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
45 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError;
46 import com.android.launcher3.config.FeatureFlags;
47 import com.android.launcher3.icons.IconCache;
48 import com.android.launcher3.logging.FileLog;
49 import com.android.launcher3.model.data.AppInfo;
50 import com.android.launcher3.model.data.IconRequestInfo;
51 import com.android.launcher3.model.data.ItemInfo;
52 import com.android.launcher3.model.data.WorkspaceItemInfo;
53 import com.android.launcher3.pm.UserCache;
54 import com.android.launcher3.shortcuts.ShortcutKey;
55 import com.android.launcher3.util.ApiWrapper;
56 import com.android.launcher3.util.ContentWriter;
57 import com.android.launcher3.util.GridOccupancy;
58 import com.android.launcher3.util.IntArray;
59 import com.android.launcher3.util.IntSparseArrayMap;
60 import com.android.launcher3.util.PackageManagerHelper;
61 import com.android.launcher3.util.UserIconInfo;
62 
63 import java.net.URISyntaxException;
64 import java.security.InvalidParameterException;
65 
66 /**
67  * Extension of {@link Cursor} with utility methods for workspace loading.
68  */
69 public class LoaderCursor extends CursorWrapper {
70 
71     private static final String TAG = "LoaderCursor";
72 
73     private final LongSparseArray<UserHandle> allUsers;
74 
75     private final LauncherAppState mApp;
76     private final Context mContext;
77     private final PackageManagerHelper mPmHelper;
78     private final IconCache mIconCache;
79     private final InvariantDeviceProfile mIDP;
80     private final @Nullable LauncherRestoreEventLogger mRestoreEventLogger;
81 
82     private final IntArray mItemsToRemove = new IntArray();
83     private final IntArray mRestoredRows = new IntArray();
84     private final IntSparseArrayMap<GridOccupancy> mOccupied = new IntSparseArrayMap<>();
85 
86     private final int mIconIndex;
87     public final int mTitleIndex;
88 
89     private final int mIdIndex;
90     private final int mContainerIndex;
91     private final int mItemTypeIndex;
92     private final int mScreenIndex;
93     private final int mCellXIndex;
94     private final int mCellYIndex;
95     private final int mProfileIdIndex;
96     private final int mRestoredIndex;
97     private final int mIntentIndex;
98 
99     private final int mAppWidgetIdIndex;
100     private final int mAppWidgetProviderIndex;
101     private final int mSpanXIndex;
102     private final int mSpanYIndex;
103     private final int mRankIndex;
104     private final int mOptionsIndex;
105     private final int mAppWidgetSourceIndex;
106 
107     @Nullable
108     private LauncherActivityInfo mActivityInfo;
109 
110     // Properties loaded per iteration
111     public long serialNumber;
112     public UserHandle user;
113     public int id;
114     public int container;
115     public int itemType;
116     public int restoreFlag;
117 
LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState, PackageManagerHelper pmHelper, @Nullable LauncherRestoreEventLogger restoreEventLogger)118     public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState,
119             PackageManagerHelper pmHelper,
120             @Nullable LauncherRestoreEventLogger restoreEventLogger) {
121         super(cursor);
122 
123         mApp = app;
124         allUsers = userManagerState.allUsers;
125         mContext = app.getContext();
126         mIconCache = app.getIconCache();
127         mPmHelper = pmHelper;
128         mIDP = app.getInvariantDeviceProfile();
129         mRestoreEventLogger = restoreEventLogger;
130 
131         // Init column indices
132         mIconIndex = getColumnIndexOrThrow(Favorites.ICON);
133         mTitleIndex = getColumnIndexOrThrow(Favorites.TITLE);
134 
135         mIdIndex = getColumnIndexOrThrow(Favorites._ID);
136         mContainerIndex = getColumnIndexOrThrow(Favorites.CONTAINER);
137         mItemTypeIndex = getColumnIndexOrThrow(Favorites.ITEM_TYPE);
138         mScreenIndex = getColumnIndexOrThrow(Favorites.SCREEN);
139         mCellXIndex = getColumnIndexOrThrow(Favorites.CELLX);
140         mCellYIndex = getColumnIndexOrThrow(Favorites.CELLY);
141         mProfileIdIndex = getColumnIndexOrThrow(Favorites.PROFILE_ID);
142         mRestoredIndex = getColumnIndexOrThrow(Favorites.RESTORED);
143         mIntentIndex = getColumnIndexOrThrow(Favorites.INTENT);
144 
145         mAppWidgetIdIndex = getColumnIndexOrThrow(Favorites.APPWIDGET_ID);
146         mAppWidgetProviderIndex = getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER);
147         mSpanXIndex = getColumnIndexOrThrow(Favorites.SPANX);
148         mSpanYIndex = getColumnIndexOrThrow(Favorites.SPANY);
149         mRankIndex = getColumnIndexOrThrow(Favorites.RANK);
150         mOptionsIndex = getColumnIndexOrThrow(Favorites.OPTIONS);
151         mAppWidgetSourceIndex = getColumnIndexOrThrow(Favorites.APPWIDGET_SOURCE);
152     }
153 
154     @Override
moveToNext()155     public boolean moveToNext() {
156         boolean result = super.moveToNext();
157         if (result) {
158             mActivityInfo = null;
159 
160             // Load common properties.
161             itemType = getInt(mItemTypeIndex);
162             container = getInt(mContainerIndex);
163             id = getInt(mIdIndex);
164             serialNumber = getInt(mProfileIdIndex);
165             user = allUsers.get(serialNumber);
166             restoreFlag = getInt(mRestoredIndex);
167         }
168         return result;
169     }
170 
parseIntent()171     public Intent parseIntent() {
172         String intentDescription = getString(mIntentIndex);
173         try {
174             return TextUtils.isEmpty(intentDescription) ?
175                     null : Intent.parseUri(intentDescription, 0);
176         } catch (URISyntaxException e) {
177             Log.e(TAG, "Error parsing Intent");
178             return null;
179         }
180     }
181 
182     @VisibleForTesting
loadSimpleWorkspaceItem()183     public WorkspaceItemInfo loadSimpleWorkspaceItem() {
184         final WorkspaceItemInfo info = new WorkspaceItemInfo();
185         info.intent = new Intent();
186         // Non-app shortcuts are only supported for current user.
187         info.user = user;
188         info.itemType = itemType;
189         info.title = getTitle();
190         // the fallback icon
191         if (!loadIcon(info)) {
192             info.bitmap = mIconCache.getDefaultIcon(info.user);
193         }
194 
195         // TODO: If there's an explicit component and we can't install that, delete it.
196 
197         return info;
198     }
199 
200     /**
201      * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
202      */
loadIcon(WorkspaceItemInfo info)203     protected boolean loadIcon(WorkspaceItemInfo info) {
204         return createIconRequestInfo(info, false).loadWorkspaceIcon(mContext);
205     }
206 
createIconRequestInfo( WorkspaceItemInfo wai, boolean useLowResIcon)207     public IconRequestInfo<WorkspaceItemInfo> createIconRequestInfo(
208             WorkspaceItemInfo wai, boolean useLowResIcon) {
209         byte[] iconBlob = itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT || restoreFlag != 0
210                 ? getIconBlob() : null;
211 
212         return new IconRequestInfo<>(wai, mActivityInfo, iconBlob, useLowResIcon);
213     }
214 
215     /**
216      * Returns the icon data for at the current position
217      */
getIconBlob()218     public byte[] getIconBlob() {
219         return getBlob(mIconIndex);
220     }
221 
222     /**
223      * Returns the title or empty string
224      */
getTitle()225     public String getTitle() {
226         return Utilities.trim(getString(mTitleIndex));
227     }
228 
229     /**
230      * When loading an app widget for the workspace, returns it's app widget id
231      */
getAppWidgetId()232     public int getAppWidgetId() {
233         return getInt(mAppWidgetIdIndex);
234     }
235 
236     /**
237      * When loading an app widget for the workspace, returns the widget provider
238      */
getAppWidgetProvider()239     public String getAppWidgetProvider() {
240         return getString(mAppWidgetProviderIndex);
241     }
242 
243     /**
244      * Returns the x position for the item in the cell layout's grid
245      */
getSpanX()246     public int getSpanX() {
247         return getInt(mSpanXIndex);
248     }
249 
250     /**
251      * Returns the y position for the item in the cell layout's grid
252      */
getSpanY()253     public int getSpanY() {
254         return getInt(mSpanYIndex);
255     }
256 
257     /**
258      * Returns the rank for the item
259      */
getRank()260     public int getRank() {
261         return getInt(mRankIndex);
262     }
263 
264     /**
265      * Returns the options for the item
266      */
getOptions()267     public int getOptions() {
268         return getInt(mOptionsIndex);
269     }
270 
271     /**
272      * When loading an app widget for the workspace, returns it's app widget source
273      */
getAppWidgetSource()274     public int getAppWidgetSource() {
275         return getInt(mAppWidgetSourceIndex);
276     }
277 
278     /**
279      * Returns the screen that the item is on
280      */
getScreen()281     public int getScreen() {
282         return getInt(mScreenIndex);
283     }
284 
285     /**
286      * Returns the UX container that the item is in
287      */
getContainer()288     public int getContainer() {
289         return getInt(mContainerIndex);
290     }
291 
292     /**
293      * Make an WorkspaceItemInfo object for a restored application or shortcut item that points
294      * to a package that is not yet installed on the system.
295      */
getRestoredItemInfo(Intent intent)296     public WorkspaceItemInfo getRestoredItemInfo(Intent intent) {
297         final WorkspaceItemInfo info = new WorkspaceItemInfo();
298         info.user = user;
299         info.intent = intent;
300 
301         // the fallback icon
302         if (!loadIcon(info)) {
303             mIconCache.getTitleAndIcon(info, false /* useLowResIcon */);
304         }
305 
306         if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON)) {
307             String title = getTitle();
308             if (!TextUtils.isEmpty(title)) {
309                 info.title = Utilities.trim(title);
310             }
311         } else if (hasRestoreFlag(WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON)) {
312             if (TextUtils.isEmpty(info.title)) {
313                 info.title = getTitle();
314             }
315         } else {
316             throw new InvalidParameterException("Invalid restoreType " + restoreFlag);
317         }
318 
319         info.contentDescription = mIconCache.getUserBadgedLabel(info.title, info.user);
320         info.itemType = itemType;
321         info.status = restoreFlag;
322         return info;
323     }
324 
getLauncherActivityInfo()325     public LauncherActivityInfo getLauncherActivityInfo() {
326         return mActivityInfo;
327     }
328 
329     /**
330      * Make an WorkspaceItemInfo object for a shortcut that is an application.
331      */
getAppShortcutInfo( Intent intent, boolean allowMissingTarget, boolean useLowResIcon)332     public WorkspaceItemInfo getAppShortcutInfo(
333             Intent intent, boolean allowMissingTarget, boolean useLowResIcon) {
334         return getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, true);
335     }
336 
getAppShortcutInfo( Intent intent, boolean allowMissingTarget, boolean useLowResIcon, boolean loadIcon)337     public WorkspaceItemInfo getAppShortcutInfo(
338             Intent intent, boolean allowMissingTarget, boolean useLowResIcon, boolean loadIcon) {
339         if (user == null) {
340             Log.d(TAG, "Null user found in getShortcutInfo");
341             return null;
342         }
343 
344         ComponentName componentName = intent.getComponent();
345         if (componentName == null) {
346             Log.d(TAG, "Missing component found in getShortcutInfo");
347             return null;
348         }
349 
350         Intent newIntent = new Intent(Intent.ACTION_MAIN, null);
351         newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
352         newIntent.setComponent(componentName);
353         mActivityInfo = mContext.getSystemService(LauncherApps.class)
354                 .resolveActivity(newIntent, user);
355         if ((mActivityInfo == null) && !allowMissingTarget) {
356             Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName);
357             return null;
358         }
359 
360         final WorkspaceItemInfo info = new WorkspaceItemInfo();
361         info.user = user;
362         info.intent = newIntent;
363         UserCache userCache = UserCache.getInstance(mContext);
364         UserIconInfo userIconInfo = userCache.getUserInfo(user);
365 
366         if (loadIcon) {
367             mIconCache.getTitleAndIcon(info, mActivityInfo, useLowResIcon);
368             if (mIconCache.isDefaultIcon(info.bitmap, user)) {
369                 loadIcon(info);
370             }
371         }
372 
373         if (mActivityInfo != null) {
374             AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo, userIconInfo,
375                     ApiWrapper.INSTANCE.get(mContext), mPmHelper);
376         }
377 
378         // from the db
379         if (TextUtils.isEmpty(info.title)) {
380             if (loadIcon) {
381                 info.title = getTitle();
382 
383                 // fall back to the class name of the activity
384                 if (info.title == null) {
385                     info.title = componentName.getClassName();
386                 }
387             } else {
388                 info.title = "";
389             }
390         }
391 
392         info.contentDescription = mIconCache.getUserBadgedLabel(info.title, info.user);
393         return info;
394     }
395 
396     /**
397      * Returns a {@link ContentWriter} which can be used to update the current item.
398      */
updater()399     public ContentWriter updater() {
400        return new ContentWriter(mContext, new ContentWriter.CommitParams(
401                mApp.getModel().getModelDbController(),
402                BaseColumns._ID + "= ?", new String[]{Integer.toString(id)}));
403     }
404 
405     /**
406      * Marks the current item for removal
407      */
markDeleted(String reason, @RestoreError String errorType)408     public void markDeleted(String reason, @RestoreError String errorType) {
409         FileLog.e(TAG, reason);
410         mItemsToRemove.add(id);
411         if (mRestoreEventLogger != null) {
412             mRestoreEventLogger.logSingleFavoritesItemRestoreFailed(itemType, errorType);
413         }
414     }
415 
416     /**
417      * Removes any items marked for removal.
418      * @return true is any item was removed.
419      */
commitDeleted()420     public boolean commitDeleted() {
421         if (mItemsToRemove.size() > 0) {
422             // Remove dead items
423             mApp.getModel().getModelDbController().delete(TABLE_NAME,
424                     Utilities.createDbSelectionQuery(Favorites._ID, mItemsToRemove), null);
425             return true;
426         }
427         return false;
428     }
429 
430     /**
431      * Marks the current item as restored
432      */
markRestored()433     public void markRestored() {
434         if (restoreFlag != 0) {
435             mRestoredRows.add(id);
436             restoreFlag = 0;
437         }
438     }
439 
hasRestoreFlag(int flagMask)440     public boolean hasRestoreFlag(int flagMask) {
441         return (restoreFlag & flagMask) != 0;
442     }
443 
commitRestoredItems()444     public void commitRestoredItems() {
445         if (mRestoredRows.size() > 0) {
446             // Update restored items that no longer require special handling
447             ContentValues values = new ContentValues();
448             values.put(Favorites.RESTORED, 0);
449             mApp.getModel().getModelDbController().update(TABLE_NAME, values,
450                     Utilities.createDbSelectionQuery(Favorites._ID, mRestoredRows), null);
451         }
452         if (mRestoreEventLogger != null) {
453             mRestoreEventLogger.reportLauncherRestoreResults();
454         }
455     }
456 
457     /**
458      * Returns true is the item is on workspace or hotseat
459      */
isOnWorkspaceOrHotseat()460     public boolean isOnWorkspaceOrHotseat() {
461         return container == Favorites.CONTAINER_DESKTOP || container == Favorites.CONTAINER_HOTSEAT;
462     }
463 
464     /**
465      * Applies the following properties:
466      * {@link ItemInfo#id}
467      * {@link ItemInfo#container}
468      * {@link ItemInfo#screenId}
469      * {@link ItemInfo#cellX}
470      * {@link ItemInfo#cellY}
471      */
applyCommonProperties(ItemInfo info)472     public void applyCommonProperties(ItemInfo info) {
473         info.id = id;
474         info.container = container;
475         info.screenId = getInt(mScreenIndex);
476         info.cellX = getInt(mCellXIndex);
477         info.cellY = getInt(mCellYIndex);
478     }
479 
checkAndAddItem(ItemInfo info, BgDataModel dataModel)480     public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) {
481         checkAndAddItem(info, dataModel, null);
482     }
483 
484     /**
485      * Adds the {@param info} to {@param dataModel} if it does not overlap with any other item,
486      * otherwise marks it for deletion.
487      */
checkAndAddItem( ItemInfo info, BgDataModel dataModel, LoaderMemoryLogger logger)488     public void checkAndAddItem(
489             ItemInfo info, BgDataModel dataModel, LoaderMemoryLogger logger) {
490         if (info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
491             // Ensure that it is a valid intent. An exception here will
492             // cause the item loading to get skipped
493             ShortcutKey.fromItemInfo(info);
494         }
495         if (checkItemPlacement(info, dataModel.isFirstPagePinnedItemEnabled)) {
496             dataModel.addItem(mContext, info, false, logger);
497             if (mRestoreEventLogger != null) {
498                 mRestoreEventLogger.logSingleFavoritesItemRestored(itemType);
499             }
500         } else {
501             markDeleted("Item position overlap", RestoreError.INVALID_LOCATION);
502         }
503     }
504 
505     /**
506      * check & update map of what's occupied; used to discard overlapping/invalid items
507      */
checkItemPlacement(ItemInfo item, boolean isFirstPagePinnedItemEnabled)508     protected boolean checkItemPlacement(ItemInfo item, boolean isFirstPagePinnedItemEnabled) {
509         int containerIndex = item.screenId;
510         if (item.container == Favorites.CONTAINER_HOTSEAT) {
511             final GridOccupancy hotseatOccupancy =
512                     mOccupied.get(Favorites.CONTAINER_HOTSEAT);
513 
514             if (item.screenId >= mIDP.numDatabaseHotseatIcons) {
515                 Log.e(TAG, "Error loading shortcut " + item
516                         + " into hotseat position " + item.screenId
517                         + ", position out of bounds: (0 to " + (mIDP.numDatabaseHotseatIcons - 1)
518                         + ")");
519                 return false;
520             }
521 
522             if (hotseatOccupancy != null) {
523                 if (hotseatOccupancy.cells[(int) item.screenId][0]) {
524                     Log.e(TAG, "Error loading shortcut into hotseat " + item
525                             + " into position (" + item.screenId + ":" + item.cellX + ","
526                             + item.cellY + ") already occupied");
527                     return false;
528                 } else {
529                     hotseatOccupancy.cells[item.screenId][0] = true;
530                     return true;
531                 }
532             } else {
533                 final GridOccupancy occupancy = new GridOccupancy(mIDP.numDatabaseHotseatIcons, 1);
534                 occupancy.cells[item.screenId][0] = true;
535                 mOccupied.put(Favorites.CONTAINER_HOTSEAT, occupancy);
536                 return true;
537             }
538         } else if (item.container != Favorites.CONTAINER_DESKTOP) {
539             // Skip further checking if it is not the hotseat or workspace container
540             return true;
541         }
542 
543         final int countX = mIDP.numColumns;
544         final int countY = mIDP.numRows;
545         if (item.container == Favorites.CONTAINER_DESKTOP && item.cellX < 0 || item.cellY < 0
546                 || item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) {
547             Log.e(TAG, "Error loading shortcut " + item
548                     + " into cell (" + containerIndex + "-" + item.screenId + ":"
549                     + item.cellX + "," + item.cellY
550                     + ") out of screen bounds ( " + countX + "x" + countY + ")");
551             return false;
552         }
553 
554         if (!mOccupied.containsKey(item.screenId)) {
555             GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1);
556             if (item.screenId == Workspace.FIRST_SCREEN_ID && (FeatureFlags.QSB_ON_FIRST_SCREEN
557                     && !SHOULD_SHOW_FIRST_PAGE_WIDGET
558                     && isFirstPagePinnedItemEnabled)) {
559                 // Mark the first X columns (X is width of the search container) in the first row as
560                 // occupied (if the feature is enabled) in order to account for the search
561                 // container.
562                 int spanX = mIDP.numSearchContainerColumns;
563                 int spanY = 1;
564                 screen.markCells(0, 0, spanX, spanY, true);
565             }
566             mOccupied.put(item.screenId, screen);
567         }
568         final GridOccupancy occupancy = mOccupied.get(item.screenId);
569 
570         // Check if any workspace icons overlap with each other
571         if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) {
572             occupancy.markCells(item, true);
573             return true;
574         } else {
575             Log.e(TAG, "Error loading shortcut " + item
576                     + " into cell (" + containerIndex + "-" + item.screenId + ":"
577                     + item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY
578                     + ") already occupied");
579             return false;
580         }
581     }
582 }
583