1 /*
2  * Copyright (C) 2021 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 package com.android.launcher3.folder;
17 
18 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_CONVERTED_TO_ICON;
19 
20 import android.content.Context;
21 import android.view.MotionEvent;
22 import android.view.View;
23 
24 import androidx.annotation.Nullable;
25 
26 import com.android.launcher3.CellLayout;
27 import com.android.launcher3.DragSource;
28 import com.android.launcher3.DropTarget;
29 import com.android.launcher3.Launcher;
30 import com.android.launcher3.LauncherAppState;
31 import com.android.launcher3.dragndrop.DragOptions;
32 import com.android.launcher3.logging.InstanceId;
33 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
34 import com.android.launcher3.model.ModelWriter;
35 import com.android.launcher3.model.data.FolderInfo;
36 import com.android.launcher3.model.data.WorkspaceItemInfo;
37 import com.android.launcher3.views.ActivityContext;
38 import com.android.launcher3.views.BaseDragLayer;
39 
40 import java.util.Optional;
41 import java.util.function.Consumer;
42 
43 /**
44  * Wrapper around Launcher methods to allow folders in non-launcher context
45  */
46 public class LauncherDelegate {
47 
48     private final Launcher mLauncher;
49 
LauncherDelegate(Launcher launcher)50     private LauncherDelegate(Launcher launcher) {
51         mLauncher = launcher;
52     }
53 
init(Folder folder, FolderIcon icon)54     void init(Folder folder, FolderIcon icon) {
55         folder.setDragController(mLauncher.getDragController());
56         icon.setOnFocusChangeListener(mLauncher.getFocusHandler());
57     }
58 
isDraggingEnabled()59     boolean isDraggingEnabled() {
60         return mLauncher.isDraggingEnabled();
61     }
62 
beginDragShared(View child, DragSource source, DragOptions options)63     void beginDragShared(View child, DragSource source, DragOptions options) {
64         mLauncher.getWorkspace().beginDragShared(child, source, options);
65     }
66 
getModelWriter()67     ModelWriter getModelWriter() {
68         return mLauncher.getModelWriter();
69     }
70 
forEachVisibleWorkspacePage(Consumer<View> callback)71     void forEachVisibleWorkspacePage(Consumer<View> callback) {
72         mLauncher.getWorkspace().forEachVisiblePage(callback);
73     }
74 
75     @Nullable
getLauncher()76     Launcher getLauncher() {
77         return mLauncher;
78     }
79 
replaceFolderWithFinalItem(Folder folder)80     boolean replaceFolderWithFinalItem(Folder folder) {
81         // Add the last remaining child to the workspace in place of the folder
82         Runnable onCompleteRunnable = new Runnable() {
83             @Override
84             public void run() {
85                 int itemCount = folder.getItemCount();
86                 FolderInfo info = folder.mInfo;
87                 if (itemCount <= 1) {
88                     View newIcon = null;
89                     WorkspaceItemInfo finalItem = null;
90 
91                     if (itemCount == 1) {
92                         // Move the item from the folder to the workspace, in the position of the
93                         // folder
94                         CellLayout cellLayout = mLauncher.getCellLayout(info.container,
95                                 info.screenId);
96                         finalItem =  info.contents.remove(0);
97                         newIcon = mLauncher.createShortcut(cellLayout, finalItem);
98                         mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem,
99                                 info.container, info.screenId, info.cellX, info.cellY);
100                     }
101 
102                     // Remove the folder
103                     mLauncher.removeItem(folder.mFolderIcon, info, true /* deleteFromDb */);
104                     if (folder.mFolderIcon instanceof DropTarget) {
105                         folder.mDragController.removeDropTarget((DropTarget) folder.mFolderIcon);
106                     }
107 
108                     if (newIcon != null) {
109                         // We add the child after removing the folder to prevent both from existing
110                         // at the same time in the CellLayout.  We need to add the new item with
111                         // addInScreenFromBind() to ensure that hotseat items are placed correctly.
112                         mLauncher.getWorkspace().addInScreenFromBind(newIcon, info);
113 
114                         // Focus the newly created child
115                         newIcon.requestFocus();
116                     }
117                     if (finalItem != null) {
118                         StatsLogger logger = mLauncher.getStatsLogManager().logger()
119                                 .withItemInfo(finalItem);
120                         ((Optional<InstanceId>) folder.mDragController.getLogInstanceId())
121                                 .map(logger::withInstanceId)
122                                 .orElse(logger)
123                                 .log(LAUNCHER_FOLDER_CONVERTED_TO_ICON);
124                     }
125                 }
126             }
127         };
128         View finalChild = folder.mContent.getLastItem();
129         if (finalChild != null) {
130             folder.mFolderIcon.performDestroyAnimation(onCompleteRunnable);
131         } else {
132             onCompleteRunnable.run();
133         }
134         return true;
135     }
136 
137 
interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder)138     boolean interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder) {
139         if (mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
140             // Do not close the container if in drag and drop.
141             if (!dl.isEventOverView(mLauncher.getDropTargetBar(), ev)) {
142                 return true;
143             }
144         } else {
145             // TODO: add ww log if need to gather tap outside to close folder
146             folder.close(true);
147             return true;
148         }
149         return false;
150     }
151 
152     private static class FallbackDelegate extends LauncherDelegate {
153 
154         private final ActivityContext mContext;
155         private ModelWriter mWriter;
156 
FallbackDelegate(ActivityContext context)157         FallbackDelegate(ActivityContext context) {
158             super(null);
159             mContext = context;
160         }
161 
162         @Override
init(Folder folder, FolderIcon icon)163         void init(Folder folder, FolderIcon icon) {
164             folder.setDragController(mContext.getDragController());
165         }
166 
167         @Override
isDraggingEnabled()168         boolean isDraggingEnabled() {
169             return false;
170         }
171 
172         @Override
beginDragShared(View child, DragSource source, DragOptions options)173         void beginDragShared(View child, DragSource source, DragOptions options) { }
174 
175         @Override
getModelWriter()176         ModelWriter getModelWriter() {
177             if (mWriter == null) {
178                 mWriter = LauncherAppState.getInstance((Context) mContext).getModel()
179                         .getWriter(false, false, null);
180             }
181             return mWriter;
182         }
183 
184         @Override
forEachVisibleWorkspacePage(Consumer<View> callback)185         void forEachVisibleWorkspacePage(Consumer<View> callback) { }
186 
187         @Override
getLauncher()188         Launcher getLauncher() {
189             return null;
190         }
191 
192         @Override
replaceFolderWithFinalItem(Folder folder)193         boolean replaceFolderWithFinalItem(Folder folder) {
194             return false;
195         }
196 
197         @Override
interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder)198         boolean interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder) {
199             folder.close(true);
200             return true;
201         }
202     }
203 
from(ActivityContext context)204     static LauncherDelegate from(ActivityContext context) {
205         return context instanceof Launcher
206                 ? new LauncherDelegate((Launcher) context)
207                 : new FallbackDelegate(context);
208     }
209 }
210