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