1/*
2 * Copyright (C) 2016 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 */
16package com.android.launcher3.model;
17
18import android.content.Context;
19import android.content.Intent;
20import android.os.UserHandle;
21import android.util.LongSparseArray;
22import android.util.Pair;
23import com.android.launcher3.AllAppsList;
24import com.android.launcher3.AppInfo;
25import com.android.launcher3.FolderInfo;
26import com.android.launcher3.InvariantDeviceProfile;
27import com.android.launcher3.ItemInfo;
28import com.android.launcher3.LauncherAppState;
29import com.android.launcher3.LauncherAppWidgetInfo;
30import com.android.launcher3.LauncherModel;
31import com.android.launcher3.LauncherModel.CallbackTask;
32import com.android.launcher3.LauncherModel.Callbacks;
33import com.android.launcher3.LauncherSettings;
34import com.android.launcher3.ShortcutInfo;
35import com.android.launcher3.Utilities;
36import com.android.launcher3.util.GridOccupancy;
37import java.util.ArrayList;
38import java.util.List;
39
40/**
41 * Task to add auto-created workspace items.
42 */
43public class AddWorkspaceItemsTask extends BaseModelUpdateTask {
44
45    private final List<Pair<ItemInfo, Object>> mItemList;
46
47    /**
48     * @param itemList items to add on the workspace
49     */
50    public AddWorkspaceItemsTask(List<Pair<ItemInfo, Object>> itemList) {
51        mItemList = itemList;
52    }
53
54    @Override
55    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
56        if (mItemList.isEmpty()) {
57            return;
58        }
59        Context context = app.getContext();
60
61        final ArrayList<ItemInfo> addedItemsFinal = new ArrayList<>();
62        final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<>();
63
64        // Get the list of workspace screens.  We need to append to this list and
65        // can not use sBgWorkspaceScreens because loadWorkspace() may not have been
66        // called.
67        ArrayList<Long> workspaceScreens = LauncherModel.loadWorkspaceScreensDb(context);
68        synchronized(dataModel) {
69
70            List<ItemInfo> filteredItems = new ArrayList<>();
71            for (Pair<ItemInfo, Object> entry : mItemList) {
72                ItemInfo item = entry.first;
73                if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
74                        item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
75                    // Short-circuit this logic if the icon exists somewhere on the workspace
76                    if (shortcutExists(dataModel, item.getIntent(), item.user)) {
77                        continue;
78                    }
79                }
80
81                if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
82                    if (item instanceof AppInfo) {
83                        item = ((AppInfo) item).makeShortcut();
84                    }
85                }
86                if (item != null) {
87                    filteredItems.add(item);
88                }
89            }
90
91            for (ItemInfo item : filteredItems) {
92                // Find appropriate space for the item.
93                Pair<Long, int[]> coords = findSpaceForItem(app, dataModel, workspaceScreens,
94                        addedWorkspaceScreensFinal, item.spanX, item.spanY);
95                long screenId = coords.first;
96                int[] cordinates = coords.second;
97
98                ItemInfo itemInfo;
99                if (item instanceof ShortcutInfo || item instanceof FolderInfo ||
100                        item instanceof LauncherAppWidgetInfo) {
101                    itemInfo = item;
102                } else if (item instanceof AppInfo) {
103                    itemInfo = ((AppInfo) item).makeShortcut();
104                } else {
105                    throw new RuntimeException("Unexpected info type");
106                }
107
108                // Add the shortcut to the db
109                getModelWriter().addItemToDatabase(itemInfo,
110                        LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId,
111                        cordinates[0], cordinates[1]);
112
113                // Save the ShortcutInfo for binding in the workspace
114                addedItemsFinal.add(itemInfo);
115            }
116        }
117
118        // Update the workspace screens
119        updateScreens(context, workspaceScreens);
120
121        if (!addedItemsFinal.isEmpty()) {
122            scheduleCallbackTask(new CallbackTask() {
123                @Override
124                public void execute(Callbacks callbacks) {
125                    final ArrayList<ItemInfo> addAnimated = new ArrayList<>();
126                    final ArrayList<ItemInfo> addNotAnimated = new ArrayList<>();
127                    if (!addedItemsFinal.isEmpty()) {
128                        ItemInfo info = addedItemsFinal.get(addedItemsFinal.size() - 1);
129                        long lastScreenId = info.screenId;
130                        for (ItemInfo i : addedItemsFinal) {
131                            if (i.screenId == lastScreenId) {
132                                addAnimated.add(i);
133                            } else {
134                                addNotAnimated.add(i);
135                            }
136                        }
137                    }
138                    callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
139                            addNotAnimated, addAnimated);
140                }
141            });
142        }
143    }
144
145    protected void updateScreens(Context context, ArrayList<Long> workspaceScreens) {
146        LauncherModel.updateWorkspaceScreenOrder(context, workspaceScreens);
147    }
148
149    /**
150     * Returns true if the shortcuts already exists on the workspace. This must be called after
151     * the workspace has been loaded. We identify a shortcut by its intent.
152     */
153    protected boolean shortcutExists(BgDataModel dataModel, Intent intent, UserHandle user) {
154        final String compPkgName, intentWithPkg, intentWithoutPkg;
155        if (intent == null) {
156            // Skip items with null intents
157            return true;
158        }
159        if (intent.getComponent() != null) {
160            // If component is not null, an intent with null package will produce
161            // the same result and should also be a match.
162            compPkgName = intent.getComponent().getPackageName();
163            if (intent.getPackage() != null) {
164                intentWithPkg = intent.toUri(0);
165                intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0);
166            } else {
167                intentWithPkg = new Intent(intent).setPackage(compPkgName).toUri(0);
168                intentWithoutPkg = intent.toUri(0);
169            }
170        } else {
171            compPkgName = null;
172            intentWithPkg = intent.toUri(0);
173            intentWithoutPkg = intent.toUri(0);
174        }
175
176        boolean isLauncherAppTarget = Utilities.isLauncherAppTarget(intent);
177        synchronized (dataModel) {
178            for (ItemInfo item : dataModel.itemsIdMap) {
179                if (item instanceof ShortcutInfo) {
180                    ShortcutInfo info = (ShortcutInfo) item;
181                    if (item.getIntent() != null && info.user.equals(user)) {
182                        Intent copyIntent = new Intent(item.getIntent());
183                        copyIntent.setSourceBounds(intent.getSourceBounds());
184                        String s = copyIntent.toUri(0);
185                        if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) {
186                            return true;
187                        }
188
189                        // checking for existing promise icon with same package name
190                        if (isLauncherAppTarget
191                                && info.isPromise()
192                                && info.hasStatusFlag(ShortcutInfo.FLAG_AUTOINSTALL_ICON)
193                                && info.getTargetComponent() != null
194                                && compPkgName != null
195                                && compPkgName.equals(info.getTargetComponent().getPackageName())) {
196                            return true;
197                        }
198                    }
199                }
200            }
201        }
202        return false;
203    }
204
205    /**
206     * Find a position on the screen for the given size or adds a new screen.
207     * @return screenId and the coordinates for the item.
208     */
209    protected Pair<Long, int[]> findSpaceForItem(
210            LauncherAppState app, BgDataModel dataModel,
211            ArrayList<Long> workspaceScreens,
212            ArrayList<Long> addedWorkspaceScreensFinal,
213            int spanX, int spanY) {
214        LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>();
215
216        // Use sBgItemsIdMap as all the items are already loaded.
217        synchronized (dataModel) {
218            for (ItemInfo info : dataModel.itemsIdMap) {
219                if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
220                    ArrayList<ItemInfo> items = screenItems.get(info.screenId);
221                    if (items == null) {
222                        items = new ArrayList<>();
223                        screenItems.put(info.screenId, items);
224                    }
225                    items.add(info);
226                }
227            }
228        }
229
230        // Find appropriate space for the item.
231        long screenId = 0;
232        int[] cordinates = new int[2];
233        boolean found = false;
234
235        int screenCount = workspaceScreens.size();
236        // First check the preferred screen.
237        int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1;
238        if (preferredScreenIndex < screenCount) {
239            screenId = workspaceScreens.get(preferredScreenIndex);
240            found = findNextAvailableIconSpaceInScreen(
241                    app, screenItems.get(screenId), cordinates, spanX, spanY);
242        }
243
244        if (!found) {
245            // Search on any of the screens starting from the first screen.
246            for (int screen = 1; screen < screenCount; screen++) {
247                screenId = workspaceScreens.get(screen);
248                if (findNextAvailableIconSpaceInScreen(
249                        app, screenItems.get(screenId), cordinates, spanX, spanY)) {
250                    // We found a space for it
251                    found = true;
252                    break;
253                }
254            }
255        }
256
257        if (!found) {
258            // Still no position found. Add a new screen to the end.
259            screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(),
260                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
261                    .getLong(LauncherSettings.Settings.EXTRA_VALUE);
262
263            // Save the screen id for binding in the workspace
264            workspaceScreens.add(screenId);
265            addedWorkspaceScreensFinal.add(screenId);
266
267            // If we still can't find an empty space, then God help us all!!!
268            if (!findNextAvailableIconSpaceInScreen(
269                    app, screenItems.get(screenId), cordinates, spanX, spanY)) {
270                throw new RuntimeException("Can't find space to add the item");
271            }
272        }
273        return Pair.create(screenId, cordinates);
274    }
275
276    private boolean findNextAvailableIconSpaceInScreen(
277            LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
278            int[] xy, int spanX, int spanY) {
279        InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
280
281        GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
282        if (occupiedPos != null) {
283            for (ItemInfo r : occupiedPos) {
284                occupied.markCells(r, true);
285            }
286        }
287        return occupied.findVacantCell(xy, spanX, spanY);
288    }
289
290}
291