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 package com.android.launcher3.widget;
17 
18 import android.content.Context;
19 import android.graphics.Point;
20 import android.graphics.Rect;
21 import android.util.AttributeSet;
22 import android.util.Log;
23 import android.view.View;
24 import android.view.View.OnClickListener;
25 import android.view.View.OnLongClickListener;
26 import android.widget.Toast;
27 
28 import androidx.annotation.GuardedBy;
29 import androidx.annotation.Nullable;
30 import androidx.core.view.ViewCompat;
31 
32 import com.android.launcher3.DeviceProfile;
33 import com.android.launcher3.DragSource;
34 import com.android.launcher3.DropTarget.DragObject;
35 import com.android.launcher3.Insettable;
36 import com.android.launcher3.Launcher;
37 import com.android.launcher3.R;
38 import com.android.launcher3.Utilities;
39 import com.android.launcher3.dragndrop.DragOptions;
40 import com.android.launcher3.popup.PopupDataProvider;
41 import com.android.launcher3.testing.TestLogging;
42 import com.android.launcher3.testing.TestProtocol;
43 import com.android.launcher3.touch.ItemLongClickListener;
44 import com.android.launcher3.util.SystemUiController;
45 import com.android.launcher3.util.Themes;
46 import com.android.launcher3.views.AbstractSlideInView;
47 import com.android.launcher3.views.ArrowTipView;
48 
49 /**
50  * Base class for various widgets popup
51  */
52 public abstract class BaseWidgetSheet extends AbstractSlideInView<Launcher>
53         implements OnClickListener, OnLongClickListener, DragSource,
54         PopupDataProvider.PopupDataChangeListener, Insettable {
55     /** The default number of cells that can fit horizontally in a widget sheet. */
56     protected static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4;
57     /**
58      * The maximum scale, [0, 1], of the device screen width that the widgets picker can consume
59      * on large screen devices.
60      */
61     protected static final float MAX_WIDTH_SCALE_FOR_LARGER_SCREEN = 0.89f;
62 
63     protected static final String KEY_WIDGETS_EDUCATION_TIP_SEEN =
64             "launcher.widgets_education_tip_seen";
65     protected final Rect mInsets = new Rect();
66 
67     /* Touch handling related member variables. */
68     private Toast mWidgetInstructionToast;
69 
70     private int mContentHorizontalMarginInPx;
71 
BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr)72     public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
73         super(context, attrs, defStyleAttr);
74         mContentHorizontalMarginInPx = getResources().getDimensionPixelSize(
75                 R.dimen.widget_list_horizontal_margin);
76     }
77 
getScrimColor(Context context)78     protected int getScrimColor(Context context) {
79         return context.getResources().getColor(R.color.widgets_picker_scrim);
80     }
81 
82     @Override
onAttachedToWindow()83     protected void onAttachedToWindow() {
84         super.onAttachedToWindow();
85         mActivityContext.getPopupDataProvider().setChangeListener(this);
86     }
87 
88     @Override
onDetachedFromWindow()89     protected void onDetachedFromWindow() {
90         super.onDetachedFromWindow();
91         mActivityContext.getPopupDataProvider().setChangeListener(null);
92     }
93 
94     @Override
onClick(View v)95     public final void onClick(View v) {
96         Object tag = null;
97         if (v instanceof WidgetCell) {
98             tag = v.getTag();
99         } else if (v.getParent() instanceof WidgetCell) {
100             tag = ((WidgetCell) v.getParent()).getTag();
101         }
102         if (tag instanceof PendingAddShortcutInfo) {
103             mWidgetInstructionToast = showShortcutToast(getContext(), mWidgetInstructionToast);
104         } else {
105             mWidgetInstructionToast = showWidgetToast(getContext(), mWidgetInstructionToast);
106         }
107 
108     }
109 
110     @Override
onLongClick(View v)111     public boolean onLongClick(View v) {
112         if (TestProtocol.sDebugTracing) {
113             Log.d(TestProtocol.NO_DROP_TARGET, "1");
114         }
115         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick");
116         v.cancelLongPress();
117         if (!ItemLongClickListener.canStartDrag(mActivityContext)) return false;
118 
119         if (v instanceof WidgetCell) {
120             return beginDraggingWidget((WidgetCell) v);
121         } else if (v.getParent() instanceof WidgetCell) {
122             return beginDraggingWidget((WidgetCell) v.getParent());
123         }
124         return true;
125     }
126 
127     @Override
setInsets(Rect insets)128     public void setInsets(Rect insets) {
129         mInsets.set(insets);
130         int contentHorizontalMarginInPx = getResources().getDimensionPixelSize(
131                 R.dimen.widget_list_horizontal_margin);
132         if (contentHorizontalMarginInPx != mContentHorizontalMarginInPx) {
133             onContentHorizontalMarginChanged(contentHorizontalMarginInPx);
134             mContentHorizontalMarginInPx = contentHorizontalMarginInPx;
135         }
136     }
137 
138     /** Called when the horizontal margin of the content view has changed. */
onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)139     protected abstract void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx);
140 
141     /**
142      * Measures the dimension of this view and its children by taking system insets, navigation bar,
143      * status bar, into account.
144      */
145     @GuardedBy("MainThread")
doMeasure(int widthMeasureSpec, int heightMeasureSpec)146     protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) {
147         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
148         int widthUsed;
149         if (mInsets.bottom > 0) {
150             widthUsed = mInsets.left + mInsets.right;
151         } else {
152             Rect padding = deviceProfile.workspacePadding;
153             widthUsed = Math.max(padding.left + padding.right,
154                     2 * (mInsets.left + mInsets.right));
155         }
156 
157         if (deviceProfile.isTablet || deviceProfile.isTwoPanels) {
158             // In large screen devices, we restrict the width of the widgets picker to show part of
159             // the home screen. Let's ensure the minimum width used is at least the minimum width
160             // that isn't taken by the widgets picker.
161             int minUsedWidth = (int) (deviceProfile.availableWidthPx
162                     * (1 - MAX_WIDTH_SCALE_FOR_LARGER_SCREEN));
163             widthUsed = Math.max(widthUsed, minUsedWidth);
164         }
165 
166         int heightUsed = mInsets.top + deviceProfile.edgeMarginPx;
167         measureChildWithMargins(mContent, widthMeasureSpec,
168                 widthUsed, heightMeasureSpec, heightUsed);
169         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
170                 MeasureSpec.getSize(heightMeasureSpec));
171     }
172 
173     /** Returns the number of cells that can fit horizontally in a given {@code content}. */
computeMaxHorizontalSpans(View content, int contentHorizontalPaddingPx)174     protected int computeMaxHorizontalSpans(View content, int contentHorizontalPaddingPx) {
175         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
176         int availableWidth = content.getMeasuredWidth() - contentHorizontalPaddingPx;
177         Point cellSize = deviceProfile.getCellSize();
178         if (cellSize.x > 0) {
179             return availableWidth / cellSize.x;
180         }
181         return DEFAULT_MAX_HORIZONTAL_SPANS;
182     }
183 
beginDraggingWidget(WidgetCell v)184     private boolean beginDraggingWidget(WidgetCell v) {
185         if (TestProtocol.sDebugTracing) {
186             Log.d(TestProtocol.NO_DROP_TARGET, "2");
187         }
188         // Get the widget preview as the drag representation
189         WidgetImageView image = v.getWidgetView();
190 
191         // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and
192         // we abort the drag.
193         if (image.getDrawable() == null && v.getAppWidgetHostViewPreview() == null) {
194             return false;
195         }
196 
197         PendingItemDragHelper dragHelper = new PendingItemDragHelper(v);
198         // RemoteViews are being rendered in AppWidgetHostView in WidgetCell. And thus, the scale of
199         // RemoteViews is equivalent to the AppWidgetHostView scale.
200         dragHelper.setRemoteViewsPreview(v.getRemoteViewsPreview(), v.getAppWidgetHostViewScale());
201         dragHelper.setAppWidgetHostViewPreview(v.getAppWidgetHostViewPreview());
202 
203         if (image.getDrawable() != null) {
204             int[] loc = new int[2];
205             getPopupContainer().getLocationInDragLayer(image, loc);
206 
207             dragHelper.startDrag(image.getBitmapBounds(), image.getDrawable().getIntrinsicWidth(),
208                     image.getWidth(), new Point(loc[0], loc[1]), this, new DragOptions());
209         } else {
210             NavigableAppWidgetHostView preview = v.getAppWidgetHostViewPreview();
211             int[] loc = new int[2];
212             getPopupContainer().getLocationInDragLayer(preview, loc);
213             Rect r = new Rect();
214             preview.getWorkspaceVisualDragBounds(r);
215             dragHelper.startDrag(r, preview.getMeasuredWidth(), preview.getMeasuredWidth(),
216                     new Point(loc[0], loc[1]), this, new DragOptions());
217         }
218         close(true);
219         return true;
220     }
221 
222     //
223     // Drag related handling methods that implement {@link DragSource} interface.
224     //
225 
226     @Override
onDropCompleted(View target, DragObject d, boolean success)227     public void onDropCompleted(View target, DragObject d, boolean success) { }
228 
229 
onCloseComplete()230     protected void onCloseComplete() {
231         super.onCloseComplete();
232         clearNavBarColor();
233     }
234 
clearNavBarColor()235     protected void clearNavBarColor() {
236         getSystemUiController().updateUiState(
237                 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 0);
238     }
239 
setupNavBarColor()240     protected void setupNavBarColor() {
241         boolean isSheetDark = Themes.getAttrBoolean(getContext(), R.attr.isMainColorDark);
242         getSystemUiController().updateUiState(
243                 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET,
244                 isSheetDark ? SystemUiController.FLAG_DARK_NAV : SystemUiController.FLAG_LIGHT_NAV);
245     }
246 
getSystemUiController()247     protected SystemUiController getSystemUiController() {
248         return mActivityContext.getSystemUiController();
249     }
250 
251     /**
252      * Show Widget tap toast prompting user to drag instead
253      */
showWidgetToast(Context context, Toast toast)254     public static Toast showWidgetToast(Context context, Toast toast) {
255         // Let the user know that they have to long press to add a widget
256         if (toast != null) {
257             toast.cancel();
258         }
259 
260         CharSequence msg = Utilities.wrapForTts(
261                 context.getText(R.string.long_press_widget_to_add),
262                 context.getString(R.string.long_accessible_way_to_add));
263         toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
264         toast.show();
265         return toast;
266     }
267 
268     /**
269      * Show shortcut tap toast prompting user to drag instead.
270      */
showShortcutToast(Context context, Toast toast)271     private static Toast showShortcutToast(Context context, Toast toast) {
272         // Let the user know that they have to long press to add a widget
273         if (toast != null) {
274             toast.cancel();
275         }
276 
277         CharSequence msg = Utilities.wrapForTts(
278                 context.getText(R.string.long_press_shortcut_to_add),
279                 context.getString(R.string.long_accessible_way_to_add_shortcut));
280         toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
281         toast.show();
282         return toast;
283     }
284 
285     /** Shows education tip on top center of {@code view} if view is laid out. */
286     @Nullable
showEducationTipOnViewIfPossible(@ullable View view)287     protected ArrowTipView showEducationTipOnViewIfPossible(@Nullable View view) {
288         if (view == null || !ViewCompat.isLaidOut(view)) {
289             return null;
290         }
291         int[] coords = new int[2];
292         view.getLocationOnScreen(coords);
293         ArrowTipView arrowTipView =
294                 new ArrowTipView(mActivityContext,  /* isPointingUp= */ false).showAtLocation(
295                         getContext().getString(R.string.long_press_widget_to_add),
296                         /* arrowXCoord= */coords[0] + view.getWidth() / 2,
297                         /* yCoord= */coords[1]);
298         if (arrowTipView != null) {
299             mActivityContext.getSharedPrefs().edit()
300                     .putBoolean(KEY_WIDGETS_EDUCATION_TIP_SEEN, true).apply();
301         }
302         return arrowTipView;
303     }
304 
305     /** Returns {@code true} if tip has previously been shown on any of {@link BaseWidgetSheet}. */
hasSeenEducationTip()306     protected boolean hasSeenEducationTip() {
307         return mActivityContext.getSharedPrefs().getBoolean(KEY_WIDGETS_EDUCATION_TIP_SEEN, false)
308                 || Utilities.IS_RUNNING_IN_TEST_HARNESS;
309     }
310 }
311