• 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 android.graphics.BitmapFactory.decodeByteArray;
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.Intent.ShortcutIconResource;
26 import android.content.pm.LauncherActivityInfo;
27 import android.content.pm.LauncherApps;
28 import android.content.pm.PackageManager;
29 import android.database.Cursor;
30 import android.database.CursorWrapper;
31 import android.net.Uri;
32 import android.os.UserHandle;
33 import android.provider.BaseColumns;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.util.LongSparseArray;
37 
38 import androidx.annotation.Nullable;
39 import androidx.annotation.VisibleForTesting;
40 
41 import com.android.launcher3.InvariantDeviceProfile;
42 import com.android.launcher3.LauncherAppState;
43 import com.android.launcher3.LauncherSettings;
44 import com.android.launcher3.LauncherSettings.Favorites;
45 import com.android.launcher3.Utilities;
46 import com.android.launcher3.Workspace;
47 import com.android.launcher3.config.FeatureFlags;
48 import com.android.launcher3.icons.BitmapInfo;
49 import com.android.launcher3.icons.IconCache;
50 import com.android.launcher3.icons.LauncherIcons;
51 import com.android.launcher3.logging.FileLog;
52 import com.android.launcher3.model.data.AppInfo;
53 import com.android.launcher3.model.data.ItemInfo;
54 import com.android.launcher3.model.data.WorkspaceItemInfo;
55 import com.android.launcher3.shortcuts.ShortcutKey;
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 
61 import java.net.URISyntaxException;
62 import java.security.InvalidParameterException;
63 
64 /**
65  * Extension of {@link Cursor} with utility methods for workspace loading.
66  */
67 public class LoaderCursor extends CursorWrapper {
68 
69     private static final String TAG = "LoaderCursor";
70 
71     private final LongSparseArray<UserHandle> allUsers;
72 
73     private final Uri mContentUri;
74     private final Context mContext;
75     private final PackageManager mPM;
76     private final IconCache mIconCache;
77     private final InvariantDeviceProfile mIDP;
78 
79     private final IntArray itemsToRemove = new IntArray();
80     private final IntArray restoredRows = new IntArray();
81     private final IntSparseArrayMap<GridOccupancy> occupied = new IntSparseArrayMap<>();
82 
83     private final int iconPackageIndex;
84     private final int iconResourceIndex;
85     private final int iconIndex;
86     public final int titleIndex;
87 
88     private final int idIndex;
89     private final int containerIndex;
90     private final int itemTypeIndex;
91     private final int screenIndex;
92     private final int cellXIndex;
93     private final int cellYIndex;
94     private final int profileIdIndex;
95     private final int restoredIndex;
96     private final int intentIndex;
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, Uri contentUri, LauncherAppState app, UserManagerState userManagerState)109     public LoaderCursor(Cursor cursor, Uri contentUri, LauncherAppState app,
110             UserManagerState userManagerState) {
111         super(cursor);
112 
113         allUsers = userManagerState.allUsers;
114         mContentUri = contentUri;
115         mContext = app.getContext();
116         mIconCache = app.getIconCache();
117         mIDP = app.getInvariantDeviceProfile();
118         mPM = mContext.getPackageManager();
119 
120         // Init column indices
121         iconIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
122         iconPackageIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
123         iconResourceIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
124         titleIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
125 
126         idIndex = getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
127         containerIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
128         itemTypeIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
129         screenIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
130         cellXIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
131         cellYIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
132         profileIdIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID);
133         restoredIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.RESTORED);
134         intentIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
135     }
136 
137     @Override
moveToNext()138     public boolean moveToNext() {
139         boolean result = super.moveToNext();
140         if (result) {
141             mActivityInfo = null;
142 
143             // Load common properties.
144             itemType = getInt(itemTypeIndex);
145             container = getInt(containerIndex);
146             id = getInt(idIndex);
147             serialNumber = getInt(profileIdIndex);
148             user = allUsers.get(serialNumber);
149             restoreFlag = getInt(restoredIndex);
150         }
151         return result;
152     }
153 
parseIntent()154     public Intent parseIntent() {
155         String intentDescription = getString(intentIndex);
156         try {
157             return TextUtils.isEmpty(intentDescription) ?
158                     null : Intent.parseUri(intentDescription, 0);
159         } catch (URISyntaxException e) {
160             Log.e(TAG, "Error parsing Intent");
161             return null;
162         }
163     }
164 
165     @VisibleForTesting
loadSimpleWorkspaceItem()166     public WorkspaceItemInfo loadSimpleWorkspaceItem() {
167         final WorkspaceItemInfo info = new WorkspaceItemInfo();
168         info.intent = new Intent();
169         // Non-app shortcuts are only supported for current user.
170         info.user = user;
171         info.itemType = itemType;
172         info.title = getTitle();
173         // the fallback icon
174         if (!loadIcon(info)) {
175             info.bitmap = mIconCache.getDefaultIcon(info.user);
176         }
177 
178         // TODO: If there's an explicit component and we can't install that, delete it.
179 
180         return info;
181     }
182 
183     /**
184      * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
185      */
loadIcon(WorkspaceItemInfo info)186     protected boolean loadIcon(WorkspaceItemInfo info) {
187         try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
188             if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
189                 String packageName = getString(iconPackageIndex);
190                 String resourceName = getString(iconResourceIndex);
191                 if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) {
192                     info.iconResource = new ShortcutIconResource();
193                     info.iconResource.packageName = packageName;
194                     info.iconResource.resourceName = resourceName;
195                     BitmapInfo iconInfo = li.createIconBitmap(info.iconResource);
196                     if (iconInfo != null) {
197                         info.bitmap = iconInfo;
198                         return true;
199                     }
200                 }
201             }
202 
203             // Failed to load from resource, try loading from DB.
204             byte[] data = getBlob(iconIndex);
205             try {
206                 info.bitmap = li.createIconBitmap(decodeByteArray(data, 0, data.length));
207                 return true;
208             } catch (Exception e) {
209                 Log.e(TAG, "Failed to decode byte array for info " + info, e);
210                 return false;
211             }
212         }
213     }
214 
215     /**
216      * Returns the title or empty string
217      */
getTitle()218     private String getTitle() {
219         String title = getString(titleIndex);
220         return TextUtils.isEmpty(title) ? "" : Utilities.trim(title);
221     }
222 
223     /**
224      * Make an WorkspaceItemInfo object for a restored application or shortcut item that points
225      * to a package that is not yet installed on the system.
226      */
getRestoredItemInfo(Intent intent)227     public WorkspaceItemInfo getRestoredItemInfo(Intent intent) {
228         final WorkspaceItemInfo info = new WorkspaceItemInfo();
229         info.user = user;
230         info.intent = intent;
231 
232         // the fallback icon
233         if (!loadIcon(info)) {
234             mIconCache.getTitleAndIcon(info, false /* useLowResIcon */);
235         }
236 
237         if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON)) {
238             String title = getTitle();
239             if (!TextUtils.isEmpty(title)) {
240                 info.title = Utilities.trim(title);
241             }
242         } else if (hasRestoreFlag(WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON)) {
243             if (TextUtils.isEmpty(info.title)) {
244                 info.title = getTitle();
245             }
246         } else {
247             throw new InvalidParameterException("Invalid restoreType " + restoreFlag);
248         }
249 
250         info.contentDescription = mPM.getUserBadgedLabel(info.title, info.user);
251         info.itemType = itemType;
252         info.status = restoreFlag;
253         return info;
254     }
255 
getLauncherActivityInfo()256     public LauncherActivityInfo getLauncherActivityInfo() {
257         return mActivityInfo;
258     }
259 
260     /**
261      * Make an WorkspaceItemInfo object for a shortcut that is an application.
262      */
getAppShortcutInfo( Intent intent, boolean allowMissingTarget, boolean useLowResIcon)263     public WorkspaceItemInfo getAppShortcutInfo(
264             Intent intent, boolean allowMissingTarget, boolean useLowResIcon) {
265         if (user == null) {
266             Log.d(TAG, "Null user found in getShortcutInfo");
267             return null;
268         }
269 
270         ComponentName componentName = intent.getComponent();
271         if (componentName == null) {
272             Log.d(TAG, "Missing component found in getShortcutInfo");
273             return null;
274         }
275 
276         Intent newIntent = new Intent(Intent.ACTION_MAIN, null);
277         newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
278         newIntent.setComponent(componentName);
279         mActivityInfo = mContext.getSystemService(LauncherApps.class)
280                 .resolveActivity(newIntent, user);
281         if ((mActivityInfo == null) && !allowMissingTarget) {
282             Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName);
283             return null;
284         }
285 
286         final WorkspaceItemInfo info = new WorkspaceItemInfo();
287         info.itemType = Favorites.ITEM_TYPE_APPLICATION;
288         info.user = user;
289         info.intent = newIntent;
290 
291         mIconCache.getTitleAndIcon(info, mActivityInfo, useLowResIcon);
292         if (mIconCache.isDefaultIcon(info.bitmap, user)) {
293             loadIcon(info);
294         }
295 
296         if (mActivityInfo != null) {
297             AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo);
298         }
299 
300         // from the db
301         if (TextUtils.isEmpty(info.title)) {
302             info.title = getTitle();
303         }
304 
305         // fall back to the class name of the activity
306         if (info.title == null) {
307             info.title = componentName.getClassName();
308         }
309 
310         info.contentDescription = mPM.getUserBadgedLabel(info.title, info.user);
311         return info;
312     }
313 
314     /**
315      * Returns a {@link ContentWriter} which can be used to update the current item.
316      */
updater()317     public ContentWriter updater() {
318        return new ContentWriter(mContext, new ContentWriter.CommitParams(
319                BaseColumns._ID + "= ?", new String[]{Integer.toString(id)}));
320     }
321 
322     /**
323      * Marks the current item for removal
324      */
markDeleted(String reason)325     public void markDeleted(String reason) {
326         FileLog.e(TAG, reason);
327         itemsToRemove.add(id);
328     }
329 
330     /**
331      * Removes any items marked for removal.
332      * @return true is any item was removed.
333      */
commitDeleted()334     public boolean commitDeleted() {
335         if (itemsToRemove.size() > 0) {
336             // Remove dead items
337             mContext.getContentResolver().delete(mContentUri, Utilities.createDbSelectionQuery(
338                     LauncherSettings.Favorites._ID, itemsToRemove), null);
339             return true;
340         }
341         return false;
342     }
343 
344     /**
345      * Marks the current item as restored
346      */
markRestored()347     public void markRestored() {
348         if (restoreFlag != 0) {
349             restoredRows.add(id);
350             restoreFlag = 0;
351         }
352     }
353 
hasRestoreFlag(int flagMask)354     public boolean hasRestoreFlag(int flagMask) {
355         return (restoreFlag & flagMask) != 0;
356     }
357 
commitRestoredItems()358     public void commitRestoredItems() {
359         if (restoredRows.size() > 0) {
360             // Update restored items that no longer require special handling
361             ContentValues values = new ContentValues();
362             values.put(LauncherSettings.Favorites.RESTORED, 0);
363             mContext.getContentResolver().update(mContentUri, values,
364                     Utilities.createDbSelectionQuery(
365                             LauncherSettings.Favorites._ID, restoredRows), null);
366         }
367     }
368 
369     /**
370      * Returns true is the item is on workspace or hotseat
371      */
isOnWorkspaceOrHotseat()372     public boolean isOnWorkspaceOrHotseat() {
373         return container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
374                 container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
375     }
376 
377     /**
378      * Applies the following properties:
379      * {@link ItemInfo#id}
380      * {@link ItemInfo#container}
381      * {@link ItemInfo#screenId}
382      * {@link ItemInfo#cellX}
383      * {@link ItemInfo#cellY}
384      */
applyCommonProperties(ItemInfo info)385     public void applyCommonProperties(ItemInfo info) {
386         info.id = id;
387         info.container = container;
388         info.screenId = getInt(screenIndex);
389         info.cellX = getInt(cellXIndex);
390         info.cellY = getInt(cellYIndex);
391     }
392 
393     /**
394      * Adds the {@param info} to {@param dataModel} if it does not overlap with any other item,
395      * otherwise marks it for deletion.
396      */
checkAndAddItem(ItemInfo info, BgDataModel dataModel)397     public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) {
398         if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
399             // Ensure that it is a valid intent. An exception here will
400             // cause the item loading to get skipped
401             ShortcutKey.fromItemInfo(info);
402         }
403         if (checkItemPlacement(info)) {
404             dataModel.addItem(mContext, info, false);
405         } else {
406             markDeleted("Item position overlap");
407         }
408     }
409 
410     /**
411      * check & update map of what's occupied; used to discard overlapping/invalid items
412      */
checkItemPlacement(ItemInfo item)413     protected boolean checkItemPlacement(ItemInfo item) {
414         int containerIndex = item.screenId;
415         if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
416             final GridOccupancy hotseatOccupancy =
417                     occupied.get(LauncherSettings.Favorites.CONTAINER_HOTSEAT);
418 
419             if (item.screenId >= mIDP.numDatabaseHotseatIcons) {
420                 Log.e(TAG, "Error loading shortcut " + item
421                         + " into hotseat position " + item.screenId
422                         + ", position out of bounds: (0 to " + (mIDP.numDatabaseHotseatIcons - 1)
423                         + ")");
424                 return false;
425             }
426 
427             if (hotseatOccupancy != null) {
428                 if (hotseatOccupancy.cells[(int) item.screenId][0]) {
429                     Log.e(TAG, "Error loading shortcut into hotseat " + item
430                             + " into position (" + item.screenId + ":" + item.cellX + ","
431                             + item.cellY + ") already occupied");
432                     return false;
433                 } else {
434                     hotseatOccupancy.cells[item.screenId][0] = true;
435                     return true;
436                 }
437             } else {
438                 final GridOccupancy occupancy = new GridOccupancy(mIDP.numDatabaseHotseatIcons, 1);
439                 occupancy.cells[item.screenId][0] = true;
440                 occupied.put(LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy);
441                 return true;
442             }
443         } else if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
444             // Skip further checking if it is not the hotseat or workspace container
445             return true;
446         }
447 
448         final int countX = mIDP.numColumns;
449         final int countY = mIDP.numRows;
450         if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
451                 item.cellX < 0 || item.cellY < 0 ||
452                 item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) {
453             Log.e(TAG, "Error loading shortcut " + item
454                     + " into cell (" + containerIndex + "-" + item.screenId + ":"
455                     + item.cellX + "," + item.cellY
456                     + ") out of screen bounds ( " + countX + "x" + countY + ")");
457             return false;
458         }
459 
460         if (!occupied.containsKey(item.screenId)) {
461             GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1);
462             if (item.screenId == Workspace.FIRST_SCREEN_ID) {
463                 // Mark the first row as occupied (if the feature is enabled)
464                 // in order to account for the QSB.
465                 int spanY = FeatureFlags.EXPANDED_SMARTSPACE.get() ? 2 : 1;
466                 screen.markCells(0, 0, countX + 1, spanY, FeatureFlags.QSB_ON_FIRST_SCREEN);
467             }
468             occupied.put(item.screenId, screen);
469         }
470         final GridOccupancy occupancy = occupied.get(item.screenId);
471 
472         // Check if any workspace icons overlap with each other
473         if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) {
474             occupancy.markCells(item, true);
475             return true;
476         } else {
477             Log.e(TAG, "Error loading shortcut " + item
478                     + " into cell (" + containerIndex + "-" + item.screenId + ":"
479                     + item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY
480                     + ") already occupied");
481             return false;
482         }
483     }
484 }
485