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