1 /*
2  * Copyright (C) 2015 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.internal.view;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.graphics.Point;
23 import android.graphics.Rect;
24 import android.view.ActionMode;
25 import android.view.Menu;
26 import android.view.MenuInflater;
27 import android.view.MenuItem;
28 import android.view.View;
29 import android.view.ViewConfiguration;
30 import android.view.ViewGroup;
31 import android.view.ViewParent;
32 import android.view.WindowManager;
33 
34 import android.widget.PopupWindow;
35 import com.android.internal.R;
36 import com.android.internal.util.Preconditions;
37 import com.android.internal.view.menu.MenuBuilder;
38 import com.android.internal.widget.FloatingToolbar;
39 
40 import java.util.Arrays;
41 
42 public final class FloatingActionMode extends ActionMode {
43 
44     private static final int MAX_HIDE_DURATION = 3000;
45     private static final int MOVING_HIDE_DELAY = 50;
46 
47     @NonNull private final Context mContext;
48     @NonNull private final ActionMode.Callback2 mCallback;
49     @NonNull private final MenuBuilder mMenu;
50     @NonNull private final Rect mContentRect;
51     @NonNull private final Rect mContentRectOnScreen;
52     @NonNull private final Rect mPreviousContentRectOnScreen;
53     @NonNull private final int[] mViewPositionOnScreen;
54     @NonNull private final int[] mPreviousViewPositionOnScreen;
55     @NonNull private final int[] mRootViewPositionOnScreen;
56     @NonNull private final Rect mViewRectOnScreen;
57     @NonNull private final Rect mPreviousViewRectOnScreen;
58     @NonNull private final Rect mScreenRect;
59     @NonNull private final View mOriginatingView;
60     @NonNull private final Point mDisplaySize;
61     private final int mBottomAllowance;
62 
63     private final Runnable mMovingOff = new Runnable() {
64         public void run() {
65             if (isViewStillActive()) {
66                 mFloatingToolbarVisibilityHelper.setMoving(false);
67                 mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
68             }
69         }
70     };
71 
72     private final Runnable mHideOff = new Runnable() {
73         public void run() {
74             if (isViewStillActive()) {
75                 mFloatingToolbarVisibilityHelper.setHideRequested(false);
76                 mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
77             }
78         }
79     };
80 
81     @NonNull private FloatingToolbar mFloatingToolbar;
82     @NonNull private FloatingToolbarVisibilityHelper mFloatingToolbarVisibilityHelper;
83 
FloatingActionMode( Context context, ActionMode.Callback2 callback, View originatingView, FloatingToolbar floatingToolbar)84     public FloatingActionMode(
85             Context context, ActionMode.Callback2 callback,
86             View originatingView, FloatingToolbar floatingToolbar) {
87         mContext = Preconditions.checkNotNull(context);
88         mCallback = Preconditions.checkNotNull(callback);
89         mMenu = new MenuBuilder(context).setDefaultShowAsAction(
90                 MenuItem.SHOW_AS_ACTION_IF_ROOM);
91         setType(ActionMode.TYPE_FLOATING);
92         mMenu.setCallback(new MenuBuilder.Callback() {
93             @Override
94             public void onMenuModeChange(MenuBuilder menu) {}
95 
96             @Override
97             public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
98                 return mCallback.onActionItemClicked(FloatingActionMode.this, item);
99             }
100         });
101         mContentRect = new Rect();
102         mContentRectOnScreen = new Rect();
103         mPreviousContentRectOnScreen = new Rect();
104         mViewPositionOnScreen = new int[2];
105         mPreviousViewPositionOnScreen = new int[2];
106         mRootViewPositionOnScreen = new int[2];
107         mViewRectOnScreen = new Rect();
108         mPreviousViewRectOnScreen = new Rect();
109         mScreenRect = new Rect();
110         mOriginatingView = Preconditions.checkNotNull(originatingView);
111         mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
112         // Allow the content rect to overshoot a little bit beyond the
113         // bottom view bound if necessary.
114         mBottomAllowance = context.getResources()
115                 .getDimensionPixelSize(R.dimen.content_rect_bottom_clip_allowance);
116         mDisplaySize = new Point();
117         setFloatingToolbar(Preconditions.checkNotNull(floatingToolbar));
118     }
119 
setFloatingToolbar(FloatingToolbar floatingToolbar)120     private void setFloatingToolbar(FloatingToolbar floatingToolbar) {
121         mFloatingToolbar = floatingToolbar
122                 .setMenu(mMenu)
123                 .setOnMenuItemClickListener(item -> mMenu.performItemAction(item, 0));
124         mFloatingToolbarVisibilityHelper = new FloatingToolbarVisibilityHelper(mFloatingToolbar);
125         mFloatingToolbarVisibilityHelper.activate();
126     }
127 
128     @Override
setTitle(CharSequence title)129     public void setTitle(CharSequence title) {}
130 
131     @Override
setTitle(int resId)132     public void setTitle(int resId) {}
133 
134     @Override
setSubtitle(CharSequence subtitle)135     public void setSubtitle(CharSequence subtitle) {}
136 
137     @Override
setSubtitle(int resId)138     public void setSubtitle(int resId) {}
139 
140     @Override
setCustomView(View view)141     public void setCustomView(View view) {}
142 
143     @Override
invalidate()144     public void invalidate() {
145         mCallback.onPrepareActionMode(this, mMenu);
146         invalidateContentRect();  // Will re-layout and show the toolbar if necessary.
147     }
148 
149     @Override
invalidateContentRect()150     public void invalidateContentRect() {
151         mCallback.onGetContentRect(this, mOriginatingView, mContentRect);
152         repositionToolbar();
153     }
154 
updateViewLocationInWindow()155     public void updateViewLocationInWindow() {
156         mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
157         mOriginatingView.getRootView().getLocationOnScreen(mRootViewPositionOnScreen);
158         mOriginatingView.getGlobalVisibleRect(mViewRectOnScreen);
159         mViewRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
160 
161         if (!Arrays.equals(mViewPositionOnScreen, mPreviousViewPositionOnScreen)
162                 || !mViewRectOnScreen.equals(mPreviousViewRectOnScreen)) {
163             repositionToolbar();
164             mPreviousViewPositionOnScreen[0] = mViewPositionOnScreen[0];
165             mPreviousViewPositionOnScreen[1] = mViewPositionOnScreen[1];
166             mPreviousViewRectOnScreen.set(mViewRectOnScreen);
167         }
168     }
169 
repositionToolbar()170     private void repositionToolbar() {
171         mContentRectOnScreen.set(mContentRect);
172 
173         // Offset the content rect into screen coordinates, taking into account any transformations
174         // that may be applied to the originating view or its ancestors.
175         final ViewParent parent = mOriginatingView.getParent();
176         if (parent instanceof ViewGroup) {
177             ((ViewGroup) parent).getChildVisibleRect(
178                     mOriginatingView, mContentRectOnScreen,
179                     null /* offset */, true /* forceParentCheck */);
180             mContentRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
181         } else {
182             mContentRectOnScreen.offset(mViewPositionOnScreen[0], mViewPositionOnScreen[1]);
183         }
184 
185         if (isContentRectWithinBounds()) {
186             mFloatingToolbarVisibilityHelper.setOutOfBounds(false);
187             // Make sure that content rect is not out of the view's visible bounds.
188             mContentRectOnScreen.set(
189                     Math.max(mContentRectOnScreen.left, mViewRectOnScreen.left),
190                     Math.max(mContentRectOnScreen.top, mViewRectOnScreen.top),
191                     Math.min(mContentRectOnScreen.right, mViewRectOnScreen.right),
192                     Math.min(mContentRectOnScreen.bottom,
193                             mViewRectOnScreen.bottom + mBottomAllowance));
194 
195             if (!mContentRectOnScreen.equals(mPreviousContentRectOnScreen)) {
196                 // Content rect is moving.
197                 mOriginatingView.removeCallbacks(mMovingOff);
198                 mFloatingToolbarVisibilityHelper.setMoving(true);
199                 mOriginatingView.postDelayed(mMovingOff, MOVING_HIDE_DELAY);
200 
201                 mFloatingToolbar.setContentRect(mContentRectOnScreen);
202                 mFloatingToolbar.updateLayout();
203             }
204         } else {
205             mFloatingToolbarVisibilityHelper.setOutOfBounds(true);
206             mContentRectOnScreen.setEmpty();
207         }
208         mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
209 
210         mPreviousContentRectOnScreen.set(mContentRectOnScreen);
211     }
212 
isContentRectWithinBounds()213     private boolean isContentRectWithinBounds() {
214         mContext.getSystemService(WindowManager.class)
215             .getDefaultDisplay().getRealSize(mDisplaySize);
216         mScreenRect.set(0, 0, mDisplaySize.x, mDisplaySize.y);
217 
218         return intersectsClosed(mContentRectOnScreen, mScreenRect)
219             && intersectsClosed(mContentRectOnScreen, mViewRectOnScreen);
220     }
221 
222     /*
223      * Same as Rect.intersects, but includes cases where the rectangles touch.
224     */
intersectsClosed(Rect a, Rect b)225     private static boolean intersectsClosed(Rect a, Rect b) {
226          return a.left <= b.right && b.left <= a.right
227                  && a.top <= b.bottom && b.top <= a.bottom;
228     }
229 
230     @Override
hide(long duration)231     public void hide(long duration) {
232         if (duration == ActionMode.DEFAULT_HIDE_DURATION) {
233             duration = ViewConfiguration.getDefaultActionModeHideDuration();
234         }
235         duration = Math.min(MAX_HIDE_DURATION, duration);
236         mOriginatingView.removeCallbacks(mHideOff);
237         if (duration <= 0) {
238             mHideOff.run();
239         } else {
240             mFloatingToolbarVisibilityHelper.setHideRequested(true);
241             mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
242             mOriginatingView.postDelayed(mHideOff, duration);
243         }
244     }
245 
246     /**
247      * If this is set to true, the action mode view will dismiss itself on touch events outside of
248      * its window. This only makes sense if the action mode view is a PopupWindow that is touchable
249      * but not focusable, which means touches outside of the window will be delivered to the window
250      * behind. The default is false.
251      *
252      * This is for internal use only and the approach to this may change.
253      * @hide
254      *
255      * @param outsideTouchable whether or not this action mode is "outside touchable"
256      * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself
257      */
setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss)258     public void setOutsideTouchable(
259             boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
260         mFloatingToolbar.setOutsideTouchable(outsideTouchable, onDismiss);
261     }
262 
263     @Override
onWindowFocusChanged(boolean hasWindowFocus)264     public void onWindowFocusChanged(boolean hasWindowFocus) {
265         mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus);
266         mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
267     }
268 
269     @Override
finish()270     public void finish() {
271         reset();
272         mCallback.onDestroyActionMode(this);
273     }
274 
275     @Override
getMenu()276     public Menu getMenu() {
277         return mMenu;
278     }
279 
280     @Override
getTitle()281     public CharSequence getTitle() {
282         return null;
283     }
284 
285     @Override
getSubtitle()286     public CharSequence getSubtitle() {
287         return null;
288     }
289 
290     @Override
getCustomView()291     public View getCustomView() {
292         return null;
293     }
294 
295     @Override
getMenuInflater()296     public MenuInflater getMenuInflater() {
297         return new MenuInflater(mContext);
298     }
299 
reset()300     private void reset() {
301         mFloatingToolbar.dismiss();
302         mFloatingToolbarVisibilityHelper.deactivate();
303         mOriginatingView.removeCallbacks(mMovingOff);
304         mOriginatingView.removeCallbacks(mHideOff);
305     }
306 
isViewStillActive()307     private boolean isViewStillActive() {
308         return mOriginatingView.getWindowVisibility() == View.VISIBLE
309                 && mOriginatingView.isShown();
310     }
311 
312     /**
313      * A helper for showing/hiding the floating toolbar depending on certain states.
314      */
315     private static final class FloatingToolbarVisibilityHelper {
316 
317         private static final long MIN_SHOW_DURATION_FOR_MOVE_HIDE = 500;
318 
319         private final FloatingToolbar mToolbar;
320 
321         private boolean mHideRequested;
322         private boolean mMoving;
323         private boolean mOutOfBounds;
324         private boolean mWindowFocused = true;
325 
326         private boolean mActive;
327 
328         private long mLastShowTime;
329 
FloatingToolbarVisibilityHelper(FloatingToolbar toolbar)330         public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) {
331             mToolbar = Preconditions.checkNotNull(toolbar);
332         }
333 
activate()334         public void activate() {
335             mHideRequested = false;
336             mMoving = false;
337             mOutOfBounds = false;
338             mWindowFocused = true;
339 
340             mActive = true;
341         }
342 
deactivate()343         public void deactivate() {
344             mActive = false;
345             mToolbar.dismiss();
346         }
347 
setHideRequested(boolean hide)348         public void setHideRequested(boolean hide) {
349             mHideRequested = hide;
350         }
351 
setMoving(boolean moving)352         public void setMoving(boolean moving) {
353             // Avoid unintended flickering by allowing the toolbar to show long enough before
354             // triggering the 'moving' flag - which signals a hide.
355             final boolean showingLongEnough =
356                 System.currentTimeMillis() - mLastShowTime > MIN_SHOW_DURATION_FOR_MOVE_HIDE;
357             if (!moving || showingLongEnough) {
358                 mMoving = moving;
359             }
360         }
361 
setOutOfBounds(boolean outOfBounds)362         public void setOutOfBounds(boolean outOfBounds) {
363             mOutOfBounds = outOfBounds;
364         }
365 
setWindowFocused(boolean windowFocused)366         public void setWindowFocused(boolean windowFocused) {
367             mWindowFocused = windowFocused;
368         }
369 
updateToolbarVisibility()370         public void updateToolbarVisibility() {
371             if (!mActive) {
372                 return;
373             }
374 
375             if (mHideRequested || mMoving || mOutOfBounds || !mWindowFocused) {
376                 mToolbar.hide();
377             } else {
378                 mToolbar.show();
379                 mLastShowTime = System.currentTimeMillis();
380             }
381         }
382     }
383 }
384