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