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