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 
17 package com.android.launcher3.widget;
18 
19 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
20 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
21 
22 import android.animation.PropertyValuesHolder;
23 import android.content.Context;
24 import android.graphics.Rect;
25 import android.util.AttributeSet;
26 import android.util.IntProperty;
27 import android.util.Pair;
28 import android.view.Gravity;
29 import android.view.LayoutInflater;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.animation.Interpolator;
34 import android.widget.ScrollView;
35 import android.widget.TableLayout;
36 import android.widget.TableRow;
37 import android.widget.TextView;
38 
39 import com.android.launcher3.R;
40 import com.android.launcher3.anim.PendingAnimation;
41 import com.android.launcher3.model.WidgetItem;
42 import com.android.launcher3.model.data.ItemInfo;
43 import com.android.launcher3.util.PackageUserKey;
44 import com.android.launcher3.widget.util.WidgetsTableUtils;
45 
46 import java.util.List;
47 
48 /**
49  * Bottom sheet for the "Widgets" system shortcut in the long-press popup.
50  */
51 public class WidgetsBottomSheet extends BaseWidgetSheet {
52     private static final String TAG = "WidgetsBottomSheet";
53 
54     private static final IntProperty<View> PADDING_BOTTOM =
55             new IntProperty<View>("paddingBottom") {
56                 @Override
57                 public void setValue(View view, int paddingBottom) {
58                     view.setPadding(view.getPaddingLeft(), view.getPaddingTop(),
59                             view.getPaddingRight(), paddingBottom);
60                 }
61 
62                 @Override
63                 public Integer get(View view) {
64                     return view.getPaddingBottom();
65                 }
66             };
67 
68     private static final int DEFAULT_CLOSE_DURATION = 200;
69     private static final long EDUCATION_TIP_DELAY_MS = 300;
70 
71     private ItemInfo mOriginalItemInfo;
72     private int mMaxHorizontalSpan = DEFAULT_MAX_HORIZONTAL_SPANS;
73     private final int mWidgetCellHorizontalPadding;
74 
75     private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
76             new OnLayoutChangeListener() {
77                 @Override
78                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
79                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
80                     if (hasSeenEducationTip()) {
81                         removeOnLayoutChangeListener(this);
82                         return;
83                     }
84                     // Widgets are loaded asynchronously, We are adding a delay because we only want
85                     // to show the tip when the widget preview has finished loading and rendering in
86                     // this view.
87                     removeCallbacks(mShowEducationTipTask);
88                     postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS);
89                 }
90             };
91 
92     private final Runnable mShowEducationTipTask = () -> {
93         if (hasSeenEducationTip()) {
94             removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
95             return;
96         }
97         View viewForTip = ((ViewGroup) ((TableLayout) findViewById(R.id.widgets_table))
98                                     .getChildAt(0)).getChildAt(0);
99         if (showEducationTipOnViewIfPossible(viewForTip) != null) {
100             removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
101         }
102     };
103 
WidgetsBottomSheet(Context context, AttributeSet attrs)104     public WidgetsBottomSheet(Context context, AttributeSet attrs) {
105         this(context, attrs, 0);
106     }
107 
WidgetsBottomSheet(Context context, AttributeSet attrs, int defStyleAttr)108     public WidgetsBottomSheet(Context context, AttributeSet attrs, int defStyleAttr) {
109         super(context, attrs, defStyleAttr);
110         setWillNotDraw(false);
111         if (!hasSeenEducationTip()) {
112             addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
113         }
114         mWidgetCellHorizontalPadding = getResources().getDimensionPixelSize(
115                 R.dimen.widget_cell_horizontal_padding);
116     }
117 
118     @Override
onFinishInflate()119     protected void onFinishInflate() {
120         super.onFinishInflate();
121         mContent = findViewById(R.id.widgets_bottom_sheet);
122     }
123 
124     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)125     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
126         doMeasure(widthMeasureSpec, heightMeasureSpec);
127         if (updateMaxSpansPerRow()) {
128             doMeasure(widthMeasureSpec, heightMeasureSpec);
129         }
130     }
131 
132     /** Returns {@code true} if the max spans have been updated. */
updateMaxSpansPerRow()133     private boolean updateMaxSpansPerRow() {
134         if (getMeasuredWidth() == 0) return false;
135 
136         int maxHorizontalSpan = computeMaxHorizontalSpans(mContent, mWidgetCellHorizontalPadding);
137         if (mMaxHorizontalSpan != maxHorizontalSpan) {
138             // Ensure the table layout is showing widgets in the right column after measure.
139             mMaxHorizontalSpan = maxHorizontalSpan;
140             onWidgetsBound();
141             return true;
142         }
143         return false;
144     }
145 
146     @Override
onLayout(boolean changed, int l, int t, int r, int b)147     protected void onLayout(boolean changed, int l, int t, int r, int b) {
148         int width = r - l;
149         int height = b - t;
150 
151         // Content is laid out as center bottom aligned.
152         int contentWidth = mContent.getMeasuredWidth();
153         int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
154         mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
155                 contentLeft + contentWidth, height);
156 
157         setTranslationShift(mTranslationShift);
158 
159         ScrollView widgetsTableScrollView = findViewById(R.id.widgets_table_scroll_view);
160         TableLayout widgetsTable = findViewById(R.id.widgets_table);
161         if (widgetsTable.getMeasuredHeight() > widgetsTableScrollView.getMeasuredHeight()) {
162             findViewById(R.id.collapse_handle).setVisibility(VISIBLE);
163         }
164     }
165 
populateAndShow(ItemInfo itemInfo)166     public void populateAndShow(ItemInfo itemInfo) {
167         mOriginalItemInfo = itemInfo;
168         ((TextView) findViewById(R.id.title)).setText(mOriginalItemInfo.title);
169 
170         onWidgetsBound();
171         attachToContainer();
172         mIsOpen = false;
173         animateOpen();
174     }
175 
176     @Override
onWidgetsBound()177     public void onWidgetsBound() {
178         List<WidgetItem> widgets = mActivityContext.getPopupDataProvider().getWidgetsForPackageUser(
179                 new PackageUserKey(
180                         mOriginalItemInfo.getTargetComponent().getPackageName(),
181                         mOriginalItemInfo.user));
182 
183         TableLayout widgetsTable = findViewById(R.id.widgets_table);
184         widgetsTable.removeAllViews();
185 
186         WidgetsTableUtils.groupWidgetItemsIntoTableWithReordering(widgets, mMaxHorizontalSpan)
187                 .forEach(row -> {
188                     TableRow tableRow = new TableRow(getContext());
189                     tableRow.setGravity(Gravity.TOP);
190                     row.forEach(widgetItem -> {
191                         WidgetCell widget = addItemCell(tableRow);
192                         widget.applyFromCellItem(widgetItem);
193                     });
194                     widgetsTable.addView(tableRow);
195                 });
196     }
197 
198     @Override
onControllerInterceptTouchEvent(MotionEvent ev)199     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
200         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
201             mNoIntercept = false;
202             ScrollView scrollView = findViewById(R.id.widgets_table_scroll_view);
203             if (getPopupContainer().isEventOverView(scrollView, ev)
204                     && scrollView.getScrollY() > 0) {
205                 mNoIntercept = true;
206             }
207         }
208         return super.onControllerInterceptTouchEvent(ev);
209     }
210 
addItemCell(ViewGroup parent)211     protected WidgetCell addItemCell(ViewGroup parent) {
212         WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext())
213                 .inflate(R.layout.widget_cell, parent, false);
214 
215         View previewContainer = widget.findViewById(R.id.widget_preview_container);
216         previewContainer.setOnClickListener(this);
217         previewContainer.setOnLongClickListener(this);
218         widget.setAnimatePreview(false);
219         widget.setSourceContainer(CONTAINER_BOTTOM_WIDGETS_TRAY);
220 
221         parent.addView(widget);
222         return widget;
223     }
224 
animateOpen()225     private void animateOpen() {
226         if (mIsOpen || mOpenCloseAnimator.isRunning()) {
227             return;
228         }
229         mIsOpen = true;
230         setupNavBarColor();
231         mOpenCloseAnimator.setValues(
232                 PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
233         mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
234         mOpenCloseAnimator.start();
235     }
236 
237     @Override
handleClose(boolean animate)238     protected void handleClose(boolean animate) {
239         handleClose(animate, DEFAULT_CLOSE_DURATION);
240     }
241 
242     @Override
isOfType(@loatingViewType int type)243     protected boolean isOfType(@FloatingViewType int type) {
244         return (type & TYPE_WIDGETS_BOTTOM_SHEET) != 0;
245     }
246 
247     @Override
setInsets(Rect insets)248     public void setInsets(Rect insets) {
249         super.setInsets(insets);
250 
251         mContent.setPadding(mContent.getPaddingStart(),
252                 mContent.getPaddingTop(), mContent.getPaddingEnd(), insets.bottom);
253         if (insets.bottom > 0) {
254             setupNavBarColor();
255         } else {
256             clearNavBarColor();
257         }
258     }
259 
260     @Override
onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)261     protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) {
262         ViewGroup.MarginLayoutParams layoutParams =
263                 ((ViewGroup.MarginLayoutParams) findViewById(R.id.widgets_table).getLayoutParams());
264         layoutParams.setMarginStart(contentHorizontalMarginInPx);
265         layoutParams.setMarginEnd(contentHorizontalMarginInPx);
266     }
267 
268     @Override
getAccessibilityTarget()269     protected Pair<View, String> getAccessibilityTarget() {
270         return Pair.create(findViewById(R.id.title),  getContext().getString(
271                 mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
272     }
273 
274     @Override
addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target)275     public void addHintCloseAnim(
276             float distanceToMove, Interpolator interpolator, PendingAnimation target) {
277         target.setInt(this, PADDING_BOTTOM, (int) (distanceToMove + mInsets.bottom), interpolator);
278     }
279 }
280