/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.phone; import android.animation.LayoutTransition; import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.app.ActivityManagerNative; import android.app.StatusBarManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.provider.MediaStore; import android.util.AttributeSet; import android.util.Log; import android.view.Display; import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; import android.widget.LinearLayout; import com.android.systemui.R; import com.android.systemui.statusbar.BaseStatusBar; import com.android.systemui.statusbar.DelegateViewHelper; import com.android.systemui.statusbar.policy.DeadZone; import com.android.systemui.statusbar.policy.KeyButtonView; import java.io.FileDescriptor; import java.io.PrintWriter; import static com.android.systemui.statusbar.phone.KeyguardTouchDelegate.OnKeyguardConnectionListener; public class NavigationBarView extends LinearLayout { final static boolean DEBUG = false; final static String TAG = "PhoneStatusBar/NavigationBarView"; final static boolean NAVBAR_ALWAYS_AT_RIGHT = true; // slippery nav bar when everything is disabled, e.g. during setup final static boolean SLIPPERY_WHEN_DISABLED = true; final Display mDisplay; View mCurrentView = null; View[] mRotatedViews = new View[4]; int mBarSize; boolean mVertical; boolean mScreenOn; boolean mShowMenu; int mDisabledFlags = 0; int mNavigationIconHints = 0; private Drawable mBackIcon, mBackLandIcon, mBackAltIcon, mBackAltLandIcon; private Drawable mRecentIcon; private Drawable mRecentLandIcon; private DelegateViewHelper mDelegateHelper; private DeadZone mDeadZone; private final NavigationBarTransitions mBarTransitions; // workaround for LayoutTransitions leaving the nav buttons in a weird state (bug 5549288) final static boolean WORKAROUND_INVALID_LAYOUT = true; final static int MSG_CHECK_INVALID_LAYOUT = 8686; private final float mCameraDragDistance; // used to disable the camera icon in navbar when disabled by DPM private boolean mCameraDisabledByDpm; // performs manual animation in sync with layout transitions private final NavTransitionListener mTransitionListener = new NavTransitionListener(); private final PowerManager mPowerManager; private class NavTransitionListener implements TransitionListener { private boolean mBackTransitioning; private boolean mHomeAppearing; private long mStartDelay; private long mDuration; private TimeInterpolator mInterpolator; @Override public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { if (view.getId() == R.id.back) { mBackTransitioning = true; } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) { mHomeAppearing = true; mStartDelay = transition.getStartDelay(transitionType); mDuration = transition.getDuration(transitionType); mInterpolator = transition.getInterpolator(transitionType); } } @Override public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { if (view.getId() == R.id.back) { mBackTransitioning = false; } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) { mHomeAppearing = false; } } public void onBackAltCleared() { // When dismissing ime during unlock, force the back button to run the same appearance // animation as home (if we catch this condition early enough). if (!mBackTransitioning && getBackButton().getVisibility() == VISIBLE && mHomeAppearing && getHomeButton().getAlpha() == 0) { getBackButton().setAlpha(0); ValueAnimator a = ObjectAnimator.ofFloat(getBackButton(), "alpha", 0, 1); a.setStartDelay(mStartDelay); a.setDuration(mDuration); a.setInterpolator(mInterpolator); a.start(); } } } // simplified click handler to be used when device is in accessibility mode private final OnClickListener mAccessibilityClickListener = new OnClickListener() { @Override public void onClick(View v) { if (v.getId() == R.id.camera_button) { KeyguardTouchDelegate.getInstance(getContext()).launchCamera(); } else if (v.getId() == R.id.search_light) { KeyguardTouchDelegate.getInstance(getContext()).showAssistant(); } } }; private final int mScaledTouchSlop; private final OnTouchListener mCameraTouchListener = new OnTouchListener() { private float mStartX; private boolean mTouchSlopReached; private boolean mSkipCancelAnimation; @Override public boolean onTouch(final View cameraButtonView, MotionEvent event) { float realX = event.getRawX(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // disable search gesture while interacting with camera mDelegateHelper.setDisabled(true); mBarTransitions.setContentVisible(false); mStartX = realX; mTouchSlopReached = false; mSkipCancelAnimation = false; break; case MotionEvent.ACTION_MOVE: if (realX > mStartX) { realX = mStartX; } if (realX < mStartX - mCameraDragDistance) { ((KeyButtonView) cameraButtonView).setPressed(true); mPowerManager.userActivity(event.getEventTime(), false); } else { ((KeyButtonView) cameraButtonView).setPressed(false); } if (realX < mStartX - mScaledTouchSlop) { mTouchSlopReached = true; } cameraButtonView.setTranslationX(Math.max(realX - mStartX, -mCameraDragDistance)); break; case MotionEvent.ACTION_UP: if (realX < mStartX - mCameraDragDistance) { mContext.startActivityAsUser( new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE), UserHandle.CURRENT); } if (realX < mStartX - mScaledTouchSlop) { mTouchSlopReached = true; } if (!mTouchSlopReached) { mSkipCancelAnimation = true; cameraButtonView.animate().translationX(-mCameraDragDistance / 2). setInterpolator(new DecelerateInterpolator()).withEndAction( new Runnable() { @Override public void run() { cameraButtonView.animate().translationX(0). setInterpolator(new AccelerateInterpolator()); } }); } case MotionEvent.ACTION_CANCEL: ((KeyButtonView) cameraButtonView).setPressed(false); mDelegateHelper.setDisabled(false); mBarTransitions.setContentVisible(true); if (!mSkipCancelAnimation) { cameraButtonView.animate().translationX(0) .setInterpolator(new AccelerateInterpolator(2f)); } break; } return true; } }; private final OnKeyguardConnectionListener mKeyguardConnectionListener = new OnKeyguardConnectionListener() { @Override public void onKeyguardServiceConnected( KeyguardTouchDelegate keyguardTouchDelegate) { post(new Runnable() { @Override public void run() { mCameraDisabledByDpm = isCameraDisabledByDpm(); } }); } @Override public void onKeyguardServiceDisconnected( KeyguardTouchDelegate keyguardTouchDelegate) { } }; private class H extends Handler { public void handleMessage(Message m) { switch (m.what) { case MSG_CHECK_INVALID_LAYOUT: final String how = "" + m.obj; final int w = getWidth(); final int h = getHeight(); final int vw = mCurrentView.getWidth(); final int vh = mCurrentView.getHeight(); if (h != vh || w != vw) { Log.w(TAG, String.format( "*** Invalid layout in navigation bar (%s this=%dx%d cur=%dx%d)", how, w, h, vw, vh)); if (WORKAROUND_INVALID_LAYOUT) { requestLayout(); } } break; } } } public NavigationBarView(Context context, AttributeSet attrs) { super(context, attrs); mDisplay = ((WindowManager)context.getSystemService( Context.WINDOW_SERVICE)).getDefaultDisplay(); final Resources res = getContext().getResources(); mBarSize = res.getDimensionPixelSize(R.dimen.navigation_bar_size); mVertical = false; mShowMenu = false; mDelegateHelper = new DelegateViewHelper(this); getIcons(res); mBarTransitions = new NavigationBarTransitions(this); KeyguardTouchDelegate.addListener(mKeyguardConnectionListener); mCameraDisabledByDpm = isCameraDisabledByDpm(); watchForDevicePolicyChanges(); mCameraDragDistance = res.getDimension(R.dimen.camera_drag_distance); mScaledTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); } private void watchForDevicePolicyChanges() { final IntentFilter filter = new IntentFilter(); filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); getContext().registerReceiver(new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { post(new Runnable() { @Override public void run() { mCameraDisabledByDpm = isCameraDisabledByDpm(); } }); } }, filter); } public BarTransitions getBarTransitions() { return mBarTransitions; } public void setDelegateView(View view) { mDelegateHelper.setDelegateView(view); } public void setBar(BaseStatusBar phoneStatusBar) { mDelegateHelper.setBar(phoneStatusBar); } @Override public boolean onTouchEvent(MotionEvent event) { if (mDeadZone != null && event.getAction() == MotionEvent.ACTION_OUTSIDE) { mDeadZone.poke(event); } if (mDelegateHelper != null) { boolean ret = mDelegateHelper.onInterceptTouchEvent(event); if (ret) return true; } return super.onTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return mDelegateHelper.onInterceptTouchEvent(event); } private H mHandler = new H(); public View getCurrentView() { return mCurrentView; } public View getRecentsButton() { return mCurrentView.findViewById(R.id.recent_apps); } public View getMenuButton() { return mCurrentView.findViewById(R.id.menu); } public View getBackButton() { return mCurrentView.findViewById(R.id.back); } public View getHomeButton() { return mCurrentView.findViewById(R.id.home); } // for when home is disabled, but search isn't public View getSearchLight() { return mCurrentView.findViewById(R.id.search_light); } // shown when keyguard is visible and camera is available public View getCameraButton() { return mCurrentView.findViewById(R.id.camera_button); } private void getIcons(Resources res) { mBackIcon = res.getDrawable(R.drawable.ic_sysbar_back); mBackLandIcon = res.getDrawable(R.drawable.ic_sysbar_back_land); mBackAltIcon = res.getDrawable(R.drawable.ic_sysbar_back_ime); mBackAltLandIcon = res.getDrawable(R.drawable.ic_sysbar_back_ime); mRecentIcon = res.getDrawable(R.drawable.ic_sysbar_recent); mRecentLandIcon = res.getDrawable(R.drawable.ic_sysbar_recent_land); } @Override public void setLayoutDirection(int layoutDirection) { getIcons(getContext().getResources()); super.setLayoutDirection(layoutDirection); } public void notifyScreenOn(boolean screenOn) { mScreenOn = screenOn; setDisabledFlags(mDisabledFlags, true); } public void setNavigationIconHints(int hints) { setNavigationIconHints(hints, false); } public void setNavigationIconHints(int hints, boolean force) { if (!force && hints == mNavigationIconHints) return; final boolean backAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; if ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0 && !backAlt) { mTransitionListener.onBackAltCleared(); } if (DEBUG) { android.widget.Toast.makeText(getContext(), "Navigation icon hints = " + hints, 500).show(); } mNavigationIconHints = hints; ((ImageView)getBackButton()).setImageDrawable(backAlt ? (mVertical ? mBackAltLandIcon : mBackAltIcon) : (mVertical ? mBackLandIcon : mBackIcon)); ((ImageView)getRecentsButton()).setImageDrawable(mVertical ? mRecentLandIcon : mRecentIcon); setDisabledFlags(mDisabledFlags, true); } public void setDisabledFlags(int disabledFlags) { setDisabledFlags(disabledFlags, false); } public void setDisabledFlags(int disabledFlags, boolean force) { if (!force && mDisabledFlags == disabledFlags) return; mDisabledFlags = disabledFlags; final boolean disableHome = ((disabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0); final boolean disableRecent = ((disabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0); final boolean disableBack = ((disabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0) && ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) == 0); final boolean disableSearch = ((disabledFlags & View.STATUS_BAR_DISABLE_SEARCH) != 0); if (SLIPPERY_WHEN_DISABLED) { setSlippery(disableHome && disableRecent && disableBack && disableSearch); } ViewGroup navButtons = (ViewGroup) mCurrentView.findViewById(R.id.nav_buttons); if (navButtons != null) { LayoutTransition lt = navButtons.getLayoutTransition(); if (lt != null) { if (!lt.getTransitionListeners().contains(mTransitionListener)) { lt.addTransitionListener(mTransitionListener); } if (!mScreenOn && mCurrentView != null) { lt.disableTransitionType( LayoutTransition.CHANGE_APPEARING | LayoutTransition.CHANGE_DISAPPEARING | LayoutTransition.APPEARING | LayoutTransition.DISAPPEARING); } } } getBackButton() .setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE); getHomeButton() .setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); final boolean showSearch = disableHome && !disableSearch; final boolean showCamera = showSearch && !mCameraDisabledByDpm; setVisibleOrGone(getSearchLight(), showSearch); setVisibleOrGone(getCameraButton(), showCamera); mBarTransitions.applyBackButtonQuiescentAlpha(mBarTransitions.getMode(), true /*animate*/); } private void setVisibleOrGone(View view, boolean visible) { if (view != null) { view.setVisibility(visible ? VISIBLE : GONE); } } private boolean isCameraDisabledByDpm() { final DevicePolicyManager dpm = (DevicePolicyManager) getContext().getSystemService(Context.DEVICE_POLICY_SERVICE); if (dpm != null) { try { final int userId = ActivityManagerNative.getDefault().getCurrentUser().id; final int disabledFlags = dpm.getKeyguardDisabledFeatures(null, userId); final boolean disabledBecauseKeyguardSecure = (disabledFlags & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) != 0 && KeyguardTouchDelegate.getInstance(getContext()).isSecure(); return dpm.getCameraDisabled(null) || disabledBecauseKeyguardSecure; } catch (RemoteException e) { Log.e(TAG, "Can't get userId", e); } } return false; } public void setSlippery(boolean newSlippery) { WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams(); if (lp != null) { boolean oldSlippery = (lp.flags & WindowManager.LayoutParams.FLAG_SLIPPERY) != 0; if (!oldSlippery && newSlippery) { lp.flags |= WindowManager.LayoutParams.FLAG_SLIPPERY; } else if (oldSlippery && !newSlippery) { lp.flags &= ~WindowManager.LayoutParams.FLAG_SLIPPERY; } else { return; } WindowManager wm = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE); wm.updateViewLayout(this, lp); } } public void setMenuVisibility(final boolean show) { setMenuVisibility(show, false); } public void setMenuVisibility(final boolean show, final boolean force) { if (!force && mShowMenu == show) return; mShowMenu = show; getMenuButton().setVisibility(mShowMenu ? View.VISIBLE : View.INVISIBLE); } @Override public void onFinishInflate() { mRotatedViews[Surface.ROTATION_0] = mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0); mRotatedViews[Surface.ROTATION_90] = findViewById(R.id.rot90); mRotatedViews[Surface.ROTATION_270] = NAVBAR_ALWAYS_AT_RIGHT ? findViewById(R.id.rot90) : findViewById(R.id.rot270); mCurrentView = mRotatedViews[Surface.ROTATION_0]; watchForAccessibilityChanges(); } private void watchForAccessibilityChanges() { final AccessibilityManager am = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); // Set the initial state enableAccessibility(am.isTouchExplorationEnabled()); // Watch for changes am.addTouchExplorationStateChangeListener(new TouchExplorationStateChangeListener() { @Override public void onTouchExplorationStateChanged(boolean enabled) { enableAccessibility(enabled); } }); } private void enableAccessibility(boolean touchEnabled) { Log.v(TAG, "touchEnabled:" + touchEnabled); // Add a touch handler or accessibility click listener for camera and search buttons // for all view orientations. final OnClickListener onClickListener = touchEnabled ? mAccessibilityClickListener : null; final OnTouchListener onTouchListener = touchEnabled ? null : mCameraTouchListener; boolean hasCamera = false; for (int i = 0; i < mRotatedViews.length; i++) { final View cameraButton = mRotatedViews[i].findViewById(R.id.camera_button); final View searchLight = mRotatedViews[i].findViewById(R.id.search_light); if (cameraButton != null) { hasCamera = true; cameraButton.setOnTouchListener(onTouchListener); cameraButton.setOnClickListener(onClickListener); } if (searchLight != null) { searchLight.setOnClickListener(onClickListener); } } if (hasCamera) { // Warm up KeyguardTouchDelegate so it's ready by the time the camera button is touched. // This will connect to KeyguardService so that touch events are processed. KeyguardTouchDelegate.getInstance(getContext()); } } public boolean isVertical() { return mVertical; } public void reorient() { final int rot = mDisplay.getRotation(); for (int i=0; i<4; i++) { mRotatedViews[i].setVisibility(View.GONE); } mCurrentView = mRotatedViews[rot]; mCurrentView.setVisibility(View.VISIBLE); mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone); // force the low profile & disabled states into compliance mBarTransitions.init(mVertical); setDisabledFlags(mDisabledFlags, true /* force */); setMenuVisibility(mShowMenu, true /* force */); if (DEBUG) { Log.d(TAG, "reorient(): rot=" + mDisplay.getRotation()); } // swap to x coordinate if orientation is not in vertical if (mDelegateHelper != null) { mDelegateHelper.setSwapXY(!mVertical); } setNavigationIconHints(mNavigationIconHints, true); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mDelegateHelper.setInitialTouchRegion(getHomeButton(), getBackButton(), getRecentsButton()); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if (DEBUG) Log.d(TAG, String.format( "onSizeChanged: (%dx%d) old: (%dx%d)", w, h, oldw, oldh)); final boolean newVertical = w > 0 && h > w; if (newVertical != mVertical) { mVertical = newVertical; //Log.v(TAG, String.format("onSizeChanged: h=%d, w=%d, vert=%s", h, w, mVertical?"y":"n")); reorient(); } postCheckForInvalidLayout("sizeChanged"); super.onSizeChanged(w, h, oldw, oldh); } /* @Override protected void onLayout (boolean changed, int left, int top, int right, int bottom) { if (DEBUG) Log.d(TAG, String.format( "onLayout: %s (%d,%d,%d,%d)", changed?"changed":"notchanged", left, top, right, bottom)); super.onLayout(changed, left, top, right, bottom); } // uncomment this for extra defensiveness in WORKAROUND_INVALID_LAYOUT situations: if all else // fails, any touch on the display will fix the layout. @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (DEBUG) Log.d(TAG, "onInterceptTouchEvent: " + ev.toString()); if (ev.getAction() == MotionEvent.ACTION_DOWN) { postCheckForInvalidLayout("touch"); } return super.onInterceptTouchEvent(ev); } */ private String getResourceName(int resId) { if (resId != 0) { final android.content.res.Resources res = getContext().getResources(); try { return res.getResourceName(resId); } catch (android.content.res.Resources.NotFoundException ex) { return "(unknown)"; } } else { return "(null)"; } } private void postCheckForInvalidLayout(final String how) { mHandler.obtainMessage(MSG_CHECK_INVALID_LAYOUT, 0, 0, how).sendToTarget(); } private static String visibilityToString(int vis) { switch (vis) { case View.INVISIBLE: return "INVISIBLE"; case View.GONE: return "GONE"; default: return "VISIBLE"; } } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("NavigationBarView {"); final Rect r = new Rect(); final Point size = new Point(); mDisplay.getRealSize(size); pw.println(String.format(" this: " + PhoneStatusBar.viewInfo(this) + " " + visibilityToString(getVisibility()))); getWindowVisibleDisplayFrame(r); final boolean offscreen = r.right > size.x || r.bottom > size.y; pw.println(" window: " + r.toShortString() + " " + visibilityToString(getWindowVisibility()) + (offscreen ? " OFFSCREEN!" : "")); pw.println(String.format(" mCurrentView: id=%s (%dx%d) %s", getResourceName(mCurrentView.getId()), mCurrentView.getWidth(), mCurrentView.getHeight(), visibilityToString(mCurrentView.getVisibility()))); pw.println(String.format(" disabled=0x%08x vertical=%s menu=%s", mDisabledFlags, mVertical ? "true" : "false", mShowMenu ? "true" : "false")); dumpButton(pw, "back", getBackButton()); dumpButton(pw, "home", getHomeButton()); dumpButton(pw, "rcnt", getRecentsButton()); dumpButton(pw, "menu", getMenuButton()); dumpButton(pw, "srch", getSearchLight()); dumpButton(pw, "cmra", getCameraButton()); pw.println(" }"); } private static void dumpButton(PrintWriter pw, String caption, View button) { pw.print(" " + caption + ": "); if (button == null) { pw.print("null"); } else { pw.print(PhoneStatusBar.viewInfo(button) + " " + visibilityToString(button.getVisibility()) + " alpha=" + button.getAlpha() ); if (button instanceof KeyButtonView) { pw.print(" drawingAlpha=" + ((KeyButtonView)button).getDrawingAlpha()); pw.print(" quiescentAlpha=" + ((KeyButtonView)button).getQuiescentAlpha()); } } pw.println(); } }