diff options
Diffstat (limited to 'packages/SystemUI/src')
156 files changed, 9165 insertions, 4862 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/DemoMode.java b/packages/SystemUI/src/com/android/systemui/DemoMode.java index c16c3a1..9c206e2 100644 --- a/packages/SystemUI/src/com/android/systemui/DemoMode.java +++ b/packages/SystemUI/src/com/android/systemui/DemoMode.java @@ -32,4 +32,5 @@ public interface DemoMode { public static final String COMMAND_BARS = "bars"; public static final String COMMAND_STATUS = "status"; public static final String COMMAND_NOTIFICATIONS = "notifications"; + public static final String COMMAND_VOLUME = "volume"; } diff --git a/packages/SystemUI/src/com/android/systemui/EventLogConstants.java b/packages/SystemUI/src/com/android/systemui/EventLogConstants.java new file mode 100644 index 0000000..c8af2d4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/EventLogConstants.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 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; + +/** + * Constants to be passed as sysui_* eventlog parameters. + */ +public class EventLogConstants { + /** The user swiped up on the lockscreen, unlocking the device. */ + public static final int SYSUI_LOCKSCREEN_GESTURE_SWIPE_UP_UNLOCK = 1; + /** The user swiped down on the lockscreen, going to the full shade. */ + public static final int SYSUI_LOCKSCREEN_GESTURE_SWIPE_DOWN_FULL_SHADE = 2; + /** The user tapped in an empty area, causing the unlock hint to be shown. */ + public static final int SYSUI_LOCKSCREEN_GESTURE_TAP_UNLOCK_HINT = 3; + /** The user swiped inward on the camera icon, launching the camera. */ + public static final int SYSUI_LOCKSCREEN_GESTURE_SWIPE_CAMERA = 4; + /** The user swiped inward on the dialer icon, launching the dialer. */ + public static final int SYSUI_LOCKSCREEN_GESTURE_SWIPE_DIALER = 5; + /** The user tapped the lock, locking the device. */ + public static final int SYSUI_LOCKSCREEN_GESTURE_TAP_LOCK = 6; + /** The user tapped a notification, needs to tap again to launch. */ + public static final int SYSUI_LOCKSCREEN_GESTURE_TAP_NOTIFICATION_ACTIVATE = 7; +} diff --git a/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags index 5f2c348..d2ce94b 100644 --- a/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags +++ b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags @@ -7,6 +7,16 @@ option java_package com.android.systemui; # --------------------------- 36000 sysui_statusbar_touch (type|1),(x|1),(y|1),(enabled|1) 36001 sysui_heads_up_status (key|3),(visible|1) +36002 sysui_fullscreen_notification (key|3) +36003 sysui_heads_up_escalation (key|3) +# sysui_status_bar_state: Logged whenever the status bar / keyguard state changes +## state: 0: SHADE, 1: KEYGUARD, 2: SHADE_LOCKED +## keyguardShowing: 1: Keyguard shown to the user (or keyguardOccluded) +## keyguardOccluded: 1: Keyguard active, but another activity is occluding it +## bouncerShowing: 1: Bouncer currently shown to the user +## secure: 1: The user has set up a secure unlock method (PIN, password, etc.) +## currentlyInsecure: 1: No secure unlock method set up (!secure), or trusted environment (TrustManager) +36004 sysui_status_bar_state (state|1),(keyguardShowing|1),(keyguardOccluded|1),(bouncerShowing|1),(secure|1),(currentlyInsecure|1) # --------------------------- # PhoneStatusBarView.java @@ -17,6 +27,15 @@ option java_package com.android.systemui; # NotificationPanelView.java # --------------------------- 36020 sysui_notificationpanel_touch (type|1),(x|1),(y|1) +## type: 1: SWIPE_UP_UNLOCK Swiped up to dismiss the lockscreen. +## 2: SWIPE_DOWN_FULL_SHADE Swiped down to enter full shade. +## 3: TAP_UNLOCK_HINT Tapped in empty area, causes unlock hint. +## 4: SWIPE_CAMERA Swiped the camera icon, launches. +## 5: SWIPE_DIALER Swiped the dialer icon, launches. +## 6: TAP_LOCK Tapped the (unlocked) lock icon, locks the device. +## 7: TAP_NOTIFICATION_ACTIVATE Tapped a lockscreen notification, causes "tap again" hint. +## Note: Second tap logged as notification_clicked. +36021 sysui_lockscreen_gesture (type|1),(lengthDp|1),(velocityDp|1) # --------------------------- # SettingsPanelView.java diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java index 0f8fe1c..d42ac61 100644 --- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java +++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java @@ -351,6 +351,9 @@ public class ExpandHelper implements Gefingerpoken { mVelocityTracker.addMovement(event); break; case MotionEvent.ACTION_MOVE: + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } mVelocityTracker.addMovement(event); break; default: diff --git a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java index 4857adc..7c725b3 100644 --- a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java @@ -105,10 +105,6 @@ public class ImageWallpaper extends WallpaperService { static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; static final int EGL_OPENGL_ES2_BIT = 4; - // TODO: Not currently used, keeping around until we know we don't need it - @SuppressWarnings({"UnusedDeclaration"}) - private WallpaperObserver mReceiver; - Bitmap mBackground; int mBackgroundWidth = -1, mBackgroundHeight = -1; int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1; @@ -151,22 +147,6 @@ public class ImageWallpaper extends WallpaperService { private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; - class WallpaperObserver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (DEBUG) { - Log.d(TAG, "onReceive"); - } - - mLastSurfaceWidth = mLastSurfaceHeight = -1; - mBackground = null; - mBackgroundWidth = -1; - mBackgroundHeight = -1; - mRedrawNeeded = true; - drawFrame(); - } - } - public DrawableEngine() { super(); setFixedSizeAllowed(true); @@ -174,7 +154,7 @@ public class ImageWallpaper extends WallpaperService { public void trimMemory(int level) { if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW && - mBackground != null && mIsHwAccelerated) { + mBackground != null) { if (DEBUG) { Log.d(TAG, "trimMemory"); } @@ -194,12 +174,6 @@ public class ImageWallpaper extends WallpaperService { super.onCreate(surfaceHolder); - // TODO: Don't need this currently because the wallpaper service - // will restart the image wallpaper whenever the image changes. - //IntentFilter filter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED); - //mReceiver = new WallpaperObserver(); - //registerReceiver(mReceiver, filter, null, mHandler); - updateSurfaceSize(surfaceHolder); setOffsetNotificationsEnabled(false); @@ -208,10 +182,8 @@ public class ImageWallpaper extends WallpaperService { @Override public void onDestroy() { super.onDestroy(); - if (mReceiver != null) { - unregisterReceiver(mReceiver); - } mBackground = null; + mWallpaperManager.forgetLoadedWallpaper(); } void updateSurfaceSize(SurfaceHolder surfaceHolder) { @@ -337,111 +309,116 @@ public class ImageWallpaper extends WallpaperService { } void drawFrame() { - int newRotation = ((WindowManager) getSystemService(WINDOW_SERVICE)). - getDefaultDisplay().getRotation(); - - // Sometimes a wallpaper is not large enough to cover the screen in one dimension. - // Call updateSurfaceSize -- it will only actually do the update if the dimensions - // should change - if (newRotation != mLastRotation) { - // Update surface size (if necessary) - updateSurfaceSize(getSurfaceHolder()); - } - SurfaceHolder sh = getSurfaceHolder(); - final Rect frame = sh.getSurfaceFrame(); - final int dw = frame.width(); - final int dh = frame.height(); - boolean surfaceDimensionsChanged = dw != mLastSurfaceWidth || dh != mLastSurfaceHeight; - - boolean redrawNeeded = surfaceDimensionsChanged || newRotation != mLastRotation; - if (!redrawNeeded && !mOffsetsChanged) { - if (DEBUG) { - Log.d(TAG, "Suppressed drawFrame since redraw is not needed " - + "and offsets have not changed."); + try { + int newRotation = ((WindowManager) getSystemService(WINDOW_SERVICE)). + getDefaultDisplay().getRotation(); + + // Sometimes a wallpaper is not large enough to cover the screen in one dimension. + // Call updateSurfaceSize -- it will only actually do the update if the dimensions + // should change + if (newRotation != mLastRotation) { + // Update surface size (if necessary) + updateSurfaceSize(getSurfaceHolder()); } - return; - } - mLastRotation = newRotation; + SurfaceHolder sh = getSurfaceHolder(); + final Rect frame = sh.getSurfaceFrame(); + final int dw = frame.width(); + final int dh = frame.height(); + boolean surfaceDimensionsChanged = dw != mLastSurfaceWidth + || dh != mLastSurfaceHeight; - // Load bitmap if it is not yet loaded or if it was loaded at a different size - if (mBackground == null || surfaceDimensionsChanged) { - if (DEBUG) { - Log.d(TAG, "Reloading bitmap: mBackground, bgw, bgh, dw, dh = " + - mBackground + ", " + - ((mBackground == null) ? 0 : mBackground.getWidth()) + ", " + - ((mBackground == null) ? 0 : mBackground.getHeight()) + ", " + - dw + ", " + dh); - } - mWallpaperManager.forgetLoadedWallpaper(); - updateWallpaperLocked(); - if (mBackground == null) { + boolean redrawNeeded = surfaceDimensionsChanged || newRotation != mLastRotation; + if (!redrawNeeded && !mOffsetsChanged) { if (DEBUG) { - Log.d(TAG, "Unable to load bitmap"); + Log.d(TAG, "Suppressed drawFrame since redraw is not needed " + + "and offsets have not changed."); } return; } - if (DEBUG) { - if (dw != mBackground.getWidth() || dh != mBackground.getHeight()) { - Log.d(TAG, "Surface != bitmap dimensions: surface w/h, bitmap w/h: " + - dw + ", " + dh + ", " + mBackground.getWidth() + ", " + - mBackground.getHeight()); + mLastRotation = newRotation; + + // Load bitmap if it is not yet loaded or if it was loaded at a different size + if (mBackground == null || surfaceDimensionsChanged) { + if (DEBUG) { + Log.d(TAG, "Reloading bitmap: mBackground, bgw, bgh, dw, dh = " + + mBackground + ", " + + ((mBackground == null) ? 0 : mBackground.getWidth()) + ", " + + ((mBackground == null) ? 0 : mBackground.getHeight()) + ", " + + dw + ", " + dh); + } + mWallpaperManager.forgetLoadedWallpaper(); + updateWallpaperLocked(); + if (mBackground == null) { + if (DEBUG) { + Log.d(TAG, "Unable to load bitmap"); + } + return; + } + if (DEBUG) { + if (dw != mBackground.getWidth() || dh != mBackground.getHeight()) { + Log.d(TAG, "Surface != bitmap dimensions: surface w/h, bitmap w/h: " + + dw + ", " + dh + ", " + mBackground.getWidth() + ", " + + mBackground.getHeight()); + } } } - } - // Center the scaled image - mScale = Math.max(1f, Math.max(dw / (float) mBackground.getWidth(), - dh / (float) mBackground.getHeight())); - final int availw = dw - (int) (mBackground.getWidth() * mScale); - final int availh = dh - (int) (mBackground.getHeight() * mScale); - int xPixels = availw / 2; - int yPixels = availh / 2; - - // Adjust the image for xOffset/yOffset values. If window manager is handling offsets, - // mXOffset and mYOffset are set to 0.5f by default and therefore xPixels and yPixels - // will remain unchanged - final int availwUnscaled = dw - mBackground.getWidth(); - final int availhUnscaled = dh - mBackground.getHeight(); - if (availwUnscaled < 0) xPixels += (int)(availwUnscaled * (mXOffset - .5f) + .5f); - if (availhUnscaled < 0) yPixels += (int)(availhUnscaled * (mYOffset - .5f) + .5f); - - mOffsetsChanged = false; - mRedrawNeeded = false; - if (surfaceDimensionsChanged) { - mLastSurfaceWidth = dw; - mLastSurfaceHeight = dh; - } - if (!redrawNeeded && xPixels == mLastXTranslation && yPixels == mLastYTranslation) { - if (DEBUG) { - Log.d(TAG, "Suppressed drawFrame since the image has not " - + "actually moved an integral number of pixels."); + // Center the scaled image + mScale = Math.max(1f, Math.max(dw / (float) mBackground.getWidth(), + dh / (float) mBackground.getHeight())); + final int availw = dw - (int) (mBackground.getWidth() * mScale); + final int availh = dh - (int) (mBackground.getHeight() * mScale); + int xPixels = availw / 2; + int yPixels = availh / 2; + + // Adjust the image for xOffset/yOffset values. If window manager is handling offsets, + // mXOffset and mYOffset are set to 0.5f by default and therefore xPixels and yPixels + // will remain unchanged + final int availwUnscaled = dw - mBackground.getWidth(); + final int availhUnscaled = dh - mBackground.getHeight(); + if (availwUnscaled < 0) + xPixels += (int) (availwUnscaled * (mXOffset - .5f) + .5f); + if (availhUnscaled < 0) + yPixels += (int) (availhUnscaled * (mYOffset - .5f) + .5f); + + mOffsetsChanged = false; + mRedrawNeeded = false; + if (surfaceDimensionsChanged) { + mLastSurfaceWidth = dw; + mLastSurfaceHeight = dh; } - return; - } - mLastXTranslation = xPixels; - mLastYTranslation = yPixels; + if (!redrawNeeded && xPixels == mLastXTranslation && yPixels == mLastYTranslation) { + if (DEBUG) { + Log.d(TAG, "Suppressed drawFrame since the image has not " + + "actually moved an integral number of pixels."); + } + return; + } + mLastXTranslation = xPixels; + mLastYTranslation = yPixels; - if (DEBUG) { - Log.d(TAG, "Redrawing wallpaper"); - } + if (DEBUG) { + Log.d(TAG, "Redrawing wallpaper"); + } - if (mIsHwAccelerated) { - if (!drawWallpaperWithOpenGL(sh, availw, availh, xPixels, yPixels)) { + if (mIsHwAccelerated) { + if (!drawWallpaperWithOpenGL(sh, availw, availh, xPixels, yPixels)) { + drawWallpaperWithCanvas(sh, availw, availh, xPixels, yPixels); + } + } else { drawWallpaperWithCanvas(sh, availw, availh, xPixels, yPixels); } - } else { - drawWallpaperWithCanvas(sh, availw, availh, xPixels, yPixels); - if (FIXED_SIZED_SURFACE) { + } finally { + if (FIXED_SIZED_SURFACE && !mIsHwAccelerated) { // If the surface is fixed-size, we should only need to // draw it once and then we'll let the window manager // position it appropriately. As such, we no longer needed // the loaded bitmap. Yay! - // hw-accelerated path retains bitmap for faster rotation + // hw-accelerated renderer retains bitmap for faster rotation mBackground = null; mWallpaperManager.forgetLoadedWallpaper(); } } - } private void updateWallpaperLocked() { @@ -556,7 +533,7 @@ public class ImageWallpaper extends WallpaperService { boolean status = mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); checkEglError(); - finishGL(); + finishGL(texture, program); return status; } @@ -609,21 +586,18 @@ public class ImageWallpaper extends WallpaperService { int program = glCreateProgram(); glAttachShader(program, vertexShader); - checkGlError(); - glAttachShader(program, fragmentShader); - checkGlError(); - glLinkProgram(program); checkGlError(); + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + int[] status = new int[1]; glGetProgramiv(program, GL_LINK_STATUS, status, 0); if (status[0] != GL_TRUE) { String error = glGetProgramInfoLog(program); Log.d(GL_LOG_TAG, "Error while linking program:\n" + error); - glDeleteShader(vertexShader); - glDeleteShader(fragmentShader); glDeleteProgram(program); return 0; } @@ -666,7 +640,11 @@ public class ImageWallpaper extends WallpaperService { } } - private void finishGL() { + private void finishGL(int texture, int program) { + int[] textures = new int[1]; + textures[0] = texture; + glDeleteTextures(1, textures, 0); + glDeleteProgram(program); mEgl.eglMakeCurrent(mEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); mEgl.eglDestroySurface(mEglDisplay, mEglSurface); mEgl.eglDestroyContext(mEglDisplay, mEglContext); diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index 0d393bf..33f6564 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -311,11 +311,14 @@ public class SwipeHelper implements Gefingerpoken { final View animView = mCallback.getChildContentView(view); final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); float newPos; + boolean isLayoutRtl = view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; if (velocity < 0 || (velocity == 0 && getTranslation(animView) < 0) // if we use the Menu to dismiss an item in landscape, animate up - || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) { + || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y) + // if the language is rtl we prefer swiping to the left + || (velocity == 0 && getTranslation(animView) == 0 && isLayoutRtl)) { newPos = -getSize(animView); } else { newPos = getSize(animView); @@ -450,7 +453,8 @@ public class SwipeHelper implements Gefingerpoken { && !mTouchAboveFalsingThreshold; boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) - && !falsingDetected && (childSwipedFastEnough || childSwipedFarEnough); + && !falsingDetected && (childSwipedFastEnough || childSwipedFarEnough) + && ev.getActionMasked() == MotionEvent.ACTION_UP; if (dismissChild) { // flingadingy diff --git a/packages/SystemUI/src/com/android/systemui/ViewInvertHelper.java b/packages/SystemUI/src/com/android/systemui/ViewInvertHelper.java new file mode 100644 index 0000000..2ff8f8a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ViewInvertHelper.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 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; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +import com.android.systemui.statusbar.phone.NotificationPanelView; + +/** + * Helper to invert the colors of views and fade between the states. + */ +public class ViewInvertHelper { + + private final Paint mDarkPaint = new Paint(); + private final Interpolator mLinearOutSlowInInterpolator; + private final View mTarget; + private final ColorMatrix mMatrix = new ColorMatrix(); + private final ColorMatrix mGrayscaleMatrix = new ColorMatrix(); + private final long mFadeDuration; + + public ViewInvertHelper(View target, long fadeDuration) { + mTarget = target; + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(mTarget.getContext(), + android.R.interpolator.linear_out_slow_in); + mFadeDuration = fadeDuration; + } + + public void fade(final boolean invert, long delay) { + float startIntensity = invert ? 0f : 1f; + float endIntensity = invert ? 1f : 0f; + ValueAnimator animator = ValueAnimator.ofFloat(startIntensity, endIntensity); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + updateInvertPaint((Float) animation.getAnimatedValue()); + mTarget.setLayerType(View.LAYER_TYPE_HARDWARE, mDarkPaint); + } + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (!invert) { + mTarget.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + }); + animator.setDuration(mFadeDuration); + animator.setInterpolator(mLinearOutSlowInInterpolator); + animator.setStartDelay(delay); + animator.start(); + } + + public void update(boolean invert) { + if (invert) { + updateInvertPaint(1f); + mTarget.setLayerType(View.LAYER_TYPE_HARDWARE, mDarkPaint); + } else { + mTarget.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + public View getTarget() { + return mTarget; + } + + private void updateInvertPaint(float intensity) { + float components = 1 - 2 * intensity; + final float[] invert = { + components, 0f, 0f, 0f, 255f * intensity, + 0f, components, 0f, 0f, 255f * intensity, + 0f, 0f, components, 0f, 255f * intensity, + 0f, 0f, 0f, 1f, 0f + }; + mMatrix.set(invert); + mGrayscaleMatrix.setSaturation(1 - intensity); + mMatrix.preConcat(mGrayscaleMatrix); + mDarkPaint.setColorFilter(new ColorMatrixColorFilter(mMatrix)); + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java index e92f988..89a2c74 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java @@ -25,9 +25,10 @@ public interface DozeHost { void addCallback(@NonNull Callback callback); void removeCallback(@NonNull Callback callback); void startDozing(@NonNull Runnable ready); - void pulseWhileDozing(@NonNull PulseCallback callback); + void pulseWhileDozing(@NonNull PulseCallback callback, int reason); void stopDozing(); boolean isPowerSaveActive(); + boolean isNotificationLightOn(); public interface Callback { void onNewNotifications(); @@ -40,4 +41,4 @@ public interface DozeHost { void onPulseStarted(); void onPulseFinished(); } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index fcdbfc1..1f3a830 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -35,6 +35,13 @@ public class DozeLog { private static final int SIZE = Build.IS_DEBUGGABLE ? 400 : 50; private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); + private static final int PULSE_REASONS = 4; + + public static final int PULSE_REASON_INTENT = 0; + public static final int PULSE_REASON_NOTIFICATION = 1; + public static final int PULSE_REASON_SENSOR_SIGMOTION = 2; + public static final int PULSE_REASON_SENSOR_PICKUP = 3; + private static long[] sTimes; private static String[] sMessages; private static int sPosition; @@ -48,8 +55,7 @@ public class DozeLog { private static SummaryStats sScreenOnPulsingStats; private static SummaryStats sScreenOnNotPulsingStats; private static SummaryStats sEmergencyCallStats; - private static SummaryStats sProxNearStats; - private static SummaryStats sProxFarStats; + private static SummaryStats[][] sProxStats; // [reason][near/far] public static void tracePickupPulse(boolean withinVibrationThreshold) { if (!ENABLED) return; @@ -58,10 +64,10 @@ public class DozeLog { : sPickupPulseNotNearVibrationStats).append(); } - public static void tracePulseStart() { + public static void tracePulseStart(int reason) { if (!ENABLED) return; sPulsing = true; - log("pulseStart"); + log("pulseStart reason=" + pulseReasonToString(reason)); } public static void tracePulseFinish() { @@ -90,8 +96,11 @@ public class DozeLog { sScreenOnPulsingStats = new SummaryStats(); sScreenOnNotPulsingStats = new SummaryStats(); sEmergencyCallStats = new SummaryStats(); - sProxNearStats = new SummaryStats(); - sProxFarStats = new SummaryStats(); + sProxStats = new SummaryStats[PULSE_REASONS][2]; + for (int i = 0; i < PULSE_REASONS; i++) { + sProxStats[i][0] = new SummaryStats(); + sProxStats[i][1] = new SummaryStats(); + } log("init"); KeyguardUpdateMonitor.getInstance(context).registerCallback(sKeyguardCallback); } @@ -137,10 +146,21 @@ public class DozeLog { } } - public static void traceProximityResult(boolean near, long millis) { + public static void traceProximityResult(boolean near, long millis, int pulseReason) { if (!ENABLED) return; - log("proximityResult near=" + near + " millis=" + millis); - (near ? sProxNearStats : sProxFarStats).append(); + log("proximityResult reason=" + pulseReasonToString(pulseReason) + " near=" + near + + " millis=" + millis); + sProxStats[pulseReason][near ? 0 : 1].append(); + } + + public static String pulseReasonToString(int pulseReason) { + switch (pulseReason) { + case PULSE_REASON_INTENT: return "intent"; + case PULSE_REASON_NOTIFICATION: return "notification"; + case PULSE_REASON_SENSOR_SIGMOTION: return "sigmotion"; + case PULSE_REASON_SENSOR_PICKUP: return "pickup"; + default: throw new IllegalArgumentException("bad reason: " + pulseReason); + } } public static void dump(PrintWriter pw) { @@ -164,8 +184,11 @@ public class DozeLog { sScreenOnPulsingStats.dump(pw, "Screen on (pulsing)"); sScreenOnNotPulsingStats.dump(pw, "Screen on (not pulsing)"); sEmergencyCallStats.dump(pw, "Emergency call"); - sProxNearStats.dump(pw, "Proximity (near)"); - sProxFarStats.dump(pw, "Proximity (far)"); + for (int i = 0; i < PULSE_REASONS; i++) { + final String reason = pulseReasonToString(i); + sProxStats[i][0].dump(pw, "Proximity near (" + reason + ")"); + sProxStats[i][1].dump(pw, "Proximity far (" + reason + ")"); + } } } @@ -188,6 +211,7 @@ public class DozeLog { } public void dump(PrintWriter pw, String type) { + if (mCount == 0) return; pw.print(" "); pw.print(type); pw.print(": n="); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index f8c5e9c..8d27450 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -56,6 +56,17 @@ public class DozeService extends DreamService { private static final String NOTIFICATION_PULSE_ACTION = ACTION_BASE + ".notification_pulse"; private static final String EXTRA_INSTANCE = "instance"; + /** + * Earliest time we pulse due to a notification light after the service started. + * + * <p>Incoming notification light events during the blackout period are + * delayed to the earliest time defined by this constant.</p> + * + * <p>This delay avoids a pulse immediately after screen off, at which + * point the notification light is re-enabled again by NoMan.</p> + */ + private static final int EARLIEST_LIGHT_PULSE_AFTER_START_MS = 10 * 1000; + private final String mTag = String.format(TAG + ".%08x", hashCode()); private final Context mContext = this; private final DozeParameters mDozeParameters = new DozeParameters(mContext); @@ -77,6 +88,7 @@ public class DozeService extends DreamService { private boolean mPowerSaveActive; private boolean mCarMode; private long mNotificationPulseTime; + private long mEarliestPulseDueToLight; private int mScheduleResetsRemaining; public DozeService() { @@ -118,9 +130,11 @@ public class DozeService extends DreamService { mSensors = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); mSigMotionSensor = new TriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION, - mDozeParameters.getPulseOnSigMotion(), mDozeParameters.getVibrateOnSigMotion()); + mDozeParameters.getPulseOnSigMotion(), mDozeParameters.getVibrateOnSigMotion(), + DozeLog.PULSE_REASON_SENSOR_SIGMOTION); mPickupSensor = new TriggerSensor(Sensor.TYPE_PICK_UP_GESTURE, - mDozeParameters.getPulseOnPickup(), mDozeParameters.getVibrateOnPickup()); + mDozeParameters.getPulseOnPickup(), mDozeParameters.getVibrateOnPickup(), + DozeLog.PULSE_REASON_SENSOR_PICKUP); mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag); mWakeLock.setReferenceCounted(true); @@ -159,8 +173,9 @@ public class DozeService extends DreamService { } mDreaming = true; - listenForPulseSignals(true); rescheduleNotificationPulse(false /*predicate*/); // cancel any pending pulse alarms + mEarliestPulseDueToLight = System.currentTimeMillis() + EARLIEST_LIGHT_PULSE_AFTER_START_MS; + listenForPulseSignals(true); // Ask the host to get things ready to start dozing. // Once ready, we call startDozing() at which point the CPU may suspend @@ -195,20 +210,37 @@ public class DozeService extends DreamService { mHost.stopDozing(); } - private void requestPulse() { + private void requestPulse(final int reason) { if (mHost != null && mDreaming && !mPulsing) { // Let the host know we want to pulse. Wait for it to be ready, then // turn the screen on. When finished, turn the screen off again. // Here we need a wakelock to stay awake until the pulse is finished. mWakeLock.acquire(); mPulsing = true; + if (!mDozeParameters.getProxCheckBeforePulse()) { + // skip proximity check + continuePulsing(reason); + return; + } final long start = SystemClock.uptimeMillis(); + final boolean nonBlocking = reason == DozeLog.PULSE_REASON_SENSOR_PICKUP + && mDozeParameters.getPickupPerformsProxCheck(); + if (nonBlocking) { + // proximity check is only done to capture statistics, continue pulsing + continuePulsing(reason); + } + // perform a proximity check new ProximityCheck() { @Override public void onProximityResult(int result) { - // avoid pulsing in pockets final boolean isNear = result == RESULT_NEAR; - DozeLog.traceProximityResult(isNear, SystemClock.uptimeMillis() - start); + final long end = SystemClock.uptimeMillis(); + DozeLog.traceProximityResult(isNear, end - start, reason); + if (nonBlocking) { + // we already continued + return; + } + // avoid pulsing in pockets if (isNear) { mPulsing = false; mWakeLock.release(); @@ -216,28 +248,32 @@ public class DozeService extends DreamService { } // not in-pocket, continue pulsing - mHost.pulseWhileDozing(new DozeHost.PulseCallback() { - @Override - public void onPulseStarted() { - if (mPulsing && mDreaming) { - turnDisplayOn(); - } - } - - @Override - public void onPulseFinished() { - if (mPulsing && mDreaming) { - mPulsing = false; - turnDisplayOff(); - } - mWakeLock.release(); // needs to be unconditional to balance acquire - } - }); + continuePulsing(reason); } }.check(); } } + private void continuePulsing(int reason) { + mHost.pulseWhileDozing(new DozeHost.PulseCallback() { + @Override + public void onPulseStarted() { + if (mPulsing && mDreaming) { + turnDisplayOn(); + } + } + + @Override + public void onPulseFinished() { + if (mPulsing && mDreaming) { + mPulsing = false; + turnDisplayOff(); + } + mWakeLock.release(); // needs to be unconditional to balance acquire + } + }, reason); + } + private void turnDisplayOff() { if (DEBUG) Log.d(mTag, "Display off"); setDozeScreenState(Display.STATE_OFF); @@ -285,6 +321,12 @@ public class DozeService extends DreamService { if (listen) { resetNotificationResets(); mHost.addCallback(mHostCallback); + + // Continue to pulse for existing LEDs. + mNotificationLightOn = mHost.isNotificationLightOn(); + if (mNotificationLightOn) { + updateNotificationPulseDueToLight(); + } } else { mHost.removeCallback(mHostCallback); } @@ -295,21 +337,27 @@ public class DozeService extends DreamService { mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets(); } - private void updateNotificationPulse() { - if (DEBUG) Log.d(mTag, "updateNotificationPulse"); + private void updateNotificationPulseDueToLight() { + long timeMs = System.currentTimeMillis(); + timeMs = Math.max(timeMs, mEarliestPulseDueToLight); + updateNotificationPulse(timeMs); + } + + private void updateNotificationPulse(long notificationTimeMs) { + if (DEBUG) Log.d(mTag, "updateNotificationPulse notificationTimeMs=" + notificationTimeMs); if (!mDozeParameters.getPulseOnNotifications()) return; if (mScheduleResetsRemaining <= 0) { if (DEBUG) Log.d(mTag, "No more schedule resets remaining"); return; } - final long now = System.currentTimeMillis(); - if ((now - mNotificationPulseTime) < mDozeParameters.getPulseDuration()) { + final long pulseDuration = mDozeParameters.getPulseDuration(false /*pickup*/); + if ((notificationTimeMs - mNotificationPulseTime) < pulseDuration) { if (DEBUG) Log.d(mTag, "Recently updated, not resetting schedule"); return; } mScheduleResetsRemaining--; if (DEBUG) Log.d(mTag, "mScheduleResetsRemaining = " + mScheduleResetsRemaining); - mNotificationPulseTime = now; + mNotificationPulseTime = notificationTimeMs; rescheduleNotificationPulse(true /*predicate*/); } @@ -370,13 +418,13 @@ public class DozeService extends DreamService { public void onReceive(Context context, Intent intent) { if (PULSE_ACTION.equals(intent.getAction())) { if (DEBUG) Log.d(mTag, "Received pulse intent"); - requestPulse(); + requestPulse(DozeLog.PULSE_REASON_INTENT); } if (NOTIFICATION_PULSE_ACTION.equals(intent.getAction())) { final long instance = intent.getLongExtra(EXTRA_INSTANCE, -1); if (DEBUG) Log.d(mTag, "Received notification pulse intent instance=" + instance); DozeLog.traceNotificationPulse(instance); - requestPulse(); + requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); rescheduleNotificationPulse(mNotificationLightOn); } if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(intent.getAction())) { @@ -391,14 +439,14 @@ public class DozeService extends DreamService { private final DozeHost.Callback mHostCallback = new DozeHost.Callback() { @Override public void onNewNotifications() { - if (DEBUG) Log.d(mTag, "onNewNotifications"); + if (DEBUG) Log.d(mTag, "onNewNotifications (noop)"); // noop for now } @Override public void onBuzzBeepBlinked() { if (DEBUG) Log.d(mTag, "onBuzzBeepBlinked"); - updateNotificationPulse(); + updateNotificationPulse(System.currentTimeMillis()); } @Override @@ -407,7 +455,7 @@ public class DozeService extends DreamService { if (mNotificationLightOn == on) return; mNotificationLightOn = on; if (mNotificationLightOn) { - updateNotificationPulse(); + updateNotificationPulseDueToLight(); } } @@ -424,15 +472,17 @@ public class DozeService extends DreamService { private final Sensor mSensor; private final boolean mConfigured; private final boolean mDebugVibrate; + private final int mPulseReason; private boolean mRequested; private boolean mRegistered; private boolean mDisabled; - public TriggerSensor(int type, boolean configured, boolean debugVibrate) { + public TriggerSensor(int type, boolean configured, boolean debugVibrate, int pulseReason) { mSensor = mSensors.getDefaultSensor(type); mConfigured = configured; mDebugVibrate = debugVibrate; + mPulseReason = pulseReason; } public void setListening(boolean listen) { @@ -449,10 +499,12 @@ public class DozeService extends DreamService { private void updateListener() { if (!mConfigured || mSensor == null) return; - if (mRequested && !mDisabled) { + if (mRequested && !mDisabled && !mRegistered) { mRegistered = mSensors.requestTriggerSensor(this, mSensor); + if (DEBUG) Log.d(mTag, "requestTriggerSensor " + mRegistered); } else if (mRegistered) { - mSensors.cancelTriggerSensor(this, mSensor); + final boolean rt = mSensors.cancelTriggerSensor(this, mSensor); + if (DEBUG) Log.d(mTag, "cancelTriggerSensor " + rt); mRegistered = false; } } @@ -482,8 +534,9 @@ public class DozeService extends DreamService { } } - requestPulse(); - setListening(true); // reregister, this sensor only fires once + requestPulse(mPulseReason); + mRegistered = false; + updateListener(); // reregister, this sensor only fires once // reset the notification pulse schedule, but only if we think we were not triggered // by a notification-related vibration diff --git a/packages/SystemUI/src/com/android/systemui/egg/LLand.java b/packages/SystemUI/src/com/android/systemui/egg/LLand.java index cdfe6e5..fa257b1 100644 --- a/packages/SystemUI/src/com/android/systemui/egg/LLand.java +++ b/packages/SystemUI/src/com/android/systemui/egg/LLand.java @@ -29,11 +29,15 @@ import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.os.Vibrator; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; +import android.util.Slog; import android.view.View; import android.view.ViewOutlineProvider; import android.view.animation.DecelerateInterpolator; @@ -51,9 +55,9 @@ public class LLand extends FrameLayout { public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); public static final boolean DEBUG_DRAW = false; // DEBUG - public static final void L(String s, Object ... objects) { + public static void L(String s, Object ... objects) { if (DEBUG) { - Log.d(TAG, String.format(s, objects)); + Slog.d(TAG, objects.length == 0 ? s : String.format(s, objects)); } } @@ -61,17 +65,18 @@ public class LLand extends FrameLayout { public static final boolean HAVE_STARS = true; public static final float DEBUG_SPEED_MULTIPLIER = 1f; // 0.1f; - public static final boolean DEBUG_IDDQD = false; + public static final boolean DEBUG_IDDQD = Log.isLoggable(TAG + ".iddqd", Log.DEBUG); final static int[] POPS = { - // resid // spinny! - R.drawable.pop_belt, 0, - R.drawable.pop_droid, 0, - R.drawable.pop_pizza, 1, - R.drawable.pop_stripes, 0, - R.drawable.pop_swirl, 1, - R.drawable.pop_vortex, 1, - R.drawable.pop_vortex2, 1, + // resid // spinny! // alpha + R.drawable.pop_belt, 0, 255, + R.drawable.pop_droid, 0, 255, + R.drawable.pop_pizza, 1, 255, + R.drawable.pop_stripes, 0, 255, + R.drawable.pop_swirl, 1, 255, + R.drawable.pop_vortex, 1, 255, + R.drawable.pop_vortex2, 1, 255, + R.drawable.pop_ball, 0, 190, }; private static class Params { @@ -117,10 +122,20 @@ public class LLand extends FrameLayout { PLAYER_Z = res.getDimensionPixelSize(R.dimen.player_z); PLAYER_Z_BOOST = res.getDimensionPixelSize(R.dimen.player_z_boost); HUD_Z = res.getDimensionPixelSize(R.dimen.hud_z); + + // Sanity checking + if (OBSTACLE_MIN <= OBSTACLE_WIDTH / 2) { + Slog.e(TAG, "error: obstacles might be too short, adjusting"); + OBSTACLE_MIN = OBSTACLE_WIDTH / 2 + 1; + } } } private TimeAnimator mAnim; + private Vibrator mVibrator; + private AudioManager mAudioManager; + private final AudioAttributes mAudioAttrs = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_GAME).build(); private TextView mScoreField; private View mSplash; @@ -158,9 +173,14 @@ public class LLand extends FrameLayout { public LLand(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); setFocusable(true); PARAMS = new Params(getResources()); mTimeOfDay = irand(0, SKIES.length); + + // we assume everything will be laid out left|top + setLayoutDirection(LAYOUT_DIRECTION_LTR); } @Override @@ -198,7 +218,15 @@ public class LLand extends FrameLayout { final float hsv[] = {0, 0, 0}; - private void reset() { + private void thump() { + if (mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT) { + // No interruptions. Not even game haptics. + return; + } + mVibrator.vibrate(80, mAudioAttrs); + } + + public void reset() { L("reset"); final Drawable sky = new GradientDrawable( GradientDrawable.Orientation.BOTTOM_TOP, @@ -313,14 +341,16 @@ public class LLand extends FrameLayout { private void setScore(int score) { mScore = score; - if (mScoreField != null) mScoreField.setText(String.valueOf(score)); + if (mScoreField != null) { + mScoreField.setText(DEBUG_IDDQD ? "??" : String.valueOf(score)); + } } private void addScore(int incr) { setScore(mScore + incr); } - private void start(boolean startPlaying) { + public void start(boolean startPlaying) { L("start(startPlaying=%s)", startPlaying?"true":"false"); if (startPlaying) { mPlaying = true; @@ -352,7 +382,7 @@ public class LLand extends FrameLayout { } } - private void stop() { + public void stop() { if (mAnimating) { mAnim.cancel(); mAnim = null; @@ -417,8 +447,10 @@ public class LLand extends FrameLayout { if (mPlaying && mDroid.below(mHeight)) { if (DEBUG_IDDQD) { poke(); + unpoke(); } else { L("player hit the floor"); + thump(); stop(); } } @@ -429,6 +461,7 @@ public class LLand extends FrameLayout { final Obstacle ob = mObstaclesInPlay.get(j); if (mPlaying && ob.intersects(mDroid) && !DEBUG_IDDQD) { L("player hit an obstacle"); + thump(); stop(); } else if (ob.cleared(mDroid)) { if (ob instanceof Stem) passedBarrier = true; @@ -459,8 +492,9 @@ public class LLand extends FrameLayout { // 3. Time for more obstacles! if (mPlaying && (t - mLastPipeTime) > PARAMS.OBSTACLE_PERIOD) { mLastPipeTime = t; - final int obstacley = (int) (Math.random() - * (mHeight - 2*PARAMS.OBSTACLE_MIN - PARAMS.OBSTACLE_GAP)) + PARAMS.OBSTACLE_MIN; + final int obstacley = + (int)(frand() * (mHeight - 2*PARAMS.OBSTACLE_MIN - PARAMS.OBSTACLE_GAP)) + + PARAMS.OBSTACLE_MIN; final int inset = (PARAMS.OBSTACLE_WIDTH - PARAMS.OBSTACLE_STEM_WIDTH) / 2; final int yinset = PARAMS.OBSTACLE_WIDTH/2; @@ -539,7 +573,7 @@ public class LLand extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent ev) { - if (DEBUG) L("touch: %s", ev); + L("touch: %s", ev); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: poke(); @@ -553,7 +587,7 @@ public class LLand extends FrameLayout { @Override public boolean onTrackballEvent(MotionEvent ev) { - if (DEBUG) L("trackball: %s", ev); + L("trackball: %s", ev); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: poke(); @@ -567,7 +601,7 @@ public class LLand extends FrameLayout { @Override public boolean onKeyDown(int keyCode, KeyEvent ev) { - if (DEBUG) L("keyDown: %d", keyCode); + L("keyDown: %d", keyCode); switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_UP: @@ -582,7 +616,7 @@ public class LLand extends FrameLayout { @Override public boolean onKeyUp(int keyCode, KeyEvent ev) { - if (DEBUG) L("keyDown: %d", keyCode); + L("keyDown: %d", keyCode); switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_UP: @@ -597,7 +631,7 @@ public class LLand extends FrameLayout { @Override public boolean onGenericMotionEvent (MotionEvent ev) { - if (DEBUG) L("generic: %s", ev); + L("generic: %s", ev); return false; } @@ -684,6 +718,10 @@ public class LLand extends FrameLayout { private boolean mBoosting; + private final int[] sColors = new int[] { + 0xFF78C557, + }; + private final float[] sHull = new float[] { 0.3f, 0f, // left antenna 0.7f, 0f, // right antenna @@ -692,7 +730,7 @@ public class LLand extends FrameLayout { 0.6f, 1f, // right foot 0.4f, 1f, // left foot BLUE! 0.08f, 0.75f, // sinistram - 0.08f, 0.33f, // cold shoulder + 0.08f, 0.33f, // cold shoulder }; public final float[] corners = new float[sHull.length]; @@ -701,7 +739,7 @@ public class LLand extends FrameLayout { setBackgroundResource(R.drawable.android); getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP); - getBackground().setTint(0xFF00FF00); + getBackground().setTint(sColors[0]); setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { @@ -822,8 +860,9 @@ public class LLand extends FrameLayout { int cx, cy, r; public Pop(Context context, float h) { super(context, h); - int idx = 2*irand(0, POPS.length/2); + int idx = 3*irand(0, POPS.length/3); setBackgroundResource(POPS[idx]); + setAlpha((float)(POPS[idx+2])/255); setScaleX(frand() < 0.5f ? -1 : 1); mRotate = POPS[idx+1] == 0 ? 0 : (frand() < 0.5f ? -1 : 1); setOutlineProvider(new ViewOutlineProvider() { diff --git a/packages/SystemUI/src/com/android/systemui/egg/LLandActivity.java b/packages/SystemUI/src/com/android/systemui/egg/LLandActivity.java index 88fd952..b9f8106 100644 --- a/packages/SystemUI/src/com/android/systemui/egg/LLandActivity.java +++ b/packages/SystemUI/src/com/android/systemui/egg/LLandActivity.java @@ -24,13 +24,21 @@ import android.widget.TextView; import com.android.systemui.R; public class LLandActivity extends Activity { + LLand mLand; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.lland); - LLand world = (LLand) findViewById(R.id.world); - world.setScoreField((TextView) findViewById(R.id.score)); - world.setSplash(findViewById(R.id.welcome)); - Log.v(LLand.TAG, "focus: " + world.requestFocus()); + mLand = (LLand) findViewById(R.id.world); + mLand.setScoreField((TextView) findViewById(R.id.score)); + mLand.setSplash(findViewById(R.id.welcome)); + //Log.v(LLand.TAG, "focus: " + mLand.requestFocus()); + } + + @Override + public void onPause() { + mLand.stop(); + super.onPause(); } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index ee699d2..73fa2ed 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -22,13 +22,13 @@ import android.os.Binder; import android.os.Bundle; import android.os.Debug; import android.os.IBinder; +import android.os.Process; import android.util.Log; -import android.view.MotionEvent; import com.android.internal.policy.IKeyguardExitCallback; import com.android.internal.policy.IKeyguardService; -import com.android.internal.policy.IKeyguardServiceConstants; import com.android.internal.policy.IKeyguardShowCallback; +import com.android.internal.policy.IKeyguardStateCallback; import com.android.systemui.SystemUIApplication; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -52,6 +52,10 @@ public class KeyguardService extends Service { } void checkPermission() { + // Avoid deadlock by avoiding calling back into the system process. + if (Binder.getCallingUid() == Process.SYSTEM_UID) return; + + // Otherwise,explicitly check for caller permission ... if (getBaseContext().checkCallingOrSelfPermission(PERMISSION) != PERMISSION_GRANTED) { Log.w(TAG, "Caller needs permission '" + PERMISSION + "' to call " + Debug.getCaller()); throw new SecurityException("Access denied to process: " + Binder.getCallingPid() @@ -61,143 +65,85 @@ public class KeyguardService extends Service { private final IKeyguardService.Stub mBinder = new IKeyguardService.Stub() { - private boolean mIsOccluded; - - @Override - public boolean isShowing() { - return mKeyguardViewMediator.isShowing(); - } - - @Override - public boolean isSecure() { - return mKeyguardViewMediator.isSecure(); - } - - @Override - public boolean isShowingAndNotOccluded() { - return mKeyguardViewMediator.isShowingAndNotOccluded(); - } - - @Override - public boolean isInputRestricted() { - return mKeyguardViewMediator.isInputRestricted(); + @Override // Binder interface + public void addStateMonitorCallback(IKeyguardStateCallback callback) { + checkPermission(); + mKeyguardViewMediator.addStateMonitorCallback(callback); } - @Override + @Override // Binder interface public void verifyUnlock(IKeyguardExitCallback callback) { checkPermission(); mKeyguardViewMediator.verifyUnlock(callback); } - @Override + @Override // Binder interface public void keyguardDone(boolean authenticated, boolean wakeup) { checkPermission(); mKeyguardViewMediator.keyguardDone(authenticated, wakeup); } - @Override - public int setOccluded(boolean isOccluded) { - checkPermission(); - synchronized (this) { - int result; - if (isOccluded && mKeyguardViewMediator.isShowing() - && !mIsOccluded) { - result = IKeyguardServiceConstants - .KEYGUARD_SERVICE_SET_OCCLUDED_RESULT_UNSET_FLAGS; - } else if (!isOccluded && mKeyguardViewMediator.isShowing() - && mIsOccluded) { - result = IKeyguardServiceConstants - .KEYGUARD_SERVICE_SET_OCCLUDED_RESULT_SET_FLAGS; - } else { - result = IKeyguardServiceConstants.KEYGUARD_SERVICE_SET_OCCLUDED_RESULT_NONE; - } - if (mIsOccluded != isOccluded) { - mKeyguardViewMediator.setOccluded(isOccluded); - - // Cache the value so we always have a fresh view in whether Keyguard is occluded. - // If we would just call mKeyguardViewMediator.isOccluded(), this might be stale - // because that value gets updated in another thread. - mIsOccluded = isOccluded; - } - return result; - } + @Override // Binder interface + public void setOccluded(boolean isOccluded) { + checkPermission(); + mKeyguardViewMediator.setOccluded(isOccluded); } - @Override + @Override // Binder interface public void dismiss() { checkPermission(); mKeyguardViewMediator.dismiss(); } - @Override + @Override // Binder interface public void onDreamingStarted() { checkPermission(); mKeyguardViewMediator.onDreamingStarted(); } - @Override + @Override // Binder interface public void onDreamingStopped() { checkPermission(); mKeyguardViewMediator.onDreamingStopped(); } - @Override + @Override // Binder interface public void onScreenTurnedOff(int reason) { checkPermission(); mKeyguardViewMediator.onScreenTurnedOff(reason); } - @Override + @Override // Binder interface public void onScreenTurnedOn(IKeyguardShowCallback callback) { checkPermission(); mKeyguardViewMediator.onScreenTurnedOn(callback); } - @Override + @Override // Binder interface public void setKeyguardEnabled(boolean enabled) { checkPermission(); mKeyguardViewMediator.setKeyguardEnabled(enabled); } - @Override - public boolean isDismissable() { - return mKeyguardViewMediator.isDismissable(); - } - - @Override + @Override // Binder interface public void onSystemReady() { checkPermission(); mKeyguardViewMediator.onSystemReady(); } - @Override + @Override // Binder interface public void doKeyguardTimeout(Bundle options) { checkPermission(); mKeyguardViewMediator.doKeyguardTimeout(options); } - @Override + @Override // Binder interface public void setCurrentUser(int userId) { checkPermission(); mKeyguardViewMediator.setCurrentUser(userId); } @Override - public void showAssistant() { - checkPermission(); - } - - @Override - public void dispatch(MotionEvent event) { - checkPermission(); - } - - @Override - public void launchCamera() { - checkPermission(); - } - - @Override public void onBootCompleted() { checkPermission(); mKeyguardViewMediator.onBootCompleted(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 4af8499..e66934e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -23,6 +23,7 @@ import android.app.AlarmManager; import android.app.PendingIntent; import android.app.SearchManager; import android.app.StatusBarManager; +import android.app.trust.TrustManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -42,6 +43,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.util.EventLog; import android.util.Log; @@ -54,8 +56,10 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import com.android.internal.policy.IKeyguardExitCallback; import com.android.internal.policy.IKeyguardShowCallback; +import com.android.internal.policy.IKeyguardStateCallback; import com.android.internal.telephony.IccCardConstants; import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardConstants; import com.android.keyguard.KeyguardDisplayManager; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; @@ -67,6 +71,9 @@ import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.StatusBarWindowManager; +import java.util.ArrayList; +import java.util.List; + import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; @@ -113,7 +120,10 @@ import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; */ public class KeyguardViewMediator extends SystemUI { private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000; - final static boolean DEBUG = false; + private static final long KEYGUARD_DONE_PENDING_TIMEOUT_MS = 3000; + + private static final boolean DEBUG = KeyguardConstants.DEBUG; + private static final boolean DEBUG_SIM_STATES = KeyguardConstants.DEBUG_SIM_STATES; private final static boolean DBG_WAKE = false; private final static String TAG = "KeyguardViewMediator"; @@ -136,6 +146,7 @@ public class KeyguardViewMediator extends SystemUI { private static final int DISMISS = 17; private static final int START_KEYGUARD_EXIT_ANIM = 18; private static final int ON_ACTIVITY_DRAWN = 19; + private static final int KEYGUARD_DONE_PENDING_TIMEOUT = 20; /** * The default amount of time we stay awake (used for all key input) @@ -184,8 +195,9 @@ public class KeyguardViewMediator extends SystemUI { /** High level access to the window manager for dismissing keyguard animation */ private IWindowManager mWM; - /** UserManager for querying number of users */ - private UserManager mUserManager; + + /** TrustManager for letting it know when we change visibility */ + private TrustManager mTrustManager; /** SearchManager for determining whether or not search assistant is available */ private SearchManager mSearchManager; @@ -216,6 +228,9 @@ public class KeyguardViewMediator extends SystemUI { // answer whether the input should be restricted) private boolean mShowing; + /** Cached value of #isInputRestricted */ + private boolean mInputRestricted; + // true if the keyguard is hidden by another window private boolean mOccluded = false; @@ -285,6 +300,8 @@ public class KeyguardViewMediator extends SystemUI { */ private KeyguardDisplayManager mKeyguardDisplayManager; + private final ArrayList<IKeyguardStateCallback> mKeyguardStateCallbacks = new ArrayList<>(); + KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { @Override @@ -294,7 +311,7 @@ public class KeyguardViewMediator extends SystemUI { // ActivityManagerService) will not reconstruct the keyguard if it is already showing. synchronized (KeyguardViewMediator.this) { mSwitchingUser = true; - mKeyguardDonePending = false; + resetKeyguardDonePendingLocked(); resetStateLocked(); adjustStatusBarLocked(); // When we switch users we want to bring the new user to the biometric unlock even @@ -352,11 +369,26 @@ public class KeyguardViewMediator extends SystemUI { @Override public void onDeviceProvisioned() { sendUserPresentBroadcast(); + updateInputRestricted(); } @Override - public void onSimStateChanged(IccCardConstants.State simState) { - if (DEBUG) Log.d(TAG, "onSimStateChanged: " + simState); + public void onSimStateChanged(int subId, int slotId, IccCardConstants.State simState) { + + if (DEBUG_SIM_STATES) { + Log.d(TAG, "onSimStateChanged(subId=" + subId + ", slotId=" + slotId + + ",state=" + simState + ")"); + } + + try { + int size = mKeyguardStateCallbacks.size(); + boolean simPinSecure = mUpdateMonitor.isSimPinSecure(); + for (int i = 0; i < size; i++) { + mKeyguardStateCallbacks.get(i).onSimSecureStateChanged(simPinSecure); + } + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onSimSecureStateChanged", e); + } switch (simState) { case NOT_READY: @@ -364,9 +396,9 @@ public class KeyguardViewMediator extends SystemUI { // only force lock screen in case of missing sim if user hasn't // gone through setup wizard synchronized (this) { - if (!mUpdateMonitor.isDeviceProvisioned()) { - if (!isShowing()) { - if (DEBUG) Log.d(TAG, "ICC_ABSENT isn't showing," + if (shouldWaitForProvisioning()) { + if (!mShowing) { + if (DEBUG_SIM_STATES) Log.d(TAG, "ICC_ABSENT isn't showing," + " we need to show the keyguard since the " + "device isn't provisioned yet."); doKeyguardLocked(null); @@ -379,8 +411,9 @@ public class KeyguardViewMediator extends SystemUI { case PIN_REQUIRED: case PUK_REQUIRED: synchronized (this) { - if (!isShowing()) { - if (DEBUG) Log.d(TAG, "INTENT_VALUE_ICC_LOCKED and keygaurd isn't " + if (!mShowing) { + if (DEBUG_SIM_STATES) Log.d(TAG, + "INTENT_VALUE_ICC_LOCKED and keygaurd isn't " + "showing; need to show keyguard so user can enter sim pin"); doKeyguardLocked(null); } else { @@ -390,12 +423,12 @@ public class KeyguardViewMediator extends SystemUI { break; case PERM_DISABLED: synchronized (this) { - if (!isShowing()) { - if (DEBUG) Log.d(TAG, "PERM_DISABLED and " + if (!mShowing) { + if (DEBUG_SIM_STATES) Log.d(TAG, "PERM_DISABLED and " + "keygaurd isn't showing."); doKeyguardLocked(null); } else { - if (DEBUG) Log.d(TAG, "PERM_DISABLED, resetStateLocked to" + if (DEBUG_SIM_STATES) Log.d(TAG, "PERM_DISABLED, resetStateLocked to" + "show permanently disabled message in lockscreen."); resetStateLocked(); } @@ -403,11 +436,14 @@ public class KeyguardViewMediator extends SystemUI { break; case READY: synchronized (this) { - if (isShowing()) { + if (mShowing) { resetStateLocked(); } } break; + default: + if (DEBUG_SIM_STATES) Log.v(TAG, "Ignoring state: " + simState); + break; } } @@ -426,7 +462,9 @@ public class KeyguardViewMediator extends SystemUI { } public void keyguardDone(boolean authenticated) { - KeyguardViewMediator.this.keyguardDone(authenticated, true); + if (!mKeyguardDonePending) { + KeyguardViewMediator.this.keyguardDone(authenticated, true); + } } public void keyguardDoneDrawing() { @@ -448,6 +486,8 @@ public class KeyguardViewMediator extends SystemUI { mKeyguardDonePending = true; mHideAnimationRun = true; mStatusBarKeyguardViewManager.startPreHideAnimation(null /* finishRunnable */); + mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_PENDING_TIMEOUT, + KEYGUARD_DONE_PENDING_TIMEOUT_MS); } @Override @@ -468,16 +508,22 @@ public class KeyguardViewMediator extends SystemUI { public void playTrustedSound() { KeyguardViewMediator.this.playTrustedSound(); } + + @Override + public boolean isInputRestricted() { + return KeyguardViewMediator.this.isInputRestricted(); + } }; public void userActivity() { mPM.userActivity(SystemClock.uptimeMillis(), false); } - private void setup() { + private void setupLocked() { mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mWM = WindowManagerGlobal.getWindowManagerService(); - mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mTrustManager = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE); + mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard"); mShowKeyguardWakeLock.setReferenceCounted(false); @@ -493,8 +539,8 @@ public class KeyguardViewMediator extends SystemUI { mLockPatternUtils.setCurrentUser(ActivityManager.getCurrentUser()); // Assume keyguard is showing (unless it's disabled) until we know for sure... - mShowing = (mUpdateMonitor.isDeviceProvisioned() || mLockPatternUtils.isSecure()) - && !mLockPatternUtils.isLockScreenDisabled(); + setShowingLocked(!shouldWaitForProvisioning() && !mLockPatternUtils.isLockScreenDisabled()); + mTrustManager.reportKeyguardShowingChanged(); mStatusBarKeyguardViewManager = new StatusBarKeyguardViewManager(mContext, mViewMediatorCallback, mLockPatternUtils); @@ -535,7 +581,9 @@ public class KeyguardViewMediator extends SystemUI { @Override public void start() { - setup(); + synchronized (this) { + setupLocked(); + } putComponent(KeyguardViewMediator.class, this); } @@ -583,7 +631,7 @@ public class KeyguardViewMediator extends SystemUI { mScreenOn = false; if (DEBUG) Log.d(TAG, "onScreenTurnedOff(" + why + ")"); - mKeyguardDonePending = false; + resetKeyguardDonePendingLocked(); mHideAnimationRun = false; // Lock immediately based on setting if secure (user has a pin/pattern/password). @@ -739,12 +787,14 @@ public class KeyguardViewMediator extends SystemUI { if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, " + "disabling status bar expansion"); mNeedToReshowWhenReenabled = true; + updateInputRestrictedLocked(); hideLocked(); } else if (enabled && mNeedToReshowWhenReenabled) { // reenabled after previously hidden, reshow if (DEBUG) Log.d(TAG, "previously hidden, reshowing, reenabling " + "status bar expansion"); mNeedToReshowWhenReenabled = false; + updateInputRestrictedLocked(); if (mExitSecureCallback != null) { if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting"); @@ -783,7 +833,7 @@ public class KeyguardViewMediator extends SystemUI { public void verifyUnlock(IKeyguardExitCallback callback) { synchronized (this) { if (DEBUG) Log.d(TAG, "verifyUnlock"); - if (!mUpdateMonitor.isDeviceProvisioned()) { + if (shouldWaitForProvisioning()) { // don't allow this api when the device isn't provisioned if (DEBUG) Log.d(TAG, "ignoring because device isn't provisioned"); try { @@ -816,17 +866,6 @@ public class KeyguardViewMediator extends SystemUI { } /** - * Is the keyguard currently showing? - */ - public boolean isShowing() { - return mShowing; - } - - public boolean isOccluded() { - return mOccluded; - } - - /** * Is the keyguard currently showing and not being force hidden? */ public boolean isShowingAndNotOccluded() { @@ -873,7 +912,27 @@ public class KeyguardViewMediator extends SystemUI { * was suppressed by an app that disabled the keyguard or we haven't been provisioned yet. */ public boolean isInputRestricted() { - return mShowing || mNeedToReshowWhenReenabled || !mUpdateMonitor.isDeviceProvisioned(); + return mShowing || mNeedToReshowWhenReenabled || shouldWaitForProvisioning(); + } + + private void updateInputRestricted() { + synchronized (this) { + updateInputRestrictedLocked(); + } + } + private void updateInputRestrictedLocked() { + boolean inputRestricted = isInputRestricted(); + if (mInputRestricted != inputRestricted) { + mInputRestricted = inputRestricted; + try { + int size = mKeyguardStateCallbacks.size(); + for (int i = 0; i < size; i++) { + mKeyguardStateCallbacks.get(i).onInputRestrictedStateChanged(inputRestricted); + } + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onDeviceProvisioned", e); + } + } } /** @@ -899,20 +958,20 @@ public class KeyguardViewMediator extends SystemUI { // if the keyguard is already showing, don't bother if (mStatusBarKeyguardViewManager.isShowing()) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); + resetStateLocked(); return; } // if the setup wizard hasn't run yet, don't show - final boolean requireSim = !SystemProperties.getBoolean("keyguard.no_require_sim", - false); - final boolean provisioned = mUpdateMonitor.isDeviceProvisioned(); - final IccCardConstants.State state = mUpdateMonitor.getSimState(); - final boolean lockedOrMissing = state.isPinLocked() - || ((state == IccCardConstants.State.ABSENT - || state == IccCardConstants.State.PERM_DISABLED) - && requireSim); - - if (!lockedOrMissing && !provisioned) { + final boolean requireSim = !SystemProperties.getBoolean("keyguard.no_require_sim", false); + final boolean absent = SubscriptionManager.isValidSubscriptionId( + mUpdateMonitor.getNextSubIdForState(IccCardConstants.State.ABSENT)); + final boolean disabled = SubscriptionManager.isValidSubscriptionId( + mUpdateMonitor.getNextSubIdForState(IccCardConstants.State.PERM_DISABLED)); + final boolean lockedOrMissing = mUpdateMonitor.isSimPinSecure() + || ((absent || disabled) && requireSim); + + if (!lockedOrMissing && shouldWaitForProvisioning()) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because device isn't provisioned" + " and the sim is not locked or missing"); return; @@ -926,7 +985,7 @@ public class KeyguardViewMediator extends SystemUI { if (mLockPatternUtils.checkVoldPassword()) { if (DEBUG) Log.d(TAG, "Not showing lock screen since just decrypted"); // Without this, settings is not enabled until the lock screen first appears - mShowing = false; + setShowingLocked(false); hideLocked(); return; } @@ -935,6 +994,10 @@ public class KeyguardViewMediator extends SystemUI { showLocked(options); } + private boolean shouldWaitForProvisioning() { + return !mUpdateMonitor.isDeviceProvisioned() && !isSecure(); + } + /** * Dismiss the keyguard through the security layers. */ @@ -1047,9 +1110,6 @@ public class KeyguardViewMediator extends SystemUI { public void keyguardDone(boolean authenticated, boolean wakeup) { if (DEBUG) Log.d(TAG, "keyguardDone(" + authenticated + ")"); EventLog.writeEvent(70000, 2); - synchronized (this) { - mKeyguardDonePending = false; - } Message msg = mHandler.obtainMessage(KEYGUARD_DONE, authenticated ? 1 : 0, wakeup ? 1 : 0); mHandler.sendMessage(msg); } @@ -1107,6 +1167,9 @@ public class KeyguardViewMediator extends SystemUI { StartKeyguardExitAnimParams params = (StartKeyguardExitAnimParams) msg.obj; handleStartKeyguardExitAnimation(params.startTime, params.fadeoutDuration); break; + case KEYGUARD_DONE_PENDING_TIMEOUT: + Log.w(TAG, "Timeout while waiting for activity drawn!"); + // Fall through. case ON_ACTIVITY_DRAWN: handleOnActivityDrawn(); break; @@ -1120,6 +1183,9 @@ public class KeyguardViewMediator extends SystemUI { */ private void handleKeyguardDone(boolean authenticated, boolean wakeup) { if (DEBUG) Log.d(TAG, "handleKeyguardDone"); + synchronized (this) { + resetKeyguardDonePendingLocked(); + } if (authenticated) { mUpdateMonitor.clearFailedUnlockAttempts(); @@ -1140,6 +1206,7 @@ public class KeyguardViewMediator extends SystemUI { // the keyguard when they've released the lock mExternallyEnabled = true; mNeedToReshowWhenReenabled = false; + updateInputRestricted(); } } @@ -1150,7 +1217,12 @@ public class KeyguardViewMediator extends SystemUI { synchronized (this) { if (mBootCompleted) { final UserHandle currentUser = new UserHandle(mLockPatternUtils.getCurrentUser()); - mContext.sendBroadcastAsUser(USER_PRESENT_INTENT, currentUser); + final UserManager um = (UserManager) mContext.getSystemService( + Context.USER_SERVICE); + List <UserInfo> userHandles = um.getProfiles(currentUser.getIdentifier()); + for (UserInfo ui : userHandles) { + mContext.sendBroadcastAsUser(USER_PRESENT_INTENT, ui.getUserHandle()); + } } else { mBootSendUserPresent = true; } @@ -1235,10 +1307,10 @@ public class KeyguardViewMediator extends SystemUI { if (DEBUG) Log.d(TAG, "handleShow"); } + setShowingLocked(true); mStatusBarKeyguardViewManager.show(options); mHiding = false; - mShowing = true; - mKeyguardDonePending = false; + resetKeyguardDonePendingLocked(); mHideAnimationRun = false; updateActivityLockScreenState(); adjustStatusBarLocked(); @@ -1295,6 +1367,7 @@ public class KeyguardViewMediator extends SystemUI { } private void handleOnActivityDrawn() { + if (DEBUG) Log.d(TAG, "handleOnActivityDrawn: mKeyguardDonePending=" + mKeyguardDonePending); if (mKeyguardDonePending) { mStatusBarKeyguardViewManager.onActivityDrawn(); } @@ -1314,9 +1387,9 @@ public class KeyguardViewMediator extends SystemUI { playSounds(false); } + setShowingLocked(false); mStatusBarKeyguardViewManager.hide(startTime, fadeoutDuration); - mShowing = false; - mKeyguardDonePending = false; + resetKeyguardDonePendingLocked(); mHideAnimationRun = false; updateActivityLockScreenState(); adjustStatusBarLocked(); @@ -1375,8 +1448,8 @@ public class KeyguardViewMediator extends SystemUI { private void handleVerifyUnlock() { synchronized (KeyguardViewMediator.this) { if (DEBUG) Log.d(TAG, "handleVerifyUnlock"); + setShowingLocked(true); mStatusBarKeyguardViewManager.verifyUnlock(); - mShowing = true; updateActivityLockScreenState(); } } @@ -1403,13 +1476,9 @@ public class KeyguardViewMediator extends SystemUI { } } - public boolean isDismissable() { - return mKeyguardDonePending || !isSecure(); - } - - private boolean isAssistantAvailable() { - return mSearchManager != null - && mSearchManager.getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null; + private void resetKeyguardDonePendingLocked() { + mKeyguardDonePending = false; + mHandler.removeMessages(KEYGUARD_DONE_PENDING_TIMEOUT); } public void onBootCompleted() { @@ -1453,4 +1522,32 @@ public class KeyguardViewMediator extends SystemUI { this.fadeoutDuration = fadeoutDuration; } } + + private void setShowingLocked(boolean showing) { + if (showing != mShowing) { + mShowing = showing; + try { + int size = mKeyguardStateCallbacks.size(); + for (int i = 0; i < size; i++) { + mKeyguardStateCallbacks.get(i).onShowingStateChanged(showing); + } + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onShowingStateChanged", e); + } + updateInputRestrictedLocked(); + mTrustManager.reportKeyguardShowingChanged(); + } + } + + public void addStateMonitorCallback(IKeyguardStateCallback callback) { + synchronized (this) { + mKeyguardStateCallbacks.add(callback); + try { + callback.onSimSecureStateChanged(mUpdateMonitor.isSimPinSecure()); + callback.onShowingStateChanged(mShowing); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onShowingStateChanged or onSimSecureStateChanged", e); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java index b441eaa..c23f45d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java @@ -16,6 +16,7 @@ package com.android.systemui.media; +import android.app.Activity; import android.app.AlertDialog; import android.app.PendingIntent; import android.content.Context; @@ -32,6 +33,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; import android.view.LayoutInflater; +import android.view.WindowManager; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.TextView; @@ -39,9 +41,11 @@ import android.widget.TextView; import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; import com.android.systemui.R; +import com.android.systemui.statusbar.phone.SystemUIDialog; -public class MediaProjectionPermissionActivity extends AlertActivity - implements DialogInterface.OnClickListener, CheckBox.OnCheckedChangeListener { +public class MediaProjectionPermissionActivity extends Activity + implements DialogInterface.OnClickListener, CheckBox.OnCheckedChangeListener, + DialogInterface.OnCancelListener { private static final String TAG = "MediaProjectionPermissionActivity"; private boolean mPermanentGrant; @@ -49,11 +53,12 @@ public class MediaProjectionPermissionActivity extends AlertActivity private int mUid; private IMediaProjectionManager mService; + private AlertDialog mDialog; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - Intent intent = getIntent(); mPackageName = getCallingPackage(); IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); mService = IMediaProjectionManager.Stub.asInterface(b); @@ -89,22 +94,29 @@ public class MediaProjectionPermissionActivity extends AlertActivity String appName = aInfo.loadLabel(packageManager).toString(); - final AlertController.AlertParams ap = mAlertParams; - ap.mIcon = aInfo.loadIcon(packageManager); - ap.mMessage = getString(R.string.media_projection_dialog_text, appName); - ap.mPositiveButtonText = getString(R.string.media_projection_action_text); - ap.mNegativeButtonText = getString(android.R.string.cancel); - ap.mPositiveButtonListener = this; - ap.mNegativeButtonListener = this; - - // add "always use" checkbox - LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); - ap.mView = inflater.inflate(R.layout.remember_permission_checkbox, null); - CheckBox rememberPermissionCheckbox = - (CheckBox)ap.mView.findViewById(R.id.remember); - rememberPermissionCheckbox.setOnCheckedChangeListener(this); - - setupAlert(); + mDialog = new AlertDialog.Builder(this) + .setIcon(aInfo.loadIcon(packageManager)) + .setMessage(getString(R.string.media_projection_dialog_text, appName)) + .setPositiveButton(R.string.media_projection_action_text, this) + .setNegativeButton(android.R.string.cancel, this) + .setView(R.layout.remember_permission_checkbox) + .setOnCancelListener(this) + .create(); + + mDialog.create(); + + ((CheckBox) mDialog.findViewById(R.id.remember)).setOnCheckedChangeListener(this); + mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + + mDialog.show(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mDialog != null) { + mDialog.dismiss(); + } } @Override @@ -118,6 +130,9 @@ public class MediaProjectionPermissionActivity extends AlertActivity Log.e(TAG, "Error granting projection permission", e); setResult(RESULT_CANCELED); } finally { + if (mDialog != null) { + mDialog.dismiss(); + } finish(); } } @@ -135,4 +150,9 @@ public class MediaProjectionPermissionActivity extends AlertActivity intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder()); return intent; } + + @Override + public void onCancel(DialogInterface dialog) { + finish(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index f184ad2..4391bfc 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.phone.SystemUIDialog; import java.io.PrintWriter; +import java.text.NumberFormat; public class PowerNotificationWarnings implements PowerUI.WarningsUI { private static final String TAG = PowerUI.TAG + ".Notification"; @@ -65,6 +66,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private static final String ACTION_SHOW_BATTERY_SETTINGS = "PNW.batterySettings"; private static final String ACTION_START_SAVER = "PNW.startSaver"; private static final String ACTION_STOP_SAVER = "PNW.stopSaver"; + private static final String ACTION_DISMISSED_WARNING = "PNW.dismissedWarning"; private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) @@ -143,7 +145,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { showSaverNotification(); mShowing = SHOWING_SAVER; } else { - mNoMan.cancel(TAG_NOTIFICATION, ID_NOTIFICATION); + mNoMan.cancelAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, UserHandle.ALL); mShowing = SHOWING_NOTHING; } } @@ -157,7 +159,6 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { .setContentTitle(mContext.getString(R.string.invalid_charger_title)) .setContentText(mContext.getString(R.string.invalid_charger_text)) .setPriority(Notification.PRIORITY_MAX) - .setCategory(Notification.CATEGORY_SYSTEM) .setVisibility(Notification.VISIBILITY_PUBLIC) .setColor(mContext.getResources().getColor( com.android.internal.R.color.system_notification_accent_color)); @@ -165,22 +166,23 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { if (n.headsUpContentView != null) { n.headsUpContentView.setViewVisibility(com.android.internal.R.id.right_icon, View.GONE); } - mNoMan.notifyAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, n, UserHandle.CURRENT); + mNoMan.notifyAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, n, UserHandle.ALL); } private void showWarningNotification() { final int textRes = mSaver ? R.string.battery_low_percent_format_saver_started : R.string.battery_low_percent_format; + final String percentage = NumberFormat.getPercentInstance().format((double) mBatteryLevel / 100.0); final Notification.Builder nb = new Notification.Builder(mContext) .setSmallIcon(R.drawable.ic_power_low) // Bump the notification when the bucket dropped. .setWhen(mBucketDroppedNegativeTimeMs) .setShowWhen(false) .setContentTitle(mContext.getString(R.string.battery_low_title)) - .setContentText(mContext.getString(textRes, mBatteryLevel)) + .setContentText(mContext.getString(textRes, percentage)) .setOnlyAlertOnce(true) + .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING)) .setPriority(Notification.PRIORITY_MAX) - .setCategory(Notification.CATEGORY_SYSTEM) .setVisibility(Notification.VISIBILITY_PUBLIC) .setColor(mContext.getResources().getColor( com.android.internal.R.color.battery_saver_mode_color)); @@ -202,7 +204,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { if (n.headsUpContentView != null) { n.headsUpContentView.setViewVisibility(com.android.internal.R.id.right_icon, View.GONE); } - mNoMan.notifyAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, n, UserHandle.CURRENT); + mNoMan.notifyAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, n, UserHandle.ALL); } private void showSaverNotification() { @@ -212,7 +214,6 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { .setContentText(mContext.getString(R.string.battery_saver_notification_text)) .setOngoing(true) .setShowWhen(false) - .setCategory(Notification.CATEGORY_SYSTEM) .setVisibility(Notification.VISIBILITY_PUBLIC) .setColor(mContext.getResources().getColor( com.android.internal.R.color.battery_saver_mode_color)); @@ -220,7 +221,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { if (hasSaverSettings()) { nb.setContentIntent(pendingActivity(mOpenSaverSettings)); } - mNoMan.notifyAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, nb.build(), UserHandle.CURRENT); + mNoMan.notifyAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, nb.build(), UserHandle.ALL); } private void addStopSaverAction(Notification.Builder nb) { @@ -341,6 +342,11 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { updateNotification(); } + @Override + public void userSwitched() { + updateNotification(); + } + private void showStartSaverConfirmation() { if (mSaverConfirmation != null) return; final SystemUIDialog d = new SystemUIDialog(mContext); @@ -370,7 +376,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { filter.addAction(ACTION_SHOW_BATTERY_SETTINGS); filter.addAction(ACTION_START_SAVER); filter.addAction(ACTION_STOP_SAVER); - mContext.registerReceiver(this, filter, null, mHandler); + filter.addAction(ACTION_DISMISSED_WARNING); + mContext.registerReceiverAsUser(this, UserHandle.ALL, filter, null, mHandler); } @Override @@ -387,6 +394,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { dismissSaverNotification(); dismissLowBatteryNotification(); setSaverMode(false); + } else if (action.equals(ACTION_DISMISSED_WARNING)) { + dismissLowBatteryWarning(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index d3c7dee..9459740 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -137,6 +137,7 @@ public class PowerUI extends SystemUI { filter.addAction(Intent.ACTION_BATTERY_CHANGED); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING); filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); mContext.registerReceiver(this, filter, null, mHandler); @@ -207,6 +208,8 @@ public class PowerUI extends SystemUI { mScreenOffTime = SystemClock.elapsedRealtime(); } else if (Intent.ACTION_SCREEN_ON.equals(action)) { mScreenOffTime = -1; + } else if (Intent.ACTION_USER_SWITCHED.equals(action)) { + mWarnings.userSwitched(); } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(action)) { updateSaverMode(); } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGING.equals(action)) { @@ -256,6 +259,7 @@ public class PowerUI extends SystemUI { void updateLowBatteryWarning(); boolean isInvalidChargerWarningShowing(); void dump(PrintWriter pw); + void userSwitched(); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainer.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainer.java new file mode 100644 index 0000000..cfe8d07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainer.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 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.qs; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import com.android.systemui.R; + +/** + * Wrapper view with background which contains {@link QSPanel} + */ +public class QSContainer extends FrameLayout { + + private int mHeightOverride = -1; + private QSPanel mQSPanel; + + public QSContainer(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mQSPanel = (QSPanel) findViewById(R.id.quick_settings_panel); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateBottom(); + } + + /** + * Overrides the height of this view (post-layout), so that the content is clipped to that + * height and the background is set to that height. + * + * @param heightOverride the overridden height + */ + public void setHeightOverride(int heightOverride) { + mHeightOverride = heightOverride; + updateBottom(); + } + + /** + * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that + * during closing the detail panel, this already returns the smaller height. + */ + public int getDesiredHeight() { + if (mQSPanel.isClosingDetail()) { + return mQSPanel.getGridHeight() + getPaddingTop() + getPaddingBottom(); + } else { + return getMeasuredHeight(); + } + } + + private void updateBottom() { + int height = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight(); + setBottom(getTop() + height); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java index eb4560d..111484b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java @@ -55,7 +55,6 @@ public class QSDetailClipper { if (listener != null) { mAnimator.addListener(listener); } - mDetail.setLayerType(View.LAYER_TYPE_HARDWARE, null); if (in) { mBackground.startTransition((int)(mAnimator.getDuration() * 0.6)); mAnimator.addListener(mVisibleOnStart); @@ -82,7 +81,6 @@ public class QSDetailClipper { } public void onAnimationEnd(Animator animation) { - mDetail.setLayerType(View.LAYER_TYPE_NONE, null); mAnimator = null; } }; @@ -90,7 +88,6 @@ public class QSDetailClipper { private final AnimatorListenerAdapter mGoneOnEnd = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mDetail.setLayerType(View.LAYER_TYPE_NONE, null); mDetail.setVisibility(View.GONE); mBackground.resetTransition(); mAnimator = null; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java index ce0d5f4..95ac558 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java @@ -18,13 +18,13 @@ package com.android.systemui.qs; import android.content.Context; import android.content.res.Configuration; +import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -51,8 +51,10 @@ public class QSDetailItems extends FrameLayout { private boolean mItemsVisible = true; private LinearLayout mItems; private View mEmpty; + private View mMinHeightSpacer; private TextView mEmptyText; private ImageView mEmptyIcon; + private int mMaxItems; public QSDetailItems(Context context, AttributeSet attrs) { super(context, attrs); @@ -77,6 +79,12 @@ public class QSDetailItems extends FrameLayout { mEmpty.setVisibility(GONE); mEmptyText = (TextView) mEmpty.findViewById(android.R.id.title); mEmptyIcon = (ImageView) mEmpty.findViewById(android.R.id.icon); + mMinHeightSpacer = findViewById(R.id.min_height_spacer); + + // By default, a detail item view has fixed size. + mMaxItems = getResources().getInteger( + R.integer.quick_settings_detail_max_item_count); + setMinHeightInItems(mMaxItems); } @Override @@ -102,6 +110,16 @@ public class QSDetailItems extends FrameLayout { mEmptyText.setText(text); } + /** + * Set the minimum height of this detail view, in item count. + */ + public void setMinHeightInItems(int minHeightInItems) { + ViewGroup.LayoutParams lp = mMinHeightSpacer.getLayoutParams(); + lp.height = minHeightInItems * getResources().getDimensionPixelSize( + R.dimen.qs_detail_item_height); + mMinHeightSpacer.setLayoutParams(lp); + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -135,7 +153,7 @@ public class QSDetailItems extends FrameLayout { } private void handleSetItems(Item[] items) { - final int itemCount = items != null ? items.length : 0; + final int itemCount = items != null ? Math.min(items.length, mMaxItems) : 0; mEmpty.setVisibility(itemCount == 0 ? VISIBLE : GONE); mItems.setVisibility(itemCount == 0 ? GONE : VISIBLE); for (int i = mItems.getChildCount() - 1; i >= itemCount; i--) { @@ -162,10 +180,17 @@ public class QSDetailItems extends FrameLayout { view.setVisibility(mItemsVisible ? VISIBLE : INVISIBLE); final ImageView iv = (ImageView) view.findViewById(android.R.id.icon); iv.setImageResource(item.icon); + iv.getOverlay().clear(); + if (item.overlay != null) { + item.overlay.setBounds(0, 0, item.overlay.getIntrinsicWidth(), + item.overlay.getIntrinsicHeight()); + iv.getOverlay().add(item.overlay); + } final TextView title = (TextView) view.findViewById(android.R.id.title); title.setText(item.line1); final TextView summary = (TextView) view.findViewById(android.R.id.summary); final boolean twoLines = !TextUtils.isEmpty(item.line2); + title.setMaxLines(twoLines ? 1 : 2); summary.setVisibility(twoLines ? VISIBLE : GONE); summary.setText(twoLines ? item.line2 : null); view.setMinimumHeight(mContext.getResources() .getDimensionPixelSize( @@ -213,6 +238,7 @@ public class QSDetailItems extends FrameLayout { public static class Item { public int icon; + public Drawable overlay; public String line1; public String line2; public Object tag; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDualTileLabel.java b/packages/SystemUI/src/com/android/systemui/qs/QSDualTileLabel.java index a9fdc86..67cfc59 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDualTileLabel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDualTileLabel.java @@ -100,9 +100,6 @@ public class QSDualTileLabel extends LinearLayout { mFirstLineCaret.setImageDrawable(d); if (d != null) { final int h = d.getIntrinsicHeight(); - final LayoutParams lp = (LayoutParams) mSecondLine.getLayoutParams(); - lp.topMargin = h * 4 / 5; - mSecondLine.setLayoutParams(lp); mFirstLine.setMinHeight(h); mFirstLine.setPadding(mHorizontalPaddingPx, 0, 0, 0); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index fdebdd3..4dacacf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -67,8 +67,10 @@ public class QSPanel extends ViewGroup { private int mPanelPaddingBottom; private int mDualTileUnderlap; private int mBrightnessPaddingTop; + private int mGridHeight; private boolean mExpanded; private boolean mListening; + private boolean mClosingDetail; private Record mDetailRecord; private Callback mCallback; @@ -203,7 +205,7 @@ public class QSPanel extends ViewGroup { } } - private void refreshAllTiles() { + public void refreshAllTiles() { for (TileRecord r : mRecords) { r.tile.refreshState(); } @@ -296,7 +298,14 @@ public class QSPanel extends ViewGroup { r.tile.secondaryClick(); } }; - r.tileView.init(click, clickSecondary); + final View.OnLongClickListener longClick = new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + r.tile.longClick(); + return true; + } + }; + r.tileView.init(click, clickSecondary, longClick); r.tile.setListening(mListening); callback.onStateChanged(r.tile.getState()); r.tile.refreshState(); @@ -313,6 +322,14 @@ public class QSPanel extends ViewGroup { showDetail(false, mDetailRecord); } + public boolean isClosingDetail() { + return mClosingDetail; + } + + public int getGridHeight() { + return mGridHeight; + } + private void handleShowDetail(Record r, boolean show) { if (r instanceof TileRecord) { handleShowDetailTile((TileRecord) r, show); @@ -357,6 +374,7 @@ public class QSPanel extends ViewGroup { setDetailRecord(r); listener = mHideGridContentWhenDone; } else { + mClosingDetail = true; setGridContentVisibility(true); listener = mTeardownDetailWhenDone; fireScanStateChanged(false); @@ -405,7 +423,9 @@ public class QSPanel extends ViewGroup { } for (TileRecord record : mRecords) { - record.tileView.setDual(record.tile.supportsDualTargets()); + if (record.tileView.setDual(record.tile.supportsDualTargets())) { + record.tileView.handleStateChanged(record.tile.getState()); + } if (record.tileView.getVisibility() == GONE) continue; final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth; final int ch = record.row == 0 ? mLargeCellHeight : mCellHeight; @@ -419,6 +439,7 @@ public class QSPanel extends ViewGroup { if (mDetail.getMeasuredHeight() < h) { mDetail.measure(exactly(width), exactly(h)); } + mGridHeight = h; setMeasuredDimension(width, Math.max(h, mDetail.getMeasuredHeight())); } @@ -530,6 +551,7 @@ public class QSPanel extends ViewGroup { public void onAnimationEnd(Animator animation) { mDetailContent.removeAllViews(); setDetailRecord(null); + mClosingDetail = false; }; }; @@ -542,7 +564,10 @@ public class QSPanel extends ViewGroup { @Override public void onAnimationEnd(Animator animation) { - setGridContentVisibility(false); + // Only hide content if still in detail state. + if (mDetailRecord != null) { + setGridContentVisibility(false); + } } }; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java index 6ef6e9e..1790a4e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java @@ -18,11 +18,13 @@ package com.android.systemui.qs; import android.content.Context; import android.content.Intent; +import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; +import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; @@ -110,6 +112,10 @@ public abstract class QSTile<TState extends State> implements Listenable { mHandler.sendEmptyMessage(H.SECONDARY_CLICK); } + public void longClick() { + mHandler.sendEmptyMessage(H.LONG_CLICK); + } + public void showDetail(boolean show) { mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget(); } @@ -153,6 +159,10 @@ public abstract class QSTile<TState extends State> implements Listenable { // optional } + protected void handleLongClick() { + // optional + } + protected void handleRefreshState(Object arg) { handleUpdateState(mTmpState, arg); final boolean changed = mTmpState.copyTo(mState); @@ -214,12 +224,13 @@ public abstract class QSTile<TState extends State> implements Listenable { private static final int SET_CALLBACK = 1; private static final int CLICK = 2; private static final int SECONDARY_CLICK = 3; - private static final int REFRESH_STATE = 4; - private static final int SHOW_DETAIL = 5; - private static final int USER_SWITCH = 6; - private static final int TOGGLE_STATE_CHANGED = 7; - private static final int SCAN_STATE_CHANGED = 8; - private static final int DESTROY = 9; + private static final int LONG_CLICK = 4; + private static final int REFRESH_STATE = 5; + private static final int SHOW_DETAIL = 6; + private static final int USER_SWITCH = 7; + private static final int TOGGLE_STATE_CHANGED = 8; + private static final int SCAN_STATE_CHANGED = 9; + private static final int DESTROY = 10; private H(Looper looper) { super(looper); @@ -239,6 +250,9 @@ public abstract class QSTile<TState extends State> implements Listenable { } else if (msg.what == SECONDARY_CLICK) { name = "handleSecondaryClick"; handleSecondaryClick(); + } else if (msg.what == LONG_CLICK) { + name = "handleLongClick"; + handleLongClick(); } else if (msg.what == REFRESH_STATE) { name = "handleRefreshState"; handleRefreshState(msg.obj); @@ -299,10 +313,91 @@ public abstract class QSTile<TState extends State> implements Listenable { } } + public static abstract class Icon { + abstract public Drawable getDrawable(Context context); + + @Override + public int hashCode() { + return Icon.class.hashCode(); + } + } + + public static class ResourceIcon extends Icon { + private static final SparseArray<Icon> ICONS = new SparseArray<Icon>(); + + private final int mResId; + + private ResourceIcon(int resId) { + mResId = resId; + } + + public static Icon get(int resId) { + Icon icon = ICONS.get(resId); + if (icon == null) { + icon = new ResourceIcon(resId); + ICONS.put(resId, icon); + } + return icon; + } + + @Override + public Drawable getDrawable(Context context) { + return context.getDrawable(mResId); + } + + @Override + public boolean equals(Object o) { + return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId; + } + + @Override + public String toString() { + return String.format("ResourceIcon[resId=0x%08x]", mResId); + } + } + + protected class AnimationIcon extends ResourceIcon { + private boolean mAllowAnimation; + + public AnimationIcon(int resId) { + super(resId); + } + + public void setAllowAnimation(boolean allowAnimation) { + mAllowAnimation = allowAnimation; + } + + @Override + public Drawable getDrawable(Context context) { + // workaround: get a clean state for every new AVD + final AnimatedVectorDrawable d = (AnimatedVectorDrawable) super.getDrawable(context) + .getConstantState().newDrawable(); + d.start(); + if (mAllowAnimation) { + mAllowAnimation = false; + } else { + d.stop(); // skip directly to end state + } + return d; + } + } + + protected enum UserBoolean { + USER_TRUE(true, true), + USER_FALSE(true, false), + BACKGROUND_TRUE(false, true), + BACKGROUND_FALSE(false, false); + public final boolean value; + public final boolean userInitiated; + private UserBoolean(boolean userInitiated, boolean value) { + this.value = value; + this.userInitiated = userInitiated; + } + } + public static class State { public boolean visible; - public int iconId; - public Drawable icon; + public Icon icon; public String label; public String contentDescription; public String dualLabelContentDescription; @@ -312,7 +407,6 @@ public abstract class QSTile<TState extends State> implements Listenable { if (other == null) throw new IllegalArgumentException(); if (!other.getClass().equals(getClass())) throw new IllegalArgumentException(); final boolean changed = other.visible != visible - || other.iconId != iconId || !Objects.equals(other.icon, icon) || !Objects.equals(other.label, label) || !Objects.equals(other.contentDescription, contentDescription) @@ -320,7 +414,6 @@ public abstract class QSTile<TState extends State> implements Listenable { || !Objects.equals(other.dualLabelContentDescription, dualLabelContentDescription); other.visible = visible; - other.iconId = iconId; other.icon = icon; other.label = label; other.contentDescription = contentDescription; @@ -335,9 +428,8 @@ public abstract class QSTile<TState extends State> implements Listenable { } protected StringBuilder toStringBuilder() { - final StringBuilder sb = new StringBuilder( getClass().getSimpleName()).append('['); + final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('['); sb.append("visible=").append(visible); - sb.append(",iconId=").append(iconId); sb.append(",icon=").append(icon); sb.append(",label=").append(label); sb.append(",contentDescription=").append(contentDescription); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java index 3574877..16ae6b4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java @@ -21,6 +21,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Typeface; +import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.os.Handler; @@ -39,6 +40,8 @@ import com.android.systemui.FontSizeUtils; import com.android.systemui.R; import com.android.systemui.qs.QSTile.State; +import java.util.Objects; + /** View that represents a standard quick settings tile. **/ public class QSTileView extends ViewGroup { private static final Typeface CONDENSED = Typeface.create("sans-serif-condensed", @@ -60,6 +63,8 @@ public class QSTileView extends ViewGroup { private boolean mDual; private OnClickListener mClickPrimary; private OnClickListener mClickSecondary; + private OnLongClickListener mLongClick; + private Drawable mTileBackground; private RippleDrawable mRipple; public QSTileView(Context context) { @@ -72,6 +77,7 @@ public class QSTileView extends ViewGroup { mTilePaddingBelowIconPx = res.getDimensionPixelSize(R.dimen.qs_tile_padding_below_icon); mDualTileVerticalPaddingPx = res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical); + mTileBackground = newTileBackground(); recreateLabel(); setClipChildren(false); @@ -132,6 +138,7 @@ public class QSTileView extends ViewGroup { mDualLabel = new QSDualTileLabel(mContext); mDualLabel.setId(android.R.id.title); mDualLabel.setBackgroundResource(R.drawable.btn_borderless_rect); + mDualLabel.setFirstLineCaret(res.getDrawable(R.drawable.qs_dual_tile_caret)); mDualLabel.setTextColor(res.getColor(R.color.qs_tile_text)); mDualLabel.setPadding(0, mDualTileVerticalPaddingPx, 0, mDualTileVerticalPaddingPx); mDualLabel.setTypeface(CONDENSED); @@ -165,33 +172,34 @@ public class QSTileView extends ViewGroup { } } - public void setDual(boolean dual) { + public boolean setDual(boolean dual) { final boolean changed = dual != mDual; mDual = dual; if (changed) { recreateLabel(); } - Drawable tileBackground = getTileBackground(); - if (tileBackground instanceof RippleDrawable) { - setRipple((RippleDrawable) tileBackground); + if (mTileBackground instanceof RippleDrawable) { + setRipple((RippleDrawable) mTileBackground); } if (dual) { mTopBackgroundView.setOnClickListener(mClickPrimary); setOnClickListener(null); setClickable(false); setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - mTopBackgroundView.setBackground(tileBackground); + mTopBackgroundView.setBackground(mTileBackground); } else { mTopBackgroundView.setOnClickListener(null); mTopBackgroundView.setClickable(false); setOnClickListener(mClickPrimary); + setOnLongClickListener(mLongClick); setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - setBackground(tileBackground); + setBackground(mTileBackground); } mTopBackgroundView.setFocusable(dual); setFocusable(!dual); mDivider.setVisibility(dual ? VISIBLE : GONE); postInvalidate(); + return changed; } private void setRipple(RippleDrawable tileBackground) { @@ -201,9 +209,11 @@ public class QSTileView extends ViewGroup { } } - public void init(OnClickListener clickPrimary, OnClickListener clickSecondary) { + public void init(OnClickListener clickPrimary, OnClickListener clickSecondary, + OnLongClickListener longClick) { mClickPrimary = clickPrimary; mClickSecondary = clickSecondary; + mLongClick = longClick; } protected View createIcon() { @@ -213,7 +223,7 @@ public class QSTileView extends ViewGroup { return icon; } - private Drawable getTileBackground() { + private Drawable newTileBackground() { final int[] attrs = new int[] { android.R.attr.selectableItemBackgroundBorderless }; final TypedArray ta = mContext.obtainStyledAttributes(attrs); final Drawable d = ta.getDrawable(0); @@ -284,16 +294,7 @@ public class QSTileView extends ViewGroup { protected void handleStateChanged(QSTile.State state) { if (mIcon instanceof ImageView) { - ImageView iv = (ImageView) mIcon; - if (state.icon != null) { - iv.setImageDrawable(state.icon); - } else if (state.iconId > 0) { - iv.setImageResource(state.iconId); - } - Drawable drawable = iv.getDrawable(); - if (state.autoMirrorDrawable && drawable != null) { - drawable.setAutoMirrored(true); - } + setIcon((ImageView) mIcon, state); } if (mDual) { mDualLabel.setText(state.label); @@ -305,6 +306,22 @@ public class QSTileView extends ViewGroup { } } + protected void setIcon(ImageView iv, QSTile.State state) { + if (!Objects.equals(state.icon, iv.getTag(R.id.qs_icon_tag))) { + Drawable d = state.icon != null ? state.icon.getDrawable(mContext) : null; + if (d != null && state.autoMirrorDrawable) { + d.setAutoMirrored(true); + } + iv.setImageDrawable(d); + iv.setTag(R.id.qs_icon_tag, state.icon); + if (d instanceof Animatable) { + if (!iv.isShown()) { + ((Animatable) d).stop(); // skip directly to end state + } + } + } + } + public void onStateChanged(QSTile.State state) { mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java b/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java index 0ab6626..4f812bc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java +++ b/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java @@ -26,24 +26,26 @@ import com.android.systemui.statusbar.policy.Listenable; /** Helper for managing a secure setting. **/ public abstract class SecureSetting extends ContentObserver implements Listenable { + private static final int DEFAULT = 0; + private final Context mContext; private final String mSettingName; private boolean mListening; private int mUserId; + private int mObservedValue = DEFAULT; - protected abstract void handleValueChanged(int value); + protected abstract void handleValueChanged(int value, boolean observedChange); public SecureSetting(Context context, Handler handler, String settingName) { super(handler); mContext = context; mSettingName = settingName; mUserId = ActivityManager.getCurrentUser(); - setListening(true); } public int getValue() { - return Secure.getIntForUser(mContext.getContentResolver(), mSettingName, 0, mUserId); + return Secure.getIntForUser(mContext.getContentResolver(), mSettingName, DEFAULT, mUserId); } public void setValue(int value) { @@ -52,18 +54,23 @@ public abstract class SecureSetting extends ContentObserver implements Listenabl @Override public void setListening(boolean listening) { + if (listening == mListening) return; mListening = listening; if (listening) { + mObservedValue = getValue(); mContext.getContentResolver().registerContentObserver( Secure.getUriFor(mSettingName), false, this, mUserId); } else { mContext.getContentResolver().unregisterContentObserver(this); + mObservedValue = DEFAULT; } } @Override public void onChange(boolean selfChange) { - handleValueChanged(getValue()); + final int value = getValue(); + handleValueChanged(value, value != mObservedValue); + mObservedValue = value; } public void setUserId(int userId) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/SignalTileView.java b/packages/SystemUI/src/com/android/systemui/qs/SignalTileView.java index cfcd74e..9ac7944 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/SignalTileView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/SignalTileView.java @@ -104,7 +104,7 @@ public final class SignalTileView extends QSTileView { protected void handleStateChanged(QSTile.State state) { super.handleStateChanged(state); final SignalState s = (SignalState) state; - mSignal.setImageResource(s.iconId); + setIcon(mSignal, s); if (s.overlayIconId > 0) { mOverlay.setVisibility(VISIBLE); mOverlay.setImageResource(s.overlayIconId); diff --git a/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java b/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java index ad79aba..e60aa53 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java +++ b/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java @@ -18,11 +18,13 @@ package com.android.systemui.qs; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import com.android.systemui.R; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.Listenable; public class UsageTracker implements Listenable { @@ -35,11 +37,10 @@ public class UsageTracker implements Listenable { private boolean mRegistered; - public UsageTracker(Context context, Class<?> tile) { + public UsageTracker(Context context, Class<?> tile, int timeoutResource) { mContext = context; mPrefKey = tile.getSimpleName() + "LastUsed"; - mTimeToShowTile = MILLIS_PER_DAY * mContext.getResources() - .getInteger(R.integer.days_to_show_timeout_tiles); + mTimeToShowTile = MILLIS_PER_DAY * mContext.getResources().getInteger(timeoutResource); mResetAction = "com.android.systemui.qs." + tile.getSimpleName() + ".usage_reset"; } @@ -67,6 +68,25 @@ public class UsageTracker implements Listenable { getSharedPrefs().edit().remove(mPrefKey).commit(); } + public void showResetConfirmation(String title, final Runnable onConfirmed) { + final SystemUIDialog d = new SystemUIDialog(mContext); + d.setTitle(title); + d.setMessage(mContext.getString(R.string.quick_settings_reset_confirmation_message)); + d.setNegativeButton(android.R.string.cancel, null); + d.setPositiveButton(R.string.quick_settings_reset_confirmation_button, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + reset(); + if (onConfirmed != null) { + onConfirmed.run(); + } + } + }); + d.setCanceledOnTouchOutside(true); + d.show(); + } + private SharedPreferences getSharedPrefs() { return mContext.getSharedPreferences(mContext.getPackageName(), 0); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java index 51401c8..2dd02a5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java @@ -29,6 +29,10 @@ import com.android.systemui.qs.QSTile; /** Quick settings tile: Airplane mode **/ public class AirplaneModeTile extends QSTile<QSTile.BooleanState> { + private final AnimationIcon mEnable = + new AnimationIcon(R.drawable.ic_signal_airplane_enable_animation); + private final AnimationIcon mDisable = + new AnimationIcon(R.drawable.ic_signal_airplane_disable_animation); private final GlobalSetting mSetting; private boolean mListening; @@ -52,6 +56,8 @@ public class AirplaneModeTile extends QSTile<QSTile.BooleanState> { @Override public void handleClick() { setEnabled(!mState.value); + mEnable.setAllowAnimation(true); + mDisable.setAllowAnimation(true); } private void setEnabled(boolean enabled) { @@ -68,11 +74,11 @@ public class AirplaneModeTile extends QSTile<QSTile.BooleanState> { state.visible = true; state.label = mContext.getString(R.string.quick_settings_airplane_mode_label); if (airplaneMode) { - state.iconId = R.drawable.ic_qs_airplane_on; + state.icon = mEnable; state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_airplane_on); } else { - state.iconId = R.drawable.ic_qs_airplane_off; + state.icon = mDisable; state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_airplane_off); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java index e99b4c5..c15566f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -77,7 +77,11 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { @Override protected void handleSecondaryClick() { - mHost.startSettingsActivity(BLUETOOTH_SETTINGS); + if (!mState.value) { + mState.value = true; + mController.setBluetoothEnabled(true); + } + showDetail(true); } @Override @@ -92,17 +96,17 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { if (enabled) { state.label = null; if (connected) { - state.iconId = R.drawable.ic_qs_bluetooth_connected; + state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_connected); state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_bluetooth_connected); state.label = mController.getLastDeviceName(); } else if (connecting) { - state.iconId = R.drawable.ic_qs_bluetooth_connecting; + state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_connecting); state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_bluetooth_connecting); state.label = mContext.getString(R.string.quick_settings_bluetooth_label); } else { - state.iconId = R.drawable.ic_qs_bluetooth_on; + state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_on); state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_bluetooth_on); } @@ -110,7 +114,7 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { state.label = mContext.getString(R.string.quick_settings_bluetooth_label); } } else { - state.iconId = R.drawable.ic_qs_bluetooth_off; + state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_off); state.label = mContext.getString(R.string.quick_settings_bluetooth_label); state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_bluetooth_off); @@ -181,6 +185,7 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { mItems.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty, R.string.quick_settings_bluetooth_detail_empty_text); mItems.setCallback(this); + mItems.setMinHeightInItems(0); updateItems(); setItemsVisible(mState.value); return mItems; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index 8304291..5bf6fb5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -107,7 +107,8 @@ public class CastTile extends QSTile<QSTile.BooleanState> { if (!state.value && connecting) { state.label = mContext.getString(R.string.quick_settings_connecting); } - state.iconId = state.value ? R.drawable.ic_qs_cast_on : R.drawable.ic_qs_cast_off; + state.icon = ResourceIcon.get(state.value ? R.drawable.ic_qs_cast_on + : R.drawable.ic_qs_cast_off); mDetailAdapter.updateItems(devices); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java index 359a259..30f92b9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java @@ -29,7 +29,8 @@ import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QSTileView; import com.android.systemui.qs.SignalTileView; import com.android.systemui.statusbar.policy.NetworkController; -import com.android.systemui.statusbar.policy.NetworkController.DataUsageInfo; +import com.android.systemui.statusbar.policy.NetworkController.MobileDataController; +import com.android.systemui.statusbar.policy.NetworkController.MobileDataController.DataUsageInfo; import com.android.systemui.statusbar.policy.NetworkController.NetworkSignalChangedCallback; /** Quick settings tile: Cellular **/ @@ -38,11 +39,13 @@ public class CellularTile extends QSTile<QSTile.SignalState> { "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity")); private final NetworkController mController; + private final MobileDataController mDataController; private final CellularDetailAdapter mDetailAdapter; public CellularTile(Host host) { super(host); mController = host.getNetworkController(); + mDataController = mController.getMobileDataController(); mDetailAdapter = new CellularDetailAdapter(); } @@ -72,7 +75,7 @@ public class CellularTile extends QSTile<QSTile.SignalState> { @Override protected void handleClick() { - if (mController.isMobileDataSupported()) { + if (mDataController.isMobileDataSupported()) { showDetail(true); } else { mHost.startSettingsActivity(CELLULAR_SETTINGS); @@ -87,16 +90,15 @@ public class CellularTile extends QSTile<QSTile.SignalState> { if (cb == null) return; final Resources r = mContext.getResources(); - state.iconId = cb.noSim ? R.drawable.ic_qs_no_sim + final int iconId = cb.noSim ? R.drawable.ic_qs_no_sim : !cb.enabled || cb.airplaneModeEnabled ? R.drawable.ic_qs_signal_disabled : cb.mobileSignalIconId > 0 ? cb.mobileSignalIconId : R.drawable.ic_qs_signal_no_signal; + state.icon = ResourceIcon.get(iconId); state.isOverlayIconWide = cb.isDataTypeIconWide; state.autoMirrorDrawable = !cb.noSim; - state.overlayIconId = cb.enabled && (cb.dataTypeIconId > 0) && !cb.wifiConnected - ? cb.dataTypeIconId - : 0; - state.filter = state.iconId != R.drawable.ic_qs_no_sim; + state.overlayIconId = cb.enabled && (cb.dataTypeIconId > 0) ? cb.dataTypeIconId : 0; + state.filter = iconId != R.drawable.ic_qs_no_sim; state.activityIn = cb.enabled && cb.activityIn; state.activityOut = cb.enabled && cb.activityOut; @@ -143,16 +145,15 @@ public class CellularTile extends QSTile<QSTile.SignalState> { } private final NetworkSignalChangedCallback mCallback = new NetworkSignalChangedCallback() { - private boolean mWifiEnabled; - private boolean mWifiConnected; - private boolean mAirplaneModeEnabled; + private final CallbackInfo mInfo = new CallbackInfo(); @Override public void onWifiSignalChanged(boolean enabled, boolean connected, int wifiSignalIconId, boolean activityIn, boolean activityOut, String wifiSignalContentDescriptionId, String description) { - mWifiEnabled = enabled; - mWifiConnected = connected; + mInfo.wifiEnabled = enabled; + mInfo.wifiConnected = connected; + refreshState(mInfo); } @Override @@ -160,28 +161,40 @@ public class CellularTile extends QSTile<QSTile.SignalState> { int mobileSignalIconId, String mobileSignalContentDescriptionId, int dataTypeIconId, boolean activityIn, boolean activityOut, - String dataTypeContentDescriptionId, String description, boolean noSim, + String dataTypeContentDescriptionId, String description, boolean isDataTypeIconWide) { - final CallbackInfo info = new CallbackInfo(); // TODO pool? - info.enabled = enabled; - info.wifiEnabled = mWifiEnabled; - info.wifiConnected = mWifiConnected; - info.airplaneModeEnabled = mAirplaneModeEnabled; - info.mobileSignalIconId = mobileSignalIconId; - info.signalContentDescription = mobileSignalContentDescriptionId; - info.dataTypeIconId = dataTypeIconId; - info.dataContentDescription = dataTypeContentDescriptionId; - info.activityIn = activityIn; - info.activityOut = activityOut; - info.enabledDesc = description; - info.noSim = noSim; - info.isDataTypeIconWide = isDataTypeIconWide; - refreshState(info); + mInfo.enabled = enabled; + mInfo.mobileSignalIconId = mobileSignalIconId; + mInfo.signalContentDescription = mobileSignalContentDescriptionId; + mInfo.dataTypeIconId = dataTypeIconId; + mInfo.dataContentDescription = dataTypeContentDescriptionId; + mInfo.activityIn = activityIn; + mInfo.activityOut = activityOut; + mInfo.enabledDesc = description; + mInfo.isDataTypeIconWide = isDataTypeIconWide; + refreshState(mInfo); + } + + @Override + public void onNoSimVisibleChanged(boolean visible) { + mInfo.noSim = visible; + if (mInfo.noSim) { + // Make sure signal gets cleared out when no sims. + mInfo.mobileSignalIconId = 0; + mInfo.dataTypeIconId = 0; + // Show a No SIMs description to avoid emergency calls message. + mInfo.enabled = true; + mInfo.enabledDesc = mContext.getString( + R.string.keyguard_missing_sim_message_short); + mInfo.signalContentDescription = mInfo.enabledDesc; + } + refreshState(mInfo); } @Override public void onAirplaneModeChanged(boolean enabled) { - mAirplaneModeEnabled = enabled; + mInfo.airplaneModeEnabled = enabled; + refreshState(mInfo); } public void onMobileDataEnabled(boolean enabled) { @@ -198,7 +211,9 @@ public class CellularTile extends QSTile<QSTile.SignalState> { @Override public Boolean getToggleState() { - return mController.isMobileDataSupported() ? mController.isMobileDataEnabled() : null; + return mDataController.isMobileDataSupported() + ? mDataController.isMobileDataEnabled() + : null; } @Override @@ -208,7 +223,7 @@ public class CellularTile extends QSTile<QSTile.SignalState> { @Override public void setToggleState(boolean state) { - mController.setMobileDataEnabled(state); + mDataController.setMobileDataEnabled(state); } @Override @@ -216,7 +231,7 @@ public class CellularTile extends QSTile<QSTile.SignalState> { final DataUsageDetailView v = (DataUsageDetailView) (convertView != null ? convertView : LayoutInflater.from(mContext).inflate(R.layout.data_usage, parent, false)); - final DataUsageInfo info = mController.getDataUsageInfo(); + final DataUsageInfo info = mDataController.getDataUsageInfo(); if (info == null) return v; v.bind(info); return v; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java index 01849c1..5963a45 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java @@ -26,6 +26,10 @@ import com.android.systemui.qs.UsageTracker; /** Quick settings tile: Invert colors **/ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { + private final AnimationIcon mEnable + = new AnimationIcon(R.drawable.ic_invert_colors_enable_animation); + private final AnimationIcon mDisable + = new AnimationIcon(R.drawable.ic_invert_colors_disable_animation); private final SecureSetting mSetting; private final UsageTracker mUsageTracker; @@ -37,14 +41,17 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { mSetting = new SecureSetting(mContext, mHandler, Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) { @Override - protected void handleValueChanged(int value) { - mUsageTracker.trackUsage(); + protected void handleValueChanged(int value, boolean observedChange) { + if (value != 0 || observedChange) { + mUsageTracker.trackUsage(); + } if (mListening) { handleRefreshState(value); } } }; - mUsageTracker = new UsageTracker(host.getContext(), ColorInversionTile.class); + mUsageTracker = new UsageTracker(host.getContext(), ColorInversionTile.class, + R.integer.days_to_show_color_inversion_tile); if (mSetting.getValue() != 0 && !mUsageTracker.isRecentlyUsed()) { mUsageTracker.trackUsage(); } @@ -78,6 +85,21 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { mSetting.setValue(mState.value ? 0 : 1); + mEnable.setAllowAnimation(true); + mDisable.setAllowAnimation(true); + } + + @Override + protected void handleLongClick() { + if (mState.value) return; // don't allow usage reset if inversion is active + final String title = mContext.getString(R.string.quick_settings_reset_confirmation_title, + mState.label); + mUsageTracker.showResetConfirmation(title, new Runnable() { + @Override + public void run() { + refreshState(); + } + }); } @Override @@ -87,7 +109,7 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { state.visible = enabled || mUsageTracker.isRecentlyUsed(); state.value = enabled; state.label = mContext.getString(R.string.quick_settings_inversion_label); - state.iconId = enabled ? R.drawable.ic_qs_inversion_on : R.drawable.ic_qs_inversion_off; + state.icon = enabled ? mEnable : mDisable; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataUsageDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataUsageDetailView.java index 7bdb58f..eb816b7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataUsageDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataUsageDetailView.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.util.AttributeSet; -import android.util.TypedValue; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; @@ -61,7 +60,7 @@ public class DataUsageDetailView extends LinearLayout { R.dimen.qs_data_usage_text_size); } - public void bind(NetworkController.DataUsageInfo info) { + public void bind(NetworkController.MobileDataController.DataUsageInfo info) { final Resources res = mContext.getResources(); final int titleId; final long bytes; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index e6b7f02..5c1a317 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -31,6 +31,10 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements * still available because it was recently on. */ private static final long RECENTLY_ON_DURATION_MILLIS = 500; + private final AnimationIcon mEnable + = new AnimationIcon(R.drawable.ic_signal_flashlight_enable_animation); + private final AnimationIcon mDisable + = new AnimationIcon(R.drawable.ic_signal_flashlight_disable_animation); private final FlashlightController mFlashlightController; private long mWasLastOn; @@ -66,7 +70,7 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements } boolean newState = !mState.value; mFlashlightController.setFlashlight(newState); - refreshState(newState); + refreshState(newState ? UserBoolean.USER_TRUE : UserBoolean.USER_FALSE); } @Override @@ -75,8 +79,8 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements mWasLastOn = SystemClock.uptimeMillis(); } - if (arg instanceof Boolean) { - state.value = (Boolean) arg; + if (arg instanceof UserBoolean) { + state.value = ((UserBoolean) arg).value; } if (!state.value && mWasLastOn != 0) { @@ -92,8 +96,9 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements // the camera is not available while it is being used for the flashlight. state.visible = mWasLastOn != 0 || mFlashlightController.isAvailable(); state.label = mHost.getContext().getString(R.string.quick_settings_flashlight_label); - state.iconId = state.value - ? R.drawable.ic_qs_flashlight_on : R.drawable.ic_qs_flashlight_off; + final AnimationIcon icon = state.value ? mEnable : mDisable; + icon.setAllowAnimation(arg instanceof UserBoolean && ((UserBoolean) arg).userInitiated); + state.icon = icon; int onOrOffId = state.value ? R.string.accessibility_quick_settings_flashlight_on : R.string.accessibility_quick_settings_flashlight_off; @@ -111,12 +116,12 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements @Override public void onFlashlightOff() { - refreshState(false); + refreshState(UserBoolean.BACKGROUND_FALSE); } @Override public void onFlashlightError() { - refreshState(false); + refreshState(UserBoolean.BACKGROUND_FALSE); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java index ce99cc3..fcc517f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java @@ -24,18 +24,25 @@ import com.android.systemui.R; import com.android.systemui.qs.UsageTracker; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.statusbar.policy.KeyguardMonitor; /** Quick settings tile: Hotspot **/ public class HotspotTile extends QSTile<QSTile.BooleanState> { + private final AnimationIcon mEnable = + new AnimationIcon(R.drawable.ic_hotspot_enable_animation); + private final AnimationIcon mDisable = + new AnimationIcon(R.drawable.ic_hotspot_disable_animation); private final HotspotController mController; private final Callback mCallback = new Callback(); private final UsageTracker mUsageTracker; + private final KeyguardMonitor mKeyguard; public HotspotTile(Host host) { super(host); mController = host.getHotspotController(); - mUsageTracker = new UsageTracker(host.getContext(), HotspotTile.class); + mUsageTracker = newUsageTracker(host.getContext()); mUsageTracker.setListening(true); + mKeyguard = host.getKeyguardMonitor(); } @Override @@ -62,17 +69,30 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { protected void handleClick() { final boolean isEnabled = (Boolean) mState.value; mController.setHotspotEnabled(!isEnabled); + mEnable.setAllowAnimation(true); + mDisable.setAllowAnimation(true); + } + + @Override + protected void handleLongClick() { + if (mState.value) return; // don't allow usage reset if hotspot is active + final String title = mContext.getString(R.string.quick_settings_reset_confirmation_title, + mState.label); + mUsageTracker.showResetConfirmation(title, new Runnable() { + @Override + public void run() { + refreshState(); + } + }); } @Override protected void handleUpdateState(BooleanState state, Object arg) { - state.visible = mController.isHotspotSupported() && mUsageTracker.isRecentlyUsed() - && !mController.isProvisioningNeeded(); + state.visible = mController.isHotspotSupported() && mUsageTracker.isRecentlyUsed(); state.label = mContext.getString(R.string.quick_settings_hotspot_label); state.value = mController.isHotspotEnabled(); - state.iconId = state.visible && state.value ? R.drawable.ic_qs_hotspot_on - : R.drawable.ic_qs_hotspot_off; + state.icon = state.visible && state.value ? mEnable : mDisable; } @Override @@ -84,6 +104,10 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { } } + private static UsageTracker newUsageTracker(Context context) { + return new UsageTracker(context, HotspotTile.class, R.integer.days_to_show_hotspot_tile); + } + private final class Callback implements HotspotController.Callback { @Override public void onHotspotChanged(boolean enabled) { @@ -101,7 +125,7 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { @Override public void onReceive(Context context, Intent intent) { if (mUsageTracker == null) { - mUsageTracker = new UsageTracker(context, HotspotTile.class); + mUsageTracker = newUsageTracker(context); } mUsageTracker.trackUsage(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java index 58587e6..2736530 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java @@ -31,11 +31,16 @@ import android.util.Log; import com.android.systemui.qs.QSTile; +import java.util.Arrays; +import java.util.Objects; + public class IntentTile extends QSTile<QSTile.State> { public static final String PREFIX = "intent("; private PendingIntent mOnClick; private String mOnClickUri; + private PendingIntent mOnLongClick; + private String mOnLongClickUri; private int mCurrentUserId; private IntentTile(Host host, String action) { @@ -77,15 +82,24 @@ public class IntentTile extends QSTile<QSTile.State> { @Override protected void handleClick() { + sendIntent("click", mOnClick, mOnClickUri); + } + + @Override + protected void handleLongClick() { + sendIntent("long-click", mOnLongClick, mOnLongClickUri); + } + + private void sendIntent(String type, PendingIntent pi, String uri) { try { - if (mOnClick != null) { - mOnClick.send(); - } else if (mOnClickUri != null) { - final Intent intent = Intent.parseUri(mOnClickUri, Intent.URI_INTENT_SCHEME); + if (pi != null) { + pi.send(); + } else if (uri != null) { + final Intent intent = Intent.parseUri(uri, Intent.URI_INTENT_SCHEME); mContext.sendBroadcastAsUser(intent, new UserHandle(mCurrentUserId)); } } catch (Throwable t) { - Log.w(TAG, "Error sending click intent", t); + Log.w(TAG, "Error sending " + type + " intent", t); } } @@ -96,13 +110,11 @@ public class IntentTile extends QSTile<QSTile.State> { state.visible = intent.getBooleanExtra("visible", true); state.contentDescription = intent.getStringExtra("contentDescription"); state.label = intent.getStringExtra("label"); - state.iconId = 0; state.icon = null; final byte[] iconBitmap = intent.getByteArrayExtra("iconBitmap"); if (iconBitmap != null) { try { - final Bitmap b = BitmapFactory.decodeByteArray(iconBitmap, 0, iconBitmap.length); - state.icon = new BitmapDrawable(mContext.getResources(), b); + state.icon = new BytesIcon(iconBitmap); } catch (Throwable t) { Log.w(TAG, "Error loading icon bitmap, length " + iconBitmap.length, t); } @@ -111,23 +123,16 @@ public class IntentTile extends QSTile<QSTile.State> { if (iconId != 0) { final String iconPackage = intent.getStringExtra("iconPackage"); if (!TextUtils.isEmpty(iconPackage)) { - state.icon = getPackageDrawable(iconPackage, iconId); + state.icon = new PackageDrawableIcon(iconPackage, iconId); } else { - state.iconId = iconId; + state.icon = ResourceIcon.get(iconId); } } } mOnClick = intent.getParcelableExtra("onClick"); mOnClickUri = intent.getStringExtra("onClickUri"); - } - - private Drawable getPackageDrawable(String pkg, int id) { - try { - return mContext.createPackageContext(pkg, 0).getDrawable(id); - } catch (Throwable t) { - Log.w(TAG, "Error loading package drawable pkg=" + pkg + " id=" + id, t); - return null; - } + mOnLongClick = intent.getParcelableExtra("onLongClick"); + mOnLongClickUri = intent.getStringExtra("onLongClickUri"); } private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -136,4 +141,60 @@ public class IntentTile extends QSTile<QSTile.State> { refreshState(intent); } }; + + private static class BytesIcon extends Icon { + private final byte[] mBytes; + + public BytesIcon(byte[] bytes) { + mBytes = bytes; + } + + @Override + public Drawable getDrawable(Context context) { + final Bitmap b = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length); + return new BitmapDrawable(context.getResources(), b); + } + + @Override + public boolean equals(Object o) { + return o instanceof BytesIcon && Arrays.equals(((BytesIcon) o).mBytes, mBytes); + } + + @Override + public String toString() { + return String.format("BytesIcon[len=%s]", mBytes.length); + } + } + + private class PackageDrawableIcon extends Icon { + private final String mPackage; + private final int mResId; + + public PackageDrawableIcon(String pkg, int resId) { + mPackage = pkg; + mResId = resId; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PackageDrawableIcon)) return false; + final PackageDrawableIcon other = (PackageDrawableIcon) o; + return Objects.equals(other.mPackage, mPackage) && other.mResId == mResId; + } + + @Override + public Drawable getDrawable(Context context) { + try { + return context.createPackageContext(mPackage, 0).getDrawable(mResId); + } catch (Throwable t) { + Log.w(TAG, "Error loading package drawable pkg=" + mPackage + " id=" + mResId, t); + return null; + } + } + + @Override + public String toString() { + return String.format("PackageDrawableIcon[pkg=%s,id=0x%08x]", mPackage, mResId); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java index d1dc5d2..11ec722 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java @@ -25,6 +25,11 @@ import com.android.systemui.statusbar.policy.LocationController.LocationSettings /** Quick settings tile: Location **/ public class LocationTile extends QSTile<QSTile.BooleanState> { + private final AnimationIcon mEnable = + new AnimationIcon(R.drawable.ic_signal_location_enable_animation); + private final AnimationIcon mDisable = + new AnimationIcon(R.drawable.ic_signal_location_disable_animation); + private final LocationController mController; private final KeyguardMonitor mKeyguard; private final Callback mCallback = new Callback(); @@ -55,6 +60,8 @@ public class LocationTile extends QSTile<QSTile.BooleanState> { protected void handleClick() { final boolean wasEnabled = (Boolean) mState.value; mController.setLocationEnabled(!wasEnabled); + mEnable.setAllowAnimation(true); + mDisable.setAllowAnimation(true); } @Override @@ -67,12 +74,12 @@ public class LocationTile extends QSTile<QSTile.BooleanState> { state.visible = !mKeyguard.isShowing(); state.value = locationEnabled; if (locationEnabled) { - state.iconId = R.drawable.ic_qs_location_on; + state.icon = mEnable; state.label = mContext.getString(R.string.quick_settings_location_label); state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_location_on); } else { - state.iconId = R.drawable.ic_qs_location_off; + state.icon = mDisable; state.label = mContext.getString(R.string.quick_settings_location_label); state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_location_off); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java index ae40a4d..f46b9a6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java @@ -17,7 +17,6 @@ package com.android.systemui.qs.tiles; import android.content.res.Configuration; -import android.content.res.Resources; import com.android.systemui.R; import com.android.systemui.qs.QSTile; @@ -26,6 +25,15 @@ import com.android.systemui.statusbar.policy.RotationLockController.RotationLock /** Quick settings tile: Rotation **/ public class RotationLockTile extends QSTile<QSTile.BooleanState> { + private final AnimationIcon mPortraitToAuto + = new AnimationIcon(R.drawable.ic_portrait_to_auto_rotate_animation); + private final AnimationIcon mAutoToPortrait + = new AnimationIcon(R.drawable.ic_portrait_from_auto_rotate_animation); + + private final AnimationIcon mLandscapeToAuto + = new AnimationIcon(R.drawable.ic_landscape_to_auto_rotate_animation); + private final AnimationIcon mAutoToLandscape + = new AnimationIcon(R.drawable.ic_landscape_from_auto_rotate_animation); private final RotationLockController mController; @@ -51,30 +59,34 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { if (mController == null) return; - mController.setRotationLocked(!mState.value); + final boolean newState = !mState.value; + mController.setRotationLocked(newState); + refreshState(newState ? UserBoolean.USER_TRUE : UserBoolean.USER_FALSE); } @Override protected void handleUpdateState(BooleanState state, Object arg) { if (mController == null) return; - final boolean rotationLocked = mController.isRotationLocked(); + final boolean rotationLocked = arg != null ? ((UserBoolean) arg).value + : mController.isRotationLocked(); + final boolean userInitiated = arg != null ? ((UserBoolean) arg).userInitiated : false; state.visible = mController.isRotationLockAffordanceVisible(); - final Resources res = mContext.getResources(); state.value = rotationLocked; + final boolean portrait = mContext.getResources().getConfiguration().orientation + != Configuration.ORIENTATION_LANDSCAPE; + final AnimationIcon icon; if (rotationLocked) { - final boolean portrait = res.getConfiguration().orientation - != Configuration.ORIENTATION_LANDSCAPE; final int label = portrait ? R.string.quick_settings_rotation_locked_portrait_label : R.string.quick_settings_rotation_locked_landscape_label; - final int icon = portrait ? R.drawable.ic_qs_rotation_portrait - : R.drawable.ic_qs_rotation_landscape; state.label = mContext.getString(label); - state.icon = mContext.getDrawable(icon); + icon = portrait ? mAutoToPortrait : mAutoToLandscape; } else { state.label = mContext.getString(R.string.quick_settings_rotation_unlocked_label); - state.icon = res.getDrawable(R.drawable.ic_qs_rotation_unlocked); + icon = portrait ? mPortraitToAuto : mLandscapeToAuto; } - state.contentDescription = getAccessibilityString( + icon.setAllowAnimation(userInitiated); + state.icon = icon; + state.contentDescription = getAccessibilityString(rotationLocked, R.string.accessibility_rotation_lock_on_portrait, R.string.accessibility_rotation_lock_on_landscape, R.string.accessibility_rotation_lock_off); @@ -83,14 +95,16 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> { /** * Get the correct accessibility string based on the state * + * @param locked Whether or not rotation is locked. * @param idWhenPortrait The id which should be used when locked in portrait. * @param idWhenLandscape The id which should be used when locked in landscape. * @param idWhenOff The id which should be used when the rotation lock is off. * @return */ - private String getAccessibilityString(int idWhenPortrait, int idWhenLandscape, int idWhenOff) { + private String getAccessibilityString(boolean locked, int idWhenPortrait, int idWhenLandscape, + int idWhenOff) { int stringID; - if (mState.value) { + if (locked) { final boolean portrait = mContext.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE; stringID = portrait ? idWhenPortrait: idWhenLandscape; @@ -102,7 +116,7 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> { @Override protected String composeChangeAnnouncement() { - return getAccessibilityString( + return getAccessibilityString(mState.value, R.string.accessibility_rotation_lock_on_portrait_changed, R.string.accessibility_rotation_lock_on_landscape_changed, R.string.accessibility_rotation_lock_off_changed); @@ -111,7 +125,8 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> { private final RotationLockControllerCallback mCallback = new RotationLockControllerCallback() { @Override public void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible) { - refreshState(); + refreshState(rotationLocked ? UserBoolean.BACKGROUND_TRUE + : UserBoolean.BACKGROUND_FALSE); } }; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index c524edc..6bad652 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -47,6 +47,10 @@ public class UserDetailView extends PseudoGridView { ViewGroupAdapterBridge.link(this, mAdapter); } + public void refreshAdapter() { + mAdapter.refresh(); + } + public static class Adapter extends UserSwitcherController.BaseUserAdapter implements OnClickListener { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java index 0985812..e09024b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java @@ -30,8 +30,10 @@ import com.android.systemui.qs.QSDetailItems.Item; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QSTileView; import com.android.systemui.qs.SignalTileView; +import com.android.systemui.statusbar.phone.QSTileHost; import com.android.systemui.statusbar.policy.NetworkController; -import com.android.systemui.statusbar.policy.NetworkController.AccessPoint; +import com.android.systemui.statusbar.policy.NetworkController.AccessPointController; +import com.android.systemui.statusbar.policy.NetworkController.AccessPointController.AccessPoint; import com.android.systemui.statusbar.policy.NetworkController.NetworkSignalChangedCallback; /** Quick settings tile: Wifi **/ @@ -39,12 +41,14 @@ public class WifiTile extends QSTile<QSTile.SignalState> { private static final Intent WIFI_SETTINGS = new Intent(Settings.ACTION_WIFI_SETTINGS); private final NetworkController mController; + private final AccessPointController mWifiController; private final WifiDetailAdapter mDetailAdapter; private final QSTile.SignalState mStateBeforeClick = newTileState(); public WifiTile(Host host) { super(host); mController = host.getNetworkController(); + mWifiController = mController.getAccessPointController(); mDetailAdapter = new WifiDetailAdapter(); } @@ -62,10 +66,10 @@ public class WifiTile extends QSTile<QSTile.SignalState> { public void setListening(boolean listening) { if (listening) { mController.addNetworkSignalChangedCallback(mCallback); - mController.addAccessPointCallback(mDetailAdapter); + mWifiController.addAccessPointCallback(mDetailAdapter); } else { mController.removeNetworkSignalChangedCallback(mCallback); - mController.removeAccessPointCallback(mDetailAdapter); + mWifiController.removeAccessPointCallback(mDetailAdapter); } } @@ -87,7 +91,15 @@ public class WifiTile extends QSTile<QSTile.SignalState> { @Override protected void handleSecondaryClick() { - mHost.startSettingsActivity(WIFI_SETTINGS); + if (!mWifiController.canConfigWifi()) { + mHost.startSettingsActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)); + return; + } + if (!mState.enabled) { + mController.setWifiEnabled(true); + mState.enabled = true; + } + showDetail(true); } @Override @@ -112,19 +124,19 @@ public class WifiTile extends QSTile<QSTile.SignalState> { final String signalContentDescription; final Resources r = mContext.getResources(); if (!state.enabled) { - state.iconId = R.drawable.ic_qs_wifi_disabled; + state.icon = ResourceIcon.get(R.drawable.ic_qs_wifi_disabled); state.label = r.getString(R.string.quick_settings_wifi_label); signalContentDescription = r.getString(R.string.accessibility_wifi_off); } else if (wifiConnected) { - state.iconId = cb.wifiSignalIconId; + state.icon = ResourceIcon.get(cb.wifiSignalIconId); state.label = removeDoubleQuotes(cb.enabledDesc); signalContentDescription = cb.wifiSignalContentDescription; } else if (wifiNotConnected) { - state.iconId = R.drawable.ic_qs_wifi_0; + state.icon = ResourceIcon.get(R.drawable.ic_qs_wifi_0); state.label = r.getString(R.string.quick_settings_wifi_label); signalContentDescription = r.getString(R.string.accessibility_no_wifi); } else { - state.iconId = R.drawable.ic_qs_wifi_no_network; + state.icon = ResourceIcon.get(R.drawable.ic_qs_wifi_no_network); state.label = r.getString(R.string.quick_settings_wifi_label); signalContentDescription = r.getString(R.string.accessibility_wifi_off); } @@ -206,11 +218,15 @@ public class WifiTile extends QSTile<QSTile.SignalState> { int mobileSignalIconId, String mobileSignalContentDescriptionId, int dataTypeIconId, boolean activityIn, boolean activityOut, - String dataTypeContentDescriptionId, String description, boolean noSim, + String dataTypeContentDescriptionId, String description, boolean isDataTypeIconWide) { // noop } + public void onNoSimVisibleChanged(boolean noSims) { + // noop + } + @Override public void onAirplaneModeChanged(boolean enabled) { // noop @@ -223,7 +239,7 @@ public class WifiTile extends QSTile<QSTile.SignalState> { }; private final class WifiDetailAdapter implements DetailAdapter, - NetworkController.AccessPointCallback, QSDetailItems.Callback { + NetworkController.AccessPointController.AccessPointCallback, QSDetailItems.Callback { private QSDetailItems mItems; private AccessPoint[] mAccessPoints; @@ -253,7 +269,7 @@ public class WifiTile extends QSTile<QSTile.SignalState> { public View createDetailView(Context context, View convertView, ViewGroup parent) { if (DEBUG) Log.d(TAG, "createDetailView convertView=" + (convertView != null)); mAccessPoints = null; - mController.scanForAccessPoints(); + mWifiController.scanForAccessPoints(); fireScanStateChanged(true); mItems = QSDetailItems.convertOrInflate(context, convertView, parent); mItems.setTagSuffix("Wifi"); @@ -275,11 +291,18 @@ public class WifiTile extends QSTile<QSTile.SignalState> { } @Override + public void onSettingsActivityTriggered(Intent settingsIntent) { + mHost.startSettingsActivity(settingsIntent); + } + + @Override public void onDetailItemClick(Item item) { if (item == null || item.tag == null) return; final AccessPoint ap = (AccessPoint) item.tag; if (!ap.isConnected) { - mController.connect(ap); + if (mWifiController.connect(ap)) { + mHost.collapsePanels(); + } } showDetail(false); } @@ -306,8 +329,15 @@ public class WifiTile extends QSTile<QSTile.SignalState> { item.icon = ap.iconId; item.line1 = ap.ssid; if (ap.isConnected) { - item.line2 = mContext.getString(R.string.quick_settings_connected); + item.line2 = mContext.getString(ap.isConfigured ? + R.string.quick_settings_connected : + R.string.quick_settings_connected_via_wfa); + } else if (ap.networkId >= 0) { + // TODO: Set line 2 to wifi saved string here. } + item.overlay = ap.hasSecurity + ? mContext.getDrawable(R.drawable.qs_ic_wifi_lock) + : null; items[i] = item; } } diff --git a/packages/SystemUI/src/com/android/systemui/recent/Recents.java b/packages/SystemUI/src/com/android/systemui/recent/Recents.java index 9a55590..e9f3cf9 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/Recents.java +++ b/packages/SystemUI/src/com/android/systemui/recent/Recents.java @@ -18,6 +18,7 @@ package com.android.systemui.recent; import android.app.ActivityOptions; import android.content.ActivityNotFoundException; +import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; @@ -44,16 +45,29 @@ public class Recents extends SystemUI implements RecentsComponent { // Which recents to use boolean mUseAlternateRecents = true; - AlternateRecentsComponent mAlternateRecents; boolean mBootCompleted = false; + static AlternateRecentsComponent sAlternateRecents; + + /** Returns the Recents component, creating a new one in-process if necessary. */ + public static AlternateRecentsComponent getRecentsComponent(Context context, + boolean forceInitialize) { + if (sAlternateRecents == null) { + sAlternateRecents = new AlternateRecentsComponent(context); + if (forceInitialize) { + sAlternateRecents.onStart(); + sAlternateRecents.onBootCompleted(); + } + } + return sAlternateRecents; + } @Override public void start() { if (mUseAlternateRecents) { - if (mAlternateRecents == null) { - mAlternateRecents = new AlternateRecentsComponent(mContext); + if (sAlternateRecents == null) { + sAlternateRecents = getRecentsComponent(mContext, false); } - mAlternateRecents.onStart(); + sAlternateRecents.onStart(); } putComponent(RecentsComponent.class, this); @@ -62,8 +76,8 @@ public class Recents extends SystemUI implements RecentsComponent { @Override protected void onBootCompleted() { if (mUseAlternateRecents) { - if (mAlternateRecents != null) { - mAlternateRecents.onBootCompleted(); + if (sAlternateRecents != null) { + sAlternateRecents.onBootCompleted(); } } mBootCompleted = true; @@ -72,14 +86,14 @@ public class Recents extends SystemUI implements RecentsComponent { @Override public void showRecents(boolean triggeredFromAltTab, View statusBarView) { if (mUseAlternateRecents) { - mAlternateRecents.onShowRecents(triggeredFromAltTab, statusBarView); + sAlternateRecents.onShowRecents(triggeredFromAltTab); } } @Override public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { if (mUseAlternateRecents) { - mAlternateRecents.onHideRecents(triggeredFromAltTab, triggeredFromHomeKey); + sAlternateRecents.onHideRecents(triggeredFromAltTab, triggeredFromHomeKey); } else { Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT); intent.setPackage("com.android.systemui"); @@ -93,7 +107,7 @@ public class Recents extends SystemUI implements RecentsComponent { public void toggleRecents(Display display, int layoutDirection, View statusBarView) { if (mUseAlternateRecents) { // Launch the alternate recents if required - mAlternateRecents.onToggleRecents(statusBarView); + sAlternateRecents.onToggleRecents(); return; } @@ -241,14 +255,14 @@ public class Recents extends SystemUI implements RecentsComponent { @Override protected void onConfigurationChanged(Configuration newConfig) { if (mUseAlternateRecents) { - mAlternateRecents.onConfigurationChanged(newConfig); + sAlternateRecents.onConfigurationChanged(newConfig); } } @Override public void preloadRecents() { if (mUseAlternateRecents) { - mAlternateRecents.onPreloadRecents(); + sAlternateRecents.onPreloadRecents(); } else { Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT); intent.setClassName("com.android.systemui", @@ -262,7 +276,7 @@ public class Recents extends SystemUI implements RecentsComponent { @Override public void cancelPreloadingRecents() { if (mUseAlternateRecents) { - mAlternateRecents.onCancelPreloadingRecents(); + sAlternateRecents.onCancelPreloadingRecents(); } else { Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT); intent.setClassName("com.android.systemui", @@ -276,21 +290,21 @@ public class Recents extends SystemUI implements RecentsComponent { @Override public void showNextAffiliatedTask() { if (mUseAlternateRecents) { - mAlternateRecents.onShowNextAffiliatedTask(); + sAlternateRecents.onShowNextAffiliatedTask(); } } @Override public void showPrevAffiliatedTask() { if (mUseAlternateRecents) { - mAlternateRecents.onShowPrevAffiliatedTask(); + sAlternateRecents.onShowPrevAffiliatedTask(); } } @Override public void setCallback(Callbacks cb) { if (mUseAlternateRecents) { - mAlternateRecents.setRecentsComponentCallback(cb); + sAlternateRecents.setRecentsComponentCallback(cb); } } diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java index 2a782cc..4c3460e 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java @@ -728,7 +728,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener final ActivityManager am = (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE); if (am != null) { - am.removeTask(ad.persistentTaskId, ActivityManager.REMOVE_TASK_KILL_PROCESS); + am.removeTask(ad.persistentTaskId); // Accessibility feedback setContentDescription( diff --git a/packages/SystemUI/src/com/android/systemui/recent/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recent/ScreenPinningRequest.java new file mode 100644 index 0000000..2fa0b58 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recent/ScreenPinningRequest.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2014 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.recent; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.app.ActivityManagerNative; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.drawable.ColorDrawable; +import android.os.RemoteException; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.DecelerateInterpolator; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.systemui.R; +import com.android.systemui.recents.model.RecentsTaskLoader; + +import java.util.ArrayList; + +public class ScreenPinningRequest implements View.OnClickListener { + private final Context mContext; + + private final AccessibilityManager mAccessibilityService; + private final WindowManager mWindowManager; + + private RequestWindowView mRequestWindow; + + public ScreenPinningRequest(Context context) { + mContext = context; + mAccessibilityService = (AccessibilityManager) + mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + mWindowManager = (WindowManager) + mContext.getSystemService(Context.WINDOW_SERVICE); + } + + public void clearPrompt() { + if (mRequestWindow != null) { + mWindowManager.removeView(mRequestWindow); + mRequestWindow = null; + } + } + + public void showPrompt(boolean allowCancel) { + clearPrompt(); + + mRequestWindow = new RequestWindowView(mContext, allowCancel); + + mRequestWindow.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + + // show the confirmation + WindowManager.LayoutParams lp = getWindowLayoutParams(); + mWindowManager.addView(mRequestWindow, lp); + } + + public void onConfigurationChanged() { + if (mRequestWindow != null) { + mRequestWindow.onConfigurationChanged(); + } + } + + private WindowManager.LayoutParams getWindowLayoutParams() { + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + 0 + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED + , + PixelFormat.TRANSLUCENT); + lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + lp.setTitle("ScreenPinningConfirmation"); + lp.gravity = Gravity.FILL; + return lp; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.screen_pinning_ok_button || mRequestWindow == v) { + try { + ActivityManagerNative.getDefault().startLockTaskModeOnCurrent(); + } catch (RemoteException e) {} + } + clearPrompt(); + } + + public FrameLayout.LayoutParams getRequestLayoutParams(boolean isLandscape) { + return new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + isLandscape ? (Gravity.CENTER_VERTICAL | Gravity.RIGHT) + : (Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM)); + } + + private class RequestWindowView extends FrameLayout { + private static final int OFFSET_DP = 96; + + private final ColorDrawable mColor = new ColorDrawable(0); + private ValueAnimator mColorAnim; + private ViewGroup mLayout; + private boolean mShowCancel; + + public RequestWindowView(Context context, boolean showCancel) { + super(context); + setClickable(true); + setOnClickListener(ScreenPinningRequest.this); + setBackground(mColor); + mShowCancel = showCancel; + } + + @Override + public void onAttachedToWindow() { + DisplayMetrics metrics = new DisplayMetrics(); + mWindowManager.getDefaultDisplay().getMetrics(metrics); + float density = metrics.density; + boolean isLandscape = isLandscapePhone(mContext); + + inflateView(isLandscape); + int bgColor = mContext.getResources().getColor( + R.color.screen_pinning_request_window_bg); + if (ActivityManager.isHighEndGfx()) { + mLayout.setAlpha(0f); + if (isLandscape) { + mLayout.setTranslationX(OFFSET_DP * density); + } else { + mLayout.setTranslationY(OFFSET_DP * density); + } + mLayout.animate() + .alpha(1f) + .translationX(0) + .translationY(0) + .setDuration(300) + .setInterpolator(new DecelerateInterpolator()) + .start(); + + mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, bgColor); + mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final int c = (Integer) animation.getAnimatedValue(); + mColor.setColor(c); + } + }); + mColorAnim.setDuration(1000); + mColorAnim.start(); + } else { + mColor.setColor(bgColor); + } + + IntentFilter filter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + filter.addAction(Intent.ACTION_USER_SWITCHED); + filter.addAction(Intent.ACTION_SCREEN_OFF); + mContext.registerReceiver(mReceiver, filter); + } + + private boolean isLandscapePhone(Context context) { + Configuration config = mContext.getResources().getConfiguration(); + return config.orientation == Configuration.ORIENTATION_LANDSCAPE + && config.smallestScreenWidthDp < 600; + } + + private void inflateView(boolean isLandscape) { + // We only want this landscape orientation on <600dp, so rather than handle + // resource overlay for -land and -sw600dp-land, just inflate this + // other view for this single case. + mLayout = (ViewGroup) View.inflate(getContext(), isLandscape + ? R.layout.screen_pinning_request_land_phone : R.layout.screen_pinning_request, + null); + // Catch touches so they don't trigger cancel/activate, like outside does. + mLayout.setClickable(true); + // Status bar is always on the right. + mLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); + // Buttons and text do switch sides though. + View buttons = mLayout.findViewById(R.id.screen_pinning_buttons); + buttons.setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); + mLayout.findViewById(R.id.screen_pinning_text_area) + .setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); + swapChildrenIfRtlAndVertical(buttons); + + ((Button) mLayout.findViewById(R.id.screen_pinning_ok_button)) + .setOnClickListener(ScreenPinningRequest.this); + if (mShowCancel) { + ((Button) mLayout.findViewById(R.id.screen_pinning_cancel_button)) + .setOnClickListener(ScreenPinningRequest.this); + } else { + ((Button) mLayout.findViewById(R.id.screen_pinning_cancel_button)) + .setVisibility(View.INVISIBLE); + } + + final int description = mAccessibilityService.isEnabled() + ? R.string.screen_pinning_description_accessible + : R.string.screen_pinning_description; + ((TextView) mLayout.findViewById(R.id.screen_pinning_description)) + .setText(description); + final int backBgVisibility = + mAccessibilityService.isEnabled() ? View.INVISIBLE : View.VISIBLE; + mLayout.findViewById(R.id.screen_pinning_back_bg).setVisibility(backBgVisibility); + mLayout.findViewById(R.id.screen_pinning_back_bg_light).setVisibility(backBgVisibility); + + addView(mLayout, getRequestLayoutParams(isLandscape)); + } + + private void swapChildrenIfRtlAndVertical(View group) { + if (mContext.getResources().getConfiguration().getLayoutDirection() + != View.LAYOUT_DIRECTION_RTL) { + return; + } + LinearLayout linearLayout = (LinearLayout) group; + if (linearLayout.getOrientation() == LinearLayout.VERTICAL) { + int childCount = linearLayout.getChildCount(); + ArrayList<View> childList = new ArrayList<>(childCount); + for (int i = 0; i < childCount; i++) { + childList.add(linearLayout.getChildAt(i)); + } + linearLayout.removeAllViews(); + for (int i = childCount - 1; i >= 0; i--) { + linearLayout.addView(childList.get(i)); + } + } + } + + @Override + public void onDetachedFromWindow() { + mContext.unregisterReceiver(mReceiver); + } + + protected void onConfigurationChanged() { + removeAllViews(); + inflateView(isLandscapePhone(mContext)); + } + + private final Runnable mUpdateLayoutRunnable = new Runnable() { + @Override + public void run() { + if (mLayout != null && mLayout.getParent() != null) { + mLayout.setLayoutParams(getRequestLayoutParams(isLandscapePhone(mContext))); + } + } + }; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) { + post(mUpdateLayoutRunnable); + } else if (intent.getAction().equals(Intent.ACTION_USER_SWITCHED) + || intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { + clearPrompt(); + } + } + }; + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java index 1283dcd..910a57e 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java +++ b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java @@ -19,6 +19,7 @@ package com.android.systemui.recents; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityOptions; +import android.app.ITaskStackListener; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetProviderInfo; import android.content.ActivityNotFoundException; @@ -26,12 +27,14 @@ import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Handler; +import android.os.SystemClock; import android.os.UserHandle; import android.util.Pair; import android.view.LayoutInflater; @@ -40,6 +43,7 @@ import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.recents.misc.Console; import com.android.systemui.recents.misc.SystemServicesProxy; +import com.android.systemui.recents.model.RecentsTaskLoadPlan; import com.android.systemui.recents.model.RecentsTaskLoader; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskGrouping; @@ -53,16 +57,27 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +/** + * Annotation for a method that is only called from the primary user's SystemUI process and will be + * proxied to the current user. + */ +@interface ProxyFromPrimaryToCurrentUser {} +/** + * Annotation for a method that may be called from any user's SystemUI process and will be proxied + * to the primary user. + */ +@interface ProxyFromAnyToPrimaryUser {} + /** A proxy implementation for the recents component */ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationStartedListener { - final public static String EXTRA_FROM_HOME = "recents.triggeredOverHome"; - final public static String EXTRA_FROM_SEARCH_HOME = "recents.triggeredOverSearchHome"; - final public static String EXTRA_FROM_APP_THUMBNAIL = "recents.animatingWithThumbnail"; - final public static String EXTRA_FROM_APP_FULL_SCREENSHOT = "recents.thumbnail"; - final public static String EXTRA_FROM_TASK_ID = "recents.activeTaskId"; - final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "recents.triggeredFromAltTab"; - final public static String EXTRA_TRIGGERED_FROM_HOME_KEY = "recents.triggeredFromHomeKey"; + final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "triggeredFromAltTab"; + final public static String EXTRA_TRIGGERED_FROM_HOME_KEY = "triggeredFromHomeKey"; + final public static String EXTRA_RECENTS_VISIBILITY = "recentsVisibility"; + + // Owner proxy events + final public static String ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER = + "action_notify_recents_visibility_change"; final public static String ACTION_START_ENTER_ANIMATION = "action_start_enter_animation"; final public static String ACTION_TOGGLE_RECENTS_ACTIVITY = "action_toggle_recents_activity"; @@ -71,18 +86,80 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta final static int sMinToggleDelay = 350; final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS"; - final static String sRecentsPackage = "com.android.systemui"; - final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; + public final static String sRecentsPackage = "com.android.systemui"; + public final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; + + /** + * An implementation of ITaskStackListener, that allows us to listen for changes to the system + * task stacks and update recents accordingly. + */ + class TaskStackListenerImpl extends ITaskStackListener.Stub implements Runnable { + Handler mHandler; + + public TaskStackListenerImpl(Handler handler) { + mHandler = handler; + } + + @Override + public void onTaskStackChanged() { + // Debounce any task stack changes + mHandler.removeCallbacks(this); + mHandler.post(this); + } + + /** Preloads the next task */ + public void run() { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) { + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + SystemServicesProxy ssp = loader.getSystemServicesProxy(); + ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getTopMostTask(); + + // Load the next task only if we aren't svelte + RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); + loader.preloadTasks(plan, true /* isTopTaskHome */); + RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); + // This callback is made when a new activity is launched and the old one is paused + // so ignore the current activity and try and preload the thumbnail for the + // previous one. + if (runningTaskInfo != null) { + launchOpts.runningTaskId = runningTaskInfo.id; + } + launchOpts.numVisibleTasks = 2; + launchOpts.numVisibleTaskThumbnails = 2; + launchOpts.onlyLoadForCache = true; + launchOpts.onlyLoadPausedActivities = true; + loader.loadTasks(mContext, plan, launchOpts); + } + } + } + + /** + * A proxy for Recents events which happens strictly for the owner. + */ + class RecentsOwnerEventProxyReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER: + visibilityChanged(intent.getBooleanExtra(EXTRA_RECENTS_VISIBILITY, false)); + break; + } + } + } - static Bitmap sLastScreenshot; static RecentsComponent.Callbacks sRecentsComponentCallbacks; + static RecentsTaskLoadPlan sInstanceLoadPlan; Context mContext; LayoutInflater mInflater; SystemServicesProxy mSystemServicesProxy; Handler mHandler; + TaskStackListenerImpl mTaskStackListener; + RecentsOwnerEventProxyReceiver mProxyBroadcastReceiver; boolean mBootCompleted; boolean mStartAnimationTriggered; + boolean mCanReuseTaskStackViews = true; // Task launching RecentsConfiguration mConfig; @@ -99,7 +176,6 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta TaskStackView mDummyStackView; // Variables to keep track of if we need to start recents after binding - View mStatusBarView; boolean mTriggeredFromAltTab; long mLastToggleTime; @@ -110,37 +186,69 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta mSystemServicesProxy = new SystemServicesProxy(context); mHandler = new Handler(); mTaskStackBounds = new Rect(); + + // Register the task stack listener + mTaskStackListener = new TaskStackListenerImpl(mHandler); + mSystemServicesProxy.registerTaskStackListener(mTaskStackListener); + + // Only the owner has the callback to update the SysUI visibility flags, so all non-owner + // instances of AlternateRecentsComponent needs to notify the owner when the visibility + // changes. + if (mSystemServicesProxy.isForegroundUserOwner()) { + mProxyBroadcastReceiver = new RecentsOwnerEventProxyReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(AlternateRecentsComponent.ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER); + mContext.registerReceiverAsUser(mProxyBroadcastReceiver, UserHandle.CURRENT, filter, + null, mHandler); + } } + /** Creates a new broadcast intent */ + static Intent createLocalBroadcastIntent(Context context, String action) { + Intent intent = new Intent(action); + intent.setPackage(context.getPackageName()); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | + Intent.FLAG_RECEIVER_FOREGROUND); + return intent; + } + + /** Initializes the Recents. */ + @ProxyFromPrimaryToCurrentUser public void onStart() { // Initialize some static datastructures TaskStackViewLayoutAlgorithm.initializeCurve(); // Load the header bar layout - reloadHeaderBarLayout(); - // Try and pre-emptively bind the search widget on startup to ensure that we - // have the right thumbnail bounds to animate to. - if (Constants.DebugFlags.App.EnableSearchLayout) { - // If there is no id, then bind a new search app widget - if (mConfig.searchBarAppWidgetId < 0) { - AppWidgetHost host = new RecentsAppWidgetHost(mContext, - Constants.Values.App.AppWidgetHostId); - Pair<Integer, AppWidgetProviderInfo> widgetInfo = - mSystemServicesProxy.bindSearchAppWidget(host); - if (widgetInfo != null) { - // Save the app widget id into the settings - mConfig.updateSearchBarAppWidgetId(mContext, widgetInfo.first); - } - } - } + reloadHeaderBarLayout(true); + + // When we start, preload the data associated with the previous recent tasks. + // We can use a new plan since the caches will be the same. + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); + loader.preloadTasks(plan, true /* isTopTaskHome */); + RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); + launchOpts.numVisibleTasks = loader.getApplicationIconCacheSize(); + launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize(); + launchOpts.onlyLoadForCache = true; + loader.loadTasks(mContext, plan, launchOpts); } public void onBootCompleted() { mBootCompleted = true; } - /** Shows the recents */ - public void onShowRecents(boolean triggeredFromAltTab, View statusBarView) { - mStatusBarView = statusBarView; + /** Shows the Recents. */ + @ProxyFromPrimaryToCurrentUser + public void onShowRecents(boolean triggeredFromAltTab) { + if (mSystemServicesProxy.isForegroundUserOwner()) { + showRecents(triggeredFromAltTab); + } else { + Intent intent = createLocalBroadcastIntent(mContext, + RecentsUserEventProxyReceiver.ACTION_PROXY_SHOW_RECENTS_TO_USER); + intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } + } + void showRecents(boolean triggeredFromAltTab) { mTriggeredFromAltTab = triggeredFromAltTab; try { @@ -150,15 +258,25 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } } - /** Hides the recents */ + /** Hides the Recents. */ + @ProxyFromPrimaryToCurrentUser public void onHideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { + if (mSystemServicesProxy.isForegroundUserOwner()) { + hideRecents(triggeredFromAltTab, triggeredFromHomeKey); + } else { + Intent intent = createLocalBroadcastIntent(mContext, + RecentsUserEventProxyReceiver.ACTION_PROXY_HIDE_RECENTS_TO_USER); + intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab); + intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } + } + void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { if (mBootCompleted) { - if (isRecentsTopMost(getTopMostTask(), null)) { + ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); + if (topTask != null && mSystemServicesProxy.isRecentsTopMost(topTask, null)) { // Notify recents to hide itself - Intent intent = new Intent(ACTION_HIDE_RECENTS_ACTIVITY); - intent.setPackage(mContext.getPackageName()); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | - Intent.FLAG_RECEIVER_FOREGROUND); + Intent intent = createLocalBroadcastIntent(mContext, ACTION_HIDE_RECENTS_ACTIVITY); intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab); intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); @@ -166,9 +284,18 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } } - /** Toggles the alternate recents activity */ - public void onToggleRecents(View statusBarView) { - mStatusBarView = statusBarView; + /** Toggles the Recents activity. */ + @ProxyFromPrimaryToCurrentUser + public void onToggleRecents() { + if (mSystemServicesProxy.isForegroundUserOwner()) { + toggleRecents(); + } else { + Intent intent = createLocalBroadcastIntent(mContext, + RecentsUserEventProxyReceiver.ACTION_PROXY_TOGGLE_RECENTS_TO_USER); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } + } + void toggleRecents() { mTriggeredFromAltTab = false; try { @@ -178,8 +305,23 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } } + /** Preloads info for the Recents activity. */ + @ProxyFromPrimaryToCurrentUser public void onPreloadRecents() { - // Do nothing + if (mSystemServicesProxy.isForegroundUserOwner()) { + preloadRecents(); + } else { + Intent intent = createLocalBroadcastIntent(mContext, + RecentsUserEventProxyReceiver.ACTION_PROXY_PRELOAD_RECENTS_TO_USER); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } + } + void preloadRecents() { + // Preload only the raw task list into a new load plan (which will be consumed by the + // RecentsActivity) + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + sInstanceLoadPlan = loader.createLoadPlan(mContext); + sInstanceLoadPlan.preloadRawTasks(true); } public void onCancelPreloadingRecents() { @@ -188,12 +330,16 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta void showRelativeAffiliatedTask(boolean showNextTask) { RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); - TaskStack stack = loader.getTaskStack(mSystemServicesProxy, mContext.getResources(), - -1, -1, false, true, null, null); + RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); + loader.preloadTasks(plan, true /* isTopTaskHome */); + TaskStack stack = plan.getTaskStack(); + // Return early if there are no tasks if (stack.getTaskCount() == 0) return; - ActivityManager.RunningTaskInfo runningTask = getTopMostTask(); + ActivityManager.RunningTaskInfo runningTask = mSystemServicesProxy.getTopMostTask(); + // Return early if there is no running task (can't determine affiliated tasks in this case) + if (runningTask == null) return; // Return early if the running task is in the home stack (optimization) if (mSystemServicesProxy.isInHomeStack(runningTask.id)) return; @@ -202,6 +348,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta Task toTask = null; ActivityOptions launchOpts = null; int taskCount = tasks.size(); + int numAffiliatedTasks = 0; for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); if (task.key.id == runningTask.id) { @@ -221,16 +368,23 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta if (toTaskKey != null) { toTask = stack.findTaskWithId(toTaskKey.id); } + numAffiliatedTasks = group.getTaskCount(); break; } } // Return early if there is no next task if (toTask == null) { - if (showNextTask) { - // XXX: Show the next-task bounce animation - } else { - // XXX: Show the prev-task bounce animation + if (numAffiliatedTasks > 1) { + if (showNextTask) { + mSystemServicesProxy.startInPlaceAnimationOnFrontMostApplication( + ActivityOptions.makeCustomInPlaceAnimation(mContext, + R.anim.recents_launch_next_affiliated_task_bounce)); + } else { + mSystemServicesProxy.startInPlaceAnimationOnFrontMostApplication( + ActivityOptions.makeCustomInPlaceAnimation(mContext, + R.anim.recents_launch_prev_affiliated_task_bounce)); + } } return; } @@ -253,14 +407,26 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta showRelativeAffiliatedTask(false); } + /** Updates on configuration change. */ + @ProxyFromPrimaryToCurrentUser public void onConfigurationChanged(Configuration newConfig) { + if (mSystemServicesProxy.isForegroundUserOwner()) { + configurationChanged(); + } else { + Intent intent = createLocalBroadcastIntent(mContext, + RecentsUserEventProxyReceiver.ACTION_PROXY_CONFIG_CHANGE_TO_USER); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } + } + void configurationChanged() { + // Don't reuse task stack views if the configuration changes + mCanReuseTaskStackViews = false; // Reload the header bar layout - reloadHeaderBarLayout(); - sLastScreenshot = null; + reloadHeaderBarLayout(false); } /** Prepares the header bar layout. */ - void reloadHeaderBarLayout() { + void reloadHeaderBarLayout(boolean reloadWidget) { Resources res = mContext.getResources(); mWindowRect = mSystemServicesProxy.getWindowRect(); mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); @@ -268,6 +434,10 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta mNavBarWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_width); mConfig = RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy); mConfig.updateOnConfigurationChange(); + if (reloadWidget) { + // Reload the widget id before we get the task stack bounds + reloadSearchBarAppWidget(mContext, mSystemServicesProxy); + } mConfig.getTaskStackBounds(mWindowRect.width(), mWindowRect.height(), mStatusBarHeight, (mConfig.hasTransposedNavBar ? mNavBarWidth : 0), mTaskStackBounds); if (mConfig.isLandscape && mConfig.hasTransposedNavBar) { @@ -293,36 +463,22 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta mHeaderBar.layout(0, 0, taskViewSize.width(), taskBarHeight); } - /** Gets the top task. */ - ActivityManager.RunningTaskInfo getTopMostTask() { - SystemServicesProxy ssp = mSystemServicesProxy; - List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1); - if (!tasks.isEmpty()) { - return tasks.get(0); - } - return null; - } - - /** Returns whether the recents is currently running */ - boolean isRecentsTopMost(ActivityManager.RunningTaskInfo topTask, AtomicBoolean isHomeTopMost) { - SystemServicesProxy ssp = mSystemServicesProxy; - if (topTask != null) { - ComponentName topActivity = topTask.topActivity; - - // Check if the front most activity is recents - if (topActivity.getPackageName().equals(sRecentsPackage) && - topActivity.getClassName().equals(sRecentsActivity)) { - if (isHomeTopMost != null) { - isHomeTopMost.set(false); + /** Prepares the search bar app widget */ + void reloadSearchBarAppWidget(Context context, SystemServicesProxy ssp) { + // Try and pre-emptively bind the search widget on startup to ensure that we + // have the right thumbnail bounds to animate to. + if (Constants.DebugFlags.App.EnableSearchLayout) { + // If there is no id, then bind a new search app widget + if (mConfig.searchBarAppWidgetId < 0) { + AppWidgetHost host = new RecentsAppWidgetHost(context, + Constants.Values.App.AppWidgetHostId); + Pair<Integer, AppWidgetProviderInfo> widgetInfo = ssp.bindSearchAppWidget(host); + if (widgetInfo != null) { + // Save the app widget id into the settings + mConfig.updateSearchBarAppWidgetId(context, widgetInfo.first); } - return true; - } - - if (isHomeTopMost != null) { - isHomeTopMost.set(ssp.isInHomeStack(topTask.id)); } } - return false; } /** Toggles the recents activity */ @@ -330,22 +486,19 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta // If the user has toggled it too quickly, then just eat up the event here (it's better than // showing a janky screenshot). // NOTE: Ideally, the screenshot mechanism would take the window transform into account - if (System.currentTimeMillis() - mLastToggleTime < sMinToggleDelay) { + if ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) { return; } // If Recents is the front most activity, then we should just communicate with it directly // to launch the first task or dismiss itself - ActivityManager.RunningTaskInfo topTask = getTopMostTask(); - AtomicBoolean isTopTaskHome = new AtomicBoolean(); - if (isRecentsTopMost(topTask, isTopTaskHome)) { + ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); + AtomicBoolean isTopTaskHome = new AtomicBoolean(true); + if (topTask != null && mSystemServicesProxy.isRecentsTopMost(topTask, isTopTaskHome)) { // Notify recents to toggle itself - Intent intent = new Intent(ACTION_TOGGLE_RECENTS_ACTIVITY); - intent.setPackage(mContext.getPackageName()); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | - Intent.FLAG_RECEIVER_FOREGROUND); + Intent intent = createLocalBroadcastIntent(mContext, ACTION_TOGGLE_RECENTS_ACTIVITY); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); - mLastToggleTime = System.currentTimeMillis(); + mLastToggleTime = SystemClock.elapsedRealtime(); return; } else { // Otherwise, start the recents activity @@ -356,9 +509,9 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta /** Starts the recents activity if it is not already running */ void startRecentsActivity() { // Check if the top task is in the home stack, and start the recents activity - ActivityManager.RunningTaskInfo topTask = getTopMostTask(); - AtomicBoolean isTopTaskHome = new AtomicBoolean(); - if (!isRecentsTopMost(topTask, isTopTaskHome)) { + ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); + AtomicBoolean isTopTaskHome = new AtomicBoolean(true); + if (topTask == null || !mSystemServicesProxy.isRecentsTopMost(topTask, isTopTaskHome)) { startRecentsActivity(topTask, isTopTaskHome.get()); } } @@ -370,7 +523,8 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta mStartAnimationTriggered = false; return ActivityOptions.makeCustomAnimation(mContext, R.anim.recents_from_unknown_enter, - R.anim.recents_from_unknown_exit, mHandler, this); + R.anim.recents_from_unknown_exit, + mHandler, this); } /** @@ -381,37 +535,24 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta if (fromSearchHome) { return ActivityOptions.makeCustomAnimation(mContext, R.anim.recents_from_search_launcher_enter, - R.anim.recents_from_search_launcher_exit, mHandler, this); + R.anim.recents_from_search_launcher_exit, + mHandler, this); } return ActivityOptions.makeCustomAnimation(mContext, R.anim.recents_from_launcher_enter, - R.anim.recents_from_launcher_exit, mHandler, this); + R.anim.recents_from_launcher_exit, + mHandler, this); } /** - * Creates the activity options for an app->recents transition. If this method sets the static - * screenshot, then we will use that for the transition. + * Creates the activity options for an app->recents transition. */ ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask, - boolean isTopTaskHome) { - if (Constants.DebugFlags.App.EnableScreenshotAppTransition) { - // Recycle the last screenshot - consumeLastScreenshot(); - - // Take the full screenshot - sLastScreenshot = mSystemServicesProxy.takeAppScreenshot(); - if (sLastScreenshot != null) { - mStartAnimationTriggered = false; - return ActivityOptions.makeCustomAnimation(mContext, - R.anim.recents_from_app_enter, - R.anim.recents_from_app_exit, mHandler, this); - } - } - + TaskStack stack, TaskStackView stackView) { // Update the destination rect Task toTask = new Task(); - TaskViewTransform toTransform = getThumbnailTransitionTransform(topTask.id, isTopTaskHome, - toTask); + TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView, + topTask.id, toTask); if (toTransform != null && toTask.key != null) { Rect toTaskRect = toTransform.rect; int toHeaderWidth = (int) (mHeaderBar.getMeasuredWidth() * toTransform.scale); @@ -429,9 +570,9 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } mStartAnimationTriggered = false; - return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mStatusBarView, + return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView, thumbnail, toTaskRect.left, toTaskRect.top, toTaskRect.width(), - toTaskRect.height(), this); + toTaskRect.height(), mHandler, this); } // If both the screenshot and thumbnail fails, then just fall back to the default transition @@ -439,16 +580,8 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } /** Returns the transition rect for the given task id. */ - TaskViewTransform getThumbnailTransitionTransform(int runningTaskId, boolean isTopTaskHome, - Task runningTaskOut) { - // Get the stack of tasks that we are animating into - RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); - TaskStack stack = loader.getTaskStack(mSystemServicesProxy, mContext.getResources(), - runningTaskId, -1, false, isTopTaskHome, null, null); - if (stack.getTaskCount() == 0) { - return null; - } - + TaskViewTransform getThumbnailTransitionTransform(TaskStack stack, TaskStackView stackView, + int runningTaskId, Task runningTaskOut) { // Find the running task in the TaskStack Task task = null; ArrayList<Task> tasks = stack.getTasks(); @@ -470,34 +603,45 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } // Get the transform for the running task - mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome); - mDummyStackView.getScroller().setStackScrollToInitialState(); - mTmpTransform = mDummyStackView.getStackAlgorithm().getStackTransform(task, - mDummyStackView.getScroller().getStackScroll(), mTmpTransform, null); + stackView.getScroller().setStackScrollToInitialState(); + mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task, + stackView.getScroller().getStackScroll(), mTmpTransform, null); return mTmpTransform; } /** Starts the recents activity */ void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, boolean isTopTaskHome) { - // If Recents is not the front-most activity and we should animate into it. If - // the activity at the root of the top task stack in the home stack, then we just do a - // simple transition. Otherwise, we animate to the rects defined by the Recents service, - // which can differ depending on the number of items in the list. - SystemServicesProxy ssp = mSystemServicesProxy; - List<ActivityManager.RecentTaskInfo> recentTasks = - ssp.getRecentTasks(3, UserHandle.CURRENT.getIdentifier(), isTopTaskHome); - boolean useThumbnailTransition = !isTopTaskHome; - boolean hasRecentTasks = !recentTasks.isEmpty(); + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy); + + if (sInstanceLoadPlan == null) { + // Create a new load plan if onPreloadRecents() was never triggered + sInstanceLoadPlan = loader.createLoadPlan(mContext); + } + loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome); + TaskStack stack = sInstanceLoadPlan.getTaskStack(); + + // Prepare the dummy stack for the transition + mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome); + TaskStackViewLayoutAlgorithm.VisibilityReport stackVr = + mDummyStackView.computeStackVisibilityReport(); + boolean hasRecentTasks = stack.getTaskCount() > 0; + boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks; if (useThumbnailTransition) { + // Ensure that we load the running task's icon + RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); + launchOpts.runningTaskId = topTask.id; + launchOpts.loadThumbnails = false; + launchOpts.onlyLoadForCache = true; + loader.loadTasks(mContext, sInstanceLoadPlan, launchOpts); + // Try starting with a thumbnail transition - ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, isTopTaskHome); + ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, stack, + mDummyStackView); if (opts != null) { - if (sLastScreenshot != null) { - startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_FULL_SCREENSHOT); - } else { - startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_THUMBNAIL); - } + startAlternateRecentsActivity(topTask, opts, false /* fromHome */, + false /* fromSearchHome */, true /* fromThumbnail */, stackVr); } else { // Fall through below to the non-thumbnail transition useThumbnailTransition = false; @@ -530,48 +674,44 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } ActivityOptions opts = getHomeTransitionActivityOptions(fromSearchHome); - startAlternateRecentsActivity(topTask, opts, - fromSearchHome ? EXTRA_FROM_SEARCH_HOME : EXTRA_FROM_HOME); + startAlternateRecentsActivity(topTask, opts, true /* fromHome */, fromSearchHome, + false /* fromThumbnail */, stackVr); } else { // Otherwise we do the normal fade from an unknown source ActivityOptions opts = getUnknownTransitionActivityOptions(); - startAlternateRecentsActivity(topTask, opts, null); + startAlternateRecentsActivity(topTask, opts, true /* fromHome */, + false /* fromSearchHome */, false /* fromThumbnail */, stackVr); } } - mLastToggleTime = System.currentTimeMillis(); + mLastToggleTime = SystemClock.elapsedRealtime(); } /** Starts the recents activity */ void startAlternateRecentsActivity(ActivityManager.RunningTaskInfo topTask, - ActivityOptions opts, String extraFlag) { + ActivityOptions opts, boolean fromHome, boolean fromSearchHome, boolean fromThumbnail, + TaskStackViewLayoutAlgorithm.VisibilityReport vr) { + // Update the configuration based on the launch options + mConfig.launchedFromHome = fromSearchHome || fromHome; + mConfig.launchedFromSearchHome = fromSearchHome; + mConfig.launchedFromAppWithThumbnail = fromThumbnail; + mConfig.launchedToTaskId = (topTask != null) ? topTask.id : -1; + mConfig.launchedWithAltTab = mTriggeredFromAltTab; + mConfig.launchedReuseTaskStackViews = mCanReuseTaskStackViews; + mConfig.launchedNumVisibleTasks = vr.numVisibleTasks; + mConfig.launchedNumVisibleThumbnails = vr.numVisibleThumbnails; + mConfig.launchedHasConfigurationChanged = false; + Intent intent = new Intent(sToggleRecentsAction); intent.setClassName(sRecentsPackage, sRecentsActivity); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | Intent.FLAG_ACTIVITY_TASK_ON_HOME); - if (extraFlag != null) { - intent.putExtra(extraFlag, true); - } - intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, mTriggeredFromAltTab); - intent.putExtra(EXTRA_FROM_TASK_ID, (topTask != null) ? topTask.id : -1); if (opts != null) { mContext.startActivityAsUser(intent, opts.toBundle(), UserHandle.CURRENT); } else { mContext.startActivityAsUser(intent, UserHandle.CURRENT); } - } - - /** Returns the last screenshot taken, this will be called by the RecentsActivity. */ - public static Bitmap getLastScreenshot() { - return sLastScreenshot; - } - - /** Recycles the last screenshot taken, this will be called by the RecentsActivity. */ - public static void consumeLastScreenshot() { - if (sLastScreenshot != null) { - sLastScreenshot.recycle(); - sLastScreenshot = null; - } + mCanReuseTaskStackViews = true; } /** Sets the RecentsComponent callbacks. */ @@ -580,12 +720,33 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } /** Notifies the callbacks that the visibility of Recents has changed. */ - public static void notifyVisibilityChanged(boolean visible) { + @ProxyFromAnyToPrimaryUser + public static void notifyVisibilityChanged(Context context, SystemServicesProxy ssp, + boolean visible) { + if (ssp.isForegroundUserOwner()) { + visibilityChanged(visible); + } else { + Intent intent = createLocalBroadcastIntent(context, + ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER); + intent.putExtra(EXTRA_RECENTS_VISIBILITY, visible); + context.sendBroadcastAsUser(intent, UserHandle.OWNER); + } + } + static void visibilityChanged(boolean visible) { if (sRecentsComponentCallbacks != null) { sRecentsComponentCallbacks.onVisibilityChanged(visible); } } + /** + * Returns the preloaded load plan and invalidates it. + */ + public static RecentsTaskLoadPlan consumeInstanceLoadPlan() { + RecentsTaskLoadPlan plan = sInstanceLoadPlan; + sInstanceLoadPlan = null; + return plan; + } + /**** OnAnimationStartedListener Implementation ****/ @Override @@ -610,15 +771,12 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta public void run() { onAnimationStarted(); } - }, 75); + }, 25); } }; // Send the broadcast to notify Recents that the animation has started - Intent intent = new Intent(ACTION_START_ENTER_ANIMATION); - intent.setPackage(mContext.getPackageName()); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | - Intent.FLAG_RECEIVER_FOREGROUND); + Intent intent = createLocalBroadcastIntent(mContext, ACTION_START_ENTER_ANIMATION); mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null, fallbackReceiver, null, Activity.RESULT_CANCELED, null, null); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java index 85cf077..0a1718d 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/Constants.java +++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java @@ -25,8 +25,6 @@ public class Constants { public static final boolean Verbose = false; public static class App { - // Enables the screenshot app->Recents transition - public static final boolean EnableScreenshotAppTransition = false; // Enables debug drawing for the transition thumbnail public static final boolean EnableTransitionThumbnailDebugMode = false; // Enables the filtering of tasks according to their grouping @@ -66,13 +64,9 @@ public class Constants { public static String DebugModeVersion = "A"; } - public static class RecentsTaskLoader { - // XXX: This should be calculated on the first load - public static final int PreloadFirstTasksCount = 6; - } - public static class TaskStackView { - public static final int TaskStackOverscrollRange = 150; + public static final int TaskStackMinOverscrollRange = 32; + public static final int TaskStackMaxOverscrollRange = 128; public static final int FilterStartDelay = 25; } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java index 01ba5a2..cb1baeb 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -28,17 +28,20 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Bundle; +import android.os.SystemClock; import android.os.UserHandle; import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.ViewStub; import android.widget.Toast; + import com.android.systemui.R; import com.android.systemui.recents.misc.DebugTrigger; import com.android.systemui.recents.misc.ReferenceCountedTrigger; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.Utilities; +import com.android.systemui.recents.model.RecentsTaskLoadPlan; import com.android.systemui.recents.model.RecentsTaskLoader; import com.android.systemui.recents.model.SpaceNode; import com.android.systemui.recents.model.Task; @@ -47,6 +50,8 @@ import com.android.systemui.recents.views.DebugOverlayView; import com.android.systemui.recents.views.RecentsView; import com.android.systemui.recents.views.SystemBarScrimViews; import com.android.systemui.recents.views.ViewAnimation; +import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.SystemUIApplication; import java.lang.ref.WeakReference; import java.lang.reflect.InvocationTargetException; @@ -60,7 +65,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView DebugOverlayView.DebugOverlayViewCallbacks { RecentsConfiguration mConfig; - boolean mVisible; long mLastTabKeyEventTime; // Top level views @@ -79,6 +83,8 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView // Runnables to finish the Recents activity FinishRecentsRunnable mFinishLaunchHomeRunnable; + private PhoneStatusBar mStatusBar; + /** * A common Runnable to finish Recents either by calling finish() (with a custom animation) or * launching Home with some ActivityOptions. Generally we always launch home when we exit @@ -101,9 +107,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView @Override public void run() { - // Mark Recents as no longer visible - AlternateRecentsComponent.notifyVisibilityChanged(false); - mVisible = false; // Finish Recents if (mLaunchIntent != null) { if (mLaunchOpts != null) { @@ -127,8 +130,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(AlternateRecentsComponent.ACTION_HIDE_RECENTS_ACTIVITY)) { - // Mark Recents as no longer visible - AlternateRecentsComponent.notifyVisibilityChanged(false); if (intent.getBooleanExtra(AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_ALT_TAB, false)) { // If we are hiding from releasing Alt-Tab, dismiss Recents to the focused app dismissRecentsToFocusedTaskOrHome(false); @@ -142,9 +143,7 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView // If we are toggling Recents, then first unfilter any filtered stacks first dismissRecentsToFocusedTaskOrHome(true); } else if (action.equals(AlternateRecentsComponent.ACTION_START_ENTER_ANIMATION)) { - // Try and start the enter animation (or restart it on configuration changed) - ReferenceCountedTrigger t = new ReferenceCountedTrigger(context, null, null, null); - mRecentsView.startEnterRecentsAnimation(new ViewAnimation.TaskViewEnterContext(t)); + // Trigger the enter animation onEnterAnimationTriggered(); // Notify the fallback receiver that we have successfully got the broadcast // See AlternateRecentsComponent.onAnimationStarted() @@ -182,30 +181,31 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView /** Updates the set of recent tasks */ void updateRecentsTasks(Intent launchIntent) { - // Update the configuration based on the launch intent - boolean fromSearchHome = launchIntent.getBooleanExtra( - AlternateRecentsComponent.EXTRA_FROM_SEARCH_HOME, false); - mConfig.launchedFromHome = fromSearchHome || launchIntent.getBooleanExtra( - AlternateRecentsComponent.EXTRA_FROM_HOME, false); - mConfig.launchedFromAppWithThumbnail = launchIntent.getBooleanExtra( - AlternateRecentsComponent.EXTRA_FROM_APP_THUMBNAIL, false); - mConfig.launchedFromAppWithScreenshot = launchIntent.getBooleanExtra( - AlternateRecentsComponent.EXTRA_FROM_APP_FULL_SCREENSHOT, false); - mConfig.launchedToTaskId = launchIntent.getIntExtra( - AlternateRecentsComponent.EXTRA_FROM_TASK_ID, -1); - mConfig.launchedWithAltTab = launchIntent.getBooleanExtra( - AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_ALT_TAB, false); - - // Load all the tasks + // If AlternateRecentsComponent has preloaded a load plan, then use that to prevent + // reconstructing the task stack RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); - SpaceNode root = loader.reload(this, - Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount, - mConfig.launchedFromHome); + RecentsTaskLoadPlan plan = AlternateRecentsComponent.consumeInstanceLoadPlan(); + if (plan == null) { + plan = loader.createLoadPlan(this); + } + + // Start loading tasks according to the load plan + if (plan.getTaskStack() == null) { + loader.preloadTasks(plan, mConfig.launchedFromHome); + } + RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options(); + loadOpts.runningTaskId = mConfig.launchedToTaskId; + loadOpts.numVisibleTasks = mConfig.launchedNumVisibleTasks; + loadOpts.numVisibleTaskThumbnails = mConfig.launchedNumVisibleThumbnails; + loader.loadTasks(this, plan, loadOpts); + + SpaceNode root = plan.getSpaceNode(); ArrayList<TaskStack> stacks = root.getStacks(); - if (!stacks.isEmpty()) { - mRecentsView.setTaskStacks(root.getStacks()); + boolean hasTasks = root.hasTasks(); + if (hasTasks) { + mRecentsView.setTaskStacks(stacks); } - mConfig.launchedWithNoRecentTasks = !root.hasTasks(); + mConfig.launchedWithNoRecentTasks = !hasTasks; // Create the home intent runnable Intent homeIntent = new Intent(Intent.ACTION_MAIN, null); @@ -214,9 +214,9 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); mFinishLaunchHomeRunnable = new FinishRecentsRunnable(homeIntent, ActivityOptions.makeCustomAnimation(this, - fromSearchHome ? R.anim.recents_to_search_launcher_enter : + mConfig.launchedFromSearchHome ? R.anim.recents_to_search_launcher_enter : R.anim.recents_to_launcher_enter, - fromSearchHome ? R.anim.recents_to_search_launcher_exit : + mConfig.launchedFromSearchHome ? R.anim.recents_to_search_launcher_exit : R.anim.recents_to_launcher_exit)); // Mark the task that is the launch target @@ -314,7 +314,8 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView /** Dismisses recents if we are already visible and the intent is to toggle the recents view */ boolean dismissRecentsToFocusedTaskOrHome(boolean checkFilteredStackState) { - if (mVisible) { + SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); + if (ssp.isRecentsTopMost(ssp.getTopMostTask(), null)) { // If we currently have filtered stacks, then unfilter those first if (checkFilteredStackState && mRecentsView.unfilterFilteredStacks()) return true; @@ -348,7 +349,8 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView /** Dismisses Recents directly to Home if we currently aren't transitioning. */ boolean dismissRecentsToHome(boolean animated) { - if (mVisible) { + SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); + if (ssp.isRecentsTopMost(ssp.getTopMostTask(), null)) { // Return to Home dismissRecentsToHomeRaw(animated); return true; @@ -360,12 +362,11 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // For the non-primary user, ensure that the SystemSericesProxy is initialized + // For the non-primary user, ensure that the SystemServicesProxy and configuration is + // initialized RecentsTaskLoader.initialize(this); - - // Initialize the loader and the configuration - mConfig = RecentsConfiguration.reinitialize(this, - RecentsTaskLoader.getInstance().getSystemServicesProxy()); + SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); + mConfig = RecentsConfiguration.reinitialize(this, ssp); // Initialize the widget host (the host id is static and does not change) mAppWidgetHost = new RecentsAppWidgetHost(this, Constants.Values.App.AppWidgetHostId); @@ -380,12 +381,12 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView mEmptyViewStub = (ViewStub) findViewById(R.id.empty_view_stub); mDebugOverlayStub = (ViewStub) findViewById(R.id.debug_overlay_stub); mScrimViews = new SystemBarScrimViews(this, mConfig); + mStatusBar = ((SystemUIApplication) getApplication()) + .getComponent(PhoneStatusBar.class); inflateDebugOverlay(); // Bind the search app widget when we first start up bindSearchBarAppWidget(); - // Update the recent tasks - updateRecentsTasks(getIntent()); // Register the broadcast receiver to handle messages when the screen is turned off IntentFilter filter = new IntentFilter(); @@ -401,32 +402,12 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView } catch (InvocationTargetException e) { e.printStackTrace(); } - - // Update if we are getting a configuration change - if (savedInstanceState != null) { - mConfig.updateOnConfigurationChange(); - onConfigurationChange(); - } - - // Start listening for widget package changes if there is one bound, post it since we don't - // want it stalling the startup - if (mConfig.searchBarAppWidgetId >= 0) { - final WeakReference<RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks> callback = - new WeakReference<RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks>(this); - mRecentsView.post(new Runnable() { - @Override - public void run() { - RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks cb = callback.get(); - if (cb != null) { - mAppWidgetHost.startListening(cb); - } - } - }); - } } /** Inflates the debug overlay if debug mode is enabled. */ void inflateDebugOverlay() { + if (!Constants.DebugFlags.App.EnableDebugMode) return; + if (mConfig.debugModeEnabled && mDebugOverlay == null) { // Inflate the overlay and seek bars mDebugOverlay = (DebugOverlayView) mDebugOverlayStub.inflate(); @@ -435,37 +416,23 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView } } - void onConfigurationChange() { - // Update RecentsConfiguration - mConfig = RecentsConfiguration.reinitialize(this, - RecentsTaskLoader.getInstance().getSystemServicesProxy()); - - // Try and start the enter animation (or restart it on configuration changed) - ReferenceCountedTrigger t = new ReferenceCountedTrigger(this, null, null, null); - mRecentsView.startEnterRecentsAnimation(new ViewAnimation.TaskViewEnterContext(t)); - onEnterAnimationTriggered(); - } - @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); - // Reinitialize the configuration - RecentsConfiguration.reinitialize(this, RecentsTaskLoader.getInstance().getSystemServicesProxy()); - // Clear any debug rects if (mDebugOverlay != null) { mDebugOverlay.clear(); } - - // Update the recent tasks - updateRecentsTasks(intent); } @Override protected void onStart() { super.onStart(); + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + SystemServicesProxy ssp = loader.getSystemServicesProxy(); + AlternateRecentsComponent.notifyVisibilityChanged(this, ssp, true); // Register the broadcast receiver to handle messages from our service IntentFilter filter = new IntentFilter(); @@ -475,29 +442,33 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView registerReceiver(mServiceBroadcastReceiver, filter); // Register any broadcast receivers for the task loader - RecentsTaskLoader.getInstance().registerReceivers(this, mRecentsView); - } + loader.registerReceivers(this, mRecentsView); - @Override - protected void onResume() { - super.onResume(); + // Update the recent tasks + updateRecentsTasks(getIntent()); - // Mark Recents as visible - mVisible = true; + // If this is a new instance from a configuration change, then we have to manually trigger + // the enter animation state + if (mConfig.launchedHasConfigurationChanged) { + onEnterAnimationTriggered(); + } } @Override protected void onStop() { super.onStop(); + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + SystemServicesProxy ssp = loader.getSystemServicesProxy(); + AlternateRecentsComponent.notifyVisibilityChanged(this, ssp, false); - // Remove all the views - mRecentsView.removeAllTaskStacks(); + // Notify the views that we are no longer visible + mRecentsView.onRecentsHidden(); // Unregister the RecentsService receiver unregisterReceiver(mServiceBroadcastReceiver); // Unregister any broadcast receivers for the task loader - RecentsTaskLoader.getInstance().unregisterReceivers(); + loader.unregisterReceivers(); } @Override @@ -508,9 +479,32 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView unregisterReceiver(mSystemBroadcastReceiver); // Stop listening for widget package changes if there was one bound - if (mAppWidgetHost.isListening()) { - mAppWidgetHost.stopListening(); + mAppWidgetHost.stopListening(); + } + + public void onEnterAnimationTriggered() { + // Try and start the enter animation (or restart it on configuration changed) + ReferenceCountedTrigger t = new ReferenceCountedTrigger(this, null, null, null); + ViewAnimation.TaskViewEnterContext ctx = new ViewAnimation.TaskViewEnterContext(t); + mRecentsView.startEnterRecentsAnimation(ctx); + if (mConfig.searchBarAppWidgetId >= 0) { + final WeakReference<RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks> cbRef = + new WeakReference<RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks>( + RecentsActivity.this); + ctx.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { + @Override + public void run() { + // Start listening for widget package changes if there is one bound + RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks cb = cbRef.get(); + if (cb != null) { + mAppWidgetHost.startListening(cb); + } + } + }); } + + // Animate the SystemUI scrim views + mScrimViews.startEnterRecentsAnimation(); } @Override @@ -525,13 +519,13 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_TAB: { - boolean hasRepKeyTimeElapsed = (System.currentTimeMillis() - + boolean hasRepKeyTimeElapsed = (SystemClock.elapsedRealtime() - mLastTabKeyEventTime) > mConfig.altTabKeyDelay; if (event.getRepeatCount() <= 0 || hasRepKeyTimeElapsed) { // Focus the next task in the stack final boolean backward = event.isShiftPressed(); mRecentsView.focusNextTask(!backward); - mLastTabKeyEventTime = System.currentTimeMillis(); + mLastTabKeyEventTime = SystemClock.elapsedRealtime(); } return true; } @@ -572,7 +566,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView /** Called when debug mode is triggered */ public void onDebugModeTriggered() { - if (mConfig.developerOptionsEnabled) { SharedPreferences settings = getSharedPreferences(getPackageName(), 0); if (settings.getBoolean(Constants.Values.App.Key_DebugModeEnabled, false)) { @@ -580,13 +573,17 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView settings.edit().remove(Constants.Values.App.Key_DebugModeEnabled).apply(); mConfig.debugModeEnabled = false; inflateDebugOverlay(); - mDebugOverlay.disable(); + if (mDebugOverlay != null) { + mDebugOverlay.disable(); + } } else { // Enable the debug mode settings.edit().putBoolean(Constants.Values.App.Key_DebugModeEnabled, true).apply(); mConfig.debugModeEnabled = true; inflateDebugOverlay(); - mDebugOverlay.enable(); + if (mDebugOverlay != null) { + mDebugOverlay.enable(); + } } Toast.makeText(this, "Debug mode (" + Constants.Values.App.DebugModeVersion + ") " + (mConfig.debugModeEnabled ? "Enabled" : "Disabled") + ", please restart Recents now", @@ -594,12 +591,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView } } - /** Called when the enter recents animation is triggered. */ - public void onEnterAnimationTriggered() { - // Animate the SystemUI scrim views - mScrimViews.startEnterRecentsAnimation(); - } - /**** RecentsView.RecentsViewCallbacks Implementation ****/ @Override @@ -610,9 +601,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView @Override public void onTaskViewClicked() { - // Mark recents as no longer visible - AlternateRecentsComponent.notifyVisibilityChanged(false); - mVisible = false; } @Override @@ -626,6 +614,13 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView mFinishLaunchHomeRunnable.run(); } + @Override + public void onScreenPinningRequest() { + if (mStatusBar != null) { + mStatusBar.showScreenPinningRequest(false); + } + } + /**** RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks Implementation ****/ @Override diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java index a63e167..5bae37a 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java @@ -43,23 +43,23 @@ public class RecentsAppWidgetHost extends AppWidgetHost { public void startListening(RecentsAppWidgetHostCallbacks cb) { mCb = cb; - mIsListening = true; - super.startListening(); + if (!mIsListening) { + mIsListening = true; + super.startListening(); + } } @Override public void stopListening() { - super.stopListening(); + if (mIsListening) { + super.stopListening(); + } // Ensure that we release any references to the callbacks mCb = null; mContext = null; mIsListening = false; } - public boolean isListening() { - return mIsListening; - } - @Override protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidgetInfo) { if (mCb == null) return; diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java index 2aca576..52e7e7f 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java @@ -38,6 +38,18 @@ public class RecentsConfiguration { static RecentsConfiguration sInstance; static int sPrevConfigurationHashCode; + /** Levels of svelte in increasing severity/austerity. */ + // No svelting. + public static final int SVELTE_NONE = 0; + // Limit thumbnail cache to number of visible thumbnails when Recents was loaded, disable + // caching thumbnails as you scroll. + public static final int SVELTE_LIMIT_CACHE = 1; + // Disable the thumbnail cache, load thumbnails asynchronously when the activity loads and + // evict all thumbnails when hidden. + public static final int SVELTE_DISABLE_CACHE = 2; + // Disable all thumbnail loading. + public static final int SVELTE_DISABLE_LOADING = 3; + /** Animations */ public float animationPxMovementPerSecond; @@ -74,11 +86,15 @@ public class RecentsConfiguration { public float taskStackWidthPaddingPct; public float taskStackOverscrollPct; + /** Transitions */ + public int transitionEnterFromAppDelay; + public int transitionEnterFromHomeDelay; + /** Task view animation and styles */ - public int taskViewEnterFromHomeDelay; + public int taskViewEnterFromAppDuration; public int taskViewEnterFromHomeDuration; public int taskViewEnterFromHomeStaggerDelay; - public int taskViewEnterFromHomeStaggerDuration; + public int taskViewExitToAppDuration; public int taskViewExitToHomeDuration; public int taskViewRemoveAnimDuration; public int taskViewRemoveAnimTranslationXPx; @@ -98,16 +114,8 @@ public class RecentsConfiguration { /** Task bar size & animations */ public int taskBarHeight; - public int taskBarEnterAnimDuration; - public int taskBarEnterAnimDelay; - public int taskBarExitAnimDuration; public int taskBarDismissDozeDelaySeconds; - /** Lock to app */ - public int taskViewLockToAppButtonHeight; - public int taskViewLockToAppShortAnimDuration; - public int taskViewLockToAppLongAnimDuration; - /** Nav bar scrim */ public int navBarScrimEnterDuration; @@ -115,9 +123,13 @@ public class RecentsConfiguration { public boolean launchedWithAltTab; public boolean launchedWithNoRecentTasks; public boolean launchedFromAppWithThumbnail; - public boolean launchedFromAppWithScreenshot; public boolean launchedFromHome; + public boolean launchedFromSearchHome; + public boolean launchedReuseTaskStackViews; + public boolean launchedHasConfigurationChanged; public int launchedToTaskId; + public int launchedNumVisibleTasks; + public int launchedNumVisibleThumbnails; /** Misc **/ public boolean useHardwareLayers; @@ -128,6 +140,7 @@ public class RecentsConfiguration { public boolean lockToAppEnabled; public boolean developerOptionsEnabled; public boolean debugModeEnabled; + public int svelteLevel; /** Private constructor */ private RecentsConfiguration(Context context) { @@ -213,15 +226,23 @@ public class RecentsConfiguration { taskStackMaxDim = res.getInteger(R.integer.recents_max_task_stack_view_dim); taskStackTopPaddingPx = res.getDimensionPixelSize(R.dimen.recents_stack_top_padding); + // Transition + transitionEnterFromAppDelay = + res.getInteger(R.integer.recents_enter_from_app_transition_duration); + transitionEnterFromHomeDelay = + res.getInteger(R.integer.recents_enter_from_home_transition_duration); + // Task view animation and styles - taskViewEnterFromHomeDelay = - res.getInteger(R.integer.recents_animate_task_enter_from_home_delay); + taskViewEnterFromAppDuration = + res.getInteger(R.integer.recents_task_enter_from_app_duration); taskViewEnterFromHomeDuration = - res.getInteger(R.integer.recents_animate_task_enter_from_home_duration); + res.getInteger(R.integer.recents_task_enter_from_home_duration); taskViewEnterFromHomeStaggerDelay = - res.getInteger(R.integer.recents_animate_task_enter_from_home_stagger_delay); + res.getInteger(R.integer.recents_task_enter_from_home_stagger_delay); + taskViewExitToAppDuration = + res.getInteger(R.integer.recents_task_exit_to_app_duration); taskViewExitToHomeDuration = - res.getInteger(R.integer.recents_animate_task_exit_to_home_duration); + res.getInteger(R.integer.recents_task_exit_to_home_duration); taskViewRemoveAnimDuration = res.getInteger(R.integer.recents_animate_task_view_remove_duration); taskViewRemoveAnimTranslationXPx = @@ -252,23 +273,9 @@ public class RecentsConfiguration { // Task bar size & animations taskBarHeight = res.getDimensionPixelSize(R.dimen.recents_task_bar_height); - taskBarEnterAnimDuration = - res.getInteger(R.integer.recents_animate_task_bar_enter_duration); - taskBarEnterAnimDelay = - res.getInteger(R.integer.recents_animate_task_bar_enter_delay); - taskBarExitAnimDuration = - res.getInteger(R.integer.recents_animate_task_bar_exit_duration); taskBarDismissDozeDelaySeconds = res.getInteger(R.integer.recents_task_bar_dismiss_delay_seconds); - // Lock to app - taskViewLockToAppButtonHeight = - res.getDimensionPixelSize(R.dimen.recents_task_view_lock_to_app_button_height); - taskViewLockToAppShortAnimDuration = - res.getInteger(R.integer.recents_animate_lock_to_app_button_short_duration); - taskViewLockToAppLongAnimDuration = - res.getInteger(R.integer.recents_animate_lock_to_app_button_long_duration); - // Nav bar scrim navBarScrimEnterDuration = res.getInteger(R.integer.recents_nav_bar_scrim_enter_duration); @@ -277,6 +284,7 @@ public class RecentsConfiguration { useHardwareLayers = res.getBoolean(R.bool.config_recents_use_hardware_layers); altTabKeyDelay = res.getInteger(R.integer.recents_alt_tab_key_delay); fakeShadows = res.getBoolean(R.bool.config_recents_fake_shadows); + svelteLevel = res.getInteger(R.integer.recents_svelte_level); } /** Updates the system insets */ @@ -304,12 +312,10 @@ public class RecentsConfiguration { /** Called when the configuration has changed, and we want to reset any configuration specific * members. */ public void updateOnConfigurationChange() { - launchedWithAltTab = false; - launchedWithNoRecentTasks = false; - launchedFromAppWithThumbnail = false; - launchedFromAppWithScreenshot = false; - launchedFromHome = false; - launchedToTaskId = -1; + // Reset this flag on configuration change to ensure that we recreate new task views + launchedReuseTaskStackViews = false; + // Set this flag to indicate that the configuration has changed since Recents last launched + launchedHasConfigurationChanged = true; } /** Returns whether the search bar app widget exists. */ diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsUserEventProxyReceiver.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsUserEventProxyReceiver.java new file mode 100644 index 0000000..236da5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsUserEventProxyReceiver.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2014 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.recents; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import com.android.systemui.recent.Recents; + + +/** + * A proxy for Recents events which happens strictly for non-owner users. + */ +public class RecentsUserEventProxyReceiver extends BroadcastReceiver { + final public static String ACTION_PROXY_SHOW_RECENTS_TO_USER = + "com.android.systemui.recents.action.SHOW_RECENTS_FOR_USER"; + final public static String ACTION_PROXY_HIDE_RECENTS_TO_USER = + "com.android.systemui.recents.action.HIDE_RECENTS_FOR_USER"; + final public static String ACTION_PROXY_TOGGLE_RECENTS_TO_USER = + "com.android.systemui.recents.action.TOGGLE_RECENTS_FOR_USER"; + final public static String ACTION_PROXY_PRELOAD_RECENTS_TO_USER = + "com.android.systemui.recents.action.PRELOAD_RECENTS_FOR_USER"; + final public static String ACTION_PROXY_CONFIG_CHANGE_TO_USER = + "com.android.systemui.recents.action.CONFIG_CHANGED_FOR_USER"; + + @Override + public void onReceive(Context context, Intent intent) { + AlternateRecentsComponent recents = Recents.getRecentsComponent( + context.getApplicationContext(), true); + switch (intent.getAction()) { + case ACTION_PROXY_SHOW_RECENTS_TO_USER: { + boolean triggeredFromAltTab = intent.getBooleanExtra( + AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_ALT_TAB, false); + recents.showRecents(triggeredFromAltTab); + break; + } + case ACTION_PROXY_HIDE_RECENTS_TO_USER: { + boolean triggeredFromAltTab = intent.getBooleanExtra( + AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_ALT_TAB, false); + boolean triggeredFromHome = intent.getBooleanExtra( + AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_HOME_KEY, false); + recents.hideRecents(triggeredFromAltTab, triggeredFromHome); + break; + } + case ACTION_PROXY_TOGGLE_RECENTS_TO_USER: + recents.toggleRecents(); + break; + case ACTION_PROXY_PRELOAD_RECENTS_TO_USER: + recents.preloadRecents(); + break; + case ACTION_PROXY_CONFIG_CHANGE_TO_USER: + recents.configurationChanged(); + break; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/DozeTrigger.java b/packages/SystemUI/src/com/android/systemui/recents/misc/DozeTrigger.java index 4456066..735f79f 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/DozeTrigger.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/DozeTrigger.java @@ -82,4 +82,9 @@ public class DozeTrigger { public boolean hasTriggered() { return mHasTriggered; } + + /** Resets the doze trigger state. */ + public void resetTrigger() { + mHasTriggered = false; + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java index 71a3ef1..90b099c 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java @@ -21,6 +21,7 @@ import android.app.ActivityManagerNative; import android.app.ActivityOptions; import android.app.AppGlobals; import android.app.IActivityManager; +import android.app.ITaskStackListener; import android.app.SearchManager; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; @@ -58,6 +59,7 @@ import android.view.SurfaceControl; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import com.android.systemui.R; +import com.android.systemui.recents.AlternateRecentsComponent; import com.android.systemui.recents.Constants; import java.io.IOException; @@ -65,6 +67,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; /** * Acts as a shim around the real system services that we need to access data from, and provides @@ -216,6 +219,37 @@ public class SystemServicesProxy { return mAm.getRunningTasks(numTasks); } + /** Returns the top task. */ + public ActivityManager.RunningTaskInfo getTopMostTask() { + List<ActivityManager.RunningTaskInfo> tasks = getRunningTasks(1); + if (!tasks.isEmpty()) { + return tasks.get(0); + } + return null; + } + + /** Returns whether the recents is currently running */ + public boolean isRecentsTopMost(ActivityManager.RunningTaskInfo topTask, + AtomicBoolean isHomeTopMost) { + if (topTask != null) { + ComponentName topActivity = topTask.topActivity; + + // Check if the front most activity is recents + if (topActivity.getPackageName().equals(AlternateRecentsComponent.sRecentsPackage) && + topActivity.getClassName().equals(AlternateRecentsComponent.sRecentsActivity)) { + if (isHomeTopMost != null) { + isHomeTopMost.set(false); + } + return true; + } + + if (isHomeTopMost != null) { + isHomeTopMost.set(isInHomeStack(topTask.id)); + } + } + return false; + } + /** Returns whether the specified task is in the home stack */ public boolean isInHomeStack(int taskId) { if (mAm == null) return false; @@ -242,6 +276,7 @@ public class SystemServicesProxy { Bitmap thumbnail = SystemServicesProxy.getThumbnail(mAm, taskId); if (thumbnail != null) { + thumbnail.setHasAlpha(false); // We use a dumb heuristic for now, if the thumbnail is purely transparent in the top // left pixel, then assume the whole thumbnail is transparent. Generally, proper // screenshots are always composed onto a bitmap that has no alpha. @@ -291,18 +326,18 @@ public class SystemServicesProxy { } } - /** Removes the task and kills the process */ - public void removeTask(int taskId, boolean isDocument) { + /** Removes the task */ + public void removeTask(int taskId) { if (mAm == null) return; if (Constants.DebugFlags.App.EnableSystemServicesProxy) return; - // Remove the task, and only kill the process if it is not a document - mAm.removeTask(taskId, isDocument ? 0 : ActivityManager.REMOVE_TASK_KILL_PROCESS); + // Remove the task. + mAm.removeTask(taskId); } /** * Returns the activity info for a given component name. - * + * * @param cn The component name of the activity. * @param userId The userId of the user that this is for. */ @@ -392,6 +427,15 @@ public class SystemServicesProxy { } /** + * Returns whether the foreground user is the owner. + */ + public boolean isForegroundUserOwner() { + if (mAm == null) return false; + + return mAm.getCurrentUser() == UserHandle.USER_OWNER; + } + + /** * Resolves and returns the first Recents widget from the same package as the global * assist activity. */ @@ -429,6 +473,7 @@ public class SystemServicesProxy { opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); if (!mAwm.bindAppWidgetIdIfAllowed(searchWidgetId, searchWidgetInfo.provider, opts)) { + host.deleteAppWidgetId(searchWidgetId); return null; } return new Pair<Integer, AppWidgetProviderInfo>(searchWidgetId, searchWidgetInfo); @@ -492,17 +537,6 @@ public class SystemServicesProxy { } /** - * Locks the current task. - */ - public void lockCurrentTask() { - if (mIam == null) return; - - try { - mIam.startLockTaskModeOnCurrent(); - } catch (RemoteException e) {} - } - - /** * Takes a screenshot of the current surface. */ public Bitmap takeScreenshot() { @@ -532,4 +566,26 @@ public class SystemServicesProxy { } return false; } + + /** Starts an in-place animation on the front most application windows. */ + public void startInPlaceAnimationOnFrontMostApplication(ActivityOptions opts) { + if (mIam == null) return; + + try { + mIam.startInPlaceAnimationOnFrontMostApplication(opts); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** Registers a task stack listener with the system. */ + public void registerTaskStackListener(ITaskStackListener listener) { + if (mIam == null) return; + + try { + mIam.registerTaskStackListener(listener); + } catch (Exception e) { + e.printStackTrace(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java index f01d17c..e1179fa 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java @@ -16,7 +16,7 @@ package com.android.systemui.recents.misc; -import android.content.Intent; +import android.animation.Animator; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; @@ -183,9 +183,14 @@ public class Utilities { sPropertyMethod.invoke(null, property, value); } - /** Returns whether the specified intent is a document. */ - public static boolean isDocument(Intent intent) { - int flags = intent.getFlags(); - return (flags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) == Intent.FLAG_ACTIVITY_NEW_DOCUMENT; + /** + * Cancels an animation ensuring that if it has listeners, onCancel and onEnd + * are not called. + */ + public static void cancelAnimationWithoutCallbacks(Animator animator) { + if (animator != null) { + animator.removeAllListeners(); + animator.cancel(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/KeyStoreLruCache.java b/packages/SystemUI/src/com/android/systemui/recents/model/KeyStoreLruCache.java index 7ccefc6..97e0916 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/KeyStoreLruCache.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/KeyStoreLruCache.java @@ -21,15 +21,16 @@ import android.util.LruCache; import java.util.HashMap; /** - * An LRU cache that support querying the keys as well as values. By using the Task's key, we can - * prevent holding onto a reference to the Task resource data, while keeping the cache data in - * memory where necessary. + * An LRU cache that internally support querying the keys as well as values. We use this to keep + * track of the task metadata to determine when to invalidate the cache when tasks have been + * updated. Generally, this cache will return the last known cache value for the requested task + * key. */ public class KeyStoreLruCache<V> { // We keep a set of keys that are associated with the LRU cache, so that we can find out // information about the Task that was previously in the cache. HashMap<Integer, Task.TaskKey> mTaskKeys = new HashMap<Integer, Task.TaskKey>(); - // The cache implementation + // The cache implementation, mapping task id -> value LruCache<Integer, V> mCache; public KeyStoreLruCache(int cacheSize) { diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsPackageMonitor.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsPackageMonitor.java index 60e89bf..e48e5f0 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsPackageMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsPackageMonitor.java @@ -19,6 +19,7 @@ package com.android.systemui.recents.model; import android.content.ComponentName; import android.content.Context; import android.os.Looper; +import android.os.UserHandle; import com.android.internal.content.PackageMonitor; import com.android.systemui.recents.misc.SystemServicesProxy; @@ -26,16 +27,16 @@ import java.util.HashSet; import java.util.List; /** - * The package monitor listens for changes from PackageManager to update the contents of the Recents - * list. + * The package monitor listens for changes from PackageManager to update the contents of the + * Recents list. */ public class RecentsPackageMonitor extends PackageMonitor { public interface PackageCallbacks { - public void onComponentRemoved(HashSet<ComponentName> cns); + public void onPackagesChanged(RecentsPackageMonitor monitor, String packageName, + int userId); } PackageCallbacks mCb; - List<Task.TaskKey> mTasks; SystemServicesProxy mSystemServicesProxy; /** Registers the broadcast receivers with the specified callbacks. */ @@ -43,7 +44,9 @@ public class RecentsPackageMonitor extends PackageMonitor { mSystemServicesProxy = new SystemServicesProxy(context); mCb = cb; try { - register(context, Looper.getMainLooper(), true); + // We register for events from all users, but will cross-reference them with + // packages for the current user and any profiles they have + register(context, Looper.getMainLooper(), UserHandle.ALL, true); } catch (IllegalStateException e) { e.printStackTrace(); } @@ -59,29 +62,15 @@ public class RecentsPackageMonitor extends PackageMonitor { } mSystemServicesProxy = null; mCb = null; - mTasks.clear(); - } - - /** Sets the list of tasks to match against package broadcast changes. */ - void setTasks(List<Task.TaskKey> tasks) { - mTasks = tasks; } @Override public void onPackageRemoved(String packageName, int uid) { if (mCb == null) return; - // Identify all the tasks that should be removed as a result of the package being removed. - // Using a set to ensure that we callback once per unique component. - HashSet<ComponentName> componentsToRemove = new HashSet<ComponentName>(); - for (Task.TaskKey t : mTasks) { - ComponentName cn = t.baseIntent.getComponent(); - if (cn.getPackageName().equals(packageName)) { - componentsToRemove.add(cn); - } - } - // Notify our callbacks that the components no longer exist - mCb.onComponentRemoved(componentsToRemove); + // Notify callbacks that a package has changed + final int eventUserId = getChangingUserId(); + mCb.onPackagesChanged(this, packageName, eventUserId); } @Override @@ -94,25 +83,38 @@ public class RecentsPackageMonitor extends PackageMonitor { public void onPackageModified(String packageName) { if (mCb == null) return; + // Notify callbacks that a package has changed + final int eventUserId = getChangingUserId(); + mCb.onPackagesChanged(this, packageName, eventUserId); + } + + /** + * Computes the components that have been removed as a result of a change in the specified + * package. + */ + public HashSet<ComponentName> computeComponentsRemoved(List<Task.TaskKey> taskKeys, + String packageName, int userId) { // Identify all the tasks that should be removed as a result of the package being removed. // Using a set to ensure that we callback once per unique component. - HashSet<ComponentName> componentsKnownToExist = new HashSet<ComponentName>(); - HashSet<ComponentName> componentsToRemove = new HashSet<ComponentName>(); - for (Task.TaskKey t : mTasks) { + HashSet<ComponentName> existingComponents = new HashSet<ComponentName>(); + HashSet<ComponentName> removedComponents = new HashSet<ComponentName>(); + for (Task.TaskKey t : taskKeys) { + // Skip if this doesn't apply to the current user + if (t.userId != userId) continue; + ComponentName cn = t.baseIntent.getComponent(); if (cn.getPackageName().equals(packageName)) { - if (componentsKnownToExist.contains(cn)) { + if (existingComponents.contains(cn)) { // If we know that the component still exists in the package, then skip continue; } - if (mSystemServicesProxy.getActivityInfo(cn) != null) { - componentsKnownToExist.add(cn); + if (mSystemServicesProxy.getActivityInfo(cn, userId) != null) { + existingComponents.add(cn); } else { - componentsToRemove.add(cn); + removedComponents.add(cn); } } } - // Notify our callbacks that the components no longer exist - mCb.onComponentRemoved(componentsToRemove); + return removedComponents; } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java new file mode 100644 index 0000000..0e1c01a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2014 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.recents.model; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.util.Log; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.misc.SystemServicesProxy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + + +/** + * This class stores the loading state as it goes through multiple stages of loading: + * - preloadRawTasks() will load the raw set of recents tasks from the system + * - preloadPlan() will construct a new task stack with all metadata and only icons and thumbnails + * that are currently in the cache + * - executePlan() will actually load and fill in the icons and thumbnails according to the load + * options specified, such that we can transition into the Recents activity seamlessly + */ +public class RecentsTaskLoadPlan { + static String TAG = "RecentsTaskLoadPlan"; + static boolean DEBUG = false; + + /** The set of conditions to load tasks. */ + public static class Options { + public int runningTaskId = -1; + public boolean loadIcons = true; + public boolean loadThumbnails = true; + public boolean onlyLoadForCache = false; + public boolean onlyLoadPausedActivities = false; + public int numVisibleTasks = 0; + public int numVisibleTaskThumbnails = 0; + } + + Context mContext; + RecentsConfiguration mConfig; + SystemServicesProxy mSystemServicesProxy; + + List<ActivityManager.RecentTaskInfo> mRawTasks; + TaskStack mStack; + HashMap<Task.ComponentNameKey, ActivityInfoHandle> mActivityInfoCache = + new HashMap<Task.ComponentNameKey, ActivityInfoHandle>(); + + /** Package level ctor */ + RecentsTaskLoadPlan(Context context, RecentsConfiguration config, SystemServicesProxy ssp) { + mContext = context; + mConfig = config; + mSystemServicesProxy = ssp; + } + + /** + * An optimization to preload the raw list of tasks. + */ + public synchronized void preloadRawTasks(boolean isTopTaskHome) { + mRawTasks = mSystemServicesProxy.getRecentTasks(mConfig.maxNumTasksToLoad, + UserHandle.CURRENT.getIdentifier(), isTopTaskHome); + Collections.reverse(mRawTasks); + + if (DEBUG) Log.d(TAG, "preloadRawTasks, tasks: " + mRawTasks.size()); + } + + /** + * Preloads the list of recent tasks from the system. After this call, the TaskStack will + * have a list of all the recent tasks with their metadata, not including icons or + * thumbnails which were not cached and have to be loaded. + */ + synchronized void preloadPlan(RecentsTaskLoader loader, boolean isTopTaskHome) { + if (DEBUG) Log.d(TAG, "preloadPlan"); + + mActivityInfoCache.clear(); + mStack = new TaskStack(); + + Resources res = mContext.getResources(); + ArrayList<Task> loadedTasks = new ArrayList<Task>(); + if (mRawTasks == null) { + preloadRawTasks(isTopTaskHome); + } + int taskCount = mRawTasks.size(); + for (int i = 0; i < taskCount; i++) { + ActivityManager.RecentTaskInfo t = mRawTasks.get(i); + + // Compose the task key + Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.baseIntent, t.userId, + t.firstActiveTime, t.lastActiveTime); + + // Get an existing activity info handle if possible + Task.ComponentNameKey cnKey = taskKey.getComponentNameKey(); + ActivityInfoHandle infoHandle; + boolean hadCachedActivityInfo = false; + if (mActivityInfoCache.containsKey(cnKey)) { + infoHandle = mActivityInfoCache.get(cnKey); + hadCachedActivityInfo = true; + } else { + infoHandle = new ActivityInfoHandle(); + } + + // Load the label, icon, and color + String activityLabel = loader.getAndUpdateActivityLabel(taskKey, t.taskDescription, + mSystemServicesProxy, infoHandle); + Drawable activityIcon = loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, + mSystemServicesProxy, res, infoHandle, false); + int activityColor = loader.getActivityPrimaryColor(t.taskDescription, mConfig); + + // Update the activity info cache + if (!hadCachedActivityInfo && infoHandle.info != null) { + mActivityInfoCache.put(cnKey, infoHandle); + } + + Bitmap icon = t.taskDescription != null + ? t.taskDescription.getInMemoryIcon() + : null; + String iconFilename = t.taskDescription != null + ? t.taskDescription.getIconFilename() + : null; + + // Add the task to the stack + Task task = new Task(taskKey, (t.id != RecentsTaskLoader.INVALID_TASK_ID), + t.affiliatedTaskId, t.affiliatedTaskColor, activityLabel, activityIcon, + activityColor, (i == (taskCount - 1)), mConfig.lockToAppEnabled, icon, + iconFilename); + task.thumbnail = loader.getAndUpdateThumbnail(taskKey, mSystemServicesProxy, false); + if (DEBUG) Log.d(TAG, "\tthumbnail: " + taskKey + ", " + task.thumbnail); + loadedTasks.add(task); + } + mStack.setTasks(loadedTasks); + mStack.createAffiliatedGroupings(mConfig); + + // Assertion + if (mStack.getTaskCount() != mRawTasks.size()) { + throw new RuntimeException("Loading failed"); + } + } + + /** + * Called to apply the actual loading based on the specified conditions. + */ + synchronized void executePlan(Options opts, RecentsTaskLoader loader, + TaskResourceLoadQueue loadQueue) { + if (DEBUG) Log.d(TAG, "executePlan, # tasks: " + opts.numVisibleTasks + + ", # thumbnails: " + opts.numVisibleTaskThumbnails + + ", running task id: " + opts.runningTaskId); + + Resources res = mContext.getResources(); + + // Iterate through each of the tasks and load them according to the load conditions. + ArrayList<Task> tasks = mStack.getTasks(); + int taskCount = tasks.size(); + for (int i = 0; i < taskCount; i++) { + ActivityManager.RecentTaskInfo t = mRawTasks.get(i); + Task task = tasks.get(i); + Task.TaskKey taskKey = task.key; + + // Get an existing activity info handle if possible + Task.ComponentNameKey cnKey = taskKey.getComponentNameKey(); + ActivityInfoHandle infoHandle; + boolean hadCachedActivityInfo = false; + if (mActivityInfoCache.containsKey(cnKey)) { + infoHandle = mActivityInfoCache.get(cnKey); + hadCachedActivityInfo = true; + } else { + infoHandle = new ActivityInfoHandle(); + } + + boolean isRunningTask = (task.key.id == opts.runningTaskId); + boolean isVisibleTask = i >= (taskCount - opts.numVisibleTasks); + boolean isVisibleThumbnail = i >= (taskCount - opts.numVisibleTaskThumbnails); + + // If requested, skip the running task + if (opts.onlyLoadPausedActivities && isRunningTask) { + continue; + } + + if (opts.loadIcons && (isRunningTask || isVisibleTask)) { + if (task.activityIcon == null) { + if (DEBUG) Log.d(TAG, "\tLoading icon: " + taskKey); + task.activityIcon = loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, + mSystemServicesProxy, res, infoHandle, true); + } + } + if (opts.loadThumbnails && (isRunningTask || isVisibleThumbnail)) { + if (task.thumbnail == null || isRunningTask) { + if (DEBUG) Log.d(TAG, "\tLoading thumbnail: " + taskKey); + if (mConfig.svelteLevel <= RecentsConfiguration.SVELTE_LIMIT_CACHE) { + task.thumbnail = loader.getAndUpdateThumbnail(taskKey, mSystemServicesProxy, + true); + } else if (mConfig.svelteLevel == RecentsConfiguration.SVELTE_DISABLE_CACHE) { + loadQueue.addTask(task); + } + } + } + + // Update the activity info cache + if (!hadCachedActivityInfo && infoHandle.info != null) { + mActivityInfoCache.put(cnKey, infoHandle); + } + } + } + + /** + * Composes and returns a TaskStack from the preloaded list of recent tasks. + */ + public TaskStack getTaskStack() { + return mStack; + } + + /** + * Composes and returns a SpaceNode from the preloaded list of recent tasks. + */ + public SpaceNode getSpaceNode() { + SpaceNode node = new SpaceNode(); + node.setStack(mStack); + return node; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java index d40e847..ba2903a 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java @@ -26,7 +26,6 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.HandlerThread; -import android.os.UserHandle; import android.util.Log; import com.android.systemui.R; @@ -34,11 +33,7 @@ import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.SystemServicesProxy; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; @@ -99,6 +94,9 @@ class TaskResourceLoadQueue { /* Task resource loader */ class TaskResourceLoader implements Runnable { + static String TAG = "TaskResourceLoader"; + static boolean DEBUG = false; + Context mContext; HandlerThread mLoadThread; Handler mLoadThreadHandler; @@ -170,56 +168,67 @@ class TaskResourceLoader implements Runnable { } } } else { + RecentsConfiguration config = RecentsConfiguration.getInstance(); SystemServicesProxy ssp = mSystemServicesProxy; - - // Load the next item from the queue - final Task t = mLoadQueue.nextTask(); - if (t != null) { - Drawable cachedIcon = mApplicationIconCache.get(t.key); - Bitmap cachedThumbnail = mThumbnailCache.get(t.key); - - // Load the application icon if it is stale or we haven't cached one yet - if (cachedIcon == null) { - cachedIcon = getTaskDescriptionIcon(t.key, t.icon, t.iconFilename, ssp, - mContext.getResources()); - + // If we've stopped the loader, then fall through to the above logic to wait on + // the load thread + if (ssp != null) { + // Load the next item from the queue + final Task t = mLoadQueue.nextTask(); + if (t != null) { + Drawable cachedIcon = mApplicationIconCache.get(t.key); + Bitmap cachedThumbnail = mThumbnailCache.get(t.key); + + // Load the application icon if it is stale or we haven't cached one yet if (cachedIcon == null) { - ActivityInfo info = ssp.getActivityInfo(t.key.baseIntent.getComponent(), - t.key.userId); - if (info != null) { - cachedIcon = ssp.getActivityIcon(info, t.key.userId); + cachedIcon = getTaskDescriptionIcon(t.key, t.icon, t.iconFilename, ssp, + mContext.getResources()); + + if (cachedIcon == null) { + ActivityInfo info = ssp.getActivityInfo( + t.key.baseIntent.getComponent(), t.key.userId); + if (info != null) { + if (DEBUG) Log.d(TAG, "Loading icon: " + t.key); + cachedIcon = ssp.getActivityIcon(info, t.key.userId); + } } - } - if (cachedIcon == null) { - cachedIcon = mDefaultApplicationIcon; - } + if (cachedIcon == null) { + cachedIcon = mDefaultApplicationIcon; + } - // At this point, even if we can't load the icon, we will set the default - // icon. - mApplicationIconCache.put(t.key, cachedIcon); - } - // Load the thumbnail if it is stale or we haven't cached one yet - if (cachedThumbnail == null) { - cachedThumbnail = ssp.getTaskThumbnail(t.key.id); - if (cachedThumbnail != null) { - cachedThumbnail.setHasAlpha(false); - } else { - cachedThumbnail = mDefaultThumbnail; + // At this point, even if we can't load the icon, we will set the + // default icon. + mApplicationIconCache.put(t.key, cachedIcon); } - mThumbnailCache.put(t.key, cachedThumbnail); - } - if (!mCancelled) { - // Notify that the task data has changed - final Drawable newIcon = cachedIcon; - final Bitmap newThumbnail = cachedThumbnail == mDefaultThumbnail - ? null : cachedThumbnail; - mMainThreadHandler.post(new Runnable() { - @Override - public void run() { - t.notifyTaskDataLoaded(newThumbnail, newIcon); + // Load the thumbnail if it is stale or we haven't cached one yet + if (cachedThumbnail == null) { + if (config.svelteLevel < RecentsConfiguration.SVELTE_DISABLE_LOADING) { + if (DEBUG) Log.d(TAG, "Loading thumbnail: " + t.key); + cachedThumbnail = ssp.getTaskThumbnail(t.key.id); + } + if (cachedThumbnail == null) { + cachedThumbnail = mDefaultThumbnail; + } + // When svelte, we trim the memory to just the visible thumbnails when + // leaving, so don't thrash the cache as the user scrolls (just load + // them from scratch each time) + if (config.svelteLevel < RecentsConfiguration.SVELTE_LIMIT_CACHE) { + mThumbnailCache.put(t.key, cachedThumbnail); } - }); + } + if (!mCancelled) { + // Notify that the task data has changed + final Drawable newIcon = cachedIcon; + final Bitmap newThumbnail = cachedThumbnail == mDefaultThumbnail + ? null : cachedThumbnail; + mMainThreadHandler.post(new Runnable() { + @Override + public void run() { + t.notifyTaskDataLoaded(newThumbnail, newIcon); + } + }); + } } } @@ -257,6 +266,7 @@ public class RecentsTaskLoader { private static final String TAG = "RecentsTaskLoader"; static RecentsTaskLoader sInstance; + static int INVALID_TASK_ID = -1; SystemServicesProxy mSystemServicesProxy; DrawableLruCache mApplicationIconCache; @@ -269,6 +279,8 @@ public class RecentsTaskLoader { int mMaxThumbnailCacheSize; int mMaxIconCacheSize; + int mNumVisibleTasksLoaded; + int mNumVisibleThumbnailsLoaded; BitmapDrawable mDefaultApplicationIcon; Bitmap mDefaultThumbnail; @@ -321,31 +333,45 @@ public class RecentsTaskLoader { return mSystemServicesProxy; } - /** Gets the list of recent tasks, ordered from back to front. */ - private static List<ActivityManager.RecentTaskInfo> getRecentTasks(SystemServicesProxy ssp, - boolean isTopTaskHome) { - RecentsConfiguration config = RecentsConfiguration.getInstance(); - List<ActivityManager.RecentTaskInfo> tasks = - ssp.getRecentTasks(config.maxNumTasksToLoad, UserHandle.CURRENT.getIdentifier(), - isTopTaskHome); - Collections.reverse(tasks); - return tasks; + /** Returns the activity label using as many cached values as we can. */ + public String getAndUpdateActivityLabel(Task.TaskKey taskKey, + ActivityManager.TaskDescription td, SystemServicesProxy ssp, + ActivityInfoHandle infoHandle) { + // Return the task description label if it exists + if (td != null && td.getLabel() != null) { + return td.getLabel(); + } + // Return the cached activity label if it exists + String label = mActivityLabelCache.getAndInvalidateIfModified(taskKey); + if (label != null) { + return label; + } + // All short paths failed, load the label from the activity info and cache it + if (infoHandle.info == null) { + infoHandle.info = ssp.getActivityInfo(taskKey.baseIntent.getComponent(), + taskKey.userId); + } + if (infoHandle.info != null) { + label = ssp.getActivityLabel(infoHandle.info); + mActivityLabelCache.put(taskKey, label); + } else { + Log.w(TAG, "Missing ActivityInfo for " + taskKey.baseIntent.getComponent() + + " u=" + taskKey.userId); + } + return label; } /** Returns the activity icon using as many cached values as we can. */ public Drawable getAndUpdateActivityIcon(Task.TaskKey taskKey, - ActivityManager.TaskDescription td, SystemServicesProxy ssp, - Resources res, ActivityInfoHandle infoHandle, boolean preloadTask) { + ActivityManager.TaskDescription td, SystemServicesProxy ssp, + Resources res, ActivityInfoHandle infoHandle, boolean loadIfNotCached) { // Return the cached activity icon if it exists Drawable icon = mApplicationIconCache.getAndInvalidateIfModified(taskKey); if (icon != null) { return icon; } - // If we are preloading this task, continue to load the task description icon or the - // activity icon - if (preloadTask) { - + if (loadIfNotCached) { // Return and cache the task description icon if it exists Drawable tdDrawable = mLoader.getTaskDescriptionIcon(taskKey, td.getInMemoryIcon(), td.getIconFilename(), ssp, res); @@ -367,36 +393,30 @@ public class RecentsTaskLoader { } } } - // If we couldn't load any icon, return null + // We couldn't load any icon return null; } - /** Returns the activity label using as many cached values as we can. */ - public String getAndUpdateActivityLabel(Task.TaskKey taskKey, - ActivityManager.TaskDescription td, SystemServicesProxy ssp, - ActivityInfoHandle infoHandle) { - // Return the task description label if it exists - if (td != null && td.getLabel() != null) { - return td.getLabel(); - } - // Return the cached activity label if it exists - String label = mActivityLabelCache.getAndInvalidateIfModified(taskKey); - if (label != null) { - return label; + /** Returns the bitmap using as many cached values as we can. */ + public Bitmap getAndUpdateThumbnail(Task.TaskKey taskKey, SystemServicesProxy ssp, + boolean loadIfNotCached) { + // Return the cached thumbnail if it exists + Bitmap thumbnail = mThumbnailCache.getAndInvalidateIfModified(taskKey); + if (thumbnail != null) { + return thumbnail; } - // All short paths failed, load the label from the activity info and cache it - if (infoHandle.info == null) { - infoHandle.info = ssp.getActivityInfo(taskKey.baseIntent.getComponent(), - taskKey.userId); - } - if (infoHandle.info != null) { - label = ssp.getActivityLabel(infoHandle.info); - mActivityLabelCache.put(taskKey, label); - } else { - Log.w(TAG, "Missing ActivityInfo for " + taskKey.baseIntent.getComponent() - + " u=" + taskKey.userId); + + RecentsConfiguration config = RecentsConfiguration.getInstance(); + if (config.svelteLevel < RecentsConfiguration.SVELTE_DISABLE_LOADING && loadIfNotCached) { + // Load the thumbnail from the system + thumbnail = ssp.getTaskThumbnail(taskKey.id); + if (thumbnail != null) { + mThumbnailCache.put(taskKey, thumbnail); + return thumbnail; + } } - return label; + // We couldn't load any thumbnail + return null; } /** Returns the activity's primary color. */ @@ -408,116 +428,42 @@ public class RecentsTaskLoader { return config.taskBarViewDefaultBackgroundColor; } - /** Reload the set of recent tasks */ - public SpaceNode reload(Context context, int preloadCount, boolean isTopTaskHome) { - ArrayList<Task.TaskKey> taskKeys = new ArrayList<Task.TaskKey>(); - ArrayList<Task> tasksToLoad = new ArrayList<Task>(); - TaskStack stack = getTaskStack(mSystemServicesProxy, context.getResources(), - -1, preloadCount, true, isTopTaskHome, taskKeys, tasksToLoad); - SpaceNode root = new SpaceNode(); - root.setStack(stack); - - // Start the task loader and add all the tasks we need to load - mLoader.start(context); - mLoadQueue.addTasks(tasksToLoad); - - // Update the package monitor with the list of packages to listen for - mPackageMonitor.setTasks(taskKeys); + /** Returns the size of the app icon cache. */ + public int getApplicationIconCacheSize() { + return mMaxIconCacheSize; + } - return root; + /** Returns the size of the thumbnail cache. */ + public int getThumbnailCacheSize() { + return mMaxThumbnailCacheSize; } - /** Creates a lightweight stack of the current recent tasks, without thumbnails and icons. */ - public TaskStack getTaskStack(SystemServicesProxy ssp, Resources res, - int preloadTaskId, int preloadTaskCount, - boolean loadTaskThumbnails, boolean isTopTaskHome, - List<Task.TaskKey> taskKeysOut, List<Task> tasksToLoadOut) { + /** Creates a new plan for loading the recent tasks. */ + public RecentsTaskLoadPlan createLoadPlan(Context context) { RecentsConfiguration config = RecentsConfiguration.getInstance(); - List<ActivityManager.RecentTaskInfo> tasks = getRecentTasks(ssp, isTopTaskHome); - HashMap<Task.ComponentNameKey, ActivityInfoHandle> activityInfoCache = - new HashMap<Task.ComponentNameKey, ActivityInfoHandle>(); - ArrayList<Task> tasksToAdd = new ArrayList<Task>(); - TaskStack stack = new TaskStack(); - - int taskCount = tasks.size(); - for (int i = 0; i < taskCount; i++) { - ActivityManager.RecentTaskInfo t = tasks.get(i); - - // Compose the task key - Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.baseIntent, t.userId, - t.firstActiveTime, t.lastActiveTime); - - // Get an existing activity info handle if possible - Task.ComponentNameKey cnKey = taskKey.getComponentNameKey(); - ActivityInfoHandle infoHandle; - boolean hasCachedActivityInfo = false; - if (activityInfoCache.containsKey(cnKey)) { - infoHandle = activityInfoCache.get(cnKey); - hasCachedActivityInfo = true; - } else { - infoHandle = new ActivityInfoHandle(); - } - - // Determine whether to preload this task - boolean preloadTask = false; - if (preloadTaskId > 0) { - preloadTask = (t.id == preloadTaskId); - } else if (preloadTaskCount > 0) { - preloadTask = (i >= (taskCount - preloadTaskCount)); - } - - // Load the label, icon, and color - String activityLabel = getAndUpdateActivityLabel(taskKey, t.taskDescription, - ssp, infoHandle); - Drawable activityIcon = getAndUpdateActivityIcon(taskKey, t.taskDescription, - ssp, res, infoHandle, preloadTask); - int activityColor = getActivityPrimaryColor(t.taskDescription, config); + RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(context, config, mSystemServicesProxy); + return plan; + } - // Update the activity info cache - if (!hasCachedActivityInfo && infoHandle.info != null) { - activityInfoCache.put(cnKey, infoHandle); - } + /** Preloads recents tasks using the specified plan to store the output. */ + public void preloadTasks(RecentsTaskLoadPlan plan, boolean isTopTaskHome) { + plan.preloadPlan(this, isTopTaskHome); + } - Bitmap icon = t.taskDescription != null - ? t.taskDescription.getInMemoryIcon() - : null; - String iconFilename = t.taskDescription != null - ? t.taskDescription.getIconFilename() - : null; - - // Add the task to the stack - Task task = new Task(taskKey, (t.id > -1), t.affiliatedTaskId, t.affiliatedTaskColor, - activityLabel, activityIcon, activityColor, (i == (taskCount - 1)), - config.lockToAppEnabled, icon, iconFilename); - - if (preloadTask && loadTaskThumbnails) { - // Load the thumbnail from the cache if possible - task.thumbnail = mThumbnailCache.getAndInvalidateIfModified(taskKey); - if (task.thumbnail == null) { - // Load the thumbnail from the system - task.thumbnail = ssp.getTaskThumbnail(taskKey.id); - if (task.thumbnail != null) { - task.thumbnail.setHasAlpha(false); - mThumbnailCache.put(taskKey, task.thumbnail); - } - } - if (task.thumbnail == null && tasksToLoadOut != null) { - // Either the task has changed since the last active time, or it was not - // previously cached, so try and load the task anew. - tasksToLoadOut.add(task); - } - } + /** Begins loading the heavy task data according to the specified options. */ + public void loadTasks(Context context, RecentsTaskLoadPlan plan, + RecentsTaskLoadPlan.Options opts) { + if (opts == null) { + throw new RuntimeException("Requires load options"); + } + plan.executePlan(opts, this, mLoadQueue); + if (!opts.onlyLoadForCache) { + mNumVisibleTasksLoaded = opts.numVisibleTasks; + mNumVisibleThumbnailsLoaded = opts.numVisibleTaskThumbnails; - // Add to the list of task keys - if (taskKeysOut != null) { - taskKeysOut.add(taskKey); - } - // Add the task to the stack - tasksToAdd.add(task); + // Start the loader + mLoader.start(context); } - stack.setTasks(tasksToAdd); - stack.createAffiliatedGroupings(config); - return stack; } /** Acquires the task resource data directly from the pool. */ @@ -573,28 +519,33 @@ public class RecentsTaskLoader { * out of memory. */ public void onTrimMemory(int level) { + RecentsConfiguration config = RecentsConfiguration.getInstance(); switch (level) { case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: // Stop the loader immediately when the UI is no longer visible stopLoader(); - mThumbnailCache.trimToSize(Math.max( - Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount, - mMaxThumbnailCacheSize / 2)); - mApplicationIconCache.trimToSize(Math.max( - Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount, + if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) { + mThumbnailCache.trimToSize(Math.max(mNumVisibleTasksLoaded, + mMaxThumbnailCacheSize / 2)); + } else if (config.svelteLevel == RecentsConfiguration.SVELTE_LIMIT_CACHE) { + mThumbnailCache.trimToSize(mNumVisibleThumbnailsLoaded); + } else if (config.svelteLevel >= RecentsConfiguration.SVELTE_DISABLE_CACHE) { + mThumbnailCache.evictAll(); + } + mApplicationIconCache.trimToSize(Math.max(mNumVisibleTasksLoaded, mMaxIconCacheSize / 2)); break; case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: // We are leaving recents, so trim the data a bit - mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 2); - mApplicationIconCache.trimToSize(mMaxIconCacheSize / 2); + mThumbnailCache.trimToSize(Math.max(1, mMaxThumbnailCacheSize / 2)); + mApplicationIconCache.trimToSize(Math.max(1, mMaxIconCacheSize / 2)); break; case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: case ComponentCallbacks2.TRIM_MEMORY_MODERATE: // We are going to be low on memory - mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 4); - mApplicationIconCache.trimToSize(mMaxIconCacheSize / 4); + mThumbnailCache.trimToSize(Math.max(1, mMaxThumbnailCacheSize / 4)); + mApplicationIconCache.trimToSize(Math.max(1, mMaxIconCacheSize / 4)); break; case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java index a7e2b0b..55dfe45 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java @@ -129,7 +129,7 @@ public class Task { TaskCallbacks mCb; public Task() { - // Only used by RecentsService for task rect calculations. + // Do nothing } public Task(TaskKey key, boolean isActive, int taskAffiliation, int taskAffiliationColor, diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java index 1e47b50..81f0cef 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java @@ -61,6 +61,14 @@ class FilteredTaskList { } } + /** Resets this FilteredTaskList. */ + void reset() { + mTasks.clear(); + mFilteredTasks.clear(); + mTaskIndices.clear(); + mFilter = null; + } + /** Removes the task filter and returns the previous touch state */ void removeFilter() { mFilter = null; @@ -190,6 +198,14 @@ public class TaskStack { mCb = cb; } + /** Resets this TaskStack. */ + public void reset() { + mCb = null; + mTaskList.reset(); + mGroups.clear(); + mAffinitiesGroups.clear(); + } + /** Adds a new task */ public void addTask(Task t) { mTaskList.add(t); @@ -236,6 +252,8 @@ public class TaskStack { if (group.getTaskCount() == 0) { removeGroup(group); } + // Update the lock-to-app state + t.lockToThisTask = false; if (mCb != null) { // Notify that a task has been removed mCb.onStackTaskRemoved(this, t, null); @@ -255,6 +273,17 @@ public class TaskStack { return mTaskList.getTasks().get(mTaskList.size() - 1); } + /** Gets the task keys */ + public ArrayList<Task.TaskKey> getTaskKeys() { + ArrayList<Task.TaskKey> taskKeys = new ArrayList<Task.TaskKey>(); + ArrayList<Task> tasks = mTaskList.getTasks(); + int taskCount = tasks.size(); + for (int i = 0; i < taskCount; i++) { + taskKeys.add(tasks.get(i).key); + } + return taskKeys; + } + /** Gets the tasks */ public ArrayList<Task> getTasks() { return mTaskList.getTasks(); @@ -453,4 +482,4 @@ public class TaskStack { } return str; } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java b/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java index d2fdaff..fb05c01 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java @@ -16,13 +16,10 @@ package com.android.systemui.recents.views; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.graphics.Outline; import android.graphics.Rect; import android.view.View; import android.view.ViewOutlineProvider; - import com.android.systemui.recents.RecentsConfiguration; /* An outline provider that has a clip and outline that can be animated. */ @@ -31,35 +28,25 @@ public class AnimateableViewBounds extends ViewOutlineProvider { RecentsConfiguration mConfig; TaskView mSourceView; - Rect mTmpRect = new Rect(); Rect mClipRect = new Rect(); Rect mClipBounds = new Rect(); - Rect mOutlineClipRect = new Rect(); int mCornerRadius; float mAlpha = 1f; final float mMinAlpha = 0.25f; - ObjectAnimator mClipTopAnimator; - ObjectAnimator mClipRightAnimator; - ObjectAnimator mClipBottomAnimator; - public AnimateableViewBounds(TaskView source, int cornerRadius) { mConfig = RecentsConfiguration.getInstance(); mSourceView = source; mCornerRadius = cornerRadius; - setClipTop(getClipTop()); - setClipRight(getClipRight()); setClipBottom(getClipBottom()); - setOutlineClipBottom(getOutlineClipBottom()); } @Override public void getOutline(View view, Outline outline) { outline.setAlpha(mMinAlpha + mAlpha / (1f - mMinAlpha)); - outline.setRoundRect(Math.max(mClipRect.left, mOutlineClipRect.left), - Math.max(mClipRect.top, mOutlineClipRect.top), - mSourceView.getWidth() - Math.max(mClipRect.right, mOutlineClipRect.right), - mSourceView.getHeight() - Math.max(mClipRect.bottom, mOutlineClipRect.bottom), + outline.setRoundRect(mClipRect.left, mClipRect.top, + mSourceView.getWidth() - mClipRect.right, + mSourceView.getHeight() - mClipRect.bottom, mCornerRadius); } @@ -71,73 +58,6 @@ public class AnimateableViewBounds extends ViewOutlineProvider { } } - /** Animates the top clip. */ - void animateClipTop(int top, int duration, ValueAnimator.AnimatorUpdateListener updateListener) { - if (mClipTopAnimator != null) { - mClipTopAnimator.removeAllListeners(); - mClipTopAnimator.cancel(); - } - mClipTopAnimator = ObjectAnimator.ofInt(this, "clipTop", top); - mClipTopAnimator.setDuration(duration); - mClipTopAnimator.setInterpolator(mConfig.fastOutSlowInInterpolator); - if (updateListener != null) { - mClipTopAnimator.addUpdateListener(updateListener); - } - mClipTopAnimator.start(); - } - - /** Sets the top clip. */ - public void setClipTop(int top) { - if (top != mClipRect.top) { - mClipRect.top = top; - mSourceView.invalidateOutline(); - updateClipBounds(); - } - } - - /** Returns the top clip. */ - public int getClipTop() { - return mClipRect.top; - } - - /** Animates the right clip. */ - void animateClipRight(int right, int duration) { - if (mClipRightAnimator != null) { - mClipRightAnimator.removeAllListeners(); - mClipRightAnimator.cancel(); - } - mClipRightAnimator = ObjectAnimator.ofInt(this, "clipRight", right); - mClipRightAnimator.setDuration(duration); - mClipRightAnimator.setInterpolator(mConfig.fastOutSlowInInterpolator); - mClipRightAnimator.start(); - } - - /** Sets the right clip. */ - public void setClipRight(int right) { - if (right != mClipRect.right) { - mClipRect.right = right; - mSourceView.invalidateOutline(); - updateClipBounds(); - } - } - - /** Returns the right clip. */ - public int getClipRight() { - return mClipRect.right; - } - - /** Animates the bottom clip. */ - void animateClipBottom(int bottom, int duration) { - if (mClipBottomAnimator != null) { - mClipBottomAnimator.removeAllListeners(); - mClipBottomAnimator.cancel(); - } - mClipBottomAnimator = ObjectAnimator.ofInt(this, "clipBottom", bottom); - mClipBottomAnimator.setDuration(duration); - mClipBottomAnimator.setInterpolator(mConfig.fastOutSlowInInterpolator); - mClipBottomAnimator.start(); - } - /** Sets the bottom clip. */ public void setClipBottom(int bottom) { if (bottom != mClipRect.bottom) { @@ -145,7 +65,7 @@ public class AnimateableViewBounds extends ViewOutlineProvider { mSourceView.invalidateOutline(); updateClipBounds(); if (!mConfig.useHardwareLayers) { - mSourceView.mThumbnailView.updateVisibility( + mSourceView.mThumbnailView.updateThumbnailVisibility( bottom - mSourceView.getPaddingBottom()); } } @@ -156,19 +76,6 @@ public class AnimateableViewBounds extends ViewOutlineProvider { return mClipRect.bottom; } - /** Sets the outline bottom clip. */ - public void setOutlineClipBottom(int bottom) { - if (bottom != mOutlineClipRect.bottom) { - mOutlineClipRect.bottom = bottom; - mSourceView.invalidateOutline(); - } - } - - /** Gets the outline bottom clip. */ - public int getOutlineClipBottom() { - return mOutlineClipRect.bottom; - } - private void updateClipBounds() { mClipBounds.set(mClipRect.left, mClipRect.top, mSourceView.getWidth() - mClipRect.right, diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java index 6c22a3b..427ffe5 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -18,8 +18,6 @@ package com.android.systemui.recents.views; import android.app.ActivityOptions; import android.app.TaskStackBuilder; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -33,18 +31,16 @@ import android.view.LayoutInflater; import android.view.View; import android.view.WindowInsets; import android.widget.FrameLayout; + import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; -import com.android.systemui.recents.misc.Console; import com.android.systemui.recents.misc.SystemServicesProxy; -import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.RecentsPackageMonitor; import com.android.systemui.recents.model.RecentsTaskLoader; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; import java.util.ArrayList; -import java.util.HashSet; /** * This view is the the top level layout that contains TaskStacks (which are laid out according @@ -59,6 +55,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV public void onTaskLaunchFailed(); public void onAllTaskViewsDismissed(); public void onExitToHomeAnimationTriggered(); + public void onScreenPinningRequest(); } RecentsConfiguration mConfig; @@ -68,7 +65,6 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV ArrayList<TaskStack> mStacks; View mSearchBar; RecentsViewCallbacks mCb; - boolean mAlreadyLaunchingTask; public RecentsView(Context context) { super(context); @@ -100,42 +96,58 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV /** Set/get the bsp root node */ public void setTaskStacks(ArrayList<TaskStack> stacks) { - // Remove all TaskStackViews (but leave the search bar) + int numStacks = stacks.size(); + + // Make a list of the stack view children only + ArrayList<TaskStackView> stackViews = new ArrayList<TaskStackView>(); int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - View v = getChildAt(i); - if (v != mSearchBar) { - removeViewAt(i); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child != mSearchBar) { + stackViews.add((TaskStackView) child); } } - // Create and add all the stacks for this partition of space. + // Remove all/extra stack views + int numTaskStacksToKeep = 0; // Keep no tasks if we are recreating the layout + if (mConfig.launchedReuseTaskStackViews) { + numTaskStacksToKeep = Math.min(childCount, numStacks); + } + for (int i = stackViews.size() - 1; i >= numTaskStacksToKeep; i--) { + removeView(stackViews.get(i)); + stackViews.remove(i); + } + + // Update the stack views that we are keeping + for (int i = 0; i < numTaskStacksToKeep; i++) { + TaskStackView tsv = stackViews.get(i); + // If onRecentsHidden is not triggered, we need to the stack view again here + tsv.reset(); + tsv.setStack(stacks.get(i)); + } + + // Add remaining/recreate stack views mStacks = stacks; - int numStacks = mStacks.size(); - for (int i = 0; i < numStacks; i++) { - TaskStack stack = mStacks.get(i); + for (int i = stackViews.size(); i < numStacks; i++) { + TaskStack stack = stacks.get(i); TaskStackView stackView = new TaskStackView(getContext(), stack); stackView.setCallbacks(this); - // Enable debug mode drawing - if (mConfig.debugModeEnabled) { - stackView.setDebugOverlay(mDebugOverlay); - } addView(stackView); } - // Reset the launched state - mAlreadyLaunchingTask = false; - } - - /** Removes all the task stack views from this recents view. */ - public void removeAllTaskStacks() { - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - View child = getChildAt(i); - if (child != mSearchBar) { - removeViewAt(i); + // Enable debug mode drawing on all the stacks if necessary + if (mConfig.debugModeEnabled) { + for (int i = childCount - 1; i >= 0; i--) { + View v = getChildAt(i); + if (v != mSearchBar) { + TaskStackView stackView = (TaskStackView) v; + stackView.setDebugOverlay(mDebugOverlay); + } } } + + // Trigger a new layout + requestLayout(); } /** Launches the focused task from the first stack if possible */ @@ -192,6 +204,10 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV /** Requests all task stacks to start their enter-recents animation */ public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) { + // We have to increment/decrement the post animation trigger in case there are no children + // to ensure that it runs + ctx.postAnimationTrigger.increment(); + int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); @@ -200,10 +216,14 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV stackView.startEnterRecentsAnimation(ctx); } } + ctx.postAnimationTrigger.decrement(); } /** Requests all task stacks to start their exit-recents animation */ public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { + // We have to increment/decrement the post animation trigger in case there are no children + // to ensure that it runs + ctx.postAnimationTrigger.increment(); int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); @@ -212,6 +232,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV stackView.startExitToHomeAnimation(ctx); } } + ctx.postAnimationTrigger.decrement(); // Notify of the exit animation mCb.onExitToHomeAnimationTriggered(); @@ -339,7 +360,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV View child = getChildAt(i); if (child != mSearchBar) { TaskStackView stackView = (TaskStackView) child; - stackView.focusNextTask(forward); + stackView.focusNextTask(forward, true); break; } } @@ -386,11 +407,6 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV if (mCb != null) { mCb.onTaskViewClicked(); } - // Skip if we are already launching tasks - if (mAlreadyLaunchingTask) { - return; - } - mAlreadyLaunchingTask = true; // Upfront the processing of the thumbnail TaskViewTransform transform = new TaskViewTransform(); @@ -451,7 +467,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV postDelayed(new Runnable() { @Override public void run() { - ssp.lockCurrentTask(); + mCb.onScreenPinningRequest(); } }, 350); mTriggered = true; @@ -461,7 +477,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV } opts = ActivityOptions.makeThumbnailAspectScaleUpAnimation(sourceView, b, offsetX, offsetY, transform.rect.width(), transform.rect.height(), - animStartedListener); + sourceView.getHandler(), animStartedListener); } final ActivityOptions launchOpts = opts; @@ -475,7 +491,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV if (ssp.startActivityFromRecents(getContext(), task.key.id, task.activityLabel, launchOpts)) { if (launchOpts == null && lockToTask) { - ssp.lockCurrentTask(); + mCb.onScreenPinningRequest(); } } else { // Dismiss the task and return the user to home if we fail to @@ -491,7 +507,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV // Launch the app right away if there is no task view, otherwise, animate the icon out first if (tv == null) { - post(launchRunnable); + launchRunnable.run(); } else { if (!task.group.isFrontMostTask(task)) { // For affiliated tasks that are behind other tasks, we must animate the front cards @@ -500,7 +516,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV } else { // Otherwise, we can start the task transition immediately stackView.startLaunchTaskAnimation(tv, null, lockToTask); - postDelayed(launchRunnable, 17); + launchRunnable.run(); } } } @@ -526,8 +542,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV loader.deleteTaskData(t, false); // Remove the old task from activity manager - RecentsTaskLoader.getInstance().getSystemServicesProxy().removeTask(t.key.id, - Utilities.isDocument(t.key.baseIntent)); + loader.getSystemServicesProxy().removeTask(t.key.id); } @Override @@ -535,6 +550,19 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV mCb.onAllTaskViewsDismissed(); } + /** Final callback after Recents is finally hidden. */ + public void onRecentsHidden() { + // Notify each task stack view + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child != mSearchBar) { + TaskStackView stackView = (TaskStackView) child; + stackView.onRecentsHidden(); + } + } + } + @Override public void onTaskStackFilterTriggered() { // Hide the search bar @@ -566,14 +594,14 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/ @Override - public void onComponentRemoved(HashSet<ComponentName> cns) { + public void onPackagesChanged(RecentsPackageMonitor monitor, String packageName, int userId) { // Propagate this event down to each task stack view int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child != mSearchBar) { TaskStackView stackView = (TaskStackView) child; - stackView.onComponentRemoved(cns); + stackView.onPackagesChanged(monitor, packageName, userId); } } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java index fa44551..0428b48 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java @@ -50,7 +50,6 @@ public class SwipeHelper { private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec private int DEFAULT_ESCAPE_ANIMATION_DURATION = 75; // ms private int MAX_ESCAPE_ANIMATION_DURATION = 150; // ms - private int MAX_DISMISS_VELOCITY = 2000; // dp/sec private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 250; // ms public static float ALPHA_FADE_START = 0.15f; // fraction of thumbnail width @@ -350,8 +349,7 @@ public class SwipeHelper { } private void endSwipe(VelocityTracker velocityTracker) { - float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; - velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); + velocityTracker.computeCurrentVelocity(1000 /* px/sec */); float velocity = getVelocity(velocityTracker); float perpendicularVelocity = getPerpendicularVelocity(velocityTracker); float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/SystemBarScrimViews.java b/packages/SystemUI/src/com/android/systemui/recents/views/SystemBarScrimViews.java index 162897e..1086160 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/SystemBarScrimViews.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/SystemBarScrimViews.java @@ -64,7 +64,9 @@ public class SystemBarScrimViews { mStatusBarScrimView.setTranslationY(-mStatusBarScrimView.getMeasuredHeight()); mStatusBarScrimView.animate() .translationY(0) - .setStartDelay(mConfig.taskBarEnterAnimDelay) + .setStartDelay(mConfig.launchedFromHome ? + mConfig.transitionEnterFromHomeDelay : + mConfig.transitionEnterFromAppDelay) .setDuration(mConfig.navBarScrimEnterDuration) .setInterpolator(mConfig.quintOutInterpolator) .withStartAction(new Runnable() { @@ -79,7 +81,9 @@ public class SystemBarScrimViews { mNavBarScrimView.setTranslationY(mNavBarScrimView.getMeasuredHeight()); mNavBarScrimView.animate() .translationY(0) - .setStartDelay(mConfig.taskBarEnterAnimDelay) + .setStartDelay(mConfig.launchedFromHome ? + mConfig.transitionEnterFromHomeDelay : + mConfig.transitionEnterFromAppDelay) .setDuration(mConfig.navBarScrimEnterDuration) .setInterpolator(mConfig.quintOutInterpolator) .withStartAction(new Runnable() { @@ -101,7 +105,7 @@ public class SystemBarScrimViews { mStatusBarScrimView.animate() .translationY(-mStatusBarScrimView.getMeasuredHeight()) .setStartDelay(0) - .setDuration(mConfig.taskBarExitAnimDuration) + .setDuration(mConfig.taskViewExitToAppDuration) .setInterpolator(mConfig.fastOutSlowInInterpolator) .start(); } @@ -109,7 +113,7 @@ public class SystemBarScrimViews { mNavBarScrimView.animate() .translationY(mNavBarScrimView.getMeasuredHeight()) .setStartDelay(0) - .setDuration(mConfig.taskBarExitAnimDuration) + .setDuration(mConfig.taskViewExitToAppDuration) .setInterpolator(mConfig.fastOutSlowInInterpolator) .start(); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java index 40134da..169683f 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -24,7 +24,6 @@ import android.graphics.Rect; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; import android.widget.FrameLayout; import com.android.systemui.R; @@ -41,6 +40,7 @@ import com.android.systemui.recents.model.TaskStack; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; /* The visual representation of a task stack view */ @@ -100,25 +100,11 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } }; - // A convenience runnable to return all views to the pool - Runnable mReturnAllViewsToPoolRunnable = new Runnable() { - @Override - public void run() { - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - TaskView tv = (TaskView) getChildAt(i); - mViewPool.returnViewToPool(tv); - // Also hide the view since we don't need it anymore - tv.setVisibility(View.INVISIBLE); - } - } - }; - public TaskStackView(Context context, TaskStack stack) { super(context); + // Set the stack first + setStack(stack); mConfig = RecentsConfiguration.getInstance(); - mStack = stack; - mStack.setCallbacks(this); mViewPool = new ViewPool<TaskView, Task>(context, this); mInflater = LayoutInflater.from(context); mLayoutAlgorithm = new TaskStackViewLayoutAlgorithm(mConfig); @@ -144,11 +130,58 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mCb = cb; } + /** Sets the task stack */ + void setStack(TaskStack stack) { + // Set the new stack + mStack = stack; + if (mStack != null) { + mStack.setCallbacks(this); + } + // Layout again with the new stack + requestLayout(); + } + /** Sets the debug overlay */ public void setDebugOverlay(DebugOverlayView overlay) { mDebugOverlay = overlay; } + /** Resets this TaskStackView for reuse. */ + void reset() { + // Reset the focused task + resetFocusedTask(); + + // Return all the views to the pool + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) getChildAt(i); + mViewPool.returnViewToPool(tv); + } + + // Mark each task view for relayout + if (mViewPool != null) { + Iterator<TaskView> iter = mViewPool.poolViewIterator(); + if (iter != null) { + while (iter.hasNext()) { + TaskView tv = iter.next(); + tv.reset(); + } + } + } + + // Reset the stack state + mStack.reset(); + mStackViewsDirty = true; + mStackViewsClipDirty = true; + mAwaitingFirstLayout = true; + mPrevAccessibilityFocusedIndex = -1; + if (mUIDozeTrigger != null) { + mUIDozeTrigger.stopDozing(); + mUIDozeTrigger.resetTrigger(); + } + mStackScroller.reset(); + } + /** Requests that the views be synchronized with the model */ void requestSynchronizeStackViewsWithModel() { requestSynchronizeStackViewsWithModel(0); @@ -199,11 +232,6 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal float stackScroll, int[] visibleRangeOut, boolean boundTranslationsToRect) { - // XXX: We should be intelligent about where to look for the visible stack range using the - // current stack scroll. - // XXX: We should log extra cases like the ones below where we don't expect to hit very often - // XXX: Print out approximately how many indices we have to go through to find the first visible transform - int taskTransformCount = taskTransforms.size(); int taskCount = tasks.size(); int frontMostVisibleIndex = -1; @@ -255,20 +283,6 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal return frontMostVisibleIndex != -1 && backMostVisibleIndex != -1; } - /** - * Gets the stack transforms of a list of tasks, and returns the visible range of tasks. This - * call is less optimal than calling updateStackTransforms directly. - */ - private ArrayList<TaskViewTransform> getStackTransforms(ArrayList<Task> tasks, - float stackScroll, - int[] visibleRangeOut, - boolean boundTranslationsToRect) { - ArrayList<TaskViewTransform> taskTransforms = new ArrayList<TaskViewTransform>(); - updateStackTransforms(taskTransforms, tasks, stackScroll, visibleRangeOut, - boundTranslationsToRect); - return taskTransforms; - } - /** Synchronizes the views with the model */ boolean synchronizeStackViewsWithModel() { if (mStackViewsDirty) { @@ -415,7 +429,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } /** Focuses the task at the specified index in the stack */ - void focusTask(int taskIndex, boolean scrollToNewPosition) { + void focusTask(int taskIndex, boolean scrollToNewPosition, final boolean animateFocusedState) { // Return early if the task is already focused if (taskIndex == mFocusedTaskIndex) return; @@ -427,7 +441,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal TaskView tv = getChildViewForTask(t); Runnable postScrollRunnable = null; if (tv != null) { - tv.setFocusedTask(); + tv.setFocusedTask(animateFocusedState); } else { postScrollRunnable = new Runnable() { @Override @@ -435,7 +449,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal Task t = mStack.getTasks().get(mFocusedTaskIndex); TaskView tv = getChildViewForTask(t); if (tv != null) { - tv.setFocusedTask(); + tv.setFocusedTask(animateFocusedState); } } }; @@ -455,30 +469,77 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } } - /** Focuses the next task in the stack */ - void focusNextTask(boolean forward) { + /** + * Ensures that there is a task focused, if nothing is focused, then we will use the task + * at the center of the visible stack. + */ + public boolean ensureFocusedTask() { + if (mFocusedTaskIndex < 0) { + // If there is no task focused, then find the task that is closes to the center + // of the screen and use that as the currently focused task + int x = mLayoutAlgorithm.mStackVisibleRect.centerX(); + int y = mLayoutAlgorithm.mStackVisibleRect.centerY(); + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) getChildAt(i); + tv.getHitRect(mTmpRect); + if (mTmpRect.contains(x, y)) { + mFocusedTaskIndex = mStack.indexOfTask(tv.getTask()); + break; + } + } + // If we can't find the center task, then use the front most index + if (mFocusedTaskIndex < 0 && childCount > 0) { + mFocusedTaskIndex = childCount - 1; + } + } + return mFocusedTaskIndex >= 0; + } + + /** + * Focuses the next task in the stack. + * @param animateFocusedState determines whether to actually draw the highlight along with + * the change in focus, as well as whether to scroll to fit the + * task into view. + */ + public void focusNextTask(boolean forward, boolean animateFocusedState) { // Find the next index to focus int numTasks = mStack.getTaskCount(); if (numTasks == 0) return; - int nextFocusIndex = numTasks - 1; - if (0 <= mFocusedTaskIndex && mFocusedTaskIndex < numTasks) { - nextFocusIndex = Math.max(0, Math.min(numTasks - 1, - mFocusedTaskIndex + (forward ? -1 : 1))); + int direction = (forward ? -1 : 1); + int newIndex = mFocusedTaskIndex + direction; + if (newIndex >= 0 && newIndex <= (numTasks - 1)) { + newIndex = Math.max(0, Math.min(numTasks - 1, newIndex)); + focusTask(newIndex, true, animateFocusedState); } - focusTask(nextFocusIndex, true); } /** Dismisses the focused task. */ public void dismissFocusedTask() { - // Return early if there is no focused task index - if (mFocusedTaskIndex < 0) return; + // Return early if the focused task index is invalid + if (mFocusedTaskIndex < 0 || mFocusedTaskIndex >= mStack.getTaskCount()) { + mFocusedTaskIndex = -1; + return; + } Task t = mStack.getTasks().get(mFocusedTaskIndex); TaskView tv = getChildViewForTask(t); tv.dismissTask(); } + /** Resets the focused task. */ + void resetFocusedTask() { + if ((0 <= mFocusedTaskIndex) && (mFocusedTaskIndex < mStack.getTaskCount())) { + Task t = mStack.getTasks().get(mFocusedTaskIndex); + TaskView tv = getChildViewForTask(t); + if (tv != null) { + tv.unsetFocusedTask(); + } + } + mFocusedTaskIndex = -1; + } + @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); @@ -506,6 +567,11 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } @Override + public boolean onGenericMotionEvent(MotionEvent ev) { + return mTouchHandler.onGenericMotionEvent(ev); + } + + @Override public void computeScroll() { mStackScroller.computeScroll(); // Synchronize the views @@ -536,6 +602,14 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } /** + * Computes the maximum number of visible tasks and thumbnails. Requires that + * updateMinMaxScrollForStack() is called first. + */ + public TaskStackViewLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() { + return mLayoutAlgorithm.computeStackVisibilityReport(mStack.getTasks()); + } + + /** * This is called with the full window width and height to allow stack view children to * perform the full screen transition down. */ @@ -562,22 +636,18 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { TaskView tv = (TaskView) getChildAt(i); - if (tv.isFullScreenView()) { - tv.measure(widthMeasureSpec, heightMeasureSpec); + if (tv.getBackground() != null) { + tv.getBackground().getPadding(mTmpRect); } else { - if (tv.getBackground() != null) { - tv.getBackground().getPadding(mTmpRect); - } else { - mTmpRect.setEmpty(); - } - tv.measure( - MeasureSpec.makeMeasureSpec( - mLayoutAlgorithm.mTaskRect.width() + mTmpRect.left + mTmpRect.right, - MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - mLayoutAlgorithm.mTaskRect.height() + mTmpRect.top + mTmpRect.bottom + - tv.getMaxFooterHeight(), MeasureSpec.EXACTLY)); + mTmpRect.setEmpty(); } + tv.measure( + MeasureSpec.makeMeasureSpec( + mLayoutAlgorithm.mTaskRect.width() + mTmpRect.left + mTmpRect.right, + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec( + mLayoutAlgorithm.mTaskRect.height() + mTmpRect.top + mTmpRect.bottom, + MeasureSpec.EXACTLY)); } setMeasuredDimension(width, height); @@ -594,20 +664,15 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { TaskView tv = (TaskView) getChildAt(i); - if (tv.isFullScreenView()) { - tv.layout(left, top, left + tv.getMeasuredWidth(), top + tv.getMeasuredHeight()); + if (tv.getBackground() != null) { + tv.getBackground().getPadding(mTmpRect); } else { - if (tv.getBackground() != null) { - tv.getBackground().getPadding(mTmpRect); - } else { - mTmpRect.setEmpty(); - } - tv.layout(mLayoutAlgorithm.mTaskRect.left - mTmpRect.left, - mLayoutAlgorithm.mTaskRect.top - mTmpRect.top, - mLayoutAlgorithm.mTaskRect.right + mTmpRect.right, - mLayoutAlgorithm.mTaskRect.bottom + mTmpRect.bottom + - tv.getMaxFooterHeight()); + mTmpRect.setEmpty(); } + tv.layout(mLayoutAlgorithm.mTaskRect.left - mTmpRect.left, + mLayoutAlgorithm.mTaskRect.top - mTmpRect.top, + mLayoutAlgorithm.mTaskRect.right + mTmpRect.right, + mLayoutAlgorithm.mTaskRect.bottom + mTmpRect.bottom); } if (mAwaitingFirstLayout) { @@ -650,14 +715,20 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mStartEnterAnimationContext = null; } - // When Alt-Tabbing, we scroll to and focus the previous task + // When Alt-Tabbing, focus the previous task (but leave the animation until we finish the + // enter animation). if (mConfig.launchedWithAltTab) { - if (mConfig.launchedFromHome) { - focusTask(Math.max(0, mStack.getTaskCount() - 1), false); + if (mConfig.launchedFromAppWithThumbnail) { + focusTask(Math.max(0, mStack.getTaskCount() - 2), false, + mConfig.launchedHasConfigurationChanged); } else { - focusTask(Math.max(0, mStack.getTaskCount() - 2), false); + focusTask(Math.max(0, mStack.getTaskCount() - 1), false, + mConfig.launchedHasConfigurationChanged); } } + + // Start dozing + mUIDozeTrigger.startDozing(); } /** Requests this task stacks to start it's enter-recents animation */ @@ -702,16 +773,27 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal @Override public void run() { mStartEnterAnimationCompleted = true; - // Start dozing - mUIDozeTrigger.startDozing(); - // Focus the first view if accessibility is enabled + // Poke the dozer to restart the trigger after the animation completes + mUIDozeTrigger.poke(); + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); int childCount = getChildCount(); - if (childCount > 0 && ssp.isTouchExplorationEnabled()) { - TaskView tv = ((TaskView) getChildAt(childCount - 1)); - tv.requestAccessibilityFocus(); - mPrevAccessibilityFocusedIndex = mStack.indexOfTask(tv.getTask()); + if (childCount > 0) { + // Focus the first view if accessibility is enabled + if (ssp.isTouchExplorationEnabled()) { + TaskView tv = ((TaskView) getChildAt(childCount - 1)); + tv.requestAccessibilityFocus(); + mPrevAccessibilityFocusedIndex = mStack.indexOfTask(tv.getTask()); + } + } + + // Start the focus animation when alt-tabbing + if (mConfig.launchedWithAltTab && !mConfig.launchedHasConfigurationChanged) { + View tv = getChildAt(mFocusedTaskIndex); + if (tv != null) { + ((TaskView) tv).setFocusedTask(true); + } } } }); @@ -731,9 +813,6 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal TaskView tv = (TaskView) getChildAt(i); tv.startExitToHomeAnimation(ctx); } - - // Add a runnable to the post animation ref counter to clear all the views - ctx.postAnimationTrigger.addLastDecrementRunnable(mReturnAllViewsToPoolRunnable); } /** Animates a task view in this stack as it launches. */ @@ -753,6 +832,11 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } } + /** Final callback after Recents is finally hidden. */ + void onRecentsHidden() { + reset(); + } + public boolean isTransformedTouchPointInView(float x, float y, View child) { return isTransformedTouchPointInView(x, y, child, null); } @@ -811,6 +895,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal TaskView frontTv = getChildViewForTask(newFrontMostTask); if (frontTv != null) { frontTv.onTaskBound(newFrontMostTask); + frontTv.fadeInActionButton(0, mConfig.taskViewEnterFromAppDuration); } } @@ -916,27 +1001,23 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Reset the view properties tv.resetViewProperties(); + + // Reset the clip state of the task view + tv.setClipViewInStack(false); } @Override public void prepareViewToLeavePool(TaskView tv, Task task, boolean isNewView) { + // It is possible for a view to be returned to the view pool before it is laid out, + // which means that we will need to relayout the view when it is first used next. + boolean requiresRelayout = tv.getWidth() <= 0 && !isNewView; + // Rebind the task and request that this task's data be filled into the TaskView tv.onTaskBound(task); - // Mark the launch task as fullscreen - if (Constants.DebugFlags.App.EnableScreenshotAppTransition && mAwaitingFirstLayout) { - if (task.isLaunchTarget) { - tv.setIsFullScreen(true); - } - } - // Load the task data RecentsTaskLoader.getInstance().loadTaskData(task); - // Sanity check, the task view should always be clipping against the stack at this point, - // but just in case, re-enable it here - tv.setClipViewInStack(true); - // If the doze trigger has already fired, then update the state for this task view if (mUIDozeTrigger.hasTriggered()) { tv.setNoUserInteractionState(); @@ -964,13 +1045,17 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Add/attach the view to the hierarchy if (isNewView) { addView(tv, insertIndex); - - // Set the callbacks and listeners for this new view - tv.setTouchEnabled(true); - tv.setCallbacks(this); } else { attachViewToParent(tv, insertIndex, tv.getLayoutParams()); + if (requiresRelayout) { + tv.requestLayout(); + } } + + // Set the new state for this view, including the callbacks and view clipping + tv.setCallbacks(this); + tv.setTouchEnabled(true); + tv.setClipViewInStack(true); } @Override @@ -1018,14 +1103,18 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal tv.getTask().activityLabel)); // Remove the task from the view mStack.removeTask(task); - // If the dismissed task was focused, then we should focus the next task in front + // If the dismissed task was focused, then we should focus the new task in the same index if (taskWasFocused) { ArrayList<Task> tasks = mStack.getTasks(); - int nextTaskIndex = Math.min(tasks.size() - 1, taskIndex); + int nextTaskIndex = Math.min(tasks.size() - 1, taskIndex - 1); if (nextTaskIndex >= 0) { Task nextTask = tasks.get(nextTaskIndex); TaskView nextTv = getChildViewForTask(nextTask); - nextTv.setFocusedTask(); + if (nextTv != null) { + // Focus the next task, and only animate the visible state if we are launched + // from Alt-Tab + nextTv.setFocusedTask(mConfig.launchedWithAltTab); + } } } } @@ -1038,11 +1127,6 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } @Override - public void onTaskViewFullScreenTransitionCompleted() { - requestSynchronizeStackViewsWithModel(); - } - - @Override public void onTaskViewFocusChanged(TaskView tv, boolean focused) { if (focused) { mFocusedTaskIndex = mStack.indexOfTask(tv.getTask()); @@ -1061,12 +1145,16 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/ @Override - public void onComponentRemoved(HashSet<ComponentName> cns) { + public void onPackagesChanged(RecentsPackageMonitor monitor, String packageName, int userId) { + // Compute which components need to be removed + HashSet<ComponentName> removedComponents = monitor.computeComponentsRemoved( + mStack.getTaskKeys(), packageName, userId); + // For other tasks, just remove them directly if they no longer exist ArrayList<Task> tasks = mStack.getTasks(); for (int i = tasks.size() - 1; i >= 0; i--) { final Task t = tasks.get(i); - if (cns.contains(t.key.baseIntent.getComponent())) { + if (removedComponents.contains(t.key.baseIntent.getComponent())) { TaskView tv = getChildViewForTask(t); if (tv != null) { // For visible children, defer removing the task until after the animation diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java index 31fc701..49b9129 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java @@ -35,6 +35,18 @@ public class TaskStackViewLayoutAlgorithm { // These are all going to change static final float StackPeekMinScale = 0.8f; // The min scale of the last card in the peek area + // A report of the visibility state of the stack + public class VisibilityReport { + public int numVisibleTasks; + public int numVisibleThumbnails; + + /** Package level ctor */ + VisibilityReport(int tasks, int thumbnails) { + numVisibleTasks = tasks; + numVisibleThumbnails = thumbnails; + } + } + RecentsConfiguration mConfig; // The various rects that define the stack view @@ -117,12 +129,12 @@ public class TaskStackViewLayoutAlgorithm { float pTaskHeightOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - taskHeight); float pNavBarOffset = pAtBottomOfStackRect - - screenYToCurveProgress(mStackVisibleRect.bottom - (mStackVisibleRect.bottom - mStackRect.bottom)); + screenYToCurveProgress(mStackVisibleRect.bottom - (mStackVisibleRect.bottom - + mStackRect.bottom)); // Update the task offsets float pAtBackMostCardTop = 0.5f; float pAtFrontMostCardTop = pAtBackMostCardTop; - float pAtSecondFrontMostCardTop = pAtBackMostCardTop; int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); @@ -130,42 +142,89 @@ public class TaskStackViewLayoutAlgorithm { if (i < (taskCount - 1)) { // Increment the peek height - float pPeek = task.group.isFrontMostTask(task) ? pBetweenAffiliateOffset : - pWithinAffiliateOffset; - pAtSecondFrontMostCardTop = pAtFrontMostCardTop; + float pPeek = task.group.isFrontMostTask(task) ? + pBetweenAffiliateOffset : pWithinAffiliateOffset; pAtFrontMostCardTop += pPeek; } } mMaxScrollP = pAtFrontMostCardTop - ((1f - pTaskHeightOffset - pNavBarOffset)); mMinScrollP = tasks.size() == 1 ? Math.max(mMaxScrollP, 0f) : 0f; - if (launchedWithAltTab) { - if (launchedFromHome) { - // Center the top most task, since that will be focused first - mInitialScrollP = pAtSecondFrontMostCardTop - 0.5f; - } else { - // Center the second top most task, since that will be focused first - mInitialScrollP = pAtSecondFrontMostCardTop - 0.5f; - } + if (launchedWithAltTab && launchedFromHome) { + // Center the top most task, since that will be focused first + mInitialScrollP = mMaxScrollP; } else { mInitialScrollP = pAtFrontMostCardTop - 0.825f; } - mInitialScrollP = Math.max(0, mInitialScrollP); + mInitialScrollP = Math.min(mMaxScrollP, Math.max(0, mInitialScrollP)); + } + + /** + * Computes the maximum number of visible tasks and thumbnails. Requires that + * computeMinMaxScroll() is called first. + */ + public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) { + if (tasks.size() <= 1) { + return new VisibilityReport(1, 1); + } + + // Walk backwards in the task stack and count the number of tasks and visible thumbnails + int taskHeight = mTaskRect.height(); + int numVisibleTasks = 1; + int numVisibleThumbnails = 1; + float progress = mTaskProgressMap.get(tasks.get(tasks.size() - 1).key) - mInitialScrollP; + int prevScreenY = curveProgressToScreenY(progress); + for (int i = tasks.size() - 2; i >= 0; i--) { + Task task = tasks.get(i); + progress = mTaskProgressMap.get(task.key) - mInitialScrollP; + if (progress < 0) { + break; + } + boolean isFrontMostTaskInGroup = task.group.isFrontMostTask(task); + if (isFrontMostTaskInGroup) { + float scaleAtP = curveProgressToScale(progress); + int scaleYOffsetAtP = (int) (((1f - scaleAtP) * taskHeight) / 2); + int screenY = curveProgressToScreenY(progress) + scaleYOffsetAtP; + boolean hasVisibleThumbnail = (prevScreenY - screenY) > mConfig.taskBarHeight; + if (hasVisibleThumbnail) { + numVisibleThumbnails++; + numVisibleTasks++; + prevScreenY = screenY; + } else { + // Once we hit the next front most task that does not have a visible thumbnail, + // walk through remaining visible set + for (int j = i; j >= 0; j--) { + numVisibleTasks++; + progress = mTaskProgressMap.get(tasks.get(j).key) - mInitialScrollP; + if (progress < 0) { + break; + } + } + break; + } + } else if (!isFrontMostTaskInGroup) { + // Affiliated task, no thumbnail + numVisibleTasks++; + } + } + return new VisibilityReport(numVisibleTasks, numVisibleThumbnails); } /** Update/get the transform */ - public TaskViewTransform getStackTransform(Task task, float stackScroll, TaskViewTransform transformOut, - TaskViewTransform prevTransform) { + public TaskViewTransform getStackTransform(Task task, float stackScroll, + TaskViewTransform transformOut, TaskViewTransform prevTransform) { // Return early if we have an invalid index if (task == null || !mTaskProgressMap.containsKey(task.key)) { transformOut.reset(); return transformOut; } - return getStackTransform(mTaskProgressMap.get(task.key), stackScroll, transformOut, prevTransform); + return getStackTransform(mTaskProgressMap.get(task.key), stackScroll, transformOut, + prevTransform); } /** Update/get the transform */ - public TaskViewTransform getStackTransform(float taskProgress, float stackScroll, TaskViewTransform transformOut, TaskViewTransform prevTransform) { + public TaskViewTransform getStackTransform(float taskProgress, float stackScroll, + TaskViewTransform transformOut, TaskViewTransform prevTransform) { float pTaskRelative = taskProgress - stackScroll; float pBounded = Math.max(0, Math.min(pTaskRelative, 1f)); // If the task top is outside of the bounds below the screen, then immediately reset it @@ -199,19 +258,16 @@ public class TaskStackViewLayoutAlgorithm { return transformOut; } - /** - * Returns the untransformed task view size. - */ + /** Returns the untransformed task view size. */ public Rect getUntransformedTaskViewSize() { Rect tvSize = new Rect(mTaskRect); tvSize.offsetTo(0, 0); return tvSize; } - /** - * Returns the scroll to such task top = 1f; - */ + /** Returns the scroll to such task top = 1f; */ float getStackScrollForTask(Task t) { + if (!mTaskProgressMap.containsKey(t.key)) return 0f; return mTaskProgressMap.get(t.key); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java index 5852b88..f7067be 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java @@ -23,6 +23,7 @@ import android.animation.ValueAnimator; import android.content.Context; import android.widget.OverScroller; import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.misc.Utilities; /* The scrolling logic for a TaskStackView */ public class TaskStackViewScroller { @@ -38,6 +39,7 @@ public class TaskStackViewScroller { OverScroller mScroller; ObjectAnimator mScrollAnimator; + float mFinalAnimatedScroll; public TaskStackViewScroller(Context context, RecentsConfiguration config, TaskStackViewLayoutAlgorithm layoutAlgorithm) { mConfig = config; @@ -46,6 +48,11 @@ public class TaskStackViewScroller { setStackScroll(getStackScroll()); } + /** Resets the task scroller. */ + void reset() { + mStackScrollP = 0f; + } + /** Sets the callbacks */ void setCallbacks(TaskStackViewScrollerCallbacks cb) { mCb = cb; @@ -69,9 +76,14 @@ public class TaskStackViewScroller { mStackScrollP = s; } - /** Sets the current stack scroll to the initial state when you first enter recents */ - public void setStackScrollToInitialState() { + /** + * Sets the current stack scroll to the initial state when you first enter recents. + * @return whether the stack progress changed. + */ + public boolean setStackScrollToInitialState() { + float prevStackScrollP = mStackScrollP; setStackScroll(getBoundedStackScroll(mLayoutAlgorithm.mInitialScrollP)); + return Float.compare(prevStackScrollP, mStackScrollP) != 0; } /** Bounds the current scroll if necessary */ @@ -128,10 +140,15 @@ public class TaskStackViewScroller { /** Animates the stack scroll */ void animateScroll(float curScroll, float newScroll, final Runnable postRunnable) { - // Abort any current animations + // Finish any current scrolling animations + if (mScrollAnimator != null && mScrollAnimator.isRunning()) { + setStackScroll(mFinalAnimatedScroll); + mScroller.startScroll(0, progressToScrollRange(mFinalAnimatedScroll), 0, 0, 0); + } stopScroller(); stopBoundScrollAnimation(); + mFinalAnimatedScroll = newScroll; mScrollAnimator = ObjectAnimator.ofFloat(this, "stackScroll", curScroll, newScroll); mScrollAnimator.setDuration(mConfig.taskStackScrollDuration); mScrollAnimator.setInterpolator(mConfig.linearOutSlowInInterpolator); @@ -155,10 +172,7 @@ public class TaskStackViewScroller { /** Aborts any current stack scrolls */ void stopBoundScrollAnimation() { - if (mScrollAnimator != null) { - mScrollAnimator.removeAllListeners(); - mScrollAnimator.cancel(); - } + Utilities.cancelAnimationWithoutCallbacks(mScrollAnimator); } /**** OverScroller ****/ diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java index 8f9b4c2..59e38f4 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java @@ -17,6 +17,7 @@ package com.android.systemui.recents.views; import android.content.Context; +import android.view.InputDevice; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; @@ -142,22 +143,21 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { // Initialize the velocity tracker initOrResetVelocityTracker(); mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); - // Check if the scroller is finished yet - mIsScrolling = mScroller.isScrolling(); break; } case MotionEvent.ACTION_MOVE: { if (mActivePointerId == INACTIVE_POINTER_ID) break; + // Initialize the velocity tracker if necessary + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); + int activePointerIndex = ev.findPointerIndex(mActivePointerId); int y = (int) ev.getY(activePointerIndex); int x = (int) ev.getX(activePointerIndex); if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { // Save the touch move info mIsScrolling = true; - // Initialize the velocity tracker if necessary - initVelocityTrackerIfNotExists(); - mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); // Disallow parents from intercepting touch events final ViewParent parent = mSv.getParent(); if (parent != null) { @@ -189,7 +189,6 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { /** Handles touch events once we have intercepted them */ public boolean onTouchEvent(MotionEvent ev) { - // Short circuit if we have no children boolean hasChildren = (mSv.getChildCount() > 0); if (!hasChildren) { @@ -237,6 +236,8 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { case MotionEvent.ACTION_MOVE: { if (mActivePointerId == INACTIVE_POINTER_ID) break; + mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); + int activePointerIndex = ev.findPointerIndex(mActivePointerId); int x = (int) ev.getX(activePointerIndex); int y = (int) ev.getY(activePointerIndex); @@ -246,9 +247,6 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { if (!mIsScrolling) { if (yTotal > mScrollTouchSlop) { mIsScrolling = true; - // Initialize the velocity tracker - initOrResetVelocityTracker(); - mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); // Disallow parents from intercepting touch events final ViewParent parent = mSv.getParent(); if (parent != null) { @@ -267,11 +265,6 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { / maxOverScroll)); } mScroller.setStackScroll(curStackScroll + deltaP); - if (mScroller.isScrollOutOfBounds()) { - mVelocityTracker.clear(); - } else { - mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); - } } mLastMotionX = x; mLastMotionY = y; @@ -280,20 +273,21 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { break; } case MotionEvent.ACTION_UP: { - final VelocityTracker velocityTracker = mVelocityTracker; - velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - int velocity = (int) velocityTracker.getYVelocity(mActivePointerId); + mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) { - int overscrollRange = (int) (Math.min(1f, - Math.abs((float) velocity / mMaximumVelocity)) * - Constants.Values.TaskStackView.TaskStackOverscrollRange); - // Fling scroll - mScroller.mScroller.fling(0, mScroller.progressToScrollRange(mScroller.getStackScroll()), + float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity); + int overscrollRange = (int) (Math.min(1f, overscrollRangePct) * + (Constants.Values.TaskStackView.TaskStackMaxOverscrollRange - + Constants.Values.TaskStackView.TaskStackMinOverscrollRange)); + mScroller.mScroller.fling(0, + mScroller.progressToScrollRange(mScroller.getStackScroll()), 0, velocity, 0, 0, mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP), mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP), - 0, overscrollRange); + 0, Constants.Values.TaskStackView.TaskStackMinOverscrollRange + + overscrollRange); // Invalidate to kick off computeScroll mSv.invalidate(); } else if (mScroller.isScrollOutOfBounds()) { @@ -336,6 +330,30 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { return true; } + /** Handles generic motion events */ + public boolean onGenericMotionEvent(MotionEvent ev) { + if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == + InputDevice.SOURCE_CLASS_POINTER) { + int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_SCROLL: + // Find the front most task and scroll the next task to the front + float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); + if (vScroll > 0) { + if (mSv.ensureFocusedTask()) { + mSv.focusNextTask(true, false); + } + } else { + if (mSv.ensureFocusedTask()) { + mSv.focusNextTask(false, false); + } + } + return true; + } + } + return false; + } + /**** SwipeHelper Implementation ****/ @Override @@ -355,8 +373,6 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { tv.setClipViewInStack(false); // Disallow touch events from this task view tv.setTouchEnabled(false); - // Hide the footer - tv.animateFooterVisibility(false, mSv.mConfig.taskViewLockToAppShortAnimDuration); // Disallow parents from intercepting touch events final ViewParent parent = mSv.getParent(); if (parent != null) { @@ -387,8 +403,6 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { tv.setClipViewInStack(true); // Re-enable touch events from this task view tv.setTouchEnabled(true); - // Restore the footer - tv.animateFooterVisibility(true, mSv.mConfig.taskViewLockToAppShortAnimDuration); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java index 2658176..faa728d 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -21,23 +21,21 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.*; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewOutlineProvider; import android.view.animation.AccelerateInterpolator; import android.widget.FrameLayout; import com.android.systemui.R; -import com.android.systemui.recents.AlternateRecentsComponent; import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; -import com.android.systemui.recents.model.RecentsTaskLoader; +import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.Task; +import com.android.systemui.statusbar.phone.PhoneStatusBar; /* A task view */ public class TaskView extends FrameLayout implements Task.TaskCallbacks, - TaskViewFooter.TaskFooterViewCallbacks, View.OnClickListener, View.OnLongClickListener { + View.OnClickListener, View.OnLongClickListener { /** The TaskView callbacks */ interface TaskViewCallbacks { @@ -46,7 +44,6 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, public void onTaskViewClicked(TaskView tv, Task task, boolean lockToTask); public void onTaskViewDismissed(TaskView tv); public void onTaskViewClipStateChanged(TaskView tv); - public void onTaskViewFullScreenTransitionCompleted(); public void onTaskViewFocusChanged(TaskView tv, boolean focused); } @@ -54,25 +51,23 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, float mTaskProgress; ObjectAnimator mTaskProgressAnimator; - ObjectAnimator mDimAnimator; float mMaxDimScale; - int mDim; + int mDimAlpha; AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(1f); - PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.MULTIPLY); + PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); + Paint mDimLayerPaint = new Paint(); + float mActionButtonTranslationZ; Task mTask; boolean mTaskDataLoaded; boolean mIsFocused; boolean mFocusAnimationsEnabled; - boolean mIsFullScreenView; boolean mClipViewInStack; AnimateableViewBounds mViewBounds; - Paint mLayerPaint = new Paint(); View mContent; TaskViewThumbnail mThumbnailView; TaskViewHeader mHeaderView; - TaskViewFooter mFooterView; View mActionButtonView; TaskViewCallbacks mCb; @@ -117,6 +112,14 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mCb = cb; } + /** Resets this TaskView for reuse. */ + void reset() { + resetViewProperties(); + resetNoUserInteractionState(); + setClipViewInStack(false); + setCallbacks(null); + } + /** Gets the task */ Task getTask() { return mTask; @@ -133,7 +136,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mContent = findViewById(R.id.task_view_content); mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar); mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail); - mThumbnailView.enableTaskBarClip(mHeaderView); + mThumbnailView.updateClipToTaskBar(mHeaderView); mActionButtonView = findViewById(R.id.lock_to_app_fab); mActionButtonView.setOutlineProvider(new ViewOutlineProvider() { @Override @@ -142,9 +145,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, outline.setOval(0, 0, mActionButtonView.getWidth(), mActionButtonView.getHeight()); } }); - if (mFooterView != null) { - mFooterView.setCallbacks(this); - } + mActionButtonTranslationZ = mActionButtonView.getTranslationZ(); } @Override @@ -159,29 +160,16 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mContent.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY)); - // Measure the bar view, thumbnail, and footer + // Measure the bar view, and action button mHeaderView.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mConfig.taskBarHeight, MeasureSpec.EXACTLY)); - if (mFooterView != null) { - mFooterView.measure( - MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(mConfig.taskViewLockToAppButtonHeight, - MeasureSpec.EXACTLY)); - } mActionButtonView.measure( MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.AT_MOST)); - if (mIsFullScreenView) { - // Measure the thumbnail height to be the full dimensions - mThumbnailView.measure( - MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.EXACTLY)); - } else { - // Measure the thumbnail to be square - mThumbnailView.measure( - MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY)); - } + // Measure the thumbnail to be square + mThumbnailView.measure( + MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY)); setMeasuredDimension(width, height); invalidateOutline(); } @@ -193,25 +181,12 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration, ValueAnimator.AnimatorUpdateListener updateCallback) { - // If we are a full screen view, then only update the Z to keep it in order - // XXX: Also update/animate the dim as well - if (mIsFullScreenView) { - if (!mConfig.fakeShadows && - toTransform.hasTranslationZChangedFrom(getTranslationZ())) { - setTranslationZ(toTransform.translationZ); - } - return; - } - // Apply the transform toTransform.applyToTaskView(this, duration, mConfig.fastOutSlowInInterpolator, false, !mConfig.fakeShadows, updateCallback); // Update the task progress - if (mTaskProgressAnimator != null) { - mTaskProgressAnimator.removeAllListeners(); - mTaskProgressAnimator.cancel(); - } + Utilities.cancelAnimationWithoutCallbacks(mTaskProgressAnimator); if (duration <= 0) { setTaskProgress(toTransform.p); } else { @@ -225,7 +200,14 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, /** Resets this view's properties */ void resetViewProperties() { setDim(0); + setLayerType(View.LAYER_TYPE_NONE, null); TaskViewTransform.reset(this); + if (mActionButtonView != null) { + mActionButtonView.setScaleX(1f); + mActionButtonView.setScaleY(1f); + mActionButtonView.setAlpha(1f); + mActionButtonView.setTranslationZ(mActionButtonTranslationZ); + } } /** @@ -253,22 +235,14 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, boolean occludesLaunchTarget, int offscreenY) { int initialDim = getDim(); - if (mConfig.launchedFromAppWithScreenshot) { - if (isTaskViewLaunchTargetTask) { - // Hide the footer during the transition in, and animate it out afterwards? - if (mFooterView != null) { - mFooterView.animateFooterVisibility(false, 0); - } - } else { - // Don't do anything for the side views when animating in - } - + if (mConfig.launchedHasConfigurationChanged) { + // Just load the views as-is } else if (mConfig.launchedFromAppWithThumbnail) { if (isTaskViewLaunchTargetTask) { - // Hide the action button if it exists - mActionButtonView.setAlpha(0f); // Set the dim to 0 so we can animate it in initialDim = 0; + // Hide the action button + mActionButtonView.setAlpha(0f); } else if (occludesLaunchTarget) { // Move the task view off screen (below) so we can animate it in setTranslationY(offscreenY); @@ -292,105 +266,31 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, final TaskViewTransform transform = ctx.currentTaskTransform; int startDelay = 0; - if (mConfig.launchedFromAppWithScreenshot) { - if (mTask.isLaunchTarget) { - Rect taskRect = ctx.currentTaskRect; - int duration = mConfig.taskViewEnterFromHomeDuration * 10; - int windowInsetTop = mConfig.systemInsets.top; // XXX: Should be for the window - float taskScale = ((float) taskRect.width() / getMeasuredWidth()) * transform.scale; - float scaledYOffset = ((1f - taskScale) * getMeasuredHeight()) / 2; - float scaledWindowInsetTop = (int) (taskScale * windowInsetTop); - float scaledTranslationY = taskRect.top + transform.translationY - - (scaledWindowInsetTop + scaledYOffset); - startDelay = mConfig.taskViewEnterFromHomeStaggerDelay; - - // Animate the top clip - mViewBounds.animateClipTop(windowInsetTop, duration, - new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int y = (Integer) animation.getAnimatedValue(); - mHeaderView.setTranslationY(y); - } - }); - // Animate the bottom or right clip - int size = Math.round((taskRect.width() / taskScale)); - if (mConfig.hasHorizontalLayout()) { - mViewBounds.animateClipRight(getMeasuredWidth() - size, duration); - } else { - mViewBounds.animateClipBottom(getMeasuredHeight() - (windowInsetTop + size), duration); - } - // Animate the task bar of the first task view - animate() - .scaleX(taskScale) - .scaleY(taskScale) - .translationY(scaledTranslationY) - .setDuration(duration) - .withEndAction(new Runnable() { - @Override - public void run() { - setIsFullScreen(false); - requestLayout(); - - // Reset the clip - mViewBounds.setClipTop(0); - mViewBounds.setClipBottom(0); - mViewBounds.setClipRight(0); - // Reset the bar translation - mHeaderView.setTranslationY(0); - // Animate the footer into view (if it is the front most task) - animateFooterVisibility(true, mConfig.taskBarEnterAnimDuration); - - // Unbind the thumbnail from the screenshot - RecentsTaskLoader.getInstance().loadTaskData(mTask); - // Recycle the full screen screenshot - AlternateRecentsComponent.consumeLastScreenshot(); - - mCb.onTaskViewFullScreenTransitionCompleted(); - - // Decrement the post animation trigger - ctx.postAnimationTrigger.decrement(); - } - }) - .start(); - } else { - // Animate the footer into view - animateFooterVisibility(true, 0); - } - ctx.postAnimationTrigger.increment(); - - } else if (mConfig.launchedFromAppWithThumbnail) { + if (mConfig.launchedFromAppWithThumbnail) { if (mTask.isLaunchTarget) { // Animate the dim/overlay if (Constants.DebugFlags.App.EnableThumbnailAlphaOnFrontmost) { // Animate the thumbnail alpha before the dim animation (to prevent updating the // hardware layer) - mThumbnailView.startEnterRecentsAnimation(mConfig.taskBarEnterAnimDelay, + mThumbnailView.startEnterRecentsAnimation(mConfig.transitionEnterFromAppDelay, new Runnable() { @Override public void run() { - animateDimToProgress(0, mConfig.taskBarEnterAnimDuration, + animateDimToProgress(0, mConfig.taskViewEnterFromAppDuration, ctx.postAnimationTrigger.decrementOnAnimationEnd()); } }); } else { // Immediately start the dim animation - animateDimToProgress(mConfig.taskBarEnterAnimDelay, - mConfig.taskBarEnterAnimDuration, + animateDimToProgress(mConfig.transitionEnterFromAppDelay, + mConfig.taskViewEnterFromAppDuration, ctx.postAnimationTrigger.decrementOnAnimationEnd()); } ctx.postAnimationTrigger.increment(); - // Animate the footer into view - animateFooterVisibility(true, mConfig.taskBarEnterAnimDuration); - // Animate the action button in - mActionButtonView.animate().alpha(1f) - .setStartDelay(mConfig.taskBarEnterAnimDelay) - .setDuration(mConfig.taskBarEnterAnimDuration) - .setInterpolator(mConfig.fastOutLinearInInterpolator) - .withLayer() - .start(); + fadeInActionButton(mConfig.transitionEnterFromAppDelay, + mConfig.taskViewEnterFromAppDuration); } else { // Animate the task up if it was occluding the launch target if (ctx.currentTaskOccludesLaunchTarget) { @@ -398,7 +298,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, setAlpha(0f); animate().alpha(1f) .translationY(transform.translationY) - .setStartDelay(mConfig.taskBarEnterAnimDelay) + .setStartDelay(mConfig.transitionEnterFromAppDelay) .setUpdateListener(null) .setInterpolator(mConfig.fastOutSlowInInterpolator) .setDuration(mConfig.taskViewEnterFromHomeDuration) @@ -413,12 +313,12 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, ctx.postAnimationTrigger.increment(); } } - startDelay = mConfig.taskBarEnterAnimDelay; + startDelay = mConfig.transitionEnterFromAppDelay; } else if (mConfig.launchedFromHome) { // Animate the tasks up int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1); - int delay = mConfig.taskViewEnterFromHomeDelay + + int delay = mConfig.transitionEnterFromHomeDelay + frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay; setScaleX(transform.scale); @@ -442,14 +342,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, }) .start(); ctx.postAnimationTrigger.increment(); - - // Animate the footer into view - animateFooterVisibility(true, mConfig.taskViewEnterFromHomeDuration); startDelay = delay; - - } else { - // Animate the footer into view - animateFooterVisibility(true, 0); } // Enable the focus animations from this point onwards so that they aren't affected by the @@ -459,7 +352,20 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, public void run() { enableFocusAnimations(); } - }, (startDelay / 2)); + }, startDelay); + } + + public void fadeInActionButton(int delay, int duration) { + // Hide the action button + mActionButtonView.setAlpha(0f); + + // Animate the action button in + mActionButtonView.animate().alpha(1f) + .setStartDelay(delay) + .setDuration(duration) + .setInterpolator(PhoneStatusBar.ALPHA_IN) + .withLayer() + .start(); } /** Animates this task view as it leaves recents by pressing home. */ @@ -483,9 +389,9 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mThumbnailView.startLaunchTaskAnimation(postAnimRunnable); // Animate the dim - if (mDim > 0) { + if (mDimAlpha > 0) { ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0); - anim.setDuration(mConfig.taskBarExitAnimDuration); + anim.setDuration(mConfig.taskViewExitToAppDuration); anim.setInterpolator(mConfig.fastOutLinearInInterpolator); anim.start(); } @@ -500,7 +406,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mActionButtonView.animate() .alpha(0f) .setStartDelay(0) - .setDuration(mConfig.taskBarExitAnimDuration) + .setDuration(mConfig.taskViewExitToAppDuration) .setInterpolator(mConfig.fastOutLinearInInterpolator) .withLayer() .start(); @@ -515,7 +421,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, .setStartDelay(0) .setUpdateListener(null) .setInterpolator(mConfig.fastOutLinearInInterpolator) - .setDuration(mConfig.taskBarExitAnimDuration) + .setDuration(mConfig.taskViewExitToAppDuration) .start(); } } @@ -559,6 +465,11 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mHeaderView.setNoUserInteractionState(); } + /** Resets the state tracking that the user has not interacted with the stack after a certain time. */ + void resetNoUserInteractionState() { + mHeaderView.resetNoUserInteractionState(); + } + /** Dismisses this task. */ void dismissTask() { // Animate out the view and call the callback @@ -566,26 +477,11 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, startDeleteTaskAnimation(new Runnable() { @Override public void run() { - mCb.onTaskViewDismissed(tv); + if (mCb != null) { + mCb.onTaskViewDismissed(tv); + } } }); - // Hide the footer - animateFooterVisibility(false, mConfig.taskViewRemoveAnimDuration); - } - - /** Sets whether this task view is full screen or not. */ - void setIsFullScreen(boolean isFullscreen) { - mIsFullScreenView = isFullscreen; - mHeaderView.setIsFullscreen(isFullscreen); - if (isFullscreen) { - // If we are full screen, then disable the bottom outline clip for the footer - mViewBounds.setOutlineClipBottom(0); - } - } - - /** Returns whether this task view should currently be drawn as a full screen view. */ - boolean isFullScreenView() { - return mIsFullScreenView; } /** @@ -593,35 +489,16 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, * view. */ boolean shouldClipViewInStack() { - return mClipViewInStack && !mIsFullScreenView && (getVisibility() == View.VISIBLE); + return mClipViewInStack && (getVisibility() == View.VISIBLE); } /** Sets whether this view should be clipped, or clipped against. */ void setClipViewInStack(boolean clip) { if (clip != mClipViewInStack) { mClipViewInStack = clip; - mCb.onTaskViewClipStateChanged(this); - } - } - - /** Gets the max footer height. */ - public int getMaxFooterHeight() { - if (mFooterView != null) { - return mFooterView.mMaxFooterHeight; - } else { - return 0; - } - } - - /** Animates the footer into and out of view. */ - void animateFooterVisibility(boolean visible, int duration) { - // Hide the footer if we are a full screen view - if (mIsFullScreenView) return; - // Hide the footer if the current task can not be locked to - if (!mTask.lockToTaskEnabled || !mTask.lockToThisTask) return; - // Otherwise, animate the visibility - if (mFooterView != null) { - mFooterView.animateFooterVisibility(visible, duration); + if (mCb != null) { + mCb.onTaskViewClipStateChanged(this); + } } } @@ -639,26 +516,16 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, /** Returns the current dim. */ public void setDim(int dim) { - mDim = dim; - if (mDimAnimator != null) { - mDimAnimator.removeAllListeners(); - mDimAnimator.cancel(); - } + mDimAlpha = dim; if (mConfig.useHardwareLayers) { // Defer setting hardware layers if we have not yet measured, or there is no dim to draw if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) { - if (mDimAnimator != null) { - mDimAnimator.removeAllListeners(); - mDimAnimator.cancel(); - } - - int inverse = 255 - mDim; - mDimColorFilter.setColor(Color.argb(0xFF, inverse, inverse, inverse)); - mLayerPaint.setColorFilter(mDimColorFilter); - mContent.setLayerType(LAYER_TYPE_HARDWARE, mLayerPaint); + mDimColorFilter.setColor(Color.argb(mDimAlpha, 0, 0, 0)); + mDimLayerPaint.setColorFilter(mDimColorFilter); + mContent.setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); } } else { - float dimAlpha = mDim / 255.0f; + float dimAlpha = mDimAlpha / 255.0f; if (mThumbnailView != null) { mThumbnailView.setDimAlpha(dimAlpha); } @@ -670,7 +537,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, /** Returns the current dim. */ public int getDim() { - return mDim; + return mDimAlpha; } /** Animates the dim to the task progress. */ @@ -706,16 +573,18 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, * if the view is not currently visible, or we are in touch state (where we still want to keep * track of focus). */ - public void setFocusedTask() { + public void setFocusedTask(boolean animateFocusedState) { mIsFocused = true; if (mFocusAnimationsEnabled) { // Focus the header bar - mHeaderView.onTaskViewFocusChanged(true); + mHeaderView.onTaskViewFocusChanged(true, animateFocusedState); } // Update the thumbnail alpha with the focus mThumbnailView.onFocusChanged(true); // Call the callback - mCb.onTaskViewFocusChanged(this, true); + if (mCb != null) { + mCb.onTaskViewFocusChanged(this, true); + } // Workaround, we don't always want it focusable in touch mode, but we want the first task // to be focused after the enter-recents animation, which can be triggered from either touch // or keyboard @@ -732,13 +601,15 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mIsFocused = false; if (mFocusAnimationsEnabled) { // Un-focus the header bar - mHeaderView.onTaskViewFocusChanged(false); + mHeaderView.onTaskViewFocusChanged(false, true); } // Update the thumbnail alpha with the focus mThumbnailView.onFocusChanged(false); // Call the callback - mCb.onTaskViewFocusChanged(this, false); + if (mCb != null) { + mCb.onTaskViewFocusChanged(this, false); + } invalidate(); } @@ -766,7 +637,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mFocusAnimationsEnabled = true; if (mIsFocused && !wasFocusAnimationsEnabled) { // Re-notify the header if we were focused and animations were not previously enabled - mHeaderView.onTaskViewFocusChanged(true); + mHeaderView.onTaskViewFocusChanged(true, true); } } @@ -776,15 +647,12 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, public void onTaskBound(Task t) { mTask = t; mTask.setCallbacks(this); - if (getMeasuredWidth() == 0) { - // If we haven't yet measured, we should just set the footer height with any animation - animateFooterVisibility(t.lockToThisTask, 0); - } else { - animateFooterVisibility(t.lockToThisTask, mConfig.taskViewLockToAppLongAnimDuration); - } - // Hide the action button if lock to app is disabled - if (!t.lockToTaskEnabled && mActionButtonView.getVisibility() != View.GONE) { - mActionButtonView.setVisibility(View.GONE); + + // Hide the action button if lock to app is disabled for this view + int lockButtonVisibility = (!t.lockToTaskEnabled || !t.lockToThisTask) ? GONE : VISIBLE; + if (mActionButtonView.getVisibility() != lockButtonVisibility) { + mActionButtonView.setVisibility(lockButtonVisibility); + requestLayout(); } } @@ -792,18 +660,11 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, public void onTaskDataLoaded() { if (mThumbnailView != null && mHeaderView != null) { // Bind each of the views to the new task data - if (mIsFullScreenView) { - mThumbnailView.bindToScreenshot(AlternateRecentsComponent.getLastScreenshot()); - } else { - mThumbnailView.rebindToTask(mTask); - } + mThumbnailView.rebindToTask(mTask); mHeaderView.rebindToTask(mTask); // Rebind any listeners mHeaderView.mApplicationIcon.setOnClickListener(this); mHeaderView.mDismissButton.setOnClickListener(this); - if (mFooterView != null) { - mFooterView.setOnClickListener(this); - } mActionButtonView.setOnClickListener(this); if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { if (mConfig.developerOptionsEnabled) { @@ -824,9 +685,6 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, // Unbind any listeners mHeaderView.mApplicationIcon.setOnClickListener(null); mHeaderView.mDismissButton.setOnClickListener(null); - if (mFooterView != null) { - mFooterView.setOnClickListener(null); - } mActionButtonView.setOnClickListener(null); if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { mHeaderView.mApplicationIcon.setOnLongClickListener(null); @@ -840,19 +698,6 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, setOnClickListener(enabled ? this : null); } - /**** TaskViewFooter.TaskFooterViewCallbacks ****/ - - @Override - public void onTaskFooterHeightChanged(int height, int maxHeight) { - if (mIsFullScreenView) { - // Disable the bottom outline clip when fullscreen - mViewBounds.setOutlineClipBottom(0); - } else { - // Update the bottom clip in our outline provider - mViewBounds.setOutlineClipBottom(maxHeight - height); - } - } - /**** View.OnClickListener Implementation ****/ @Override @@ -865,7 +710,9 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, @Override public void run() { if (Constants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) { - mCb.onTaskViewAppIconClicked(tv); + if (mCb != null) { + mCb.onTaskViewAppIconClicked(tv); + } } else if (v == mHeaderView.mDismissButton) { dismissTask(); } @@ -876,8 +723,9 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, // Reset the translation of the action button before we animate it out mActionButtonView.setTranslationZ(0f); } - mCb.onTaskViewClicked(tv, tv.getTask(), - (v == mFooterView || v == mActionButtonView)); + if (mCb != null) { + mCb.onTaskViewClicked(tv, tv.getTask(), (v == mActionButtonView)); + } } } @@ -886,8 +734,10 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, @Override public boolean onLongClick(View v) { if (v == mHeaderView.mApplicationIcon) { - mCb.onTaskViewAppInfoClicked(this); - return true; + if (mCb != null) { + mCb.onTaskViewAppInfoClicked(this); + return true; + } } return false; } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewFooter.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewFooter.java deleted file mode 100644 index 324169e..0000000 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewFooter.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2014 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.recents.views; - -import android.animation.ObjectAnimator; -import android.content.Context; -import android.util.AttributeSet; -import android.widget.FrameLayout; -import com.android.systemui.recents.RecentsConfiguration; - - -/** The task footer view */ -public class TaskViewFooter extends FrameLayout { - - interface TaskFooterViewCallbacks { - public void onTaskFooterHeightChanged(int height, int maxHeight); - } - - RecentsConfiguration mConfig; - - TaskFooterViewCallbacks mCb; - int mFooterHeight; - int mMaxFooterHeight; - ObjectAnimator mFooterAnimator; - - public TaskViewFooter(Context context) { - this(context, null); - } - - public TaskViewFooter(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public TaskViewFooter(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public TaskViewFooter(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - mConfig = RecentsConfiguration.getInstance(); - mMaxFooterHeight = mConfig.taskViewLockToAppButtonHeight; - setFooterHeight(getFooterHeight()); - } - - /** Sets the callbacks for when the footer height changes. */ - void setCallbacks(TaskFooterViewCallbacks cb) { - mCb = cb; - mCb.onTaskFooterHeightChanged(mFooterHeight, mMaxFooterHeight); - } - - /** Sets the footer height. */ - public void setFooterHeight(int footerHeight) { - if (footerHeight != mFooterHeight) { - mFooterHeight = footerHeight; - mCb.onTaskFooterHeightChanged(footerHeight, mMaxFooterHeight); - } - } - - /** Gets the footer height. */ - public int getFooterHeight() { - return mFooterHeight; - } - - /** Animates the footer into and out of view. */ - void animateFooterVisibility(final boolean visible, int duration) { - // Return early if there is no footer - if (mMaxFooterHeight <= 0) return; - - // Cancel the previous animation - if (mFooterAnimator != null) { - mFooterAnimator.removeAllListeners(); - mFooterAnimator.cancel(); - } - int finalHeight = visible ? mMaxFooterHeight : 0; - if (duration > 0) { - mFooterAnimator = ObjectAnimator.ofInt(this, "footerHeight", finalHeight); - mFooterAnimator.setDuration(duration); - mFooterAnimator.setInterpolator(mConfig.fastOutSlowInInterpolator); - mFooterAnimator.start(); - } else { - setFooterHeight(finalHeight); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java index 396d441..05f6f40 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java @@ -36,7 +36,6 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.RippleDrawable; -import android.graphics.drawable.ShapeDrawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -56,25 +55,28 @@ public class TaskViewHeader extends FrameLayout { RecentsConfiguration mConfig; + // Header views ImageView mDismissButton; ImageView mApplicationIcon; TextView mActivityDescription; - RippleDrawable mBackground; - GradientDrawable mBackgroundColorDrawable; + // Header drawables + boolean mCurrentPrimaryColorIsDark; + int mCurrentPrimaryColor; int mBackgroundColor; Drawable mLightDismissDrawable; Drawable mDarkDismissDrawable; + RippleDrawable mBackground; + GradientDrawable mBackgroundColorDrawable; AnimatorSet mFocusAnimator; - ValueAnimator backgroundColorAnimator; - PorterDuffColorFilter mDimFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); - - boolean mIsFullscreen; - boolean mCurrentPrimaryColorIsDark; - int mCurrentPrimaryColor; + String mDismissContentDescription; + // Static highlight that we draw at the top of each view static Paint sHighlightPaint; - private Paint mDimPaint = new Paint(); + + // Header dim, which is only used when task view hardware layers are not used + Paint mDimLayerPaint = new Paint(); + PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); public TaskViewHeader(Context context) { this(context, null); @@ -104,6 +106,8 @@ public class TaskViewHeader extends FrameLayout { Resources res = context.getResources(); mLightDismissDrawable = res.getDrawable(R.drawable.recents_dismiss_light); mDarkDismissDrawable = res.getDrawable(R.drawable.recents_dismiss_dark); + mDismissContentDescription = + res.getString(R.string.accessibility_recents_item_will_be_dismissed); // Configure the highlight paint if (sHighlightPaint == null) { @@ -126,14 +130,6 @@ public class TaskViewHeader extends FrameLayout { @Override protected void onFinishInflate() { - // Set the outline provider - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRect(0, 0, getMeasuredWidth(), getMeasuredHeight()); - } - }); - // Initialize the icon and description views mApplicationIcon = (ImageView) findViewById(R.id.application_icon); mActivityDescription = (TextView) findViewById(R.id.activity_description); @@ -159,21 +155,14 @@ public class TaskViewHeader extends FrameLayout { @Override protected void onDraw(Canvas canvas) { - if (!mIsFullscreen) { - // Draw the highlight at the top edge (but put the bottom edge just out of view) - float offset = (float) Math.ceil(mConfig.taskViewHighlightPx / 2f); - float radius = mConfig.taskViewRoundedCornerRadiusPx; - int count = canvas.save(Canvas.CLIP_SAVE_FLAG); - canvas.clipRect(0, 0, getMeasuredWidth(), getMeasuredHeight()); - canvas.drawRoundRect(-offset, 0f, (float) getMeasuredWidth() + offset, - getMeasuredHeight() + radius, radius, radius, sHighlightPaint); - canvas.restoreToCount(count); - } - } - - /** Sets whether the current task is full screen or not. */ - void setIsFullscreen(boolean isFullscreen) { - mIsFullscreen = isFullscreen; + // Draw the highlight at the top edge (but put the bottom edge just out of view) + float offset = (float) Math.ceil(mConfig.taskViewHighlightPx / 2f); + float radius = mConfig.taskViewRoundedCornerRadiusPx; + int count = canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipRect(0, 0, getMeasuredWidth(), getMeasuredHeight()); + canvas.drawRoundRect(-offset, 0f, (float) getMeasuredWidth() + offset, + getMeasuredHeight() + radius, radius, radius, sHighlightPaint); + canvas.restoreToCount(count); } @Override @@ -181,6 +170,16 @@ public class TaskViewHeader extends FrameLayout { return false; } + /** + * Sets the dim alpha, only used when we are not using hardware layers. + * (see RecentsConfiguration.useHardwareLayers) + */ + void setDimAlpha(int alpha) { + mDimColorFilter.setColor(Color.argb(alpha, 0, 0, 0)); + mDimLayerPaint.setColorFilter(mDimColorFilter); + setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); + } + /** Returns the secondary color for a primary color. */ int getSecondaryColor(int primaryColor, boolean useLightOverlayColor) { int overlayColor = useLightOverlayColor ? Color.WHITE : Color.BLACK; @@ -213,9 +212,8 @@ public class TaskViewHeader extends FrameLayout { mConfig.taskBarViewLightTextColor : mConfig.taskBarViewDarkTextColor); mDismissButton.setImageDrawable(t.useLightOnPrimaryColor ? mLightDismissDrawable : mDarkDismissDrawable); - mDismissButton.setContentDescription( - getContext().getString(R.string.accessibility_recents_item_will_be_dismissed, - t.activityLabel)); + mDismissButton.setContentDescription(String.format(mDismissContentDescription, + t.activityLabel)); } /** Unbinds the bar view from the task */ @@ -231,7 +229,7 @@ public class TaskViewHeader extends FrameLayout { .alpha(0f) .setStartDelay(0) .setInterpolator(mConfig.fastOutSlowInInterpolator) - .setDuration(mConfig.taskBarExitAnimDuration) + .setDuration(mConfig.taskViewExitToAppDuration) .withLayer() .start(); } @@ -239,15 +237,17 @@ public class TaskViewHeader extends FrameLayout { /** Animates this task bar if the user does not interact with the stack after a certain time. */ void startNoUserInteractionAnimation() { - mDismissButton.setVisibility(View.VISIBLE); - mDismissButton.setAlpha(0f); - mDismissButton.animate() - .alpha(1f) - .setStartDelay(0) - .setInterpolator(mConfig.fastOutLinearInInterpolator) - .setDuration(mConfig.taskBarEnterAnimDuration) - .withLayer() - .start(); + if (mDismissButton.getVisibility() != View.VISIBLE) { + mDismissButton.setVisibility(View.VISIBLE); + mDismissButton.setAlpha(0f); + mDismissButton.animate() + .alpha(1f) + .setStartDelay(0) + .setInterpolator(mConfig.fastOutLinearInInterpolator) + .setDuration(mConfig.taskViewEnterFromAppDuration) + .withLayer() + .start(); + } } /** Mark this task view that the user does has not interacted with the stack after a certain time. */ @@ -259,6 +259,11 @@ public class TaskViewHeader extends FrameLayout { } } + /** Resets the state tracking that the user has not interacted with the stack after a certain time. */ + void resetNoUserInteractionState() { + mDismissButton.setVisibility(View.INVISIBLE); + } + @Override protected int[] onCreateDrawableState(int extraSpace) { @@ -268,13 +273,16 @@ public class TaskViewHeader extends FrameLayout { } /** Notifies the associated TaskView has been focused. */ - void onTaskViewFocusChanged(boolean focused) { + void onTaskViewFocusChanged(boolean focused, boolean animateFocusedState) { + // If we are not animating the visible state, just return + if (!animateFocusedState) return; + boolean isRunning = false; if (mFocusAnimator != null) { isRunning = mFocusAnimator.isRunning(); - mFocusAnimator.removeAllListeners(); - mFocusAnimator.cancel(); + Utilities.cancelAnimationWithoutCallbacks(mFocusAnimator); } + if (focused) { int secondaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); int[][] states = new int[][] { @@ -295,7 +303,7 @@ public class TaskViewHeader extends FrameLayout { int currentColor = mBackgroundColor; int lightPrimaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(), - lightPrimaryColor, currentColor); + currentColor, lightPrimaryColor); backgroundColor.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { @@ -349,11 +357,4 @@ public class TaskViewHeader extends FrameLayout { } } } - - public void setDimAlpha(int alpha) { - int color = Color.argb(alpha, 0, 0, 0); - mDimFilter.setColor(color); - mDimPaint.setColorFilter(mDimFilter); - setLayerType(LAYER_TYPE_HARDWARE, mDimPaint); - } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java index a946a84..117a7d3 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java @@ -33,37 +33,48 @@ import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.Task; -/** The task thumbnail view */ +/** + * The task thumbnail view. It implements an image view that allows for animating the dim and + * alpha of the thumbnail image. + */ public class TaskViewThumbnail extends View { - private final int mCornerRadius; - private final Matrix mScaleMatrix = new Matrix(); RecentsConfiguration mConfig; - // Task bar clipping - Rect mClipRect = new Rect(); + // Drawing + float mDimAlpha; + Matrix mScaleMatrix = new Matrix(); Paint mDrawPaint = new Paint(); + RectF mBitmapRect = new RectF(); + RectF mLayoutRect = new RectF(); + BitmapShader mBitmapShader; LightingColorFilter mLightingColorFilter = new LightingColorFilter(0xffffffff, 0); - private final RectF mBitmapRect = new RectF(); - private final RectF mLayoutRect = new RectF(); - private BitmapShader mBitmapShader; - private float mBitmapAlpha; - private float mDimAlpha; - private View mTaskBar; - private boolean mInvisible; - private ValueAnimator mAlphaAnimator; - private ValueAnimator.AnimatorUpdateListener mAlphaUpdateListener + + // Thumbnail alpha + float mThumbnailAlpha; + ValueAnimator mThumbnailAlphaAnimator; + ValueAnimator.AnimatorUpdateListener mThumbnailAlphaUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { - mBitmapAlpha = (float) animation.getAnimatedValue(); - updateFilter(); + mThumbnailAlpha = (float) animation.getAnimatedValue(); + updateThumbnailPaintFilter(); } }; + // Task bar clipping, the top of this thumbnail can be clipped against the opaque header + // bar that overlaps this thumbnail + View mTaskBar; + Rect mClipRect = new Rect(); + + // Visibility optimization, if the thumbnail height is less than the height of the header + // bar for the task view, then just mark this thumbnail view as invisible + boolean mInvisible; + public TaskViewThumbnail(Context context) { this(context, null); } @@ -79,53 +90,82 @@ public class TaskViewThumbnail extends View { public TaskViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mConfig = RecentsConfiguration.getInstance(); - mCornerRadius = mConfig.taskViewRoundedCornerRadiusPx; mDrawPaint.setColorFilter(mLightingColorFilter); mDrawPaint.setFilterBitmap(true); mDrawPaint.setAntiAlias(true); } @Override + protected void onFinishInflate() { + mThumbnailAlpha = mConfig.taskViewThumbnailAlpha; + updateThumbnailPaintFilter(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed) { + mLayoutRect.set(0, 0, getWidth(), getHeight()); + updateThumbnailScale(); + } + } + + @Override protected void onDraw(Canvas canvas) { if (mInvisible) { return; } - canvas.drawRoundRect(0, - 0, - getWidth(), - getHeight(), - mCornerRadius, - mCornerRadius, - mDrawPaint); + // Draw the thumbnail with the rounded corners + canvas.drawRoundRect(0, 0, getWidth(), getHeight(), + mConfig.taskViewRoundedCornerRadiusPx, + mConfig.taskViewRoundedCornerRadiusPx, mDrawPaint); } - @Override - protected void onFinishInflate() { - mBitmapAlpha = 0.9f; - updateFilter(); + /** Sets the thumbnail to a given bitmap. */ + void setThumbnail(Bitmap bm) { + if (bm != null) { + mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, + Shader.TileMode.CLAMP); + mDrawPaint.setShader(mBitmapShader); + mBitmapRect.set(0, 0, bm.getWidth(), bm.getHeight()); + updateThumbnailScale(); + } else { + mBitmapShader = null; + mDrawPaint.setShader(null); + } + updateThumbnailPaintFilter(); } - private void updateFilter() { + /** Updates the paint to draw the thumbnail. */ + void updateThumbnailPaintFilter() { if (mInvisible) { return; } - int mul = (int) ((1.0f - mDimAlpha) * mBitmapAlpha * 255); - int add = (int) ((1.0f - mDimAlpha) * (1 - mBitmapAlpha) * 255); + int mul = (int) ((1.0f - mDimAlpha) * mThumbnailAlpha * 255); + int add = (int) ((1.0f - mDimAlpha) * (1 - mThumbnailAlpha) * 255); if (mBitmapShader != null) { mLightingColorFilter.setColorMultiply(Color.argb(255, mul, mul, mul)); mLightingColorFilter.setColorAdd(Color.argb(0, add, add, add)); mDrawPaint.setColorFilter(mLightingColorFilter); mDrawPaint.setColor(0xffffffff); } else { - mDrawPaint.setColorFilter(null); int grey = mul + add; + mDrawPaint.setColorFilter(null); mDrawPaint.setColor(Color.argb(255, grey, grey, grey)); } invalidate(); } + /** Updates the thumbnail shader's scale transform. */ + void updateThumbnailScale() { + if (mBitmapShader != null) { + mScaleMatrix.setRectToRect(mBitmapRect, mLayoutRect, Matrix.ScaleToFit.FILL); + mBitmapShader.setLocalMatrix(mScaleMatrix); + } + } + /** Updates the clip rect based on the given task bar. */ - void enableTaskBarClip(View taskBar) { + void updateClipToTaskBar(View taskBar) { mTaskBar = taskBar; int top = (int) Math.max(0, taskBar.getTranslationY() + taskBar.getMeasuredHeight() - 1); @@ -133,75 +173,39 @@ public class TaskViewThumbnail extends View { setClipBounds(mClipRect); } - void updateVisibility(int clipBottom) { - boolean invisible = mTaskBar != null && getHeight() - clipBottom < mTaskBar.getHeight(); + /** Updates the visibility of the the thumbnail. */ + void updateThumbnailVisibility(int clipBottom) { + boolean invisible = mTaskBar != null && (getHeight() - clipBottom) <= mTaskBar.getHeight(); if (invisible != mInvisible) { mInvisible = invisible; if (!mInvisible) { - updateFilter(); + updateThumbnailPaintFilter(); } invalidate(); } } - /** Binds the thumbnail view to the screenshot. */ - boolean bindToScreenshot(Bitmap ss) { - setImageBitmap(ss); - return ss != null; - } - - /** Unbinds the thumbnail view from the screenshot. */ - void unbindFromScreenshot() { - setImageBitmap(null); + /** + * Sets the dim alpha, only used when we are not using hardware layers. + * (see RecentsConfiguration.useHardwareLayers) + */ + public void setDimAlpha(float dimAlpha) { + mDimAlpha = dimAlpha; + updateThumbnailPaintFilter(); } /** Binds the thumbnail view to the task */ void rebindToTask(Task t) { if (t.thumbnail != null) { - setImageBitmap(t.thumbnail); + setThumbnail(t.thumbnail); } else { - setImageBitmap(null); + setThumbnail(null); } } - public void setImageBitmap(Bitmap bm) { - if (bm != null) { - mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, - Shader.TileMode.CLAMP); - mDrawPaint.setShader(mBitmapShader); - mBitmapRect.set(0, 0, bm.getWidth(), bm.getHeight()); - updateBitmapScale(); - } else { - mBitmapShader = null; - mDrawPaint.setShader(null); - } - updateFilter(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (changed) { - mLayoutRect.set(0, 0, getWidth(), getHeight()); - updateBitmapScale(); - } - } - - private void updateBitmapScale() { - if (mBitmapShader != null) { - mScaleMatrix.setRectToRect(mBitmapRect, mLayoutRect, Matrix.ScaleToFit.FILL); - mBitmapShader.setLocalMatrix(mScaleMatrix); - } - } - - public void setDimAlpha(float dimAlpha) { - mDimAlpha = dimAlpha; - updateFilter(); - } - /** Unbinds the thumbnail view from the task */ void unbindFromTask() { - setImageBitmap(null); + setThumbnail(null); } /** Handles focus changes. */ @@ -217,54 +221,46 @@ public class TaskViewThumbnail extends View { } } - /** Prepares for the enter recents animation. */ + /** + * Prepares for the enter recents animation, this gets called before the the view + * is first visible and will be followed by a startEnterRecentsAnimation() call. + */ void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask) { if (isTaskViewLaunchTargetTask) { - mBitmapAlpha = 1f; + mThumbnailAlpha = 1f; } else { - mBitmapAlpha = mConfig.taskViewThumbnailAlpha; + mThumbnailAlpha = mConfig.taskViewThumbnailAlpha; } - updateFilter(); + updateThumbnailPaintFilter(); } - /** Animates this task thumbnail as it enters recents */ + /** Animates this task thumbnail as it enters Recents. */ void startEnterRecentsAnimation(int delay, Runnable postAnimRunnable) { startFadeAnimation(mConfig.taskViewThumbnailAlpha, delay, - mConfig.taskBarEnterAnimDuration, postAnimRunnable); + mConfig.taskViewEnterFromAppDuration, postAnimRunnable); } - /** Animates this task thumbnail as it exits recents */ + /** Animates this task thumbnail as it exits Recents. */ void startLaunchTaskAnimation(Runnable postAnimRunnable) { - startFadeAnimation(1f, 0, mConfig.taskBarExitAnimDuration, postAnimRunnable); + startFadeAnimation(1f, 0, mConfig.taskViewExitToAppDuration, postAnimRunnable); } - /** Animates the thumbnail alpha. */ + /** Starts a new thumbnail alpha animation. */ void startFadeAnimation(float finalAlpha, int delay, int duration, final Runnable postAnimRunnable) { - if (mAlphaAnimator != null) { - mAlphaAnimator.cancel(); - } - mAlphaAnimator = ValueAnimator.ofFloat(mBitmapAlpha, finalAlpha); - mAlphaAnimator.addUpdateListener(mAlphaUpdateListener); - mAlphaAnimator.setStartDelay(delay); - mAlphaAnimator.setInterpolator(mConfig.fastOutSlowInInterpolator); - mAlphaAnimator.setDuration(duration); - mAlphaAnimator.start(); + Utilities.cancelAnimationWithoutCallbacks(mThumbnailAlphaAnimator); + mThumbnailAlphaAnimator = ValueAnimator.ofFloat(mThumbnailAlpha, finalAlpha); + mThumbnailAlphaAnimator.setStartDelay(delay); + mThumbnailAlphaAnimator.setDuration(duration); + mThumbnailAlphaAnimator.setInterpolator(mConfig.fastOutSlowInInterpolator); + mThumbnailAlphaAnimator.addUpdateListener(mThumbnailAlphaUpdateListener); if (postAnimRunnable != null) { - mAlphaAnimator.addListener(new AnimatorListenerAdapter() { - public boolean mCancelled; - - @Override - public void onAnimationCancel(Animator animation) { - mCancelled = true; - } - + mThumbnailAlphaAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - if (!mCancelled) { - postAnimRunnable.run(); - } + postAnimRunnable.run(); } }); } + mThumbnailAlphaAnimator.start(); } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewAnimation.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewAnimation.java index 4586f12..e1d80fd 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/ViewAnimation.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/ViewAnimation.java @@ -27,7 +27,7 @@ public class ViewAnimation { public static class TaskViewEnterContext { // A trigger to run some logic when all the animations complete. This works around the fact // that it is difficult to coordinate ViewPropertyAnimators - ReferenceCountedTrigger postAnimationTrigger; + public ReferenceCountedTrigger postAnimationTrigger; // An update listener to notify as the enter animation progresses (used for the home transition) ValueAnimator.AnimatorUpdateListener updateListener; diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java index af0094e..12b91af 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java @@ -75,4 +75,12 @@ public class ViewPool<V, T> { mViewCreator.prepareViewToLeavePool(v, prepareData, isNewView); return v; } + + /** Returns an iterator to the list of the views in the pool. */ + Iterator<V> poolViewIterator() { + if (mPool != null) { + return mPool.iterator(); + } + return null; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java index e4faa6a..465a141 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java @@ -25,8 +25,6 @@ import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; @@ -53,6 +51,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private static final long DOUBLETAP_TIMEOUT_MS = 1200; private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220; private static final int ACTIVATE_ANIMATION_LENGTH = 220; + private static final int DARK_ANIMATION_LENGTH = 170; /** * The amount of width, which is kept in the end when performing a disappear animation (also @@ -84,6 +83,11 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView */ private static final float VERTICAL_ANIMATION_START = 1.0f; + /** + * Scale for the background to animate from when exiting dark mode. + */ + private static final float DARK_EXIT_SCALE_START = 0.93f; + private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR = new PathInterpolator(0.6f, 0, 0.5f, 1); private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR @@ -94,7 +98,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private boolean mDimmed; private boolean mDark; - private final Paint mDarkPaint = createDarkPaint(); private int mBgTint = 0; private final int mRoundedRectCornerRadius; @@ -332,40 +335,35 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView if (mDimmed != dimmed) { mDimmed = dimmed; if (fade) { - fadeBackground(); + fadeDimmedBackground(); } else { updateBackground(); } } } - public void setDark(boolean dark, boolean fade) { - // TODO implement fade - if (mDark != dark) { - mDark = dark; - if (mDark) { - setLayerType(View.LAYER_TYPE_HARDWARE, mDarkPaint); + public void setDark(boolean dark, boolean fade, long delay) { + super.setDark(dark, fade, delay); + if (mDark == dark) { + return; + } + mDark = dark; + if (!dark && fade) { + if (mActivated) { + mBackgroundDimmed.setVisibility(View.VISIBLE); + mBackgroundNormal.setVisibility(View.VISIBLE); + } else if (mDimmed) { + mBackgroundDimmed.setVisibility(View.VISIBLE); + mBackgroundNormal.setVisibility(View.INVISIBLE); } else { - setLayerType(View.LAYER_TYPE_NONE, null); + mBackgroundDimmed.setVisibility(View.INVISIBLE); + mBackgroundNormal.setVisibility(View.VISIBLE); } + fadeInFromDark(delay); + } else { + updateBackground(); } - } - - private static Paint createDarkPaint() { - final Paint p = new Paint(); - final float[] invert = { - -1f, 0f, 0f, 1f, 1f, - 0f, -1f, 0f, 1f, 1f, - 0f, 0f, -1f, 1f, 1f, - 0f, 0f, 0f, 1f, 0f - }; - final ColorMatrix m = new ColorMatrix(invert); - final ColorMatrix grayscale = new ColorMatrix(); - grayscale.setSaturation(0); - m.preConcat(grayscale); - p.setColorFilter(new ColorMatrixColorFilter(m)); - return p; - } + } public void setShowingLegacyBackground(boolean showing) { mShowingLegacyBackground = showing; @@ -402,7 +400,40 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mBackgroundNormal.setRippleColor(rippleColor); } - private void fadeBackground() { + /** + * Fades in the background when exiting dark mode. + */ + private void fadeInFromDark(long delay) { + final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal; + background.setAlpha(0f); + background.setPivotX(mBackgroundDimmed.getWidth() / 2f); + background.setPivotY(getActualHeight() / 2f); + background.setScaleX(DARK_EXIT_SCALE_START); + background.setScaleY(DARK_EXIT_SCALE_START); + background.animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .setDuration(DARK_ANIMATION_LENGTH) + .setStartDelay(delay) + .setInterpolator(mLinearOutSlowInInterpolator) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + // Jump state if we are cancelled + background.setScaleX(1f); + background.setScaleY(1f); + background.setAlpha(1f); + } + }) + .start(); + } + + /** + * Fades the background when the dimmed state changes. + */ + private void fadeDimmedBackground() { + mBackgroundDimmed.animate().cancel(); mBackgroundNormal.animate().cancel(); if (mDimmed) { mBackgroundDimmed.setVisibility(View.VISIBLE); @@ -443,11 +474,14 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private void updateBackground() { - if (mDimmed) { + cancelFadeAnimations(); + if (mDark) { + mBackgroundDimmed.setVisibility(View.INVISIBLE); + mBackgroundNormal.setVisibility(View.INVISIBLE); + } else if (mDimmed) { mBackgroundDimmed.setVisibility(View.VISIBLE); mBackgroundNormal.setVisibility(View.INVISIBLE); } else { - cancelFadeAnimations(); mBackgroundDimmed.setVisibility(View.INVISIBLE); mBackgroundNormal.setVisibility(View.VISIBLE); mBackgroundNormal.setAlpha(1f); @@ -459,6 +493,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView if (mBackgroundAnimator != null) { mBackgroundAnimator.cancel(); } + mBackgroundDimmed.animate().cancel(); mBackgroundNormal.animate().cancel(); } @@ -508,7 +543,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView if (mAppearAnimator != null) { mAppearAnimator.cancel(); } - mAnimationTranslationY = translationDirection * mActualHeight; + mAnimationTranslationY = translationDirection * getActualHeight(); if (mAppearAnimationFraction == -1.0f) { // not initialized yet, we start anew if (isAppearing) { @@ -601,14 +636,15 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView float top; float bottom; + final int actualHeight = getActualHeight(); if (mAnimationTranslationY > 0.0f) { - bottom = mActualHeight - heightFraction * mAnimationTranslationY * 0.1f + bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f - translateYTotalAmount; top = bottom * heightFraction; } else { - top = heightFraction * (mActualHeight + mAnimationTranslationY) * 0.1f - + top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f - translateYTotalAmount; - bottom = mActualHeight * (1 - heightFraction) + top * heightFraction; + bottom = actualHeight * (1 - heightFraction) + top * heightFraction; } mAppearAnimationRect.set(left, top, right, bottom); setOutlineRect(left, top + mAppearAnimationTranslation, right, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedImageView.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedImageView.java new file mode 100644 index 0000000..094161d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedImageView.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014 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; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; + +/** + * An ImageView which does not have overlapping rendering commands and therefore does not need a + * layer when alpha is changed. + */ +public class AlphaOptimizedImageView extends ImageView +{ + public AlphaOptimizedImageView(Context context) { + super(context); + } + + public AlphaOptimizedImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AlphaOptimizedImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public AlphaOptimizedImageView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index f5e5517..8a03a2b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -59,6 +59,7 @@ import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.Log; +import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.Display; @@ -69,6 +70,7 @@ import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.ViewParent; import android.view.ViewStub; import android.view.WindowManager; import android.view.WindowManagerGlobal; @@ -91,7 +93,6 @@ import com.android.systemui.SearchPanelView; import com.android.systemui.SwipeHelper; import com.android.systemui.SystemUI; import com.android.systemui.statusbar.NotificationData.Entry; -import com.android.systemui.statusbar.phone.KeyguardTouchDelegate; import com.android.systemui.statusbar.phone.NavigationBarView; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.HeadsUpNotificationView; @@ -112,6 +113,9 @@ public abstract class BaseStatusBar extends SystemUI implements public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); public static final boolean MULTIUSER_DEBUG = false; + // STOPSHIP disable once we resolve b/18102199 + private static final boolean NOTIFICATION_CLICK_DEBUG = true; + protected static final int MSG_SHOW_RECENT_APPS = 1019; protected static final int MSG_HIDE_RECENT_APPS = 1020; protected static final int MSG_TOGGLE_RECENTS_APPS = 1021; @@ -154,9 +158,6 @@ public abstract class BaseStatusBar extends SystemUI implements protected HeadsUpNotificationView mHeadsUpNotificationView; protected int mHeadsUpNotificationDecay; - // used to notify status bar for suppressing notification LED - protected boolean mPanelSlightlyVisible; - // Search panel protected SearchPanelView mSearchPanelView; @@ -168,6 +169,20 @@ public abstract class BaseStatusBar extends SystemUI implements // on-screen navigation buttons protected NavigationBarView mNavigationBarView = null; + + protected Boolean mScreenOn; + + // The second field is a bit different from the first one because it only listens to screen on/ + // screen of events from Keyguard. We need this so we don't have a race condition with the + // broadcast. In the future, we should remove the first field altogether and rename the second + // field. + protected boolean mScreenOnFromKeyguard; + + protected boolean mVisible; + + // mScreenOnFromKeyguard && mVisible. + private boolean mVisibleToUser; + private Locale mLocale; private float mFontScale; @@ -266,6 +281,7 @@ public abstract class BaseStatusBar extends SystemUI implements if (DEBUG) { Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); } + logActionClick(view); // The intent we are sending is for the application, which // won't have permission to immediately start an activity after // the user switches to home. We know it is safe to do at this @@ -295,7 +311,8 @@ public abstract class BaseStatusBar extends SystemUI implements // close the shade if it was open if (handled) { - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */); + animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, + true /* force */); visibilityChanged(false); } // Wait for activity start. @@ -308,6 +325,40 @@ public abstract class BaseStatusBar extends SystemUI implements } } + private void logActionClick(View view) { + ViewParent parent = view.getParent(); + String key = getNotificationKeyForParent(parent); + if (key == null) { + Log.w(TAG, "Couldn't determine notification for click."); + return; + } + int index = -1; + // If this is a default template, determine the index of the button. + if (view.getId() == com.android.internal.R.id.action0 && + parent != null && parent instanceof ViewGroup) { + ViewGroup actionGroup = (ViewGroup) parent; + index = actionGroup.indexOfChild(view); + } + if (NOTIFICATION_CLICK_DEBUG) { + Log.d(TAG, "Clicked on button " + index + " for " + key); + } + try { + mBarService.onNotificationActionClick(key, index); + } catch (RemoteException e) { + // Ignore + } + } + + private String getNotificationKeyForParent(ViewParent parent) { + while (parent != null) { + if (parent instanceof ExpandableNotificationRow) { + return ((ExpandableNotificationRow) parent).getStatusBarNotification().getKey(); + } + parent = parent.getParent(); + } + return null; + } + private boolean superOnClickHandler(View view, PendingIntent pendingIntent, Intent fillInIntent) { return super.onClickHandler(view, pendingIntent, fillInIntent); @@ -341,7 +392,8 @@ public abstract class BaseStatusBar extends SystemUI implements Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0); if (BANNER_ACTION_SETUP.equals(action)) { - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */); + animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, + true /* force */); mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_REDACTION) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -548,6 +600,7 @@ public abstract class BaseStatusBar extends SystemUI implements } mCurrentUserId = ActivityManager.getCurrentUser(); + setHeadsUpUser(mCurrentUserId); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); @@ -610,7 +663,13 @@ public abstract class BaseStatusBar extends SystemUI implements } public void userSwitched(int newUserId) { - // should be overridden + setHeadsUpUser(newUserId); + } + + private void setHeadsUpUser(int newUserId) { + if (mHeadsUpNotificationView != null) { + mHeadsUpNotificationView.setUser(newUserId); + } } public boolean isHeadsUp(String key) { @@ -625,9 +684,12 @@ public abstract class BaseStatusBar extends SystemUI implements Log.v(TAG, String.format("%s: current userid: %d, notification userid: %d", n, thisUserId, notificationUserId)); } + return isCurrentProfile(notificationUserId); + } + + protected boolean isCurrentProfile(int userId) { synchronized (mCurrentProfiles) { - return notificationUserId == UserHandle.USER_ALL - || mCurrentProfiles.get(notificationUserId) != null; + return userId == UserHandle.USER_ALL || mCurrentProfiles.get(userId) != null; } } @@ -766,7 +828,7 @@ public abstract class BaseStatusBar extends SystemUI implements } } }); - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */); + animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */); return true; } }, false /* afterKeyguardGone */); @@ -1434,8 +1496,6 @@ public abstract class BaseStatusBar extends SystemUI implements entry.autoRedacted = true; } - row.setClearable(sbn.isClearable()); - if (MULTIUSER_DEBUG) { TextView debug = (TextView) row.findViewById(R.id.debug_info); if (debug != null) { @@ -1480,6 +1540,9 @@ public abstract class BaseStatusBar extends SystemUI implements } public void onClick(final View v) { + if (NOTIFICATION_CLICK_DEBUG) { + Log.d(TAG, "Clicked on content of " + mNotificationKey); + } final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing(); final boolean afterKeyguardGone = mIntent.isActivity() && PreviewInflater.wouldLaunchResolverActivity(mContext, mIntent.getIntent(), @@ -1487,7 +1550,11 @@ public abstract class BaseStatusBar extends SystemUI implements dismissKeyguardThenExecute(new OnDismissAction() { public boolean onDismiss() { if (mIsHeadsUp) { - mHeadsUpNotificationView.clear(); + // Release the HUN notification to the shade. + // + // In most cases, when FLAG_AUTO_CANCEL is set, the notification will + // become canceled shortly by NoMan, but we can't assume that. + mHeadsUpNotificationView.releaseAndClose(); } new Thread() { @Override @@ -1531,7 +1598,8 @@ public abstract class BaseStatusBar extends SystemUI implements }.start(); // close the shade if it was open - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */); + animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, + true /* force */); visibilityChanged(false); return mIntent != null && mIntent.isActivity(); @@ -1553,28 +1621,45 @@ public abstract class BaseStatusBar extends SystemUI implements } } + protected void visibilityChanged(boolean visible) { + if (mVisible != visible) { + mVisible = visible; + if (!visible) { + dismissPopups(); + } + } + updateVisibleToUser(); + } + + protected void updateVisibleToUser() { + boolean oldVisibleToUser = mVisibleToUser; + mVisibleToUser = mVisible && mScreenOnFromKeyguard; + + if (oldVisibleToUser != mVisibleToUser) { + handleVisibleToUserChanged(mVisibleToUser); + } + } + /** - * The LEDs are turned o)ff when the notification panel is shown, even just a little bit. + * The LEDs are turned off when the notification panel is shown, even just a little bit. * This was added last-minute and is inconsistent with the way the rest of the notifications * are handled, because the notification isn't really cancelled. The lights are just * turned off. If any other notifications happen, the lights will turn back on. Steve says * this is what he wants. (see bug 1131461) */ - protected void visibilityChanged(boolean visible) { - if (mPanelSlightlyVisible != visible) { - mPanelSlightlyVisible = visible; - if (!visible) { - dismissPopups(); - } - try { - if (visible) { - mBarService.onPanelRevealed(); - } else { - mBarService.onPanelHidden(); - } - } catch (RemoteException ex) { - // Won't fail unless the world has ended. + protected void handleVisibleToUserChanged(boolean visibleToUser) { + try { + if (visibleToUser) { + // Only stop blinking, vibrating, ringing when the user went into the shade + // manually (SHADE or SHADE_LOCKED). + boolean clearNotificationEffects = + (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED); + mBarService.onPanelRevealed(clearNotificationEffects); + } else { + mBarService.onPanelHidden(); } + } catch (RemoteException ex) { + // Won't fail unless the world has ended. } } @@ -1998,6 +2083,10 @@ public abstract class BaseStatusBar extends SystemUI implements return false; } + if (mHeadsUpNotificationView.isSnoozed(sbn.getPackageName())) { + return false; + } + Notification notification = sbn.getNotification(); // some predicates to make the boolean logic legible boolean isNoisy = (notification.defaults & Notification.DEFAULT_SOUND) != 0 @@ -2012,13 +2101,13 @@ public abstract class BaseStatusBar extends SystemUI implements boolean accessibilityForcesLaunch = isFullscreen && mAccessibilityManager.isTouchExplorationEnabled(); - final KeyguardTouchDelegate keyguard = KeyguardTouchDelegate.getInstance(mContext); boolean interrupt = (isFullscreen || (isHighPriority && (isNoisy || hasTicker))) && isAllowed && !accessibilityForcesLaunch && mPowerManager.isScreenOn() - && !keyguard.isShowingAndNotOccluded() - && !keyguard.isInputRestricted(); + && (!mStatusBarKeyguardViewManager.isShowing() + || mStatusBarKeyguardViewManager.isOccluded()) + && !mStatusBarKeyguardViewManager.isInputRestricted(); try { interrupt = interrupt && !mDreamManager.isDreaming(); } catch (RemoteException e) { @@ -2028,10 +2117,6 @@ public abstract class BaseStatusBar extends SystemUI implements return interrupt; } - public boolean inKeyguardRestrictedInputMode() { - return KeyguardTouchDelegate.getInstance(mContext).isInputRestricted(); - } - public void setInteracting(int barWindow, boolean interacting) { // hook for subclasses } @@ -2089,4 +2174,16 @@ public abstract class BaseStatusBar extends SystemUI implements // Ignore. } } + + public boolean isKeyguardSecure() { + if (mStatusBarKeyguardViewManager == null) { + // startKeyguard() hasn't been called yet, so we don't know. + // Make sure anything that needs to know isKeyguardSecure() checks and re-checks this + // value onVisibilityChanged(). + Slog.w(TAG, "isKeyguardSecure() called before startKeyguard(), returning false", + new Throwable()); + return false; + } + return mStatusBarKeyguardViewManager.isSecure(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 9db875f..0b1b883 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -56,6 +56,7 @@ public class CommandQueue extends IStatusBar.Stub { private static final int MSG_BUZZ_BEEP_BLINKED = 15 << MSG_SHIFT; private static final int MSG_NOTIFICATION_LIGHT_OFF = 16 << MSG_SHIFT; private static final int MSG_NOTIFICATION_LIGHT_PULSE = 17 << MSG_SHIFT; + private static final int MSG_SHOW_SCREEN_PIN_REQUEST = 18 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -97,6 +98,7 @@ public class CommandQueue extends IStatusBar.Stub { public void buzzBeepBlinked(); public void notificationLightOff(); public void notificationLightPulse(int argb, int onMillis, int offMillis); + public void showScreenPinningRequest(); } public CommandQueue(Callbacks callbacks, StatusBarIconList list) { @@ -238,6 +240,12 @@ public class CommandQueue extends IStatusBar.Stub { } } + public void showScreenPinningRequest() { + synchronized (mList) { + mHandler.sendEmptyMessage(MSG_SHOW_SCREEN_PIN_REQUEST); + } + } + private final class H extends Handler { public void handleMessage(Message msg) { final int what = msg.what & MSG_MASK; @@ -317,6 +325,9 @@ public class CommandQueue extends IStatusBar.Stub { case MSG_NOTIFICATION_LIGHT_PULSE: mCallbacks.notificationLightPulse((Integer) msg.obj, msg.arg1, msg.arg2); break; + case MSG_SHOW_SCREEN_PIN_REQUEST: + mCallbacks.showScreenPinningRequest(); + break; } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DismissView.java b/packages/SystemUI/src/com/android/systemui/statusbar/DismissView.java index 897dbf2..d9276bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/DismissView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/DismissView.java @@ -17,12 +17,15 @@ package com.android.systemui.statusbar; import android.content.Context; +import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import com.android.systemui.R; public class DismissView extends StackScrollerDecorView { + private boolean mDismissAllInProgress; + private DismissViewButton mDismissButton; public DismissView(Context context, AttributeSet attrs) { super(context, attrs); @@ -33,7 +36,44 @@ public class DismissView extends StackScrollerDecorView { return findViewById(R.id.dismiss_text); } + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mDismissButton = (DismissViewButton) findContentView(); + } + public void setOnButtonClickListener(OnClickListener listener) { mContent.setOnClickListener(listener); } + + public boolean isOnEmptySpace(float touchX, float touchY) { + return touchX < mContent.getX() + || touchX > mContent.getX() + mContent.getWidth() + || touchY < mContent.getY() + || touchY > mContent.getY() + mContent.getHeight(); + } + + public void showClearButton() { + mDismissButton.showButton(); + } + + public void setDismissAllInProgress(boolean dismissAllInProgress) { + if (dismissAllInProgress) { + setClipBounds(null); + } + mDismissAllInProgress = dismissAllInProgress; + } + + @Override + public void setClipBounds(Rect clipBounds) { + if (mDismissAllInProgress) { + // we don't want any clipping to happen! + return; + } + super.setClipBounds(clipBounds); + } + + public boolean isButtonVisible() { + return mDismissButton.isButtonStatic(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java new file mode 100644 index 0000000..f2a5673 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2014 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; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.Choreographer; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewRootImpl; +import android.widget.Button; +import com.android.systemui.R; +import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; + +public class DismissViewButton extends Button { + private AnimatedVectorDrawable mAnimatedDismissDrawable; + private final Drawable mStaticDismissDrawable; + private Drawable mActiveDrawable; + + public DismissViewButton(Context context) { + this(context, null); + } + + public DismissViewButton(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DismissViewButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public DismissViewButton(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mAnimatedDismissDrawable = (AnimatedVectorDrawable) getContext().getResources().getDrawable( + R.drawable.dismiss_all_shape_animation).mutate(); + mAnimatedDismissDrawable.setCallback(this); + mAnimatedDismissDrawable.setBounds(0, + 0, + mAnimatedDismissDrawable.getIntrinsicWidth(), + mAnimatedDismissDrawable.getIntrinsicHeight()); + mStaticDismissDrawable = getContext().getResources().getDrawable( + R.drawable.dismiss_all_shape); + mStaticDismissDrawable.setBounds(0, + 0, + mStaticDismissDrawable.getIntrinsicWidth(), + mStaticDismissDrawable.getIntrinsicHeight()); + mStaticDismissDrawable.setCallback(this); + mActiveDrawable = mStaticDismissDrawable; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.save(); + int drawableHeight = mActiveDrawable.getBounds().height(); + boolean isRtl = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + int dx = isRtl ? getWidth() / 2 + drawableHeight / 2 : getWidth() / 2 - drawableHeight / 2; + canvas.translate(dx, getHeight() / 2.0f + drawableHeight / + 2.0f); + canvas.scale(isRtl ? -1.0f : 1.0f, -1.0f); + mActiveDrawable.draw(canvas); + canvas.restore(); + } + + @Override + public boolean performClick() { + if (!mAnimatedDismissDrawable.isRunning()) { + mActiveDrawable = mAnimatedDismissDrawable; + mAnimatedDismissDrawable.start(); + } + return super.performClick(); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) + || who == mAnimatedDismissDrawable + || who == mStaticDismissDrawable; + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + /** + * This method returns the drawing rect for the view which is different from the regular + * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at + * position 0 and usually the translation is neglected. The standard implementation doesn't + * account for translation. + * + * @param outRect The (scrolled) drawing bounds of the view. + */ + @Override + public void getDrawingRect(Rect outRect) { + super.getDrawingRect(outRect); + float translationX = ((ViewGroup) mParent).getTranslationX(); + float translationY = ((ViewGroup) mParent).getTranslationY(); + outRect.left += translationX; + outRect.right += translationX; + outRect.top += translationY; + outRect.bottom += translationY; + } + + public void showButton() { + mActiveDrawable = mStaticDismissDrawable; + invalidate(); + } + + /** + * @return Whether the button is currently static and not being animated. + */ + public boolean isButtonStatic() { + return mActiveDrawable == mStaticDismissDrawable; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewImageButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewImageButton.java deleted file mode 100644 index d55b0b3..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewImageButton.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2014 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; - -import android.content.Context; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import com.android.systemui.R; -import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; - -public class DismissViewImageButton extends ImageButton { - public DismissViewImageButton(Context context) { - super(context); - } - - public DismissViewImageButton(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public DismissViewImageButton(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public DismissViewImageButton(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - /** - * This method returns the drawing rect for the view which is different from the regular - * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at - * position 0 and usually the translation is neglected. The standard implementation doesn't - * account for translation. - * - * @param outRect The (scrolled) drawing bounds of the view. - */ - @Override - public void getDrawingRect(Rect outRect) { - super.getDrawingRect(outRect); - float translationX = ((ViewGroup) mParent).getTranslationX(); - float translationY = ((ViewGroup) mParent).getTranslationY(); - outRect.left += translationX; - outRect.right += translationX; - outRect.top += translationY; - outRect.bottom += translationY; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java index df475d5..c9f0260 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java @@ -126,7 +126,8 @@ public class DragDownHelper implements Gefingerpoken { } return true; case MotionEvent.ACTION_UP: - if (mDraggedFarEnough && mDragDownCallback.onDraggedDown(mStartingChild)) { + if (mDraggedFarEnough && mDragDownCallback.onDraggedDown(mStartingChild, + (int) (y - mInitialTouchY))) { if (mStartingChild == null) { mDragDownCallback.setEmptyDragAmount(0f); } @@ -221,7 +222,7 @@ public class DragDownHelper implements Gefingerpoken { /** * @return true if the interaction is accepted, false if it should be cancelled */ - boolean onDraggedDown(View startingChild); + boolean onDraggedDown(View startingChild, int dragLengthY); void onDragDownReset(); void onThresholdReached(); void onTouchSlopExceeded(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index 9196dc8..8ad8406 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -22,11 +22,10 @@ import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; import android.service.notification.StatusBarNotification; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.view.ViewStub; import android.view.accessibility.AccessibilityEvent; - import android.widget.ImageView; import com.android.systemui.R; @@ -70,6 +69,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { private NotificationGuts mGuts; private StatusBarNotification mStatusBarNotification; + private boolean mIsHeadsUp; public void setIconAnimationRunning(boolean running) { setIconAnimationRunning(running, mPublicLayout); @@ -118,12 +118,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { public void setStatusBarNotification(StatusBarNotification statusBarNotification) { mStatusBarNotification = statusBarNotification; + updateVetoButton(); } public StatusBarNotification getStatusBarNotification() { return mStatusBarNotification; } + public void setHeadsUp(boolean isHeadsUp) { + mIsHeadsUp = isHeadsUp; + } + public interface ExpansionLogger { public void logNotificationExpansion(String key, boolean userAction, boolean expanded); } @@ -149,13 +154,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { mShowingPublicInitialized = false; mIsSystemExpanded = false; mExpansionDisabled = false; - mPublicLayout.reset(); - mPrivateLayout.reset(); + mPublicLayout.reset(mIsHeadsUp); + mPrivateLayout.reset(mIsHeadsUp); resetHeight(); logExpansionEvent(false, wasExpanded); } public void resetHeight() { + if (mIsHeadsUp) { + resetActualHeight(); + } mMaxExpandHeight = 0; mWasReset = true; onHeightReset(); @@ -163,6 +171,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { } @Override + protected boolean filterMotionEvent(MotionEvent event) { + return mIsHeadsUp || super.filterMotionEvent(event); + } + + @Override protected void onFinishInflate() { super.onFinishInflate(); mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic); @@ -194,11 +207,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { } @Override - public void setDark(boolean dark, boolean fade) { - super.setDark(dark, fade); + public void setDark(boolean dark, boolean fade, long delay) { + super.setDark(dark, fade, delay); final NotificationContentView showing = getShowingLayout(); if (showing != null) { - showing.setDark(dark, fade); + showing.setDark(dark, fade, delay); } } @@ -291,17 +304,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { * @return Can the underlying notification be cleared? */ public boolean isClearable() { - return mClearable; - } - - /** - * Set whether the notification can be cleared. - * - * @param clearable - */ - public void setClearable(boolean clearable) { - mClearable = clearable; - updateVetoButton(); + return mStatusBarNotification != null && mStatusBarNotification.isClearable(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java index f85d32b..a18fff2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java @@ -41,7 +41,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { outline.setRect(0, mClipTopAmount, getWidth(), - Math.max(mActualHeight, mClipTopAmount)); + Math.max(getActualHeight(), mClipTopAmount)); } else { outline.setRect(mOutlineRect); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java index c8f756e..ebc663c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java @@ -36,9 +36,10 @@ public abstract class ExpandableView extends FrameLayout { private final int mMaxNotificationHeight; private OnHeightChangedListener mOnHeightChangedListener; - protected int mActualHeight; + private int mActualHeight; protected int mClipTopAmount; private boolean mActualHeightInitialized; + private boolean mDark; private ArrayList<View> mMatchParentViews = new ArrayList<View>(); public ExpandableView(Context context, AttributeSet attrs) { @@ -103,6 +104,15 @@ public abstract class ExpandableView extends FrameLayout { } } + /** + * Resets the height of the view on the next layout pass + */ + protected void resetActualHeight() { + mActualHeight = 0; + mActualHeightInitialized = false; + requestLayout(); + } + protected int getInitialHeight() { return getHeight(); } @@ -115,7 +125,7 @@ public abstract class ExpandableView extends FrameLayout { return false; } - private boolean filterMotionEvent(MotionEvent event) { + protected boolean filterMotionEvent(MotionEvent event) { return event.getActionMasked() != MotionEvent.ACTION_DOWN || event.getY() > mClipTopAmount && event.getY() < mActualHeight; } @@ -176,8 +186,14 @@ public abstract class ExpandableView extends FrameLayout { * * @param dark Whether the notification should be dark. * @param fade Whether an animation should be played to change the state. + * @param delay If fading, the delay of the animation. */ - public void setDark(boolean dark, boolean fade) { + public void setDark(boolean dark, boolean fade, long delay) { + mDark = dark; + } + + public boolean isDark() { + return mDark; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index ce35e4b..58067c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -146,8 +146,8 @@ public class KeyguardIndicationController { try { long chargingTimeRemaining = mBatteryInfo.computeChargeTimeRemaining(); if (chargingTimeRemaining > 0) { - String chargingTimeFormatted = - Formatter.formatShortElapsedTime(mContext, chargingTimeRemaining); + String chargingTimeFormatted = Formatter.formatShortElapsedTimeRoundingUpToMinutes( + mContext, chargingTimeRemaining); return mContext.getResources().getString( R.string.keyguard_indication_charging_time, chargingTimeFormatted); } @@ -162,8 +162,9 @@ public class KeyguardIndicationController { KeyguardUpdateMonitorCallback mUpdateMonitor = new KeyguardUpdateMonitorCallback() { @Override public void onRefreshBatteryInfo(KeyguardUpdateMonitor.BatteryStatus status) { - mPowerPluggedIn = status.status == BatteryManager.BATTERY_STATUS_CHARGING + boolean isChargingOrFull = status.status == BatteryManager.BATTERY_STATUS_CHARGING || status.status == BatteryManager.BATTERY_STATUS_FULL; + mPowerPluggedIn = status.isPluggedIn() && isChargingOrFull; mPowerCharged = status.isCharged(); updateIndication(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java index 5db680a..0fc46e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java @@ -23,9 +23,7 @@ import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; -import android.view.MotionEvent; import android.view.View; -import com.android.systemui.R; /** * A view that can be used for both the dimmed and normal background of an notification. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java index 502490f..914b3d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java @@ -27,11 +27,11 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; +import android.view.ViewTreeObserver; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; - import com.android.systemui.R; /** @@ -42,14 +42,14 @@ import com.android.systemui.R; public class NotificationContentView extends FrameLayout { private static final long ANIMATION_DURATION_LENGTH = 170; - private static final Paint INVERT_PAINT = createInvertPaint(); - private static final ColorFilter NO_COLOR_FILTER = new ColorFilter(); private final Rect mClipBounds = new Rect(); private View mContractedChild; private View mExpandedChild; + private NotificationViewWrapper mContractedWrapper; + private int mSmallHeight; private int mClipTopAmount; private int mActualHeight; @@ -60,11 +60,21 @@ public class NotificationContentView extends FrameLayout { private boolean mDark; private final Paint mFadePaint = new Paint(); + private boolean mAnimate; + private ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener + = new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + mAnimate = true; + getViewTreeObserver().removeOnPreDrawListener(this); + return true; + } + }; public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mFadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD)); - reset(); + reset(true); } @Override @@ -73,7 +83,13 @@ public class NotificationContentView extends FrameLayout { updateClipping(); } - public void reset() { + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + updateVisibility(); + } + + public void reset(boolean resetActualHeight) { if (mContractedChild != null) { mContractedChild.animate().cancel(); } @@ -84,8 +100,10 @@ public class NotificationContentView extends FrameLayout { mContractedChild = null; mExpandedChild = null; mSmallHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); - mActualHeight = mSmallHeight; mContractedVisible = true; + if (resetActualHeight) { + mActualHeight = mSmallHeight; + } } public View getContractedChild() { @@ -104,7 +122,9 @@ public class NotificationContentView extends FrameLayout { sanitizeContractedLayoutParams(child); addView(child); mContractedChild = child; + mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child); selectLayout(false /* animate */, true /* force */); + mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */); } public void setExpandedChild(View child) { @@ -117,9 +137,31 @@ public class NotificationContentView extends FrameLayout { selectLayout(false /* animate */, true /* force */); } + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + updateVisibility(); + } + + private void updateVisibility() { + setVisible(isShown()); + } + + private void setVisible(final boolean isVisible) { + if (isVisible) { + + // We only animate if we are drawn at least once, otherwise the view might animate when + // it's shown the first time + getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener); + } else { + getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); + mAnimate = false; + } + } + public void setActualHeight(int actualHeight) { mActualHeight = actualHeight; - selectLayout(true /* animate */, false /* force */); + selectLayout(mAnimate /* animate */, false /* force */); updateClipping(); } @@ -203,44 +245,20 @@ public class NotificationContentView extends FrameLayout { public void notifyContentUpdated() { selectLayout(false /* animate */, true /* force */); + if (mContractedChild != null) { + mContractedWrapper.notifyContentUpdated(); + mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */); + } } public boolean isContentExpandable() { return mExpandedChild != null; } - public void setDark(boolean dark, boolean fade) { + public void setDark(boolean dark, boolean fade, long delay) { if (mDark == dark || mContractedChild == null) return; mDark = dark; - setImageViewDark(dark, fade, com.android.internal.R.id.right_icon); - setImageViewDark(dark, fade, com.android.internal.R.id.icon); - } - - private void setImageViewDark(boolean dark, boolean fade, int imageViewId) { - // TODO: implement fade - final ImageView v = (ImageView) mContractedChild.findViewById(imageViewId); - if (v == null) return; - final Drawable d = v.getBackground(); - if (dark) { - v.setLayerType(LAYER_TYPE_HARDWARE, INVERT_PAINT); - if (d != null) { - v.setTag(R.id.doze_saved_filter_tag, d.getColorFilter() != null ? d.getColorFilter() - : NO_COLOR_FILTER); - d.setColorFilter(getResources().getColor(R.color.doze_small_icon_background_color), - PorterDuff.Mode.SRC_ATOP); - v.setImageAlpha(getResources().getInteger(R.integer.doze_small_icon_alpha)); - } - } else { - v.setLayerType(LAYER_TYPE_NONE, null); - if (d != null) { - final ColorFilter filter = (ColorFilter) v.getTag(R.id.doze_saved_filter_tag); - if (filter != null) { - d.setColorFilter(filter == NO_COLOR_FILTER ? null : filter); - v.setTag(R.id.doze_saved_filter_tag, null); - } - v.setImageAlpha(0xff); - } - } + mContractedWrapper.setDark(dark, fade, delay); } @Override @@ -250,16 +268,4 @@ public class NotificationContentView extends FrameLayout { // layout, and saves us some layers. return false; } - - private static Paint createInvertPaint() { - final Paint p = new Paint(); - final float[] invert = { - -1f, 0f, 0f, 1f, 1f, - 0f, -1f, 0f, 1f, 1f, - 0f, 0f, -1f, 1f, 1f, - 0f, 0f, 0f, 1f, 0f - }; - p.setColorFilter(new ColorMatrixColorFilter(new ColorMatrix(invert))); - return p; - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java new file mode 100644 index 0000000..0702d7e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 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; + +import android.view.View; + +import com.android.systemui.ViewInvertHelper; +import com.android.systemui.statusbar.phone.NotificationPanelView; + +/** + * Wraps a notification containing a custom view. + */ +public class NotificationCustomViewWrapper extends NotificationViewWrapper { + + private final ViewInvertHelper mInvertHelper; + + protected NotificationCustomViewWrapper(View view) { + super(view); + mInvertHelper = new ViewInvertHelper(view, NotificationPanelView.DOZE_ANIMATION_DURATION); + } + + @Override + public void setDark(boolean dark, boolean fade, long delay) { + if (fade) { + mInvertHelper.fade(dark, delay); + } else { + mInvertHelper.update(dark); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Prefs.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaViewWrapper.java index f339401..953c373 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaViewWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2014 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. @@ -11,22 +11,27 @@ * 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. + * limitations under the License */ -package com.android.systemui.statusbar.policy; +package com.android.systemui.statusbar; import android.content.Context; -import android.content.SharedPreferences; +import android.view.View; -public class Prefs { - private static final String SHARED_PREFS_NAME = "status_bar"; +/** + * Wraps a media notification. + */ +public class NotificationMediaViewWrapper extends NotificationTemplateViewWrapper { - public static SharedPreferences read(Context context) { - return context.getSharedPreferences(Prefs.SHARED_PREFS_NAME, Context.MODE_PRIVATE); + protected NotificationMediaViewWrapper(Context ctx, View view) { + super(ctx, view); } - public static SharedPreferences.Editor edit(Context context) { - return context.getSharedPreferences(Prefs.SHARED_PREFS_NAME, Context.MODE_PRIVATE).edit(); + @Override + public void setDark(boolean dark, boolean fade, long delay) { + + // Only update the large icon, because the rest is already inverted. + setPictureGrayscale(dark, fade, delay); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java index edfd205..bfa3aa5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java @@ -21,6 +21,8 @@ import android.util.AttributeSet; import android.widget.TextView; import com.android.systemui.R; +import com.android.systemui.ViewInvertHelper; +import com.android.systemui.statusbar.phone.NotificationPanelView; /** * Container view for overflowing notification icons on Keyguard. @@ -28,6 +30,8 @@ import com.android.systemui.R; public class NotificationOverflowContainer extends ActivatableNotificationView { private NotificationOverflowIconsView mIconsView; + private ViewInvertHelper mViewInvertHelper; + private boolean mDark; public NotificationOverflowContainer(Context context, AttributeSet attrs) { super(context, attrs); @@ -39,6 +43,20 @@ public class NotificationOverflowContainer extends ActivatableNotificationView { mIconsView = (NotificationOverflowIconsView) findViewById(R.id.overflow_icons_view); mIconsView.setMoreText((TextView) findViewById(R.id.more_text)); mIconsView.setOverflowIndicator(findViewById(R.id.more_icon_overflow)); + mViewInvertHelper = new ViewInvertHelper(findViewById(R.id.content), + NotificationPanelView.DOZE_ANIMATION_DURATION); + } + + @Override + public void setDark(boolean dark, boolean fade, long delay) { + super.setDark(dark, fade, delay); + if (mDark == dark) return; + mDark = dark; + if (fade) { + mViewInvertHelper.fade(dark, delay); + } else { + mViewInvertHelper.update(dark); + } } public NotificationOverflowIconsView getIconsView() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java new file mode 100644 index 0000000..59b62e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2014 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; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.ImageView; + +import com.android.systemui.R; +import com.android.systemui.ViewInvertHelper; +import com.android.systemui.statusbar.phone.NotificationPanelView; + +/** + * Wraps a notification view inflated from a template. + */ +public class NotificationTemplateViewWrapper extends NotificationViewWrapper { + + private final ColorMatrix mGrayscaleColorMatrix = new ColorMatrix(); + private final PorterDuffColorFilter mIconColorFilter = new PorterDuffColorFilter( + 0, PorterDuff.Mode.SRC_ATOP); + private final int mIconDarkAlpha; + private final int mIconBackgroundDarkColor; + private final Interpolator mLinearOutSlowInInterpolator; + + private int mIconBackgroundColor; + private ViewInvertHelper mInvertHelper; + private ImageView mIcon; + protected ImageView mPicture; + + /** Whether the icon needs to be forced grayscale when in dark mode. */ + private boolean mIconForceGraysaleWhenDark; + + protected NotificationTemplateViewWrapper(Context ctx, View view) { + super(view); + mIconDarkAlpha = ctx.getResources().getInteger(R.integer.doze_small_icon_alpha); + mIconBackgroundDarkColor = + ctx.getResources().getColor(R.color.doze_small_icon_background_color); + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(ctx, + android.R.interpolator.linear_out_slow_in); + resolveViews(); + } + + private void resolveViews() { + View mainColumn = mView.findViewById(com.android.internal.R.id.notification_main_column); + mInvertHelper = mainColumn != null + ? new ViewInvertHelper(mainColumn, NotificationPanelView.DOZE_ANIMATION_DURATION) + : null; + ImageView largeIcon = (ImageView) mView.findViewById(com.android.internal.R.id.icon); + ImageView rightIcon = (ImageView) mView.findViewById(com.android.internal.R.id.right_icon); + mIcon = resolveIcon(largeIcon, rightIcon); + mPicture = resolvePicture(largeIcon); + mIconBackgroundColor = resolveBackgroundColor(mIcon); + + // If the icon already has a color filter, we assume that we already forced the icon to be + // white when we created the notification. + mIconForceGraysaleWhenDark = mIcon != null && mIcon.getDrawable().getColorFilter() != null; + } + + private ImageView resolveIcon(ImageView largeIcon, ImageView rightIcon) { + return largeIcon != null && largeIcon.getBackground() != null ? largeIcon + : rightIcon != null && rightIcon.getVisibility() == View.VISIBLE ? rightIcon + : null; + } + + private ImageView resolvePicture(ImageView largeIcon) { + return largeIcon != null && largeIcon.getBackground() == null + ? largeIcon + : null; + } + + private int resolveBackgroundColor(ImageView icon) { + if (icon != null && icon.getBackground() != null) { + ColorFilter filter = icon.getBackground().getColorFilter(); + if (filter instanceof PorterDuffColorFilter) { + return ((PorterDuffColorFilter) filter).getColor(); + } + } + return 0; + } + + @Override + public void notifyContentUpdated() { + super.notifyContentUpdated(); + + // Reinspect the notification. + resolveViews(); + } + + @Override + public void setDark(boolean dark, boolean fade, long delay) { + if (mInvertHelper != null) { + if (fade) { + mInvertHelper.fade(dark, delay); + } else { + mInvertHelper.update(dark); + } + } + if (mIcon != null) { + if (fade) { + fadeIconColorFilter(mIcon, dark, delay); + fadeIconAlpha(mIcon, dark, delay); + if (!mIconForceGraysaleWhenDark) { + fadeGrayscale(mIcon, dark, delay); + } + } else { + updateIconColorFilter(mIcon, dark); + updateIconAlpha(mIcon, dark); + if (!mIconForceGraysaleWhenDark) { + updateGrayscale(mIcon, dark); + } + } + } + setPictureGrayscale(dark, fade, delay); + } + + protected void setPictureGrayscale(boolean grayscale, boolean fade, long delay) { + if (mPicture != null) { + if (fade) { + fadeGrayscale(mPicture, grayscale, delay); + } else { + updateGrayscale(mPicture, grayscale); + } + } + } + + private void startIntensityAnimation(ValueAnimator.AnimatorUpdateListener updateListener, + boolean dark, long delay, Animator.AnimatorListener listener) { + float startIntensity = dark ? 0f : 1f; + float endIntensity = dark ? 1f : 0f; + ValueAnimator animator = ValueAnimator.ofFloat(startIntensity, endIntensity); + animator.addUpdateListener(updateListener); + animator.setDuration(NotificationPanelView.DOZE_ANIMATION_DURATION); + animator.setInterpolator(mLinearOutSlowInInterpolator); + animator.setStartDelay(delay); + if (listener != null) { + animator.addListener(listener); + } + animator.start(); + } + + private void fadeIconColorFilter(final ImageView target, boolean dark, long delay) { + startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + updateIconColorFilter(target, (Float) animation.getAnimatedValue()); + } + }, dark, delay, null /* listener */); + } + + private void fadeIconAlpha(final ImageView target, boolean dark, long delay) { + startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = (float) animation.getAnimatedValue(); + target.setImageAlpha((int) (255 * (1f - t) + mIconDarkAlpha * t)); + } + }, dark, delay, null /* listener */); + } + + protected void fadeGrayscale(final ImageView target, final boolean dark, long delay) { + startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + updateGrayscaleMatrix((float) animation.getAnimatedValue()); + target.setColorFilter(new ColorMatrixColorFilter(mGrayscaleColorMatrix)); + } + }, dark, delay, new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (!dark) { + target.setColorFilter(null); + } + } + }); + } + + private void updateIconColorFilter(ImageView target, boolean dark) { + updateIconColorFilter(target, dark ? 1f : 0f); + } + + private void updateIconColorFilter(ImageView target, float intensity) { + int color = interpolateColor(mIconBackgroundColor, mIconBackgroundDarkColor, intensity); + mIconColorFilter.setColor(color); + Drawable background = target.getBackground(); + + // The background might be null for legacy notifications. Also, the notification might have + // been modified during the animation, so background might be null here. + if (background != null) { + background.mutate().setColorFilter(mIconColorFilter); + } + } + + private void updateIconAlpha(ImageView target, boolean dark) { + target.setImageAlpha(dark ? mIconDarkAlpha : 255); + } + + protected void updateGrayscale(ImageView target, boolean dark) { + if (dark) { + updateGrayscaleMatrix(1f); + target.setColorFilter(new ColorMatrixColorFilter(mGrayscaleColorMatrix)); + } else { + target.setColorFilter(null); + } + } + + private void updateGrayscaleMatrix(float intensity) { + mGrayscaleColorMatrix.setSaturation(1 - intensity); + } + + private static int interpolateColor(int source, int target, float t) { + int aSource = Color.alpha(source); + int rSource = Color.red(source); + int gSource = Color.green(source); + int bSource = Color.blue(source); + int aTarget = Color.alpha(target); + int rTarget = Color.red(target); + int gTarget = Color.green(target); + int bTarget = Color.blue(target); + return Color.argb( + (int) (aSource * (1f - t) + aTarget * t), + (int) (rSource * (1f - t) + rTarget * t), + (int) (gSource * (1f - t) + gTarget * t), + (int) (bSource * (1f - t) + bTarget * t)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java new file mode 100644 index 0000000..78b9739 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 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; + +import android.content.Context; +import android.view.View; + +import com.android.internal.R; + +/** + * Wraps the actual notification content view; used to implement behaviors which are different for + * the individual templates and custom views. + */ +public abstract class NotificationViewWrapper { + + protected final View mView; + + public static NotificationViewWrapper wrap(Context ctx, View v) { + + // TODO: Figure out a better way to find out which template the view is. + if (v.findViewById(com.android.internal.R.id.media_actions) != null) { + return new NotificationMediaViewWrapper(ctx, v); + } else if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) { + return new NotificationTemplateViewWrapper(ctx, v); + } else { + return new NotificationCustomViewWrapper(v); + } + } + + protected NotificationViewWrapper(View view) { + mView = view; + } + + /** + * In dark mode, we draw as little as possible, assuming a black background. + * + * @param dark whether we should display ourselves in dark mode + * @param fade whether to animate the transition if the mode changes + * @param delay if fading, the delay of the animation + */ + public abstract void setDark(boolean dark, boolean fade, long delay); + + /** + * Notifies this wrapper that the content of the view might have changed. + */ + public void notifyContentUpdated() {} +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java index b71c9bf..8e35ee9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java @@ -17,8 +17,10 @@ package com.android.systemui.statusbar; import android.content.Context; +import android.telephony.SubscriptionInfo; import android.util.AttributeSet; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; @@ -29,6 +31,9 @@ import com.android.systemui.R; import com.android.systemui.statusbar.policy.NetworkControllerImpl; import com.android.systemui.statusbar.policy.SecurityController; +import java.util.ArrayList; +import java.util.List; + // Intimately tied to the design of res/layout/signal_cluster_view.xml public class SignalClusterView extends LinearLayout @@ -41,23 +46,26 @@ public class SignalClusterView NetworkControllerImpl mNC; SecurityController mSC; + private boolean mNoSimsVisible = false; private boolean mVpnVisible = false; private boolean mWifiVisible = false; private int mWifiStrengthId = 0; - private boolean mMobileVisible = false; - private int mMobileStrengthId = 0, mMobileTypeId = 0; private boolean mIsAirplaneMode = false; private int mAirplaneIconId = 0; - private String mWifiDescription, mMobileDescription, mMobileTypeDescription; - private boolean mRoaming; - private boolean mIsMobileTypeIconWide; + private int mAirplaneContentDescription; + private String mWifiDescription; + private ArrayList<PhoneState> mPhoneStates = new ArrayList<PhoneState>(); - ViewGroup mWifiGroup, mMobileGroup; - ImageView mVpn, mWifi, mMobile, mMobileType, mAirplane; + ViewGroup mWifiGroup; + ImageView mVpn, mWifi, mAirplane, mNoSims; View mWifiAirplaneSpacer; View mWifiSignalSpacer; + LinearLayout mMobileSignalGroup; private int mWideTypeIconStartPadding; + private int mSecondaryTelephonyPadding; + private int mEndPadding; + private int mEndPaddingNothingVisible; public SignalClusterView(Context context) { this(context, null); @@ -88,6 +96,12 @@ public class SignalClusterView super.onFinishInflate(); mWideTypeIconStartPadding = getContext().getResources().getDimensionPixelSize( R.dimen.wide_type_icon_start_padding); + mSecondaryTelephonyPadding = getContext().getResources().getDimensionPixelSize( + R.dimen.secondary_telephony_padding); + mEndPadding = getContext().getResources().getDimensionPixelSize( + R.dimen.signal_cluster_battery_padding); + mEndPaddingNothingVisible = getContext().getResources().getDimensionPixelSize( + R.dimen.no_signal_cluster_battery_padding); } @Override @@ -97,12 +111,14 @@ public class SignalClusterView mVpn = (ImageView) findViewById(R.id.vpn); mWifiGroup = (ViewGroup) findViewById(R.id.wifi_combo); mWifi = (ImageView) findViewById(R.id.wifi_signal); - mMobileGroup = (ViewGroup) findViewById(R.id.mobile_combo); - mMobile = (ImageView) findViewById(R.id.mobile_signal); - mMobileType = (ImageView) findViewById(R.id.mobile_type); mAirplane = (ImageView) findViewById(R.id.airplane); + mNoSims = (ImageView) findViewById(R.id.no_sims); mWifiAirplaneSpacer = findViewById(R.id.wifi_airplane_spacer); mWifiSignalSpacer = findViewById(R.id.wifi_signal_spacer); + mMobileSignalGroup = (LinearLayout) findViewById(R.id.mobile_signal_group); + for (PhoneState state : mPhoneStates) { + mMobileSignalGroup.addView(state.mMobileGroup); + } apply(); } @@ -112,10 +128,9 @@ public class SignalClusterView mVpn = null; mWifiGroup = null; mWifi = null; - mMobileGroup = null; - mMobile = null; - mMobileType = null; mAirplane = null; + mMobileSignalGroup.removeAllViews(); + mMobileSignalGroup = null; super.onDetachedFromWindow(); } @@ -143,23 +158,60 @@ public class SignalClusterView @Override public void setMobileDataIndicators(boolean visible, int strengthIcon, int typeIcon, - String contentDescription, String typeContentDescription, boolean roaming, - boolean isTypeIconWide) { - mMobileVisible = visible; - mMobileStrengthId = strengthIcon; - mMobileTypeId = typeIcon; - mMobileDescription = contentDescription; - mMobileTypeDescription = typeContentDescription; - mRoaming = roaming; - mIsMobileTypeIconWide = isTypeIconWide; + String contentDescription, String typeContentDescription, boolean isTypeIconWide, + int subId) { + PhoneState state = getOrInflateState(subId); + state.mMobileVisible = visible; + state.mMobileStrengthId = strengthIcon; + state.mMobileTypeId = typeIcon; + state.mMobileDescription = contentDescription; + state.mMobileTypeDescription = typeContentDescription; + state.mIsMobileTypeIconWide = isTypeIconWide; apply(); } @Override - public void setIsAirplaneMode(boolean is, int airplaneIconId) { + public void setNoSims(boolean show) { + mNoSimsVisible = show; + } + + @Override + public void setSubs(List<SubscriptionInfo> subs) { + // Clear out all old subIds. + mPhoneStates.clear(); + if (mMobileSignalGroup != null) { + mMobileSignalGroup.removeAllViews(); + } + final int n = subs.size(); + for (int i = 0; i < n; i++) { + inflatePhoneState(subs.get(i).getSubscriptionId()); + } + } + + private PhoneState getOrInflateState(int subId) { + for (PhoneState state : mPhoneStates) { + if (state.mSubId == subId) { + return state; + } + } + return inflatePhoneState(subId); + } + + private PhoneState inflatePhoneState(int subId) { + PhoneState state = new PhoneState(subId, mContext); + if (mMobileSignalGroup != null) { + mMobileSignalGroup.addView(state.mMobileGroup); + } + mPhoneStates.add(state); + return state; + } + + @Override + public void setIsAirplaneMode(boolean is, int airplaneIconId, int contentDescription) { mIsAirplaneMode = is; mAirplaneIconId = airplaneIconId; + mAirplaneContentDescription = contentDescription; apply(); } @@ -170,8 +222,9 @@ public class SignalClusterView // ignore content description, so populate manually if (mWifiVisible && mWifiGroup != null && mWifiGroup.getContentDescription() != null) event.getText().add(mWifiGroup.getContentDescription()); - if (mMobileVisible && mMobileGroup != null && mMobileGroup.getContentDescription() != null) - event.getText().add(mMobileGroup.getContentDescription()); + for (PhoneState state : mPhoneStates) { + state.populateAccessibilityEvent(event); + } return super.dispatchPopulateAccessibilityEvent(event); } @@ -183,12 +236,13 @@ public class SignalClusterView mWifi.setImageDrawable(null); } - if (mMobile != null) { - mMobile.setImageDrawable(null); - } - - if (mMobileType != null) { - mMobileType.setImageDrawable(null); + for (PhoneState state : mPhoneStates) { + if (state.mMobile != null) { + state.mMobile.setImageDrawable(null); + } + if (state.mMobileType != null) { + state.mMobileType.setImageDrawable(null); + } } if(mAirplane != null) { @@ -222,17 +276,21 @@ public class SignalClusterView (mWifiVisible ? "VISIBLE" : "GONE"), mWifiStrengthId)); - if (mMobileVisible && !mIsAirplaneMode) { - mMobile.setImageResource(mMobileStrengthId); - mMobileType.setImageResource(mMobileTypeId); - mMobileGroup.setContentDescription(mMobileTypeDescription + " " + mMobileDescription); - mMobileGroup.setVisibility(View.VISIBLE); - } else { - mMobileGroup.setVisibility(View.GONE); + boolean anyMobileVisible = false; + int firstMobileTypeId = 0; + for (PhoneState state : mPhoneStates) { + if (state.apply(anyMobileVisible)) { + if (!anyMobileVisible) { + firstMobileTypeId = state.mMobileTypeId; + anyMobileVisible = true; + } + } } if (mIsAirplaneMode) { mAirplane.setImageResource(mAirplaneIconId); + mAirplane.setContentDescription(mAirplaneContentDescription != 0 ? + mContext.getString(mAirplaneContentDescription) : null); mAirplane.setVisibility(View.VISIBLE); } else { mAirplane.setVisibility(View.GONE); @@ -244,20 +302,73 @@ public class SignalClusterView mWifiAirplaneSpacer.setVisibility(View.GONE); } - if (mRoaming && mMobileVisible && mWifiVisible) { + if (((anyMobileVisible && firstMobileTypeId != 0) || mNoSimsVisible) && mWifiVisible) { mWifiSignalSpacer.setVisibility(View.VISIBLE); } else { mWifiSignalSpacer.setVisibility(View.GONE); } - mMobile.setPaddingRelative(mIsMobileTypeIconWide ? mWideTypeIconStartPadding : 0, 0, 0, 0); + mNoSims.setVisibility(mNoSimsVisible ? View.VISIBLE : View.GONE); - if (DEBUG) Log.d(TAG, - String.format("mobile: %s sig=%d typ=%d", - (mMobileVisible ? "VISIBLE" : "GONE"), - mMobileStrengthId, mMobileTypeId)); + boolean anythingVisible = mNoSimsVisible || mWifiVisible || mIsAirplaneMode + || anyMobileVisible || mVpnVisible; + setPaddingRelative(0, 0, anythingVisible ? mEndPadding : mEndPaddingNothingVisible, 0); + } + + private class PhoneState { + private final int mSubId; + private boolean mMobileVisible = false; + private int mMobileStrengthId = 0, mMobileTypeId = 0; + private boolean mIsMobileTypeIconWide; + private String mMobileDescription, mMobileTypeDescription; + + private ViewGroup mMobileGroup; + private ImageView mMobile, mMobileType; + + public PhoneState(int subId, Context context) { + ViewGroup root = (ViewGroup) LayoutInflater.from(context) + .inflate(R.layout.mobile_signal_group, null); + setViews(root); + mSubId = subId; + } + + public void setViews(ViewGroup root) { + mMobileGroup = root; + mMobile = (ImageView) root.findViewById(R.id.mobile_signal); + mMobileType = (ImageView) root.findViewById(R.id.mobile_type); + } - mMobileType.setVisibility((mRoaming || mMobileTypeId != 0) ? View.VISIBLE : View.GONE); + public boolean apply(boolean isSecondaryIcon) { + if (mMobileVisible && !mIsAirplaneMode) { + mMobile.setImageResource(mMobileStrengthId); + mMobileType.setImageResource(mMobileTypeId); + mMobileGroup.setContentDescription(mMobileTypeDescription + + " " + mMobileDescription); + mMobileGroup.setVisibility(View.VISIBLE); + } else { + mMobileGroup.setVisibility(View.GONE); + } + + // When this isn't next to wifi, give it some extra padding between the signals. + mMobileGroup.setPaddingRelative(isSecondaryIcon ? mSecondaryTelephonyPadding : 0, + 0, 0, 0); + mMobile.setPaddingRelative(mIsMobileTypeIconWide ? mWideTypeIconStartPadding : 0, + 0, 0, 0); + + if (DEBUG) Log.d(TAG, String.format("mobile: %s sig=%d typ=%d", + (mMobileVisible ? "VISIBLE" : "GONE"), mMobileStrengthId, mMobileTypeId)); + + mMobileType.setVisibility(mMobileTypeId != 0 ? View.VISIBLE : View.GONE); + + return mMobileVisible; + } + + public void populateAccessibilityEvent(AccessibilityEvent event) { + if (mMobileVisible && mMobileGroup != null + && mMobileGroup.getContentDescription() != null) { + event.getText().add(mMobileGroup.getContentDescription()); + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java index 32fb567..e89e15d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java @@ -45,6 +45,7 @@ public class BarTransitions { public static final int MODE_LIGHTS_OUT = 3; public static final int MODE_TRANSPARENT = 4; public static final int MODE_WARNING = 5; + public static final int MODE_LIGHTS_OUT_TRANSPARENT = 6; public static final int LIGHTS_IN_DURATION = 250; public static final int LIGHTS_OUT_DURATION = 750; @@ -75,6 +76,9 @@ public class BarTransitions { || mode == MODE_TRANSPARENT)) { mode = MODE_OPAQUE; } + if (!HIGH_END && (mode == MODE_LIGHTS_OUT_TRANSPARENT)) { + mode = MODE_LIGHTS_OUT; + } if (mMode == mode) return; int oldMode = mMode; mMode = mode; @@ -102,6 +106,7 @@ public class BarTransitions { if (mode == MODE_LIGHTS_OUT) return "MODE_LIGHTS_OUT"; if (mode == MODE_TRANSPARENT) return "MODE_TRANSPARENT"; if (mode == MODE_WARNING) return "MODE_WARNING"; + if (mode == MODE_LIGHTS_OUT_TRANSPARENT) return "MODE_LIGHTS_OUT_TRANSPARENT"; throw new IllegalArgumentException("Unknown mode " + mode); } @@ -109,6 +114,10 @@ public class BarTransitions { mBarBackground.finishAnimation(); } + protected boolean isLightsOut(int mode) { + return mode == MODE_LIGHTS_OUT || mode == MODE_LIGHTS_OUT_TRANSPARENT; + } + private static class BarBackgroundDrawable extends Drawable { private final int mOpaque; private final int mSemiTransparent; @@ -196,7 +205,7 @@ public class BarTransitions { targetColor = mSemiTransparent; } else if (mMode == MODE_SEMI_TRANSPARENT) { targetColor = mSemiTransparent; - } else if (mMode == MODE_TRANSPARENT) { + } else if (mMode == MODE_TRANSPARENT || mMode == MODE_LIGHTS_OUT_TRANSPARENT) { targetColor = mTransparent; } else { targetColor = mOpaque; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index c5d06b9..6cb5bcc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -122,9 +122,14 @@ public class DemoStatusIcons extends LinearLayout implements DemoMode { } String cast = args.getString("cast"); if (cast != null) { - int iconId = cast.equals("cast") ? R.drawable.stat_sys_cast : 0; + int iconId = cast.equals("show") ? R.drawable.stat_sys_cast : 0; updateSlot("cast", null, iconId); } + String hotspot = args.getString("hotspot"); + if (hotspot != null) { + int iconId = hotspot.equals("show") ? R.drawable.stat_sys_hotspot : 0; + updateSlot("hotspot", null, iconId); + } } } @@ -154,7 +159,7 @@ public class DemoStatusIcons extends LinearLayout implements DemoMode { } } StatusBarIcon icon = new StatusBarIcon(iconPkg, UserHandle.CURRENT, iconId, 0, 0, "Demo"); - StatusBarIconView v = new StatusBarIconView(getContext(), null); + StatusBarIconView v = new StatusBarIconView(getContext(), null, null); v.setTag(slot); v.set(icon); addView(v, 0, new LinearLayout.LayoutParams(mIconSize, mIconSize)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index b566bbc..6b167b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -46,30 +46,44 @@ public class DozeParameters { public void dump(PrintWriter pw) { pw.println(" DozeParameters:"); pw.print(" getDisplayStateSupported(): "); pw.println(getDisplayStateSupported()); - pw.print(" getPulseDuration(): "); pw.println(getPulseDuration()); - pw.print(" getPulseInDuration(): "); pw.println(getPulseInDuration()); + pw.print(" getPulseDuration(pickup=false): "); pw.println(getPulseDuration(false)); + pw.print(" getPulseDuration(pickup=true): "); pw.println(getPulseDuration(true)); + pw.print(" getPulseInDuration(pickup=false): "); pw.println(getPulseInDuration(false)); + pw.print(" getPulseInDuration(pickup=true): "); pw.println(getPulseInDuration(true)); + pw.print(" getPulseInDelay(pickup=false): "); pw.println(getPulseInDelay(false)); + pw.print(" getPulseInDelay(pickup=true): "); pw.println(getPulseInDelay(true)); pw.print(" getPulseInVisibleDuration(): "); pw.println(getPulseVisibleDuration()); pw.print(" getPulseOutDuration(): "); pw.println(getPulseOutDuration()); pw.print(" getPulseOnSigMotion(): "); pw.println(getPulseOnSigMotion()); pw.print(" getVibrateOnSigMotion(): "); pw.println(getVibrateOnSigMotion()); pw.print(" getPulseOnPickup(): "); pw.println(getPulseOnPickup()); pw.print(" getVibrateOnPickup(): "); pw.println(getVibrateOnPickup()); + pw.print(" getProxCheckBeforePulse(): "); pw.println(getProxCheckBeforePulse()); pw.print(" getPulseOnNotifications(): "); pw.println(getPulseOnNotifications()); pw.print(" getPulseSchedule(): "); pw.println(getPulseSchedule()); pw.print(" getPulseScheduleResets(): "); pw.println(getPulseScheduleResets()); pw.print(" getPickupVibrationThreshold(): "); pw.println(getPickupVibrationThreshold()); + pw.print(" getPickupPerformsProxCheck(): "); pw.println(getPickupPerformsProxCheck()); } public boolean getDisplayStateSupported() { return getBoolean("doze.display.supported", R.bool.doze_display_state_supported); } - public int getPulseDuration() { - return getPulseInDuration() + getPulseVisibleDuration() + getPulseOutDuration(); + public int getPulseDuration(boolean pickup) { + return getPulseInDuration(pickup) + getPulseVisibleDuration() + getPulseOutDuration(); } - public int getPulseInDuration() { - return getInt("doze.pulse.duration.in", R.integer.doze_pulse_duration_in); + public int getPulseInDuration(boolean pickup) { + return pickup + ? getInt("doze.pulse.duration.in.pickup", R.integer.doze_pulse_duration_in_pickup) + : getInt("doze.pulse.duration.in", R.integer.doze_pulse_duration_in); + } + + public int getPulseInDelay(boolean pickup) { + return pickup + ? getInt("doze.pulse.delay.in.pickup", R.integer.doze_pulse_delay_in_pickup) + : getInt("doze.pulse.delay.in", R.integer.doze_pulse_delay_in); } public int getPulseVisibleDuration() { @@ -96,6 +110,14 @@ public class DozeParameters { return SystemProperties.getBoolean("doze.vibrate.pickup", false); } + public boolean getProxCheckBeforePulse() { + return getBoolean("doze.pulse.proxcheck", R.bool.doze_proximity_check_before_pulse); + } + + public boolean getPickupPerformsProxCheck() { + return getBoolean("doze.pickup.proxcheck", R.bool.doze_pickup_performs_proximity_check); + } + public boolean getPulseOnNotifications() { return getBoolean("doze.pulse.notifications", R.bool.doze_pulse_on_notifications); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java new file mode 100644 index 0000000..3e17328 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2014 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.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.content.Context; +import android.os.Handler; +import android.util.Log; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +import com.android.systemui.doze.DozeHost; +import com.android.systemui.doze.DozeLog; + +/** + * Controller which handles all the doze animations of the scrims. + */ +public class DozeScrimController { + private static final String TAG = "DozeScrimController"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final DozeParameters mDozeParameters; + private final Interpolator mPulseInInterpolator = PhoneStatusBar.ALPHA_OUT; + private final Interpolator mPulseInInterpolatorPickup; + private final Interpolator mPulseOutInterpolator = PhoneStatusBar.ALPHA_IN; + private final Interpolator mDozeAnimationInterpolator; + private final Handler mHandler = new Handler(); + private final ScrimController mScrimController; + + private boolean mDozing; + private DozeHost.PulseCallback mPulseCallback; + private int mPulseReason; + private Animator mInFrontAnimator; + private Animator mBehindAnimator; + private float mInFrontTarget; + private float mBehindTarget; + + public DozeScrimController(ScrimController scrimController, Context context) { + mScrimController = scrimController; + mDozeParameters = new DozeParameters(context); + mDozeAnimationInterpolator = mPulseInInterpolatorPickup = + AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); + } + + public void setDozing(boolean dozing, boolean animate) { + if (mDozing == dozing) return; + mDozing = dozing; + if (mDozing) { + abortAnimations(); + mScrimController.setDozeBehindAlpha(1f); + mScrimController.setDozeInFrontAlpha(1f); + } else { + cancelPulsing(); + if (animate) { + startScrimAnimation(false /* inFront */, 0f /* target */, + NotificationPanelView.DOZE_ANIMATION_DURATION, mDozeAnimationInterpolator); + startScrimAnimation(true /* inFront */, 0f /* target */, + NotificationPanelView.DOZE_ANIMATION_DURATION, mDozeAnimationInterpolator); + } else { + abortAnimations(); + mScrimController.setDozeBehindAlpha(0f); + mScrimController.setDozeInFrontAlpha(0f); + } + } + } + + /** When dozing, fade screen contents in and out using the front scrim. */ + public void pulse(@NonNull DozeHost.PulseCallback callback, int reason) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + + if (!mDozing || mPulseCallback != null) { + // Pulse suppressed. + callback.onPulseFinished(); + return; + } + + // Begin pulse. Note that it's very important that the pulse finished callback + // be invoked when we're done so that the caller can drop the pulse wakelock. + mPulseCallback = callback; + mPulseReason = reason; + mHandler.post(mPulseIn); + } + + public boolean isPulsing() { + return mPulseCallback != null; + } + + private void cancelPulsing() { + if (DEBUG) Log.d(TAG, "Cancel pulsing"); + + if (mPulseCallback != null) { + mHandler.removeCallbacks(mPulseIn); + mHandler.removeCallbacks(mPulseOut); + pulseFinished(); + } + } + + private void pulseStarted() { + if (mPulseCallback != null) { + mPulseCallback.onPulseStarted(); + } + } + + private void pulseFinished() { + if (mPulseCallback != null) { + mPulseCallback.onPulseFinished(); + mPulseCallback = null; + } + } + + private void abortAnimations() { + if (mInFrontAnimator != null) { + mInFrontAnimator.cancel(); + } + if (mBehindAnimator != null) { + mBehindAnimator.cancel(); + } + } + + private void startScrimAnimation(final boolean inFront, float target, long duration, + Interpolator interpolator) { + startScrimAnimation(inFront, target, duration, interpolator, 0 /* delay */, + null /* endRunnable */); + } + + private void startScrimAnimation(final boolean inFront, float target, long duration, + Interpolator interpolator, long delay, final Runnable endRunnable) { + Animator current = getCurrentAnimator(inFront); + if (current != null) { + float currentTarget = getCurrentTarget(inFront); + if (currentTarget == target) { + return; + } + current.cancel(); + } + ValueAnimator anim = ValueAnimator.ofFloat(getDozeAlpha(inFront), target); + anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float value = (float) animation.getAnimatedValue(); + setDozeAlpha(inFront, value); + } + }); + anim.setInterpolator(interpolator); + anim.setDuration(duration); + anim.setStartDelay(delay); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setCurrentAnimator(inFront, null); + if (endRunnable != null) { + endRunnable.run(); + } + } + }); + anim.start(); + setCurrentAnimator(inFront, anim); + setCurrentTarget(inFront, target); + } + + private float getCurrentTarget(boolean inFront) { + return inFront ? mInFrontTarget : mBehindTarget; + } + + private void setCurrentTarget(boolean inFront, float target) { + if (inFront) { + mInFrontTarget = target; + } else { + mBehindTarget = target; + } + } + + private Animator getCurrentAnimator(boolean inFront) { + return inFront ? mInFrontAnimator : mBehindAnimator; + } + + private void setCurrentAnimator(boolean inFront, Animator animator) { + if (inFront) { + mInFrontAnimator = animator; + } else { + mBehindAnimator = animator; + } + } + + private void setDozeAlpha(boolean inFront, float alpha) { + if (inFront) { + mScrimController.setDozeInFrontAlpha(alpha); + } else { + mScrimController.setDozeBehindAlpha(alpha); + } + } + + private float getDozeAlpha(boolean inFront) { + return inFront + ? mScrimController.getDozeInFrontAlpha() + : mScrimController.getDozeBehindAlpha(); + } + + private final Runnable mPulseIn = new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "Pulse in, mDozing=" + mDozing + " mPulseReason=" + + DozeLog.pulseReasonToString(mPulseReason)); + if (!mDozing) return; + DozeLog.tracePulseStart(mPulseReason); + final boolean pickup = mPulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP; + startScrimAnimation(true /* inFront */, 0f, + mDozeParameters.getPulseInDuration(pickup), + pickup ? mPulseInInterpolatorPickup : mPulseInInterpolator, + mDozeParameters.getPulseInDelay(pickup), + mPulseInFinished); + + // Signal that the pulse is ready to turn the screen on and draw. + pulseStarted(); + } + }; + + private final Runnable mPulseInFinished = new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "Pulse in finished, mDozing=" + mDozing); + if (!mDozing) return; + mHandler.postDelayed(mPulseOut, mDozeParameters.getPulseVisibleDuration()); + } + }; + + private final Runnable mPulseOut = new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "Pulse out, mDozing=" + mDozing); + if (!mDozing) return; + startScrimAnimation(true /* inFront */, 1f, mDozeParameters.getPulseOutDuration(), + mPulseOutInterpolator, 0 /* delay */, mPulseOutFinished); + } + }; + + private final Runnable mPulseOutFinished = new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "Pulse out finished"); + DozeLog.tracePulseFinish(); + + // Signal that the pulse is all finished so we can turn the screen off now. + pulseFinished(); + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java index f1dcffb..3b8fccc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java @@ -317,7 +317,7 @@ public class KeyguardAffordanceHelper { animator.addListener(mFlingEndListener); if (!snapBack) { startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable); - mCallback.onAnimationToSideStarted(mTranslation < 0); + mCallback.onAnimationToSideStarted(mTranslation < 0, mTranslation, vel); } else { reset(true); } @@ -442,6 +442,10 @@ public class KeyguardAffordanceHelper { initIcons(); } + public void onRtlPropertiesChanged() { + initIcons(); + } + public void reset(boolean animate) { if (mSwipeAnimator != null) { mSwipeAnimator.cancel(); @@ -457,7 +461,7 @@ public class KeyguardAffordanceHelper { * * @param rightPage Is the page animated to the right page? */ - void onAnimationToSideStarted(boolean rightPage); + void onAnimationToSideStarted(boolean rightPage, float translation, float vel); /** * Notifies the callback the animation to a side page has ended. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index dd5df12..0c21b20 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -39,12 +39,16 @@ import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.TextView; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.EventLogConstants; +import com.android.systemui.EventLogTags; import com.android.systemui.R; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.KeyguardAffordanceView; @@ -72,6 +76,8 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private static final Intent INSECURE_CAMERA_INTENT = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); private static final Intent PHONE_INTENT = new Intent(Intent.ACTION_DIAL); + private static final int DOZE_ANIMATION_STAGGER_DELAY = 48; + private static final int DOZE_ANIMATION_ELEMENT_DURATION = 250; private KeyguardAffordanceView mCameraImageView; private KeyguardAffordanceView mPhoneImageView; @@ -92,7 +98,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private PhoneStatusBar mPhoneStatusBar; private final TrustDrawable mTrustDrawable; - + private final Interpolator mLinearOutSlowInInterpolator; private int mLastUnlockIconRes = 0; public KeyguardBottomAreaView(Context context) { @@ -111,6 +117,8 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mTrustDrawable = new TrustDrawable(mContext); + mLinearOutSlowInInterpolator = + AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); } private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { @@ -133,7 +141,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL if (action == ACTION_CLICK) { if (host == mLockIcon) { mPhoneStatusBar.animateCollapsePanels( - CommandQueue.FLAG_EXCLUDE_NONE, true /* force */); + CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */); return true; } else if (host == mCameraImageView) { launchCamera(); @@ -212,6 +220,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL public void setPhoneStatusBar(PhoneStatusBar phoneStatusBar) { mPhoneStatusBar = phoneStatusBar; + updateCameraVisibility(); // in case onFinishInflate() was called too early } private Intent getCameraIntent() { @@ -223,6 +232,10 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } private void updateCameraVisibility() { + if (mCameraImageView == null) { + // Things are not set up yet; reply hazy, ask again later + return; + } ResolveInfo resolved = mContext.getPackageManager().resolveActivityAsUser(getCameraIntent(), PackageManager.MATCH_DEFAULT_ONLY, mLockPatternUtils.getCurrentUser()); @@ -245,13 +258,13 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private boolean isCameraDisabledByDpm() { final DevicePolicyManager dpm = (DevicePolicyManager) getContext().getSystemService(Context.DEVICE_POLICY_SERVICE); - if (dpm != null) { + if (dpm != null && mPhoneStatusBar != 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(); + && mPhoneStatusBar.isKeyguardSecure(); return dpm.getCameraDisabled(null) || disabledBecauseKeyguardSecure; } catch (RemoteException e) { Log.e(TAG, "Can't get userId", e); @@ -314,6 +327,9 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } private void handleTrustCircleClick() { + EventLogTags.writeSysuiLockscreenGesture( + EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_TAP_LOCK, 0 /* lengthDp - N/A */, + 0 /* velocityDp - N/A */); mIndicationController.showTransientIndication( R.string.keyguard_indication_trust_disabled); mLockPatternUtils.requireCredentialEntry(mLockPatternUtils.getCurrentUser()); @@ -382,7 +398,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL // TODO: Real icon for facelock. int iconRes = mUnlockMethodCache.isFaceUnlockRunning() ? com.android.internal.R.drawable.ic_account_circle - : mUnlockMethodCache.isMethodInsecure() ? R.drawable.ic_lock_open_24dp + : mUnlockMethodCache.isCurrentlyInsecure() ? R.drawable.ic_lock_open_24dp : R.drawable.ic_lock_24dp; if (mLastUnlockIconRes != iconRes) { Drawable icon = mContext.getDrawable(iconRes); @@ -432,7 +448,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } @Override - public void onMethodSecureChanged(boolean methodSecure) { + public void onUnlockMethodStateChanged() { updateLockIcon(); updateCameraVisibility(); } @@ -450,6 +466,35 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } } + public void startFinishDozeAnimation() { + long delay = 0; + if (mPhoneImageView.getVisibility() == View.VISIBLE) { + startFinishDozeAnimationElement(mPhoneImageView, delay); + delay += DOZE_ANIMATION_STAGGER_DELAY; + } + startFinishDozeAnimationElement(mLockIcon, delay); + delay += DOZE_ANIMATION_STAGGER_DELAY; + if (mCameraImageView.getVisibility() == View.VISIBLE) { + startFinishDozeAnimationElement(mCameraImageView, delay); + } + mIndicationText.setAlpha(0f); + mIndicationText.animate() + .alpha(1f) + .setInterpolator(mLinearOutSlowInInterpolator) + .setDuration(NotificationPanelView.DOZE_ANIMATION_DURATION); + } + + private void startFinishDozeAnimationElement(View element, long delay) { + element.setAlpha(0f); + element.setTranslationY(element.getHeight() / 2); + element.animate() + .alpha(1f) + .translationY(0f) + .setInterpolator(mLinearOutSlowInInterpolator) + .setStartDelay(delay) + .setDuration(DOZE_ANIMATION_ELEMENT_DURATION); + } + private final BroadcastReceiver mDevicePolicyReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { post(new Runnable() { @@ -477,6 +522,11 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL public void onScreenTurnedOff(int why) { updateLockIcon(); } + + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + updateLockIcon(); + } }; public void setKeyguardIndicationController( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index 5507944..d0fe32e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -22,6 +22,7 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardViewBase; @@ -57,13 +58,14 @@ public class KeyguardBouncer { mWindowManager = windowManager; } - public void show() { + public void show(boolean resetSecuritySelection) { ensureView(); + if (resetSecuritySelection) { + // showPrimarySecurityScreen() updates the current security method. This is needed in + // case we are already showing and the current security method changed. + mKeyguardView.showPrimarySecurityScreen(); + } if (mRoot.getVisibility() == View.VISIBLE || mShowingSoon) { - - // show() updates the current security method. This is needed in case we are already - // showing and the current security method changed. - mKeyguardView.show(); return; } @@ -74,7 +76,7 @@ public class KeyguardBouncer { // Split up the work over multiple frames. mChoreographer.postCallbackDelayed(Choreographer.CALLBACK_ANIMATION, mShowRunnable, - null, 48); + null, 16); } } @@ -85,6 +87,7 @@ public class KeyguardBouncer { mKeyguardView.onResume(); mKeyguardView.startAppearAnimation(); mShowingSoon = false; + mKeyguardView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } }; @@ -96,7 +99,7 @@ public class KeyguardBouncer { public void showWithDismissAction(OnDismissAction r) { ensureView(); mKeyguardView.setOnDismissAction(r); - show(); + show(false /* resetSecuritySelection */); } public void hide(boolean destroyView) { @@ -152,7 +155,11 @@ public class KeyguardBouncer { } public void prepare() { + boolean wasInitialized = mRoot != null; ensureView(); + if (wasInitialized) { + mKeyguardView.showPrimarySecurityScreen(); + } } private void ensureView() { @@ -184,18 +191,32 @@ public class KeyguardBouncer { } /** - * @return True if and only if the current security method should be shown before showing - * the notifications on Keyguard, like SIM PIN/PUK. + * @return True if and only if the security method should be shown before showing the + * notifications on Keyguard, like SIM PIN/PUK. */ public boolean needsFullscreenBouncer() { if (mKeyguardView != null) { SecurityMode mode = mKeyguardView.getSecurityMode(); - return mode == SecurityMode.SimPin - || mode == SecurityMode.SimPuk; + return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk; + } + return false; + } + + /** + * Like {@link #needsFullscreenBouncer}, but uses the currently visible security method, which + * makes this method much faster. + */ + public boolean isFullscreenBouncer() { + if (mKeyguardView != null) { + SecurityMode mode = mKeyguardView.getCurrentSecurityMode(); + return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk; } return false; } + /** + * WARNING: This method might cause Binder calls. + */ public boolean isSecure() { return mKeyguardView == null || mKeyguardView.getSecurityMode() != SecurityMode.None; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 650a14f..40c9134 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -36,6 +36,8 @@ import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; import com.android.systemui.statusbar.policy.UserInfoController; +import java.text.NumberFormat; + /** * The header group on Keyguard. */ @@ -150,7 +152,8 @@ public class KeyguardStatusBarView extends RelativeLayout @Override public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { - mBatteryLevel.setText(getResources().getString(R.string.battery_level_template, level)); + String percentage = NumberFormat.getPercentInstance().format((double) level / 100.0); + mBatteryLevel.setText(percentage); boolean changed = mBatteryCharging != charging; mBatteryCharging = charging; if (changed) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java deleted file mode 100644 index 754075a..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (C) 2013 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.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.UserHandle; -import android.util.Slog; -import android.view.MotionEvent; - -import com.android.internal.policy.IKeyguardService; - -import java.util.ArrayList; -import java.util.List; - - -/** - * Facilitates event communication between navigation bar and keyguard. Currently used to - * control WidgetPager in keyguard to expose the camera widget. - * - */ -public class KeyguardTouchDelegate { - // TODO: propagate changes to these to {@link KeyguardServiceDelegate} - static final String KEYGUARD_PACKAGE = "com.android.systemui"; - static final String KEYGUARD_CLASS = "com.android.systemui.keyguard.KeyguardService"; - - private static KeyguardTouchDelegate sInstance; - private static final List<OnKeyguardConnectionListener> sConnectionListeners = - new ArrayList<OnKeyguardConnectionListener>(); - - private volatile IKeyguardService mService; - - protected static final boolean DEBUG = false; - protected static final String TAG = "KeyguardTouchDelegate"; - - private final ServiceConnection mKeyguardConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - Slog.v(TAG, "Connected to keyguard"); - mService = IKeyguardService.Stub.asInterface(service); - - for (int i = 0; i < sConnectionListeners.size(); i++) { - OnKeyguardConnectionListener listener = sConnectionListeners.get(i); - listener.onKeyguardServiceConnected(KeyguardTouchDelegate.this); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - Slog.v(TAG, "Disconnected from keyguard"); - mService = null; - sInstance = null; // force reconnection if this goes away - - for (int i = 0; i < sConnectionListeners.size(); i++) { - OnKeyguardConnectionListener listener = sConnectionListeners.get(i); - listener.onKeyguardServiceDisconnected(KeyguardTouchDelegate.this); - } - } - - }; - - private KeyguardTouchDelegate(Context context) { - Intent intent = new Intent(); - intent.setClassName(KEYGUARD_PACKAGE, KEYGUARD_CLASS); - if (!context.bindServiceAsUser(intent, mKeyguardConnection, - Context.BIND_AUTO_CREATE, UserHandle.OWNER)) { - if (DEBUG) Slog.v(TAG, "*** Keyguard: can't bind to " + KEYGUARD_CLASS); - } else { - if (DEBUG) Slog.v(TAG, "*** Keyguard started"); - } - } - - public static KeyguardTouchDelegate getInstance(Context context) { - KeyguardTouchDelegate instance = sInstance; - if (instance == null) { - instance = sInstance = new KeyguardTouchDelegate(context); - } - return instance; - } - - public boolean isSecure() { - final IKeyguardService service = mService; - if (service != null) { - try { - return service.isSecure(); - } catch (RemoteException e) { - Slog.e(TAG, "RemoteException calling keyguard.isSecure()!", e); - } - } else { - Slog.w(TAG, "isSecure(): NO SERVICE!"); - } - return false; - } - - public boolean dispatch(MotionEvent event) { - final IKeyguardService service = mService; - if (service != null) { - try { - service.dispatch(event); - return true; - } catch (RemoteException e) { - // What to do? - Slog.e(TAG, "RemoteException sending event to keyguard!", e); - } - } else { - Slog.w(TAG, "dispatch(event): NO SERVICE!"); - } - return false; - } - - public boolean isInputRestricted() { - final IKeyguardService service = mService; - if (service != null) { - try { - return service.isInputRestricted(); - } catch (RemoteException e) { - Slog.w(TAG , "Remote Exception", e); - } - } else { - Slog.w(TAG, "isInputRestricted(): NO SERVICE!"); - } - return false; - } - - public boolean isShowingAndNotOccluded() { - final IKeyguardService service = mService; - if (service != null) { - try { - return service.isShowingAndNotOccluded(); - } catch (RemoteException e) { - Slog.w(TAG , "Remote Exception", e); - } - } else { - Slog.w(TAG, "isShowingAndNotOccluded(): NO SERVICE!"); - } - return false; - } - - public void showAssistant() { - final IKeyguardService service = mService; - if (service != null) { - try { - service.showAssistant(); - } catch (RemoteException e) { - // What to do? - Slog.e(TAG, "RemoteException launching assistant!", e); - } - } else { - Slog.w(TAG, "showAssistant(event): NO SERVICE!"); - } - } - - public void launchCamera() { - final IKeyguardService service = mService; - if (service != null) { - try { - service.launchCamera(); - } catch (RemoteException e) { - // What to do? - Slog.e(TAG, "RemoteException launching camera!", e); - } - } else { - Slog.w(TAG, "launchCamera(): NO SERVICE!"); - } - } - - public void dismiss() { - final IKeyguardService service = mService; - if (service != null) { - try { - service.dismiss(); - } catch (RemoteException e) { - // What to do? - Slog.e(TAG, "RemoteException dismissing keyguard!", e); - } - } else { - Slog.w(TAG, "dismiss(): NO SERVICE!"); - } - } - - public static void addListener(OnKeyguardConnectionListener listener) { - sConnectionListeners.add(listener); - } - - public interface OnKeyguardConnectionListener { - - void onKeyguardServiceConnected(KeyguardTouchDelegate keyguardTouchDelegate); - void onKeyguardServiceDisconnected(KeyguardTouchDelegate keyguardTouchDelegate); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java index 4715d0a..82f5a9e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java @@ -67,15 +67,18 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener @Override public void onClick(View v) { - if (opensUserSwitcherWhenClicked()) { + if (UserSwitcherController.isUserSwitcherAvailable(mUserManager)) { if (mKeyguardMode) { if (mKeyguardUserSwitcher != null) { mKeyguardUserSwitcher.show(true /* animate */); } } else { if (mQsPanel != null) { - mQsPanel.showDetailAdapter(true, - mQsPanel.getHost().getUserSwitcherController().userDetailAdapter); + UserSwitcherController userSwitcherController = + mQsPanel.getHost().getUserSwitcherController(); + if (userSwitcherController != null) { + mQsPanel.showDetailAdapter(true, userSwitcherController.userDetailAdapter); + } } } } else { @@ -92,12 +95,14 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener if (isClickable()) { String text; - if (opensUserSwitcherWhenClicked()) { + if (UserSwitcherController.isUserSwitcherAvailable(mUserManager)) { String currentUser = null; if (mQsPanel != null) { UserSwitcherController controller = mQsPanel.getHost() .getUserSwitcherController(); - currentUser = controller.getCurrentUserName(mContext); + if (controller != null) { + currentUser = controller.getCurrentUserName(mContext); + } } if (TextUtils.isEmpty(currentUser)) { text = mContext.getString(R.string.accessibility_multi_user_switch_switcher); @@ -121,8 +126,4 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener return false; } - private boolean opensUserSwitcherWhenClicked() { - UserManager um = UserManager.get(getContext()); - return UserManager.supportsMultipleUsers() && um.isUserSwitcherEnabled(); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java index f3930ba..7ec84da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java @@ -60,9 +60,13 @@ public final class NavigationBarTransitions extends BarTransitions { @Override public void transitionTo(int mode, boolean animate) { mRequestedMode = mode; - if (mVertical && (mode == MODE_TRANSLUCENT || mode == MODE_TRANSPARENT)) { + if (mVertical) { // translucent mode not allowed when vertical - mode = MODE_OPAQUE; + if (mode == MODE_TRANSLUCENT || mode == MODE_TRANSPARENT) { + mode = MODE_OPAQUE; + } else if (mode == MODE_LIGHTS_OUT_TRANSPARENT) { + mode = MODE_LIGHTS_OUT; + } } super.transitionTo(mode, animate); } @@ -84,7 +88,7 @@ public final class NavigationBarTransitions extends BarTransitions { applyBackButtonQuiescentAlpha(mode, animate); // apply to lights out - applyLightsOut(mode == MODE_LIGHTS_OUT, animate, force); + applyLightsOut(isLightsOut(mode), animate, force); } private float alphaForMode(int mode) { @@ -171,7 +175,8 @@ public final class NavigationBarTransitions extends BarTransitions { applyLightsOut(false, false, false); try { - mBarService.setSystemUiVisibility(0, View.SYSTEM_UI_FLAG_LOW_PROFILE); + mBarService.setSystemUiVisibility(0, View.SYSTEM_UI_FLAG_LOW_PROFILE, + "LightsOutListener"); } catch (android.os.RemoteException ex) { } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 88e71e2..1e4dfb4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -21,6 +21,7 @@ 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.content.Context; import android.content.res.Configuration; @@ -30,17 +31,21 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Message; +import android.os.RemoteException; import android.util.AttributeSet; import android.util.Log; import android.view.Display; +import android.view.Gravity; import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; +import android.widget.FrameLayout; 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; @@ -330,7 +335,7 @@ public class NavigationBarView extends LinearLayout { mDisabledFlags = disabledFlags; final boolean disableHome = ((disabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0); - final boolean disableRecent = ((disabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0); + 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); @@ -355,6 +360,11 @@ public class NavigationBarView extends LinearLayout { } } } + if (inLockTask() && disableRecent && !disableHome) { + // Don't hide recents when in lock task, it is used for exiting. + // Unless home is hidden, then in DPM locked mode and no exit available. + disableRecent = false; + } getBackButton() .setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE); getHomeButton() .setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); @@ -363,6 +373,14 @@ public class NavigationBarView extends LinearLayout { mBarTransitions.applyBackButtonQuiescentAlpha(mBarTransitions.getMode(), true /*animate*/); } + private boolean inLockTask() { + try { + return ActivityManagerNative.getDefault().isInLockTaskMode(); + } catch (RemoteException e) { + return false; + } + } + private void setVisibleOrGone(View view, boolean visible) { if (view != null) { view.setVisibility(visible ? VISIBLE : GONE); @@ -503,15 +521,31 @@ public class NavigationBarView extends LinearLayout { // We swap all children of the 90 and 270 degree layouts, since they are vertical View rotation90 = mRotatedViews[Surface.ROTATION_90]; swapChildrenOrderIfVertical(rotation90.findViewById(R.id.nav_buttons)); + adjustExtraKeyGravity(rotation90, isLayoutRtl); View rotation270 = mRotatedViews[Surface.ROTATION_270]; if (rotation90 != rotation270) { swapChildrenOrderIfVertical(rotation270.findViewById(R.id.nav_buttons)); + adjustExtraKeyGravity(rotation270, isLayoutRtl); } mIsLayoutRtl = isLayoutRtl; } } + private void adjustExtraKeyGravity(View navBar, boolean isLayoutRtl) { + View menu = navBar.findViewById(R.id.menu); + View imeSwitcher = navBar.findViewById(R.id.ime_switcher); + if (menu != null) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) menu.getLayoutParams(); + lp.gravity = isLayoutRtl ? Gravity.BOTTOM : Gravity.TOP; + menu.setLayoutParams(lp); + } + if (imeSwitcher != null) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) imeSwitcher.getLayoutParams(); + lp.gravity = isLayoutRtl ? Gravity.BOTTOM : Gravity.TOP; + imeSwitcher.setLayoutParams(lp); + } + } /** * Swaps the children order of a LinearLayout if it's orientation is Vertical diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index a6fccb6..0ae34bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -23,8 +23,9 @@ import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; +import android.graphics.Paint; import android.util.AttributeSet; import android.util.MathUtils; import android.view.MotionEvent; @@ -38,7 +39,10 @@ import android.widget.FrameLayout; import android.widget.TextView; import com.android.keyguard.KeyguardStatusView; +import com.android.systemui.EventLogTags; +import com.android.systemui.EventLogConstants; import com.android.systemui.R; +import com.android.systemui.qs.QSContainer; import com.android.systemui.qs.QSPanel; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.FlingAnimationUtils; @@ -52,7 +56,9 @@ import com.android.systemui.statusbar.stack.StackStateAnimator; public class NotificationPanelView extends PanelView implements ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener, View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener, - KeyguardAffordanceHelper.Callback { + KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener { + + private static final boolean DEBUG = false; // Cap and total height of Roboto font. Needs to be adjusted when font for the big clock is // changed. @@ -62,15 +68,13 @@ public class NotificationPanelView extends PanelView implements private static final float HEADER_RUBBERBAND_FACTOR = 2.05f; private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f; - private static final int DOZE_BACKGROUND_COLOR = 0xff000000; - private static final int TAG_KEY_ANIM = R.id.scrim; - private static final long DOZE_BACKGROUND_ANIM_DURATION = ScrimController.ANIMATION_DURATION; + public static final long DOZE_ANIMATION_DURATION = 700; private KeyguardAffordanceHelper mAfforanceHelper; private StatusBarHeaderView mHeader; private KeyguardUserSwitcher mKeyguardUserSwitcher; private KeyguardStatusBarView mKeyguardStatusBar; - private View mQsContainer; + private QSContainer mQsContainer; private QSPanel mQsPanel; private KeyguardStatusView mKeyguardStatusView; private ObservableScrollView mScrollView; @@ -132,6 +136,7 @@ public class NotificationPanelView extends PanelView implements private Interpolator mFastOutSlowInInterpolator; private Interpolator mFastOutLinearInterpolator; + private Interpolator mDozeAnimationInterpolator; private ObjectAnimator mClockAnimator; private int mClockAnimationTarget = -1; private int mTopPaddingAdjustment; @@ -143,7 +148,8 @@ public class NotificationPanelView extends PanelView implements private boolean mBlockTouches; private int mNotificationScrimWaitDistance; - private boolean mTwoFingerQsExpand; + // Used for two finger gesture as well as accessibility shortcut to QS. + private boolean mQsExpandImmediate; private boolean mTwoFingerQsExpandPossible; /** @@ -159,6 +165,7 @@ public class NotificationPanelView extends PanelView implements private boolean mKeyguardStatusViewAnimating; private boolean mHeaderAnimatingIn; private ObjectAnimator mQsContainerAnimator; + private ValueAnimator mQsSizeChangeAnimator; private boolean mShadeEmpty; @@ -167,8 +174,12 @@ public class NotificationPanelView extends PanelView implements private boolean mQsTouchAboveFalsingThreshold; private int mQsFalsingThreshold; + private float mKeyguardStatusBarAnimateAlpha = 1f; + private int mOldLayoutDirection; + public NotificationPanelView(Context context, AttributeSet attrs) { super(context, attrs); + setWillNotDraw(!DEBUG); } public void setStatusBar(PhoneStatusBar bar) { @@ -182,7 +193,7 @@ public class NotificationPanelView extends PanelView implements mHeader.setOnClickListener(this); mKeyguardStatusBar = (KeyguardStatusBarView) findViewById(R.id.keyguard_header); mKeyguardStatusView = (KeyguardStatusView) findViewById(R.id.keyguard_status_view); - mQsContainer = findViewById(R.id.quick_settings_container); + mQsContainer = (QSContainer) findViewById(R.id.quick_settings_container); mQsPanel = (QSPanel) findViewById(R.id.quick_settings_panel); mClockView = (TextView) findViewById(R.id.clock_view); mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view); @@ -194,11 +205,14 @@ public class NotificationPanelView extends PanelView implements findViewById(R.id.notification_stack_scroller); mNotificationStackScroller.setOnHeightChangedListener(this); mNotificationStackScroller.setOverscrollTopChangedListener(this); + mNotificationStackScroller.setOnEmptySpaceClickListener(this); mNotificationStackScroller.setScrollView(mScrollView); mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(), android.R.interpolator.fast_out_slow_in); mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(), android.R.interpolator.fast_out_linear_in); + mDozeAnimationInterpolator = AnimationUtils.loadInterpolator(getContext(), + android.R.interpolator.linear_out_slow_in); mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area); mQsNavbarScrim = findViewById(R.id.qs_navbar_scrim); mAfforanceHelper = new KeyguardAffordanceHelper(this, getContext()); @@ -274,21 +288,35 @@ public class NotificationPanelView extends PanelView implements mKeyguardStatusView.setPivotY((FONT_HEIGHT - CAP_HEIGHT) / 2048f * mClockView.getTextSize()); // Calculate quick setting heights. + int oldMaxHeight = mQsMaxExpansionHeight; mQsMinExpansionHeight = mKeyguardShowing ? 0 : mHeader.getCollapsedHeight() + mQsPeekHeight; - mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight(); + mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getDesiredHeight(); positionClockAndNotifications(); - if (mQsExpanded) { - if (mQsFullyExpanded) { - mQsExpansionHeight = mQsMaxExpansionHeight; - requestScrollerTopPaddingUpdate(false /* animate */); + if (mQsExpanded && mQsFullyExpanded) { + mQsExpansionHeight = mQsMaxExpansionHeight; + requestScrollerTopPaddingUpdate(false /* animate */); + requestPanelHeightUpdate(); + + // Size has changed, start an animation. + if (mQsMaxExpansionHeight != oldMaxHeight) { + startQsSizeChangeAnimation(oldMaxHeight, mQsMaxExpansionHeight); } - } else { + } else if (!mQsExpanded) { setQsExpansion(mQsMinExpansionHeight + mLastOverscroll); - mNotificationStackScroller.setStackHeight(getExpandedHeight()); - updateHeader(); } + mNotificationStackScroller.setStackHeight(getExpandedHeight()); + updateHeader(); mNotificationStackScroller.updateIsSmallScreen( mHeader.getCollapsedHeight() + mQsPeekHeight); + + // If we are running a size change animation, the animation takes care of the height of + // the container. However, if we are not animating, we always need to make the QS container + // the desired height so when closing the QS detail, it stays smaller after the size change + // animation is finished but the detail view is still being animated away (this animation + // takes longer than the size change animation). + if (mQsSizeChangeAnimator == null) { + mQsContainer.setHeightOverride(mQsContainer.getDesiredHeight()); + } } @Override @@ -301,6 +329,32 @@ public class NotificationPanelView extends PanelView implements mSecureCameraLaunchManager.destroy(); } + private void startQsSizeChangeAnimation(int oldHeight, final int newHeight) { + if (mQsSizeChangeAnimator != null) { + oldHeight = (int) mQsSizeChangeAnimator.getAnimatedValue(); + mQsSizeChangeAnimator.cancel(); + } + mQsSizeChangeAnimator = ValueAnimator.ofInt(oldHeight, newHeight); + mQsSizeChangeAnimator.setDuration(300); + mQsSizeChangeAnimator.setInterpolator(mFastOutSlowInInterpolator); + mQsSizeChangeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + requestScrollerTopPaddingUpdate(false /* animate */); + requestPanelHeightUpdate(); + int height = (int) mQsSizeChangeAnimator.getAnimatedValue(); + mQsContainer.setHeightOverride(height - mHeader.getExpandedHeight()); + } + }); + mQsSizeChangeAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mQsSizeChangeAnimator = null; + } + }); + mQsSizeChangeAnimator.start(); + } + /** * Positions the clock and notifications dynamically depending on how many notifications are * showing. @@ -422,6 +476,13 @@ public class NotificationPanelView extends PanelView implements } } + public void expandWithQs() { + if (mQsExpansionEnabled) { + mQsExpandImmediate = true; + } + expand(); + } + @Override public void fling(float vel, boolean expand) { GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder(); @@ -500,11 +561,11 @@ public class NotificationPanelView extends PanelView implements } if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX) && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { + mQsTracking = true; onQsExpansionStarted(); mInitialHeightOnTouch = mQsExpansionHeight; mInitialTouchY = y; mInitialTouchX = x; - mQsTracking = true; mIntercepting = false; mNotificationStackScroller.removeLongPressCallback(); return true; @@ -515,19 +576,22 @@ public class NotificationPanelView extends PanelView implements case MotionEvent.ACTION_UP: trackMovement(event); if (mQsTracking) { - flingQsWithCurrentVelocity(); + flingQsWithCurrentVelocity( + event.getActionMasked() == MotionEvent.ACTION_CANCEL); mQsTracking = false; } mIntercepting = false; break; } + return super.onInterceptTouchEvent(event); + } - // Allow closing the whole panel when in SHADE state. - if (mStatusBarState == StatusBarState.SHADE) { - return super.onInterceptTouchEvent(event); - } else { - return !mQsExpanded && super.onInterceptTouchEvent(event); - } + @Override + protected boolean isInContentBounds(float x, float y) { + float yTransformed = y - mNotificationStackScroller.getY(); + float stackScrollerX = mNotificationStackScroller.getX(); + return mNotificationStackScroller.isInContentBounds(yTransformed) && stackScrollerX < x + && x < stackScrollerX + mNotificationStackScroller.getWidth(); } private void resetDownStates(MotionEvent event) { @@ -548,9 +612,9 @@ public class NotificationPanelView extends PanelView implements super.requestDisallowInterceptTouchEvent(disallowIntercept); } - private void flingQsWithCurrentVelocity() { + private void flingQsWithCurrentVelocity(boolean isCancelMotionEvent) { float vel = getCurrentVelocity(); - flingSettings(vel, flingExpandsQs(vel)); + flingSettings(vel, flingExpandsQs(vel) && !isCancelMotionEvent); } private boolean flingExpandsQs(float vel) { @@ -602,7 +666,7 @@ public class NotificationPanelView extends PanelView implements if (mExpandedHeight != 0) { handleQsDown(event); } - if (!mTwoFingerQsExpand && mQsTracking) { + if (!mQsExpandImmediate && mQsTracking) { onQsTouch(event); if (!mConflictingQsExpansionGesture) { return true; @@ -619,7 +683,7 @@ public class NotificationPanelView extends PanelView implements if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN && event.getPointerCount() == 2 && event.getY(event.getActionIndex()) < mStatusBarMinHeight) { - mTwoFingerQsExpand = true; + mQsExpandImmediate = true; requestPanelHeightUpdate(); // Normally, we start listening when the panel is expanded, but here we need to start @@ -631,10 +695,9 @@ public class NotificationPanelView extends PanelView implements } private boolean isInQsArea(float x, float y) { - return mStatusBarState != StatusBarState.SHADE || - (x >= mScrollView.getLeft() && x <= mScrollView.getRight()) && - (y <= mNotificationStackScroller.getBottomMostNotificationBottom() - || y <= mQsContainer.getY() + mQsContainer.getHeight()); + return (x >= mScrollView.getLeft() && x <= mScrollView.getRight()) && + (y <= mNotificationStackScroller.getBottomMostNotificationBottom() + || y <= mQsContainer.getY() + mQsContainer.getHeight()); } private void handleQsDown(MotionEvent event) { @@ -720,7 +783,8 @@ public class NotificationPanelView extends PanelView implements float fraction = getQsExpansionFraction(); if ((fraction != 0f || y >= mInitialTouchY) && (fraction != 1f || y <= mInitialTouchY)) { - flingQsWithCurrentVelocity(); + flingQsWithCurrentVelocity( + event.getActionMasked() == MotionEvent.ACTION_CANCEL); } else { mScrollYOverride = -1; } @@ -741,11 +805,11 @@ public class NotificationPanelView extends PanelView implements public void onOverscrolled(float lastTouchX, float lastTouchY, int amount) { if (mIntercepting && shouldQuickSettingsIntercept(lastTouchX, lastTouchY, -1 /* yDiff: Not relevant here */)) { + mQsTracking = true; onQsExpansionStarted(amount); mInitialHeightOnTouch = mQsExpansionHeight; mInitialTouchY = mLastTouchY; mInitialTouchX = mLastTouchX; - mQsTracking = true; } } @@ -792,6 +856,7 @@ public class NotificationPanelView extends PanelView implements } mScrollView.scrollTo(0, 0); setQsExpansion(height); + requestPanelHeightUpdate(); } private void setQsExpanded(boolean expanded) { @@ -802,6 +867,7 @@ public class NotificationPanelView extends PanelView implements requestPanelHeightUpdate(); mNotificationStackScroller.setInterceptDelegateEnabled(expanded); mStatusBar.setQsExpanded(expanded); + mQsPanel.setExpanded(expanded); } } @@ -908,6 +974,8 @@ public class NotificationPanelView extends PanelView implements @Override public void run() { mKeyguardStatusBar.setVisibility(View.INVISIBLE); + mKeyguardStatusBar.setAlpha(1f); + mKeyguardStatusBarAnimateAlpha = 1f; } }; @@ -917,10 +985,31 @@ public class NotificationPanelView extends PanelView implements .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) .setInterpolator(PhoneStatusBar.ALPHA_OUT) + .setUpdateListener(mStatusBarAnimateAlphaListener) .withEndAction(mAnimateKeyguardStatusBarInvisibleEndRunnable) .start(); } + private final ValueAnimator.AnimatorUpdateListener mStatusBarAnimateAlphaListener = + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mKeyguardStatusBarAnimateAlpha = mKeyguardStatusBar.getAlpha(); + } + }; + + private void animateKeyguardStatusBarIn() { + mKeyguardStatusBar.setVisibility(View.VISIBLE); + mKeyguardStatusBar.setAlpha(0f); + mKeyguardStatusBar.animate() + .alpha(1f) + .setStartDelay(0) + .setDuration(DOZE_ANIMATION_DURATION) + .setInterpolator(mDozeAnimationInterpolator) + .setUpdateListener(mStatusBarAnimateAlphaListener) + .start(); + } + private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() { @Override public void run() { @@ -1011,7 +1100,7 @@ public class NotificationPanelView extends PanelView implements ? View.VISIBLE : View.INVISIBLE); if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) { - mKeyguardUserSwitcher.hide(true /* animate */); + mKeyguardUserSwitcher.hideIfNotSimple(true /* animate */); } } @@ -1047,6 +1136,9 @@ public class NotificationPanelView extends PanelView implements R.string.accessibility_desc_quick_settings)); mLastAnnouncementWasQuickSettings = true; } + if (DEBUG) { + invalidate(); + } } private String getKeyguardOrLockScreenString() { @@ -1073,7 +1165,7 @@ public class NotificationPanelView extends PanelView implements private void setQsTranslation(float height) { if (!mHeaderAnimatingIn) { - mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation()); + mQsContainer.setY(height - mQsContainer.getDesiredHeight() + getHeaderTranslation()); } if (mKeyguardShowing) { mHeader.setY(interpolate(getQsExpansionFraction(), -mHeader.getHeight(), 0)); @@ -1081,9 +1173,28 @@ public class NotificationPanelView extends PanelView implements } private float calculateQsTopPadding() { - // We can only do the smoother transition on Keyguard when we also are not collapsing from a - // scrolled quick settings. - if (mKeyguardShowing && mScrollYOverride == -1) { + if (mKeyguardShowing + && (mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted)) { + + // Either QS pushes the notifications down when fully expanded, or QS is fully above the + // notifications (mostly on tablets). maxNotifications denotes the normal top padding + // on Keyguard, maxQs denotes the top padding from the quick settings panel. We need to + // take the maximum and linearly interpolate with the panel expansion for a nice motion. + int maxNotifications = mClockPositionResult.stackScrollerPadding + - mClockPositionResult.stackScrollerPaddingAdjustment + - mNotificationTopPadding; + int maxQs = getTempQsMaxExpansion(); + int max = mStatusBarState == StatusBarState.KEYGUARD + ? Math.max(maxNotifications, maxQs) + : maxQs; + return (int) interpolate(getExpandedFraction(), + mQsMinExpansionHeight, max); + } else if (mQsSizeChangeAnimator != null) { + return (int) mQsSizeChangeAnimator.getAnimatedValue(); + } else if (mKeyguardShowing && mScrollYOverride == -1) { + + // We can only do the smoother transition on Keyguard when we also are not collapsing + // from a scrolled quick settings. return interpolate(getQsExpansionFraction(), mNotificationStackScroller.getIntrinsicPadding() - mNotificationTopPadding, mQsMaxExpansionHeight); @@ -1095,7 +1206,9 @@ public class NotificationPanelView extends PanelView implements private void requestScrollerTopPaddingUpdate(boolean animate) { mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(), mScrollView.getScrollY(), - mAnimateNextTopPaddingChange || animate); + mAnimateNextTopPaddingChange || animate, + mKeyguardShowing + && (mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted)); mAnimateNextTopPaddingChange = false; } @@ -1190,11 +1303,9 @@ public class NotificationPanelView extends PanelView implements @Override protected boolean isScrolledToBottom() { - if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { - return true; - } if (!isInSettings()) { - return mNotificationStackScroller.isScrolledToBottom(); + return mStatusBar.getBarState() == StatusBarState.KEYGUARD + || mNotificationStackScroller.isScrolledToBottom(); } else { return mScrollView.isScrolledToBottom(); } @@ -1210,8 +1321,8 @@ public class NotificationPanelView extends PanelView implements min = Math.max(min, minHeight); } int maxHeight; - if (mTwoFingerQsExpand || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) { - maxHeight = Math.max(calculatePanelHeightQsExpanded(), calculatePanelHeightShade()); + if (mQsExpandImmediate || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) { + maxHeight = calculatePanelHeightQsExpanded(); } else { maxHeight = calculatePanelHeightShade(); } @@ -1225,18 +1336,26 @@ public class NotificationPanelView extends PanelView implements @Override protected void onHeightUpdated(float expandedHeight) { - if (!mQsExpanded) { + if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) { positionClockAndNotifications(); } - if (mTwoFingerQsExpand || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null + if (mQsExpandImmediate || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null && !mQsExpansionFromOverscroll) { - float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding() - + mNotificationStackScroller.getMinStackHeight() - + mNotificationStackScroller.getNotificationTopPadding(); - float panelHeightQsExpanded = calculatePanelHeightQsExpanded(); - float t = (expandedHeight - panelHeightQsCollapsed) - / (panelHeightQsExpanded - panelHeightQsCollapsed); + float t; + if (mKeyguardShowing) { + // On Keyguard, interpolate the QS expansion linearly to the panel expansion + t = expandedHeight / getMaxPanelHeight(); + } else { + + // In Shade, interpolate linearly such that QS is closed whenever panel height is + // minimum QS expansion + minStackHeight + float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding() + + mNotificationStackScroller.getMinStackHeight(); + float panelHeightQsExpanded = calculatePanelHeightQsExpanded(); + t = (expandedHeight - panelHeightQsCollapsed) + / (panelHeightQsExpanded - panelHeightQsCollapsed); + } setQsExpansion(mQsMinExpansionHeight + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight)); } @@ -1244,6 +1363,9 @@ public class NotificationPanelView extends PanelView implements updateHeader(); updateUnlockIcon(); updateNotificationTranslucency(); + if (DEBUG) { + invalidate(); + } } /** @@ -1270,10 +1392,30 @@ public class NotificationPanelView extends PanelView implements float notificationHeight = mNotificationStackScroller.getHeight() - mNotificationStackScroller.getEmptyBottomMargin() - mNotificationStackScroller.getTopPadding(); - float totalHeight = mQsMaxExpansionHeight + notificationHeight - + mNotificationStackScroller.getNotificationTopPadding(); + + // When only empty shade view is visible in QS collapsed state, simulate that we would have + // it in expanded QS state as well so we don't run into troubles when fading the view in/out + // and expanding/collapsing the whole panel from/to quick settings. + if (mNotificationStackScroller.getNotGoneChildCount() == 0 + && mShadeEmpty) { + notificationHeight = mNotificationStackScroller.getEmptyShadeViewHeight() + + mNotificationStackScroller.getBottomStackPeekSize() + + mNotificationStackScroller.getCollapseSecondCardPadding(); + } + int maxQsHeight = mQsMaxExpansionHeight; + + // If an animation is changing the size of the QS panel, take the animated value. + if (mQsSizeChangeAnimator != null) { + maxQsHeight = (int) mQsSizeChangeAnimator.getAnimatedValue(); + } + float totalHeight = Math.max( + maxQsHeight + mNotificationStackScroller.getNotificationTopPadding(), + mStatusBarState == StatusBarState.KEYGUARD + ? mClockPositionResult.stackScrollerPadding - mTopPaddingAdjustment + : 0) + + notificationHeight; if (totalHeight > mNotificationStackScroller.getHeight()) { - float fullyCollapsedHeight = mQsMaxExpansionHeight + float fullyCollapsedHeight = maxQsHeight + mNotificationStackScroller.getMinStackHeight() + mNotificationStackScroller.getNotificationTopPadding() - getScrollViewScrollY(); @@ -1283,7 +1425,7 @@ public class NotificationPanelView extends PanelView implements } private int getScrollViewScrollY() { - if (mScrollYOverride != -1) { + if (mScrollYOverride != -1 && !mQsTracking) { return mScrollYOverride; } else { return mScrollView.getScrollY(); @@ -1386,7 +1528,8 @@ public class NotificationPanelView extends PanelView implements alphaNotifications = MathUtils.constrain(alphaNotifications, 0, 1); alphaNotifications = (float) Math.pow(alphaNotifications, 0.75); float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2); - mKeyguardStatusBar.setAlpha(Math.min(alphaNotifications, alphaQsExpansion)); + mKeyguardStatusBar.setAlpha(Math.min(alphaNotifications, alphaQsExpansion) + * mKeyguardStatusBarAnimateAlpha); mKeyguardBottomArea.setAlpha(Math.min(1 - getQsExpansionFraction(), alphaNotifications)); setQsTranslation(mQsExpansionHeight); } @@ -1403,7 +1546,7 @@ public class NotificationPanelView extends PanelView implements super.onExpandingStarted(); mNotificationStackScroller.onExpansionStarted(); mIsExpanding = true; - mQsExpandedWhenExpandingStarted = mQsExpanded; + mQsExpandedWhenExpandingStarted = mQsFullyExpanded; if (mQsExpanded) { onQsExpansionStarted(); } @@ -1420,7 +1563,7 @@ public class NotificationPanelView extends PanelView implements } else { setListening(true); } - mTwoFingerQsExpand = false; + mQsExpandImmediate = false; mTwoFingerQsExpandPossible = false; } @@ -1438,7 +1581,7 @@ public class NotificationPanelView extends PanelView implements @Override protected void setOverExpansion(float overExpansion, boolean isPixels) { - if (mConflictingQsExpansionGesture || mTwoFingerQsExpand) { + if (mConflictingQsExpansionGesture || mQsExpandImmediate) { return; } if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { @@ -1457,11 +1600,12 @@ public class NotificationPanelView extends PanelView implements @Override protected void onTrackingStarted() { super.onTrackingStarted(); + if (mQsFullyExpanded) { + mQsExpandImmediate = true; + } if (mStatusBar.getBarState() == StatusBarState.KEYGUARD || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { mAfforanceHelper.animateHideLeftRightIcon(); - } else if (mQsExpanded) { - mTwoFingerQsExpand = true; } } @@ -1516,6 +1660,14 @@ public class NotificationPanelView extends PanelView implements } @Override + public void onRtlPropertiesChanged(int layoutDirection) { + if (layoutDirection != mOldLayoutDirection) { + mAfforanceHelper.onRtlPropertiesChanged(); + mOldLayoutDirection = layoutDirection; + } + } + + @Override public void onClick(View v) { if (v == mHeader) { onQsExpansionStarted(); @@ -1528,15 +1680,23 @@ public class NotificationPanelView extends PanelView implements } @Override - public void onAnimationToSideStarted(boolean rightPage) { + public void onAnimationToSideStarted(boolean rightPage, float translation, float vel) { boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage; mIsLaunchTransitionRunning = true; mLaunchAnimationEndRunnable = null; + float displayDensity = mStatusBar.getDisplayDensity(); + int lengthDp = Math.abs((int) (translation / displayDensity)); + int velocityDp = Math.abs((int) (vel / displayDensity)); if (start) { + EventLogTags.writeSysuiLockscreenGesture( + EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_DIALER, lengthDp, velocityDp); mKeyguardBottomArea.launchPhone(); } else { + EventLogTags.writeSysuiLockscreenGesture( + EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_CAMERA, lengthDp, velocityDp); mSecureCameraLaunchManager.startSecureCameraLaunch(); } + mStatusBar.startLaunchTransitionTimeout(); mBlockTouches = true; } @@ -1665,7 +1825,7 @@ public class NotificationPanelView extends PanelView implements @Override protected boolean fullyExpandedClearAllVisible() { return mNotificationStackScroller.isDismissViewNotGone() - && mNotificationStackScroller.isScrolledToBottom() && !mTwoFingerQsExpand; + && mNotificationStackScroller.isScrolledToBottom() && !mQsExpandImmediate; } @Override @@ -1735,19 +1895,20 @@ public class NotificationPanelView extends PanelView implements return (1 - t) * start + t * end; } - private void updateKeyguardStatusBarVisibility() { - mKeyguardStatusBar.setVisibility(mKeyguardShowing && !mDozing ? VISIBLE : INVISIBLE); - } - - public void setDozing(boolean dozing) { + public void setDozing(boolean dozing, boolean animate) { if (dozing == mDozing) return; mDozing = dozing; if (mDozing) { - setBackgroundColorAlpha(this, DOZE_BACKGROUND_COLOR, 0xff, false /*animate*/); + mKeyguardStatusBar.setVisibility(View.INVISIBLE); + mKeyguardBottomArea.setVisibility(View.INVISIBLE); } else { - setBackgroundColorAlpha(this, DOZE_BACKGROUND_COLOR, 0, true /*animate*/); + mKeyguardBottomArea.setVisibility(View.VISIBLE); + mKeyguardStatusBar.setVisibility(View.VISIBLE); + if (animate) { + animateKeyguardStatusBarIn(); + mKeyguardBottomArea.startFinishDozeAnimation(); + } } - updateKeyguardStatusBarVisibility(); } @Override @@ -1755,51 +1916,6 @@ public class NotificationPanelView extends PanelView implements return mDozing; } - private static void setBackgroundColorAlpha(final View target, int rgb, int targetAlpha, - boolean animate) { - int currentAlpha = getBackgroundAlpha(target); - if (currentAlpha == targetAlpha) { - return; - } - final int r = Color.red(rgb); - final int g = Color.green(rgb); - final int b = Color.blue(rgb); - Object runningAnim = target.getTag(TAG_KEY_ANIM); - if (runningAnim instanceof ValueAnimator) { - ((ValueAnimator) runningAnim).cancel(); - } - if (!animate) { - target.setBackgroundColor(Color.argb(targetAlpha, r, g, b)); - return; - } - ValueAnimator anim = ValueAnimator.ofInt(currentAlpha, targetAlpha); - anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int value = (int) animation.getAnimatedValue(); - target.setBackgroundColor(Color.argb(value, r, g, b)); - } - }); - anim.setDuration(DOZE_BACKGROUND_ANIM_DURATION); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - target.setTag(TAG_KEY_ANIM, null); - } - }); - anim.start(); - target.setTag(TAG_KEY_ANIM, anim); - } - - private static int getBackgroundAlpha(View view) { - if (view.getBackground() instanceof ColorDrawable) { - ColorDrawable drawable = (ColorDrawable) view.getBackground(); - return Color.alpha(drawable.getColor()); - } else { - return 0; - } - } - public void setShadeEmpty(boolean shadeEmpty) { mShadeEmpty = shadeEmpty; updateEmptyShadeView(); @@ -1833,4 +1949,35 @@ public class NotificationPanelView extends PanelView implements public void onScreenTurnedOn() { mKeyguardStatusView.refreshTime(); } + + @Override + public void onEmptySpaceClicked(float x, float y) { + onEmptySpaceClick(x); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (DEBUG) { + Paint p = new Paint(); + p.setColor(Color.RED); + p.setStrokeWidth(2); + p.setStyle(Paint.Style.STROKE); + canvas.drawLine(0, getMaxPanelHeight(), getWidth(), getMaxPanelHeight(), p); + p.setColor(Color.BLUE); + canvas.drawLine(0, getExpandedHeight(), getWidth(), getExpandedHeight(), p); + p.setColor(Color.GREEN); + canvas.drawLine(0, calculatePanelHeightQsExpanded(), getWidth(), + calculatePanelHeightQsExpanded(), p); + p.setColor(Color.YELLOW); + canvas.drawLine(0, calculatePanelHeightShade(), getWidth(), + calculatePanelHeightShade(), p); + p.setColor(Color.MAGENTA); + canvas.drawLine(0, calculateQsTopPadding(), getWidth(), + calculateQsTopPadding(), p); + p.setColor(Color.CYAN); + canvas.drawLine(0, mNotificationStackScroller.getTopPadding(), getWidth(), + mNotificationStackScroller.getTopPadding(), p); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java index f74d2f4..3efaaff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java @@ -236,4 +236,8 @@ public class PanelBar extends FrameLayout { public void onExpandingFinished() { } + + public void onClosingFinished() { + + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java index a7ff0bd..d86ccee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java @@ -32,6 +32,8 @@ import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; +import com.android.systemui.EventLogConstants; +import com.android.systemui.EventLogTags; import com.android.systemui.R; import com.android.systemui.doze.DozeLog; import com.android.systemui.statusbar.FlingAnimationUtils; @@ -68,9 +70,9 @@ public abstract class PanelView extends FrameLayout { protected int mTouchSlop; protected boolean mHintAnimationRunning; private boolean mOverExpandedBeforeFling; - private float mOriginalIndicationY; private boolean mTouchAboveFalsingThreshold; private int mUnlockFalsingThreshold; + private boolean mTouchStartedInEmptyArea; private ValueAnimator mHeightAnimator; private ObjectAnimator mPeekAnimator; @@ -98,6 +100,7 @@ public abstract class PanelView extends FrameLayout { private boolean mCollapseAfterPeek; private boolean mExpanding; private boolean mGestureWaitForTouchSlop; + private boolean mDozingOnDown; private Runnable mPeekRunnable = new Runnable() { @Override public void run() { @@ -107,7 +110,7 @@ public abstract class PanelView extends FrameLayout { }; protected void onExpandingFinished() { - mClosing = false; + endClosing(); mBar.onExpandingFinished(); } @@ -244,15 +247,14 @@ public abstract class PanelView extends FrameLayout { mUpdateFlingOnLayout = false; mPeekTouching = mPanelClosedOnDown; mTouchAboveFalsingThreshold = false; + mDozingOnDown = isDozing(); if (mVelocityTracker == null) { initVelocityTracker(); } trackMovement(event); if (!waitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) || mPeekPending || mPeekAnimator != null) { - if (mHeightAnimator != null) { - mHeightAnimator.cancel(); // end any outstanding animations - } + cancelHeightAnimator(); cancelPeek(); mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning) || mPeekPending || mPeekAnimator != null; @@ -287,15 +289,13 @@ public abstract class PanelView extends FrameLayout { || mInitialOffsetOnTouch == 0f)) { mTouchSlopExceeded = true; if (waitForTouchSlop && !mTracking) { - if (!mJustPeeked) { + if (!mJustPeeked && mInitialOffsetOnTouch != 0f) { mInitialOffsetOnTouch = mExpandedHeight; mInitialTouchX = x; mInitialTouchY = y; h = 0; } - if (mHeightAnimator != null) { - mHeightAnimator.cancel(); // end any outstanding animations - } + cancelHeightAnimator(); removeCallbacks(mPeekRunnable); mPeekPending = false; onTrackingStarted(); @@ -334,11 +334,21 @@ public abstract class PanelView extends FrameLayout { vectorVel = (float) Math.hypot( mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); } - boolean expand = flingExpands(vel, vectorVel); + boolean expand = flingExpands(vel, vectorVel) + || event.getActionMasked() == MotionEvent.ACTION_CANCEL; onTrackingStopped(expand); DozeLog.traceFling(expand, mTouchAboveFalsingThreshold, mStatusBar.isFalsingThresholdNeeded(), mStatusBar.isScreenOnComingFromTouch()); + // Log collapse gesture if on lock screen. + if (!expand && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { + float displayDensity = mStatusBar.getDisplayDensity(); + int heightDp = (int) Math.abs((y - mInitialTouchY) / displayDensity); + int velocityDp = (int) Math.abs(vel / displayDensity); + EventLogTags.writeSysuiLockscreenGesture( + EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_UP_UNLOCK, + heightDp, velocityDp); + } fling(vel, expand); mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown; if (mUpdateFlingOnLayout) { @@ -372,7 +382,7 @@ public abstract class PanelView extends FrameLayout { } protected void onTrackingStarted() { - mClosing = false; + endClosing(); mTracking = true; mCollapseAfterPeek = false; mBar.onTrackingStarted(PanelView.this); @@ -407,21 +417,21 @@ public abstract class PanelView extends FrameLayout { mStatusBar.userActivity(); if (mHeightAnimator != null && !mHintAnimationRunning || mPeekPending || mPeekAnimator != null) { - if (mHeightAnimator != null) { - mHeightAnimator.cancel(); // end any outstanding animations - } + cancelHeightAnimator(); cancelPeek(); mTouchSlopExceeded = true; return true; } mInitialTouchY = y; mInitialTouchX = x; + mTouchStartedInEmptyArea = !isInContentBounds(x, y); mTouchSlopExceeded = false; mJustPeeked = false; mPanelClosedOnDown = mExpandedHeight == 0.0f; mHasLayoutedSinceDown = false; mUpdateFlingOnLayout = false; mTouchAboveFalsingThreshold = false; + mDozingOnDown = isDozing(); initVelocityTracker(); trackMovement(event); break; @@ -439,11 +449,9 @@ public abstract class PanelView extends FrameLayout { case MotionEvent.ACTION_MOVE: final float h = y - mInitialTouchY; trackMovement(event); - if (scrolledToBottom) { + if (scrolledToBottom || mTouchStartedInEmptyArea) { if (h < -mTouchSlop && h < -Math.abs(x - mInitialTouchX)) { - if (mHeightAnimator != null) { - mHeightAnimator.cancel(); - } + cancelHeightAnimator(); mInitialOffsetOnTouch = mExpandedHeight; mInitialTouchY = y; mInitialTouchX = x; @@ -461,6 +469,25 @@ public abstract class PanelView extends FrameLayout { return false; } + /** + * @return Whether a pair of coordinates are inside the visible view content bounds. + */ + protected abstract boolean isInContentBounds(float x, float y); + + private void cancelHeightAnimator() { + if (mHeightAnimator != null) { + mHeightAnimator.cancel(); + } + endClosing(); + } + + private void endClosing() { + if (mClosing) { + mClosing = false; + onClosingFinished(); + } + } + private void initVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); @@ -627,10 +654,10 @@ public abstract class PanelView extends FrameLayout { } mExpandedHeight = Math.max(0, mExpandedHeight); - onHeightUpdated(mExpandedHeight); mExpandedFraction = Math.min(1f, fhWithoutOverExpansion == 0 ? 0 : mExpandedHeight / fhWithoutOverExpansion); + onHeightUpdated(mExpandedHeight); notifyBarPanelExpansionChanged(); } @@ -700,9 +727,7 @@ public abstract class PanelView extends FrameLayout { mPeekRunnable.run(); } } else if (!isFullyCollapsed() && !mTracking && !mClosing) { - if (mHeightAnimator != null) { - mHeightAnimator.cancel(); - } + cancelHeightAnimator(); mClosing = true; notifyExpandingStarted(); if (delayed) { @@ -785,13 +810,16 @@ public abstract class PanelView extends FrameLayout { private void abortAnimations() { cancelPeek(); - if (mHeightAnimator != null) { - mHeightAnimator.cancel(); - } + cancelHeightAnimator(); removeCallbacks(mPostCollapseRunnable); removeCallbacks(mFlingCollapseRunnable); } + protected void onClosingFinished() { + mBar.onClosingFinished(); + } + + protected void startUnlockHintAnimation() { // We don't need to hint the user if an animation is already running or the user is changing @@ -841,16 +869,15 @@ public abstract class PanelView extends FrameLayout { }); animator.start(); mHeightAnimator = animator; - mOriginalIndicationY = mKeyguardBottomArea.getIndicationView().getY(); mKeyguardBottomArea.getIndicationView().animate() - .y(mOriginalIndicationY - mHintDistance) + .translationY(-mHintDistance) .setDuration(250) .setInterpolator(mFastOutSlowInInterpolator) .withEndAction(new Runnable() { @Override public void run() { mKeyguardBottomArea.getIndicationView().animate() - .y(mOriginalIndicationY) + .translationY(0) .setDuration(450) .setInterpolator(mBounceInterpolator) .start(); @@ -898,7 +925,7 @@ public abstract class PanelView extends FrameLayout { * * @return whether the panel will be expanded after the action performed by this method */ - private boolean onEmptySpaceClick(float x) { + protected boolean onEmptySpaceClick(float x) { if (mHintAnimationRunning) { return true; } @@ -924,7 +951,10 @@ public abstract class PanelView extends FrameLayout { private boolean onMiddleClicked() { switch (mStatusBar.getBarState()) { case StatusBarState.KEYGUARD: - if (!isDozing()) { + if (!mDozingOnDown) { + EventLogTags.writeSysuiLockscreenGesture( + EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_TAP_UNLOCK_HINT, + 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); startUnlockHintAnimation(); } return true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 9e3f0f6..f227107 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -23,6 +23,7 @@ import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.app.StatusBarManager.windowStateToString; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT; +import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT; @@ -40,6 +41,7 @@ import android.app.Notification; import android.app.PendingIntent; import android.app.StatusBarManager; import android.content.BroadcastReceiver; +import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -51,10 +53,10 @@ import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.PixelFormat; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Xfermode; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.inputmethodservice.InputMethodService; @@ -67,16 +69,20 @@ import android.media.session.PlaybackState; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.Message; import android.os.PowerManager; +import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; +import android.text.TextUtils; import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.EventLog; @@ -95,8 +101,8 @@ import android.view.ViewPropertyAnimator; import android.view.ViewStub; import android.view.ViewTreeObserver; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateInterpolator; import android.view.animation.Animation; @@ -115,6 +121,7 @@ import com.android.keyguard.KeyguardHostView.OnDismissAction; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.BatteryMeterView; import com.android.systemui.DemoMode; +import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; @@ -122,6 +129,7 @@ import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.qs.QSPanel; +import com.android.systemui.recent.ScreenPinningRequest; import com.android.systemui.statusbar.ActivatableNotificationView; import com.android.systemui.statusbar.BackDropView; import com.android.systemui.statusbar.BaseStatusBar; @@ -140,6 +148,7 @@ import com.android.systemui.statusbar.SignalClusterView; import com.android.systemui.statusbar.SpeedBumpView; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener; import com.android.systemui.statusbar.policy.AccessibilityController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; @@ -173,9 +182,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; public class PhoneStatusBar extends BaseStatusBar implements DemoMode, - DragDownHelper.DragDownCallback, ActivityStarter { + DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener { static final String TAG = "PhoneStatusBar"; public static final boolean DEBUG = BaseStatusBar.DEBUG; public static final boolean SPEW = false; @@ -197,8 +207,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private static final int MSG_OPEN_NOTIFICATION_PANEL = 1000; private static final int MSG_CLOSE_PANELS = 1001; private static final int MSG_OPEN_SETTINGS_PANEL = 1002; + private static final int MSG_LAUNCH_TRANSITION_TIMEOUT = 1003; // 1020-1040 reserved for BaseStatusBar + // Time after we abort the launch transition. + private static final long LAUNCH_TRANSITION_TIMEOUT_MS = 5000; + private static final boolean CLOSE_PANEL_WHEN_EMPTIED = true; private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10; // see NotificationManagerService @@ -262,6 +276,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private UnlockMethodCache mUnlockMethodCache; private DozeServiceHost mDozeServiceHost; private boolean mScreenOnComingFromTouch; + private PointF mScreenOnTouchLocation; int mPixelFormat; Object mQueueLock = new Object(); @@ -354,7 +369,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, ? new GestureRecorder("/sdcard/statusbar_gestures.dat") : null; + private ScreenPinningRequest mScreenPinningRequest; + private int mNavigationIconHints = 0; + private HandlerThread mHandlerThread; // ensure quick settings is disabled until the current user makes it through the setup wizard private boolean mUserSetup = false; @@ -406,16 +424,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private boolean mAutohideSuspended; private int mStatusBarMode; private int mNavigationBarMode; - private Boolean mScreenOn; - - // The second field is a bit different from the first one because it only listens to screen on/ - // screen of events from Keyguard. We need this so we don't have a race condition with the - // broadcast. In the future, we should remove the first field altogether and rename the second - // field. - private boolean mScreenOnFromKeyguard; private ViewMediatorCallback mKeyguardViewMediatorCallback; private ScrimController mScrimController; + private DozeScrimController mDozeScrimController; private final Runnable mAutohide = new Runnable() { @Override @@ -426,7 +438,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } }}; - private boolean mVisible; private boolean mWaitingForKeyguardExit; private boolean mDozing; private boolean mScrimSrcModeEnabled; @@ -484,6 +495,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private boolean mLaunchTransitionFadingAway; private ExpandableNotificationRow mDraggedDownRow; + // Fingerprint (as computed by getLoggingFingerprint() of the last logged state. + private int mLastLoggedStateFingerprint; + private static final int VISIBLE_LOCATIONS = ViewState.LOCATION_FIRST_CARD | ViewState.LOCATION_TOP_STACK_PEEKING | ViewState.LOCATION_MAIN_AREA @@ -581,7 +595,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, addNavigationBar(); // Lastly, call to the icon policy to install/update all the icons. - mIconPolicy = new PhoneStatusBarPolicy(mContext, mCastController); + mIconPolicy = new PhoneStatusBarPolicy(mContext, mCastController, mHotspotController); mSettingsObserver.onChange(false); // set up mHeadsUpObserver.onChange(true); // set up @@ -594,6 +608,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mHeadsUpObserver); } mUnlockMethodCache = UnlockMethodCache.getInstance(mContext); + mUnlockMethodCache.addListener(this); startKeyguard(); mDozeServiceHost = new DozeServiceHost(); @@ -603,6 +618,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, setControllerUsers(); notifyUserAboutHiddenNotifications(); + + mScreenPinningRequest = new ScreenPinningRequest(mContext); } // ================================================================================ @@ -742,6 +759,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mScrimController = new ScrimController(scrimBehind, scrimInFront, mScrimSrcModeEnabled); mScrimController.setBackDropView(mBackdrop); mStatusBarView.setScrimController(mScrimController); + mDozeScrimController = new DozeScrimController(mScrimController, context); mHeader = (StatusBarHeaderView) mStatusBarWindow.findViewById(R.id.header); mHeader.setActivityStarter(this); @@ -773,6 +791,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // set the inital view visibility setAreThereNotifications(); + // Background thread for any controllers that need it. + mHandlerThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); + mHandlerThread.start(); + // Other icons mLocationController = new LocationControllerImpl(mContext); // will post a notification mBatteryController = new BatteryController(mContext); @@ -791,14 +813,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, }); mNetworkController = new NetworkControllerImpl(mContext); mHotspotController = new HotspotControllerImpl(mContext); - mBluetoothController = new BluetoothControllerImpl(mContext); + mBluetoothController = new BluetoothControllerImpl(mContext, mHandlerThread.getLooper()); mSecurityController = new SecurityControllerImpl(mContext); if (mContext.getResources().getBoolean(R.bool.config_showRotationLock)) { mRotationLockController = new RotationLockControllerImpl(mContext); } mUserInfoController = new UserInfoController(mContext); mVolumeComponent = getComponent(VolumeComponent.class); - mZenModeController = mVolumeComponent.getZenController(); + if (mVolumeComponent != null) { + mZenModeController = mVolumeComponent.getZenController(); + } mCastController = new CastControllerImpl(mContext); final SignalClusterView signalCluster = (SignalClusterView) mStatusBarView.findViewById(R.id.signal_cluster); @@ -817,7 +841,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, signalClusterQs.setNetworkController(mNetworkController); final boolean isAPhone = mNetworkController.hasVoiceCallingFeature(); if (isAPhone) { - mNetworkController.addEmergencyLabelView(mHeader); + mNetworkController.addEmergencyListener(new NetworkControllerImpl.EmergencyListener() { + @Override + public void setEmergencyCallsOnly(boolean emergencyOnly) { + mHeader.setShowEmergencyCallsOnly(emergencyOnly); + } + }); } mCarrierLabel = (TextView)mStatusBarWindow.findViewById(R.id.carrier_label); @@ -826,21 +855,19 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mShowCarrierInPanel) { mCarrierLabel.setVisibility(mCarrierLabelVisible ? View.VISIBLE : View.INVISIBLE); - // for mobile devices, we always show mobile connection info here (SPN/PLMN) - // for other devices, we show whatever network is connected - if (mNetworkController.hasMobileDataFeature()) { - mNetworkController.addMobileLabelView(mCarrierLabel); - } else { - mNetworkController.addCombinedLabelView(mCarrierLabel); - } - - // set up the dynamic hide/show of the label - // TODO: uncomment, handle this for the Stack scroller aswell -// ((NotificationRowLayout) mStackScroller) -// .setOnSizeChangedListener(new OnSizeChangedListener() { -// @Override -// public void onSizeChanged(View view, int w, int h, int oldw, int oldh) { -// updateCarrierLabelVisibility(false); + mNetworkController.addCarrierLabel(new NetworkControllerImpl.CarrierLabelListener() { + @Override + public void setCarrierLabel(String label) { + mCarrierLabel.setText(label); + if (mNetworkController.hasMobileDataFeature()) { + if (TextUtils.isEmpty(label)) { + mCarrierLabel.setVisibility(View.GONE); + } else { + mCarrierLabel.setVisibility(View.VISIBLE); + } + } + } + }); } mFlashlightController = new FlashlightController(mContext); @@ -850,8 +877,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mKeyguardBottomArea.setAccessibilityController(mAccessibilityController); mNextAlarmController = new NextAlarmController(mContext); mKeyguardMonitor = new KeyguardMonitor(); - mUserSwitcherController = new UserSwitcherController(mContext, mKeyguardMonitor); - + if (UserSwitcherController.isUserSwitcherAvailable(UserManager.get(mContext))) { + mUserSwitcherController = new UserSwitcherController(mContext, mKeyguardMonitor); + } mKeyguardUserSwitcher = new KeyguardUserSwitcher(mContext, (ViewStub) mStatusBarWindow.findViewById(R.id.keyguard_user_switcher), mKeyguardStatusBar, mNotificationPanel, mUserSwitcherController); @@ -903,7 +931,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, filter.addAction("fake_artwork"); } filter.addAction(ACTION_DEMO); - context.registerReceiver(mBroadcastReceiver, filter); + context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null); // listen for USER_SETUP_COMPLETE setting (per-user) resetUserSetupObserver(); @@ -965,12 +993,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // accelerating the swipes int rowDelayDecrement = 10; int currentDelay = 140; - int totalDelay = 0; + int totalDelay = 180; int numItems = hideAnimatedList.size(); - for (int i = 0; i < numItems; i++) { + for (int i = numItems - 1; i >= 0; i--) { View view = hideAnimatedList.get(i); Runnable endRunnable = null; - if (i == numItems - 1) { + if (i == 0) { endRunnable = animationFinishAction; } mStackScroller.dismissViewAnimated(view, endRunnable, totalDelay, 260); @@ -1308,6 +1336,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // not immersive & a full-screen alert should be shown if (DEBUG) Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); try { + EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, + notification.getKey()); notification.getNotification().fullScreenIntent.send(); } catch (PendingIntent.CanceledException e) { } @@ -1349,16 +1379,19 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public void scheduleHeadsUpOpen() { + mHandler.removeMessages(MSG_SHOW_HEADS_UP); mHandler.sendEmptyMessage(MSG_SHOW_HEADS_UP); } @Override public void scheduleHeadsUpClose() { + mHandler.removeMessages(MSG_HIDE_HEADS_UP); mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP); } @Override public void scheduleHeadsUpEscalation() { + mHandler.removeMessages(MSG_ESCALATE_HEADS_UP); mHandler.sendEmptyMessage(MSG_ESCALATE_HEADS_UP); } @@ -1502,7 +1535,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // If the user switcher is simple then disable QS during setup because // the user intends to use the lock screen user switcher, QS in not needed. mNotificationPanel.setQsExpansionEnabled(isDeviceProvisioned() - && (!mUserSwitcherController.isSimpleUserSwitcher() || mUserSetup)); + && (mUserSetup || mUserSwitcherController == null + || !mUserSwitcherController.isSimpleUserSwitcher())); mShadeUpdates.check(); } @@ -2131,8 +2165,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, public boolean isFalsingThresholdNeeded() { boolean onKeyguard = getBarState() == StatusBarState.KEYGUARD; - boolean isMethodInsecure = mUnlockMethodCache.isMethodInsecure(); - return onKeyguard && (isMethodInsecure || mDozing || mScreenOnComingFromTouch); + boolean isCurrentlyInsecure = mUnlockMethodCache.isCurrentlyInsecure(); + return onKeyguard && (isCurrentlyInsecure || mDozing || mScreenOnComingFromTouch); } public boolean isDozing() { @@ -2149,6 +2183,18 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } /** + * To be called when there's a state change in StatusBarKeyguardViewManager. + */ + public void onKeyguardViewManagerStatesUpdated() { + logStateToEventlog(); + } + + @Override // UnlockMethodCache.OnUnlockMethodChangedListener + public void onUnlockMethodStateChanged() { + logStateToEventlog(); + } + + /** * All changes to the status bar and notifications funnel through here and are batched. */ private class H extends BaseStatusBar.H { @@ -2179,6 +2225,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, escalateHeadsUp(); setHeadsUpVisibility(false); break; + case MSG_LAUNCH_TRANSITION_TIMEOUT: + onLaunchTransitionTimeout(); + break; } } } @@ -2193,6 +2242,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (DEBUG) Log.d(TAG, "converting a heads up to fullScreen"); try { + EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_ESCALATION, + sbn.getKey()); notification.fullScreenIntent.send(); } catch (PendingIntent.CanceledException e) { } @@ -2365,8 +2416,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // Settings are not available in setup if (!mUserSetup) return; - mNotificationPanel.expand(); - mNotificationPanel.openQs(); + mNotificationPanel.expandWithQs(); if (false) postStartTracing(); } @@ -2413,6 +2463,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false); showBouncer(); disable(mDisabledUnmodified, true /* animate */); + + // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in + // the bouncer appear animation. + if (!mStatusBarKeyguardViewManager.isShowing()) { + WindowManagerGlobal.getInstance().trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); + } } public boolean interceptTouchEvent(MotionEvent event) { @@ -2599,8 +2655,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } private int barMode(int vis, int transientFlag, int translucentFlag) { + int lightsOutTransparent = View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_TRANSPARENT; return (vis & transientFlag) != 0 ? MODE_SEMI_TRANSPARENT : (vis & translucentFlag) != 0 ? MODE_TRANSLUCENT + : (vis & lightsOutTransparent) == lightsOutTransparent ? MODE_LIGHTS_OUT_TRANSPARENT : (vis & View.SYSTEM_UI_TRANSPARENT) != 0 ? MODE_TRANSPARENT : (vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0 ? MODE_LIGHTS_OUT : MODE_OPAQUE; @@ -2641,6 +2699,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public void setInteracting(int barWindow, boolean interacting) { + final boolean changing = ((mInteractingWindows & barWindow) != 0) != interacting; mInteractingWindows = interacting ? (mInteractingWindows | barWindow) : (mInteractingWindows & ~barWindow); @@ -2649,6 +2708,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } else { resumeSuspendedAutohide(); } + // manually dismiss the volume panel when interacting with the nav bar + if (changing && interacting && barWindow == StatusBarManager.WINDOW_NAVIGATION_BAR) { + if (mVolumeComponent != null) { + mVolumeComponent.dismissNow(); + } + } checkBarModes(); } @@ -2955,6 +3020,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mSecurityController != null) { mSecurityController.dump(fd, pw, args); } + pw.println("SharedPreferences:"); + for (Map.Entry<String, ?> entry : mContext.getSharedPreferences(mContext.getPackageName(), + Context.MODE_PRIVATE).getAll().entrySet()) { + pw.print(" "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue()); + } } private String hunStateToString(Entry entry) { @@ -3008,6 +3078,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } + float getDisplayDensity() { + return mDisplayMetrics.density; + } + public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned, final boolean dismissShade) { if (onlyProvisioned && !isDeviceProvisioned()) return; @@ -3036,7 +3110,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } }); if (dismissShade) { - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */); + animateCollapsePanels( + CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */); } return true; } @@ -3048,27 +3123,25 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (DEBUG) Log.v(TAG, "onReceive: " + intent); String action = intent.getAction(); if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) { - int flags = CommandQueue.FLAG_EXCLUDE_NONE; - String reason = intent.getStringExtra("reason"); - if (reason != null && reason.equals(SYSTEM_DIALOG_REASON_RECENT_APPS)) { - flags |= CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL; + if (isCurrentProfile(getSendingUserId())) { + int flags = CommandQueue.FLAG_EXCLUDE_NONE; + String reason = intent.getStringExtra("reason"); + if (reason != null && reason.equals(SYSTEM_DIALOG_REASON_RECENT_APPS)) { + flags |= CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL; + } + animateCollapsePanels(flags); } - animateCollapsePanels(flags); } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { mScreenOn = false; notifyNavigationBarScreenOn(false); notifyHeadsUpScreenOn(false); finishBarAnimations(); - stopNotificationLogging(); resetUserExpandedStates(); } else if (Intent.ACTION_SCREEN_ON.equals(action)) { mScreenOn = true; - // work around problem where mDisplay.getRotation() is not stable while screen is off (bug 7086018) - repositionNavigationBar(); notifyNavigationBarScreenOn(true); - startNotificationLoggingIfScreenOnAndVisible(); } else if (ACTION_DEMO.equals(action)) { Bundle bundle = intent.getExtras(); @@ -3105,18 +3178,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, protected void dismissKeyguardThenExecute(final OnDismissAction action, boolean afterKeyguardGone) { if (mStatusBarKeyguardViewManager.isShowing()) { - if (UnlockMethodCache.getInstance(mContext).isMethodInsecure() - && mNotificationPanel.isLaunchTransitionRunning() && !afterKeyguardGone) { - action.onDismiss(); - mNotificationPanel.setLaunchTransitionEndRunnable(new Runnable() { - @Override - public void run() { - mStatusBarKeyguardViewManager.dismiss(); - } - }); - } else { - mStatusBarKeyguardViewManager.dismissWithAction(action, afterKeyguardGone); - } + mStatusBarKeyguardViewManager.dismissWithAction(action, afterKeyguardGone); } else { action.onDismiss(); } @@ -3138,12 +3200,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateExpandedViewPos(EXPANDED_LEAVE_ALONE); updateShowSearchHoldoff(); updateRowStates(); + mScreenPinningRequest.onConfigurationChanged(); } @Override public void userSwitched(int newUserId) { + super.userSwitched(newUserId); if (MULTIUSER_DEBUG) mNotificationPanelDebugText.setText("USER " + newUserId); animateCollapsePanels(); + updatePublicMode(); updateNotifications(); resetUserSetupObserver(); setControllerUsers(); @@ -3257,14 +3322,14 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // Visibility reporting @Override - protected void visibilityChanged(boolean visible) { - mVisible = visible; - if (visible) { - startNotificationLoggingIfScreenOnAndVisible(); + protected void handleVisibleToUserChanged(boolean visibleToUser) { + if (visibleToUser) { + super.handleVisibleToUserChanged(visibleToUser); + startNotificationLogging(); } else { stopNotificationLogging(); + super.handleVisibleToUserChanged(visibleToUser); } - super.visibilityChanged(visible); } private void stopNotificationLogging() { @@ -3279,17 +3344,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mStackScroller.setChildLocationsChangedListener(null); } - private void startNotificationLoggingIfScreenOnAndVisible() { - if (mVisible && mScreenOn) { - mStackScroller.setChildLocationsChangedListener(mNotificationLocationsChangedListener); - // Some transitions like mScreenOn=false -> mScreenOn=true don't - // cause the scroller to emit child location events. Hence generate - // one ourselves to guarantee that we're reporting visible - // notifications. - // (Note that in cases where the scroller does emit events, this - // additional event doesn't break anything.) - mNotificationLocationsChangedListener.onChildLocationsChanged(mStackScroller); - } + private void startNotificationLogging() { + mStackScroller.setChildLocationsChangedListener(mNotificationLocationsChangedListener); + // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't + // cause the scroller to emit child location events. Hence generate + // one ourselves to guarantee that we're reporting visible + // notifications. + // (Note that in cases where the scroller does emit events, this + // additional event doesn't break anything.) + mNotificationLocationsChangedListener.onChildLocationsChanged(mStackScroller); } private void logNotificationVisibilityChanges( @@ -3306,6 +3369,47 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } + // State logging + + private void logStateToEventlog() { + boolean isShowing = mStatusBarKeyguardViewManager.isShowing(); + boolean isOccluded = mStatusBarKeyguardViewManager.isOccluded(); + boolean isBouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing(); + boolean isSecure = mUnlockMethodCache.isMethodSecure(); + boolean isCurrentlyInsecure = mUnlockMethodCache.isCurrentlyInsecure(); + int stateFingerprint = getLoggingFingerprint(mState, + isShowing, + isOccluded, + isBouncerShowing, + isSecure, + isCurrentlyInsecure); + if (stateFingerprint != mLastLoggedStateFingerprint) { + EventLogTags.writeSysuiStatusBarState(mState, + isShowing ? 1 : 0, + isOccluded ? 1 : 0, + isBouncerShowing ? 1 : 0, + isSecure ? 1 : 0, + isCurrentlyInsecure ? 1 : 0); + mLastLoggedStateFingerprint = stateFingerprint; + } + } + + /** + * Returns a fingerprint of fields logged to eventlog + */ + private static int getLoggingFingerprint(int statusBarState, boolean keyguardShowing, + boolean keyguardOccluded, boolean bouncerShowing, boolean secure, + boolean currentlyInsecure) { + // Reserve 8 bits for statusBarState. We'll never go higher than + // that, right? Riiiight. + return (statusBarState & 0xFF) + | ((keyguardShowing ? 1 : 0) << 8) + | ((keyguardOccluded ? 1 : 0) << 9) + | ((bouncerShowing ? 1 : 0) << 10) + | ((secure ? 1 : 0) << 11) + | ((currentlyInsecure ? 1 : 0) << 12); + } + // // tracing // @@ -3410,6 +3514,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mWindowManager.removeViewImmediate(mNavigationBarView); mNavigationBarView = null; } + if (mHandlerThread != null) { + mHandlerThread.quitSafely(); + mHandlerThread = null; + } mContext.unregisterReceiver(mBroadcastReceiver); } @@ -3434,6 +3542,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, dispatchDemoCommand(COMMAND_ENTER, new Bundle()); } boolean modeChange = command.equals(COMMAND_ENTER) || command.equals(COMMAND_EXIT); + if ((modeChange || command.equals(COMMAND_VOLUME)) && mVolumeComponent != null) { + mVolumeComponent.dispatchDemoCommand(command, args); + } if (modeChange || command.equals(COMMAND_CLOCK)) { dispatchDemoCommandToView(command, args, R.id.clock); } @@ -3497,12 +3608,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mLaunchTransitionFadingAway) { mNotificationPanel.animate().cancel(); mNotificationPanel.setAlpha(1f); - if (mLaunchTransitionEndRunnable != null) { - mLaunchTransitionEndRunnable.run(); - } - mLaunchTransitionEndRunnable = null; + runLaunchTransitionEndRunnable(); mLaunchTransitionFadingAway = false; } + mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT); setBarState(StatusBarState.KEYGUARD); updateKeyguardState(false /* goingToFullShade */, false /* fromShadeLocked */); if (!mScreenOnFromKeyguard) { @@ -3543,6 +3652,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, */ public void fadeKeyguardAfterLaunchTransition(final Runnable beforeFading, Runnable endRunnable) { + mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT); mLaunchTransitionEndRunnable = endRunnable; Runnable hideRunnable = new Runnable() { @Override @@ -3561,10 +3671,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public void run() { mNotificationPanel.setAlpha(1); - if (mLaunchTransitionEndRunnable != null) { - mLaunchTransitionEndRunnable.run(); - } - mLaunchTransitionEndRunnable = null; + runLaunchTransitionEndRunnable(); mLaunchTransitionFadingAway = false; } }); @@ -3578,6 +3685,32 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } /** + * Starts the timeout when we try to start the affordances on Keyguard. We usually rely that + * Keyguard goes away via fadeKeyguardAfterLaunchTransition, however, that might not happen + * because the launched app crashed or something else went wrong. + */ + public void startLaunchTransitionTimeout() { + mHandler.sendEmptyMessageDelayed(MSG_LAUNCH_TRANSITION_TIMEOUT, + LAUNCH_TRANSITION_TIMEOUT_MS); + } + + private void onLaunchTransitionTimeout() { + Log.w(TAG, "Launch transition: Timeout!"); + mNotificationPanel.resetViews(); + } + + private void runLaunchTransitionEndRunnable() { + if (mLaunchTransitionEndRunnable != null) { + Runnable r = mLaunchTransitionEndRunnable; + + // mLaunchTransitionEndRunnable might call showKeyguard, which would execute it again, + // which would lead to infinite recursion. Protect against it. + mLaunchTransitionEndRunnable = null; + r.run(); + } + } + + /** * @return true if we would like to stay in the shade, false if it should go away entirely */ public boolean hideKeyguard() { @@ -3594,6 +3727,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, instantCollapseNotificationPanel(); } updateKeyguardState(staying, false /* fromShadeLocked */); + + // Keyguard state has changed, but QS is not listening anymore. Make sure to update the tile + // visibilities so next time we open the panel we know the correct height already. + if (mQSPanel != null) { + mQSPanel.refreshAllTiles(); + } + mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT); return staying; } @@ -3627,10 +3767,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } private void updatePublicMode() { - setLockscreenPublicMode( - (mStatusBarKeyguardViewManager.isShowing() || - mStatusBarKeyguardViewManager.isOccluded()) - && mStatusBarKeyguardViewManager.isSecure()); + setLockscreenPublicMode(mStatusBarKeyguardViewManager.isShowing() + && mStatusBarKeyguardViewManager.isSecure(mCurrentUserId)); } private void updateKeyguardState(boolean goingToFullShade, boolean fromShadeLocked) { @@ -3664,15 +3802,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mState != StatusBarState.KEYGUARD && !mNotificationPanel.isDozing()) { return; } - mNotificationPanel.setDozing(mDozing); - if (mDozing) { - mKeyguardBottomArea.setVisibility(View.INVISIBLE); - mStackScroller.setDark(true, false /*animate*/); - } else { - mKeyguardBottomArea.setVisibility(View.VISIBLE); - mStackScroller.setDark(false, false /*animate*/); - } + boolean animate = !mDozing && mDozeScrimController.isPulsing(); + mNotificationPanel.setDozing(mDozing, animate); + mStackScroller.setDark(mDozing, animate, mScreenOnTouchLocation); mScrimController.setDozing(mDozing); + mDozeScrimController.setDozing(mDozing, animate); } public void updateStackScrollerState(boolean goingToFullShade) { @@ -3725,7 +3859,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, public boolean onSpacePressed() { if (mScreenOn != null && mScreenOn && (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED)) { - animateCollapsePanels(0 /* flags */, true /* force */); + animateCollapsePanels( + CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL /* flags */, true /* force */); return true; } return false; @@ -3751,6 +3886,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public void onActivated(ActivatableNotificationView view) { + EventLogTags.writeSysuiLockscreenGesture( + EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_TAP_NOTIFICATION_ACTIVATE, + 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); mKeyguardIndicationController.showTransientIndication(R.string.notification_tap_again); ActivatableNotificationView previousView = mStackScroller.getActivatedChild(); if (previousView != null) { @@ -3763,6 +3901,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, * @param state The {@link StatusBarState} to set. */ public void setBarState(int state) { + // If we're visible and switched to SHADE_LOCKED (the user dragged + // down on the lockscreen), clear notification LED, vibration, + // ringing. + // Other transitions are covered in handleVisibleToUserChanged(). + if (state != mState && mVisible && state == StatusBarState.SHADE_LOCKED) { + try { + mBarService.clearNotificationEffects(); + } catch (RemoteException e) { + // Ignore. + } + } mState = state; mStatusBarWindowManager.setStatusBarState(state); } @@ -3779,6 +3928,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, runPostCollapseRunnables(); } + public void onClosingFinished() { + runPostCollapseRunnables(); + } + public void onUnlockHintStarted() { mKeyguardIndicationController.showTransientIndication(R.string.keyguard_unlock); } @@ -3798,7 +3951,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, public void onTrackingStopped(boolean expand) { if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) { - if (!expand && !mUnlockMethodCache.isMethodInsecure()) { + if (!expand && !mUnlockMethodCache.isCurrentlyInsecure()) { showBouncer(); } } @@ -3816,8 +3969,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // ---------------------- DragDownHelper.OnDragDownListener ------------------------------------ @Override - public boolean onDraggedDown(View startingChild) { + public boolean onDraggedDown(View startingChild, int dragLengthY) { if (hasActiveNotifications()) { + EventLogTags.writeSysuiLockscreenGesture( + EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_DOWN_FULL_SHADE, + (int) (dragLengthY / mDisplayMetrics.density), + 0 /* velocityDp - N/A */); // We have notifications, go to locked shade. goToLockedShade(startingChild); @@ -3921,7 +4078,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, public void onScreenTurnedOff() { mScreenOnFromKeyguard = false; mScreenOnComingFromTouch = false; + mScreenOnTouchLocation = null; mStackScroller.setAnimationsEnabled(false); + updateVisibleToUser(); } public void onScreenTurnedOn() { @@ -3929,6 +4088,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mStackScroller.setAnimationsEnabled(true); mNotificationPanel.onScreenTurnedOn(); mNotificationPanel.setTouchDisabled(false); + updateVisibleToUser(); } /** @@ -3955,6 +4115,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // long-pressed 'together' if ((time - mLastLockToAppLongPress) < LOCK_TO_APP_GESTURE_TOLERENCE) { activityManager.stopLockTaskModeOnCurrent(); + // When exiting refresh disabled flags. + mNavigationBarView.setDisabledFlags(mDisabled, true); } else if ((v.getId() == R.id.back) && !mNavigationBarView.getRecentsButton().isPressed()) { // If we aren't pressing recents right now then they presses @@ -3970,6 +4132,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // When in accessibility mode a long press that is recents (not back) // should stop lock task. activityManager.stopLockTaskModeOnCurrent(); + // When exiting refresh disabled flags. + mNavigationBarView.setDisabledFlags(mDisabled, true); } } if (sendBackLongPress) { @@ -4019,17 +4183,31 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, notifyUiVisibilityChanged(mSystemUiVisibility); } + @Override + public void showScreenPinningRequest() { + if (mKeyguardMonitor.isShowing()) { + // Don't allow apps to trigger this from keyguard. + return; + } + // Show screen pinning request, since this comes from an app, show 'no thanks', button. + showScreenPinningRequest(true); + } + + public void showScreenPinningRequest(boolean allowCancel) { + mScreenPinningRequest.showPrompt(allowCancel); + } + public boolean hasActiveNotifications() { return !mNotificationData.getActiveNotifications().isEmpty(); } - public void wakeUpIfDozing(long time, boolean fromTouch) { - if (mDozing && mScrimController.isPulsing()) { + public void wakeUpIfDozing(long time, MotionEvent event) { + if (mDozing && mDozeScrimController.isPulsing()) { PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); pm.wakeUp(time); - if (fromTouch) { - mScreenOnComingFromTouch = true; - } + mScreenOnComingFromTouch = true; + mScreenOnTouchLocation = new PointF(event.getX(), event.getY()); + mNotificationPanel.setTouchDisabled(false); } } @@ -4069,6 +4247,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); private final H mHandler = new H(); + // Keeps the last reported state by fireNotificationLight. + private boolean mNotificationLightOn; + @Override public String toString() { return "PSB.DozeServiceHost[mCallbacks=" + mCallbacks.size() + "]"; @@ -4087,6 +4268,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } public void fireNotificationLight(boolean on) { + mNotificationLightOn = on; for (Callback callback : mCallbacks) { callback.onNotificationLight(on); } @@ -4114,8 +4296,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } @Override - public void pulseWhileDozing(@NonNull PulseCallback callback) { - mHandler.obtainMessage(H.MSG_PULSE_WHILE_DOZING, callback).sendToTarget(); + public void pulseWhileDozing(@NonNull PulseCallback callback, int reason) { + mHandler.obtainMessage(H.MSG_PULSE_WHILE_DOZING, reason, 0, callback).sendToTarget(); } @Override @@ -4128,6 +4310,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mBatteryController != null && mBatteryController.isPowerSave(); } + @Override + public boolean isNotificationLightOn() { + return mNotificationLightOn; + } + private void handleStartDozing(@NonNull Runnable ready) { if (!mDozing) { mDozing = true; @@ -4137,8 +4324,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, ready.run(); } - private void handlePulseWhileDozing(@NonNull PulseCallback callback) { - mScrimController.pulse(callback); + private void handlePulseWhileDozing(@NonNull PulseCallback callback, int reason) { + mDozeScrimController.pulse(callback, reason); } private void handleStopDozing() { @@ -4161,7 +4348,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, handleStartDozing((Runnable) msg.obj); break; case MSG_PULSE_WHILE_DOZING: - handlePulseWhileDozing((PulseCallback) msg.obj); + handlePulseWhileDozing((PulseCallback) msg.obj, msg.arg1); break; case MSG_STOP_DOZING: handleStopDozing(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 47e1ab5..5c254a26 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -35,6 +35,7 @@ import com.android.internal.telephony.TelephonyIntents; import com.android.systemui.R; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.CastController.CastDevice; +import com.android.systemui.statusbar.policy.HotspotController; /** * This class contains all of the policy about which icons are installed in the status @@ -49,6 +50,7 @@ public class PhoneStatusBarPolicy { private static final String SLOT_SYNC_ACTIVE = "sync_active"; private static final String SLOT_CAST = "cast"; + private static final String SLOT_HOTSPOT = "hotspot"; private static final String SLOT_BLUETOOTH = "bluetooth"; private static final String SLOT_TTY = "tty"; private static final String SLOT_ZEN = "zen"; @@ -60,6 +62,7 @@ public class PhoneStatusBarPolicy { private final StatusBarManager mService; private final Handler mHandler = new Handler(); private final CastController mCast; + private final HotspotController mHotspot; // Assume it's all good unless we hear otherwise. We don't always seem // to get broadcasts that it *is* there. @@ -87,7 +90,8 @@ public class PhoneStatusBarPolicy { action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) { updateBluetooth(); } - else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { + else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION) || + action.equals(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)) { updateVolumeZen(); } else if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) { @@ -102,9 +106,10 @@ public class PhoneStatusBarPolicy { } }; - public PhoneStatusBarPolicy(Context context, CastController cast) { + public PhoneStatusBarPolicy(Context context, CastController cast, HotspotController hotspot) { mContext = context; mCast = cast; + mHotspot = hotspot; mService = (StatusBarManager)context.getSystemService(Context.STATUS_BAR_SERVICE); // listen for broadcasts @@ -112,6 +117,7 @@ public class PhoneStatusBarPolicy { filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); filter.addAction(Intent.ACTION_SYNC_STATE_CHANGED); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); + filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); @@ -152,6 +158,11 @@ public class PhoneStatusBarPolicy { mService.setIcon(SLOT_CAST, R.drawable.stat_sys_cast, 0, null); mService.setIconVisibility(SLOT_CAST, false); mCast.addCallback(mCastCallback); + + // hotspot + mService.setIcon(SLOT_HOTSPOT, R.drawable.stat_sys_hotspot, 0, null); + mService.setIconVisibility(SLOT_HOTSPOT, mHotspot.isHotspotEnabled()); + mHotspot.addCallback(mHotspotCallback); } public void setZenMode(int zen) { @@ -221,7 +232,7 @@ public class PhoneStatusBarPolicy { } if (mZen != Global.ZEN_MODE_NO_INTERRUPTIONS && - audioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { + audioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) { volumeVisible = true; volumeIconId = R.drawable.stat_sys_ringer_vibrate; volumeDescription = mContext.getString(R.string.accessibility_ringer_vibrate); @@ -300,6 +311,13 @@ public class PhoneStatusBarPolicy { mService.setIconVisibility(SLOT_CAST, isCasting); } + private final HotspotController.Callback mHotspotCallback = new HotspotController.Callback() { + @Override + public void onHotspotChanged(boolean enabled) { + mService.setIconVisibility(SLOT_HOTSPOT, enabled); + } + }; + private final CastController.Callback mCastCallback = new CastController.Callback() { @Override public void onCastDevicesChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java index 8520f40..fb1addf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java @@ -57,19 +57,19 @@ public final class PhoneStatusBarTransitions extends BarTransitions { } private float getNonBatteryClockAlphaFor(int mode) { - return mode == MODE_LIGHTS_OUT ? ICON_ALPHA_WHEN_LIGHTS_OUT_NON_BATTERY_CLOCK + return isLightsOut(mode) ? ICON_ALPHA_WHEN_LIGHTS_OUT_NON_BATTERY_CLOCK : !isOpaque(mode) ? ICON_ALPHA_WHEN_NOT_OPAQUE : mIconAlphaWhenOpaque; } private float getBatteryClockAlpha(int mode) { - return mode == MODE_LIGHTS_OUT ? ICON_ALPHA_WHEN_LIGHTS_OUT_BATTERY_CLOCK + return isLightsOut(mode) ? ICON_ALPHA_WHEN_LIGHTS_OUT_BATTERY_CLOCK : getNonBatteryClockAlphaFor(mode); } private boolean isOpaque(int mode) { return !(mode == MODE_SEMI_TRANSPARENT || mode == MODE_TRANSLUCENT - || mode == MODE_TRANSPARENT); + || mode == MODE_TRANSPARENT || mode == MODE_LIGHTS_OUT_TRANSPARENT); } @Override @@ -94,7 +94,7 @@ public final class PhoneStatusBarTransitions extends BarTransitions { animateTransitionTo(mBattery, newAlphaBC), animateTransitionTo(mClock, newAlphaBC) ); - if (mode == MODE_LIGHTS_OUT) { + if (isLightsOut(mode)) { anims.setDuration(LIGHTS_OUT_DURATION); } anims.start(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index 6411fb8..7cbf13f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.util.EventLog; -import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; @@ -31,7 +30,7 @@ import com.android.systemui.R; public class PhoneStatusBarView extends PanelBar { private static final String TAG = "PhoneStatusBarView"; private static final boolean DEBUG = PhoneStatusBar.DEBUG; - private static final boolean DEBUG_GESTURES = true; + private static final boolean DEBUG_GESTURES = false; PhoneStatusBar mBar; @@ -152,6 +151,12 @@ public class PhoneStatusBarView extends PanelBar { } @Override + public void onClosingFinished() { + super.onClosingFinished(); + mBar.onClosingFinished(); + } + + @Override public void onTrackingStopped(PanelView panel, boolean expand) { super.onTrackingStopped(panel, expand); mBar.onTrackingStopped(expand); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java index 2dc08d4..45a1386 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.content.res.Resources; import android.database.ContentObserver; import android.net.Uri; +import android.os.Process; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -108,7 +109,8 @@ public class QSTileHost implements QSTile.Host { mKeyguard = keyguard; mSecurity = security; - final HandlerThread ht = new HandlerThread(QSTileHost.class.getSimpleName()); + final HandlerThread ht = new HandlerThread(QSTileHost.class.getSimpleName(), + Process.THREAD_PRIORITY_BACKGROUND); ht.start(); mLooper = ht.getLooper(); @@ -120,6 +122,7 @@ public class QSTileHost implements QSTile.Host { tile.userSwitch(newUserId); } mSecurity.onUserSwitched(newUserId); + mNetwork.onUserSwitched(newUserId); mObserver.register(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 54adbf4..0e8a794 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -19,10 +19,8 @@ package com.android.systemui.statusbar.phone; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; -import android.annotation.NonNull; import android.content.Context; import android.graphics.Color; -import android.util.Log; import android.view.View; import android.view.ViewTreeObserver; import android.view.animation.AnimationUtils; @@ -30,8 +28,6 @@ import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import com.android.systemui.R; -import com.android.systemui.doze.DozeHost; -import com.android.systemui.doze.DozeLog; import com.android.systemui.statusbar.BackDropView; import com.android.systemui.statusbar.ScrimView; @@ -40,9 +36,6 @@ import com.android.systemui.statusbar.ScrimView; * security method gets shown). */ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { - private static final String TAG = "ScrimController"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - public static final long ANIMATION_DURATION = 220; private static final float SCRIM_BEHIND_ALPHA = 0.62f; @@ -54,7 +47,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { private final ScrimView mScrimBehind; private final ScrimView mScrimInFront; private final UnlockMethodCache mUnlockMethodCache; - private final DozeParameters mDozeParameters; private boolean mKeyguardShowing; private float mFraction; @@ -69,12 +61,15 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { private long mAnimationDelay; private Runnable mOnAnimationFinished; private boolean mAnimationStarted; - private boolean mDozing; - private DozeHost.PulseCallback mPulseCallback; private final Interpolator mInterpolator = new DecelerateInterpolator(); private final Interpolator mLinearOutSlowInInterpolator; private BackDropView mBackDropView; private boolean mScrimSrcEnabled; + private boolean mDozing; + private float mDozeInFrontAlpha; + private float mDozeBehindAlpha; + private float mCurrentInFrontAlpha; + private float mCurrentBehindAlpha; public ScrimController(ScrimView scrimBehind, ScrimView scrimInFront, boolean scrimSrcEnabled) { mScrimBehind = scrimBehind; @@ -83,7 +78,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { mUnlockMethodCache = UnlockMethodCache.getInstance(context); mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); - mDozeParameters = new DozeParameters(context); mScrimSrcEnabled = scrimSrcEnabled; } @@ -94,7 +88,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { public void onTrackingStarted() { mExpanding = true; - mDarkenWhileDragging = !mUnlockMethodCache.isMethodInsecure(); + mDarkenWhileDragging = !mUnlockMethodCache.isCurrentlyInsecure(); } public void onExpandingFinished() { @@ -131,60 +125,26 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { } public void setDozing(boolean dozing) { - if (mDozing == dozing) return; mDozing = dozing; - if (!mDozing) { - cancelPulsing(); - mAnimateChange = true; - } else { - mAnimateChange = false; - } scheduleUpdate(); } - /** When dozing, fade screen contents in and out using the front scrim. */ - public void pulse(@NonNull DozeHost.PulseCallback callback) { - if (callback == null) { - throw new IllegalArgumentException("callback must not be null"); - } - - if (!mDozing || mPulseCallback != null) { - // Pulse suppressed. - callback.onPulseFinished(); - return; - } - - // Begin pulse. Note that it's very important that the pulse finished callback - // be invoked when we're done so that the caller can drop the pulse wakelock. - mPulseCallback = callback; - mScrimInFront.post(mPulseIn); + public void setDozeInFrontAlpha(float alpha) { + mDozeInFrontAlpha = alpha; + updateScrimColor(mScrimInFront); } - public boolean isPulsing() { - return mPulseCallback != null; + public void setDozeBehindAlpha(float alpha) { + mDozeBehindAlpha = alpha; + updateScrimColor(mScrimBehind); } - private void cancelPulsing() { - if (DEBUG) Log.d(TAG, "Cancel pulsing"); - - if (mPulseCallback != null) { - mScrimInFront.removeCallbacks(mPulseIn); - mScrimInFront.removeCallbacks(mPulseOut); - pulseFinished(); - } - } - - private void pulseStarted() { - if (mPulseCallback != null) { - mPulseCallback.onPulseStarted(); - } + public float getDozeBehindAlpha() { + return mDozeBehindAlpha; } - private void pulseFinished() { - if (mPulseCallback != null) { - mPulseCallback.onPulseFinished(); - mPulseCallback = null; - } + public float getDozeInFrontAlpha() { + return mDozeInFrontAlpha; } private void scheduleUpdate() { @@ -220,8 +180,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { } else if (mBouncerShowing) { setScrimInFrontColor(SCRIM_IN_FRONT_ALPHA); setScrimBehindColor(0f); - } else if (mDozing) { - setScrimInFrontColor(1); } else { float fraction = Math.max(0, Math.min(mFraction, 1)); setScrimInFrontColor(0f); @@ -265,31 +223,49 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { ((ValueAnimator) runningAnim).cancel(); scrim.setTag(TAG_KEY_ANIM, null); } - int color = Color.argb((int) (alpha * 255), 0, 0, 0); if (mAnimateChange) { - startScrimAnimation(scrim, color); + startScrimAnimation(scrim, alpha); } else { - scrim.setScrimColor(color); + setCurrentScrimAlpha(scrim, alpha); + updateScrimColor(scrim); } } - private void startScrimAnimation(final ScrimView scrim, int targetColor) { - int current = Color.alpha(scrim.getScrimColor()); - int target = Color.alpha(targetColor); - if (current == targetColor) { - return; + private float getDozeAlpha(View scrim) { + return scrim == mScrimBehind ? mDozeBehindAlpha : mDozeInFrontAlpha; + } + + private float getCurrentScrimAlpha(View scrim) { + return scrim == mScrimBehind ? mCurrentBehindAlpha : mCurrentInFrontAlpha; + } + + private void setCurrentScrimAlpha(View scrim, float alpha) { + if (scrim == mScrimBehind) { + mCurrentBehindAlpha = alpha; + } else { + mCurrentInFrontAlpha = alpha; } - ValueAnimator anim = ValueAnimator.ofInt(current, target); + } + + private void updateScrimColor(ScrimView scrim) { + float alpha1 = getCurrentScrimAlpha(scrim); + float alpha2 = getDozeAlpha(scrim); + float alpha = 1 - (1 - alpha1) * (1 - alpha2); + scrim.setScrimColor(Color.argb((int) (alpha * 255), 0, 0, 0)); + } + + private void startScrimAnimation(final ScrimView scrim, float target) { + float current = getCurrentScrimAlpha(scrim); + ValueAnimator anim = ValueAnimator.ofFloat(current, target); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { - int value = (int) animation.getAnimatedValue(); - scrim.setScrimColor(Color.argb(value, 0, 0, 0)); + float alpha = (float) animation.getAnimatedValue(); + setCurrentScrimAlpha(scrim, alpha); + updateScrimColor(scrim); } }); - anim.setInterpolator(mAnimateKeyguardFadingOut - ? mLinearOutSlowInInterpolator - : mInterpolator); + anim.setInterpolator(getInterpolator()); anim.setStartDelay(mAnimationDelay); anim.setDuration(mDurationOverride != -1 ? mDurationOverride : ANIMATION_DURATION); anim.addListener(new AnimatorListenerAdapter() { @@ -307,6 +283,10 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { mAnimationStarted = true; } + private Interpolator getInterpolator() { + return mAnimateKeyguardFadingOut ? mLinearOutSlowInInterpolator : mInterpolator; + } + @Override public boolean onPreDraw() { mScrimBehind.getViewTreeObserver().removeOnPreDrawListener(this); @@ -325,56 +305,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { return true; } - private final Runnable mPulseIn = new Runnable() { - @Override - public void run() { - if (DEBUG) Log.d(TAG, "Pulse in, mDozing=" + mDozing); - if (!mDozing) return; - DozeLog.tracePulseStart(); - mDurationOverride = mDozeParameters.getPulseInDuration(); - mAnimationDelay = 0; - mAnimateChange = true; - mOnAnimationFinished = mPulseInFinished; - setScrimColor(mScrimInFront, 0); - - // Signal that the pulse is ready to turn the screen on and draw. - pulseStarted(); - } - }; - - private final Runnable mPulseInFinished = new Runnable() { - @Override - public void run() { - if (DEBUG) Log.d(TAG, "Pulse in finished, mDozing=" + mDozing); - if (!mDozing) return; - mScrimInFront.postDelayed(mPulseOut, mDozeParameters.getPulseVisibleDuration()); - } - }; - - private final Runnable mPulseOut = new Runnable() { - @Override - public void run() { - if (DEBUG) Log.d(TAG, "Pulse out, mDozing=" + mDozing); - if (!mDozing) return; - mDurationOverride = mDozeParameters.getPulseOutDuration(); - mAnimationDelay = 0; - mAnimateChange = true; - mOnAnimationFinished = mPulseOutFinished; - setScrimColor(mScrimInFront, 1); - } - }; - - private final Runnable mPulseOutFinished = new Runnable() { - @Override - public void run() { - if (DEBUG) Log.d(TAG, "Pulse out finished"); - DozeLog.tracePulseFinish(); - - // Signal that the pulse is all finished so we can turn the screen off now. - pulseFinished(); - } - }; - public void setBackDropView(BackDropView backDropView) { mBackDropView = backDropView; mBackDropView.setOnVisibilityChangedRunnable(new Runnable() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SecureCameraLaunchManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SecureCameraLaunchManager.java index 3f5cf3f..4a43c47 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SecureCameraLaunchManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SecureCameraLaunchManager.java @@ -175,6 +175,7 @@ public class SecureCameraLaunchManager { public void run() { Intent intent = new Intent(); intent.setAction(CLOSE_CAMERA_ACTION_NAME); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mContext.sendBroadcast(intent); } }); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java index b0f3ea1..181926c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java @@ -48,6 +48,8 @@ import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.NextAlarmController; import com.android.systemui.statusbar.policy.UserInfoController; +import java.text.NumberFormat; + /** * The view to manage the header area in the expanded status bar. */ @@ -300,9 +302,6 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL updateSystemIconsLayoutParams(); updateClickTargets(); updateMultiUserSwitch(); - if (mQSPanel != null) { - mQSPanel.setExpanded(mExpanded); - } updateClockScale(); updateAvatarScale(); updateClockLp(); @@ -395,7 +394,8 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL @Override public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { - mBatteryLevel.setText(getResources().getString(R.string.battery_level_template, level)); + String percentage = NumberFormat.getPercentInstance().format((double) level / 100.0); + mBatteryLevel.setText(percentage); } @Override @@ -777,9 +777,11 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL v.bringToFront(); v.setVisibility(VISIBLE); } + if (v.hasOverlappingRendering()) { + v.animate().withLayer(); + } v.animate() .alpha(in ? 1 : 0) - .withLayer() .withEndAction(new Runnable() { @Override public void run() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 55c861a..1724e70 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import android.content.ComponentCallbacks2; import android.content.Context; import android.os.Bundle; import android.os.RemoteException; @@ -24,6 +25,7 @@ import android.util.Slog; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManagerGlobal; import com.android.internal.policy.IKeyguardShowCallback; import com.android.internal.widget.LockPatternUtils; @@ -108,7 +110,7 @@ public class StatusBarKeyguardViewManager { // The keyguard might be showing (already). So we need to hide it. mPhoneStatusBar.hideKeyguard(); - mBouncer.show(); + mBouncer.show(true /* resetSecuritySelection */); } else { mPhoneStatusBar.showKeyguard(); mBouncer.hide(false /* destroyView */); @@ -118,7 +120,7 @@ public class StatusBarKeyguardViewManager { private void showBouncer() { if (mShowing) { - mBouncer.show(); + mBouncer.show(false /* resetSecuritySelection */); } updateStates(); } @@ -128,7 +130,7 @@ public class StatusBarKeyguardViewManager { if (!afterKeyguardGone) { mBouncer.showWithDismissAction(r); } else { - mBouncer.show(); + mBouncer.show(false /* resetSecuritySelection */); mAfterKeyguardGoneAction = r; } } @@ -197,7 +199,7 @@ public class StatusBarKeyguardViewManager { new Runnable() { @Override public void run() { - mStatusBarWindowManager.setKeyguardOccluded(true); + mStatusBarWindowManager.setKeyguardOccluded(mOccluded); reset(); } }); @@ -268,6 +270,8 @@ public class StatusBarKeyguardViewManager { public void run() { mStatusBarWindowManager.setKeyguardFadingAway(false); mPhoneStatusBar.finishKeyguardFadingAway(); + WindowManagerGlobal.getInstance().trimMemory( + ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); } }); } else { @@ -299,6 +303,9 @@ public class StatusBarKeyguardViewManager { } } + /** + * WARNING: This method might cause Binder calls. + */ public boolean isSecure() { return mBouncer.isSecure(); } @@ -350,7 +357,7 @@ public class StatusBarKeyguardViewManager { boolean showing = mShowing; boolean occluded = mOccluded; boolean bouncerShowing = mBouncer.isShowing(); - boolean bouncerDismissible = !mBouncer.needsFullscreenBouncer(); + boolean bouncerDismissible = !mBouncer.isFullscreenBouncer(); if ((bouncerDismissible || !showing) != (mLastBouncerDismissible || !mLastShowing) || mFirstUpdate) { @@ -392,6 +399,8 @@ public class StatusBarKeyguardViewManager { mLastOccluded = occluded; mLastBouncerShowing = bouncerShowing; mLastBouncerDismissible = bouncerDismissible; + + mPhoneStatusBar.onKeyguardViewManagerStatesUpdated(); } public boolean onMenuPressed() { @@ -422,4 +431,12 @@ public class StatusBarKeyguardViewManager { public boolean isGoingToNotificationShade() { return mPhoneStatusBar.isGoingToNotificationShade(); } + + public boolean isSecure(int userId) { + return mBouncer.isSecure() || mLockPatternUtils.isSecure(userId); + } + + public boolean isInputRestricted() { + return mViewMediatorCallback.isInputRestricted(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java index 4053c1e..0dbdca1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java @@ -39,6 +39,7 @@ public class StatusBarWindowManager { private final WindowManager mWindowManager; private View mStatusBarView; private WindowManager.LayoutParams mLp; + private WindowManager.LayoutParams mLpChanged; private int mBarHeight; private final boolean mKeyguardScreenRotation; @@ -85,41 +86,43 @@ public class StatusBarWindowManager { mStatusBarView = statusBarView; mBarHeight = barHeight; mWindowManager.addView(mStatusBarView, mLp); + mLpChanged = new WindowManager.LayoutParams(); + mLpChanged.copyFrom(mLp); } private void applyKeyguardFlags(State state) { if (state.keyguardShowing) { - mLp.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; - mLp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; + mLpChanged.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + mLpChanged.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; } else { - mLp.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; - mLp.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; + mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + mLpChanged.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; } } private void adjustScreenOrientation(State state) { if (state.isKeyguardShowingAndNotOccluded()) { if (mKeyguardScreenRotation) { - mLp.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER; + mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER; } else { - mLp.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; + mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; } } else { - mLp.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; } } private void applyFocusableFlag(State state) { if (state.isKeyguardShowingAndNotOccluded() && state.keyguardNeedsInput && state.bouncerShowing) { - mLp.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - mLp.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } else if (state.isKeyguardShowingAndNotOccluded() || state.statusBarFocusable) { - mLp.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - mLp.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mLpChanged.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } else { - mLp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - mLp.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + mLpChanged.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } } @@ -127,9 +130,9 @@ public class StatusBarWindowManager { boolean expanded = state.isKeyguardShowingAndNotOccluded() || state.statusBarExpanded || state.keyguardFadingAway || state.bouncerShowing; if (expanded) { - mLp.height = ViewGroup.LayoutParams.MATCH_PARENT; + mLpChanged.height = ViewGroup.LayoutParams.MATCH_PARENT; } else { - mLp.height = mBarHeight; + mLpChanged.height = mBarHeight; } } @@ -141,9 +144,9 @@ public class StatusBarWindowManager { if (state.isKeyguardShowingAndNotOccluded() && state.statusBarState == StatusBarState.KEYGUARD && !state.qsExpanded) { - mLp.userActivityTimeout = state.keyguardUserActivityTimeout; + mLpChanged.userActivityTimeout = state.keyguardUserActivityTimeout; } else { - mLp.userActivityTimeout = -1; + mLpChanged.userActivityTimeout = -1; } } @@ -151,9 +154,11 @@ public class StatusBarWindowManager { if (state.isKeyguardShowingAndNotOccluded() && state.statusBarState == StatusBarState.KEYGUARD && !state.qsExpanded) { - mLp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; + mLpChanged.inputFeatures |= + WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; } else { - mLp.inputFeatures &= ~WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; + mLpChanged.inputFeatures &= + ~WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; } } @@ -165,7 +170,9 @@ public class StatusBarWindowManager { applyUserActivityTimeout(state); applyInputFeatures(state); applyFitsSystemWindows(state); - mWindowManager.updateViewLayout(mStatusBarView, mLp); + if (mLp.copyFrom(mLpChanged) != 0) { + mWindowManager.updateViewLayout(mStatusBarView, mLp); + } } public void setKeyguardShowing(boolean showing) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java index f0c599d..a96f4e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java @@ -168,11 +168,12 @@ public class StatusBarWindowView extends FrameLayout { if (mNotificationPanel.isFullyExpanded() && mStackScrollLayout.getVisibility() == View.VISIBLE && mService.getBarState() == StatusBarState.KEYGUARD + && !mService.isQsExpanded() && !mService.isBouncerShowing()) { intercept = mDragDownHelper.onInterceptTouchEvent(ev); // wake up on a touch down event, if dozing if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { - mService.wakeUpIfDozing(ev.getEventTime(), true); + mService.wakeUpIfDozing(ev.getEventTime(), ev); } } if (!intercept) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TrustDrawable.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/TrustDrawable.java index dcda2c7..b89aa8f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TrustDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/TrustDrawable.java @@ -133,6 +133,7 @@ public class TrustDrawable extends Drawable { if (!mAnimating) { mAnimating = true; updateState(true); + invalidateSelf(); } } @@ -146,18 +147,21 @@ public class TrustDrawable extends Drawable { mState = STATE_UNSET; mCurAlpha = 0; mCurInnerRadius = mInnerRadiusEnter; + invalidateSelf(); } } public void setTrustManaged(boolean trustManaged) { if (trustManaged == mTrustManaged && mState != STATE_UNSET) return; mTrustManaged = trustManaged; - if (mAnimating) { - updateState(true); - } + updateState(true); } - private void updateState(boolean animate) { + private void updateState(boolean allowTransientState) { + if (!mAnimating) { + return; + } + int nextState = mState; if (mState == STATE_UNSET) { nextState = mTrustManaged ? STATE_ENTERING : STATE_GONE; @@ -170,7 +174,7 @@ public class TrustDrawable extends Drawable { } else if (mState == STATE_EXITING) { if (mTrustManaged) nextState = STATE_ENTERING; } - if (!animate) { + if (!allowTransientState) { if (nextState == STATE_ENTERING) nextState = STATE_VISIBLE; if (nextState == STATE_EXITING) nextState = STATE_GONE; } @@ -200,9 +204,8 @@ public class TrustDrawable extends Drawable { mState = nextState; if (mCurAnimator != null) { mCurAnimator.start(); - } else { - invalidateSelf(); } + invalidateSelf(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockMethodCache.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockMethodCache.java index e5eef9d..5ef345b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockMethodCache.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockMethodCache.java @@ -36,7 +36,10 @@ public class UnlockMethodCache { private final LockPatternUtils mLockPatternUtils; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final ArrayList<OnUnlockMethodChangedListener> mListeners = new ArrayList<>(); - private boolean mMethodInsecure; + /** Whether the user configured a secure unlock method (PIN, password, etc.) */ + private boolean mSecure; + /** Whether the unlock method is currently insecure (insecure method or trusted environment) */ + private boolean mCurrentlyInsecure; private boolean mTrustManaged; private boolean mFaceUnlockRunning; @@ -44,7 +47,7 @@ public class UnlockMethodCache { mLockPatternUtils = new LockPatternUtils(ctx); mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(ctx); KeyguardUpdateMonitor.getInstance(ctx).registerCallback(mCallback); - updateMethodSecure(true /* updateAlways */); + update(true /* updateAlways */); } public static UnlockMethodCache getInstance(Context context) { @@ -55,10 +58,17 @@ public class UnlockMethodCache { } /** - * @return whether the current security method is secure, i. e. the bouncer will be shown + * @return whether the user configured a secure unlock method like PIN, password, etc. */ - public boolean isMethodInsecure() { - return mMethodInsecure; + public boolean isMethodSecure() { + return mSecure; + } + + /** + * @return whether the lockscreen is currently insecure, i. e. the bouncer won't be shown + */ + public boolean isCurrentlyInsecure() { + return mCurrentlyInsecure; } public void addListener(OnUnlockMethodChangedListener listener) { @@ -69,58 +79,59 @@ public class UnlockMethodCache { mListeners.remove(listener); } - private void updateMethodSecure(boolean updateAlways) { + private void update(boolean updateAlways) { int user = mLockPatternUtils.getCurrentUser(); - boolean methodInsecure = !mLockPatternUtils.isSecure() || - mKeyguardUpdateMonitor.getUserHasTrust(user); + boolean secure = mLockPatternUtils.isSecure(); + boolean currentlyInsecure = !secure || mKeyguardUpdateMonitor.getUserHasTrust(user); boolean trustManaged = mKeyguardUpdateMonitor.getUserTrustIsManaged(user); boolean faceUnlockRunning = mKeyguardUpdateMonitor.isFaceUnlockRunning(user) && trustManaged; - boolean changed = methodInsecure != mMethodInsecure || trustManaged != mTrustManaged - || faceUnlockRunning != mFaceUnlockRunning; + boolean changed = secure != mSecure || currentlyInsecure != mCurrentlyInsecure || + trustManaged != mTrustManaged || faceUnlockRunning != mFaceUnlockRunning; if (changed || updateAlways) { - mMethodInsecure = methodInsecure; + mSecure = secure; + mCurrentlyInsecure = currentlyInsecure; mTrustManaged = trustManaged; mFaceUnlockRunning = faceUnlockRunning; - notifyListeners(mMethodInsecure); + notifyListeners(); } } - private void notifyListeners(boolean secure) { + private void notifyListeners() { for (OnUnlockMethodChangedListener listener : mListeners) { - listener.onMethodSecureChanged(secure); + listener.onUnlockMethodStateChanged(); } } private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() { @Override public void onUserSwitchComplete(int userId) { - updateMethodSecure(false /* updateAlways */); + update(false /* updateAlways */); } @Override public void onTrustChanged(int userId) { - updateMethodSecure(false /* updateAlways */); + update(false /* updateAlways */); } @Override public void onTrustManagedChanged(int userId) { - updateMethodSecure(false /* updateAlways */); + update(false /* updateAlways */); } @Override public void onScreenTurnedOn() { - updateMethodSecure(false /* updateAlways */); + update(false /* updateAlways */); } @Override public void onFingerprintRecognized(int userId) { - updateMethodSecure(false /* updateAlways */); + update(false /* updateAlways */); } @Override public void onFaceUnlockStateChanged(boolean running, int userId) { - updateMethodSecure(false /* updateAlways */); + update(false /* updateAlways */); } }; @@ -133,6 +144,6 @@ public class UnlockMethodCache { } public static interface OnUnlockMethodChangedListener { - void onMethodSecureChanged(boolean methodSecure); + void onUnlockMethodStateChanged(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiAccessPointController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java index b800fbf..ad4c211 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiAccessPointController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java @@ -16,32 +16,42 @@ package com.android.systemui.statusbar.policy; +import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiConfiguration.KeyMgmt; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.ActionListener; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import com.android.systemui.R; -import com.android.systemui.statusbar.policy.NetworkController.AccessPoint; -import com.android.systemui.statusbar.policy.NetworkController.AccessPointCallback; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -public class WifiAccessPointController { - private static final String TAG = "WifiAccessPointController"; - private static final boolean DEBUG = false; + +// TODO: Unify this logic with platform settings (see WifiSettings and AccessPoint). There is a +// fair amount of complexity here in statuses and logic beyond just connected/disconnected. +public class AccessPointControllerImpl implements NetworkController.AccessPointController { + private static final String TAG = "AccessPointController"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // This string extra specifies a network to open the connect dialog on, so the user can enter + // network credentials. This is used by quick settings for secured networks. + private static final String EXTRA_START_CONNECT_SSID = "wifi_start_connect_ssid"; private static final int[] ICONS = { R.drawable.ic_qs_wifi_0, @@ -54,52 +64,88 @@ public class WifiAccessPointController { private final Context mContext; private final ArrayList<AccessPointCallback> mCallbacks = new ArrayList<AccessPointCallback>(); private final WifiManager mWifiManager; + private final UserManager mUserManager; private final Receiver mReceiver = new Receiver(); + private NetworkControllerImpl mNetworkController; private boolean mScanning; + private int mCurrentUser; - public WifiAccessPointController(Context context) { + public AccessPointControllerImpl(Context context) { mContext = context; mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mCurrentUser = ActivityManager.getCurrentUser(); + } + + void setNetworkController(NetworkControllerImpl networkController) { + mNetworkController = networkController; + } + + public boolean canConfigWifi() { + return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, + new UserHandle(mCurrentUser)); } - public void addCallback(AccessPointCallback callback) { + public void onUserSwitched(int newUserId) { + mCurrentUser = newUserId; + } + + @Override + public void addAccessPointCallback(AccessPointCallback callback) { if (callback == null || mCallbacks.contains(callback)) return; if (DEBUG) Log.d(TAG, "addCallback " + callback); mCallbacks.add(callback); mReceiver.setListening(!mCallbacks.isEmpty()); } - public void removeCallback(AccessPointCallback callback) { + @Override + public void removeAccessPointCallback(AccessPointCallback callback) { if (callback == null) return; if (DEBUG) Log.d(TAG, "removeCallback " + callback); mCallbacks.remove(callback); mReceiver.setListening(!mCallbacks.isEmpty()); } - public void scan() { + @Override + public void scanForAccessPoints() { if (mScanning) return; if (DEBUG) Log.d(TAG, "scan!"); mScanning = mWifiManager.startScan(); + // Grab current networks immediately while we wait for scan. + updateAccessPoints(); } - public void connect(AccessPoint ap) { - if (ap == null || ap.networkId < 0) return; + public boolean connect(AccessPoint ap) { + if (ap == null) return false; if (DEBUG) Log.d(TAG, "connect networkId=" + ap.networkId); - mWifiManager.connect(ap.networkId, new ActionListener() { - @Override - public void onSuccess() { - if (DEBUG) Log.d(TAG, "connect success"); + if (ap.networkId < 0) { + // Unknown network, need to add it. + if (ap.hasSecurity) { + Intent intent = new Intent(Settings.ACTION_WIFI_SETTINGS); + intent.putExtra(EXTRA_START_CONNECT_SSID, ap.ssid); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + fireSettingsIntentCallback(intent); + return true; + } else { + WifiConfiguration config = new WifiConfiguration(); + config.SSID = "\"" + ap.ssid + "\""; + config.allowedKeyManagement.set(KeyMgmt.NONE); + mWifiManager.connect(config, mConnectListener); } + } else { + mWifiManager.connect(ap.networkId, mConnectListener); + } + return false; + } - @Override - public void onFailure(int reason) { - if (DEBUG) Log.d(TAG, "connect failure reason=" + reason); - } - }); + private void fireSettingsIntentCallback(Intent intent) { + for (AccessPointCallback callback : mCallbacks) { + callback.onSettingsActivityTriggered(intent); + } } - private void fireCallback(AccessPoint[] aps) { + private void fireAcccessPointsCallback(AccessPoint[] aps) { for (AccessPointCallback callback : mCallbacks) { callback.onAccessPointsChanged(aps); } @@ -110,8 +156,7 @@ public class WifiAccessPointController { && v.charAt(v.length() - 1) == '\"' ? v.substring(1, v.length() - 1) : v; } - private int getConnectedNetworkId() { - final WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + private int getConnectedNetworkId(WifiInfo wifiInfo) { return wifiInfo != null ? wifiInfo.getNetworkId() : AccessPoint.NO_NETWORK; } @@ -126,7 +171,8 @@ public class WifiAccessPointController { } private void updateAccessPoints() { - final int connectedNetworkId = getConnectedNetworkId(); + final WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + final int connectedNetworkId = getConnectedNetworkId(wifiInfo); if (DEBUG) Log.d(TAG, "connectedNetworkId: " + connectedNetworkId); final List<ScanResult> scanResults = mWifiManager.getScanResults(); final ArrayMap<String, WifiConfiguration> configured = getConfiguredNetworksBySsid(); @@ -139,23 +185,50 @@ public class WifiAccessPointController { } final String ssid = scanResult.SSID; if (TextUtils.isEmpty(ssid) || ssids.contains(ssid)) continue; - if (!configured.containsKey(ssid)) continue; ssids.add(ssid); final WifiConfiguration config = configured.get(ssid); final int level = WifiManager.calculateSignalLevel(scanResult.level, ICONS.length); final AccessPoint ap = new AccessPoint(); + ap.isConfigured = config != null; ap.networkId = config != null ? config.networkId : AccessPoint.NO_NETWORK; ap.ssid = ssid; - ap.iconId = ICONS[level]; - ap.isConnected = ap.networkId != AccessPoint.NO_NETWORK - && ap.networkId == connectedNetworkId; - ap.level = level; + // Connected if either: + // -The network ID in the active WifiInfo matches this network's ID. + // -The network is ephemeral (no configuration) but the SSID matches. + ap.isConnected = (ap.networkId != AccessPoint.NO_NETWORK + && ap.networkId == connectedNetworkId) || + (ap.networkId == WifiConfiguration.INVALID_NETWORK_ID && wifiInfo != null && + ap.ssid.equals(trimDoubleQuotes(wifiInfo.getSSID()))); + if (ap.isConnected && mNetworkController != null) { + // Ensure we have the connected network's RSSI. + ap.level = mNetworkController.getConnectedWifiLevel(); + } else { + ap.level = level; + } + ap.iconId = ICONS[ap.level]; + // Based on Settings AccessPoint#getSecurity, keep up to date + // with better methods of determining no security or not. + ap.hasSecurity = scanResult.capabilities.contains("WEP") + || scanResult.capabilities.contains("PSK") + || scanResult.capabilities.contains("EAP"); aps.add(ap); } Collections.sort(aps, mByStrength); - fireCallback(aps.toArray(new AccessPoint[aps.size()])); + fireAcccessPointsCallback(aps.toArray(new AccessPoint[aps.size()])); } + private final ActionListener mConnectListener = new ActionListener() { + @Override + public void onSuccess() { + if (DEBUG) Log.d(TAG, "connect success"); + } + + @Override + public void onFailure(int reason) { + if (DEBUG) Log.d(TAG, "connect failure reason=" + reason); + } + }; + private final Comparator<AccessPoint> mByStrength = new Comparator<AccessPoint> () { @Override public int compare(AccessPoint lhs, AccessPoint rhs) { @@ -163,7 +236,7 @@ public class WifiAccessPointController { } private int score(AccessPoint ap) { - return ap.level + (ap.isConnected ? 10 : 0); + return ap.level + (ap.isConnected ? 20 : 0) + (ap.isConfigured ? 10 : 0); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java index 7ac2a98..63fcbc5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java @@ -33,11 +33,6 @@ public class AccessibilityContentDescriptions { R.string.accessibility_wifi_three_bars, R.string.accessibility_wifi_signal_full }; - static final int[] WIMAX_CONNECTION_STRENGTH = { - R.string.accessibility_no_wimax, - R.string.accessibility_wimax_one_bar, - R.string.accessibility_wimax_two_bars, - R.string.accessibility_wimax_three_bars, - R.string.accessibility_wimax_signal_full - }; + + static final int WIFI_NO_CONNECTION = R.string.accessibility_no_wifi; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java index 80fec5b..81e1e45 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -19,26 +19,35 @@ package com.android.systemui.statusbar.policy; import static android.bluetooth.BluetoothAdapter.ERROR; import static com.android.systemui.statusbar.policy.BluetoothUtil.connectionStateToString; import static com.android.systemui.statusbar.policy.BluetoothUtil.deviceToString; -import static com.android.systemui.statusbar.policy.BluetoothUtil.profileStateToString; import static com.android.systemui.statusbar.policy.BluetoothUtil.profileToString; import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToProfile; import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToString; import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidsToString; +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothA2dpSink; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothHeadsetClient; +import android.bluetooth.BluetoothInputDevice; import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothMap; +import android.bluetooth.BluetoothPan; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile.ServiceListener; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.os.ParcelUuid; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; -import android.util.SparseBooleanArray; +import android.util.SparseArray; import com.android.systemui.statusbar.policy.BluetoothUtil.Profile; @@ -50,19 +59,44 @@ import java.util.Set; public class BluetoothControllerImpl implements BluetoothController { private static final String TAG = "BluetoothController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + // This controls the order in which we check the states. Since a device can only have + // one state on screen, but can have multiple profiles, the later states override the + // value of earlier states. So if a device has a profile in CONNECTING and one in + // CONNECTED, it will show as CONNECTED, theoretically this shouldn't really happen often, + // but seemed worth noting. + private static final int[] CONNECTION_STATES = { + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.STATE_DISCONNECTING, + BluetoothProfile.STATE_CONNECTING, + BluetoothProfile.STATE_CONNECTED, + }; + // Update all the BT device states. + private static final int MSG_UPDATE_CONNECTION_STATES = 1; + // Update just one BT device. + private static final int MSG_UPDATE_SINGLE_CONNECTION_STATE = 2; + // Update whether devices are bonded or not. + private static final int MSG_UPDATE_BONDED_DEVICES = 3; + + private static final int MSG_ADD_PROFILE = 4; + private static final int MSG_REM_PROFILE = 5; private final Context mContext; private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); private final BluetoothAdapter mAdapter; private final Receiver mReceiver = new Receiver(); private final ArrayMap<BluetoothDevice, DeviceInfo> mDeviceInfo = new ArrayMap<>(); + private final SparseArray<BluetoothProfile> mProfiles = new SparseArray<>(); + + private final H mHandler; private boolean mEnabled; private boolean mConnecting; private BluetoothDevice mLastDevice; - public BluetoothControllerImpl(Context context) { + public BluetoothControllerImpl(Context context, Looper bgLooper) { mContext = context; + mHandler = new H(bgLooper); + final BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); mAdapter = bluetoothManager.getAdapter(); @@ -73,7 +107,8 @@ public class BluetoothControllerImpl implements BluetoothController { mReceiver.register(); setAdapterState(mAdapter.getState()); - updateBondedBluetoothDevices(); + updateBondedDevices(); + bindAllProfiles(); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { @@ -83,6 +118,7 @@ public class BluetoothControllerImpl implements BluetoothController { pw.print(" mConnecting="); pw.println(mConnecting); pw.print(" mLastDevice="); pw.println(mLastDevice); pw.print(" mCallbacks.size="); pw.println(mCallbacks.size()); + pw.print(" mProfiles="); pw.println(profilesToString(mProfiles)); pw.print(" mDeviceInfo.size="); pw.println(mDeviceInfo.size()); for (int i = 0; i < mDeviceInfo.size(); i++) { final BluetoothDevice device = mDeviceInfo.keyAt(i); @@ -95,7 +131,23 @@ public class BluetoothControllerImpl implements BluetoothController { private static String infoToString(DeviceInfo info) { return info == null ? null : ("connectionState=" + - connectionStateToString(info.connectionState) + ",bonded=" + info.bonded); + connectionStateToString(CONNECTION_STATES[info.connectionStateIndex]) + + ",bonded=" + info.bonded + ",profiles=" + + profilesToString(info.connectedProfiles)); + } + + private static String profilesToString(SparseArray<?> profiles) { + final int N = profiles.size(); + final StringBuffer buffer = new StringBuffer(); + buffer.append('['); + for (int i = 0; i < N; i++) { + if (i != 0) { + buffer.append(','); + } + buffer.append(BluetoothUtil.profileToString(profiles.keyAt(i))); + } + buffer.append(']'); + return buffer.toString(); } public void addStateChangedCallback(Callback cb) { @@ -152,13 +204,14 @@ public class BluetoothControllerImpl implements BluetoothController { paired.id = device.getAddress(); paired.tag = device; paired.name = device.getAliasName(); - paired.state = connectionStateToPairedDeviceState(info.connectionState); + paired.state = connectionStateToPairedDeviceState(info.connectionStateIndex); rt.add(paired); } return rt; } - private static int connectionStateToPairedDeviceState(int state) { + private static int connectionStateToPairedDeviceState(int index) { + int state = CONNECTION_STATES[index]; if (state == BluetoothAdapter.STATE_CONNECTED) return PairedDevice.STATE_CONNECTED; if (state == BluetoothAdapter.STATE_CONNECTING) return PairedDevice.STATE_CONNECTING; if (state == BluetoothAdapter.STATE_DISCONNECTING) return PairedDevice.STATE_DISCONNECTING; @@ -178,6 +231,7 @@ public class BluetoothControllerImpl implements BluetoothController { private void connect(PairedDevice pd, final boolean connect) { if (mAdapter == null || pd == null || pd.tag == null) return; final BluetoothDevice device = (BluetoothDevice) pd.tag; + final DeviceInfo info = mDeviceInfo.get(device); final String action = connect ? "connect" : "disconnect"; if (DEBUG) Log.d(TAG, action + " " + deviceToString(device)); final ParcelUuid[] uuids = device.getUuids(); @@ -185,43 +239,35 @@ public class BluetoothControllerImpl implements BluetoothController { Log.w(TAG, "No uuids returned, aborting " + action + " for " + deviceToString(device)); return; } - final SparseBooleanArray profiles = new SparseBooleanArray(); - for (ParcelUuid uuid : uuids) { - final int profile = uuidToProfile(uuid); - if (profile == 0) { - Log.w(TAG, "Device " + deviceToString(device) + " has an unsupported uuid: " - + uuidToString(uuid)); - continue; - } - final int profileState = mAdapter.getProfileConnectionState(profile); - if (DEBUG && !profiles.get(profile)) Log.d(TAG, "Profile " + profileToString(profile) - + " state = " + profileStateToString(profileState)); - final boolean connected = profileState == BluetoothProfile.STATE_CONNECTED; - if (connect != connected) { - profiles.put(profile, true); + SparseArray<Boolean> profiles = new SparseArray<>(); + if (connect) { + // When connecting add every profile we can recognize by uuid. + for (ParcelUuid uuid : uuids) { + final int profile = uuidToProfile(uuid); + if (profile == 0) { + Log.w(TAG, "Device " + deviceToString(device) + " has an unsupported uuid: " + + uuidToString(uuid)); + continue; + } + final boolean connected = info.connectedProfiles.get(profile, false); + if (!connected) { + profiles.put(profile, true); + } } + } else { + // When disconnecting, just add every profile we know they are connected to. + profiles = info.connectedProfiles; } for (int i = 0; i < profiles.size(); i++) { final int profile = profiles.keyAt(i); - mAdapter.getProfileProxy(mContext, new ServiceListener() { - @Override - public void onServiceConnected(int profile, BluetoothProfile proxy) { - if (DEBUG) Log.d(TAG, "onServiceConnected " + profileToString(profile)); - final Profile p = BluetoothUtil.getProfile(proxy); - if (p == null) { - Log.w(TAG, "Unable get get Profile for " + profileToString(profile)); - } else { - final boolean ok = connect ? p.connect(device) : p.disconnect(device); - if (DEBUG) Log.d(TAG, action + " " + profileToString(profile) + " " - + (ok ? "succeeded" : "failed")); - } - } - - @Override - public void onServiceDisconnected(int profile) { - if (DEBUG) Log.d(TAG, "onServiceDisconnected " + profileToString(profile)); - } - }, profile); + if (mProfiles.indexOfKey(profile) >= 0) { + final Profile p = BluetoothUtil.getProfile(mProfiles.get(profile)); + final boolean ok = connect ? p.connect(device) : p.disconnect(device); + if (DEBUG) Log.d(TAG, action + " " + profileToString(profile) + " " + + (ok ? "succeeded" : "failed")); + } else { + Log.w(TAG, "Unable get get Profile for " + profileToString(profile)); + } } } @@ -230,7 +276,27 @@ public class BluetoothControllerImpl implements BluetoothController { return mLastDevice != null ? mLastDevice.getAliasName() : null; } - private void updateBondedBluetoothDevices() { + private void updateBondedDevices() { + mHandler.removeMessages(MSG_UPDATE_BONDED_DEVICES); + mHandler.sendEmptyMessage(MSG_UPDATE_BONDED_DEVICES); + } + + private void updateConnectionStates() { + mHandler.removeMessages(MSG_UPDATE_CONNECTION_STATES); + mHandler.removeMessages(MSG_UPDATE_SINGLE_CONNECTION_STATE); + mHandler.sendEmptyMessage(MSG_UPDATE_CONNECTION_STATES); + } + + private void updateConnectionState(BluetoothDevice device, int profile, int state) { + if (mHandler.hasMessages(MSG_UPDATE_CONNECTION_STATES)) { + // If we are about to update all the devices, then we don't need to update this one. + return; + } + mHandler.obtainMessage(MSG_UPDATE_SINGLE_CONNECTION_STATE, profile, state, device) + .sendToTarget(); + } + + private void handleUpdateBondedDevices() { if (mAdapter == null) return; final Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices(); for (DeviceInfo info : mDeviceInfo.values()) { @@ -251,9 +317,92 @@ public class BluetoothControllerImpl implements BluetoothController { if (mLastDevice == null && bondedCount == 1) { mLastDevice = lastBonded; } + updateConnectionStates(); firePairedDevicesChanged(); } + private void handleUpdateConnectionStates() { + final int N = mDeviceInfo.size(); + for (int i = 0; i < N; i++) { + BluetoothDevice device = mDeviceInfo.keyAt(i); + DeviceInfo info = updateInfo(device); + info.connectionStateIndex = 0; + info.connectedProfiles.clear(); + for (int j = 0; j < mProfiles.size(); j++) { + int state = mProfiles.valueAt(j).getConnectionState(device); + handleUpdateConnectionState(device, mProfiles.keyAt(j), state); + } + } + handleConnectionChange(); + firePairedDevicesChanged(); + } + + private void handleUpdateConnectionState(BluetoothDevice device, int profile, int state) { + if (DEBUG) Log.d(TAG, "updateConnectionState " + BluetoothUtil.deviceToString(device) + + " " + BluetoothUtil.profileToString(profile) + + " " + BluetoothUtil.connectionStateToString(state)); + DeviceInfo info = updateInfo(device); + int stateIndex = 0; + for (int i = 0; i < CONNECTION_STATES.length; i++) { + if (CONNECTION_STATES[i] == state) { + stateIndex = i; + break; + } + } + info.profileStates.put(profile, stateIndex); + + info.connectionStateIndex = 0; + final int N = info.profileStates.size(); + for (int i = 0; i < N; i++) { + if (info.profileStates.valueAt(i) > info.connectionStateIndex) { + info.connectionStateIndex = info.profileStates.valueAt(i); + } + } + if (state == BluetoothProfile.STATE_CONNECTED) { + info.connectedProfiles.put(profile, true); + } else { + info.connectedProfiles.remove(profile); + } + } + + private void handleConnectionChange() { + // If we are no longer connected to the current device, see if we are connected to + // something else, so we don't display a name we aren't connected to. + if (mLastDevice != null && + CONNECTION_STATES[mDeviceInfo.get(mLastDevice).connectionStateIndex] + != BluetoothProfile.STATE_CONNECTED) { + // Make sure we don't keep this device while it isn't connected. + mLastDevice = null; + // Look for anything else connected. + final int size = mDeviceInfo.size(); + for (int i = 0; i < size; i++) { + BluetoothDevice device = mDeviceInfo.keyAt(i); + DeviceInfo info = mDeviceInfo.valueAt(i); + if (CONNECTION_STATES[info.connectionStateIndex] + == BluetoothProfile.STATE_CONNECTED) { + mLastDevice = device; + break; + } + } + } + } + + private void bindAllProfiles() { + // Note: This needs to contain all of the types that can be returned by BluetoothUtil + // otherwise we can't find the profiles we need when we connect/disconnect. + mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP); + mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP_SINK); + mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.AVRCP_CONTROLLER); + mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET); + mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET_CLIENT); + mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.INPUT_DEVICE); + mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.MAP); + mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.PAN); + // Note Health is not in this list because health devices aren't 'connected'. + // If profiles are expanded to use more than just connection state and connect/disconnect + // then it should be added. + } + private void firePairedDevicesChanged() { for (Callback cb : mCallbacks) { cb.onBluetoothPairedDevicesChanged(); @@ -283,6 +432,43 @@ public class BluetoothControllerImpl implements BluetoothController { cb.onBluetoothStateChange(mEnabled, mConnecting); } + private static int getProfileFromAction(String action) { + if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { + return BluetoothProfile.A2DP; + } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { + return BluetoothProfile.HEADSET; + } else if (BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { + return BluetoothProfile.A2DP_SINK; + } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { + return BluetoothProfile.HEADSET_CLIENT; + } else if (BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { + return BluetoothProfile.INPUT_DEVICE; + } else if (BluetoothMap.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { + return BluetoothProfile.MAP; + } else if (BluetoothPan.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { + return BluetoothProfile.PAN; + } + if (DEBUG) Log.d(TAG, "Unknown action " + action); + return -1; + } + + private final ServiceListener mProfileListener = new ServiceListener() { + @Override + public void onServiceDisconnected(int profile) { + if (DEBUG) Log.d(TAG, "Disconnected from " + BluetoothUtil.profileToString(profile)); + // We lost a profile, don't do any updates until it gets removed. + mHandler.removeMessages(MSG_UPDATE_CONNECTION_STATES); + mHandler.removeMessages(MSG_UPDATE_SINGLE_CONNECTION_STATE); + mHandler.obtainMessage(MSG_REM_PROFILE, profile, 0).sendToTarget(); + } + + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (DEBUG) Log.d(TAG, "Connected to " + BluetoothUtil.profileToString(profile)); + mHandler.obtainMessage(MSG_ADD_PROFILE, profile, 0, proxy).sendToTarget(); + } + }; + private final class Receiver extends BroadcastReceiver { public void register() { final IntentFilter filter = new IntentFilter(); @@ -290,6 +476,13 @@ public class BluetoothControllerImpl implements BluetoothController { filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); filter.addAction(BluetoothDevice.ACTION_ALIAS_CHANGED); + filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothMap.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED); mContext.registerReceiver(this, filter); } @@ -297,28 +490,35 @@ public class BluetoothControllerImpl implements BluetoothController { public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { setAdapterState(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, ERROR)); + updateBondedDevices(); if (DEBUG) Log.d(TAG, "ACTION_STATE_CHANGED " + mEnabled); } else if (action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) { - final DeviceInfo info = updateInfo(device); + updateInfo(device); final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, ERROR); - if (state != ERROR) { - info.connectionState = state; - } mLastDevice = device; if (DEBUG) Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED " + connectionStateToString(state) + " " + deviceToString(device)); - setConnecting(info.connectionState == BluetoothAdapter.STATE_CONNECTING); + setConnecting(state == BluetoothAdapter.STATE_CONNECTING); } else if (action.equals(BluetoothDevice.ACTION_ALIAS_CHANGED)) { updateInfo(device); mLastDevice = device; } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { if (DEBUG) Log.d(TAG, "ACTION_BOND_STATE_CHANGED " + device); - // we'll update all bonded devices below + updateBondedDevices(); + } else { + int profile = getProfileFromAction(intent.getAction()); + int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); + if (DEBUG) Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGE " + + BluetoothUtil.profileToString(profile) + + " " + BluetoothUtil.connectionStateToString(state)); + if ((profile != -1) && (state != -1)) { + updateConnectionState(device, profile, state); + } } - updateBondedBluetoothDevices(); } } @@ -329,8 +529,44 @@ public class BluetoothControllerImpl implements BluetoothController { return info; } + private class H extends Handler { + public H(Looper l) { + super(l); + } + + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_CONNECTION_STATES: + handleUpdateConnectionStates(); + firePairedDevicesChanged(); + break; + case MSG_UPDATE_SINGLE_CONNECTION_STATE: + handleUpdateConnectionState((BluetoothDevice) msg.obj, msg.arg1, msg.arg2); + handleConnectionChange(); + firePairedDevicesChanged(); + break; + case MSG_UPDATE_BONDED_DEVICES: + handleUpdateBondedDevices(); + firePairedDevicesChanged(); + break; + case MSG_ADD_PROFILE: + mProfiles.put(msg.arg1, (BluetoothProfile) msg.obj); + handleUpdateConnectionStates(); + firePairedDevicesChanged(); + break; + case MSG_REM_PROFILE: + mProfiles.remove(msg.arg1); + handleUpdateConnectionStates(); + firePairedDevicesChanged(); + break; + } + }; + }; + private static class DeviceInfo { - int connectionState = BluetoothAdapter.STATE_DISCONNECTED; + int connectionStateIndex = 0; boolean bonded; // per getBondedDevices + SparseArray<Boolean> connectedProfiles = new SparseArray<>(); + SparseArray<Integer> profileStates = new SparseArray<>(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java index 1b4be85..ed8ac2c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java @@ -36,7 +36,10 @@ public class BluetoothUtil { if (profile == BluetoothProfile.HEADSET) return "HEADSET"; if (profile == BluetoothProfile.A2DP) return "A2DP"; if (profile == BluetoothProfile.AVRCP_CONTROLLER) return "AVRCP_CONTROLLER"; - return "UNKNOWN"; + if (profile == BluetoothProfile.PAN) return "PAN"; + if (profile == BluetoothProfile.INPUT_DEVICE) return "INPUT_DEVICE"; + if (profile == BluetoothProfile.MAP) return "MAP"; + return "UNKNOWN(" + profile + ")"; } public static String profileStateToString(int state) { @@ -106,6 +109,11 @@ public class BluetoothUtil { if (BluetoothUuid.AvrcpController.equals(uuid)) return BluetoothProfile.AVRCP_CONTROLLER; + if (BluetoothUuid.Hid.equals(uuid)) return BluetoothProfile.INPUT_DEVICE; + if (BluetoothUuid.Hogp.equals(uuid)) return BluetoothProfile.INPUT_DEVICE; + + if (BluetoothUuid.NAP.equals(uuid)) return BluetoothProfile.PAN; + return 0; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index 55a0bba..779ff52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -16,12 +16,14 @@ package com.android.systemui.statusbar.policy; +import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.TypedArray; import android.os.Bundle; +import android.os.UserHandle; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.format.DateFormat; @@ -91,7 +93,8 @@ public class Clock extends TextView implements DemoMode { filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); filter.addAction(Intent.ACTION_USER_SWITCHED); - getContext().registerReceiver(mIntentReceiver, filter, null, getHandler()); + getContext().registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, + null, getHandler()); } // NOTE: It's safe to do these after registering the receiver since the receiver always runs @@ -142,7 +145,7 @@ public class Clock extends TextView implements DemoMode { private final CharSequence getSmallTime() { Context context = getContext(); - boolean is24 = DateFormat.is24HourFormat(context); + boolean is24 = DateFormat.is24HourFormat(context, ActivityManager.getCurrentUser()); LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale); final char MAGIC1 = '\uEF00'; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java index 6f021ac..33f7aff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java @@ -308,7 +308,11 @@ public class FlashlightController { new CameraCaptureSession.StateListener() { @Override public void onConfigured(CameraCaptureSession session) { - mSession = session; + if (session.getDevice() == mCameraDevice) { + mSession = session; + } else { + session.close(); + } postUpdateFlashlight(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java index 84216a4..2e3e67a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java @@ -18,8 +18,13 @@ package com.android.systemui.statusbar.policy; import android.content.Context; import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.ContentObserver; import android.graphics.Outline; import android.graphics.Rect; +import android.os.SystemClock; +import android.provider.Settings; +import android.util.ArrayMap; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; @@ -39,27 +44,36 @@ import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.phone.PhoneStatusBar; +import java.util.ArrayList; + public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper.Callback, ExpandHelper.Callback, ViewTreeObserver.OnComputeInternalInsetsListener { private static final String TAG = "HeadsUpNotificationView"; private static final boolean DEBUG = false; private static final boolean SPEW = DEBUG; + private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; Rect mTmpRect = new Rect(); int[] mTmpTwoArray = new int[2]; private final int mTouchSensitivityDelay; private final float mMaxAlpha = 1f; + private final ArrayMap<String, Long> mSnoozedPackages; + private final int mDefaultSnoozeLengthMs; + private SwipeHelper mSwipeHelper; private EdgeSwipeHelper mEdgeSwipeHelper; private PhoneStatusBar mBar; - private ExpandHelper mExpandHelper; private long mStartTouchTime; private ViewGroup mContentHolder; + private int mSnoozeLengthMs; + private ContentObserver mSettingsObserver; private NotificationData.Entry mHeadsUp; + private int mUser; + private String mMostRecentPackageName; public HeadsUpNotificationView(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -67,8 +81,12 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. public HeadsUpNotificationView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mTouchSensitivityDelay = getResources().getInteger(R.integer.heads_up_sensitivity_delay); + Resources resources = context.getResources(); + mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay); if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay); + mSnoozedPackages = new ArrayMap<>(); + mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms); + mSnoozeLengthMs = mDefaultSnoozeLengthMs; } public void updateResources() { @@ -100,8 +118,10 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. } if (mHeadsUp != null) { + mMostRecentPackageName = mHeadsUp.notification.getPackageName(); mHeadsUp.row.setSystemExpanded(true); mHeadsUp.row.setSensitive(false); + mHeadsUp.row.setHeadsUp(true); mHeadsUp.row.setHideSensitive( false, false /* animated */, 0 /* delay */, 0 /* duration */); if (mContentHolder == null) { @@ -115,7 +135,7 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); mSwipeHelper.snapChild(mContentHolder, 1f); - mStartTouchTime = System.currentTimeMillis() + mTouchSensitivityDelay; + mStartTouchTime = SystemClock.elapsedRealtime() + mTouchSensitivityDelay; mHeadsUp.setInterruption(); @@ -166,6 +186,31 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. mHeadsUp = null; } + public boolean isSnoozed(String packageName) { + final String key = snoozeKey(packageName, mUser); + Long snoozedUntil = mSnoozedPackages.get(key); + if (snoozedUntil != null) { + if (snoozedUntil > SystemClock.elapsedRealtime()) { + if (DEBUG) Log.v(TAG, key + " snoozed"); + return true; + } + mSnoozedPackages.remove(packageName); + } + return false; + } + + private void snooze() { + if (mMostRecentPackageName != null) { + mSnoozedPackages.put(snoozeKey(mMostRecentPackageName, mUser), + SystemClock.elapsedRealtime() + mSnoozeLengthMs); + } + releaseAndClose(); + } + + private static String snoozeKey(String packageName, int user) { + return user + "," + packageName; + } + public void releaseAndClose() { release(); mBar.scheduleHeadsUpClose(); @@ -205,11 +250,28 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height); - mExpandHelper = new ExpandHelper(getContext(), this, minHeight, maxHeight); mContentHolder = (ViewGroup) findViewById(R.id.content_holder); mContentHolder.setOutlineProvider(CONTENT_HOLDER_OUTLINE_PROVIDER); + mSnoozeLengthMs = Settings.Global.getInt(mContext.getContentResolver(), + SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs); + mSettingsObserver = new ContentObserver(getHandler()) { + @Override + public void onChange(boolean selfChange) { + final int packageSnoozeLengthMs = Settings.Global.getInt( + mContext.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); + if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { + mSnoozeLengthMs = packageSnoozeLengthMs; + if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); + } + } + }; + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false, + mSettingsObserver); + if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); + if (mHeadsUp != null) { // whoops, we're on already! showNotification(mHeadsUp); @@ -219,14 +281,18 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. } @Override + protected void onDetachedFromWindow() { + mContext.getContentResolver().unregisterContentObserver(mSettingsObserver); + } + + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); - if (System.currentTimeMillis() < mStartTouchTime) { + if (SystemClock.elapsedRealtime() < mStartTouchTime) { return true; } return mEdgeSwipeHelper.onInterceptTouchEvent(ev) || mSwipeHelper.onInterceptTouchEvent(ev) - || mExpandHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); } @@ -248,13 +314,12 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. @Override public boolean onTouchEvent(MotionEvent ev) { - if (System.currentTimeMillis() < mStartTouchTime) { + if (SystemClock.elapsedRealtime() < mStartTouchTime) { return false; } mBar.resetHeadsUpDecayTimer(); return mEdgeSwipeHelper.onTouchEvent(ev) || mSwipeHelper.onTouchEvent(ev) - || mExpandHelper.onTouchEvent(ev) || super.onTouchEvent(ev); } @@ -373,6 +438,10 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. return mHeadsUp == null ? null : mHeadsUp.notification.getKey(); } + public void setUser(int user) { + mUser = user; + } + private class EdgeSwipeHelper implements Gefingerpoken { private static final boolean DEBUG_EDGE_SWIPE = false; private final float mTouchSlop; @@ -399,15 +468,12 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. final float dY = ev.getY() - mFirstY; final float daX = Math.abs(ev.getX() - mFirstX); final float daY = Math.abs(dY); - if (!mConsuming && (4f * daX) < daY && daY > mTouchSlop) { + if (!mConsuming && daX < daY && daY > mTouchSlop) { + snooze(); if (dY > 0) { if (DEBUG_EDGE_SWIPE) Log.d(TAG, "found an open"); mBar.animateExpandNotificationsPanel(); } - if (dY < 0) { - if (DEBUG_EDGE_SWIPE) Log.d(TAG, "found a close"); - mBar.onHeadsUpDismissed(); - } mConsuming = true; } break; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java index 63c1100..5eff5a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.policy; import android.app.ActivityManager; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -35,6 +36,13 @@ public class HotspotControllerImpl implements HotspotController { private static final String TAG = "HotspotController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + // Keep these in sync with Settings TetherService.java + public static final String EXTRA_ADD_TETHER_TYPE = "extraAddTetherType"; + public static final String EXTRA_SET_ALARM = "extraSetAlarm"; + public static final String EXTRA_RUN_PROVISION = "extraRunProvision"; + public static final String EXTRA_ENABLE_WIFI_TETHER = "extraEnableWifiTether"; + // Keep this in sync with Settings TetherSettings.java + public static final int WIFI_TETHERING = 0; private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); private final Receiver mReceiver = new Receiver(); @@ -91,20 +99,29 @@ public class HotspotControllerImpl implements HotspotController { @Override public void setHotspotEnabled(boolean enabled) { final ContentResolver cr = mContext.getContentResolver(); - // This needs to be kept up to date with Settings (WifiApEnabler.setSoftapEnabled) - // in case it is turned on in settings and off in qs (or vice versa). - // Disable Wifi if enabling tethering. - int wifiState = mWifiManager.getWifiState(); - if (enabled && ((wifiState == WifiManager.WIFI_STATE_ENABLING) || - (wifiState == WifiManager.WIFI_STATE_ENABLED))) { - mWifiManager.setWifiEnabled(false); - Settings.Global.putInt(cr, Settings.Global.WIFI_SAVED_STATE, 1); - } - - mWifiManager.setWifiApEnabled(null, enabled); - - // If needed, restore Wifi on tether disable. - if (!enabled) { + // Call provisioning app which is called when enabling Tethering from Settings + if (enabled) { + if (isProvisioningNeeded()) { + String tetherEnable = mContext.getResources().getString( + com.android.internal.R.string.config_wifi_tether_enable); + Intent intent = new Intent(); + intent.putExtra(EXTRA_ADD_TETHER_TYPE, WIFI_TETHERING); + intent.putExtra(EXTRA_SET_ALARM, true); + intent.putExtra(EXTRA_RUN_PROVISION, true); + intent.putExtra(EXTRA_ENABLE_WIFI_TETHER, true); + intent.setComponent(ComponentName.unflattenFromString(tetherEnable)); + mContext.startServiceAsUser(intent, UserHandle.CURRENT); + } else { + int wifiState = mWifiManager.getWifiState(); + if ((wifiState == WifiManager.WIFI_STATE_ENABLING) || + (wifiState == WifiManager.WIFI_STATE_ENABLED)) { + mWifiManager.setWifiEnabled(false); + Settings.Global.putInt(cr, Settings.Global.WIFI_SAVED_STATE, 1); + } + mWifiManager.setWifiApEnabled(null, true); + } + } else { + mWifiManager.setWifiApEnabled(null, false); if (Settings.Global.getInt(cr, Settings.Global.WIFI_SAVED_STATE, 0) == 1) { mWifiManager.setWifiEnabled(true); Settings.Global.putInt(cr, Settings.Global.WIFI_SAVED_STATE, 0); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java index a3765aa..6998791 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java @@ -102,7 +102,6 @@ public class KeyButtonRipple extends Drawable { } } - @Override public void draw(Canvas canvas) { mSupportHardware = canvas.isHardwareAccelerated(); @@ -176,6 +175,11 @@ public class KeyButtonRipple extends Drawable { } @Override + public void jumpToCurrentState() { + cancelAnimations(); + } + + @Override public boolean isStateful() { return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java index 7cc75da..b9cc0f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java @@ -17,38 +17,27 @@ package com.android.systemui.statusbar.policy; import android.animation.Animator; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; -import android.animation.TimeInterpolator; import android.app.ActivityManager; import android.content.Context; import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.CanvasProperty; -import android.graphics.Paint; -import android.graphics.RectF; import android.hardware.input.InputManager; import android.media.AudioManager; import android.os.Bundle; import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; -import android.util.MathUtils; import android.view.HapticFeedbackConstants; -import android.view.HardwareCanvas; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; -import android.view.RenderNodeAnimator; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; -import java.lang.Math; -import java.util.ArrayList; import com.android.systemui.R; @@ -124,6 +113,14 @@ public class KeyButtonView extends ImageView { } @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + if (visibility != View.VISIBLE) { + jumpDrawablesToCurrentState(); + } + } + + @Override public boolean performAccessibilityAction(int action, Bundle arguments) { if (action == ACTION_CLICK && mCode != 0) { sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java index 8f0000f..1460e5f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java @@ -19,15 +19,16 @@ package com.android.systemui.statusbar.policy; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.content.Context; import android.database.DataSetObserver; +import android.util.AttributeSet; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.view.animation.AnimationUtils; -import android.widget.TextView; +import android.widget.FrameLayout; import com.android.keyguard.AppearAnimationUtils; import com.android.systemui.R; @@ -35,8 +36,6 @@ import com.android.systemui.qs.tiles.UserDetailItemView; import com.android.systemui.statusbar.phone.KeyguardStatusBarView; import com.android.systemui.statusbar.phone.NotificationPanelView; import com.android.systemui.statusbar.phone.PhoneStatusBar; -import com.android.systemui.statusbar.phone.StatusBarHeaderView; -import com.android.systemui.statusbar.phone.UserAvatarView; /** * Manages the user switcher on the Keyguard. @@ -46,6 +45,7 @@ public class KeyguardUserSwitcher { private static final String TAG = "KeyguardUserSwitcher"; private static final boolean ALWAYS_ON = false; + private final Container mUserSwitcherContainer; private final ViewGroup mUserSwitcher; private final KeyguardStatusBarView mStatusBarView; private final Adapter mAdapter; @@ -53,24 +53,31 @@ public class KeyguardUserSwitcher { private final KeyguardUserSwitcherScrim mBackground; private ObjectAnimator mBgAnimator; private UserSwitcherController mUserSwitcherController; + private boolean mAnimating; public KeyguardUserSwitcher(Context context, ViewStub userSwitcher, KeyguardStatusBarView statusBarView, NotificationPanelView panelView, UserSwitcherController userSwitcherController) { - if (context.getResources().getBoolean(R.bool.config_keyguardUserSwitcher) || ALWAYS_ON) { - mUserSwitcher = (ViewGroup) userSwitcher.inflate(); + boolean keyguardUserSwitcherEnabled = + context.getResources().getBoolean(R.bool.config_keyguardUserSwitcher) || ALWAYS_ON; + if (userSwitcherController != null && keyguardUserSwitcherEnabled) { + mUserSwitcherContainer = (Container) userSwitcher.inflate(); + mUserSwitcher = (ViewGroup) + mUserSwitcherContainer.findViewById(R.id.keyguard_user_switcher_inner); mBackground = new KeyguardUserSwitcherScrim(mUserSwitcher); mUserSwitcher.setBackground(mBackground); mStatusBarView = statusBarView; mStatusBarView.setKeyguardUserSwitcher(this); panelView.setKeyguardUserSwitcher(this); - mAdapter = new Adapter(context, userSwitcherController); + mAdapter = new Adapter(context, userSwitcherController, this); mAdapter.registerDataSetObserver(mDataSetObserver); mUserSwitcherController = userSwitcherController; mAppearAnimationUtils = new AppearAnimationUtils(context, 400, -0.5f, 0.5f, AnimationUtils.loadInterpolator( context, android.R.interpolator.fast_out_slow_in)); + mUserSwitcherContainer.setKeyguardUserSwitcher(this); } else { + mUserSwitcherContainer = null; mUserSwitcher = null; mStatusBarView = null; mAdapter = null; @@ -98,9 +105,10 @@ public class KeyguardUserSwitcher { } public void show(boolean animate) { - if (mUserSwitcher != null && mUserSwitcher.getVisibility() != View.VISIBLE) { + if (mUserSwitcher != null && mUserSwitcherContainer.getVisibility() != View.VISIBLE) { cancelAnimations(); - mUserSwitcher.setVisibility(View.VISIBLE); + mAdapter.refresh(); + mUserSwitcherContainer.setVisibility(View.VISIBLE); mStatusBarView.setKeyguardUserSwitcherShowing(true, animate); if (animate) { startAppearAnimation(); @@ -108,13 +116,13 @@ public class KeyguardUserSwitcher { } } - public void hide(boolean animate) { - if (mUserSwitcher != null && mUserSwitcher.getVisibility() == View.VISIBLE) { + private void hide(boolean animate) { + if (mUserSwitcher != null && mUserSwitcherContainer.getVisibility() == View.VISIBLE) { cancelAnimations(); if (animate) { startDisappearAnimation(); } else { - mUserSwitcher.setVisibility(View.GONE); + mUserSwitcherContainer.setVisibility(View.GONE); } mStatusBarView.setKeyguardUserSwitcherShowing(false, animate); } @@ -129,6 +137,7 @@ public class KeyguardUserSwitcher { mBgAnimator.cancel(); } mUserSwitcher.animate().cancel(); + mAnimating = false; } private void startAppearAnimation() { @@ -139,13 +148,14 @@ public class KeyguardUserSwitcher { } mUserSwitcher.setClipChildren(false); mUserSwitcher.setClipToPadding(false); - mAppearAnimationUtils.startAppearAnimation(objects, new Runnable() { + mAppearAnimationUtils.startAnimation(objects, new Runnable() { @Override public void run() { mUserSwitcher.setClipChildren(true); mUserSwitcher.setClipToPadding(true); } }); + mAnimating = true; mBgAnimator = ObjectAnimator.ofInt(mBackground, "alpha", 0, 255); mBgAnimator.setDuration(400); mBgAnimator.setInterpolator(PhoneStatusBar.ALPHA_IN); @@ -153,12 +163,14 @@ public class KeyguardUserSwitcher { @Override public void onAnimationEnd(Animator animation) { mBgAnimator = null; + mAnimating = false; } }); mBgAnimator.start(); } private void startDisappearAnimation() { + mAnimating = true; mUserSwitcher.animate() .alpha(0f) .setDuration(300) @@ -166,8 +178,9 @@ public class KeyguardUserSwitcher { .withEndAction(new Runnable() { @Override public void run() { - mUserSwitcher.setVisibility(View.GONE); + mUserSwitcherContainer.setVisibility(View.GONE); mUserSwitcher.setAlpha(1f); + mAnimating = false; } }); } @@ -198,6 +211,16 @@ public class KeyguardUserSwitcher { } } + public void hideIfNotSimple(boolean animate) { + if (mUserSwitcherContainer != null && !mUserSwitcherController.isSimpleUserSwitcher()) { + hide(animate); + } + } + + boolean isAnimating() { + return mAnimating; + } + public final DataSetObserver mDataSetObserver = new DataSetObserver() { @Override public void onChanged() { @@ -209,10 +232,13 @@ public class KeyguardUserSwitcher { View.OnClickListener { private Context mContext; + private KeyguardUserSwitcher mKeyguardUserSwitcher; - public Adapter(Context context, UserSwitcherController controller) { + public Adapter(Context context, UserSwitcherController controller, + KeyguardUserSwitcher kgu) { super(controller); mContext = context; + mKeyguardUserSwitcher = kgu; } @Override @@ -240,7 +266,37 @@ public class KeyguardUserSwitcher { @Override public void onClick(View v) { - switchTo(((UserSwitcherController.UserRecord)v.getTag())); + UserSwitcherController.UserRecord user = (UserSwitcherController.UserRecord) v.getTag(); + if (user.isCurrent && !user.isGuest) { + // Close the switcher if tapping the current user. Guest is excluded because + // tapping the guest user while it's current clears the session. + mKeyguardUserSwitcher.hideIfNotSimple(true /* animate */); + } else { + switchTo(user); + } + } + } + + public static class Container extends FrameLayout { + + private KeyguardUserSwitcher mKeyguardUserSwitcher; + + public Container(Context context, AttributeSet attrs) { + super(context, attrs); + setClipChildren(false); + } + + public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) { + mKeyguardUserSwitcher = keyguardUserSwitcher; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + // Hide switcher if it didn't handle the touch event (and let the event go through). + if (mKeyguardUserSwitcher != null && !mKeyguardUserSwitcher.isAnimating()) { + mKeyguardUserSwitcher.hideIfNotSimple(true /* animate */); + } + return false; } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java index 4363037..a5fc2fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java @@ -42,7 +42,7 @@ public class KeyguardUserSwitcherScrim extends Drawable private int mDarkColor; private int mTop; - private int mAlpha; + private int mAlpha = 255; private Paint mRadialGradientPaint = new Paint(); private int mLayoutWidth; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileDataController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileDataControllerImpl.java index 33d68bf..f2b2f66 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileDataController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileDataControllerImpl.java @@ -33,17 +33,16 @@ import android.net.NetworkStatsHistory; import android.net.NetworkTemplate; import android.os.RemoteException; import android.os.ServiceManager; +import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.format.DateUtils; import android.text.format.Time; import android.util.Log; -import com.android.systemui.statusbar.policy.NetworkController.DataUsageInfo; - import java.util.Date; import java.util.Locale; -public class MobileDataController { +public class MobileDataControllerImpl implements NetworkController.MobileDataController { private static final String TAG = "MobileDataController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -61,8 +60,9 @@ public class MobileDataController { private INetworkStatsSession mSession; private Callback mCallback; + private NetworkControllerImpl mNetworkController; - public MobileDataController(Context context) { + public MobileDataControllerImpl(Context context) { mContext = context; mTelephonyManager = TelephonyManager.from(context); mConnectivityManager = ConnectivityManager.from(context); @@ -71,6 +71,10 @@ public class MobileDataController { mPolicyManager = NetworkPolicyManager.from(mContext); } + public void setNetworkController(NetworkControllerImpl networkController) { + mNetworkController = networkController; + } + private INetworkStatsSession getSession() { if (mSession == null) { try { @@ -109,7 +113,9 @@ public class MobileDataController { if (session == null) { return warn("no stats session"); } - final NetworkTemplate template = NetworkTemplate.buildTemplateMobileAll(subscriberId); + NetworkTemplate template = NetworkTemplate.buildTemplateMobileAll(subscriberId); + template = NetworkTemplate.normalize(template, mTelephonyManager.getMergedSubscriberIds()); + final NetworkPolicy policy = findNetworkPolicy(template); try { final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELDS); @@ -155,6 +161,9 @@ public class MobileDataController { } else { usage.warningLevel = DEFAULT_WARNING_LEVEL; } + if (usage != null) { + usage.carrier = mNetworkController.getMobileNetworkName(); + } return usage; } catch (RemoteException e) { return warn("remote call failed"); @@ -189,6 +198,7 @@ public class MobileDataController { } public void setMobileDataEnabled(boolean enabled) { + Log.d(TAG, "setMobileDataEnabled: enabled=" + enabled); mTelephonyManager.setDataEnabled(enabled); if (mCallback != null) { mCallback.onMobileDataEnabled(enabled); @@ -207,7 +217,8 @@ public class MobileDataController { private static String getActiveSubscriberId(Context context) { final TelephonyManager tele = TelephonyManager.from(context); - final String actualSubscriberId = tele.getSubscriberId(); + final String actualSubscriberId = tele.getSubscriberId( + SubscriptionManager.getDefaultDataSubId()); return actualSubscriberId; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java index 2ed9366..3cffc85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java @@ -16,12 +16,17 @@ package com.android.systemui.statusbar.policy; +import android.content.Intent; + public interface NetworkController { boolean hasMobileDataFeature(); void addNetworkSignalChangedCallback(NetworkSignalChangedCallback cb); void removeNetworkSignalChangedCallback(NetworkSignalChangedCallback cb); void setWifiEnabled(boolean enabled); + void onUserSwitched(int newUserId); + AccessPointController getAccessPointController(); + MobileDataController getMobileDataController(); public interface NetworkSignalChangedCallback { void onWifiSignalChanged(boolean enabled, boolean connected, int wifiSignalIconId, @@ -30,40 +35,57 @@ public interface NetworkController { void onMobileDataSignalChanged(boolean enabled, int mobileSignalIconId, String mobileSignalContentDescriptionId, int dataTypeIconId, boolean activityIn, boolean activityOut, - String dataTypeContentDescriptionId, String description, boolean noSim, + String dataTypeContentDescriptionId, String description, boolean isDataTypeIconWide); + void onNoSimVisibleChanged(boolean visible); void onAirplaneModeChanged(boolean enabled); void onMobileDataEnabled(boolean enabled); } - void addAccessPointCallback(AccessPointCallback callback); - void removeAccessPointCallback(AccessPointCallback callback); - void scanForAccessPoints(); - void connect(AccessPoint ap); - boolean isMobileDataSupported(); - boolean isMobileDataEnabled(); - void setMobileDataEnabled(boolean enabled); - DataUsageInfo getDataUsageInfo(); + /** + * Tracks changes in access points. Allows listening for changes, scanning for new APs, + * and connecting to new ones. + */ + public interface AccessPointController { + void addAccessPointCallback(AccessPointCallback callback); + void removeAccessPointCallback(AccessPointCallback callback); + void scanForAccessPoints(); + boolean connect(AccessPoint ap); + boolean canConfigWifi(); - public interface AccessPointCallback { - void onAccessPointsChanged(AccessPoint[] accessPoints); - } + public interface AccessPointCallback { + void onAccessPointsChanged(AccessPoint[] accessPoints); + void onSettingsActivityTriggered(Intent settingsIntent); + } - public static class AccessPoint { - public static final int NO_NETWORK = -1; // see WifiManager + public static class AccessPoint { + public static final int NO_NETWORK = -1; // see WifiManager - public int networkId; - public int iconId; - public String ssid; - public boolean isConnected; - public int level; // 0 - 5 + public int networkId; + public int iconId; + public String ssid; + public boolean isConnected; + public boolean isConfigured; + public boolean hasSecurity; + public int level; // 0 - 5 + } } - public static class DataUsageInfo { - public String carrier; - public String period; - public long limitLevel; - public long warningLevel; - public long usageLevel; + /** + * Tracks mobile data support and usage. + */ + public interface MobileDataController { + boolean isMobileDataSupported(); + boolean isMobileDataEnabled(); + void setMobileDataEnabled(boolean enabled); + DataUsageInfo getDataUsageInfo(); + + public static class DataUsageInfo { + public String carrier; + public String period; + public long limitLevel; + public long warningLevel; + public long usageLevel; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java index 3625997..9a7f21e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -16,17 +16,22 @@ package com.android.systemui.statusbar.policy; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import android.net.wimax.WimaxManagerConstants; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; @@ -36,185 +41,161 @@ import android.provider.Settings; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.SignalStrength; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.text.format.DateFormat; import android.util.Log; -import android.view.View; -import android.widget.TextView; +import android.util.SparseArray; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.cdma.EriInfo; import com.android.internal.util.AsyncChannel; import com.android.systemui.DemoMode; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.StatusBarHeaderView; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Objects; /** Platform implementation of the network controller. **/ public class NetworkControllerImpl extends BroadcastReceiver implements NetworkController, DemoMode { // debug - static final String TAG = "StatusBar.NetworkController"; - static final boolean DEBUG = false; - static final boolean CHATTY = false; // additional diagnostics, but not logspew - - // telephony - boolean mHspaDataDistinguishable; - final TelephonyManager mPhone; - boolean mDataConnected; - IccCardConstants.State mSimState = IccCardConstants.State.READY; - int mPhoneState = TelephonyManager.CALL_STATE_IDLE; - int mDataNetType = TelephonyManager.NETWORK_TYPE_UNKNOWN; - int mDataState = TelephonyManager.DATA_DISCONNECTED; - int mDataActivity = TelephonyManager.DATA_ACTIVITY_NONE; - ServiceState mServiceState; - SignalStrength mSignalStrength; - int[] mDataIconList = TelephonyIcons.DATA_G[0]; - String mNetworkName; - String mNetworkNameDefault; - String mNetworkNameSeparator; - int mPhoneSignalIconId; - int mQSPhoneSignalIconId; - int mDataDirectionIconId; // data + data direction on phones - int mDataSignalIconId; - int mDataTypeIconId; - int mQSDataTypeIconId; - int mAirplaneIconId; - boolean mDataActive; - boolean mNoSim; - int mLastSignalLevel; - boolean mShowPhoneRSSIForData = false; - boolean mShowAtLeastThreeGees = false; - boolean mAlwaysShowCdmaRssi = false; - - String mContentDescriptionPhoneSignal; - String mContentDescriptionWifi; - String mContentDescriptionWimax; - String mContentDescriptionCombinedSignal; - String mContentDescriptionDataType; - - // wifi - final WifiManager mWifiManager; - AsyncChannel mWifiChannel; - boolean mWifiEnabled, mWifiConnected; - int mWifiRssi, mWifiLevel; - String mWifiSsid; - int mWifiIconId = 0; - int mQSWifiIconId = 0; - int mWifiActivity = WifiManager.DATA_ACTIVITY_NONE; - - // bluetooth + static final String TAG = "NetworkController"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + // additional diagnostics, but not logspew + static final boolean CHATTY = Log.isLoggable(TAG + ".Chat", Log.DEBUG); + // Save the previous SignalController.States of all SignalControllers for dumps. + static final boolean RECORD_HISTORY = true; + // If RECORD_HISTORY how many to save, must be a power of 2. + static final int HISTORY_SIZE = 16; + + private static final int INET_CONDITION_THRESHOLD = 50; + + private final Context mContext; + private final TelephonyManager mPhone; + private final WifiManager mWifiManager; + private final ConnectivityManager mConnectivityManager; + private final SubscriptionManager mSubscriptionManager; + private final boolean mHasMobileDataFeature; + private Config mConfig; + + // Subcontrollers. + @VisibleForTesting + final WifiSignalController mWifiSignalController; + @VisibleForTesting + final Map<Integer, MobileSignalController> mMobileSignalControllers = + new HashMap<Integer, MobileSignalController>(); + // When no SIMs are around at setup, and one is added later, it seems to default to the first + // SIM for most actions. This may be null if there aren't any SIMs around. + private MobileSignalController mDefaultSignalController; + private final AccessPointControllerImpl mAccessPoints; + private final MobileDataControllerImpl mMobileDataController; + + // Network types that replace the carrier label if the device does not support mobile data. private boolean mBluetoothTethered = false; - private int mBluetoothTetherIconId = - com.android.internal.R.drawable.stat_sys_tether_bluetooth; - - //wimax - private boolean mWimaxSupported = false; - private boolean mIsWimaxEnabled = false; - private boolean mWimaxConnected = false; - private boolean mWimaxIdle = false; - private int mWimaxIconId = 0; - private int mWimaxSignal = 0; - private int mWimaxState = 0; - private int mWimaxExtraState = 0; - - // data connectivity (regardless of state, can we access the internet?) - // state of inet connection - 0 not connected, 100 connected + private boolean mEthernetConnected = false; + + // state of inet connection private boolean mConnected = false; - private int mConnectedNetworkType = ConnectivityManager.TYPE_NONE; - private String mConnectedNetworkTypeName; - private int mLastConnectedNetworkType = ConnectivityManager.TYPE_NONE; + private boolean mInetCondition; // Used for Logging and demo. - private int mInetCondition = 0; - private int mLastInetCondition = 0; - private static final int INET_CONDITION_THRESHOLD = 50; + // BitSets indicating which network transport types (e.g., TRANSPORT_WIFI, TRANSPORT_MOBILE) are + // connected and validated, respectively. + private final BitSet mConnectedTransports = new BitSet(); + private final BitSet mValidatedTransports = new BitSet(); + // States that don't belong to a subcontroller. private boolean mAirplaneMode = false; - private boolean mLastAirplaneMode = true; - + private boolean mHasNoSims; private Locale mLocale = null; - private Locale mLastLocale = null; - - // our ui - Context mContext; - ArrayList<TextView> mCombinedLabelViews = new ArrayList<TextView>(); - ArrayList<TextView> mMobileLabelViews = new ArrayList<TextView>(); - ArrayList<TextView> mWifiLabelViews = new ArrayList<TextView>(); - ArrayList<StatusBarHeaderView> mEmergencyViews = new ArrayList<>(); - ArrayList<SignalCluster> mSignalClusters = new ArrayList<SignalCluster>(); - ArrayList<NetworkSignalChangedCallback> mSignalsChangedCallbacks = + // This list holds our ordering. + private List<SubscriptionInfo> mCurrentSubscriptions + = new ArrayList<SubscriptionInfo>(); + + // All the callbacks. + private ArrayList<EmergencyListener> mEmergencyListeners = new ArrayList<EmergencyListener>(); + private ArrayList<CarrierLabelListener> mCarrierListeners = + new ArrayList<CarrierLabelListener>(); + private ArrayList<SignalCluster> mSignalClusters = new ArrayList<SignalCluster>(); + private ArrayList<NetworkSignalChangedCallback> mSignalsChangedCallbacks = new ArrayList<NetworkSignalChangedCallback>(); - int mLastPhoneSignalIconId = -1; - int mLastDataDirectionIconId = -1; - int mLastWifiIconId = -1; - int mLastWimaxIconId = -1; - int mLastCombinedSignalIconId = -1; - int mLastDataTypeIconId = -1; - String mLastCombinedLabel = ""; - - private boolean mHasMobileDataFeature; + @VisibleForTesting + boolean mListening; - boolean mDataAndWifiStacked = false; - - public interface SignalCluster { - void setWifiIndicators(boolean visible, int strengthIcon, String contentDescription); - void setMobileDataIndicators(boolean visible, int strengthIcon, int typeIcon, - String contentDescription, String typeContentDescription, boolean roaming, - boolean isTypeIconWide); - void setIsAirplaneMode(boolean is, int airplaneIcon); - } - - private final WifiAccessPointController mAccessPoints; - private final MobileDataController mMobileDataController; + // The current user ID. + private int mCurrentUserId; /** * Construct this controller object and register for updates. */ public NetworkControllerImpl(Context context) { - mContext = context; - final Resources res = context.getResources(); - - ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService( - Context.CONNECTIVITY_SERVICE); - mHasMobileDataFeature = cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE); + this(context, (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE), + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE), + (WifiManager) context.getSystemService(Context.WIFI_SERVICE), + SubscriptionManager.from(context), Config.readConfig(context), + new AccessPointControllerImpl(context), new MobileDataControllerImpl(context)); + registerListeners(); + } - mShowPhoneRSSIForData = res.getBoolean(R.bool.config_showPhoneRSSIForData); - mShowAtLeastThreeGees = res.getBoolean(R.bool.config_showMin3G); - mAlwaysShowCdmaRssi = res.getBoolean( - com.android.internal.R.bool.config_alwaysUseCdmaRssi); + @VisibleForTesting + NetworkControllerImpl(Context context, ConnectivityManager connectivityManager, + TelephonyManager telephonyManager, WifiManager wifiManager, + SubscriptionManager subManager, Config config, + AccessPointControllerImpl accessPointController, + MobileDataControllerImpl mobileDataController) { + mContext = context; + mConfig = config; - // set up the default wifi icon, used when no radios have ever appeared - updateWifiIcons(); - updateWimaxIcons(); + mSubscriptionManager = subManager; + mConnectivityManager = connectivityManager; + mHasMobileDataFeature = + mConnectivityManager.isNetworkSupported(ConnectivityManager.TYPE_MOBILE); // telephony - mPhone = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); - mPhone.listen(mPhoneStateListener, - PhoneStateListener.LISTEN_SERVICE_STATE - | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS - | PhoneStateListener.LISTEN_CALL_STATE - | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE - | PhoneStateListener.LISTEN_DATA_ACTIVITY); - mHspaDataDistinguishable = mContext.getResources().getBoolean( - R.bool.config_hspa_data_distinguishable); - mNetworkNameSeparator = mContext.getString(R.string.status_bar_network_name_separator); - mNetworkNameDefault = mContext.getString( - com.android.internal.R.string.lockscreen_carrier_default); - mNetworkName = mNetworkNameDefault; + mPhone = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); // wifi - mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - Handler handler = new WifiHandler(); - mWifiChannel = new AsyncChannel(); - Messenger wifiMessenger = mWifiManager.getWifiServiceMessenger(); - if (wifiMessenger != null) { - mWifiChannel.connect(mContext, handler, wifiMessenger); + mWifiManager = wifiManager; + + mLocale = mContext.getResources().getConfiguration().locale; + mAccessPoints = accessPointController; + mMobileDataController = mobileDataController; + mMobileDataController.setNetworkController(this); + // TODO: Find a way to move this into MobileDataController. + mMobileDataController.setCallback(new MobileDataControllerImpl.Callback() { + @Override + public void onMobileDataEnabled(boolean enabled) { + notifyMobileDataEnabled(enabled); + } + }); + mWifiSignalController = new WifiSignalController(mContext, mHasMobileDataFeature, + mSignalsChangedCallbacks, mSignalClusters, this); + + // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it + updateAirplaneMode(true /* force callback */); + mAccessPoints.setNetworkController(this); + } + + private void registerListeners() { + for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { + mobileSignalController.registerListener(); } + mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionListener); // broadcasts IntentFilter filter = new IntentFilter(); @@ -222,37 +203,56 @@ public class NetworkControllerImpl extends BroadcastReceiver filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); + filter.addAction(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED); + filter.addAction(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED); filter.addAction(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE); filter.addAction(ConnectivityManager.INET_CONDITION_ACTION); filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); - mWimaxSupported = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_wimaxEnabled); - if(mWimaxSupported) { - filter.addAction(WimaxManagerConstants.WIMAX_NETWORK_STATE_CHANGED_ACTION); - filter.addAction(WimaxManagerConstants.SIGNAL_LEVEL_CHANGED_ACTION); - filter.addAction(WimaxManagerConstants.NET_4G_STATE_CHANGED_ACTION); + mContext.registerReceiver(this, filter); + mListening = true; + + updateMobileControllers(); + } + + private void unregisterListeners() { + mListening = false; + for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { + mobileSignalController.unregisterListener(); } - context.registerReceiver(this, filter); + mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener); + mContext.unregisterReceiver(this); + } - // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it - updateAirplaneMode(); + public int getConnectedWifiLevel() { + return mWifiSignalController.getState().level; + } - mLastLocale = mContext.getResources().getConfiguration().locale; - mAccessPoints = new WifiAccessPointController(mContext); - mMobileDataController = new MobileDataController(mContext); - mMobileDataController.setCallback(new MobileDataController.Callback() { - @Override - public void onMobileDataEnabled(boolean enabled) { - notifyMobileDataEnabled(enabled); - } - }); + @Override + public AccessPointController getAccessPointController() { + return mAccessPoints; + } + + @Override + public MobileDataController getMobileDataController() { + return mMobileDataController; + } + + public void addEmergencyListener(EmergencyListener listener) { + mEmergencyListeners.add(listener); + listener.setEmergencyCallsOnly(isEmergencyOnly()); + } + + public void addCarrierLabel(CarrierLabelListener listener) { + mCarrierListeners.add(listener); + refreshCarrierLabel(); } private void notifyMobileDataEnabled(boolean enabled) { - for (NetworkSignalChangedCallback cb : mSignalsChangedCallbacks) { - cb.onMobileDataEnabled(enabled); + final int length = mSignalsChangedCallbacks.size(); + for (int i = 0; i < length; i++) { + mSignalsChangedCallbacks.get(i).onMobileDataEnabled(enabled); } } @@ -264,34 +264,77 @@ public class NetworkControllerImpl extends BroadcastReceiver return mPhone.getPhoneType() != TelephonyManager.PHONE_TYPE_NONE; } - public boolean isEmergencyOnly() { - return (mServiceState != null && mServiceState.isEmergencyOnly()); - } - - public void addCombinedLabelView(TextView v) { - mCombinedLabelViews.add(v); + private MobileSignalController getDataController() { + int dataSubId = SubscriptionManager.getDefaultDataSubId(); + if (!SubscriptionManager.isValidSubscriptionId(dataSubId)) { + if (DEBUG) Log.e(TAG, "No data sim selected"); + return mDefaultSignalController; + } + if (mMobileSignalControllers.containsKey(dataSubId)) { + return mMobileSignalControllers.get(dataSubId); + } + if (DEBUG) Log.e(TAG, "Cannot find controller for data sub: " + dataSubId); + return mDefaultSignalController; } - public void addMobileLabelView(TextView v) { - mMobileLabelViews.add(v); + public String getMobileNetworkName() { + MobileSignalController controller = getDataController(); + return controller != null ? controller.getState().networkName : ""; } - public void addWifiLabelView(TextView v) { - mWifiLabelViews.add(v); + public boolean isEmergencyOnly() { + int voiceSubId = SubscriptionManager.getDefaultVoiceSubId(); + if (!SubscriptionManager.isValidSubscriptionId(voiceSubId)) { + for (MobileSignalController mobileSignalController : + mMobileSignalControllers.values()) { + if (!mobileSignalController.isEmergencyOnly()) { + return false; + } + } + } + if (mMobileSignalControllers.containsKey(voiceSubId)) { + return mMobileSignalControllers.get(voiceSubId).isEmergencyOnly(); + } + if (DEBUG) Log.e(TAG, "Cannot find controller for voice sub: " + voiceSubId); + // Something is wrong, better assume we can't make calls... + return true; } - public void addEmergencyLabelView(StatusBarHeaderView v) { - mEmergencyViews.add(v); + /** + * Emergency status may have changed (triggered by MobileSignalController), + * so we should recheck and send out the state to listeners. + */ + void recalculateEmergency() { + final boolean emergencyOnly = isEmergencyOnly(); + final int length = mEmergencyListeners.size(); + for (int i = 0; i < length; i++) { + mEmergencyListeners.get(i).setEmergencyCallsOnly(emergencyOnly); + } + // If the emergency has a chance to change, then so does the carrier + // label. + refreshCarrierLabel(); } public void addSignalCluster(SignalCluster cluster) { mSignalClusters.add(cluster); - refreshSignalCluster(cluster); + cluster.setSubs(mCurrentSubscriptions); + cluster.setIsAirplaneMode(mAirplaneMode, TelephonyIcons.FLIGHT_MODE_ICON, + R.string.accessibility_airplane_mode); + cluster.setNoSims(mHasNoSims); + mWifiSignalController.notifyListeners(); + for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { + mobileSignalController.notifyListeners(); + } } public void addNetworkSignalChangedCallback(NetworkSignalChangedCallback cb) { mSignalsChangedCallbacks.add(cb); - notifySignalsChangedCallbacks(cb); + cb.onAirplaneModeChanged(mAirplaneMode); + cb.onNoSimVisibleChanged(mHasNoSims); + mWifiSignalController.notifyListeners(); + for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { + mobileSignalController.notifyListeners(); + } } public void removeNetworkSignalChangedCallback(NetworkSignalChangedCallback cb) { @@ -299,26 +342,6 @@ public class NetworkControllerImpl extends BroadcastReceiver } @Override - public void addAccessPointCallback(AccessPointCallback callback) { - mAccessPoints.addCallback(callback); - } - - @Override - public void removeAccessPointCallback(AccessPointCallback callback) { - mAccessPoints.removeCallback(callback); - } - - @Override - public void scanForAccessPoints() { - mAccessPoints.scan(); - } - - @Override - public void connect(AccessPoint ap) { - mAccessPoints.connect(ap); - } - - @Override public void setWifiEnabled(final boolean enabled) { new AsyncTask<Void, Void, Void>() { @Override @@ -326,7 +349,7 @@ public class NetworkControllerImpl extends BroadcastReceiver // Disable tethering if enabling Wifi final int wifiApState = mWifiManager.getWifiApState(); if (enabled && ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) || - (wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) { + (wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) { mWifiManager.setWifiApEnabled(null, false); } @@ -337,1280 +360,1464 @@ public class NetworkControllerImpl extends BroadcastReceiver } @Override - public DataUsageInfo getDataUsageInfo() { - final DataUsageInfo info = mMobileDataController.getDataUsageInfo(); - if (info != null) { - info.carrier = mNetworkName; - } - return info; - } - - @Override - public boolean isMobileDataSupported() { - return mMobileDataController.isMobileDataSupported(); - } - - @Override - public boolean isMobileDataEnabled() { - return mMobileDataController.isMobileDataEnabled(); + public void onUserSwitched(int newUserId) { + mCurrentUserId = newUserId; + mAccessPoints.onUserSwitched(newUserId); + updateConnectivity(); + refreshCarrierLabel(); } @Override - public void setMobileDataEnabled(boolean enabled) { - mMobileDataController.setMobileDataEnabled(enabled); - } - - private boolean isTypeIconWide(int iconId) { - return TelephonyIcons.ICON_LTE == iconId || TelephonyIcons.ICON_1X == iconId - || TelephonyIcons.ICON_3G == iconId || TelephonyIcons.ICON_4G == iconId; - } - - private boolean isQsTypeIconWide(int iconId) { - return TelephonyIcons.QS_ICON_LTE == iconId || TelephonyIcons.QS_ICON_1X == iconId - || TelephonyIcons.QS_ICON_3G == iconId || TelephonyIcons.QS_ICON_4G == iconId; - } - - public void refreshSignalCluster(SignalCluster cluster) { - if (mDemoMode) return; - cluster.setWifiIndicators( - // only show wifi in the cluster if connected or if wifi-only - mWifiEnabled && (mWifiConnected || !mHasMobileDataFeature), - mWifiIconId, - mContentDescriptionWifi); - - if (mIsWimaxEnabled && mWimaxConnected) { - // wimax is special - cluster.setMobileDataIndicators( - true, - mAlwaysShowCdmaRssi ? mPhoneSignalIconId : mWimaxIconId, - mDataTypeIconId, - mContentDescriptionWimax, - mContentDescriptionDataType, - mDataTypeIconId == TelephonyIcons.ROAMING_ICON, - false /* isTypeIconWide */ ); - } else { - // normal mobile data - cluster.setMobileDataIndicators( - mHasMobileDataFeature, - mShowPhoneRSSIForData ? mPhoneSignalIconId : mDataSignalIconId, - mDataTypeIconId, - mContentDescriptionPhoneSignal, - mContentDescriptionDataType, - mDataTypeIconId == TelephonyIcons.ROAMING_ICON, - isTypeIconWide(mDataTypeIconId)); - } - cluster.setIsAirplaneMode(mAirplaneMode, mAirplaneIconId); - } - - void notifySignalsChangedCallbacks(NetworkSignalChangedCallback cb) { - // only show wifi in the cluster if connected or if wifi-only - boolean wifiEnabled = mWifiEnabled && (mWifiConnected || !mHasMobileDataFeature); - String wifiDesc = wifiEnabled ? - mWifiSsid : null; - boolean wifiIn = wifiEnabled && mWifiSsid != null - && (mWifiActivity == WifiManager.DATA_ACTIVITY_INOUT - || mWifiActivity == WifiManager.DATA_ACTIVITY_IN); - boolean wifiOut = wifiEnabled && mWifiSsid != null - && (mWifiActivity == WifiManager.DATA_ACTIVITY_INOUT - || mWifiActivity == WifiManager.DATA_ACTIVITY_OUT); - cb.onWifiSignalChanged(mWifiEnabled, mWifiConnected, mQSWifiIconId, wifiIn, wifiOut, - mContentDescriptionWifi, wifiDesc); - - boolean mobileIn = mDataConnected && (mDataActivity == TelephonyManager.DATA_ACTIVITY_INOUT - || mDataActivity == TelephonyManager.DATA_ACTIVITY_IN); - boolean mobileOut = mDataConnected && (mDataActivity == TelephonyManager.DATA_ACTIVITY_INOUT - || mDataActivity == TelephonyManager.DATA_ACTIVITY_OUT); - if (isEmergencyOnly()) { - cb.onMobileDataSignalChanged(false, mQSPhoneSignalIconId, - mContentDescriptionPhoneSignal, mQSDataTypeIconId, mobileIn, mobileOut, - mContentDescriptionDataType, null, mNoSim, isQsTypeIconWide(mQSDataTypeIconId)); + public void onReceive(Context context, Intent intent) { + if (CHATTY) { + Log.d(TAG, "onReceive: intent=" + intent); + } + final String action = intent.getAction(); + if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE) || + action.equals(ConnectivityManager.INET_CONDITION_ACTION)) { + updateConnectivity(); + refreshCarrierLabel(); + } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) { + mConfig = Config.readConfig(mContext); + handleConfigurationChanged(); + } else if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { + refreshLocale(); + updateAirplaneMode(false); + refreshCarrierLabel(); + } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED)) { + // We are using different subs now, we might be able to make calls. + recalculateEmergency(); + } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) { + // Notify every MobileSignalController so they can know whether they are the + // data sim or not. + for (MobileSignalController controller : mMobileSignalControllers.values()) { + controller.handleBroadcast(intent); + } + } else if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) { + // Might have different subscriptions now. + updateMobileControllers(); } else { - if (mIsWimaxEnabled && mWimaxConnected) { - // Wimax is special - cb.onMobileDataSignalChanged(true, mQSPhoneSignalIconId, - mContentDescriptionPhoneSignal, mQSDataTypeIconId, mobileIn, mobileOut, - mContentDescriptionDataType, mNetworkName, mNoSim, - isQsTypeIconWide(mQSDataTypeIconId)); + int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + if (SubscriptionManager.isValidSubscriptionId(subId)) { + if (mMobileSignalControllers.containsKey(subId)) { + mMobileSignalControllers.get(subId).handleBroadcast(intent); + } else { + // Can't find this subscription... We must be out of date. + updateMobileControllers(); + } } else { - // Normal mobile data - cb.onMobileDataSignalChanged(mHasMobileDataFeature, mQSPhoneSignalIconId, - mContentDescriptionPhoneSignal, mQSDataTypeIconId, mobileIn, mobileOut, - mContentDescriptionDataType, mNetworkName, mNoSim, - isQsTypeIconWide(mQSDataTypeIconId)); + // No sub id, must be for the wifi. + mWifiSignalController.handleBroadcast(intent); } } - cb.onAirplaneModeChanged(mAirplaneMode); } - public void setStackedMode(boolean stacked) { - mDataAndWifiStacked = true; + @VisibleForTesting + void handleConfigurationChanged() { + for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { + mobileSignalController.setConfiguration(mConfig); + } + refreshLocale(); + refreshCarrierLabel(); } - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (action.equals(WifiManager.RSSI_CHANGED_ACTION) - || action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION) - || action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { - updateWifiState(intent); - refreshViews(); - } else if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) { - updateSimState(intent); - updateDataIcon(); - refreshViews(); - } else if (action.equals(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION)) { - updateNetworkName(intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_SPN, false), - intent.getStringExtra(TelephonyIntents.EXTRA_SPN), - intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_PLMN, false), - intent.getStringExtra(TelephonyIntents.EXTRA_PLMN)); - refreshViews(); - } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE) || - action.equals(ConnectivityManager.INET_CONDITION_ACTION)) { - updateConnectivity(intent); - refreshViews(); - } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) { - refreshLocale(); - refreshViews(); - } else if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { - refreshLocale(); - updateAirplaneMode(); - refreshViews(); - } else if (action.equals(WimaxManagerConstants.NET_4G_STATE_CHANGED_ACTION) || - action.equals(WimaxManagerConstants.SIGNAL_LEVEL_CHANGED_ACTION) || - action.equals(WimaxManagerConstants.WIMAX_NETWORK_STATE_CHANGED_ACTION)) { - updateWimaxState(intent); - refreshViews(); + private void updateMobileControllers() { + if (!mListening) { + return; + } + List<SubscriptionInfo> subscriptions = mSubscriptionManager.getActiveSubscriptionInfoList(); + if (subscriptions == null) { + subscriptions = Collections.emptyList(); } + // If there have been no relevant changes to any of the subscriptions, we can leave as is. + if (hasCorrectMobileControllers(subscriptions)) { + // Even if the controllers are correct, make sure we have the right no sims state. + // Such as on boot, don't need any controllers, because there are no sims, + // but we still need to update the no sim state. + updateNoSims(); + return; + } + setCurrentSubscriptions(subscriptions); + updateNoSims(); } - - // ===== Telephony ============================================================== - - PhoneStateListener mPhoneStateListener = new PhoneStateListener() { - @Override - public void onSignalStrengthsChanged(SignalStrength signalStrength) { - if (DEBUG) { - Log.d(TAG, "onSignalStrengthsChanged signalStrength=" + signalStrength + - ((signalStrength == null) ? "" : (" level=" + signalStrength.getLevel()))); - } - mSignalStrength = signalStrength; - updateTelephonySignalStrength(); - refreshViews(); + @VisibleForTesting + protected void updateNoSims() { + boolean hasNoSims = mHasMobileDataFeature && mMobileSignalControllers.size() == 0; + if (hasNoSims != mHasNoSims) { + mHasNoSims = hasNoSims; + notifyListeners(); } + } - @Override - public void onServiceStateChanged(ServiceState state) { - if (DEBUG) { - Log.d(TAG, "onServiceStateChanged voiceState=" + state.getVoiceRegState() - + " dataState=" + state.getDataRegState()); + @VisibleForTesting + void setCurrentSubscriptions(List<SubscriptionInfo> subscriptions) { + Collections.sort(subscriptions, new Comparator<SubscriptionInfo>() { + @Override + public int compare(SubscriptionInfo lhs, SubscriptionInfo rhs) { + return lhs.getSimSlotIndex() == rhs.getSimSlotIndex() + ? lhs.getSubscriptionId() - rhs.getSubscriptionId() + : lhs.getSimSlotIndex() - rhs.getSimSlotIndex(); } - mServiceState = state; - updateTelephonySignalStrength(); - updateDataNetType(); - updateDataIcon(); - refreshViews(); - } - - @Override - public void onCallStateChanged(int state, String incomingNumber) { - if (DEBUG) { - Log.d(TAG, "onCallStateChanged state=" + state); + }); + final int length = mSignalClusters.size(); + for (int i = 0; i < length; i++) { + mSignalClusters.get(i).setSubs(subscriptions); + } + mCurrentSubscriptions = subscriptions; + + HashMap<Integer, MobileSignalController> cachedControllers = + new HashMap<Integer, MobileSignalController>(mMobileSignalControllers); + mMobileSignalControllers.clear(); + final int num = subscriptions.size(); + for (int i = 0; i < num; i++) { + int subId = subscriptions.get(i).getSubscriptionId(); + // If we have a copy of this controller already reuse it, otherwise make a new one. + if (cachedControllers.containsKey(subId)) { + mMobileSignalControllers.put(subId, cachedControllers.remove(subId)); + } else { + MobileSignalController controller = new MobileSignalController(mContext, mConfig, + mHasMobileDataFeature, mPhone, mSignalsChangedCallbacks, mSignalClusters, + this, subscriptions.get(i)); + mMobileSignalControllers.put(subId, controller); + if (subscriptions.get(i).getSimSlotIndex() == 0) { + mDefaultSignalController = controller; + } + if (mListening) { + controller.registerListener(); + } } - // In cdma, if a voice call is made, RSSI should switch to 1x. - if (isCdma()) { - updateTelephonySignalStrength(); - refreshViews(); + } + if (mListening) { + for (Integer key : cachedControllers.keySet()) { + if (cachedControllers.get(key) == mDefaultSignalController) { + mDefaultSignalController = null; + } + cachedControllers.get(key).unregisterListener(); } } + // There may be new MobileSignalControllers around, make sure they get the current + // inet condition and airplane mode. + pushConnectivityToSignals(); + updateAirplaneMode(true /* force */); + } - @Override - public void onDataConnectionStateChanged(int state, int networkType) { - if (DEBUG) { - Log.d(TAG, "onDataConnectionStateChanged: state=" + state - + " type=" + networkType); + @VisibleForTesting + boolean hasCorrectMobileControllers(List<SubscriptionInfo> allSubscriptions) { + if (allSubscriptions.size() != mMobileSignalControllers.size()) { + return false; + } + for (SubscriptionInfo info : allSubscriptions) { + if (!mMobileSignalControllers.containsKey(info.getSubscriptionId())) { + return false; } - mDataState = state; - mDataNetType = networkType; - updateDataNetType(); - updateDataIcon(); - refreshViews(); } + return true; + } - @Override - public void onDataActivity(int direction) { - if (DEBUG) { - Log.d(TAG, "onDataActivity: direction=" + direction); + private void updateAirplaneMode(boolean force) { + boolean airplaneMode = (Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 0) == 1); + if (airplaneMode != mAirplaneMode || force) { + mAirplaneMode = airplaneMode; + for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { + mobileSignalController.setAirplaneMode(mAirplaneMode); } - mDataActivity = direction; - updateDataIcon(); - refreshViews(); + notifyListeners(); + refreshCarrierLabel(); } - }; + } - private final void updateSimState(Intent intent) { - String stateExtra = intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE); - if (IccCardConstants.INTENT_VALUE_ICC_ABSENT.equals(stateExtra)) { - mSimState = IccCardConstants.State.ABSENT; - } - else if (IccCardConstants.INTENT_VALUE_ICC_READY.equals(stateExtra)) { - mSimState = IccCardConstants.State.READY; + private void refreshLocale() { + Locale current = mContext.getResources().getConfiguration().locale; + if (!current.equals(mLocale)) { + mLocale = current; + notifyAllListeners(); } - else if (IccCardConstants.INTENT_VALUE_ICC_LOCKED.equals(stateExtra)) { - final String lockedReason = - intent.getStringExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON); - if (IccCardConstants.INTENT_VALUE_LOCKED_ON_PIN.equals(lockedReason)) { - mSimState = IccCardConstants.State.PIN_REQUIRED; - } - else if (IccCardConstants.INTENT_VALUE_LOCKED_ON_PUK.equals(lockedReason)) { - mSimState = IccCardConstants.State.PUK_REQUIRED; - } - else { - mSimState = IccCardConstants.State.NETWORK_LOCKED; - } - } else { - mSimState = IccCardConstants.State.UNKNOWN; + } + + /** + * Forces update of all callbacks on both SignalClusters and + * NetworkSignalChangedCallbacks. + */ + private void notifyAllListeners() { + notifyListeners(); + for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { + mobileSignalController.notifyListeners(); } - if (DEBUG) Log.d(TAG, "updateSimState: mSimState=" + mSimState); + mWifiSignalController.notifyListeners(); } - private boolean isCdma() { - return (mSignalStrength != null) && !mSignalStrength.isGsm(); + /** + * Notifies listeners of changes in state of to the NetworkController, but + * does not notify for any info on SignalControllers, for that call + * notifyAllListeners. + */ + private void notifyListeners() { + int length = mSignalClusters.size(); + for (int i = 0; i < length; i++) { + mSignalClusters.get(i).setIsAirplaneMode(mAirplaneMode, TelephonyIcons.FLIGHT_MODE_ICON, + R.string.accessibility_airplane_mode); + mSignalClusters.get(i).setNoSims(mHasNoSims); + } + int signalsChangedLength = mSignalsChangedCallbacks.size(); + for (int i = 0; i < signalsChangedLength; i++) { + mSignalsChangedCallbacks.get(i).onAirplaneModeChanged(mAirplaneMode); + mSignalsChangedCallbacks.get(i).onNoSimVisibleChanged(mHasNoSims); + } } - private boolean hasService() { - boolean retVal; - if (mServiceState != null) { - // Consider the device to be in service if either voice or data service is available. - // Some SIM cards are marketed as data-only and do not support voice service, and on - // these SIM cards, we want to show signal bars for data service as well as the "no - // service" or "emergency calls only" text that indicates that voice is not available. - switch(mServiceState.getVoiceRegState()) { - case ServiceState.STATE_POWER_OFF: - retVal = false; - break; - case ServiceState.STATE_OUT_OF_SERVICE: - case ServiceState.STATE_EMERGENCY_ONLY: - retVal = mServiceState.getDataRegState() == ServiceState.STATE_IN_SERVICE; - break; - default: - retVal = true; + /** + * Update the Inet conditions and what network we are connected to. + */ + private void updateConnectivity() { + mConnectedTransports.clear(); + mValidatedTransports.clear(); + for (NetworkCapabilities nc : + mConnectivityManager.getDefaultNetworkCapabilitiesForUser(mCurrentUserId)) { + for (int transportType : nc.getTransportTypes()) { + mConnectedTransports.set(transportType); + if (nc.hasCapability(NET_CAPABILITY_VALIDATED)) { + mValidatedTransports.set(transportType); + } } - } else { - retVal = false; } - if (DEBUG) Log.d(TAG, "hasService: mServiceState=" + mServiceState + " retVal=" + retVal); - return retVal; - } - private void updateAirplaneMode() { - mAirplaneMode = (Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.AIRPLANE_MODE_ON, 0) == 1); + if (CHATTY) { + Log.d(TAG, "updateConnectivity: mConnectedTransports=" + mConnectedTransports); + Log.d(TAG, "updateConnectivity: mValidatedTransports=" + mValidatedTransports); + } + + mConnected = !mConnectedTransports.isEmpty(); + mInetCondition = !mValidatedTransports.isEmpty(); + mBluetoothTethered = mConnectedTransports.get(TRANSPORT_BLUETOOTH); + mEthernetConnected = mConnectedTransports.get(TRANSPORT_ETHERNET); + + pushConnectivityToSignals(); } - private void refreshLocale() { - mLocale = mContext.getResources().getConfiguration().locale; + /** + * Pushes the current connectivity state to all SignalControllers. + */ + private void pushConnectivityToSignals() { + // We want to update all the icons, all at once, for any condition change + for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { + mobileSignalController.setInetCondition( + mInetCondition ? 1 : 0, + mValidatedTransports.get(mobileSignalController.getTransportType()) ? 1 : 0); + } + mWifiSignalController.setInetCondition( + mValidatedTransports.get(mWifiSignalController.getTransportType()) ? 1 : 0); } - private final void updateTelephonySignalStrength() { - if (DEBUG) { - Log.d(TAG, "updateTelephonySignalStrength: hasService=" + hasService() - + " ss=" + mSignalStrength); - } - if (!hasService()) { - if (CHATTY) Log.d(TAG, "updateTelephonySignalStrength: !hasService()"); - mPhoneSignalIconId = R.drawable.stat_sys_signal_null; - mQSPhoneSignalIconId = R.drawable.ic_qs_signal_no_signal; - mDataSignalIconId = R.drawable.stat_sys_signal_null; - mContentDescriptionPhoneSignal = mContext.getString( - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0]); - } else { - if (mSignalStrength == null) { - if (CHATTY) Log.d(TAG, "updateTelephonySignalStrength: mSignalStrength == null"); - mPhoneSignalIconId = R.drawable.stat_sys_signal_null; - mQSPhoneSignalIconId = R.drawable.ic_qs_signal_no_signal; - mDataSignalIconId = R.drawable.stat_sys_signal_null; - mContentDescriptionPhoneSignal = mContext.getString( - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0]); - } else { - int iconLevel; - int[] iconList; - if (isCdma() && mAlwaysShowCdmaRssi) { - mLastSignalLevel = iconLevel = mSignalStrength.getCdmaLevel(); - if (DEBUG) { - Log.d(TAG, "updateTelephonySignalStrength:" - + " mAlwaysShowCdmaRssi=" + mAlwaysShowCdmaRssi - + " set to cdmaLevel=" + mSignalStrength.getCdmaLevel() - + " instead of level=" + mSignalStrength.getLevel()); - } - } else { - mLastSignalLevel = iconLevel = mSignalStrength.getLevel(); - } + /** + * Recalculate and update the carrier label. + */ + void refreshCarrierLabel() { + Context context = mContext; - if (isRoaming()) { - iconList = TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH_ROAMING[mInetCondition]; - } else { - iconList = TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH[mInetCondition]; - } - mPhoneSignalIconId = iconList[iconLevel]; - mQSPhoneSignalIconId = - TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH[mInetCondition][iconLevel]; - mContentDescriptionPhoneSignal = mContext.getString( - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[iconLevel]); - mDataSignalIconId = TelephonyIcons.DATA_SIGNAL_STRENGTH[mInetCondition][iconLevel]; - if (DEBUG) Log.d(TAG, "updateTelephonySignalStrength: iconLevel=" + iconLevel); - } + WifiSignalController.WifiState wifiState = mWifiSignalController.getState(); + String label = ""; + for (MobileSignalController controller : mMobileSignalControllers.values()) { + label = controller.getLabel(label, mConnected, mHasMobileDataFeature); } - } - private int inetConditionForNetwork(int networkType) { - return (mInetCondition == 1 && mConnectedNetworkType == networkType) ? 1 : 0; - } + // TODO Simplify this ugliness, some of the flows below shouldn't be possible anymore + // but stay for the sake of history. + if (mBluetoothTethered && !mHasMobileDataFeature) { + label = mContext.getString(R.string.bluetooth_tethered); + } - private final void updateDataNetType() { - int inetCondition; - mDataTypeIconId = mQSDataTypeIconId = 0; - if (mIsWimaxEnabled && mWimaxConnected) { - // wimax is a special 4g network not handled by telephony - inetCondition = inetConditionForNetwork(ConnectivityManager.TYPE_WIMAX); - mDataIconList = TelephonyIcons.DATA_4G[inetCondition]; - mDataTypeIconId = R.drawable.stat_sys_data_fully_connected_4g; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_4G[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_4g); - } else { - inetCondition = inetConditionForNetwork(ConnectivityManager.TYPE_MOBILE); - final boolean showDataTypeIcon = (inetCondition > 0); - switch (mDataNetType) { - case TelephonyManager.NETWORK_TYPE_UNKNOWN: - if (!mShowAtLeastThreeGees) { - mDataIconList = TelephonyIcons.DATA_G[inetCondition]; - mContentDescriptionDataType = ""; - break; - } else { - // fall through - } - case TelephonyManager.NETWORK_TYPE_EDGE: - if (!mShowAtLeastThreeGees) { - mDataIconList = TelephonyIcons.DATA_E[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_e : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_E[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_edge); - break; - } else { - // fall through - } - case TelephonyManager.NETWORK_TYPE_UMTS: - mDataIconList = TelephonyIcons.DATA_3G[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_3g : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_3G[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_3g); - break; - case TelephonyManager.NETWORK_TYPE_HSDPA: - case TelephonyManager.NETWORK_TYPE_HSUPA: - case TelephonyManager.NETWORK_TYPE_HSPA: - case TelephonyManager.NETWORK_TYPE_HSPAP: - if (mHspaDataDistinguishable) { - mDataIconList = TelephonyIcons.DATA_H[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_h : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_H[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_3_5g); - } else { - mDataIconList = TelephonyIcons.DATA_3G[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_3g : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_3G[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_3g); - } - break; - case TelephonyManager.NETWORK_TYPE_CDMA: - if (!mShowAtLeastThreeGees) { - // display 1xRTT for IS95A/B - mDataIconList = TelephonyIcons.DATA_1X[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_1x : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_1X[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_cdma); - break; - } else { - // fall through - } - case TelephonyManager.NETWORK_TYPE_1xRTT: - if (!mShowAtLeastThreeGees) { - mDataIconList = TelephonyIcons.DATA_1X[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_1x : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_1X[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_cdma); - break; - } else { - // fall through - } - case TelephonyManager.NETWORK_TYPE_EVDO_0: //fall through - case TelephonyManager.NETWORK_TYPE_EVDO_A: - case TelephonyManager.NETWORK_TYPE_EVDO_B: - case TelephonyManager.NETWORK_TYPE_EHRPD: - mDataIconList = TelephonyIcons.DATA_3G[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_3g : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_3G[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_3g); - break; - case TelephonyManager.NETWORK_TYPE_LTE: - boolean show4GforLTE = mContext.getResources().getBoolean(R.bool.config_show4GForLTE); - if (show4GforLTE) { - mDataIconList = TelephonyIcons.DATA_4G[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_4g : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_4G[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_4g); - } else { - mDataIconList = TelephonyIcons.DATA_LTE[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? TelephonyIcons.ICON_LTE : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_LTE[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_lte); - } - break; - default: - if (!mShowAtLeastThreeGees) { - mDataIconList = TelephonyIcons.DATA_G[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_g : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_G[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_gprs); - } else { - mDataIconList = TelephonyIcons.DATA_3G[inetCondition]; - mDataTypeIconId = showDataTypeIcon ? - R.drawable.stat_sys_data_fully_connected_3g : 0; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_3G[inetCondition]; - mContentDescriptionDataType = mContext.getString( - R.string.accessibility_data_connection_3g); - } - break; + if (mEthernetConnected && !mHasMobileDataFeature) { + label = context.getString(R.string.ethernet_label); + } + + if (mAirplaneMode && !isEmergencyOnly()) { + // combined values from connected wifi take precedence over airplane mode + if (wifiState.connected && mHasMobileDataFeature) { + // Suppress "No internet connection." from mobile if wifi connected. + label = ""; + } else { + if (!mHasMobileDataFeature) { + label = context.getString( + R.string.status_bar_settings_signal_meter_disconnected); + } } + } else if (!isMobileDataConnected() && !wifiState.connected && !mBluetoothTethered && + !mEthernetConnected && !mHasMobileDataFeature) { + // Pretty much no connection. + label = context.getString(R.string.status_bar_settings_signal_meter_disconnected); } - if (isRoaming()) { - mDataTypeIconId = TelephonyIcons.ROAMING_ICON; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_R[mInetCondition]; + // for mobile devices, we always show mobile connection info here (SPN/PLMN) + // for other devices, we show whatever network is connected + // This is determined above by references to mHasMobileDataFeature. + int length = mCarrierListeners.size(); + for (int i = 0; i < length; i++) { + mCarrierListeners.get(i).setCarrierLabel(label); } } - boolean isCdmaEri() { - if (mServiceState != null) { - final int iconIndex = mServiceState.getCdmaEriIconIndex(); - if (iconIndex != EriInfo.ROAMING_INDICATOR_OFF) { - final int iconMode = mServiceState.getCdmaEriIconMode(); - if (iconMode == EriInfo.ROAMING_ICON_MODE_NORMAL - || iconMode == EriInfo.ROAMING_ICON_MODE_FLASH) { - return true; - } - } - } - return false; + private boolean isMobileDataConnected() { + MobileSignalController controller = getDataController(); + return controller != null ? controller.getState().dataConnected : false; } - private boolean isRoaming() { - if (isCdma()) { - return isCdmaEri(); - } else { - return mServiceState != null && mServiceState.getRoaming(); + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("NetworkController state:"); + + pw.println(" - telephony ------"); + pw.print(" hasVoiceCallingFeature()="); + pw.println(hasVoiceCallingFeature()); + + pw.println(" - Bluetooth ----"); + pw.print(" mBtReverseTethered="); + pw.println(mBluetoothTethered); + + pw.println(" - connectivity ------"); + pw.print(" mConnectedTransports="); + pw.println(mConnectedTransports); + pw.print(" mValidatedTransports="); + pw.println(mValidatedTransports); + pw.print(" mInetCondition="); + pw.println(mInetCondition); + pw.print(" mAirplaneMode="); + pw.println(mAirplaneMode); + pw.print(" mLocale="); + pw.println(mLocale); + + for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { + mobileSignalController.dump(pw); } + mWifiSignalController.dump(pw); } - private final void updateDataIcon() { - int iconId; - boolean visible = true; - - if (!isCdma()) { - // GSM case, we have to check also the sim state - if (mSimState == IccCardConstants.State.READY || - mSimState == IccCardConstants.State.UNKNOWN) { - mNoSim = false; - if (hasService() && mDataState == TelephonyManager.DATA_CONNECTED) { - switch (mDataActivity) { - case TelephonyManager.DATA_ACTIVITY_IN: - iconId = mDataIconList[1]; - break; - case TelephonyManager.DATA_ACTIVITY_OUT: - iconId = mDataIconList[2]; - break; - case TelephonyManager.DATA_ACTIVITY_INOUT: - iconId = mDataIconList[3]; - break; - default: - iconId = mDataIconList[0]; - break; + private boolean mDemoMode; + private int mDemoInetCondition; + private WifiSignalController.WifiState mDemoWifiState; + + @Override + public void dispatchDemoCommand(String command, Bundle args) { + if (!mDemoMode && command.equals(COMMAND_ENTER)) { + if (DEBUG) Log.d(TAG, "Entering demo mode"); + unregisterListeners(); + mDemoMode = true; + mDemoInetCondition = mInetCondition ? 1 : 0; + mDemoWifiState = mWifiSignalController.getState(); + } else if (mDemoMode && command.equals(COMMAND_EXIT)) { + if (DEBUG) Log.d(TAG, "Exiting demo mode"); + mDemoMode = false; + // Update what MobileSignalControllers, because they may change + // to set the number of sim slots. + updateMobileControllers(); + for (MobileSignalController controller : mMobileSignalControllers.values()) { + controller.resetLastState(); + } + mWifiSignalController.resetLastState(); + registerListeners(); + notifyAllListeners(); + refreshCarrierLabel(); + } else if (mDemoMode && command.equals(COMMAND_NETWORK)) { + String airplane = args.getString("airplane"); + if (airplane != null) { + boolean show = airplane.equals("show"); + int length = mSignalClusters.size(); + for (int i = 0; i < length; i++) { + mSignalClusters.get(i).setIsAirplaneMode(show, TelephonyIcons.FLIGHT_MODE_ICON, + R.string.accessibility_airplane_mode); + } + } + String fully = args.getString("fully"); + if (fully != null) { + mDemoInetCondition = Boolean.parseBoolean(fully) ? 1 : 0; + mWifiSignalController.setInetCondition(mDemoInetCondition); + for (MobileSignalController controller : mMobileSignalControllers.values()) { + controller.setInetCondition(mDemoInetCondition, mDemoInetCondition); + } + } + String wifi = args.getString("wifi"); + if (wifi != null) { + boolean show = wifi.equals("show"); + String level = args.getString("level"); + if (level != null) { + mDemoWifiState.level = level.equals("null") ? -1 + : Math.min(Integer.parseInt(level), WifiIcons.WIFI_LEVEL_COUNT - 1); + mDemoWifiState.connected = mDemoWifiState.level >= 0; + } + mDemoWifiState.enabled = show; + mWifiSignalController.notifyListeners(); + } + String sims = args.getString("sims"); + if (sims != null) { + int num = Integer.parseInt(sims); + List<SubscriptionInfo> subs = new ArrayList<SubscriptionInfo>(); + if (num != mMobileSignalControllers.size()) { + mMobileSignalControllers.clear(); + int start = mSubscriptionManager.getActiveSubscriptionInfoCountMax(); + for (int i = start /* get out of normal index range */; i < start + num; i++) { + SubscriptionInfo info = new SubscriptionInfo(i, "", i, "", "", 0, 0, "", 0, + null, 0, 0, ""); + subs.add(info); + mMobileSignalControllers.put(i, new MobileSignalController(mContext, + mConfig, mHasMobileDataFeature, mPhone, mSignalsChangedCallbacks, + mSignalClusters, this, info)); } - mDataDirectionIconId = iconId; - } else { - iconId = 0; - visible = false; } - } else { - iconId = 0; - mNoSim = true; - visible = false; // no SIM? no data + final int n = mSignalClusters.size(); + for (int i = 0; i < n; i++) { + mSignalClusters.get(i).setSubs(subs); + } } - } else { - // CDMA case, mDataActivity can be also DATA_ACTIVITY_DORMANT - if (hasService() && mDataState == TelephonyManager.DATA_CONNECTED) { - switch (mDataActivity) { - case TelephonyManager.DATA_ACTIVITY_IN: - iconId = mDataIconList[1]; - break; - case TelephonyManager.DATA_ACTIVITY_OUT: - iconId = mDataIconList[2]; - break; - case TelephonyManager.DATA_ACTIVITY_INOUT: - iconId = mDataIconList[3]; - break; - case TelephonyManager.DATA_ACTIVITY_DORMANT: - default: - iconId = mDataIconList[0]; - break; + String nosim = args.getString("nosim"); + if (nosim != null) { + boolean show = nosim.equals("show"); + final int n = mSignalClusters.size(); + for (int i = 0; i < n; i++) { + mSignalClusters.get(i).setNoSims(show); } - } else { - iconId = 0; - visible = false; } + String mobile = args.getString("mobile"); + if (mobile != null) { + boolean show = mobile.equals("show"); + String datatype = args.getString("datatype"); + String slotString = args.getString("slot"); + int slot = TextUtils.isEmpty(slotString) ? 0 : Integer.parseInt(slotString); + // Hack to index linearly for easy use. + MobileSignalController controller = mMobileSignalControllers + .values().toArray(new MobileSignalController[0])[slot]; + controller.getState().dataSim = datatype != null; + if (datatype != null) { + controller.getState().iconGroup = + datatype.equals("1x") ? TelephonyIcons.ONE_X : + datatype.equals("3g") ? TelephonyIcons.THREE_G : + datatype.equals("4g") ? TelephonyIcons.FOUR_G : + datatype.equals("e") ? TelephonyIcons.E : + datatype.equals("g") ? TelephonyIcons.G : + datatype.equals("h") ? TelephonyIcons.H : + datatype.equals("lte") ? TelephonyIcons.LTE : + datatype.equals("roam") ? TelephonyIcons.ROAMING : + TelephonyIcons.UNKNOWN; + } + int[][] icons = TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH; + String level = args.getString("level"); + if (level != null) { + controller.getState().level = level.equals("null") ? -1 + : Math.min(Integer.parseInt(level), icons[0].length - 1); + controller.getState().connected = controller.getState().level >= 0; + } + controller.getState().enabled = show; + controller.notifyListeners(); + } + refreshCarrierLabel(); } - - mDataDirectionIconId = iconId; - mDataConnected = visible; } - void updateNetworkName(boolean showSpn, String spn, boolean showPlmn, String plmn) { - if (false) { - Log.d("CarrierLabel", "updateNetworkName showSpn=" + showSpn + " spn=" + spn - + " showPlmn=" + showPlmn + " plmn=" + plmn); - } - StringBuilder str = new StringBuilder(); - boolean something = false; - if (showPlmn && plmn != null) { - str.append(plmn); - something = true; - } - if (showSpn && spn != null) { - if (something) { - str.append(mNetworkNameSeparator); + private final OnSubscriptionsChangedListener mSubscriptionListener = + new OnSubscriptionsChangedListener() { + @Override + public void onSubscriptionsChanged() { + updateMobileControllers(); + }; + }; + + // TODO: Move to its own file. + static class WifiSignalController extends + SignalController<WifiSignalController.WifiState, SignalController.IconGroup> { + private final WifiManager mWifiManager; + private final AsyncChannel mWifiChannel; + private final boolean mHasMobileData; + + public WifiSignalController(Context context, boolean hasMobileData, + List<NetworkSignalChangedCallback> signalCallbacks, + List<SignalCluster> signalClusters, NetworkControllerImpl networkController) { + super("WifiSignalController", context, NetworkCapabilities.TRANSPORT_WIFI, + signalCallbacks, signalClusters, networkController); + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + mHasMobileData = hasMobileData; + Handler handler = new WifiHandler(); + mWifiChannel = new AsyncChannel(); + Messenger wifiMessenger = mWifiManager.getWifiServiceMessenger(); + if (wifiMessenger != null) { + mWifiChannel.connect(context, handler, wifiMessenger); } - str.append(spn); - something = true; - } - if (something) { - mNetworkName = str.toString(); - } else { - mNetworkName = mNetworkNameDefault; + // WiFi only has one state. + mCurrentState.iconGroup = mLastState.iconGroup = new IconGroup( + "Wi-Fi Icons", + WifiIcons.WIFI_SIGNAL_STRENGTH, + WifiIcons.QS_WIFI_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH, + WifiIcons.WIFI_NO_NETWORK, + WifiIcons.QS_WIFI_NO_NETWORK, + WifiIcons.WIFI_NO_NETWORK, + WifiIcons.QS_WIFI_NO_NETWORK, + AccessibilityContentDescriptions.WIFI_NO_CONNECTION + ); } - } - // ===== Wifi =================================================================== + @Override + protected WifiState cleanState() { + return new WifiState(); + } - class WifiHandler extends Handler { @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: - if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) { - mWifiChannel.sendMessage(Message.obtain(this, - AsyncChannel.CMD_CHANNEL_FULL_CONNECTION)); - } else { - Log.e(TAG, "Failed to connect to wifi"); - } - break; - case WifiManager.DATA_ACTIVITY_NOTIFICATION: - if (msg.arg1 != mWifiActivity) { - mWifiActivity = msg.arg1; - refreshViews(); - } - break; - default: - //Ignore - break; + public void notifyListeners() { + // only show wifi in the cluster if connected or if wifi-only + boolean wifiVisible = mCurrentState.enabled + && (mCurrentState.connected || !mHasMobileData); + String wifiDesc = wifiVisible ? mCurrentState.ssid : null; + boolean ssidPresent = wifiVisible && mCurrentState.ssid != null; + String contentDescription = getStringIfExists(getContentDescription()); + int length = mSignalsChangedCallbacks.size(); + for (int i = 0; i < length; i++) { + mSignalsChangedCallbacks.get(i).onWifiSignalChanged(mCurrentState.enabled, + mCurrentState.connected, getQsCurrentIconId(), + ssidPresent && mCurrentState.activityIn, + ssidPresent && mCurrentState.activityOut, contentDescription, wifiDesc); + } + + int signalClustersLength = mSignalClusters.size(); + for (int i = 0; i < signalClustersLength; i++) { + mSignalClusters.get(i).setWifiIndicators(wifiVisible, getCurrentIconId(), + contentDescription); } } - } - private void updateWifiState(Intent intent) { - final String action = intent.getAction(); - if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { - mWifiEnabled = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, - WifiManager.WIFI_STATE_UNKNOWN) == WifiManager.WIFI_STATE_ENABLED; - - } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { - final NetworkInfo networkInfo = (NetworkInfo) - intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); - boolean wasConnected = mWifiConnected; - mWifiConnected = networkInfo != null && networkInfo.isConnected(); - // If Connected grab the signal strength and ssid - if (mWifiConnected) { - // try getting it out of the intent first - WifiInfo info = (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO); - if (info == null) { - info = mWifiManager.getConnectionInfo(); - } - if (info != null) { - mWifiSsid = huntForSsid(info); - } else { - mWifiSsid = null; + /** + * Extract wifi state directly from broadcasts about changes in wifi state. + */ + public void handleBroadcast(Intent intent) { + String action = intent.getAction(); + if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { + mCurrentState.enabled = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, + WifiManager.WIFI_STATE_UNKNOWN) == WifiManager.WIFI_STATE_ENABLED; + } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { + final NetworkInfo networkInfo = (NetworkInfo) + intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); + mCurrentState.connected = networkInfo != null && networkInfo.isConnected(); + // If Connected grab the signal strength and ssid. + if (mCurrentState.connected) { + // try getting it out of the intent first + WifiInfo info = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) != null + ? (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) + : mWifiManager.getConnectionInfo(); + if (info != null) { + mCurrentState.ssid = getSsid(info); + } else { + mCurrentState.ssid = null; + } + } else if (!mCurrentState.connected) { + mCurrentState.ssid = null; } - } else if (!mWifiConnected) { - mWifiSsid = null; + } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) { + // Default to -200 as its below WifiManager.MIN_RSSI. + mCurrentState.rssi = intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200); + mCurrentState.level = WifiManager.calculateSignalLevel( + mCurrentState.rssi, WifiIcons.WIFI_LEVEL_COUNT); } - } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) { - mWifiRssi = intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200); - mWifiLevel = WifiManager.calculateSignalLevel( - mWifiRssi, WifiIcons.WIFI_LEVEL_COUNT); - } - updateWifiIcons(); - } + notifyListenersIfNecessary(); + } - private void updateWifiIcons() { - int inetCondition = inetConditionForNetwork(ConnectivityManager.TYPE_WIFI); - if (mWifiConnected) { - mWifiIconId = WifiIcons.WIFI_SIGNAL_STRENGTH[inetCondition][mWifiLevel]; - mQSWifiIconId = WifiIcons.QS_WIFI_SIGNAL_STRENGTH[inetCondition][mWifiLevel]; - mContentDescriptionWifi = mContext.getString( - AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH[mWifiLevel]); - } else { - if (mDataAndWifiStacked) { - mWifiIconId = 0; - mQSWifiIconId = 0; - } else { - mWifiIconId = mWifiEnabled ? R.drawable.stat_sys_wifi_signal_null : 0; - mQSWifiIconId = mWifiEnabled ? R.drawable.ic_qs_wifi_no_network : 0; + private String getSsid(WifiInfo info) { + String ssid = info.getSSID(); + if (ssid != null) { + return ssid; + } + // OK, it's not in the connectionInfo; we have to go hunting for it + List<WifiConfiguration> networks = mWifiManager.getConfiguredNetworks(); + int length = networks.size(); + for (int i = 0; i < length; i++) { + if (networks.get(i).networkId == info.getNetworkId()) { + return networks.get(i).SSID; + } } - mContentDescriptionWifi = mContext.getString(R.string.accessibility_no_wifi); + return null; } - } - private String huntForSsid(WifiInfo info) { - String ssid = info.getSSID(); - if (ssid != null) { - return ssid; + @VisibleForTesting + void setActivity(int wifiActivity) { + mCurrentState.activityIn = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT + || wifiActivity == WifiManager.DATA_ACTIVITY_IN; + mCurrentState.activityOut = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT + || wifiActivity == WifiManager.DATA_ACTIVITY_OUT; + notifyListenersIfNecessary(); } - // OK, it's not in the connectionInfo; we have to go hunting for it - List<WifiConfiguration> networks = mWifiManager.getConfiguredNetworks(); - for (WifiConfiguration net : networks) { - if (net.networkId == info.getNetworkId()) { - return net.SSID; + + /** + * Handler to receive the data activity on wifi. + */ + class WifiHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: + if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) { + mWifiChannel.sendMessage(Message.obtain(this, + AsyncChannel.CMD_CHANNEL_FULL_CONNECTION)); + } else { + Log.e(mTag, "Failed to connect to wifi"); + } + break; + case WifiManager.DATA_ACTIVITY_NOTIFICATION: + setActivity(msg.arg1); + break; + default: + // Ignore + break; + } } } - return null; - } + static class WifiState extends SignalController.State { + String ssid; - // ===== Wimax =================================================================== - private final void updateWimaxState(Intent intent) { - final String action = intent.getAction(); - boolean wasConnected = mWimaxConnected; - if (action.equals(WimaxManagerConstants.NET_4G_STATE_CHANGED_ACTION)) { - int wimaxStatus = intent.getIntExtra(WimaxManagerConstants.EXTRA_4G_STATE, - WimaxManagerConstants.NET_4G_STATE_UNKNOWN); - mIsWimaxEnabled = (wimaxStatus == - WimaxManagerConstants.NET_4G_STATE_ENABLED); - } else if (action.equals(WimaxManagerConstants.SIGNAL_LEVEL_CHANGED_ACTION)) { - mWimaxSignal = intent.getIntExtra(WimaxManagerConstants.EXTRA_NEW_SIGNAL_LEVEL, 0); - } else if (action.equals(WimaxManagerConstants.WIMAX_NETWORK_STATE_CHANGED_ACTION)) { - mWimaxState = intent.getIntExtra(WimaxManagerConstants.EXTRA_WIMAX_STATE, - WimaxManagerConstants.NET_4G_STATE_UNKNOWN); - mWimaxExtraState = intent.getIntExtra( - WimaxManagerConstants.EXTRA_WIMAX_STATE_DETAIL, - WimaxManagerConstants.NET_4G_STATE_UNKNOWN); - mWimaxConnected = (mWimaxState == - WimaxManagerConstants.WIMAX_STATE_CONNECTED); - mWimaxIdle = (mWimaxExtraState == WimaxManagerConstants.WIMAX_IDLE); - } - updateDataNetType(); - updateWimaxIcons(); - } + @Override + public void copyFrom(State s) { + super.copyFrom(s); + WifiState state = (WifiState) s; + ssid = state.ssid; + } - private void updateWimaxIcons() { - if (mIsWimaxEnabled) { - if (mWimaxConnected) { - int inetCondition = inetConditionForNetwork(ConnectivityManager.TYPE_WIMAX); - if (mWimaxIdle) - mWimaxIconId = WimaxIcons.WIMAX_IDLE; - else - mWimaxIconId = WimaxIcons.WIMAX_SIGNAL_STRENGTH[inetCondition][mWimaxSignal]; - mContentDescriptionWimax = mContext.getString( - AccessibilityContentDescriptions.WIMAX_CONNECTION_STRENGTH[mWimaxSignal]); - } else { - mWimaxIconId = WimaxIcons.WIMAX_DISCONNECTED; - mContentDescriptionWimax = mContext.getString(R.string.accessibility_no_wimax); + @Override + protected void toString(StringBuilder builder) { + super.toString(builder); + builder.append(',').append("ssid=").append(ssid); + } + + @Override + public boolean equals(Object o) { + return super.equals(o) + && Objects.equals(((WifiState) o).ssid, ssid); } - } else { - mWimaxIconId = 0; } } - // ===== Full or limited Internet connectivity ================================== + // TODO: Move to its own file. + public static class MobileSignalController extends SignalController< + MobileSignalController.MobileState, MobileSignalController.MobileIconGroup> { + private final TelephonyManager mPhone; + private final String mNetworkNameDefault; + private final String mNetworkNameSeparator; + @VisibleForTesting + final PhoneStateListener mPhoneStateListener; + // Save entire info for logging, we only use the id. + private final SubscriptionInfo mSubscriptionInfo; + + // @VisibleForDemoMode + final SparseArray<MobileIconGroup> mNetworkToIconLookup; + + // Since some pieces of the phone state are interdependent we store it locally, + // this could potentially become part of MobileState for simplification/complication + // of code. + private IccCardConstants.State mSimState = IccCardConstants.State.READY; + private int mDataNetType = TelephonyManager.NETWORK_TYPE_UNKNOWN; + private int mDataState = TelephonyManager.DATA_DISCONNECTED; + private ServiceState mServiceState; + private SignalStrength mSignalStrength; + private MobileIconGroup mDefaultIcons; + private Config mConfig; + + // TODO: Reduce number of vars passed in, if we have the NetworkController, probably don't + // need listener lists anymore. + public MobileSignalController(Context context, Config config, boolean hasMobileData, + TelephonyManager phone, List<NetworkSignalChangedCallback> signalCallbacks, + List<SignalCluster> signalClusters, NetworkControllerImpl networkController, + SubscriptionInfo info) { + super("MobileSignalController(" + info.getSubscriptionId() + ")", context, + NetworkCapabilities.TRANSPORT_CELLULAR, signalCallbacks, signalClusters, + networkController); + mNetworkToIconLookup = new SparseArray<>(); + mConfig = config; + mPhone = phone; + mSubscriptionInfo = info; + mPhoneStateListener = new MobilePhoneStateListener(info.getSubscriptionId()); + mNetworkNameSeparator = getStringIfExists(R.string.status_bar_network_name_separator); + mNetworkNameDefault = getStringIfExists( + com.android.internal.R.string.lockscreen_carrier_default); + + mapIconSets(); + + mLastState.networkName = mCurrentState.networkName = mNetworkNameDefault; + mLastState.enabled = mCurrentState.enabled = hasMobileData; + mLastState.iconGroup = mCurrentState.iconGroup = mDefaultIcons; + // Get initial data sim state. + updateDataSim(); + } + + public void setConfiguration(Config config) { + mConfig = config; + mapIconSets(); + updateTelephony(); + } + + /** + * Get (the mobile parts of) the carrier string. + * + * @param currentLabel can be used for concatenation, currently just empty + * @param connected whether the device has connection to the internet at all + * @param isMobileLabel whether to always return the network or just when data is connected + */ + public String getLabel(String currentLabel, boolean connected, boolean isMobileLabel) { + if (!mCurrentState.enabled) { + return ""; + } else { + String mobileLabel = ""; + // We want to show the carrier name if in service and either: + // - We are connected to mobile data, or + // - We are not connected to mobile data, as long as the *reason* packets are not + // being routed over that link is that we have better connectivity via wifi. + // If data is disconnected for some other reason but wifi (or ethernet/bluetooth) + // is connected, we show nothing. + // Otherwise (nothing connected) we show "No internet connection". + if (mCurrentState.dataConnected) { + mobileLabel = mCurrentState.networkName; + } else if (connected || mCurrentState.isEmergency) { + if (mCurrentState.connected || mCurrentState.isEmergency) { + // The isEmergencyOnly test covers the case of a phone with no SIM + mobileLabel = mCurrentState.networkName; + } + } else { + mobileLabel = mContext.getString( + R.string.status_bar_settings_signal_meter_disconnected); + } - private void updateConnectivity(Intent intent) { - if (CHATTY) { - Log.d(TAG, "updateConnectivity: intent=" + intent); + if (currentLabel.length() != 0) { + currentLabel = currentLabel + mNetworkNameSeparator; + } + // Now for things that should only be shown when actually using mobile data. + if (isMobileLabel) { + return currentLabel + mobileLabel; + } else { + return currentLabel + + (mCurrentState.dataConnected ? mobileLabel : currentLabel); + } + } } - final ConnectivityManager connManager = (ConnectivityManager) mContext - .getSystemService(Context.CONNECTIVITY_SERVICE); - final NetworkInfo info = connManager.getActiveNetworkInfo(); - - // Are we connected at all, by any interface? - mConnected = info != null && info.isConnected(); - if (mConnected) { - mConnectedNetworkType = info.getType(); - mConnectedNetworkTypeName = info.getTypeName(); - } else { - mConnectedNetworkType = ConnectivityManager.TYPE_NONE; - mConnectedNetworkTypeName = null; + public int getDataContentDescription() { + return getIcons().mDataContentDescription; } - int connectionStatus = intent.getIntExtra(ConnectivityManager.EXTRA_INET_CONDITION, 0); - - if (CHATTY) { - Log.d(TAG, "updateConnectivity: networkInfo=" + info); - Log.d(TAG, "updateConnectivity: connectionStatus=" + connectionStatus); + public void setAirplaneMode(boolean airplaneMode) { + mCurrentState.airplaneMode = airplaneMode; + notifyListenersIfNecessary(); } - mInetCondition = (connectionStatus > INET_CONDITION_THRESHOLD ? 1 : 0); - - if (info != null && info.getType() == ConnectivityManager.TYPE_BLUETOOTH) { - mBluetoothTethered = info.isConnected(); - } else { - mBluetoothTethered = false; + public void setInetCondition(int inetCondition, int inetConditionForNetwork) { + // For mobile data, use general inet condition for phone signal indexing, + // and network specific for data indexing (I think this might be a bug, but + // keeping for now). + // TODO: Update with explanation of why. + mCurrentState.inetForNetwork = inetConditionForNetwork; + setInetCondition(inetCondition); } - // We want to update all the icons, all at once, for any condition change - updateDataNetType(); - updateWimaxIcons(); - updateDataIcon(); - updateTelephonySignalStrength(); - updateWifiIcons(); - } + /** + * Start listening for phone state changes. + */ + public void registerListener() { + mPhone.listen(mPhoneStateListener, + PhoneStateListener.LISTEN_SERVICE_STATE + | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS + | PhoneStateListener.LISTEN_CALL_STATE + | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE + | PhoneStateListener.LISTEN_DATA_ACTIVITY); + } + /** + * Stop listening for phone state changes. + */ + public void unregisterListener() { + mPhone.listen(mPhoneStateListener, 0); + } - // ===== Update the views ======================================================= + /** + * Produce a mapping of data network types to icon groups for simple and quick use in + * updateTelephony. + */ + private void mapIconSets() { + mNetworkToIconLookup.clear(); - void refreshViews() { - Context context = mContext; + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_A, TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UMTS, TelephonyIcons.THREE_G); - int combinedSignalIconId = 0; - String combinedLabel = ""; - String wifiLabel = ""; - String mobileLabel = ""; - int N; - final boolean emergencyOnly = isEmergencyOnly(); + if (!mConfig.showAtLeast3G) { + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, + TelephonyIcons.UNKNOWN); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE, TelephonyIcons.E); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA, TelephonyIcons.ONE_X); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT, TelephonyIcons.ONE_X); - if (!mHasMobileDataFeature) { - mDataSignalIconId = mPhoneSignalIconId = 0; - mQSPhoneSignalIconId = 0; - mobileLabel = ""; - } else { - // We want to show the carrier name if in service and either: - // - We are connected to mobile data, or - // - We are not connected to mobile data, as long as the *reason* packets are not - // being routed over that link is that we have better connectivity via wifi. - // If data is disconnected for some other reason but wifi (or ethernet/bluetooth) - // is connected, we show nothing. - // Otherwise (nothing connected) we show "No internet connection". - - if (mDataConnected) { - mobileLabel = mNetworkName; - } else if (mConnected || emergencyOnly) { - if (hasService() || emergencyOnly) { - // The isEmergencyOnly test covers the case of a phone with no SIM - mobileLabel = mNetworkName; - } else { - // Tablets, basically - mobileLabel = ""; - } + mDefaultIcons = TelephonyIcons.G; } else { - mobileLabel - = context.getString(R.string.status_bar_settings_signal_meter_disconnected); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, + TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT, + TelephonyIcons.THREE_G); + mDefaultIcons = TelephonyIcons.THREE_G; } - // Now for things that should only be shown when actually using mobile data. - if (mDataConnected) { - combinedSignalIconId = mDataSignalIconId; + MobileIconGroup hGroup = TelephonyIcons.THREE_G; + if (mConfig.hspaDataDistinguishable) { + hGroup = TelephonyIcons.H; + } + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSDPA, hGroup); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSUPA, hGroup); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPA, hGroup); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPAP, hGroup); - combinedLabel = mobileLabel; - combinedSignalIconId = mDataSignalIconId; // set by updateDataIcon() - mContentDescriptionCombinedSignal = mContentDescriptionDataType; + if (mConfig.show4gForLte) { + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.FOUR_G); + } else { + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.LTE); } } - if (mWifiConnected) { - if (mWifiSsid == null) { - wifiLabel = context.getString(R.string.status_bar_settings_signal_meter_wifi_nossid); - } else { - wifiLabel = mWifiSsid; - if (DEBUG) { - wifiLabel += "xxxxXXXXxxxxXXXX"; + @Override + public void notifyListeners() { + MobileIconGroup icons = getIcons(); + + String contentDescription = getStringIfExists(getContentDescription()); + String dataContentDescription = getStringIfExists(icons.mDataContentDescription); + + boolean showDataIcon = mCurrentState.dataConnected && mCurrentState.inetForNetwork != 0 + || mCurrentState.iconGroup == TelephonyIcons.ROAMING; + + // Only send data sim callbacks to QS. + if (mCurrentState.dataSim) { + int qsTypeIcon = showDataIcon ? icons.mQsDataType[mCurrentState.inetForNetwork] : 0; + int length = mSignalsChangedCallbacks.size(); + for (int i = 0; i < length; i++) { + mSignalsChangedCallbacks.get(i).onMobileDataSignalChanged(mCurrentState.enabled + && !mCurrentState.isEmergency, + getQsCurrentIconId(), contentDescription, + qsTypeIcon, + mCurrentState.dataConnected && mCurrentState.activityIn, + mCurrentState.dataConnected && mCurrentState.activityOut, + dataContentDescription, + mCurrentState.isEmergency ? null : mCurrentState.networkName, + // Only wide if actually showing something. + icons.mIsWide && qsTypeIcon != 0); } } + int typeIcon = showDataIcon ? icons.mDataType : 0; + int signalClustersLength = mSignalClusters.size(); + for (int i = 0; i < signalClustersLength; i++) { + mSignalClusters.get(i).setMobileDataIndicators( + mCurrentState.enabled && !mCurrentState.airplaneMode, + getCurrentIconId(), + typeIcon, + contentDescription, + dataContentDescription, + // Only wide if actually showing something. + icons.mIsWide && typeIcon != 0, + mSubscriptionInfo.getSubscriptionId()); + } + } - combinedLabel = wifiLabel; - combinedSignalIconId = mWifiIconId; // set by updateWifiIcons() - mContentDescriptionCombinedSignal = mContentDescriptionWifi; - } else { - if (mHasMobileDataFeature) { - wifiLabel = ""; + @Override + protected MobileState cleanState() { + return new MobileState(); + } + + private boolean hasService() { + if (mServiceState != null) { + // Consider the device to be in service if either voice or data + // service is available. Some SIM cards are marketed as data-only + // and do not support voice service, and on these SIM cards, we + // want to show signal bars for data service as well as the "no + // service" or "emergency calls only" text that indicates that voice + // is not available. + switch (mServiceState.getVoiceRegState()) { + case ServiceState.STATE_POWER_OFF: + return false; + case ServiceState.STATE_OUT_OF_SERVICE: + case ServiceState.STATE_EMERGENCY_ONLY: + return mServiceState.getDataRegState() == ServiceState.STATE_IN_SERVICE; + default: + return true; + } } else { - wifiLabel = context.getString(R.string.status_bar_settings_signal_meter_disconnected); + return false; } } - if (mBluetoothTethered) { - combinedLabel = mContext.getString(R.string.bluetooth_tethered); - combinedSignalIconId = mBluetoothTetherIconId; - mContentDescriptionCombinedSignal = mContext.getString( - R.string.accessibility_bluetooth_tether); + private boolean isCdma() { + return (mSignalStrength != null) && !mSignalStrength.isGsm(); } - final boolean ethernetConnected = (mConnectedNetworkType == ConnectivityManager.TYPE_ETHERNET); - if (ethernetConnected) { - combinedLabel = context.getString(R.string.ethernet_label); + public boolean isEmergencyOnly() { + return (mServiceState != null && mServiceState.isEmergencyOnly()); } - if (mAirplaneMode && - (mServiceState == null || (!hasService() && !mServiceState.isEmergencyOnly()))) { - // Only display the flight-mode icon if not in "emergency calls only" mode. + private boolean isRoaming() { + if (isCdma()) { + final int iconMode = mServiceState.getCdmaEriIconMode(); + return mServiceState.getCdmaEriIconIndex() != EriInfo.ROAMING_INDICATOR_OFF + && (iconMode == EriInfo.ROAMING_ICON_MODE_NORMAL + || iconMode == EriInfo.ROAMING_ICON_MODE_FLASH); + } else { + return mServiceState != null && mServiceState.getRoaming(); + } + } - // look again; your radios are now airplanes - mContentDescriptionPhoneSignal = mContext.getString( - R.string.accessibility_airplane_mode); - mAirplaneIconId = TelephonyIcons.FLIGHT_MODE_ICON; - mPhoneSignalIconId = mDataSignalIconId = mDataTypeIconId = mQSDataTypeIconId = 0; - mQSPhoneSignalIconId = 0; + public void handleBroadcast(Intent intent) { + String action = intent.getAction(); + if (action.equals(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION)) { + updateNetworkName(intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_SPN, false), + intent.getStringExtra(TelephonyIntents.EXTRA_SPN), + intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_PLMN, false), + intent.getStringExtra(TelephonyIntents.EXTRA_PLMN)); + notifyListenersIfNecessary(); + } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) { + updateDataSim(); + } + } - // combined values from connected wifi take precedence over airplane mode - if (mWifiConnected) { - // Suppress "No internet connection." from mobile if wifi connected. - mobileLabel = ""; + private void updateDataSim() { + int defaultDataSub = SubscriptionManager.getDefaultDataSubId(); + if (SubscriptionManager.isValidSubscriptionId(defaultDataSub)) { + mCurrentState.dataSim = defaultDataSub == mSubscriptionInfo.getSubscriptionId(); } else { - if (mHasMobileDataFeature) { - // let the mobile icon show "No internet connection." - wifiLabel = ""; - } else { - wifiLabel = context.getString(R.string.status_bar_settings_signal_meter_disconnected); - combinedLabel = wifiLabel; - } - mContentDescriptionCombinedSignal = mContentDescriptionPhoneSignal; - combinedSignalIconId = mDataSignalIconId; + // There doesn't seem to be a data sim selected, however if + // there isn't a MobileSignalController with dataSim set, then + // QS won't get any callbacks and will be blank. Instead + // lets just assume we are the data sim (which will basically + // show one at random) in QS until one is selected. The user + // should pick one soon after, so we shouldn't be in this state + // for long. + mCurrentState.dataSim = true; } + notifyListenersIfNecessary(); } - else if (!mDataConnected && !mWifiConnected && !mBluetoothTethered && !mWimaxConnected && !ethernetConnected) { - // pretty much totally disconnected - combinedLabel = context.getString(R.string.status_bar_settings_signal_meter_disconnected); - // On devices without mobile radios, we want to show the wifi icon - combinedSignalIconId = - mHasMobileDataFeature ? mDataSignalIconId : mWifiIconId; - mContentDescriptionCombinedSignal = mHasMobileDataFeature - ? mContentDescriptionDataType : mContentDescriptionWifi; + /** + * Updates the network's name based on incoming spn and plmn. + */ + void updateNetworkName(boolean showSpn, String spn, boolean showPlmn, String plmn) { + if (CHATTY) { + Log.d("CarrierLabel", "updateNetworkName showSpn=" + showSpn + " spn=" + spn + + " showPlmn=" + showPlmn + " plmn=" + plmn); + } + StringBuilder str = new StringBuilder(); + if (showPlmn && plmn != null) { + str.append(plmn); + } + if (showSpn && spn != null) { + if (str.length() != 0) { + str.append(mNetworkNameSeparator); + } + str.append(spn); + } + if (str.length() != 0) { + mCurrentState.networkName = str.toString(); + } else { + mCurrentState.networkName = mNetworkNameDefault; + } + } - int inetCondition = inetConditionForNetwork(ConnectivityManager.TYPE_MOBILE); + /** + * Updates the current state based on mServiceState, mSignalStrength, mDataNetType, + * mDataState, and mSimState. It should be called any time one of these is updated. + * This will call listeners if necessary. + */ + private final void updateTelephony() { + if (DEBUG) { + Log.d(TAG, "updateTelephonySignalStrength: hasService=" + hasService() + + " ss=" + mSignalStrength); + } + mCurrentState.connected = hasService() && mSignalStrength != null; + if (mCurrentState.connected) { + if (!mSignalStrength.isGsm() && mConfig.alwaysShowCdmaRssi) { + mCurrentState.level = mSignalStrength.getCdmaLevel(); + } else { + mCurrentState.level = mSignalStrength.getLevel(); + } + } + if (mNetworkToIconLookup.indexOfKey(mDataNetType) >= 0) { + mCurrentState.iconGroup = mNetworkToIconLookup.get(mDataNetType); + } else { + mCurrentState.iconGroup = mDefaultIcons; + } + mCurrentState.dataConnected = mCurrentState.connected + && mDataState == TelephonyManager.DATA_CONNECTED; - mDataTypeIconId = 0; - mQSDataTypeIconId = 0; if (isRoaming()) { - mDataTypeIconId = TelephonyIcons.ROAMING_ICON; - mQSDataTypeIconId = TelephonyIcons.QS_DATA_R[mInetCondition]; + mCurrentState.iconGroup = TelephonyIcons.ROAMING; } + if (isEmergencyOnly() != mCurrentState.isEmergency) { + mCurrentState.isEmergency = isEmergencyOnly(); + mNetworkController.recalculateEmergency(); + } + // Fill in the network name if we think we have it. + if (mCurrentState.networkName == mNetworkNameDefault && mServiceState != null + && mServiceState.getOperatorAlphaShort() != null) { + mCurrentState.networkName = mServiceState.getOperatorAlphaShort(); + } + notifyListenersIfNecessary(); } - if (mDemoMode) { - mQSWifiIconId = mDemoWifiLevel < 0 ? R.drawable.ic_qs_wifi_no_network - : WifiIcons.QS_WIFI_SIGNAL_STRENGTH[mDemoInetCondition][mDemoWifiLevel]; - mQSPhoneSignalIconId = mDemoMobileLevel < 0 ? R.drawable.ic_qs_signal_no_signal : - TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH[mDemoInetCondition][mDemoMobileLevel]; - mQSDataTypeIconId = mDemoQSDataTypeIconId; + @VisibleForTesting + void setActivity(int activity) { + mCurrentState.activityIn = activity == TelephonyManager.DATA_ACTIVITY_INOUT + || activity == TelephonyManager.DATA_ACTIVITY_IN; + mCurrentState.activityOut = activity == TelephonyManager.DATA_ACTIVITY_INOUT + || activity == TelephonyManager.DATA_ACTIVITY_OUT; + notifyListenersIfNecessary(); } - if (DEBUG) { - Log.d(TAG, "refreshViews connected={" - + (mWifiConnected?" wifi":"") - + (mDataConnected?" data":"") - + " } level=" - + ((mSignalStrength == null)?"??":Integer.toString(mSignalStrength.getLevel())) - + " combinedSignalIconId=0x" - + Integer.toHexString(combinedSignalIconId) - + "/" + getResourceName(combinedSignalIconId) - + " mobileLabel=" + mobileLabel - + " wifiLabel=" + wifiLabel - + " emergencyOnly=" + emergencyOnly - + " combinedLabel=" + combinedLabel - + " mAirplaneMode=" + mAirplaneMode - + " mDataActivity=" + mDataActivity - + " mPhoneSignalIconId=0x" + Integer.toHexString(mPhoneSignalIconId) - + " mQSPhoneSignalIconId=0x" + Integer.toHexString(mQSPhoneSignalIconId) - + " mDataDirectionIconId=0x" + Integer.toHexString(mDataDirectionIconId) - + " mDataSignalIconId=0x" + Integer.toHexString(mDataSignalIconId) - + " mDataTypeIconId=0x" + Integer.toHexString(mDataTypeIconId) - + " mQSDataTypeIconId=0x" + Integer.toHexString(mQSDataTypeIconId) - + " mWifiIconId=0x" + Integer.toHexString(mWifiIconId) - + " mQSWifiIconId=0x" + Integer.toHexString(mQSWifiIconId) - + " mBluetoothTetherIconId=0x" + Integer.toHexString(mBluetoothTetherIconId)); - } + @Override + public void dump(PrintWriter pw) { + super.dump(pw); + pw.println(" mSubscription=" + mSubscriptionInfo + ","); + pw.println(" mServiceState=" + mServiceState + ","); + pw.println(" mSignalStrength=" + mSignalStrength + ","); + pw.println(" mDataState=" + mDataState + ","); + pw.println(" mDataNetType=" + mDataNetType + ","); + } + + class MobilePhoneStateListener extends PhoneStateListener { + public MobilePhoneStateListener(int subId) { + super(subId); + } - // update QS - for (NetworkSignalChangedCallback cb : mSignalsChangedCallbacks) { - notifySignalsChangedCallbacks(cb); - } + @Override + public void onSignalStrengthsChanged(SignalStrength signalStrength) { + if (DEBUG) { + Log.d(mTag, "onSignalStrengthsChanged signalStrength=" + signalStrength + + ((signalStrength == null) ? "" : (" level=" + signalStrength.getLevel()))); + } + mSignalStrength = signalStrength; + updateTelephony(); + } - if (mLastPhoneSignalIconId != mPhoneSignalIconId - || mLastWifiIconId != mWifiIconId - || mLastInetCondition != mInetCondition - || mLastWimaxIconId != mWimaxIconId - || mLastDataTypeIconId != mDataTypeIconId - || mLastAirplaneMode != mAirplaneMode - || mLastLocale != mLocale - || mLastConnectedNetworkType != mConnectedNetworkType) - { - // NB: the mLast*s will be updated later - for (SignalCluster cluster : mSignalClusters) { - refreshSignalCluster(cluster); + @Override + public void onServiceStateChanged(ServiceState state) { + if (DEBUG) { + Log.d(mTag, "onServiceStateChanged voiceState=" + state.getVoiceRegState() + + " dataState=" + state.getDataRegState()); + } + mServiceState = state; + updateTelephony(); } - } - if (mLastAirplaneMode != mAirplaneMode) { - mLastAirplaneMode = mAirplaneMode; - } + @Override + public void onDataConnectionStateChanged(int state, int networkType) { + if (DEBUG) { + Log.d(mTag, "onDataConnectionStateChanged: state=" + state + + " type=" + networkType); + } + mDataState = state; + mDataNetType = networkType; + updateTelephony(); + } - if (mLastLocale != mLocale) { - mLastLocale = mLocale; + @Override + public void onDataActivity(int direction) { + if (DEBUG) { + Log.d(mTag, "onDataActivity: direction=" + direction); + } + setActivity(direction); + } + }; + + static class MobileIconGroup extends SignalController.IconGroup { + final int mDataContentDescription; // mContentDescriptionDataType + final int mDataType; + final boolean mIsWide; + final int[] mQsDataType; + + public MobileIconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc, + int sbNullState, int qsNullState, int sbDiscState, int qsDiscState, + int discContentDesc, int dataContentDesc, int dataType, boolean isWide, + int[] qsDataType) { + super(name, sbIcons, qsIcons, contentDesc, sbNullState, qsNullState, sbDiscState, + qsDiscState, discContentDesc); + mDataContentDescription = dataContentDesc; + mDataType = dataType; + mIsWide = isWide; + mQsDataType = qsDataType; + } } - // the phone icon on phones - if (mLastPhoneSignalIconId != mPhoneSignalIconId) { - mLastPhoneSignalIconId = mPhoneSignalIconId; - } + static class MobileState extends SignalController.State { + String networkName; + boolean dataSim; + boolean dataConnected; + boolean isEmergency; + boolean airplaneMode; + int inetForNetwork; - // the data icon on phones - if (mLastDataDirectionIconId != mDataDirectionIconId) { - mLastDataDirectionIconId = mDataDirectionIconId; - } + @Override + public void copyFrom(State s) { + super.copyFrom(s); + MobileState state = (MobileState) s; + dataSim = state.dataSim; + networkName = state.networkName; + dataConnected = state.dataConnected; + inetForNetwork = state.inetForNetwork; + isEmergency = state.isEmergency; + airplaneMode = state.airplaneMode; + } - // the wifi icon on phones - if (mLastWifiIconId != mWifiIconId) { - mLastWifiIconId = mWifiIconId; + @Override + protected void toString(StringBuilder builder) { + super.toString(builder); + builder.append(','); + builder.append("dataSim=").append(dataSim).append(','); + builder.append("networkName=").append(networkName).append(','); + builder.append("dataConnected=").append(dataConnected).append(','); + builder.append("inetForNetwork=").append(inetForNetwork).append(','); + builder.append("isEmergency=").append(isEmergency).append(','); + builder.append("airplaneMode=").append(airplaneMode); + } + + @Override + public boolean equals(Object o) { + return super.equals(o) + && Objects.equals(((MobileState) o).networkName, networkName) + && ((MobileState) o).dataSim == dataSim + && ((MobileState) o).dataConnected == dataConnected + && ((MobileState) o).isEmergency == isEmergency + && ((MobileState) o).airplaneMode == airplaneMode + && ((MobileState) o).inetForNetwork == inetForNetwork; + } } + } - if (mLastInetCondition != mInetCondition) { - mLastInetCondition = mInetCondition; + /** + * Common base class for handling signal for both wifi and mobile data. + */ + static abstract class SignalController<T extends SignalController.State, + I extends SignalController.IconGroup> { + protected final String mTag; + protected final T mCurrentState; + protected final T mLastState; + protected final int mTransportType; + protected final Context mContext; + // The owner of the SignalController (i.e. NetworkController will maintain the following + // lists and call notifyListeners whenever the list has changed to ensure everyone + // is aware of current state. + protected final List<NetworkSignalChangedCallback> mSignalsChangedCallbacks; + protected final List<SignalCluster> mSignalClusters; + protected final NetworkControllerImpl mNetworkController; + + // Save the previous HISTORY_SIZE states for logging. + private final State[] mHistory; + // Where to copy the next state into. + private int mHistoryIndex; + + public SignalController(String tag, Context context, int type, + List<NetworkSignalChangedCallback> signalCallbacks, + List<SignalCluster> signalClusters, NetworkControllerImpl networkController) { + mTag = TAG + "." + tag; + mNetworkController = networkController; + mTransportType = type; + mContext = context; + mSignalsChangedCallbacks = signalCallbacks; + mSignalClusters = signalClusters; + mCurrentState = cleanState(); + mLastState = cleanState(); + if (RECORD_HISTORY) { + mHistory = new State[HISTORY_SIZE]; + for (int i = 0; i < HISTORY_SIZE; i++) { + mHistory[i] = cleanState(); + } + } } - if (mLastConnectedNetworkType != mConnectedNetworkType) { - mLastConnectedNetworkType = mConnectedNetworkType; + public T getState() { + return mCurrentState; } - // the wimax icon on phones - if (mLastWimaxIconId != mWimaxIconId) { - mLastWimaxIconId = mWimaxIconId; + public int getTransportType() { + return mTransportType; } - // the combined data signal icon - if (mLastCombinedSignalIconId != combinedSignalIconId) { - mLastCombinedSignalIconId = combinedSignalIconId; + + public void setInetCondition(int inetCondition) { + mCurrentState.inetCondition = inetCondition; + notifyListenersIfNecessary(); } - // the data network type overlay - if (mLastDataTypeIconId != mDataTypeIconId) { - mLastDataTypeIconId = mDataTypeIconId; + /** + * Used at the end of demo mode to clear out any ugly state that it has created. + * Since we haven't had any callbacks, then isDirty will not have been triggered, + * so we can just take the last good state directly from there. + * + * Used for demo mode. + */ + void resetLastState() { + mCurrentState.copyFrom(mLastState); } - // the combinedLabel in the notification panel - if (!mLastCombinedLabel.equals(combinedLabel)) { - mLastCombinedLabel = combinedLabel; - N = mCombinedLabelViews.size(); - for (int i=0; i<N; i++) { - TextView v = mCombinedLabelViews.get(i); - v.setText(combinedLabel); + /** + * Determines if the state of this signal controller has changed and + * needs to trigger callbacks related to it. + */ + public boolean isDirty() { + if (!mLastState.equals(mCurrentState)) { + if (DEBUG) { + Log.d(mTag, "Change in state from: " + mLastState + "\n" + + "\tto: " + mCurrentState); + } + return true; } + return false; } - // wifi label - N = mWifiLabelViews.size(); - for (int i=0; i<N; i++) { - TextView v = mWifiLabelViews.get(i); - v.setText(wifiLabel); - if ("".equals(wifiLabel)) { - v.setVisibility(View.GONE); + public void saveLastState() { + if (RECORD_HISTORY) { + recordLastState(); + } + // Updates the current time. + mCurrentState.time = System.currentTimeMillis(); + mLastState.copyFrom(mCurrentState); + } + + /** + * Gets the signal icon for QS based on current state of connected, enabled, and level. + */ + public int getQsCurrentIconId() { + if (mCurrentState.connected) { + return getIcons().mQsIcons[mCurrentState.inetCondition][mCurrentState.level]; + } else if (mCurrentState.enabled) { + return getIcons().mQsDiscState; } else { - v.setVisibility(View.VISIBLE); + return getIcons().mQsNullState; } } - // mobile label - N = mMobileLabelViews.size(); - for (int i=0; i<N; i++) { - TextView v = mMobileLabelViews.get(i); - v.setText(mobileLabel); - if ("".equals(mobileLabel)) { - v.setVisibility(View.GONE); + /** + * Gets the signal icon for SB based on current state of connected, enabled, and level. + */ + public int getCurrentIconId() { + if (mCurrentState.connected) { + return getIcons().mSbIcons[mCurrentState.inetCondition][mCurrentState.level]; + } else if (mCurrentState.enabled) { + return getIcons().mSbDiscState; } else { - v.setVisibility(View.VISIBLE); + return getIcons().mSbNullState; } } - // e-call label - N = mEmergencyViews.size(); - for (int i=0; i<N; i++) { - StatusBarHeaderView v = mEmergencyViews.get(i); - v.setShowEmergencyCallsOnly(emergencyOnly); + /** + * Gets the content description id for the signal based on current state of connected and + * level. + */ + public int getContentDescription() { + if (mCurrentState.connected) { + return getIcons().mContentDesc[mCurrentState.level]; + } else { + return getIcons().mDiscContentDesc; + } } - } - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("NetworkController state:"); - pw.println(String.format(" %s network type %d (%s)", - mConnected?"CONNECTED":"DISCONNECTED", - mConnectedNetworkType, mConnectedNetworkTypeName)); - pw.println(" - telephony ------"); - pw.print(" hasVoiceCallingFeature()="); - pw.println(hasVoiceCallingFeature()); - pw.print(" hasService()="); - pw.println(hasService()); - pw.print(" mHspaDataDistinguishable="); - pw.println(mHspaDataDistinguishable); - pw.print(" mDataConnected="); - pw.println(mDataConnected); - pw.print(" mSimState="); - pw.println(mSimState); - pw.print(" mPhoneState="); - pw.println(mPhoneState); - pw.print(" mDataState="); - pw.println(mDataState); - pw.print(" mDataActivity="); - pw.println(mDataActivity); - pw.print(" mDataNetType="); - pw.print(mDataNetType); - pw.print("/"); - pw.println(TelephonyManager.getNetworkTypeName(mDataNetType)); - pw.print(" mServiceState="); - pw.println(mServiceState); - pw.print(" mSignalStrength="); - pw.println(mSignalStrength); - pw.print(" mLastSignalLevel="); - pw.println(mLastSignalLevel); - pw.print(" mNetworkName="); - pw.println(mNetworkName); - pw.print(" mNetworkNameDefault="); - pw.println(mNetworkNameDefault); - pw.print(" mNetworkNameSeparator="); - pw.println(mNetworkNameSeparator.replace("\n","\\n")); - pw.print(" mPhoneSignalIconId=0x"); - pw.print(Integer.toHexString(mPhoneSignalIconId)); - pw.print("/"); - pw.print(" mQSPhoneSignalIconId=0x"); - pw.print(Integer.toHexString(mQSPhoneSignalIconId)); - pw.print("/"); - pw.println(getResourceName(mPhoneSignalIconId)); - pw.print(" mDataDirectionIconId="); - pw.print(Integer.toHexString(mDataDirectionIconId)); - pw.print("/"); - pw.println(getResourceName(mDataDirectionIconId)); - pw.print(" mDataSignalIconId="); - pw.print(Integer.toHexString(mDataSignalIconId)); - pw.print("/"); - pw.println(getResourceName(mDataSignalIconId)); - pw.print(" mDataTypeIconId="); - pw.print(Integer.toHexString(mDataTypeIconId)); - pw.print("/"); - pw.println(getResourceName(mDataTypeIconId)); - pw.print(" mQSDataTypeIconId="); - pw.print(Integer.toHexString(mQSDataTypeIconId)); - pw.print("/"); - pw.println(getResourceName(mQSDataTypeIconId)); - - pw.println(" - wifi ------"); - pw.print(" mWifiEnabled="); - pw.println(mWifiEnabled); - pw.print(" mWifiConnected="); - pw.println(mWifiConnected); - pw.print(" mWifiRssi="); - pw.println(mWifiRssi); - pw.print(" mWifiLevel="); - pw.println(mWifiLevel); - pw.print(" mWifiSsid="); - pw.println(mWifiSsid); - pw.println(String.format(" mWifiIconId=0x%08x/%s", - mWifiIconId, getResourceName(mWifiIconId))); - pw.println(String.format(" mQSWifiIconId=0x%08x/%s", - mQSWifiIconId, getResourceName(mQSWifiIconId))); - pw.print(" mWifiActivity="); - pw.println(mWifiActivity); - - if (mWimaxSupported) { - pw.println(" - wimax ------"); - pw.print(" mIsWimaxEnabled="); pw.println(mIsWimaxEnabled); - pw.print(" mWimaxConnected="); pw.println(mWimaxConnected); - pw.print(" mWimaxIdle="); pw.println(mWimaxIdle); - pw.println(String.format(" mWimaxIconId=0x%08x/%s", - mWimaxIconId, getResourceName(mWimaxIconId))); - pw.println(String.format(" mWimaxSignal=%d", mWimaxSignal)); - pw.println(String.format(" mWimaxState=%d", mWimaxState)); - pw.println(String.format(" mWimaxExtraState=%d", mWimaxExtraState)); + public void notifyListenersIfNecessary() { + if (isDirty()) { + saveLastState(); + notifyListeners(); + mNetworkController.refreshCarrierLabel(); + } } - pw.println(" - Bluetooth ----"); - pw.print(" mBtReverseTethered="); - pw.println(mBluetoothTethered); + /** + * Returns the resource if resId is not 0, and an empty string otherwise. + */ + protected String getStringIfExists(int resId) { + return resId != 0 ? mContext.getString(resId) : ""; + } - pw.println(" - connectivity ------"); - pw.print(" mInetCondition="); - pw.println(mInetCondition); + protected I getIcons() { + return (I) mCurrentState.iconGroup; + } - pw.println(" - icons ------"); - pw.print(" mLastPhoneSignalIconId=0x"); - pw.print(Integer.toHexString(mLastPhoneSignalIconId)); - pw.print("/"); - pw.println(getResourceName(mLastPhoneSignalIconId)); - pw.print(" mLastDataDirectionIconId=0x"); - pw.print(Integer.toHexString(mLastDataDirectionIconId)); - pw.print("/"); - pw.println(getResourceName(mLastDataDirectionIconId)); - pw.print(" mLastWifiIconId=0x"); - pw.print(Integer.toHexString(mLastWifiIconId)); - pw.print("/"); - pw.println(getResourceName(mLastWifiIconId)); - pw.print(" mLastCombinedSignalIconId=0x"); - pw.print(Integer.toHexString(mLastCombinedSignalIconId)); - pw.print("/"); - pw.println(getResourceName(mLastCombinedSignalIconId)); - pw.print(" mLastDataTypeIconId=0x"); - pw.print(Integer.toHexString(mLastDataTypeIconId)); - pw.print("/"); - pw.println(getResourceName(mLastDataTypeIconId)); - pw.print(" mLastCombinedLabel="); - pw.print(mLastCombinedLabel); - pw.println(""); - } + /** + * Saves the last state of any changes, so we can log the current + * and last value of any state data. + */ + protected void recordLastState() { + mHistory[mHistoryIndex++ & (HISTORY_SIZE - 1)].copyFrom(mLastState); + } - private String getResourceName(int resId) { - if (resId != 0) { - final Resources res = mContext.getResources(); - try { - return res.getResourceName(resId); - } catch (android.content.res.Resources.NotFoundException ex) { - return "(unknown)"; + public void dump(PrintWriter pw) { + pw.println(" - " + mTag + " -----"); + pw.println(" Current State: " + mCurrentState); + if (RECORD_HISTORY) { + // Count up the states that actually contain time stamps, and only display those. + int size = 0; + for (int i = 0; i < HISTORY_SIZE; i++) { + if (mHistory[i].time != 0) size++; + } + // Print out the previous states in ordered number. + for (int i = mHistoryIndex + HISTORY_SIZE - 1; + i >= mHistoryIndex + HISTORY_SIZE - size; i--) { + pw.println(" Previous State(" + (mHistoryIndex + HISTORY_SIZE - i) + ": " + + mHistory[i & (HISTORY_SIZE - 1)]); + } } - } else { - return "(null)"; } - } - private boolean mDemoMode; - private int mDemoInetCondition; - private int mDemoWifiLevel; - private int mDemoDataTypeIconId; - private int mDemoQSDataTypeIconId; - private int mDemoMobileLevel; - - @Override - public void dispatchDemoCommand(String command, Bundle args) { - if (!mDemoMode && command.equals(COMMAND_ENTER)) { - mDemoMode = true; - mDemoWifiLevel = mWifiLevel; - mDemoInetCondition = mInetCondition; - mDemoDataTypeIconId = mDataTypeIconId; - mDemoQSDataTypeIconId = mQSDataTypeIconId; - mDemoMobileLevel = mLastSignalLevel; - } else if (mDemoMode && command.equals(COMMAND_EXIT)) { - mDemoMode = false; - for (SignalCluster cluster : mSignalClusters) { - refreshSignalCluster(cluster); + /** + * Trigger callbacks based on current state. The callbacks should be completely + * based on current state, and only need to be called in the scenario where + * mCurrentState != mLastState. + */ + public abstract void notifyListeners(); + + /** + * Generate a blank T. + */ + protected abstract T cleanState(); + + /* + * Holds icons for a given state. Arrays are generally indexed as inet + * state (full connectivity or not) first, and second dimension as + * signal strength. + */ + static class IconGroup { + final int[][] mSbIcons; + final int[][] mQsIcons; + final int[] mContentDesc; + final int mSbNullState; + final int mQsNullState; + final int mSbDiscState; + final int mQsDiscState; + final int mDiscContentDesc; + // For logging. + final String mName; + + public IconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc, + int sbNullState, int qsNullState, int sbDiscState, int qsDiscState, + int discContentDesc) { + mName = name; + mSbIcons = sbIcons; + mQsIcons = qsIcons; + mContentDesc = contentDesc; + mSbNullState = sbNullState; + mQsNullState = qsNullState; + mSbDiscState = sbDiscState; + mQsDiscState = qsDiscState; + mDiscContentDesc = discContentDesc; } - refreshViews(); - } else if (mDemoMode && command.equals(COMMAND_NETWORK)) { - String airplane = args.getString("airplane"); - if (airplane != null) { - boolean show = airplane.equals("show"); - for (SignalCluster cluster : mSignalClusters) { - cluster.setIsAirplaneMode(show, TelephonyIcons.FLIGHT_MODE_ICON); - } + + @Override + public String toString() { + return "IconGroup(" + mName + ")"; } - String fully = args.getString("fully"); - if (fully != null) { - mDemoInetCondition = Boolean.parseBoolean(fully) ? 1 : 0; + } + + static class State { + boolean connected; + boolean enabled; + boolean activityIn; + boolean activityOut; + int level; + IconGroup iconGroup; + int inetCondition; + int rssi; // Only for logging. + + // Not used for comparison, just used for logging. + long time; + + public void copyFrom(State state) { + connected = state.connected; + enabled = state.enabled; + level = state.level; + iconGroup = state.iconGroup; + inetCondition = state.inetCondition; + activityIn = state.activityIn; + activityOut = state.activityOut; + rssi = state.rssi; + time = state.time; } - String wifi = args.getString("wifi"); - if (wifi != null) { - boolean show = wifi.equals("show"); - String level = args.getString("level"); - if (level != null) { - mDemoWifiLevel = level.equals("null") ? -1 - : Math.min(Integer.parseInt(level), WifiIcons.WIFI_LEVEL_COUNT - 1); - } - int iconId = mDemoWifiLevel < 0 ? R.drawable.stat_sys_wifi_signal_null - : WifiIcons.WIFI_SIGNAL_STRENGTH[mDemoInetCondition][mDemoWifiLevel]; - for (SignalCluster cluster : mSignalClusters) { - cluster.setWifiIndicators( - show, - iconId, - "Demo"); + + @Override + public String toString() { + if (time != 0) { + StringBuilder builder = new StringBuilder(); + toString(builder); + return builder.toString(); + } else { + return "Empty " + getClass().getSimpleName(); } - refreshViews(); } - String mobile = args.getString("mobile"); - if (mobile != null) { - boolean show = mobile.equals("show"); - String datatype = args.getString("datatype"); - if (datatype != null) { - mDemoDataTypeIconId = - datatype.equals("1x") ? TelephonyIcons.ICON_1X : - datatype.equals("3g") ? TelephonyIcons.ICON_3G : - datatype.equals("4g") ? TelephonyIcons.ICON_4G : - datatype.equals("e") ? R.drawable.stat_sys_data_fully_connected_e : - datatype.equals("g") ? R.drawable.stat_sys_data_fully_connected_g : - datatype.equals("h") ? R.drawable.stat_sys_data_fully_connected_h : - datatype.equals("lte") ? TelephonyIcons.ICON_LTE : - datatype.equals("roam") ? TelephonyIcons.ROAMING_ICON : - 0; - mDemoQSDataTypeIconId = - datatype.equals("1x") ? TelephonyIcons.QS_ICON_1X : - datatype.equals("3g") ? TelephonyIcons.QS_ICON_3G : - datatype.equals("4g") ? TelephonyIcons.QS_ICON_4G : - datatype.equals("e") ? R.drawable.ic_qs_signal_e : - datatype.equals("g") ? R.drawable.ic_qs_signal_g : - datatype.equals("h") ? R.drawable.ic_qs_signal_h : - datatype.equals("lte") ? TelephonyIcons.QS_ICON_LTE : - datatype.equals("roam") ? R.drawable.ic_qs_signal_r : - 0; - } - int[][] icons = TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH; - String level = args.getString("level"); - if (level != null) { - mDemoMobileLevel = level.equals("null") ? -1 - : Math.min(Integer.parseInt(level), icons[0].length - 1); - } - int iconId = mDemoMobileLevel < 0 ? R.drawable.stat_sys_signal_null : - icons[mDemoInetCondition][mDemoMobileLevel]; - for (SignalCluster cluster : mSignalClusters) { - cluster.setMobileDataIndicators( - show, - iconId, - mDemoDataTypeIconId, - "Demo", - "Demo", - mDemoDataTypeIconId == TelephonyIcons.ROAMING_ICON, - isTypeIconWide(mDemoDataTypeIconId)); + + protected void toString(StringBuilder builder) { + builder.append("connected=").append(connected).append(',') + .append("enabled=").append(enabled).append(',') + .append("level=").append(level).append(',') + .append("inetCondition=").append(inetCondition).append(',') + .append("iconGroup=").append(iconGroup).append(',') + .append("activityIn=").append(activityIn).append(',') + .append("activityOut=").append(activityOut).append(',') + .append("rssi=").append(rssi).append(',') + .append("lastModified=").append(DateFormat.format("MM-dd hh:mm:ss", time)); + } + + @Override + public boolean equals(Object o) { + if (!o.getClass().equals(getClass())) { + return false; } - refreshViews(); + State other = (State) o; + return other.connected == connected + && other.enabled == enabled + && other.level == level + && other.inetCondition == inetCondition + && other.iconGroup == iconGroup + && other.activityIn == activityIn + && other.activityOut == activityOut + && other.rssi == rssi; } } } + + public interface SignalCluster { + void setWifiIndicators(boolean visible, int strengthIcon, String contentDescription); + + void setMobileDataIndicators(boolean visible, int strengthIcon, int typeIcon, + String contentDescription, String typeContentDescription, boolean isTypeIconWide, + int subId); + void setSubs(List<SubscriptionInfo> subs); + void setNoSims(boolean show); + + void setIsAirplaneMode(boolean is, int airplaneIcon, int contentDescription); + } + + public interface EmergencyListener { + void setEmergencyCallsOnly(boolean emergencyOnly); + } + + public interface CarrierLabelListener { + void setCarrierLabel(String label); + } + + @VisibleForTesting + static class Config { + boolean showAtLeast3G = false; + boolean alwaysShowCdmaRssi = false; + boolean show4gForLte = false; + boolean hspaDataDistinguishable; + + static Config readConfig(Context context) { + Config config = new Config(); + Resources res = context.getResources(); + + config.showAtLeast3G = res.getBoolean(R.bool.config_showMin3G); + config.alwaysShowCdmaRssi = + res.getBoolean(com.android.internal.R.bool.config_alwaysUseCdmaRssi); + config.show4gForLte = res.getBoolean(R.bool.config_show4GForLTE); + config.hspaDataDistinguishable = + res.getBoolean(R.bool.config_hspa_data_distinguishable); + return config; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java index 8f1f7c7..787acc5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java @@ -39,7 +39,7 @@ public class NextAlarmController extends BroadcastReceiver { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); - context.registerReceiver(this, filter); + context.registerReceiverAsUser(this, UserHandle.ALL, filter, null, null); updateNextAlarm(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitClockView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitClockView.java index e7c4ede..50e3977 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitClockView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitClockView.java @@ -16,10 +16,12 @@ package com.android.systemui.statusbar.policy; +import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.UserHandle; import android.text.format.DateFormat; import android.util.AttributeSet; import android.widget.LinearLayout; @@ -42,7 +44,9 @@ public class SplitClockView extends LinearLayout { final String action = intent.getAction(); if (Intent.ACTION_TIME_CHANGED.equals(action) || Intent.ACTION_TIMEZONE_CHANGED.equals(action) - || Intent.ACTION_LOCALE_CHANGED.equals(action)) { + || Intent.ACTION_LOCALE_CHANGED.equals(action) + || Intent.ACTION_CONFIGURATION_CHANGED.equals(action) + || Intent.ACTION_USER_SWITCHED.equals(action)) { updatePatterns(); } } @@ -57,6 +61,8 @@ public class SplitClockView extends LinearLayout { super.onFinishInflate(); mTimeView = (TextClock) findViewById(R.id.time_view); mAmPmView = (TextClock) findViewById(R.id.am_pm_view); + mTimeView.setShowCurrentUserTime(true); + mAmPmView.setShowCurrentUserTime(true); } @Override @@ -67,7 +73,9 @@ public class SplitClockView extends LinearLayout { filter.addAction(Intent.ACTION_TIME_CHANGED); filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); filter.addAction(Intent.ACTION_LOCALE_CHANGED); - getContext().registerReceiver(mIntentReceiver, filter, null, null); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + filter.addAction(Intent.ACTION_USER_SWITCHED); + getContext().registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, null, null); updatePatterns(); } @@ -79,7 +87,8 @@ public class SplitClockView extends LinearLayout { } private void updatePatterns() { - String formatString = DateFormat.getTimeFormatString(getContext()); + String formatString = DateFormat.getTimeFormatString(getContext(), + ActivityManager.getCurrentUser()); int index = getAmPmPartEndIndex(formatString); String timeString; String amPmString; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java index 1f2b918..4091619 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java @@ -17,11 +17,16 @@ package com.android.systemui.statusbar.policy; import com.android.systemui.R; +import com.android.systemui.statusbar.policy.NetworkControllerImpl.MobileSignalController.MobileIconGroup; class TelephonyIcons { //***** Signal strength icons + static final int TELEPHONY_NUM_LEVELS = 5; + //GSM/UMTS + static final int TELEPHONY_NO_NETWORK = R.drawable.stat_sys_signal_null; + static final int[][] TELEPHONY_SIGNAL_STRENGTH = { { R.drawable.stat_sys_signal_0, R.drawable.stat_sys_signal_1, @@ -35,6 +40,8 @@ class TelephonyIcons { R.drawable.stat_sys_signal_4_fully } }; + static final int QS_TELEPHONY_NO_NETWORK = R.drawable.ic_qs_signal_no_signal; + static final int[][] QS_TELEPHONY_SIGNAL_STRENGTH = { { R.drawable.ic_qs_signal_0, R.drawable.ic_qs_signal_1, @@ -66,8 +73,6 @@ class TelephonyIcons { R.drawable.ic_qs_signal_r }; - static final int[][] DATA_SIGNAL_STRENGTH = TELEPHONY_SIGNAL_STRENGTH; - //***** Data connection icons //GSM/UMTS @@ -191,6 +196,9 @@ class TelephonyIcons { static final int FLIGHT_MODE_ICON = R.drawable.stat_sys_airplane_mode; static final int ROAMING_ICON = R.drawable.stat_sys_data_fully_connected_roam; static final int ICON_LTE = R.drawable.stat_sys_data_fully_connected_lte; + static final int ICON_G = R.drawable.stat_sys_data_fully_connected_g; + static final int ICON_E = R.drawable.stat_sys_data_fully_connected_e; + static final int ICON_H = R.drawable.stat_sys_data_fully_connected_h; static final int ICON_3G = R.drawable.stat_sys_data_fully_connected_3g; static final int ICON_4G = R.drawable.stat_sys_data_fully_connected_4g; static final int ICON_1X = R.drawable.stat_sys_data_fully_connected_1x; @@ -199,5 +207,137 @@ class TelephonyIcons { static final int QS_ICON_3G = R.drawable.ic_qs_signal_3g; static final int QS_ICON_4G = R.drawable.ic_qs_signal_4g; static final int QS_ICON_1X = R.drawable.ic_qs_signal_1x; + + static final MobileIconGroup THREE_G = new MobileIconGroup( + "3G", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_3g, + TelephonyIcons.ICON_3G, + true, + TelephonyIcons.QS_DATA_3G + ); + + static final MobileIconGroup UNKNOWN = new MobileIconGroup( + "Unknown", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + 0, 0, false, new int[2] + ); + + static final MobileIconGroup E = new MobileIconGroup( + "E", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_edge, + TelephonyIcons.ICON_E, + false, + TelephonyIcons.QS_DATA_E + ); + + static final MobileIconGroup ONE_X = new MobileIconGroup( + "1X", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_cdma, + TelephonyIcons.ICON_1X, + true, + TelephonyIcons.QS_DATA_1X + ); + + static final MobileIconGroup G = new MobileIconGroup( + "G", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_gprs, + TelephonyIcons.ICON_G, + false, + TelephonyIcons.QS_DATA_G + ); + + static final MobileIconGroup H = new MobileIconGroup( + "H", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_3_5g, + TelephonyIcons.ICON_H, + false, + TelephonyIcons.QS_DATA_H + ); + + static final MobileIconGroup FOUR_G = new MobileIconGroup( + "4G", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_4g, + TelephonyIcons.ICON_4G, + true, + TelephonyIcons.QS_DATA_4G + ); + + static final MobileIconGroup LTE = new MobileIconGroup( + "LTE", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_lte, + TelephonyIcons.ICON_LTE, + true, + TelephonyIcons.QS_DATA_LTE + ); + + static final MobileIconGroup ROAMING = new MobileIconGroup( + "Roaming", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH_ROAMING, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_roaming, + TelephonyIcons.ROAMING_ICON, + false, + TelephonyIcons.QS_DATA_R + ); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java index d50e39f..a8d4f13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.UserInfo; +import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; @@ -131,8 +132,11 @@ public final class UserInfoController { final int userId = userInfo.id; final boolean isGuest = userInfo.isGuest(); final String userName = userInfo.name; - final int avatarSize - = mContext.getResources().getDimensionPixelSize(R.dimen.max_avatar_size); + + final Resources res = mContext.getResources(); + final int avatarSize = Math.max( + res.getDimensionPixelSize(R.dimen.multi_user_avatar_expanded_size), + res.getDimensionPixelSize(R.dimen.multi_user_avatar_keyguard_size)); final Context context = currentUserContext; mUserInfoTask = new AsyncTask<Void, Void, Pair<String, Drawable>>() { @@ -160,8 +164,9 @@ public final class UserInfoController { // Try and read the display name from the local profile final Cursor cursor = context.getContentResolver().query( ContactsContract.Profile.CONTENT_URI, new String[] { - ContactsContract.CommonDataKinds.Phone._ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME}, - null, null, null); + ContactsContract.CommonDataKinds.Phone._ID, + ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + }, null, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index 5c7909a..4ac41a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -154,10 +154,11 @@ public class UserSwitcherController { Bitmap picture = bitmaps.get(info.id); if (picture == null) { picture = mUserManager.getUserIcon(info.id); - } - if (picture != null) { - picture = BitmapHelper.createCircularClip( - picture, avatarSize, avatarSize); + + if (picture != null) { + picture = BitmapHelper.createCircularClip( + picture, avatarSize, avatarSize); + } } int index = isCurrent ? 0 : records.size(); records.add(index, new UserRecord(info, picture, false /* isGuest */, @@ -416,18 +417,6 @@ public class UserSwitcherController { } } - public int getSwitchableUsers() { - int result = 0; - ArrayList<UserRecord> users = mController.mUsers; - int N = users.size(); - for (int i = 0; i < N; i++) { - if (users.get(i).info != null) { - result++; - } - } - return result; - } - public Drawable getDrawable(Context context, UserRecord item) { if (item.isAddUser) { return context.getDrawable(R.drawable.ic_add_circle_qs); @@ -435,6 +424,10 @@ public class UserSwitcherController { return UserIcons.getDefaultUserIcon(item.isGuest ? UserHandle.USER_NULL : item.info.id, /* light= */ true); } + + public void refresh() { + mController.refreshUsers(UserHandle.USER_NULL); + } } public static final class UserRecord { @@ -499,6 +492,7 @@ public class UserSwitcherController { } else { v = (UserDetailView) convertView; } + v.refreshAdapter(); return v; } @@ -589,4 +583,9 @@ public class UserSwitcherController { } } } + + public static boolean isUserSwitcherAvailable(UserManager um) { + return UserManager.supportsMultipleUsers() && um.isUserSwitcherEnabled(); + } + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiIcons.java index 49af979..c56646f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiIcons.java @@ -45,5 +45,8 @@ class WifiIcons { R.drawable.ic_qs_wifi_full_4 } }; + static final int QS_WIFI_NO_NETWORK = R.drawable.ic_qs_wifi_no_network; + static final int WIFI_NO_NETWORK = R.drawable.stat_sys_wifi_signal_null; + static final int WIFI_LEVEL_COUNT = WIFI_SIGNAL_STRENGTH[0].length; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WimaxIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WimaxIcons.java deleted file mode 100644 index 4877828..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WimaxIcons.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2011 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.policy; - -import com.android.systemui.statusbar.policy.TelephonyIcons; - -class WimaxIcons { - static final int[][] WIMAX_SIGNAL_STRENGTH = TelephonyIcons.DATA_SIGNAL_STRENGTH; - - static final int WIMAX_DISCONNECTED = WIMAX_SIGNAL_STRENGTH[0][0]; - - static final int WIMAX_IDLE = WIMAX_DISCONNECTED; // XXX: unclear if we need a different icon -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index 415eb27..37ed7d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -157,8 +157,9 @@ public class ZenModeControllerImpl implements ZenModeController { if (mRegistered) { mContext.unregisterReceiver(mReceiver); } - mContext.registerReceiverAsUser(mReceiver, new UserHandle(mUserId), - new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED), null, null); + final IntentFilter filter = new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); + filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED); + mContext.registerReceiverAsUser(mReceiver, new UserHandle(mUserId), filter, null, null); mRegistered = true; mSetupObserver.register(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java index 3c93b19..753a7f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java @@ -33,6 +33,8 @@ public class AnimationFilter { boolean animateHideSensitive; boolean hasDelays; boolean hasGoToFullShadeEvent; + boolean hasDarkEvent; + int darkAnimationOriginIndex; public AnimationFilter animateAlpha() { animateAlpha = true; @@ -93,11 +95,17 @@ public class AnimationFilter { reset(); int size = events.size(); for (int i = 0; i < size; i++) { + NotificationStackScrollLayout.AnimationEvent ev = events.get(i); combineFilter(events.get(i).filter); - if (events.get(i).animationType == + if (ev.animationType == NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE) { hasGoToFullShadeEvent = true; } + if (ev.animationType == + NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_DARK) { + hasDarkEvent = true; + darkAnimationOriginIndex = ev.darkAnimationOriginIndex; + } } } @@ -126,5 +134,8 @@ public class AnimationFilter { animateHideSensitive = false; hasDelays = false; hasGoToFullShadeEvent = false; + hasDarkEvent = false; + darkAnimationOriginIndex = + NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java index 4a20406..6dcbed6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -16,10 +16,12 @@ package com.android.systemui.statusbar.stack; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.PointF; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; @@ -40,6 +42,7 @@ import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.SpeedBumpView; +import com.android.systemui.statusbar.StackScrollerDecorView; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.policy.ScrollAdapter; @@ -70,6 +73,12 @@ public class NotificationStackScrollLayout extends ViewGroup private SwipeHelper mSwipeHelper; private boolean mSwipingInProgress; private int mCurrentStackHeight = Integer.MAX_VALUE; + + /** + * mCurrentStackHeight is the actual stack height, mLastSetStackHeight is the stack height set + * externally from {@link #setStackHeight} + */ + private float mLastSetStackHeight; private int mOwnScrollY; private int mMaxLayoutHeight; @@ -84,6 +93,9 @@ public class NotificationStackScrollLayout extends ViewGroup private int mLastMotionY; private int mDownX; private int mActivePointerId; + private boolean mTouchIsClick; + private float mInitialTouchX; + private float mInitialTouchY; private int mSidePaddings; private Paint mDebugPaint; @@ -133,11 +145,13 @@ public class NotificationStackScrollLayout extends ViewGroup private OnChildLocationsChangedListener mListener; private OnOverscrollTopChangedListener mOverscrollTopChangedListener; private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; + private OnEmptySpaceClickListener mOnEmptySpaceClickListener; private boolean mNeedsAnimation; private boolean mTopPaddingNeedsAnimation; private boolean mDimmedNeedsAnimation; private boolean mHideSensitiveNeedsAnimation; private boolean mDarkNeedsAnimation; + private int mDarkAnimationOriginIndex; private boolean mActivateNeedsAnimation; private boolean mGoToFullShadeNeedsAnimation; private boolean mIsExpanded = true; @@ -199,6 +213,7 @@ public class NotificationStackScrollLayout extends ViewGroup } }; private PhoneStatusBar mPhoneStatusBar; + private int[] mTempInt2 = new int[2]; public NotificationStackScrollLayout(Context context) { this(context, null); @@ -367,6 +382,9 @@ public class NotificationStackScrollLayout extends ViewGroup if (childViewState == null) { return ViewState.LOCATION_UNKNOWN; } + if (childViewState.gone) { + return ViewState.LOCATION_GONE; + } return childViewState.location; } @@ -445,11 +463,13 @@ public class NotificationStackScrollLayout extends ViewGroup * @param height the new height of the stack */ public void setStackHeight(float height) { + mLastSetStackHeight = height; setIsExpanded(height > 0.0f); int newStackHeight = (int) height; int minStackHeight = getMinStackHeight(); int stackHeight; - if (newStackHeight - mTopPadding >= minStackHeight || getNotGoneChildCount() == 0) { + if (newStackHeight - mTopPadding - mTopPaddingOverflow >= minStackHeight + || getNotGoneChildCount() == 0) { setTranslationY(mTopPaddingOverflow); stackHeight = newStackHeight; } else { @@ -459,7 +479,8 @@ public class NotificationStackScrollLayout extends ViewGroup int translationY = (newStackHeight - minStackHeight); // A slight parallax effect is introduced in order for the stack to catch up with // the top card. - float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight; + float partiallyThere = (newStackHeight - mTopPadding - mTopPaddingOverflow) + / minStackHeight; partiallyThere = Math.max(0, partiallyThere); translationY += (1 - partiallyThere) * (mBottomStackPeekSize + mCollapseSecondCardPadding); @@ -570,10 +591,38 @@ public class NotificationStackScrollLayout extends ViewGroup return getChildAtPosition(ev.getX(), ev.getY()); } + public ExpandableView getClosestChildAtRawPosition(float touchX, float touchY) { + getLocationOnScreen(mTempInt2); + float localTouchY = touchY - mTempInt2[1]; + + ExpandableView closestChild = null; + float minDist = Float.MAX_VALUE; + + // find the view closest to the location, accounting for GONE views + final int count = getChildCount(); + for (int childIdx = 0; childIdx < count; childIdx++) { + ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); + if (slidingChild.getVisibility() == GONE + || slidingChild instanceof StackScrollerDecorView + || slidingChild == mSpeedBumpView) { + continue; + } + float childTop = slidingChild.getTranslationY(); + float top = childTop + slidingChild.getClipTopAmount(); + float bottom = childTop + slidingChild.getActualHeight(); + + float dist = Math.min(Math.abs(top - localTouchY), Math.abs(bottom - localTouchY)); + if (dist < minDist) { + closestChild = slidingChild; + minDist = dist; + } + } + return closestChild; + } + public ExpandableView getChildAtRawPosition(float touchX, float touchY) { - int[] location = new int[2]; - getLocationOnScreen(location); - return getChildAtPosition(touchX - location[0], touchY - location[1]); + getLocationOnScreen(mTempInt2); + return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]); } public ExpandableView getChildAtPosition(float touchX, float touchY) { @@ -581,7 +630,9 @@ public class NotificationStackScrollLayout extends ViewGroup final int count = getChildCount(); for (int childIdx = 0; childIdx < count; childIdx++) { ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); - if (slidingChild.getVisibility() == GONE) { + if (slidingChild.getVisibility() == GONE + || slidingChild instanceof StackScrollerDecorView + || slidingChild == mSpeedBumpView) { continue; } float childTop = slidingChild.getTranslationY(); @@ -687,6 +738,7 @@ public class NotificationStackScrollLayout extends ViewGroup transformTouchEvent(ev, this, mScrollView); return mScrollView.onTouchEvent(ev); } + handleEmptySpaceClick(ev); boolean expandWantsIt = false; if (!mSwipingInProgress && !mOnlyScrollingInThisMotion && isScrollingEnabled()) { if (isCancelOrUp) { @@ -1338,7 +1390,19 @@ public class NotificationStackScrollLayout extends ViewGroup && initialVelocity > 0; } - public void updateTopPadding(float qsHeight, int scrollY, boolean animate) { + /** + * Updates the top padding of the notifications, taking {@link #getIntrinsicPadding()} into + * account. + * + * @param qsHeight the top padding imposed by the quick settings panel + * @param scrollY how much the notifications are scrolled inside the QS/notifications scroll + * container + * @param animate whether to animate the change + * @param ignoreIntrinsicPadding if true, {@link #getIntrinsicPadding()} is ignored and + * {@code qsHeight} is the final top padding + */ + public void updateTopPadding(float qsHeight, int scrollY, boolean animate, + boolean ignoreIntrinsicPadding) { float start = qsHeight - scrollY + mNotificationTopPadding; float stackHeight = getHeight() - start; int minStackHeight = getMinStackHeight(); @@ -1346,13 +1410,13 @@ public class NotificationStackScrollLayout extends ViewGroup float overflow = minStackHeight - stackHeight; stackHeight = minStackHeight; start = getHeight() - stackHeight; - setTranslationY(overflow); mTopPaddingOverflow = overflow; } else { - setTranslationY(0); mTopPaddingOverflow = 0; } - setTopPadding(clampPadding((int) start), animate); + setTopPadding(ignoreIntrinsicPadding ? (int) start : clampPadding((int) start), + animate); + setStackHeight(mLastSetStackHeight); } public int getNotificationTopPadding() { @@ -1430,6 +1494,7 @@ public class NotificationStackScrollLayout extends ViewGroup transformTouchEvent(ev, mScrollView, this); } initDownStates(ev); + handleEmptySpaceClick(ev); boolean expandWantsIt = false; if (!mSwipingInProgress && !mOnlyScrollingInThisMotion && isScrollingEnabled()) { expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev); @@ -1448,11 +1513,31 @@ public class NotificationStackScrollLayout extends ViewGroup return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev); } + private void handleEmptySpaceClick(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_MOVE: + if (mTouchIsClick && (Math.abs(ev.getY() - mInitialTouchY) > mTouchSlop + || Math.abs(ev.getX() - mInitialTouchX) > mTouchSlop )) { + mTouchIsClick = false; + } + break; + case MotionEvent.ACTION_UP: + if (mPhoneStatusBar.getBarState() != StatusBarState.KEYGUARD && mTouchIsClick && + isBelowLastNotification(mInitialTouchX, mInitialTouchY)) { + mOnEmptySpaceClickListener.onEmptySpaceClicked(mInitialTouchX, mInitialTouchY); + } + break; + } + } + private void initDownStates(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { mExpandedInThisMotion = false; mOnlyScrollingInThisMotion = !mScroller.isFinished(); mDisallowScrollingInThisMotion = false; + mTouchIsClick = true; + mInitialTouchX = ev.getX(); + mInitialTouchY = ev.getY(); } } @@ -1553,6 +1638,12 @@ public class NotificationStackScrollLayout extends ViewGroup ((ExpandableView) child).setOnHeightChangedListener(this); generateAddAnimation(child, false /* fromMoreCard */); updateAnimationState(child); + if (canChildBeDismissed(child)) { + // Make sure the dismissButton is visible and not in the animated state. + // We need to do this to avoid a race where a clearable notification is added after the + // dismiss animation is finished + mDismissView.showClearButton(); + } } public void setAnimationsEnabled(boolean animationsEnabled) { @@ -1759,8 +1850,9 @@ public class NotificationStackScrollLayout extends ViewGroup private void generateDarkEvent() { if (mDarkNeedsAnimation) { - mAnimationEvents.add( - new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DARK)); + AnimationEvent ev = new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DARK); + ev.darkAnimationOriginIndex = mDarkAnimationOriginIndex; + mAnimationEvents.add(ev); } mDarkNeedsAnimation = false; } @@ -1886,7 +1978,14 @@ public class NotificationStackScrollLayout extends ViewGroup * @return Whether the specified motion event is actually happening over the content. */ private boolean isInContentBounds(MotionEvent event) { - return event.getY() < getHeight() - getEmptyBottomMargin(); + return isInContentBounds(event.getY()); + } + + /** + * @return Whether a y coordinate is inside the content. + */ + public boolean isInContentBounds(float y) { + return y < getHeight() - getEmptyBottomMargin(); } private void setIsBeingDragged(boolean isDragged) { @@ -1944,6 +2043,9 @@ public class NotificationStackScrollLayout extends ViewGroup mStackScrollAlgorithm.onExpansionStopped(); if (!mIsExpanded) { mOwnScrollY = 0; + + // lets make sure nothing is in the overlay anymore + getOverlay().clear(); } } @@ -1995,6 +2097,10 @@ public class NotificationStackScrollLayout extends ViewGroup this.mOnHeightChangedListener = mOnHeightChangedListener; } + public void setOnEmptySpaceClickListener(OnEmptySpaceClickListener listener) { + mOnEmptySpaceClickListener = listener; + } + public void onChildAnimationFinished() { requestChildrenUpdate(); } @@ -2078,7 +2184,7 @@ public class NotificationStackScrollLayout extends ViewGroup mEmptyShadeView.setInvisible(); mGoToFullShadeNeedsAnimation = true; mGoToFullShadeDelay = delay; - mNeedsAnimation = true; + mNeedsAnimation = true; requestChildrenUpdate(); } @@ -2109,15 +2215,46 @@ public class NotificationStackScrollLayout extends ViewGroup /** * See {@link AmbientState#setDark}. */ - public void setDark(boolean dark, boolean animate) { + public void setDark(boolean dark, boolean animate, @Nullable PointF touchWakeUpScreenLocation) { mAmbientState.setDark(dark); if (animate && mAnimationsEnabled) { mDarkNeedsAnimation = true; + mDarkAnimationOriginIndex = findDarkAnimationOriginIndex(touchWakeUpScreenLocation); mNeedsAnimation = true; } requestChildrenUpdate(); } + private int findDarkAnimationOriginIndex(@Nullable PointF screenLocation) { + if (screenLocation == null || screenLocation.y < mTopPadding + mTopPaddingOverflow) { + return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE; + } + if (screenLocation.y > getBottomMostNotificationBottom()) { + return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_BELOW; + } + View child = getClosestChildAtRawPosition(screenLocation.x, screenLocation.y); + if (child != null) { + return getNotGoneIndex(child); + } else { + return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE; + } + } + + private int getNotGoneIndex(View child) { + int count = getChildCount(); + int notGoneIndex = 0; + for (int i = 0; i < count; i++) { + View v = getChildAt(i); + if (child == v) { + return notGoneIndex; + } + if (v.getVisibility() != View.GONE) { + notGoneIndex++; + } + } + return -1; + } + public void setDismissView(DismissView dismissView) { mDismissView = dismissView; addView(mDismissView); @@ -2143,8 +2280,7 @@ public class NotificationStackScrollLayout extends ViewGroup updateContentHeight(); notifyHeightChangeListener(mDismissView); } else { - mEmptyShadeView.setWillBeGone(true); - mEmptyShadeView.performVisibilityAnimation(false, new Runnable() { + Runnable onFinishedRunnable = new Runnable() { @Override public void run() { mEmptyShadeView.setVisibility(GONE); @@ -2152,7 +2288,14 @@ public class NotificationStackScrollLayout extends ViewGroup updateContentHeight(); notifyHeightChangeListener(mDismissView); } - }); + }; + if (mAnimationsEnabled) { + mEmptyShadeView.setWillBeGone(true); + mEmptyShadeView.performVisibilityAnimation(false, onFinishedRunnable); + } else { + mEmptyShadeView.setInvisible(); + onFinishedRunnable.run(); + } } } } @@ -2172,8 +2315,7 @@ public class NotificationStackScrollLayout extends ViewGroup updateContentHeight(); notifyHeightChangeListener(mDismissView); } else { - mDismissView.setWillBeGone(true); - mDismissView.performVisibilityAnimation(false, new Runnable() { + Runnable dimissHideFinishRunnable = new Runnable() { @Override public void run() { mDismissView.setVisibility(GONE); @@ -2181,13 +2323,21 @@ public class NotificationStackScrollLayout extends ViewGroup updateContentHeight(); notifyHeightChangeListener(mDismissView); } - }); + }; + if (mDismissView.isButtonVisible() && mIsExpanded && mAnimationsEnabled) { + mDismissView.setWillBeGone(true); + mDismissView.performVisibilityAnimation(false, dimissHideFinishRunnable); + } else { + dimissHideFinishRunnable.run(); + mDismissView.showClearButton(); + } } } } public void setDismissAllInProgress(boolean dismissAllInProgress) { mDismissAllInProgress = dismissAllInProgress; + mDismissView.setDismissAllInProgress(dismissAllInProgress); } public boolean isDismissViewNotGone() { @@ -2210,6 +2360,10 @@ public class NotificationStackScrollLayout extends ViewGroup return height; } + public int getEmptyShadeViewHeight() { + return mEmptyShadeView.getHeight(); + } + public float getBottomMostNotificationBottom() { final int count = getChildCount(); float max = 0; @@ -2245,6 +2399,24 @@ public class NotificationStackScrollLayout extends ViewGroup } } + private boolean isBelowLastNotification(float touchX, float touchY) { + ExpandableView lastChildNotGone = (ExpandableView) getLastChildNotGone(); + if (lastChildNotGone == null) { + return touchY > mIntrinsicPadding; + } + if (lastChildNotGone != mDismissView && lastChildNotGone != mEmptyShadeView) { + return touchY > lastChildNotGone.getY() + lastChildNotGone.getActualHeight(); + } else if (lastChildNotGone == mEmptyShadeView) { + return touchY > mEmptyShadeView.getY(); + } else { + float dismissY = mDismissView.getY(); + boolean belowDismissView = touchY > dismissY + mDismissView.getActualHeight(); + return belowDismissView || (touchY > dismissY + && mDismissView.isOnEmptySpace(touchX - mDismissView.getX(), + touchY - dismissY)); + } + } + /** * A listener that is notified when some child locations might have changed. */ @@ -2253,6 +2425,13 @@ public class NotificationStackScrollLayout extends ViewGroup } /** + * A listener that is notified when the empty space below the notifications is clicked on + */ + public interface OnEmptySpaceClickListener { + public void onEmptySpaceClicked(float x, float y); + } + + /** * A listener that gets notified when the overscroll at the top has changed. */ public interface OnOverscrollTopChangedListener { @@ -2348,7 +2527,8 @@ public class NotificationStackScrollLayout extends ViewGroup // ANIMATION_TYPE_DARK new AnimationFilter() - .animateDark(), + .animateDark() + .hasDelays(), // ANIMATION_TYPE_GO_TO_FULL_SHADE new AnimationFilter() @@ -2446,12 +2626,16 @@ public class NotificationStackScrollLayout extends ViewGroup static final int ANIMATION_TYPE_VIEW_RESIZE = 12; static final int ANIMATION_TYPE_EVERYTHING = 13; + static final int DARK_ANIMATION_ORIGIN_INDEX_ABOVE = -1; + static final int DARK_ANIMATION_ORIGIN_INDEX_BELOW = -2; + final long eventStartTime; final View changingView; final int animationType; final AnimationFilter filter; final long length; View viewAfterChangingView; + int darkAnimationOriginIndex; AnimationEvent(View view, int type) { this(view, type, LENGTHS[type]); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java index 853628e..ddc4251 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java @@ -200,15 +200,25 @@ public class StackScrollAlgorithm { // apply clipping and shadow float newNotificationEnd = newYTranslation + newHeight; - // In the unlocked shade we have to clip a little bit higher because of the rounded - // corners of the notifications. - float clippingCorrection = state.dimmed ? 0 : mRoundedRectCornerRadius * state.scale; - - // When the previous notification is swiped, we don't clip the content to the - // bottom of it. - float clipHeight = previousNotificationIsSwiped - ? newHeight - : newNotificationEnd - (previousNotificationEnd - clippingCorrection); + float clipHeight; + if (previousNotificationIsSwiped) { + // When the previous notification is swiped, we don't clip the content to the + // bottom of it. + clipHeight = newHeight; + } else { + clipHeight = newNotificationEnd - previousNotificationEnd; + clipHeight = Math.max(0.0f, clipHeight); + if (clipHeight != 0.0f) { + + // In the unlocked shade we have to clip a little bit higher because of the rounded + // corners of the notifications, but only if we are not fully overlapped by + // the top card. + float clippingCorrection = state.dimmed + ? 0 + : mRoundedRectCornerRadius * state.scale; + clipHeight += clippingCorrection; + } + } updateChildClippingAndBackground(state, newHeight, clipHeight, newHeight - (previousNotificationStart - newYTranslation)); @@ -669,7 +679,11 @@ public class StackScrollAlgorithm { StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); if (i < algorithmState.itemsInTopStack) { float stackIndex = algorithmState.itemsInTopStack - i; - stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2); + + // Ensure that the topmost item is a little bit higher than the rest when fully + // scrolled, to avoid drawing errors when swiping it out + float max = MAX_ITEMS_IN_TOP_STACK + (i == 0 ? 2.5f : 2); + stackIndex = Math.min(stackIndex, max); if (i == 0 && algorithmState.itemsInTopStack < 2.0f) { // We only have the top item and an additional item in the top stack, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java index 0967ecd..0b1ce8f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java @@ -119,9 +119,7 @@ public class StackScrollState { } // apply alpha - if (!becomesInvisible) { - child.setAlpha(newAlpha); - } + child.setAlpha(newAlpha); } // apply visibility @@ -156,7 +154,7 @@ public class StackScrollState { child.setDimmed(state.dimmed, false /* animate */); // apply dark - child.setDark(state.dark, false /* animate */); + child.setDark(state.dark, false /* animate */, 0 /* delay */); // apply hiding sensitive child.setHideSensitive( @@ -236,6 +234,8 @@ public class StackScrollState { public static final int LOCATION_MAIN_AREA = 0x08; public static final int LOCATION_BOTTOM_STACK_PEEKING = 0x10; public static final int LOCATION_BOTTOM_STACK_HIDDEN = 0x20; + /** The view isn't layouted at all. */ + public static final int LOCATION_GONE = 0x40; float alpha; float yTranslation; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java index 433357e..b027787 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java @@ -46,6 +46,7 @@ public class StackStateAnimator { public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80; public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32; public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48; + public static final int ANIMATION_DELAY_PER_ELEMENT_DARK = 24; private static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2; private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag; @@ -161,11 +162,12 @@ public class StackStateAnimator { boolean scaleChanging = child.getScaleX() != viewState.scale; boolean alphaChanging = alpha != child.getAlpha(); boolean heightChanging = viewState.height != child.getActualHeight(); + boolean darkChanging = viewState.dark != child.isDark(); boolean topInsetChanging = viewState.clipTopAmount != child.getClipTopAmount(); boolean wasAdded = mNewAddChildren.contains(child); boolean hasDelays = mAnimationFilter.hasDelays; boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || scaleChanging || - alphaChanging || heightChanging || topInsetChanging; + alphaChanging || heightChanging || topInsetChanging || darkChanging; boolean noAnimation = wasAdded; long delay = 0; long duration = mCurrentLength; @@ -242,7 +244,7 @@ public class StackStateAnimator { && !noAnimation); // start dark animation - child.setDark(viewState.dark, mAnimationFilter.animateDark && !noAnimation); + child.setDark(viewState.dark, mAnimationFilter.animateDark && !noAnimation, delay); // apply speed bump state child.setBelowSpeedBump(viewState.belowSpeedBump); @@ -262,6 +264,9 @@ public class StackStateAnimator { private long calculateChildAnimationDelay(StackScrollState.ViewState viewState, StackScrollState finalState) { + if (mAnimationFilter.hasDarkEvent) { + return calculateDelayDark(viewState); + } if (mAnimationFilter.hasGoToFullShadeEvent) { return calculateDelayGoToFullShade(viewState); } @@ -309,6 +314,20 @@ public class StackStateAnimator { return minDelay; } + private long calculateDelayDark(StackScrollState.ViewState viewState) { + int referenceIndex; + if (mAnimationFilter.darkAnimationOriginIndex == + NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE) { + referenceIndex = 0; + } else if (mAnimationFilter.darkAnimationOriginIndex == + NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_BELOW) { + referenceIndex = mHostLayout.getNotGoneChildCount() - 1; + } else { + referenceIndex = mAnimationFilter.darkAnimationOriginIndex; + } + return Math.abs(referenceIndex - viewState.notGoneIndex) * ANIMATION_DELAY_PER_ELEMENT_DARK; + } + private long calculateDelayGoToFullShade(StackScrollState.ViewState viewState) { float index = viewState.notGoneIndex; index = (float) Math.pow(index, 0.7f); @@ -477,6 +496,7 @@ public class StackStateAnimator { if (newEndValue == 0 && !mWasCancelled) { child.setVisibility(View.INVISIBLE); } + // remove the tag when the animation is finished child.setTag(TAG_ANIMATOR_ALPHA, null); child.setTag(TAG_START_ALPHA, null); child.setTag(TAG_END_ALPHA, null); @@ -498,13 +518,7 @@ public class StackStateAnimator { animator.setStartDelay(delay); } animator.addListener(getGlobalAnimationFinishedListener()); - // remove the tag when the animation is finished - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - } - }); startAnimator(animator); child.setTag(TAG_ANIMATOR_ALPHA, animator); child.setTag(TAG_START_ALPHA, child.getAlpha()); @@ -799,6 +813,11 @@ public class StackStateAnimator { mHostLayout.getOverlay().remove(changingView); } }); + } else if (event.animationType == + NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) { + // A race condition can trigger the view to be added to the overlay even though + // it is swiped out. So let's remove it + mHostLayout.getOverlay().remove(changingView); } mNewEvents.add(event); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java index 1b6a9e1..08732e5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java @@ -178,4 +178,8 @@ public class TvStatusBar extends BaseStatusBar { @Override public void onActivationReset(ActivatableNotificationView view) { } + + @Override + public void showScreenPinningRequest() { + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/IconPulser.java b/packages/SystemUI/src/com/android/systemui/volume/IconPulser.java new file mode 100644 index 0000000..9438af1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/IconPulser.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 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.volume; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +public class IconPulser { + private static final float PULSE_SCALE = 1.1f; + + private final Interpolator mFastOutSlowInInterpolator; + + public IconPulser(Context context) { + mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, + android.R.interpolator.fast_out_slow_in); + } + + public void start(final View target) { + if (target == null || target.getScaleX() != 1) return; // n/a, or already running + target.animate().cancel(); + target.animate().scaleX(PULSE_SCALE).scaleY(PULSE_SCALE) + .setInterpolator(mFastOutSlowInInterpolator) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + target.animate().scaleX(1).scaleY(1).setListener(null); + } + }); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java b/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java index f7f5047..2f02f7c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java +++ b/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java @@ -17,7 +17,6 @@ package com.android.systemui.volume; import android.content.Context; -import android.graphics.Typeface; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -30,8 +29,6 @@ import com.android.systemui.R; import java.util.Objects; public class SegmentedButtons extends LinearLayout { - private static final Typeface MEDIUM = Typeface.create("sans-serif-medium", Typeface.NORMAL); - private static final Typeface BOLD = Typeface.create("sans-serif", Typeface.BOLD); private static final int LABEL_RES_KEY = R.id.label; private final Context mContext; @@ -63,15 +60,17 @@ public class SegmentedButtons extends LinearLayout { final Object tag = c.getTag(); final boolean selected = Objects.equals(mSelectedValue, tag); c.setSelected(selected); - c.setTypeface(selected ? BOLD : MEDIUM); + c.getCompoundDrawables()[1].setTint(mContext.getResources().getColor(selected + ? R.color.segmented_button_selected : R.color.segmented_button_unselected)); } fireOnSelected(); } - public void addButton(int labelResId, Object value) { + public void addButton(int labelResId, int iconResId, Object value) { final Button b = (Button) mInflater.inflate(R.layout.segmented_button, this, false); b.setTag(LABEL_RES_KEY, labelResId); b.setText(labelResId); + b.setCompoundDrawablesWithIntrinsicBounds(0, iconResId, 0, 0); final LayoutParams lp = (LayoutParams) b.getLayoutParams(); if (getChildCount() == 0) { lp.leftMargin = lp.rightMargin = 0; // first button has no margin diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java index 3c186c2..e3f8f3d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java @@ -16,8 +16,10 @@ package com.android.systemui.volume; +import com.android.systemui.DemoMode; import com.android.systemui.statusbar.policy.ZenModeController; -public interface VolumeComponent { +public interface VolumeComponent extends DemoMode { ZenModeController getZenController(); + void dismissNow(); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java index 360dee5..acdcfc1 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java @@ -16,6 +16,9 @@ package com.android.systemui.volume; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.app.AlertDialog; import android.app.Dialog; import android.content.BroadcastReceiver; @@ -42,6 +45,8 @@ import android.media.VolumeProvider; import android.media.session.MediaController; import android.media.session.MediaController.PlaybackInfo; import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; import android.os.Debug; import android.os.Handler; import android.os.Message; @@ -59,12 +64,15 @@ import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; import com.android.internal.R; +import com.android.systemui.DemoMode; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.ZenModeController; @@ -76,7 +84,7 @@ import java.io.PrintWriter; * * @hide */ -public class VolumePanel extends Handler { +public class VolumePanel extends Handler implements DemoMode { private static final String TAG = "VolumePanel"; private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); @@ -116,6 +124,7 @@ public class VolumePanel extends Handler { private static final int MSG_ZEN_MODE_AVAILABLE_CHANGED = 13; private static final int MSG_USER_ACTIVITY = 14; private static final int MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED = 15; + private static final int MSG_INTERNAL_RINGER_MODE_CHANGED = 16; // Pseudo stream type for master volume private static final int STREAM_MASTER = -100; @@ -129,6 +138,8 @@ public class VolumePanel extends Handler { private static final int IC_AUDIO_VOL = com.android.systemui.R.drawable.ic_audio_vol; private static final int IC_AUDIO_VOL_MUTE = com.android.systemui.R.drawable.ic_audio_vol_mute; + private static final int IC_AUDIO_BT = com.android.systemui.R.drawable.ic_audio_bt; + private static final int IC_AUDIO_BT_MUTE = com.android.systemui.R.drawable.ic_audio_bt_mute; private final String mTag; protected final Context mContext; @@ -142,6 +153,7 @@ public class VolumePanel extends Handler { private float mDisabledAlpha; private int mLastRingerMode = AudioManager.RINGER_MODE_NORMAL; private int mLastRingerProgress = 0; + private int mDemoIcon; // True if we want to play tones on the system stream when the master stream is specified. private final boolean mPlayMasterStreamTones; @@ -158,6 +170,8 @@ public class VolumePanel extends Handler { private final ViewGroup mSliderPanel; /** The zen mode configuration panel view */ private ZenModePanel mZenPanel; + /** The component currently suppressing notification stream effects */ + private ComponentName mNotificationEffectsSuppressor; private Callback mCallback; @@ -166,22 +180,24 @@ public class VolumePanel extends Handler { /** All the slider controls mapped by stream type */ private SparseArray<StreamControl> mStreamControls; private final AccessibilityManager mAccessibilityManager; + private final SecondaryIconTransition mSecondaryIconTransition; + private final IconPulser mIconPulser; private enum StreamResources { BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO, R.string.volume_icon_description_bluetooth, - R.drawable.ic_audio_bt, - R.drawable.ic_audio_bt, + IC_AUDIO_BT, + IC_AUDIO_BT_MUTE, false), RingerStream(AudioManager.STREAM_RING, R.string.volume_icon_description_ringer, com.android.systemui.R.drawable.ic_ringer_audible, - com.android.systemui.R.drawable.ic_ringer_vibrate, + com.android.systemui.R.drawable.ic_ringer_mute, false), VoiceStream(AudioManager.STREAM_VOICE_CALL, R.string.volume_icon_description_incall, - R.drawable.ic_audio_phone, - R.drawable.ic_audio_phone, + com.android.systemui.R.drawable.ic_audio_phone, + com.android.systemui.R.drawable.ic_audio_phone, false), AlarmStream(AudioManager.STREAM_ALARM, R.string.volume_alarm, @@ -196,7 +212,7 @@ public class VolumePanel extends Handler { NotificationStream(AudioManager.STREAM_NOTIFICATION, R.string.volume_icon_description_notification, com.android.systemui.R.drawable.ic_ringer_audible, - com.android.systemui.R.drawable.ic_ringer_vibrate, + com.android.systemui.R.drawable.ic_ringer_mute, true), // for now, use media resources for master volume MasterStream(STREAM_MASTER, @@ -206,8 +222,8 @@ public class VolumePanel extends Handler { false), RemoteStream(STREAM_REMOTE_MUSIC, R.string.volume_icon_description_media, //FIXME should have its own description - R.drawable.ic_media_route_on_holo_dark, - R.drawable.ic_media_route_disabled_holo_dark, + com.android.systemui.R.drawable.ic_audio_remote, + com.android.systemui.R.drawable.ic_audio_remote, false);// will be dynamically updated int streamType; @@ -246,6 +262,8 @@ public class VolumePanel extends Handler { ImageView icon; SeekBar seekbarView; TextView suppressorView; + View divider; + ImageView secondaryIcon; int iconRes; int iconMuteRes; int iconSuppressedRes; @@ -254,6 +272,7 @@ public class VolumePanel extends Handler { // Synchronize when accessing this private ToneGenerator mToneGenerators[]; private Vibrator mVibrator; + private boolean mHasVibrator; private static AlertDialog sSafetyWarning; private static Object sSafetyWarningLock = new Object(); @@ -339,6 +358,8 @@ public class VolumePanel extends Handler { mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mAccessibilityManager = (AccessibilityManager) context.getSystemService( Context.ACCESSIBILITY_SERVICE); + mSecondaryIconTransition = new SecondaryIconTransition(); + mIconPulser = new IconPulser(context); // For now, only show master volume if master volume is supported final Resources res = context.getResources(); @@ -381,6 +402,8 @@ public class VolumePanel extends Handler { mActiveStreamType = -1; mAudioManager.forceVolumeControlStream(mActiveStreamType); setZenPanelVisible(false); + mDemoIcon = 0; + mSecondaryIconTransition.cancel(); } }); @@ -418,10 +441,12 @@ public class VolumePanel extends Handler { mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + mHasVibrator = mVibrator != null && mVibrator.hasVibrator(); mVoiceCapable = context.getResources().getBoolean(R.bool.config_voice_capable); if (mZenController != null && !useMasterVolume) { mZenModeAvailable = mZenController.isZenAvailable(); + mNotificationEffectsSuppressor = mZenController.getEffectsSuppressor(); mZenController.addCallback(mZenCallback); } @@ -453,8 +478,10 @@ public class VolumePanel extends Handler { pw.print(" mTag="); pw.println(mTag); pw.print(" mRingIsSilent="); pw.println(mRingIsSilent); pw.print(" mVoiceCapable="); pw.println(mVoiceCapable); + pw.print(" mHasVibrator="); pw.println(mHasVibrator); pw.print(" mZenModeAvailable="); pw.println(mZenModeAvailable); pw.print(" mZenPanelExpanded="); pw.println(mZenPanelExpanded); + pw.print(" mNotificationEffectsSuppressor="); pw.println(mNotificationEffectsSuppressor); pw.print(" mTimeoutDelay="); pw.println(mTimeoutDelay); pw.print(" mDisabledAlpha="); pw.println(mDisabledAlpha); pw.print(" mLastRingerMode="); pw.println(mLastRingerMode); @@ -483,6 +510,9 @@ public class VolumePanel extends Handler { pw.println(); } } + if (mZenPanel != null) { + mZenPanel.dump(fd, pw, args); + } } private void initZenModePanel() { @@ -518,6 +548,7 @@ public class VolumePanel extends Handler { private void registerReceiver() { final IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); + filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); filter.addAction(Intent.ACTION_SCREEN_OFF); mContext.registerReceiver(new BroadcastReceiver() { @Override @@ -526,7 +557,12 @@ public class VolumePanel extends Handler { if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { removeMessages(MSG_RINGER_MODE_CHANGED); - sendMessage(obtainMessage(MSG_RINGER_MODE_CHANGED)); + sendEmptyMessage(MSG_RINGER_MODE_CHANGED); + } + + if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) { + removeMessages(MSG_INTERNAL_RINGER_MODE_CHANGED); + sendEmptyMessage(MSG_INTERNAL_RINGER_MODE_CHANGED); } if (Intent.ACTION_SCREEN_OFF.equals(action)) { @@ -604,10 +640,12 @@ public class VolumePanel extends Handler { mStreamControls = new SparseArray<StreamControl>(STREAMS.length); + final StreamResources notificationStream = StreamResources.NotificationStream; for (int i = 0; i < STREAMS.length; i++) { StreamResources streamRes = STREAMS[i]; final int streamType = streamRes.streamType; + final boolean isNotification = isNotificationOrRing(streamType); final StreamControl sc = new StreamControl(); sc.streamType = streamType; @@ -620,22 +658,42 @@ public class VolumePanel extends Handler { sc.iconRes = streamRes.iconRes; sc.iconMuteRes = streamRes.iconMuteRes; sc.icon.setImageResource(sc.iconRes); - sc.icon.setClickable(isNotificationOrRing(streamType)); - if (sc.icon.isClickable()) { - sc.icon.setSoundEffectsEnabled(false); - sc.icon.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - resetTimeout(); - toggle(sc); - } - }); + sc.icon.setClickable(isNotification && mHasVibrator); + if (isNotification) { + if (mHasVibrator) { + sc.icon.setSoundEffectsEnabled(false); + sc.iconMuteRes = com.android.systemui.R.drawable.ic_ringer_vibrate; + sc.icon.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + resetTimeout(); + toggleRinger(sc); + } + }); + } sc.iconSuppressedRes = com.android.systemui.R.drawable.ic_ringer_mute; } sc.seekbarView = (SeekBar) sc.group.findViewById(com.android.systemui.R.id.seekbar); sc.suppressorView = (TextView) sc.group.findViewById(com.android.systemui.R.id.suppressor); sc.suppressorView.setVisibility(View.GONE); + final boolean showSecondary = !isNotification && notificationStream.show; + sc.divider = sc.group.findViewById(com.android.systemui.R.id.divider); + sc.secondaryIcon = (ImageView) sc.group + .findViewById(com.android.systemui.R.id.secondary_icon); + sc.secondaryIcon.setImageResource(com.android.systemui.R.drawable.ic_ringer_audible); + sc.secondaryIcon.setContentDescription(res.getString(notificationStream.descRes)); + sc.secondaryIcon.setClickable(showSecondary); + sc.divider.setVisibility(showSecondary ? View.VISIBLE : View.GONE); + sc.secondaryIcon.setVisibility(showSecondary ? View.VISIBLE : View.GONE); + if (showSecondary) { + sc.secondaryIcon.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mSecondaryIconTransition.start(sc); + } + }); + } final int plusOne = (streamType == AudioSystem.STREAM_BLUETOOTH_SCO || streamType == AudioSystem.STREAM_VOICE_CALL) ? 1 : 0; sc.seekbarView.setMax(getStreamMaxVolume(streamType) + plusOne); @@ -645,12 +703,13 @@ public class VolumePanel extends Handler { } } - private void toggle(StreamControl sc) { - if (mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL) { - mAudioManager.setRingerMode(AudioManager.RINGER_MODE_VIBRATE); + private void toggleRinger(StreamControl sc) { + if (!mHasVibrator) return; + if (mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_NORMAL) { + mAudioManager.setRingerModeInternal(AudioManager.RINGER_MODE_VIBRATE); postVolumeChanged(sc.streamType, AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE); } else { - mAudioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL); + mAudioManager.setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL); postVolumeChanged(sc.streamType, AudioManager.FLAG_PLAY_SOUND); } } @@ -666,7 +725,7 @@ public class VolumePanel extends Handler { mSliderPanel.addView(active.group); mActiveStreamType = activeStreamType; active.group.setVisibility(View.VISIBLE); - updateSlider(active); + updateSlider(active, true /*forceReloadIcon*/); updateTimeoutDelay(); updateZenPanelVisible(); } @@ -674,7 +733,7 @@ public class VolumePanel extends Handler { private void updateSliderProgress(StreamControl sc, int progress) { final boolean isRinger = isNotificationOrRing(sc.streamType); - if (isRinger && mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT) { + if (isRinger && mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) { progress = mLastRingerProgress; } if (progress < 0) { @@ -687,30 +746,38 @@ public class VolumePanel extends Handler { } private void updateSliderIcon(StreamControl sc, boolean muted) { + ComponentName suppressor = null; if (isNotificationOrRing(sc.streamType)) { - int ringerMode = mAudioManager.getRingerMode(); + suppressor = mNotificationEffectsSuppressor; + int ringerMode = mAudioManager.getRingerModeInternal(); if (ringerMode == AudioManager.RINGER_MODE_SILENT) { ringerMode = mLastRingerMode; } else { mLastRingerMode = ringerMode; } - muted = ringerMode == AudioManager.RINGER_MODE_VIBRATE; + if (mHasVibrator) { + muted = ringerMode == AudioManager.RINGER_MODE_VIBRATE; + } else { + muted = false; + } } - sc.icon.setImageResource(muted ? sc.iconMuteRes : sc.iconRes); + sc.icon.setImageResource(mDemoIcon != 0 ? mDemoIcon + : suppressor != null ? sc.iconSuppressedRes + : muted ? sc.iconMuteRes + : sc.iconRes); } - private void updateSliderSupressor(StreamControl sc) { + private void updateSliderSuppressor(StreamControl sc) { final ComponentName suppressor = isNotificationOrRing(sc.streamType) - ? mZenController.getEffectsSuppressor() : null; + ? mNotificationEffectsSuppressor : null; if (suppressor == null) { sc.seekbarView.setVisibility(View.VISIBLE); sc.suppressorView.setVisibility(View.GONE); } else { sc.seekbarView.setVisibility(View.GONE); sc.suppressorView.setVisibility(View.VISIBLE); - sc.suppressorView.setText(mContext.getString(com.android.systemui.R.string.muted_by, + sc.suppressorView.setText(mContext.getString(R.string.muted_by, getSuppressorCaption(suppressor))); - sc.icon.setImageResource(sc.iconSuppressedRes); } } @@ -734,14 +801,15 @@ public class VolumePanel extends Handler { } /** Update the mute and progress state of a slider */ - private void updateSlider(StreamControl sc) { + private void updateSlider(StreamControl sc, boolean forceReloadIcon) { updateSliderProgress(sc, -1); final boolean muted = isMuted(sc.streamType); - // Force reloading the image resource - sc.icon.setImageDrawable(null); + if (forceReloadIcon) { + sc.icon.setImageDrawable(null); + } updateSliderIcon(sc, muted); updateSliderEnabled(sc, muted, false); - updateSliderSupressor(sc); + updateSliderSuppressor(sc); } private void updateSliderEnabled(final StreamControl sc, boolean muted, boolean fixedVolume) { @@ -751,13 +819,18 @@ public class VolumePanel extends Handler { // never disable touch interactions for remote playback, the muting is not tied to // the state of the phone. sc.seekbarView.setEnabled(!fixedVolume); - } else if (isRinger && mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT) { + } else if (isRinger && mNotificationEffectsSuppressor != null) { + sc.icon.setEnabled(true); + sc.icon.setAlpha(1f); + sc.icon.setClickable(false); + } else if (isRinger + && mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) { sc.seekbarView.setEnabled(false); sc.icon.setEnabled(false); sc.icon.setAlpha(mDisabledAlpha); sc.icon.setClickable(false); } else if (fixedVolume || - (sc.streamType != mAudioManager.getMasterStreamType() && muted) || + (sc.streamType != mAudioManager.getMasterStreamType() && !isRinger && muted) || (sSafetyWarning != null)) { sc.seekbarView.setEnabled(false); } else { @@ -769,7 +842,7 @@ public class VolumePanel extends Handler { if (isRinger && wasEnabled != sc.seekbarView.isEnabled()) { if (sc.seekbarView.isEnabled()) { sc.group.setOnTouchListener(null); - sc.icon.setClickable(true); + sc.icon.setClickable(mHasVibrator); } else { final View.OnTouchListener showHintOnTouch = new View.OnTouchListener() { @Override @@ -790,6 +863,16 @@ public class VolumePanel extends Handler { } } + private void showVibrateHint() { + final StreamControl active = mStreamControls.get(mActiveStreamType); + if (active != null) { + mIconPulser.start(active.icon); + if (!hasMessages(MSG_VIBRATE)) { + sendEmptyMessageDelayed(MSG_VIBRATE, VIBRATE_DELAY); + } + } + } + private static boolean isNotificationOrRing(int streamType) { return streamType == AudioManager.STREAM_RING || streamType == AudioManager.STREAM_NOTIFICATION; @@ -800,7 +883,8 @@ public class VolumePanel extends Handler { } private void updateTimeoutDelay() { - mTimeoutDelay = sSafetyWarning != null ? TIMEOUT_DELAY_SAFETY_WARNING + mTimeoutDelay = mDemoIcon != 0 ? TIMEOUT_DELAY_EXPANDED + : sSafetyWarning != null ? TIMEOUT_DELAY_SAFETY_WARNING : mActiveStreamType == AudioManager.STREAM_MUSIC ? TIMEOUT_DELAY_SHORT : mZenPanelExpanded ? TIMEOUT_DELAY_EXPANDED : isZenPanelVisible() ? TIMEOUT_DELAY_COLLAPSED @@ -826,11 +910,18 @@ public class VolumePanel extends Handler { } } - public void updateStates() { + private void updateStates() { final int count = mSliderPanel.getChildCount(); for (int i = 0; i < count; i++) { StreamControl sc = (StreamControl) mSliderPanel.getChildAt(i).getTag(); - updateSlider(sc); + updateSlider(sc, true /*forceReloadIcon*/); + } + } + + private void updateActiveSlider() { + final StreamControl active = mStreamControls.get(mActiveStreamType); + if (active != null) { + updateSlider(active, false /*forceReloadIcon*/); } } @@ -916,6 +1007,14 @@ public class VolumePanel extends Handler { obtainMessage(MSG_LAYOUT_DIRECTION, layoutDirection, 0).sendToTarget(); } + private static String flagsToString(int flags) { + return flags == 0 ? "0" : (flags + "=" + AudioManager.flagsToString(flags)); + } + + private static String streamToString(int stream) { + return AudioService.streamToString(stream); + } + /** * Override this if you have other work to do when the volume changes (for * example, vibrating, playing a sound, etc.). Make sure to call through to @@ -923,7 +1022,8 @@ public class VolumePanel extends Handler { */ protected void onVolumeChanged(int streamType, int flags) { - if (LOGD) Log.d(mTag, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")"); + if (LOGD) Log.d(mTag, "onVolumeChanged(streamType: " + streamToString(streamType) + + ", flags: " + flagsToString(flags) + ")"); if ((flags & AudioManager.FLAG_SHOW_UI) != 0) { synchronized (this) { @@ -952,7 +1052,8 @@ public class VolumePanel extends Handler { protected void onMuteChanged(int streamType, int flags) { - if (LOGD) Log.d(mTag, "onMuteChanged(streamType: " + streamType + ", flags: " + flags + ")"); + if (LOGD) Log.d(mTag, "onMuteChanged(streamType: " + streamToString(streamType) + + ", flags: " + flagsToString(flags) + ")"); StreamControl sc = mStreamControls.get(streamType); if (sc != null) { @@ -968,8 +1069,8 @@ public class VolumePanel extends Handler { mRingIsSilent = false; if (LOGD) { - Log.d(mTag, "onShowVolumeChanged(streamType: " + streamType - + ", flags: " + flags + "), index: " + index); + Log.d(mTag, "onShowVolumeChanged(streamType: " + streamToString(streamType) + + ", flags: " + flagsToString(flags) + "), index: " + index); } // get max volume for progress bar @@ -980,7 +1081,6 @@ public class VolumePanel extends Handler { switch (streamType) { case AudioManager.STREAM_RING: { -// setRingerIcon(); Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( mContext, RingtoneManager.TYPE_RINGTONE); if (ringuri == null) { @@ -995,7 +1095,7 @@ public class VolumePanel extends Handler { (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP | AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) { - setMusicIcon(R.drawable.ic_audio_bt, R.drawable.ic_audio_bt_mute); + setMusicIcon(IC_AUDIO_BT, IC_AUDIO_BT_MUTE); } else { setMusicIcon(IC_AUDIO_VOL, IC_AUDIO_VOL_MUTE); } @@ -1073,14 +1173,25 @@ public class VolumePanel extends Handler { sc.seekbarView.setMax(max); } updateSliderProgress(sc, index); - updateSliderEnabled(sc, isMuted(streamType), - (flags & AudioManager.FLAG_FIXED_VOLUME) != 0); + final boolean muted = isMuted(streamType); + updateSliderEnabled(sc, muted, (flags & AudioManager.FLAG_FIXED_VOLUME) != 0); + if (isNotificationOrRing(streamType)) { + // check for secondary-icon transition completion + if (mSecondaryIconTransition.isRunning()) { + mSecondaryIconTransition.cancel(); // safe to reset + sc.seekbarView.setAlpha(0); sc.seekbarView.animate().alpha(1); + mZenPanel.setAlpha(0); mZenPanel.animate().alpha(1); + } + updateSliderIcon(sc, muted); + } } if (!isShowing()) { int stream = (streamType == STREAM_REMOTE_MUSIC) ? -1 : streamType; // when the stream is for remote playback, use -1 to reset the stream type evaluation - mAudioManager.forceVolumeControlStream(stream); + if (stream != STREAM_MASTER) { + mAudioManager.forceVolumeControlStream(stream); + } mDialog.show(); if (mCallback != null) { mCallback.onVisible(true); @@ -1091,15 +1202,20 @@ public class VolumePanel extends Handler { // Do a little vibrate if applicable (only when going into vibrate mode) if ((streamType != STREAM_REMOTE_MUSIC) && ((flags & AudioManager.FLAG_VIBRATE) != 0) && - mAudioManager.isStreamAffectedByRingerMode(streamType) && - mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { + isNotificationOrRing(streamType) && + mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) { sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY); } - // Pulse the slider icon if an adjustment was suppressed due to silent mode. + // Pulse the zen icon if an adjustment was suppressed due to silent mode. if ((flags & AudioManager.FLAG_SHOW_SILENT_HINT) != 0) { showSilentHint(); } + + // Pulse the slider icon & vibrate if an adjustment down was suppressed due to vibrate mode. + if ((flags & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0) { + showVibrateHint(); + } } private void announceDialogShown() { @@ -1143,16 +1259,17 @@ public class VolumePanel extends Handler { protected void onVibrate() { // Make sure we ended up in vibrate ringer mode - if (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_VIBRATE) { + if (mAudioManager.getRingerModeInternal() != AudioManager.RINGER_MODE_VIBRATE) { return; } - - mVibrator.vibrate(VIBRATE_DURATION, VIBRATION_ATTRIBUTES); + if (mVibrator != null) { + mVibrator.vibrate(VIBRATE_DURATION, VIBRATION_ATTRIBUTES); + } } protected void onRemoteVolumeChanged(MediaController controller, int flags) { - if (LOGD) Log.d(mTag, "onRemoteVolumeChanged(controller:" + controller + ", flags: " + flags - + ")"); + if (LOGD) Log.d(mTag, "onRemoteVolumeChanged(controller:" + controller + ", flags: " + + flagsToString(flags) + ")"); if (((flags & AudioManager.FLAG_SHOW_UI) != 0) || isShowing()) { synchronized (this) { @@ -1343,9 +1460,10 @@ public class VolumePanel extends Handler { } case MSG_RINGER_MODE_CHANGED: + case MSG_INTERNAL_RINGER_MODE_CHANGED: case MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED: { if (isShowing()) { - updateStates(); + updateActiveSlider(); } break; } @@ -1406,6 +1524,22 @@ public class VolumePanel extends Handler { return mZenController; } + @Override + public void dispatchDemoCommand(String command, Bundle args) { + if (!COMMAND_VOLUME.equals(command)) return; + String icon = args.getString("icon"); + final String iconMute = args.getString("iconmute"); + final boolean mute = iconMute != null; + icon = mute ? iconMute : icon; + icon = icon.endsWith("Stream") ? icon : (icon + "Stream"); + final StreamResources sr = StreamResources.valueOf(icon); + mDemoIcon = mute ? sr.iconMuteRes : sr.iconRes; + final int forcedStreamType = StreamResources.MediaStream.streamType; + mAudioManager.forceVolumeControlStream(forcedStreamType); + mAudioManager.adjustStreamVolume(forcedStreamType, AudioManager.ADJUST_SAME, + AudioManager.FLAG_SHOW_UI); + } + private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { @@ -1432,10 +1566,11 @@ public class VolumePanel extends Handler { public void onZenAvailableChanged(boolean available) { obtainMessage(MSG_ZEN_MODE_AVAILABLE_CHANGED, available ? 1 : 0, 0).sendToTarget(); } + @Override public void onEffectsSupressorChanged() { - obtainMessage(MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED, - mZenController.getEffectsSuppressor()).sendToTarget(); + mNotificationEffectsSuppressor = mZenController.getEffectsSuppressor(); + sendEmptyMessage(MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED); } }; @@ -1445,6 +1580,82 @@ public class VolumePanel extends Handler { } }; + private final class SecondaryIconTransition extends AnimatorListenerAdapter + implements Runnable { + private static final int ANIMATION_TIME = 400; + private static final int WAIT_FOR_SWITCH_TIME = 1000; + + private final int mAnimationTime = (int)(ANIMATION_TIME * ValueAnimator.getDurationScale()); + private final int mFadeOutTime = mAnimationTime / 2; + private final int mDelayTime = mAnimationTime / 3; + + private final Interpolator mIconInterpolator = + AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); + + private StreamControl mTarget; + + public void start(StreamControl sc) { + if (sc == null) throw new IllegalArgumentException(); + if (LOGD) Log.d(mTag, "Secondary icon animation start"); + if (mTarget != null) { + cancel(); + } + mTarget = sc; + mTimeoutDelay = mAnimationTime + WAIT_FOR_SWITCH_TIME; + resetTimeout(); + mTarget.secondaryIcon.setClickable(false); + final int N = mTarget.group.getChildCount(); + for (int i = 0; i < N; i++) { + final View child = mTarget.group.getChildAt(i); + if (child != mTarget.secondaryIcon) { + child.animate().alpha(0).setDuration(mFadeOutTime).start(); + } + } + mTarget.secondaryIcon.animate() + .translationXBy(mTarget.icon.getX() - mTarget.secondaryIcon.getX()) + .setInterpolator(mIconInterpolator) + .setStartDelay(mDelayTime) + .setDuration(mAnimationTime - mDelayTime) + .setListener(this) + .start(); + } + + public boolean isRunning() { + return mTarget != null; + } + + public void cancel() { + if (mTarget == null) return; + mTarget.secondaryIcon.setClickable(true); + final int N = mTarget.group.getChildCount(); + for (int i = 0; i < N; i++) { + final View child = mTarget.group.getChildAt(i); + if (child != mTarget.secondaryIcon) { + child.animate().cancel(); + child.setAlpha(1); + } + } + mTarget.secondaryIcon.animate().cancel(); + mTarget.secondaryIcon.setTranslationX(0); + mTarget = null; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mTarget == null) return; + AsyncTask.execute(this); + } + + @Override + public void run() { + if (mTarget == null) return; + if (LOGD) Log.d(mTag, "Secondary icon animation complete, show notification slider"); + mAudioManager.forceVolumeControlStream(StreamResources.NotificationStream.streamType); + mAudioManager.adjustStreamVolume(StreamResources.NotificationStream.streamType, + AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI); + } + } + public interface Callback { void onZenSettings(); void onInteraction(); diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java index 0586a83..7102c2a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java @@ -1,7 +1,6 @@ package com.android.systemui.volume; import android.content.Context; -import android.content.Intent; import android.content.res.Configuration; import android.database.ContentObserver; import android.media.AudioManager; @@ -11,13 +10,11 @@ import android.media.session.ISessionController; import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.net.Uri; -import android.os.AsyncTask; +import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; -import android.os.UserHandle; import android.provider.Settings; import android.util.Log; -import android.view.WindowManagerGlobal; import com.android.systemui.R; import com.android.systemui.SystemUI; @@ -53,6 +50,7 @@ public class VolumeUI extends SystemUI { private final Handler mHandler = new Handler(); + private boolean mEnabled; private AudioManager mAudioManager; private MediaSessionManager mMediaSessionManager; private VolumeController mVolumeController; @@ -63,6 +61,8 @@ public class VolumeUI extends SystemUI { @Override public void start() { + mEnabled = mContext.getResources().getBoolean(R.bool.enable_volume_ui); + if (!mEnabled) return; mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); mMediaSessionManager = (MediaSessionManager) mContext .getSystemService(Context.MEDIA_SESSION_SERVICE); @@ -84,6 +84,7 @@ public class VolumeUI extends SystemUI { @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.print("mEnabled="); pw.println(mEnabled); if (mPanel != null) { mPanel.dump(fd, pw, args); } @@ -177,13 +178,23 @@ public class VolumeUI extends SystemUI { @Override public void dismiss() throws RemoteException { - mPanel.postDismiss(0); + dismissNow(); } @Override public ZenModeController getZenController() { return mPanel.getZenController(); } + + @Override + public void dispatchDemoCommand(String command, Bundle args) { + mPanel.dispatchDemoCommand(command, args); + } + + @Override + public void dismissNow() { + mPanel.postDismiss(0); + } } private final class RemoteVolumeController extends IRemoteVolumeController.Stub { diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java index ea431ae..d40a2c0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java @@ -16,14 +16,17 @@ package com.android.systemui.volume; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; +import android.animation.LayoutTransition; +import android.animation.LayoutTransition.TransitionListener; +import android.app.ActivityManager; +import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.res.Resources; import android.net.Uri; +import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -31,11 +34,14 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.service.notification.Condition; import android.service.notification.ZenModeConfig; +import android.text.TextUtils; +import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; import android.util.MathUtils; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.CompoundButton; @@ -48,6 +54,8 @@ import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.statusbar.policy.ZenModeController; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.Arrays; import java.util.Objects; @@ -65,9 +73,7 @@ public class ZenModePanel extends LinearLayout { private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1]; private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60); private static final int FOREVER_CONDITION_INDEX = 0; - private static final int TIME_CONDITION_INDEX = 1; - private static final int FIRST_CONDITION_INDEX = 2; - private static final float SILENT_HINT_PULSE_SCALE = 1.1f; + private static final int COUNTDOWN_CONDITION_INDEX = 1; public static final Intent ZEN_SETTINGS = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); @@ -75,10 +81,16 @@ public class ZenModePanel extends LinearLayout { private final LayoutInflater mInflater; private final H mHandler = new H(); private final Prefs mPrefs; - private final Interpolator mFastOutSlowInInterpolator; + private final IconPulser mIconPulser; private final int mSubheadWarningColor; private final int mSubheadColor; - private final ZenToast mZenToast; + private final Interpolator mInterpolator; + private final int mMaxConditions; + private final int mMaxOptionalConditions; + private final boolean mCountdownConditionSupported; + private final int mFirstConditionIndex; + private final TransitionHelper mTransitionHelper = new TransitionHelper(); + private final Uri mForeverId; private String mTag = TAG + "/" + Integer.toHexString(System.identityHashCode(this)); @@ -96,9 +108,10 @@ public class ZenModePanel extends LinearLayout { private String mExitConditionText; private int mBucketIndex = -1; private boolean mExpanded; - private boolean mHidden = false; + private boolean mHidden; private int mSessionZen; private int mAttachedZen; + private boolean mAttached; private Condition mSessionExitCondition; private Condition[] mConditions; private Condition mTimeCondition; @@ -108,26 +121,53 @@ public class ZenModePanel extends LinearLayout { mContext = context; mPrefs = new Prefs(); mInflater = LayoutInflater.from(mContext.getApplicationContext()); - mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.fast_out_slow_in); + mIconPulser = new IconPulser(mContext); final Resources res = mContext.getResources(); mSubheadWarningColor = res.getColor(R.color.system_warning_color); mSubheadColor = res.getColor(R.color.qs_subhead); - mZenToast = new ZenToast(mContext); + mInterpolator = AnimationUtils.loadInterpolator(mContext, + com.android.internal.R.interpolator.fast_out_slow_in); + mCountdownConditionSupported = NotificationManager.from(mContext) + .isSystemConditionProviderEnabled(ZenModeConfig.COUNTDOWN_PATH); + final int countdownDelta = mCountdownConditionSupported ? 1 : 0; + mFirstConditionIndex = COUNTDOWN_CONDITION_INDEX + countdownDelta; + final int minConditions = 1 /*forever*/ + countdownDelta; + mMaxConditions = MathUtils.constrain(res.getInteger(R.integer.zen_mode_max_conditions), + minConditions, 100); + mMaxOptionalConditions = mMaxConditions - minConditions; + mForeverId = Condition.newId(mContext).appendPath("forever").build(); if (DEBUG) Log.d(mTag, "new ZenModePanel"); } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("ZenModePanel state:"); + pw.print(" mCountdownConditionSupported="); pw.println(mCountdownConditionSupported); + pw.print(" mMaxConditions="); pw.println(mMaxConditions); + pw.print(" mRequestingConditions="); pw.println(mRequestingConditions); + pw.print(" mAttached="); pw.println(mAttached); + pw.print(" mHidden="); pw.println(mHidden); + pw.print(" mExpanded="); pw.println(mExpanded); + pw.print(" mSessionZen="); pw.println(mSessionZen); + pw.print(" mAttachedZen="); pw.println(mAttachedZen); + mTransitionHelper.dump(fd, pw, args); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); mZenButtons = (SegmentedButtons) findViewById(R.id.zen_buttons); - mZenButtons.addButton(R.string.interruption_level_none, Global.ZEN_MODE_NO_INTERRUPTIONS); - mZenButtons.addButton(R.string.interruption_level_priority, + mZenButtons.addButton(R.string.interruption_level_none, R.drawable.ic_zen_none, + Global.ZEN_MODE_NO_INTERRUPTIONS); + mZenButtons.addButton(R.string.interruption_level_priority, R.drawable.ic_zen_important, Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); - mZenButtons.addButton(R.string.interruption_level_all, Global.ZEN_MODE_OFF); + mZenButtons.addButton(R.string.interruption_level_all, R.drawable.ic_zen_all, + Global.ZEN_MODE_OFF); mZenButtons.setCallback(mZenButtonsCallback); + final ViewGroup zenButtonsContainer = (ViewGroup) findViewById(R.id.zen_buttons_container); + zenButtonsContainer.setLayoutTransition(newLayoutTransition(null)); + mZenSubhead = findViewById(R.id.zen_subhead); mZenSubheadCollapsed = (TextView) findViewById(R.id.zen_subhead_collapsed); @@ -152,18 +192,37 @@ public class ZenModePanel extends LinearLayout { Interaction.register(mMoreSettings, mInteractionCallback); mZenConditions = (LinearLayout) findViewById(R.id.zen_conditions); + for (int i = 0; i < mMaxConditions; i++) { + mZenConditions.addView(mInflater.inflate(R.layout.zen_mode_condition, this, false)); + } + + setLayoutTransition(newLayoutTransition(mTransitionHelper)); + } + + private LayoutTransition newLayoutTransition(TransitionListener listener) { + final LayoutTransition transition = new LayoutTransition(); + transition.disableTransitionType(LayoutTransition.DISAPPEARING); + transition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); + transition.disableTransitionType(LayoutTransition.APPEARING); + transition.setInterpolator(LayoutTransition.CHANGE_APPEARING, mInterpolator); + if (listener != null) { + transition.addTransitionListener(listener); + } + return transition; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (DEBUG) Log.d(mTag, "onAttachedToWindow"); - mZenToast.hide(); + mAttached = true; mAttachedZen = getSelectedZen(-1); mSessionZen = mAttachedZen; - mSessionExitCondition = copy(mExitCondition); + mTransitionHelper.clear(); + setSessionExitCondition(copy(mExitCondition)); refreshExitConditionText(); updateWidgets(); + setRequestingConditions(!mHidden); } @Override @@ -171,15 +230,26 @@ public class ZenModePanel extends LinearLayout { super.onDetachedFromWindow(); if (DEBUG) Log.d(mTag, "onDetachedFromWindow"); checkForAttachedZenChange(); + mAttached = false; mAttachedZen = -1; mSessionZen = -1; - mSessionExitCondition = null; + setSessionExitCondition(null); setExpanded(false); + setRequestingConditions(false); + mTransitionHelper.clear(); + } + + private void setSessionExitCondition(Condition condition) { + if (Objects.equals(condition, mSessionExitCondition)) return; + if (DEBUG) Log.d(mTag, "mSessionExitCondition=" + getConditionId(condition)); + mSessionExitCondition = condition; } public void setHidden(boolean hidden) { if (mHidden == hidden) return; + if (DEBUG) Log.d(mTag, "hidden=" + hidden); mHidden = hidden; + setRequestingConditions(mAttached && !mHidden); updateWidgets(); } @@ -191,28 +261,31 @@ public class ZenModePanel extends LinearLayout { if (selectedZen == Global.ZEN_MODE_NO_INTERRUPTIONS) { mPrefs.trackNoneSelected(); } - if (selectedZen == Global.ZEN_MODE_NO_INTERRUPTIONS - || selectedZen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) { - mZenToast.show(selectedZen); - } } } private void setExpanded(boolean expanded) { if (expanded == mExpanded) return; mExpanded = expanded; + if (mExpanded) { + ensureSelection(); + } updateWidgets(); - setRequestingConditions(mExpanded); fireExpanded(); } /** Start or stop requesting relevant zen mode exit conditions */ - private void setRequestingConditions(boolean requesting) { + private void setRequestingConditions(final boolean requesting) { if (mRequestingConditions == requesting) return; if (DEBUG) Log.d(mTag, "setRequestingConditions " + requesting); mRequestingConditions = requesting; if (mController != null) { - mController.requestConditions(mRequestingConditions); + AsyncTask.execute(new Runnable() { + @Override + public void run() { + mController.requestConditions(requesting); + } + }); } if (mRequestingConditions) { mTimeCondition = parseExistingTimeCondition(mExitCondition); @@ -220,13 +293,14 @@ public class ZenModePanel extends LinearLayout { mBucketIndex = -1; } else { mBucketIndex = DEFAULT_BUCKET_INDEX; - mTimeCondition = ZenModeConfig.toTimeCondition(MINUTE_BUCKETS[mBucketIndex]); + mTimeCondition = ZenModeConfig.toTimeCondition(mContext, + MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser()); } if (DEBUG) Log.d(mTag, "Initial bucket index: " + mBucketIndex); mConditions = null; // reset conditions handleUpdateConditions(); } else { - mZenConditions.removeAllViews(); + hideAllConditions(); } } @@ -237,7 +311,7 @@ public class ZenModePanel extends LinearLayout { mSessionZen = getSelectedZen(-1); handleUpdateZen(mController.getZen()); if (DEBUG) Log.d(mTag, "init mExitCondition=" + mExitCondition); - mZenConditions.removeAllViews(); + hideAllConditions(); mController.addCallback(mZenCallback); } @@ -246,8 +320,9 @@ public class ZenModePanel extends LinearLayout { } private void setExitCondition(Condition exitCondition) { - if (sameConditionId(mExitCondition, exitCondition)) return; + if (Objects.equals(mExitCondition, exitCondition)) return; mExitCondition = exitCondition; + if (DEBUG) Log.d(mTag, "mExitCondition=" + getConditionId(mExitCondition)); refreshExitConditionText(); updateWidgets(); } @@ -265,12 +340,11 @@ public class ZenModePanel extends LinearLayout { } private void refreshExitConditionText() { - final String forever = mContext.getString(com.android.internal.R.string.zen_mode_forever); if (mExitCondition == null) { - mExitConditionText = forever; - } else if (ZenModeConfig.isValidCountdownConditionId(mExitCondition.id)) { + mExitConditionText = foreverSummary(); + } else if (isCountdown(mExitCondition)) { final Condition condition = parseExistingTimeCondition(mExitCondition); - mExitConditionText = condition != null ? condition.summary : forever; + mExitConditionText = condition != null ? condition.summary : foreverSummary(); } else { mExitConditionText = mExitCondition.summary; } @@ -284,16 +358,7 @@ public class ZenModePanel extends LinearLayout { if (DEBUG) Log.d(mTag, "showSilentHint"); if (mZenButtons == null || mZenButtons.getChildCount() == 0) return; final View noneButton = mZenButtons.getChildAt(0); - if (noneButton.getScaleX() != 1) return; // already running - noneButton.animate().cancel(); - noneButton.animate().scaleX(SILENT_HINT_PULSE_SCALE).scaleY(SILENT_HINT_PULSE_SCALE) - .setInterpolator(mFastOutSlowInInterpolator) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - noneButton.animate().scaleX(1).scaleY(1).setListener(null); - } - }); + mIconPulser.start(noneButton); } private void handleUpdateZen(int zen) { @@ -303,6 +368,24 @@ public class ZenModePanel extends LinearLayout { } mZenButtons.setSelectedValue(zen); updateWidgets(); + handleUpdateConditions(); + if (mExpanded) { + final Condition selected = getSelectedCondition(); + if (!Objects.equals(mExitCondition, selected)) { + select(selected); + } + } + } + + private Condition getSelectedCondition() { + final int N = getVisibleConditions(); + for (int i = 0; i < N; i++) { + final ConditionTag tag = getConditionTagAt(i); + if (tag != null && tag.rb.isChecked()) { + return tag.condition; + } + } + return null; } private int getSelectedZen(int defValue) { @@ -311,6 +394,10 @@ public class ZenModePanel extends LinearLayout { } private void updateWidgets() { + if (mTransitionHelper.isTransitioning()) { + mTransitionHelper.pendingUpdateWidgets(); + return; + } final int zen = getSelectedZen(Global.ZEN_MODE_OFF); final boolean zenOff = zen == Global.ZEN_MODE_OFF; final boolean zenImportant = zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; @@ -339,109 +426,183 @@ public class ZenModePanel extends LinearLayout { if (condition == null) return null; final long time = ZenModeConfig.tryParseCountdownConditionId(condition.id); if (time == 0) return null; - final long span = time - System.currentTimeMillis(); + final long now = System.currentTimeMillis(); + final long span = time - now; if (span <= 0 || span > MAX_BUCKET_MINUTES * MINUTES_MS) return null; - return ZenModeConfig.toTimeCondition(time, Math.round(span / (float) MINUTES_MS)); + return ZenModeConfig.toTimeCondition(mContext, + time, Math.round(span / (float) MINUTES_MS), now, ActivityManager.getCurrentUser()); } private void handleUpdateConditions(Condition[] conditions) { + conditions = trimConditions(conditions); + if (Arrays.equals(conditions, mConditions)) { + final int count = mConditions == null ? 0 : mConditions.length; + if (DEBUG) Log.d(mTag, "handleUpdateConditions unchanged conditionCount=" + count); + return; + } mConditions = conditions; handleUpdateConditions(); } + private Condition[] trimConditions(Condition[] conditions) { + if (conditions == null || conditions.length <= mMaxOptionalConditions) { + // no need to trim + return conditions; + } + // look for current exit condition, ensure it is included if found + int found = -1; + for (int i = 0; i < conditions.length; i++) { + final Condition c = conditions[i]; + if (mSessionExitCondition != null && sameConditionId(mSessionExitCondition, c)) { + found = i; + break; + } + } + final Condition[] rt = Arrays.copyOf(conditions, mMaxOptionalConditions); + if (found >= mMaxOptionalConditions) { + // found after the first N, promote to the end of the first N + rt[mMaxOptionalConditions - 1] = conditions[found]; + } + return rt; + } + private void handleUpdateConditions() { + if (mTransitionHelper.isTransitioning()) { + mTransitionHelper.pendingUpdateConditions(); + return; + } final int conditionCount = mConditions == null ? 0 : mConditions.length; if (DEBUG) Log.d(mTag, "handleUpdateConditions conditionCount=" + conditionCount); - for (int i = mZenConditions.getChildCount() - 1; i >= FIRST_CONDITION_INDEX; i--) { - mZenConditions.removeViewAt(i); - } // forever - bind(null, mZenConditions.getChildAt(FOREVER_CONDITION_INDEX)); + bind(forever(), mZenConditions.getChildAt(FOREVER_CONDITION_INDEX)); // countdown - bind(mTimeCondition, mZenConditions.getChildAt(TIME_CONDITION_INDEX)); + if (mCountdownConditionSupported && mTimeCondition != null) { + bind(mTimeCondition, mZenConditions.getChildAt(COUNTDOWN_CONDITION_INDEX)); + } // provider conditions - boolean foundDowntime = false; for (int i = 0; i < conditionCount; i++) { - bind(mConditions[i], mZenConditions.getChildAt(FIRST_CONDITION_INDEX + i)); - foundDowntime |= isDowntime(mConditions[i]); + bind(mConditions[i], mZenConditions.getChildAt(mFirstConditionIndex + i)); } - // ensure downtime exists, if active - if (isDowntime(mSessionExitCondition) && !foundDowntime) { - bind(mSessionExitCondition, null); + // hide the rest + for (int i = mZenConditions.getChildCount() - 1; i > mFirstConditionIndex + conditionCount; + i--) { + mZenConditions.getChildAt(i).setVisibility(GONE); } // ensure something is selected - checkForDefault(); + if (mExpanded) { + ensureSelection(); + } } - private static boolean isDowntime(Condition c) { - return ZenModeConfig.isValidDowntimeConditionId(getConditionId(c)); + private Condition forever() { + return new Condition(mForeverId, foreverSummary(), "", "", 0 /*icon*/, Condition.STATE_TRUE, + 0 /*flags*/); + } + + private String foreverSummary() { + return mContext.getString(com.android.internal.R.string.zen_mode_forever); } private ConditionTag getConditionTagAt(int index) { return (ConditionTag) mZenConditions.getChildAt(index).getTag(); } - private void checkForDefault() { + private int getVisibleConditions() { + int rt = 0; + final int N = mZenConditions.getChildCount(); + for (int i = 0; i < N; i++) { + rt += mZenConditions.getChildAt(i).getVisibility() == VISIBLE ? 1 : 0; + } + return rt; + } + + private void hideAllConditions() { + final int N = mZenConditions.getChildCount(); + for (int i = 0; i < N; i++) { + mZenConditions.getChildAt(i).setVisibility(GONE); + } + } + + private void ensureSelection() { // are we left without anything selected? if so, set a default - for (int i = 0; i < mZenConditions.getChildCount(); i++) { - if (getConditionTagAt(i).rb.isChecked()) { - if (DEBUG) Log.d(mTag, "Not selecting a default, checked=" - + getConditionTagAt(i).condition); + final int visibleConditions = getVisibleConditions(); + if (visibleConditions == 0) return; + for (int i = 0; i < visibleConditions; i++) { + final ConditionTag tag = getConditionTagAt(i); + if (tag != null && tag.rb.isChecked()) { + if (DEBUG) Log.d(mTag, "Not selecting a default, checked=" + tag.condition); return; } } + final ConditionTag foreverTag = getConditionTagAt(FOREVER_CONDITION_INDEX); + if (foreverTag == null) return; if (DEBUG) Log.d(mTag, "Selecting a default"); final int favoriteIndex = mPrefs.getMinuteIndex(); - if (favoriteIndex == -1) { - getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true); + if (favoriteIndex == -1 || !mCountdownConditionSupported) { + foreverTag.rb.setChecked(true); } else { - mTimeCondition = ZenModeConfig.toTimeCondition(MINUTE_BUCKETS[favoriteIndex]); + mTimeCondition = ZenModeConfig.toTimeCondition(mContext, + MINUTE_BUCKETS[favoriteIndex], ActivityManager.getCurrentUser()); mBucketIndex = favoriteIndex; - bind(mTimeCondition, mZenConditions.getChildAt(TIME_CONDITION_INDEX)); - getConditionTagAt(TIME_CONDITION_INDEX).rb.setChecked(true); + bind(mTimeCondition, mZenConditions.getChildAt(COUNTDOWN_CONDITION_INDEX)); + getConditionTagAt(COUNTDOWN_CONDITION_INDEX).rb.setChecked(true); } } private void handleExitConditionChanged(Condition exitCondition) { setExitCondition(exitCondition); if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitCondition); - final int N = mZenConditions.getChildCount(); + final int N = getVisibleConditions(); for (int i = 0; i < N; i++) { final ConditionTag tag = getConditionTagAt(i); - tag.rb.setChecked(sameConditionId(tag.condition, mExitCondition)); + if (tag != null) { + if (sameConditionId(tag.condition, mExitCondition)) { + bind(exitCondition, mZenConditions.getChildAt(i)); + } + } } } - private void bind(final Condition condition, View convertView) { - final boolean enabled = condition == null || condition.state == Condition.STATE_TRUE; - final View row; - if (convertView == null) { - row = mInflater.inflate(R.layout.zen_mode_condition, this, false); - if (DEBUG) Log.d(mTag, "Adding new condition view for: " + condition); - mZenConditions.addView(row); - } else { - row = convertView; - } + private boolean isCountdown(Condition c) { + return c != null && ZenModeConfig.isValidCountdownConditionId(c.id); + } + + private boolean isForever(Condition c) { + return c != null && mForeverId.equals(c.id); + } + + private void bind(final Condition condition, final View row) { + if (condition == null) throw new IllegalArgumentException("condition must not be null"); + final boolean enabled = condition.state == Condition.STATE_TRUE; final ConditionTag tag = row.getTag() != null ? (ConditionTag) row.getTag() : new ConditionTag(); row.setTag(tag); + final boolean first = tag.rb == null; if (tag.rb == null) { tag.rb = (RadioButton) row.findViewById(android.R.id.checkbox); } tag.condition = condition; + final Uri conditionId = getConditionId(tag.condition); + if (DEBUG) Log.d(mTag, "bind i=" + mZenConditions.indexOfChild(row) + " first=" + first + + " condition=" + conditionId); tag.rb.setEnabled(enabled); - if (sameConditionId(mSessionExitCondition, tag.condition)) { - tag.rb.setChecked(true); + final boolean checked = (mSessionExitCondition != null + || mAttachedZen != Global.ZEN_MODE_OFF) + && (sameConditionId(mSessionExitCondition, tag.condition) + || isCountdown(mSessionExitCondition) && isCountdown(tag.condition)); + if (checked != tag.rb.isChecked()) { + if (DEBUG) Log.d(mTag, "bind checked=" + checked + " condition=" + conditionId); + tag.rb.setChecked(checked); } tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (mExpanded && isChecked) { - if (DEBUG) Log.d(mTag, "onCheckedChanged " + tag.condition); - final int N = mZenConditions.getChildCount(); + if (DEBUG) Log.d(mTag, "onCheckedChanged " + conditionId); + final int N = getVisibleConditions(); for (int i = 0; i < N; i++) { - ConditionTag childTag = getConditionTagAt(i); - if (childTag == tag) continue; + final ConditionTag childTag = getConditionTagAt(i); + if (childTag == null || childTag == tag) continue; childTag.rb.setChecked(false); } select(tag.condition); @@ -450,16 +611,27 @@ public class ZenModePanel extends LinearLayout { } }); - if (tag.title == null) { - tag.title = (TextView) row.findViewById(android.R.id.title); + if (tag.lines == null) { + tag.lines = row.findViewById(android.R.id.content); + } + if (tag.line1 == null) { + tag.line1 = (TextView) row.findViewById(android.R.id.text1); } - if (condition == null) { - tag.title.setText(mContext.getString(com.android.internal.R.string.zen_mode_forever)); + if (tag.line2 == null) { + tag.line2 = (TextView) row.findViewById(android.R.id.text2); + } + final String line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1 + : condition.summary; + final String line2 = condition.line2; + tag.line1.setText(line1); + if (TextUtils.isEmpty(line2)) { + tag.line2.setVisibility(GONE); } else { - tag.title.setText(condition.summary); + tag.line2.setVisibility(VISIBLE); + tag.line2.setText(line2); } - tag.title.setEnabled(enabled); - tag.title.setAlpha(enabled ? 1 : .4f); + tag.lines.setEnabled(enabled); + tag.lines.setAlpha(enabled ? 1 : .4f); final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1); button1.setOnClickListener(new OnClickListener() { @@ -476,38 +648,42 @@ public class ZenModePanel extends LinearLayout { onClickTimeButton(row, tag, true /*up*/); } }); - tag.title.setOnClickListener(new OnClickListener() { + tag.lines.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { tag.rb.setChecked(true); } }); - final long time = ZenModeConfig.tryParseCountdownConditionId(getConditionId(tag.condition)); + final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId); if (time > 0) { + button1.setVisibility(VISIBLE); + button2.setVisibility(VISIBLE); if (mBucketIndex > -1) { button1.setEnabled(mBucketIndex > 0); button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1); } else { final long span = time - System.currentTimeMillis(); button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS); - final Condition maxCondition = ZenModeConfig.toTimeCondition(MAX_BUCKET_MINUTES); + final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext, + MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser()); button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary)); } button1.setAlpha(button1.isEnabled() ? 1f : .5f); button2.setAlpha(button2.isEnabled() ? 1f : .5f); } else { - button1.setVisibility(View.GONE); - button2.setVisibility(View.GONE); + button1.setVisibility(GONE); + button2.setVisibility(GONE); } // wire up interaction callbacks for newly-added condition rows - if (convertView == null) { + if (first) { Interaction.register(tag.rb, mInteractionCallback); - Interaction.register(tag.title, mInteractionCallback); + Interaction.register(tag.lines, mInteractionCallback); Interaction.register(button1, mInteractionCallback); Interaction.register(button2, mInteractionCallback); } + row.setVisibility(VISIBLE); } private void announceConditionSelection(ConditionTag tag) { @@ -524,7 +700,7 @@ public class ZenModePanel extends LinearLayout { return; } announceForAccessibility(mContext.getString(R.string.zen_mode_and_condition, modeText, - tag.title.getText())); + tag.line1.getText())); } private void onClickTimeButton(View row, ConditionTag tag, boolean up) { @@ -541,18 +717,21 @@ public class ZenModePanel extends LinearLayout { final long bucketTime = now + bucketMinutes * MINUTES_MS; if (up && bucketTime > time || !up && bucketTime < time) { mBucketIndex = j; - newCondition = ZenModeConfig.toTimeCondition(bucketTime, bucketMinutes); + newCondition = ZenModeConfig.toTimeCondition(mContext, + bucketTime, bucketMinutes, now, ActivityManager.getCurrentUser()); break; } } if (newCondition == null) { mBucketIndex = DEFAULT_BUCKET_INDEX; - newCondition = ZenModeConfig.toTimeCondition(MINUTE_BUCKETS[mBucketIndex]); + newCondition = ZenModeConfig.toTimeCondition(mContext, + MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser()); } } else { // on a known index, simply increment or decrement mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1))); - newCondition = ZenModeConfig.toTimeCondition(MINUTE_BUCKETS[mBucketIndex]); + newCondition = ZenModeConfig.toTimeCondition(mContext, + MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser()); } mTimeCondition = newCondition; bind(mTimeCondition, row); @@ -561,18 +740,24 @@ public class ZenModePanel extends LinearLayout { announceConditionSelection(tag); } - private void select(Condition condition) { + private void select(final Condition condition) { if (DEBUG) Log.d(mTag, "select " + condition); + final boolean isForever = isForever(condition); if (mController != null) { - mController.setExitCondition(condition); + AsyncTask.execute(new Runnable() { + @Override + public void run() { + mController.setExitCondition(isForever ? null : condition); + } + }); } setExitCondition(condition); - if (condition == null) { + if (isForever) { mPrefs.setMinuteIndex(-1); - } else if (ZenModeConfig.isValidCountdownConditionId(condition.id) && mBucketIndex != -1) { + } else if (isCountdown(condition) && mBucketIndex != -1) { mPrefs.setMinuteIndex(mBucketIndex); } - mSessionExitCondition = copy(condition); + setSessionExitCondition(copy(condition)); } private void fireMoreSettings() { @@ -639,7 +824,9 @@ public class ZenModePanel extends LinearLayout { // used as the view tag on condition rows private static class ConditionTag { RadioButton rb; - TextView title; + View lines; + TextView line1; + TextView line2; Condition condition; } @@ -690,7 +877,7 @@ public class ZenModePanel extends LinearLayout { } private SharedPreferences prefs() { - return mContext.getSharedPreferences(ZenModePanel.class.getSimpleName(), 0); + return mContext.getSharedPreferences(mContext.getPackageName(), 0); } private void updateMinuteIndex() { @@ -714,10 +901,15 @@ public class ZenModePanel extends LinearLayout { private final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() { @Override - public void onSelected(Object value) { + public void onSelected(final Object value) { if (value != null && mZenButtons.isShown()) { if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + value); - mController.setZen((Integer) value); + AsyncTask.execute(new Runnable() { + @Override + public void run() { + mController.setZen((Integer) value); + } + }); } } @@ -733,4 +925,79 @@ public class ZenModePanel extends LinearLayout { fireInteraction(); } }; + + private final class TransitionHelper implements TransitionListener, Runnable { + private final ArraySet<View> mTransitioningViews = new ArraySet<View>(); + + private boolean mTransitioning; + private boolean mPendingUpdateConditions; + private boolean mPendingUpdateWidgets; + + public void clear() { + mTransitioningViews.clear(); + mPendingUpdateConditions = mPendingUpdateWidgets = false; + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println(" TransitionHelper state:"); + pw.print(" mPendingUpdateConditions="); pw.println(mPendingUpdateConditions); + pw.print(" mPendingUpdateWidgets="); pw.println(mPendingUpdateWidgets); + pw.print(" mTransitioning="); pw.println(mTransitioning); + pw.print(" mTransitioningViews="); pw.println(mTransitioningViews); + } + + public void pendingUpdateConditions() { + mPendingUpdateConditions = true; + } + + public void pendingUpdateWidgets() { + mPendingUpdateWidgets = true; + } + + public boolean isTransitioning() { + return !mTransitioningViews.isEmpty(); + } + + @Override + public void startTransition(LayoutTransition transition, + ViewGroup container, View view, int transitionType) { + mTransitioningViews.add(view); + updateTransitioning(); + } + + @Override + public void endTransition(LayoutTransition transition, + ViewGroup container, View view, int transitionType) { + mTransitioningViews.remove(view); + updateTransitioning(); + } + + @Override + public void run() { + if (DEBUG) Log.d(mTag, "TransitionHelper run" + + " mPendingUpdateWidgets=" + mPendingUpdateWidgets + + " mPendingUpdateConditions=" + mPendingUpdateConditions); + if (mPendingUpdateWidgets) { + updateWidgets(); + } + if (mPendingUpdateConditions) { + handleUpdateConditions(); + } + mPendingUpdateWidgets = mPendingUpdateConditions = false; + } + + private void updateTransitioning() { + final boolean transitioning = isTransitioning(); + if (mTransitioning == transitioning) return; + mTransitioning = transitioning; + if (DEBUG) Log.d(mTag, "TransitionHelper mTransitioning=" + mTransitioning); + if (!mTransitioning) { + if (mPendingUpdateConditions || mPendingUpdateWidgets) { + mHandler.post(this); + } else { + mPendingUpdateConditions = mPendingUpdateWidgets = false; + } + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenToast.java b/packages/SystemUI/src/com/android/systemui/volume/ZenToast.java deleted file mode 100644 index d887712..0000000 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenToast.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2014 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.volume; - -import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; -import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Resources; -import android.graphics.PixelFormat; -import android.os.Handler; -import android.os.Message; -import android.os.UserHandle; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnAttachStateChangeListener; -import android.view.WindowManager; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.systemui.R; - -public class ZenToast { - private static final String ACTION_SHOW = ZenToast.class.getName() + ".SHOW"; - private static final String ACTION_HIDE = ZenToast.class.getName() + ".HIDE"; - private static final String EXTRA_ZEN = "zen"; - private static final String EXTRA_TEXT = "text"; - - private static final int MSG_SHOW = 1; - private static final int MSG_HIDE = 2; - - private final Context mContext; - private final WindowManager mWindowManager; - - private View mZenToast; - - public ZenToast(Context context) { - mContext = context; - mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_SHOW); - filter.addAction(ACTION_HIDE); - mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); - } - - public void show(int zen) { - mHandler.removeMessages(MSG_HIDE); - mHandler.removeMessages(MSG_SHOW); - mHandler.obtainMessage(MSG_SHOW, zen, 0).sendToTarget(); - } - - public void hide() { - mHandler.removeMessages(MSG_HIDE); - mHandler.removeMessages(MSG_SHOW); - mHandler.obtainMessage(MSG_HIDE).sendToTarget(); - } - - private void handleShow(int zen, String overrideText) { - handleHide(); - - String text; - final int iconRes; - switch (zen) { - case ZEN_MODE_NO_INTERRUPTIONS: - text = mContext.getString(R.string.zen_no_interruptions); - iconRes = R.drawable.ic_zen_none; - break; - case ZEN_MODE_IMPORTANT_INTERRUPTIONS: - text = mContext.getString(R.string.zen_important_interruptions); - iconRes = R.drawable.ic_zen_important; - break; - default: - return; - } - if (overrideText != null) { - text = overrideText; - } - final Resources res = mContext.getResources(); - final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); - params.height = WindowManager.LayoutParams.WRAP_CONTENT; - params.width = res.getDimensionPixelSize(R.dimen.zen_toast_width); - params.format = PixelFormat.TRANSLUCENT; - params.windowAnimations = R.style.ZenToastAnimations; - params.type = WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL; - params.setTitle(getClass().getSimpleName()); - params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; - params.gravity = Gravity.CENTER; - params.packageName = mContext.getPackageName(); - mZenToast = LayoutInflater.from(mContext).inflate(R.layout.zen_toast, null); - final TextView message = (TextView) mZenToast.findViewById(android.R.id.message); - message.setText(text); - final ImageView icon = (ImageView) mZenToast.findViewById(android.R.id.icon); - icon.setImageResource(iconRes); - mZenToast.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { - @Override - public void onViewDetachedFromWindow(View v) { - // noop - } - - @Override - public void onViewAttachedToWindow(View v) { - mZenToast.announceForAccessibility(message.getText()); - } - }); - mWindowManager.addView(mZenToast, params); - final int animDuration = res.getInteger(R.integer.zen_toast_animation_duration); - final int visibleDuration = res.getInteger(R.integer.zen_toast_visible_duration); - mHandler.sendEmptyMessageDelayed(MSG_HIDE, animDuration + visibleDuration); - } - - private void handleHide() { - if (mZenToast != null) { - mWindowManager.removeView(mZenToast); - mZenToast = null; - } - } - - private final Handler mHandler = new Handler() { - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_SHOW: - handleShow(msg.arg1, null); - break; - case MSG_HIDE: - handleHide(); - break; - } - } - }; - - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (ACTION_SHOW.equals(intent.getAction())) { - final int zen = intent.getIntExtra(EXTRA_ZEN, ZEN_MODE_IMPORTANT_INTERRUPTIONS); - final String text = intent.getStringExtra(EXTRA_TEXT); - handleShow(zen, text); - } else if (ACTION_HIDE.equals(intent.getAction())) { - handleHide(); - } - } - }; -} |
