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