NavigationBarView.java revision 8cd9e2db31ce16f38c9d0b642645d7b594110d3e
1/*
2 * Copyright (C) 2008 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.animation.LayoutTransition;
20import android.animation.LayoutTransition.TransitionListener;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.animation.ValueAnimator;
24import android.app.StatusBarManager;
25import android.content.Context;
26import android.content.res.Configuration;
27import android.content.res.Resources;
28import android.graphics.Point;
29import android.graphics.Rect;
30import android.graphics.drawable.Drawable;
31import android.os.Handler;
32import android.os.Message;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.view.Display;
36import android.view.MotionEvent;
37import android.view.Surface;
38import android.view.View;
39import android.view.ViewGroup;
40import android.view.WindowManager;
41import android.view.inputmethod.InputMethodManager;
42import android.widget.ImageView;
43import android.widget.LinearLayout;
44import com.android.systemui.R;
45import com.android.systemui.statusbar.BaseStatusBar;
46import com.android.systemui.statusbar.DelegateViewHelper;
47import com.android.systemui.statusbar.policy.DeadZone;
48import com.android.systemui.statusbar.policy.KeyButtonView;
49
50import java.io.FileDescriptor;
51import java.io.PrintWriter;
52import java.util.ArrayList;
53
54public class NavigationBarView extends LinearLayout {
55    final static boolean DEBUG = false;
56    final static String TAG = "PhoneStatusBar/NavigationBarView";
57
58    // slippery nav bar when everything is disabled, e.g. during setup
59    final static boolean SLIPPERY_WHEN_DISABLED = true;
60
61    final Display mDisplay;
62    View mCurrentView = null;
63    View[] mRotatedViews = new View[4];
64
65    int mBarSize;
66    boolean mVertical;
67    boolean mScreenOn;
68
69    boolean mShowMenu;
70    int mDisabledFlags = 0;
71    int mNavigationIconHints = 0;
72
73    private Drawable mBackIcon, mBackLandIcon, mBackAltIcon, mBackAltLandIcon;
74    private Drawable mRecentIcon;
75    private Drawable mRecentLandIcon;
76
77    private NavigationBarViewTaskSwitchHelper mTaskSwitchHelper;
78    private DelegateViewHelper mDelegateHelper;
79    private DeadZone mDeadZone;
80    private final NavigationBarTransitions mBarTransitions;
81
82    // workaround for LayoutTransitions leaving the nav buttons in a weird state (bug 5549288)
83    final static boolean WORKAROUND_INVALID_LAYOUT = true;
84    final static int MSG_CHECK_INVALID_LAYOUT = 8686;
85
86    // performs manual animation in sync with layout transitions
87    private final NavTransitionListener mTransitionListener = new NavTransitionListener();
88
89    private OnVerticalChangedListener mOnVerticalChangedListener;
90    private boolean mIsLayoutRtl;
91
92    private class NavTransitionListener implements TransitionListener {
93        private boolean mBackTransitioning;
94        private boolean mHomeAppearing;
95        private long mStartDelay;
96        private long mDuration;
97        private TimeInterpolator mInterpolator;
98
99        @Override
100        public void startTransition(LayoutTransition transition, ViewGroup container,
101                View view, int transitionType) {
102            if (view.getId() == R.id.back) {
103                mBackTransitioning = true;
104            } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) {
105                mHomeAppearing = true;
106                mStartDelay = transition.getStartDelay(transitionType);
107                mDuration = transition.getDuration(transitionType);
108                mInterpolator = transition.getInterpolator(transitionType);
109            }
110        }
111
112        @Override
113        public void endTransition(LayoutTransition transition, ViewGroup container,
114                View view, int transitionType) {
115            if (view.getId() == R.id.back) {
116                mBackTransitioning = false;
117            } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) {
118                mHomeAppearing = false;
119            }
120        }
121
122        public void onBackAltCleared() {
123            // When dismissing ime during unlock, force the back button to run the same appearance
124            // animation as home (if we catch this condition early enough).
125            if (!mBackTransitioning && getBackButton().getVisibility() == VISIBLE
126                    && mHomeAppearing && getHomeButton().getAlpha() == 0) {
127                getBackButton().setAlpha(0);
128                ValueAnimator a = ObjectAnimator.ofFloat(getBackButton(), "alpha", 0, 1);
129                a.setStartDelay(mStartDelay);
130                a.setDuration(mDuration);
131                a.setInterpolator(mInterpolator);
132                a.start();
133            }
134        }
135    }
136
137    private final OnClickListener mImeSwitcherClickListener = new OnClickListener() {
138        @Override
139        public void onClick(View view) {
140            ((InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE))
141                    .showInputMethodPicker();
142        }
143    };
144
145    private class H extends Handler {
146        public void handleMessage(Message m) {
147            switch (m.what) {
148                case MSG_CHECK_INVALID_LAYOUT:
149                    final String how = "" + m.obj;
150                    final int w = getWidth();
151                    final int h = getHeight();
152                    final int vw = mCurrentView.getWidth();
153                    final int vh = mCurrentView.getHeight();
154
155                    if (h != vh || w != vw) {
156                        Log.w(TAG, String.format(
157                            "*** Invalid layout in navigation bar (%s this=%dx%d cur=%dx%d)",
158                            how, w, h, vw, vh));
159                        if (WORKAROUND_INVALID_LAYOUT) {
160                            requestLayout();
161                        }
162                    }
163                    break;
164            }
165        }
166    }
167
168    public NavigationBarView(Context context, AttributeSet attrs) {
169        super(context, attrs);
170
171        mDisplay = ((WindowManager)context.getSystemService(
172                Context.WINDOW_SERVICE)).getDefaultDisplay();
173
174        final Resources res = getContext().getResources();
175        mBarSize = res.getDimensionPixelSize(R.dimen.navigation_bar_size);
176        mVertical = false;
177        mShowMenu = false;
178        mDelegateHelper = new DelegateViewHelper(this);
179        mTaskSwitchHelper = new NavigationBarViewTaskSwitchHelper(context);
180
181        getIcons(res);
182
183        mBarTransitions = new NavigationBarTransitions(this);
184    }
185
186    public BarTransitions getBarTransitions() {
187        return mBarTransitions;
188    }
189
190    public void setDelegateView(View view) {
191        mDelegateHelper.setDelegateView(view);
192    }
193
194    public void setBar(BaseStatusBar phoneStatusBar) {
195        mTaskSwitchHelper.setBar(phoneStatusBar);
196        mDelegateHelper.setBar(phoneStatusBar);
197    }
198
199    public void setOnVerticalChangedListener(OnVerticalChangedListener onVerticalChangedListener) {
200        mOnVerticalChangedListener = onVerticalChangedListener;
201    }
202
203    @Override
204    public boolean onTouchEvent(MotionEvent event) {
205        if (mTaskSwitchHelper.onTouchEvent(event)) {
206            return true;
207        }
208        if (mDeadZone != null && event.getAction() == MotionEvent.ACTION_OUTSIDE) {
209            mDeadZone.poke(event);
210        }
211        if (mDelegateHelper != null) {
212            boolean ret = mDelegateHelper.onInterceptTouchEvent(event);
213            if (ret) return true;
214        }
215        return super.onTouchEvent(event);
216    }
217
218    @Override
219    public boolean onInterceptTouchEvent(MotionEvent event) {
220        return mTaskSwitchHelper.onInterceptTouchEvent(event) ||
221                mDelegateHelper.onInterceptTouchEvent(event);
222    }
223
224    private H mHandler = new H();
225
226    public View getCurrentView() {
227        return mCurrentView;
228    }
229
230    public View getRecentsButton() {
231        return mCurrentView.findViewById(R.id.recent_apps);
232    }
233
234    public View getMenuButton() {
235        return mCurrentView.findViewById(R.id.menu);
236    }
237
238    public View getBackButton() {
239        return mCurrentView.findViewById(R.id.back);
240    }
241
242    public View getHomeButton() {
243        return mCurrentView.findViewById(R.id.home);
244    }
245
246    public View getImeSwitchButton() {
247        return mCurrentView.findViewById(R.id.ime_switcher);
248    }
249
250    private void getIcons(Resources res) {
251        mBackIcon = res.getDrawable(R.drawable.ic_sysbar_back);
252        mBackLandIcon = res.getDrawable(R.drawable.ic_sysbar_back_land);
253        mBackAltIcon = res.getDrawable(R.drawable.ic_sysbar_back_ime);
254        mBackAltLandIcon = res.getDrawable(R.drawable.ic_sysbar_back_ime);
255        mRecentIcon = res.getDrawable(R.drawable.ic_sysbar_recent);
256        mRecentLandIcon = res.getDrawable(R.drawable.ic_sysbar_recent_land);
257    }
258
259    @Override
260    public void setLayoutDirection(int layoutDirection) {
261        getIcons(getContext().getResources());
262
263        super.setLayoutDirection(layoutDirection);
264    }
265
266    public void notifyScreenOn(boolean screenOn) {
267        mScreenOn = screenOn;
268        setDisabledFlags(mDisabledFlags, true);
269    }
270
271    public void setNavigationIconHints(int hints) {
272        setNavigationIconHints(hints, false);
273    }
274
275    public void setNavigationIconHints(int hints, boolean force) {
276        if (!force && hints == mNavigationIconHints) return;
277        final boolean backAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
278        if ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0 && !backAlt) {
279            mTransitionListener.onBackAltCleared();
280        }
281        if (DEBUG) {
282            android.widget.Toast.makeText(getContext(),
283                "Navigation icon hints = " + hints,
284                500).show();
285        }
286
287        mNavigationIconHints = hints;
288
289        ((ImageView)getBackButton()).setImageDrawable(backAlt
290                ? (mVertical ? mBackAltLandIcon : mBackAltIcon)
291                : (mVertical ? mBackLandIcon : mBackIcon));
292
293        ((ImageView)getRecentsButton()).setImageDrawable(mVertical ? mRecentLandIcon : mRecentIcon);
294
295        final boolean showImeButton = ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0);
296        getImeSwitchButton().setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE);
297        // Update menu button in case the IME state has changed.
298        setMenuVisibility(mShowMenu, true);
299
300
301        setDisabledFlags(mDisabledFlags, true);
302    }
303
304    public void setDisabledFlags(int disabledFlags) {
305        setDisabledFlags(disabledFlags, false);
306    }
307
308    public void setDisabledFlags(int disabledFlags, boolean force) {
309        if (!force && mDisabledFlags == disabledFlags) return;
310
311        mDisabledFlags = disabledFlags;
312
313        final boolean disableHome = ((disabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
314        final boolean disableRecent = ((disabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0);
315        final boolean disableBack = ((disabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)
316                && ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) == 0);
317        final boolean disableSearch = ((disabledFlags & View.STATUS_BAR_DISABLE_SEARCH) != 0);
318
319        if (SLIPPERY_WHEN_DISABLED) {
320            setSlippery(disableHome && disableRecent && disableBack && disableSearch);
321        }
322
323        ViewGroup navButtons = (ViewGroup) mCurrentView.findViewById(R.id.nav_buttons);
324        if (navButtons != null) {
325            LayoutTransition lt = navButtons.getLayoutTransition();
326            if (lt != null) {
327                if (!lt.getTransitionListeners().contains(mTransitionListener)) {
328                    lt.addTransitionListener(mTransitionListener);
329                }
330                if (!mScreenOn && mCurrentView != null) {
331                    lt.disableTransitionType(
332                            LayoutTransition.CHANGE_APPEARING |
333                            LayoutTransition.CHANGE_DISAPPEARING |
334                            LayoutTransition.APPEARING |
335                            LayoutTransition.DISAPPEARING);
336                }
337            }
338        }
339
340        getBackButton()   .setVisibility(disableBack       ? View.INVISIBLE : View.VISIBLE);
341        getHomeButton()   .setVisibility(disableHome       ? View.INVISIBLE : View.VISIBLE);
342        getRecentsButton().setVisibility(disableRecent     ? View.INVISIBLE : View.VISIBLE);
343
344        mBarTransitions.applyBackButtonQuiescentAlpha(mBarTransitions.getMode(), true /*animate*/);
345    }
346
347    private void setVisibleOrGone(View view, boolean visible) {
348        if (view != null) {
349            view.setVisibility(visible ? VISIBLE : GONE);
350        }
351    }
352
353    public void setSlippery(boolean newSlippery) {
354        WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
355        if (lp != null) {
356            boolean oldSlippery = (lp.flags & WindowManager.LayoutParams.FLAG_SLIPPERY) != 0;
357            if (!oldSlippery && newSlippery) {
358                lp.flags |= WindowManager.LayoutParams.FLAG_SLIPPERY;
359            } else if (oldSlippery && !newSlippery) {
360                lp.flags &= ~WindowManager.LayoutParams.FLAG_SLIPPERY;
361            } else {
362                return;
363            }
364            WindowManager wm = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
365            wm.updateViewLayout(this, lp);
366        }
367    }
368
369    public void setMenuVisibility(final boolean show) {
370        setMenuVisibility(show, false);
371    }
372
373    public void setMenuVisibility(final boolean show, final boolean force) {
374        if (!force && mShowMenu == show) return;
375
376        mShowMenu = show;
377
378        // Only show Menu if IME switcher not shown.
379        final boolean shouldShow = mShowMenu &&
380                ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) == 0);
381        getMenuButton().setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE);
382    }
383
384    @Override
385    public void onFinishInflate() {
386        mRotatedViews[Surface.ROTATION_0] =
387        mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0);
388
389        mRotatedViews[Surface.ROTATION_90] = findViewById(R.id.rot90);
390
391        mRotatedViews[Surface.ROTATION_270] = mRotatedViews[Surface.ROTATION_90];
392
393        mCurrentView = mRotatedViews[Surface.ROTATION_0];
394
395        getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
396
397        updateRTLOrder();
398    }
399
400    public boolean isVertical() {
401        return mVertical;
402    }
403
404    public void reorient() {
405        final int rot = mDisplay.getRotation();
406        for (int i=0; i<4; i++) {
407            mRotatedViews[i].setVisibility(View.GONE);
408        }
409        mCurrentView = mRotatedViews[rot];
410        mCurrentView.setVisibility(View.VISIBLE);
411
412        getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
413
414        mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone);
415
416        // force the low profile & disabled states into compliance
417        mBarTransitions.init(mVertical);
418        setDisabledFlags(mDisabledFlags, true /* force */);
419        setMenuVisibility(mShowMenu, true /* force */);
420
421        if (DEBUG) {
422            Log.d(TAG, "reorient(): rot=" + mDisplay.getRotation());
423        }
424
425        // swap to x coordinate if orientation is not in vertical
426        if (mDelegateHelper != null) {
427            mDelegateHelper.setSwapXY(mVertical);
428        }
429        boolean isRTL = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
430        mTaskSwitchHelper.setBarState(mVertical, isRTL);
431
432        setNavigationIconHints(mNavigationIconHints, true);
433    }
434
435    @Override
436    protected void onLayout(boolean changed, int l, int t, int r, int b) {
437        super.onLayout(changed, l, t, r, b);
438        mDelegateHelper.setInitialTouchRegion(getHomeButton(), getBackButton(), getRecentsButton());
439    }
440
441    @Override
442    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
443        if (DEBUG) Log.d(TAG, String.format(
444                    "onSizeChanged: (%dx%d) old: (%dx%d)", w, h, oldw, oldh));
445
446        final boolean newVertical = w > 0 && h > w;
447        if (newVertical != mVertical) {
448            mVertical = newVertical;
449            //Log.v(TAG, String.format("onSizeChanged: h=%d, w=%d, vert=%s", h, w, mVertical?"y":"n"));
450            reorient();
451            if (mOnVerticalChangedListener != null) {
452                mOnVerticalChangedListener.onVerticalChanged(newVertical);
453            }
454        }
455
456        postCheckForInvalidLayout("sizeChanged");
457        super.onSizeChanged(w, h, oldw, oldh);
458    }
459
460    @Override
461    protected void onConfigurationChanged(Configuration newConfig) {
462        super.onConfigurationChanged(newConfig);
463        updateRTLOrder();
464    }
465
466    /**
467     * In landscape, the LinearLayout is not auto mirrored since it is vertical. Therefore we
468     * have to do it manually
469     */
470    private void updateRTLOrder() {
471        boolean isLayoutRtl = getResources().getConfiguration()
472                .getLayoutDirection() == LAYOUT_DIRECTION_RTL;
473        if (mIsLayoutRtl != isLayoutRtl) {
474
475            // We swap all children of the 90 and 270 degree layouts, since they are vertical
476            View rotation90 = mRotatedViews[Surface.ROTATION_90];
477            swapChildrenOrderIfVertical(rotation90.findViewById(R.id.nav_buttons));
478
479            View rotation270 = mRotatedViews[Surface.ROTATION_270];
480            if (rotation90 != rotation270) {
481                swapChildrenOrderIfVertical(rotation270.findViewById(R.id.nav_buttons));
482            }
483            mIsLayoutRtl = isLayoutRtl;
484        }
485    }
486
487
488    /**
489     * Swaps the children order of a LinearLayout if it's orientation is Vertical
490     *
491     * @param group The LinearLayout to swap the children from.
492     */
493    private void swapChildrenOrderIfVertical(View group) {
494        if (group instanceof LinearLayout) {
495            LinearLayout linearLayout = (LinearLayout) group;
496            if (linearLayout.getOrientation() == VERTICAL) {
497                int childCount = linearLayout.getChildCount();
498                ArrayList<View> childList = new ArrayList<>(childCount);
499                for (int i = 0; i < childCount; i++) {
500                    childList.add(linearLayout.getChildAt(i));
501                }
502                linearLayout.removeAllViews();
503                for (int i = childCount - 1; i >= 0; i--) {
504                    linearLayout.addView(childList.get(i));
505                }
506            }
507        }
508    }
509
510    /*
511    @Override
512    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
513        if (DEBUG) Log.d(TAG, String.format(
514                    "onLayout: %s (%d,%d,%d,%d)",
515                    changed?"changed":"notchanged", left, top, right, bottom));
516        super.onLayout(changed, left, top, right, bottom);
517    }
518
519    // uncomment this for extra defensiveness in WORKAROUND_INVALID_LAYOUT situations: if all else
520    // fails, any touch on the display will fix the layout.
521    @Override
522    public boolean onInterceptTouchEvent(MotionEvent ev) {
523        if (DEBUG) Log.d(TAG, "onInterceptTouchEvent: " + ev.toString());
524        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
525            postCheckForInvalidLayout("touch");
526        }
527        return super.onInterceptTouchEvent(ev);
528    }
529    */
530
531
532    private String getResourceName(int resId) {
533        if (resId != 0) {
534            final android.content.res.Resources res = getContext().getResources();
535            try {
536                return res.getResourceName(resId);
537            } catch (android.content.res.Resources.NotFoundException ex) {
538                return "(unknown)";
539            }
540        } else {
541            return "(null)";
542        }
543    }
544
545    private void postCheckForInvalidLayout(final String how) {
546        mHandler.obtainMessage(MSG_CHECK_INVALID_LAYOUT, 0, 0, how).sendToTarget();
547    }
548
549    private static String visibilityToString(int vis) {
550        switch (vis) {
551            case View.INVISIBLE:
552                return "INVISIBLE";
553            case View.GONE:
554                return "GONE";
555            default:
556                return "VISIBLE";
557        }
558    }
559
560    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
561        pw.println("NavigationBarView {");
562        final Rect r = new Rect();
563        final Point size = new Point();
564        mDisplay.getRealSize(size);
565
566        pw.println(String.format("      this: " + PhoneStatusBar.viewInfo(this)
567                        + " " + visibilityToString(getVisibility())));
568
569        getWindowVisibleDisplayFrame(r);
570        final boolean offscreen = r.right > size.x || r.bottom > size.y;
571        pw.println("      window: "
572                + r.toShortString()
573                + " " + visibilityToString(getWindowVisibility())
574                + (offscreen ? " OFFSCREEN!" : ""));
575
576        pw.println(String.format("      mCurrentView: id=%s (%dx%d) %s",
577                        getResourceName(mCurrentView.getId()),
578                        mCurrentView.getWidth(), mCurrentView.getHeight(),
579                        visibilityToString(mCurrentView.getVisibility())));
580
581        pw.println(String.format("      disabled=0x%08x vertical=%s menu=%s",
582                        mDisabledFlags,
583                        mVertical ? "true" : "false",
584                        mShowMenu ? "true" : "false"));
585
586        dumpButton(pw, "back", getBackButton());
587        dumpButton(pw, "home", getHomeButton());
588        dumpButton(pw, "rcnt", getRecentsButton());
589        dumpButton(pw, "menu", getMenuButton());
590
591        pw.println("    }");
592    }
593
594    private static void dumpButton(PrintWriter pw, String caption, View button) {
595        pw.print("      " + caption + ": ");
596        if (button == null) {
597            pw.print("null");
598        } else {
599            pw.print(PhoneStatusBar.viewInfo(button)
600                    + " " + visibilityToString(button.getVisibility())
601                    + " alpha=" + button.getAlpha()
602                    );
603            if (button instanceof KeyButtonView) {
604                pw.print(" drawingAlpha=" + ((KeyButtonView)button).getDrawingAlpha());
605                pw.print(" quiescentAlpha=" + ((KeyButtonView)button).getQuiescentAlpha());
606            }
607        }
608        pw.println();
609    }
610
611    public interface OnVerticalChangedListener {
612        void onVerticalChanged(boolean isVertical);
613    }
614}
615