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.ItemInfo;
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                     ItemInfo 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                                 mLauncher.getCellPosMapper().mapModelToPresenter(info).screenId);
96                         finalItem =  info.getContents().remove(0);
97                         newIcon = mLauncher.getItemInflater().inflateItem(
98                                 finalItem, mLauncher.getModelWriter(), cellLayout);
99                         mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem,
100                                 info.container, info.screenId, info.cellX, info.cellY);
101                     }
102 
103                     // Remove the folder
104                     mLauncher.removeItem(folder.mFolderIcon, info, true /* deleteFromDb */,
105                             "folder removed because there's only 1 item in it");
106                     if (folder.mFolderIcon instanceof DropTarget) {
107                         folder.mDragController.removeDropTarget((DropTarget) folder.mFolderIcon);
108                     }
109 
110                     if (newIcon != null) {
111                         // We add the child after removing the folder to prevent both from existing
112                         // at the same time in the CellLayout.  We need to add the new item with
113                         // addInScreenFromBind() to ensure that hotseat items are placed correctly.
114                         mLauncher.getWorkspace().addInScreenFromBind(newIcon, info);
115 
116                         // Focus the newly created child
117                         newIcon.requestFocus();
118                     }
119                     if (finalItem != null) {
120                         StatsLogger logger = mLauncher.getStatsLogManager().logger()
121                                 .withItemInfo(finalItem);
122                         ((Optional<InstanceId>) folder.mDragController.getLogInstanceId())
123                                 .map(logger::withInstanceId)
124                                 .orElse(logger)
125                                 .log(LAUNCHER_FOLDER_CONVERTED_TO_ICON);
126                     }
127                 }
128             }
129         };
130         View finalChild = folder.mContent.getLastItem();
131         if (finalChild != null) {
132             folder.mFolderIcon.performDestroyAnimation(onCompleteRunnable);
133         } else {
134             onCompleteRunnable.run();
135         }
136         return true;
137     }
138 
139 
interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder)140     boolean interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder) {
141         if (mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
142             // Do not close the container if in drag and drop.
143             if (!dl.isEventOverView(mLauncher.getDropTargetBar(), ev)) {
144                 return true;
145             }
146         } else {
147             // TODO: add ww log if need to gather tap outside to close folder
148             folder.close(true);
149             return true;
150         }
151         return false;
152     }
153 
154     private static class FallbackDelegate extends LauncherDelegate {
155 
156         private final ActivityContext mContext;
157         private ModelWriter mWriter;
158 
FallbackDelegate(ActivityContext context)159         FallbackDelegate(ActivityContext context) {
160             super(null);
161             mContext = context;
162         }
163 
164         @Override
init(Folder folder, FolderIcon icon)165         void init(Folder folder, FolderIcon icon) {
166             folder.setDragController(mContext.getDragController());
167         }
168 
169         @Override
isDraggingEnabled()170         boolean isDraggingEnabled() {
171             return false;
172         }
173 
174         @Override
beginDragShared(View child, DragSource source, DragOptions options)175         void beginDragShared(View child, DragSource source, DragOptions options) { }
176 
177         @Override
getModelWriter()178         ModelWriter getModelWriter() {
179             if (mWriter == null) {
180                 mWriter = LauncherAppState.getInstance((Context) mContext).getModel().getWriter(
181                         false, mContext.getCellPosMapper(), null);
182             }
183             return mWriter;
184         }
185 
186         @Override
forEachVisibleWorkspacePage(Consumer<View> callback)187         void forEachVisibleWorkspacePage(Consumer<View> callback) { }
188 
189         @Override
getLauncher()190         Launcher getLauncher() {
191             return null;
192         }
193 
194         @Override
replaceFolderWithFinalItem(Folder folder)195         boolean replaceFolderWithFinalItem(Folder folder) {
196             return false;
197         }
198 
199         @Override
interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder)200         boolean interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder) {
201             folder.close(true);
202             return true;
203         }
204     }
205 
from(ActivityContext context)206     static LauncherDelegate from(ActivityContext context) {
207         return context instanceof Launcher
208                 ? new LauncherDelegate((Launcher) context)
209                 : new FallbackDelegate(context);
210     }
211 }
212