1/*
2 * Copyright (C) 2012 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
17package com.android.systemui.statusbar.phone;
18
19import android.annotation.ColorInt;
20import android.annotation.DrawableRes;
21import android.annotation.LayoutRes;
22import android.app.StatusBarManager;
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.TypedArray;
26import android.graphics.Canvas;
27import android.graphics.Paint;
28import android.graphics.PorterDuff;
29import android.graphics.PorterDuffXfermode;
30import android.graphics.Rect;
31import android.graphics.drawable.Drawable;
32import android.media.AudioManager;
33import android.media.session.MediaSessionLegacyHelper;
34import android.net.Uri;
35import android.os.Bundle;
36import android.os.IBinder;
37import android.os.SystemClock;
38import android.util.AttributeSet;
39import android.view.ActionMode;
40import android.view.InputDevice;
41import android.view.InputQueue;
42import android.view.KeyEvent;
43import android.view.LayoutInflater;
44import android.view.Menu;
45import android.view.MenuItem;
46import android.view.MotionEvent;
47import android.view.SurfaceHolder;
48import android.view.View;
49import android.view.ViewGroup;
50import android.view.ViewTreeObserver;
51import android.view.Window;
52import android.view.WindowManager;
53import android.view.WindowManagerGlobal;
54import android.widget.FrameLayout;
55
56import com.android.internal.annotations.VisibleForTesting;
57import com.android.internal.view.FloatingActionMode;
58import com.android.internal.widget.FloatingToolbar;
59import com.android.systemui.R;
60import com.android.systemui.classifier.FalsingManager;
61import com.android.systemui.statusbar.DragDownHelper;
62import com.android.systemui.statusbar.StatusBarState;
63import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
64
65
66public class StatusBarWindowView extends FrameLayout {
67    public static final String TAG = "StatusBarWindowView";
68    public static final boolean DEBUG = StatusBar.DEBUG;
69
70    private DragDownHelper mDragDownHelper;
71    private DoubleTapHelper mDoubleTapHelper;
72    private NotificationStackScrollLayout mStackScrollLayout;
73    private NotificationPanelView mNotificationPanel;
74    private View mBrightnessMirror;
75
76    private int mRightInset = 0;
77    private int mLeftInset = 0;
78
79    private StatusBar mService;
80    private final Paint mTransparentSrcPaint = new Paint();
81    private FalsingManager mFalsingManager;
82
83    // Implements the floating action mode for TextView's Cut/Copy/Past menu. Normally provided by
84    // DecorView, but since this is a special window we have to roll our own.
85    private View mFloatingActionModeOriginatingView;
86    private ActionMode mFloatingActionMode;
87    private FloatingToolbar mFloatingToolbar;
88    private ViewTreeObserver.OnPreDrawListener mFloatingToolbarPreDrawListener;
89    private boolean mTouchCancelled;
90    private boolean mTouchActive;
91
92    public StatusBarWindowView(Context context, AttributeSet attrs) {
93        super(context, attrs);
94        setMotionEventSplittingEnabled(false);
95        mTransparentSrcPaint.setColor(0);
96        mTransparentSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
97        mFalsingManager = FalsingManager.getInstance(context);
98        mDoubleTapHelper = new DoubleTapHelper(this, active -> {}, () -> {
99            mService.wakeUpIfDozing(SystemClock.uptimeMillis(), this);
100            return true;
101        }, null, null);
102    }
103
104    @Override
105    protected boolean fitSystemWindows(Rect insets) {
106        if (getFitsSystemWindows()) {
107            boolean paddingChanged = insets.top != getPaddingTop()
108                    || insets.bottom != getPaddingBottom();
109
110            // Super-special right inset handling, because scrims and backdrop need to ignore it.
111            if (insets.right != mRightInset || insets.left != mLeftInset) {
112                mRightInset = insets.right;
113                mLeftInset = insets.left;
114                applyMargins();
115            }
116            // Drop top inset, and pass through bottom inset.
117            if (paddingChanged) {
118                setPadding(0, 0, 0, 0);
119            }
120            insets.left = 0;
121            insets.top = 0;
122            insets.right = 0;
123        } else {
124            if (mRightInset != 0 || mLeftInset != 0) {
125                mRightInset = 0;
126                mLeftInset = 0;
127                applyMargins();
128            }
129            boolean changed = getPaddingLeft() != 0
130                    || getPaddingRight() != 0
131                    || getPaddingTop() != 0
132                    || getPaddingBottom() != 0;
133            if (changed) {
134                setPadding(0, 0, 0, 0);
135            }
136            insets.top = 0;
137        }
138        return false;
139    }
140
141    private void applyMargins() {
142        final int N = getChildCount();
143        for (int i = 0; i < N; i++) {
144            View child = getChildAt(i);
145            if (child.getLayoutParams() instanceof LayoutParams) {
146                LayoutParams lp = (LayoutParams) child.getLayoutParams();
147                if (!lp.ignoreRightInset
148                        && (lp.rightMargin != mRightInset || lp.leftMargin != mLeftInset)) {
149                    lp.rightMargin = mRightInset;
150                    lp.leftMargin = mLeftInset;
151                    child.requestLayout();
152                }
153            }
154        }
155    }
156
157    @Override
158    public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
159        return new LayoutParams(getContext(), attrs);
160    }
161
162    @Override
163    protected FrameLayout.LayoutParams generateDefaultLayoutParams() {
164        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
165    }
166
167    @Override
168    protected void onFinishInflate() {
169        super.onFinishInflate();
170        mStackScrollLayout = (NotificationStackScrollLayout) findViewById(
171                R.id.notification_stack_scroller);
172        mNotificationPanel = (NotificationPanelView) findViewById(R.id.notification_panel);
173        mBrightnessMirror = findViewById(R.id.brightness_mirror);
174    }
175
176    @Override
177    public void onViewAdded(View child) {
178        super.onViewAdded(child);
179        if (child.getId() == R.id.brightness_mirror) {
180            mBrightnessMirror = child;
181        }
182    }
183
184    public void setService(StatusBar service) {
185        mService = service;
186        setDragDownHelper(new DragDownHelper(getContext(), this, mStackScrollLayout, mService));
187    }
188
189    @VisibleForTesting
190    void setDragDownHelper(DragDownHelper dragDownHelper) {
191        mDragDownHelper = dragDownHelper;
192    }
193
194    @Override
195    protected void onAttachedToWindow () {
196        super.onAttachedToWindow();
197
198        // We need to ensure that our window doesn't suffer from overdraw which would normally
199        // occur if our window is translucent. Since we are drawing the whole window anyway with
200        // the scrim, we don't need the window to be cleared in the beginning.
201        if (mService.isScrimSrcModeEnabled()) {
202            IBinder windowToken = getWindowToken();
203            WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
204            lp.token = windowToken;
205            setLayoutParams(lp);
206            WindowManagerGlobal.getInstance().changeCanvasOpacity(windowToken, true);
207            setWillNotDraw(false);
208        } else {
209            setWillNotDraw(!DEBUG);
210        }
211    }
212
213    @Override
214    public boolean dispatchKeyEvent(KeyEvent event) {
215        if (mService.interceptMediaKey(event)) {
216            return true;
217        }
218        if (super.dispatchKeyEvent(event)) {
219            return true;
220        }
221        boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
222        switch (event.getKeyCode()) {
223            case KeyEvent.KEYCODE_BACK:
224                if (!down) {
225                    mService.onBackPressed();
226                }
227                return true;
228            case KeyEvent.KEYCODE_MENU:
229                if (!down) {
230                    return mService.onMenuPressed();
231                }
232            case KeyEvent.KEYCODE_SPACE:
233                if (!down) {
234                    return mService.onSpacePressed();
235                }
236                break;
237            case KeyEvent.KEYCODE_VOLUME_DOWN:
238            case KeyEvent.KEYCODE_VOLUME_UP:
239                if (mService.isDozing()) {
240                    MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(
241                            event, AudioManager.USE_DEFAULT_STREAM_TYPE, true);
242                    return true;
243                }
244                break;
245        }
246        return false;
247    }
248
249    @Override
250    public boolean dispatchTouchEvent(MotionEvent ev) {
251        boolean isDown = ev.getActionMasked() == MotionEvent.ACTION_DOWN;
252        if (isDown && mNotificationPanel.isFullyCollapsed()) {
253            mNotificationPanel.startExpandLatencyTracking();
254        }
255        if (isDown) {
256            mTouchActive = true;
257            mTouchCancelled = false;
258        } else if (ev.getActionMasked() == MotionEvent.ACTION_UP
259                || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
260            mTouchActive = false;
261        }
262        if (mTouchCancelled) {
263            return false;
264        }
265        mFalsingManager.onTouchEvent(ev, getWidth(), getHeight());
266        if (mBrightnessMirror != null && mBrightnessMirror.getVisibility() == VISIBLE) {
267            // Disallow new pointers while the brightness mirror is visible. This is so that you
268            // can't touch anything other than the brightness slider while the mirror is showing
269            // and the rest of the panel is transparent.
270            if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
271                return false;
272            }
273        }
274        if (isDown) {
275            mStackScrollLayout.closeControlsIfOutsideTouch(ev);
276        }
277        if (mService.isDozing()) {
278            mService.mDozeScrimController.extendPulse();
279        }
280
281        return super.dispatchTouchEvent(ev);
282    }
283
284    @Override
285    public boolean onInterceptTouchEvent(MotionEvent ev) {
286        if (mService.isDozing() && !mStackScrollLayout.hasPulsingNotifications()) {
287            // Capture all touch events in always-on.
288            return true;
289        }
290        boolean intercept = false;
291        if (mNotificationPanel.isFullyExpanded()
292                && mStackScrollLayout.getVisibility() == View.VISIBLE
293                && mService.getBarState() == StatusBarState.KEYGUARD
294                && !mService.isBouncerShowing()
295                && !mService.isDozing()) {
296            intercept = mDragDownHelper.onInterceptTouchEvent(ev);
297        }
298        if (!intercept) {
299            super.onInterceptTouchEvent(ev);
300        }
301        if (intercept) {
302            MotionEvent cancellation = MotionEvent.obtain(ev);
303            cancellation.setAction(MotionEvent.ACTION_CANCEL);
304            mStackScrollLayout.onInterceptTouchEvent(cancellation);
305            mNotificationPanel.onInterceptTouchEvent(cancellation);
306            cancellation.recycle();
307        }
308        return intercept;
309    }
310
311    @Override
312    public boolean onTouchEvent(MotionEvent ev) {
313        boolean handled = false;
314        if (mService.isDozing()) {
315            mDoubleTapHelper.onTouchEvent(ev);
316            handled = true;
317        }
318        if ((mService.getBarState() == StatusBarState.KEYGUARD && !handled)
319                || mDragDownHelper.isDraggingDown()) {
320            // we still want to finish our drag down gesture when locking the screen
321            handled = mDragDownHelper.onTouchEvent(ev);
322        }
323        if (!handled) {
324            handled = super.onTouchEvent(ev);
325        }
326        final int action = ev.getAction();
327        if (!handled && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) {
328            mService.setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
329        }
330        return handled;
331    }
332
333    @Override
334    public void onDraw(Canvas canvas) {
335        super.onDraw(canvas);
336        if (mService.isScrimSrcModeEnabled()) {
337            // We need to ensure that our window is always drawn fully even when we have paddings,
338            // since we simulate it to be opaque.
339            int paddedBottom = getHeight() - getPaddingBottom();
340            int paddedRight = getWidth() - getPaddingRight();
341            if (getPaddingTop() != 0) {
342                canvas.drawRect(0, 0, getWidth(), getPaddingTop(), mTransparentSrcPaint);
343            }
344            if (getPaddingBottom() != 0) {
345                canvas.drawRect(0, paddedBottom, getWidth(), getHeight(), mTransparentSrcPaint);
346            }
347            if (getPaddingLeft() != 0) {
348                canvas.drawRect(0, getPaddingTop(), getPaddingLeft(), paddedBottom,
349                        mTransparentSrcPaint);
350            }
351            if (getPaddingRight() != 0) {
352                canvas.drawRect(paddedRight, getPaddingTop(), getWidth(), paddedBottom,
353                        mTransparentSrcPaint);
354            }
355        }
356        if (DEBUG) {
357            Paint pt = new Paint();
358            pt.setColor(0x80FFFF00);
359            pt.setStrokeWidth(12.0f);
360            pt.setStyle(Paint.Style.STROKE);
361            canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), pt);
362        }
363    }
364
365    public void cancelExpandHelper() {
366        if (mStackScrollLayout != null) {
367            mStackScrollLayout.cancelExpandHelper();
368        }
369    }
370
371    public void cancelCurrentTouch() {
372        if (mTouchActive) {
373            final long now = SystemClock.uptimeMillis();
374            MotionEvent event = MotionEvent.obtain(now, now,
375                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
376            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
377            dispatchTouchEvent(event);
378            event.recycle();
379            mTouchCancelled = true;
380        }
381    }
382
383    public class LayoutParams extends FrameLayout.LayoutParams {
384
385        public boolean ignoreRightInset;
386
387        public LayoutParams(int width, int height) {
388            super(width, height);
389        }
390
391        public LayoutParams(Context c, AttributeSet attrs) {
392            super(c, attrs);
393
394            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout);
395            ignoreRightInset = a.getBoolean(
396                    R.styleable.StatusBarWindowView_Layout_ignoreRightInset, false);
397            a.recycle();
398        }
399    }
400
401    @Override
402    public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback,
403            int type) {
404        if (type == ActionMode.TYPE_FLOATING) {
405            return startActionMode(originalView, callback, type);
406        }
407        return super.startActionModeForChild(originalView, callback, type);
408    }
409
410    private ActionMode createFloatingActionMode(
411            View originatingView, ActionMode.Callback2 callback) {
412        if (mFloatingActionMode != null) {
413            mFloatingActionMode.finish();
414        }
415        cleanupFloatingActionModeViews();
416        mFloatingToolbar = new FloatingToolbar(mContext, mFakeWindow);
417        final FloatingActionMode mode =
418                new FloatingActionMode(mContext, callback, originatingView, mFloatingToolbar);
419        mFloatingActionModeOriginatingView = originatingView;
420        mFloatingToolbarPreDrawListener =
421                new ViewTreeObserver.OnPreDrawListener() {
422                    @Override
423                    public boolean onPreDraw() {
424                        mode.updateViewLocationInWindow();
425                        return true;
426                    }
427                };
428        return mode;
429    }
430
431    private void setHandledFloatingActionMode(ActionMode mode) {
432        mFloatingActionMode = mode;
433        mFloatingActionMode.invalidate();  // Will show the floating toolbar if necessary.
434        mFloatingActionModeOriginatingView.getViewTreeObserver()
435                .addOnPreDrawListener(mFloatingToolbarPreDrawListener);
436    }
437
438    private void cleanupFloatingActionModeViews() {
439        if (mFloatingToolbar != null) {
440            mFloatingToolbar.dismiss();
441            mFloatingToolbar = null;
442        }
443        if (mFloatingActionModeOriginatingView != null) {
444            if (mFloatingToolbarPreDrawListener != null) {
445                mFloatingActionModeOriginatingView.getViewTreeObserver()
446                        .removeOnPreDrawListener(mFloatingToolbarPreDrawListener);
447                mFloatingToolbarPreDrawListener = null;
448            }
449            mFloatingActionModeOriginatingView = null;
450        }
451    }
452
453    private ActionMode startActionMode(
454            View originatingView, ActionMode.Callback callback, int type) {
455        ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback);
456        ActionMode mode = createFloatingActionMode(originatingView, wrappedCallback);
457        if (mode != null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) {
458            setHandledFloatingActionMode(mode);
459        } else {
460            mode = null;
461        }
462        return mode;
463    }
464
465    private class ActionModeCallback2Wrapper extends ActionMode.Callback2 {
466        private final ActionMode.Callback mWrapped;
467
468        public ActionModeCallback2Wrapper(ActionMode.Callback wrapped) {
469            mWrapped = wrapped;
470        }
471
472        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
473            return mWrapped.onCreateActionMode(mode, menu);
474        }
475
476        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
477            requestFitSystemWindows();
478            return mWrapped.onPrepareActionMode(mode, menu);
479        }
480
481        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
482            return mWrapped.onActionItemClicked(mode, item);
483        }
484
485        public void onDestroyActionMode(ActionMode mode) {
486            mWrapped.onDestroyActionMode(mode);
487            if (mode == mFloatingActionMode) {
488                cleanupFloatingActionModeViews();
489                mFloatingActionMode = null;
490            }
491            requestFitSystemWindows();
492        }
493
494        @Override
495        public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
496            if (mWrapped instanceof ActionMode.Callback2) {
497                ((ActionMode.Callback2) mWrapped).onGetContentRect(mode, view, outRect);
498            } else {
499                super.onGetContentRect(mode, view, outRect);
500            }
501        }
502    }
503
504    /**
505     * Minimal window to satisfy FloatingToolbar.
506     */
507    private Window mFakeWindow = new Window(mContext) {
508        @Override
509        public void takeSurface(SurfaceHolder.Callback2 callback) {
510        }
511
512        @Override
513        public void takeInputQueue(InputQueue.Callback callback) {
514        }
515
516        @Override
517        public boolean isFloating() {
518            return false;
519        }
520
521        @Override
522        public void alwaysReadCloseOnTouchAttr() {
523        }
524
525        @Override
526        public void setContentView(@LayoutRes int layoutResID) {
527        }
528
529        @Override
530        public void setContentView(View view) {
531        }
532
533        @Override
534        public void setContentView(View view, ViewGroup.LayoutParams params) {
535        }
536
537        @Override
538        public void addContentView(View view, ViewGroup.LayoutParams params) {
539        }
540
541        @Override
542        public void clearContentView() {
543        }
544
545        @Override
546        public View getCurrentFocus() {
547            return null;
548        }
549
550        @Override
551        public LayoutInflater getLayoutInflater() {
552            return null;
553        }
554
555        @Override
556        public void setTitle(CharSequence title) {
557        }
558
559        @Override
560        public void setTitleColor(@ColorInt int textColor) {
561        }
562
563        @Override
564        public void openPanel(int featureId, KeyEvent event) {
565        }
566
567        @Override
568        public void closePanel(int featureId) {
569        }
570
571        @Override
572        public void togglePanel(int featureId, KeyEvent event) {
573        }
574
575        @Override
576        public void invalidatePanelMenu(int featureId) {
577        }
578
579        @Override
580        public boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, int flags) {
581            return false;
582        }
583
584        @Override
585        public boolean performPanelIdentifierAction(int featureId, int id, int flags) {
586            return false;
587        }
588
589        @Override
590        public void closeAllPanels() {
591        }
592
593        @Override
594        public boolean performContextMenuIdentifierAction(int id, int flags) {
595            return false;
596        }
597
598        @Override
599        public void onConfigurationChanged(Configuration newConfig) {
600        }
601
602        @Override
603        public void setBackgroundDrawable(Drawable drawable) {
604        }
605
606        @Override
607        public void setFeatureDrawableResource(int featureId, @DrawableRes int resId) {
608        }
609
610        @Override
611        public void setFeatureDrawableUri(int featureId, Uri uri) {
612        }
613
614        @Override
615        public void setFeatureDrawable(int featureId, Drawable drawable) {
616        }
617
618        @Override
619        public void setFeatureDrawableAlpha(int featureId, int alpha) {
620        }
621
622        @Override
623        public void setFeatureInt(int featureId, int value) {
624        }
625
626        @Override
627        public void takeKeyEvents(boolean get) {
628        }
629
630        @Override
631        public boolean superDispatchKeyEvent(KeyEvent event) {
632            return false;
633        }
634
635        @Override
636        public boolean superDispatchKeyShortcutEvent(KeyEvent event) {
637            return false;
638        }
639
640        @Override
641        public boolean superDispatchTouchEvent(MotionEvent event) {
642            return false;
643        }
644
645        @Override
646        public boolean superDispatchTrackballEvent(MotionEvent event) {
647            return false;
648        }
649
650        @Override
651        public boolean superDispatchGenericMotionEvent(MotionEvent event) {
652            return false;
653        }
654
655        @Override
656        public View getDecorView() {
657            return StatusBarWindowView.this;
658        }
659
660        @Override
661        public View peekDecorView() {
662            return null;
663        }
664
665        @Override
666        public Bundle saveHierarchyState() {
667            return null;
668        }
669
670        @Override
671        public void restoreHierarchyState(Bundle savedInstanceState) {
672        }
673
674        @Override
675        protected void onActive() {
676        }
677
678        @Override
679        public void setChildDrawable(int featureId, Drawable drawable) {
680        }
681
682        @Override
683        public void setChildInt(int featureId, int value) {
684        }
685
686        @Override
687        public boolean isShortcutKey(int keyCode, KeyEvent event) {
688            return false;
689        }
690
691        @Override
692        public void setVolumeControlStream(int streamType) {
693        }
694
695        @Override
696        public int getVolumeControlStream() {
697            return 0;
698        }
699
700        @Override
701        public int getStatusBarColor() {
702            return 0;
703        }
704
705        @Override
706        public void setStatusBarColor(@ColorInt int color) {
707        }
708
709        @Override
710        public int getNavigationBarColor() {
711            return 0;
712        }
713
714        @Override
715        public void setNavigationBarColor(@ColorInt int color) {
716        }
717
718        @Override
719        public void setDecorCaptionShade(int decorCaptionShade) {
720        }
721
722        @Override
723        public void setResizingCaptionDrawable(Drawable drawable) {
724        }
725
726        @Override
727        public void onMultiWindowModeChanged() {
728        }
729
730        @Override
731        public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
732        }
733
734        @Override
735        public void reportActivityRelaunched() {
736        }
737    };
738
739}
740
741