Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \
--bid 4335822 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4335822.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
diff --git a/com/android/systemui/statusbar/Abortable.java b/com/android/systemui/statusbar/Abortable.java
new file mode 100644
index 0000000..d5ec4f6
--- /dev/null
+++ b/com/android/systemui/statusbar/Abortable.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * An interface that allows aborting existing operations.
+ */
+public interface Abortable {
+ void abort();
+}
diff --git a/com/android/systemui/statusbar/ActivatableNotificationView.java b/com/android/systemui/statusbar/ActivatableNotificationView.java
new file mode 100644
index 0000000..68fe9a8
--- /dev/null
+++ b/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -0,0 +1,1012 @@
+/*
+ * 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.ObjectAnimator;
+import android.animation.TimeAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.notification.FakeShadowView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.phone.DoubleTapHelper;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf}
+ * to implement dimming/activating on Keyguard for the double-tap gesture
+ */
+public abstract class ActivatableNotificationView extends ExpandableOutlineView {
+
+ private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
+ private static final int ACTIVATE_ANIMATION_LENGTH = 220;
+ private static final long DARK_ANIMATION_LENGTH = StackStateAnimator.ANIMATION_DURATION_WAKEUP;
+
+ /**
+ * The amount of width, which is kept in the end when performing a disappear animation (also
+ * the amount from which the horizontal appearing begins)
+ */
+ private static final float HORIZONTAL_COLLAPSED_REST_PARTIAL = 0.05f;
+
+ /**
+ * At which point from [0,1] does the horizontal collapse animation end (or start when
+ * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
+ */
+ private static final float HORIZONTAL_ANIMATION_END = 0.2f;
+
+ /**
+ * At which point from [0,1] does the alpha animation end (or start when
+ * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
+ */
+ private static final float ALPHA_ANIMATION_END = 0.0f;
+
+ /**
+ * At which point from [0,1] does the horizontal collapse animation start (or start when
+ * expanding)? 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
+ */
+ private static final float HORIZONTAL_ANIMATION_START = 1.0f;
+
+ /**
+ * At which point from [0,1] does the vertical collapse animation start (or end when
+ * expanding) 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
+ */
+ 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;
+
+ /**
+ * A sentinel value when no color should be used. Can be used with {@link #setTintColor(int)}
+ * or {@link #setOverrideTintColor(int, float)}.
+ */
+ protected static final int NO_COLOR = 0;
+
+ private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
+ = new PathInterpolator(0.6f, 0, 0.5f, 1);
+ private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
+ = new PathInterpolator(0, 0, 0.5f, 1);
+ private final int mTintedRippleColor;
+ private final int mLowPriorityRippleColor;
+ protected final int mNormalRippleColor;
+ private final AccessibilityManager mAccessibilityManager;
+ private final DoubleTapHelper mDoubleTapHelper;
+
+ private boolean mDimmed;
+ private boolean mDark;
+
+ protected int mBgTint = NO_COLOR;
+ private float mBgAlpha = 1f;
+
+ /**
+ * Flag to indicate that the notification has been touched once and the second touch will
+ * click it.
+ */
+ private boolean mActivated;
+
+ private OnActivatedListener mOnActivatedListener;
+
+ private final Interpolator mSlowOutFastInInterpolator;
+ private final Interpolator mSlowOutLinearInInterpolator;
+ private Interpolator mCurrentAppearInterpolator;
+ private Interpolator mCurrentAlphaInterpolator;
+
+ private NotificationBackgroundView mBackgroundNormal;
+ private NotificationBackgroundView mBackgroundDimmed;
+ private ObjectAnimator mBackgroundAnimator;
+ private RectF mAppearAnimationRect = new RectF();
+ private float mAnimationTranslationY;
+ private boolean mDrawingAppearAnimation;
+ private ValueAnimator mAppearAnimator;
+ private ValueAnimator mBackgroundColorAnimator;
+ private float mAppearAnimationFraction = -1.0f;
+ private float mAppearAnimationTranslation;
+ private final int mNormalColor;
+ private final int mLowPriorityColor;
+ private boolean mIsBelowSpeedBump;
+ private FalsingManager mFalsingManager;
+
+ private float mNormalBackgroundVisibilityAmount;
+ private ValueAnimator mFadeInFromDarkAnimator;
+ private float mDimmedBackgroundFadeInAmount = -1;
+ private ValueAnimator.AnimatorUpdateListener mBackgroundVisibilityUpdater
+ = new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setNormalBackgroundVisibilityAmount(mBackgroundNormal.getAlpha());
+ mDimmedBackgroundFadeInAmount = mBackgroundDimmed.getAlpha();
+ }
+ };
+ private AnimatorListenerAdapter mFadeInEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mFadeInFromDarkAnimator = null;
+ mDimmedBackgroundFadeInAmount = -1;
+ updateBackground();
+ }
+ };
+ private ValueAnimator.AnimatorUpdateListener mUpdateOutlineListener
+ = new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ updateOutlineAlpha();
+ }
+ };
+ private float mShadowAlpha = 1.0f;
+ private FakeShadowView mFakeShadow;
+ private int mCurrentBackgroundTint;
+ private int mTargetTint;
+ private int mStartTint;
+ private int mOverrideTint;
+ private float mOverrideAmount;
+ private boolean mShadowHidden;
+ private boolean mWasActivatedOnDown;
+ /**
+ * Similar to mDimmed but is also true if it's not dimmable but should be
+ */
+ private boolean mNeedsDimming;
+ private int mDimmedAlpha;
+
+ public ActivatableNotificationView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
+ mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
+ setClipChildren(false);
+ setClipToPadding(false);
+ mNormalColor = context.getColor(R.color.notification_material_background_color);
+ mLowPriorityColor = context.getColor(
+ R.color.notification_material_background_low_priority_color);
+ mTintedRippleColor = context.getColor(
+ R.color.notification_ripple_tinted_color);
+ mLowPriorityRippleColor = context.getColor(
+ R.color.notification_ripple_color_low_priority);
+ mNormalRippleColor = context.getColor(
+ R.color.notification_ripple_untinted_color);
+ mFalsingManager = FalsingManager.getInstance(context);
+ mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+
+ mDoubleTapHelper = new DoubleTapHelper(this, (active) -> {
+ if (active) {
+ makeActive();
+ } else {
+ makeInactive(true /* animate */);
+ }
+ }, this::performClick, this::handleSlideBack, mFalsingManager::onNotificationDoubleTap);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mBackgroundNormal = findViewById(R.id.backgroundNormal);
+ mFakeShadow = findViewById(R.id.fake_shadow);
+ mShadowHidden = mFakeShadow.getVisibility() != VISIBLE;
+ mBackgroundDimmed = findViewById(R.id.backgroundDimmed);
+ mDimmedAlpha = Color.alpha(mContext.getColor(
+ R.color.notification_material_background_dimmed_color));
+ initBackground();
+ updateBackground();
+ updateBackgroundTint();
+ updateOutlineAlpha();
+ }
+
+ /**
+ * Sets the custom backgrounds on {@link #mBackgroundNormal} and {@link #mBackgroundDimmed}.
+ * This method can also be used to reload the backgrounds on both of those views, which can
+ * be useful in a configuration change.
+ */
+ protected void initBackground() {
+ mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg);
+ mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim);
+ }
+
+ private final Runnable mTapTimeoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ makeInactive(true /* animate */);
+ }
+ };
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (mNeedsDimming && !mActivated && ev.getActionMasked() == MotionEvent.ACTION_DOWN
+ && disallowSingleClick(ev) && !isTouchExplorationEnabled()) {
+ return true;
+ }
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ private boolean isTouchExplorationEnabled() {
+ return mAccessibilityManager.isTouchExplorationEnabled();
+ }
+
+ protected boolean disallowSingleClick(MotionEvent ev) {
+ return false;
+ }
+
+ protected boolean handleSlideBack() {
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean result;
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ mWasActivatedOnDown = mActivated;
+ }
+ if ((mNeedsDimming && !mActivated) && !isTouchExplorationEnabled() && isInteractive()) {
+ boolean wasActivated = mActivated;
+ result = handleTouchEventDimmed(event);
+ if (wasActivated && result && event.getAction() == MotionEvent.ACTION_UP) {
+ removeCallbacks(mTapTimeoutRunnable);
+ }
+ } else {
+ result = super.onTouchEvent(event);
+ }
+ return result;
+ }
+
+ /**
+ * @return whether this view is interactive and can be double tapped
+ */
+ protected boolean isInteractive() {
+ return true;
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ if (!mDimmed){
+ mBackgroundNormal.drawableHotspotChanged(x, y);
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mDimmed) {
+ mBackgroundDimmed.setState(getDrawableState());
+ } else {
+ mBackgroundNormal.setState(getDrawableState());
+ }
+ }
+
+ private boolean handleTouchEventDimmed(MotionEvent event) {
+ if (mNeedsDimming && !mDimmed) {
+ // We're actually dimmed, but our content isn't dimmable, let's ensure we have a ripple
+ super.onTouchEvent(event);
+ }
+ return mDoubleTapHelper.onTouchEvent(event, getActualHeight());
+ }
+
+ @Override
+ public boolean performClick() {
+ if (mWasActivatedOnDown || !mNeedsDimming || isTouchExplorationEnabled()) {
+ return super.performClick();
+ }
+ return false;
+ }
+
+ private void makeActive() {
+ mFalsingManager.onNotificationActive();
+ startActivateAnimation(false /* reverse */);
+ mActivated = true;
+ if (mOnActivatedListener != null) {
+ mOnActivatedListener.onActivated(this);
+ }
+ }
+
+ private void startActivateAnimation(final boolean reverse) {
+ if (!isAttachedToWindow()) {
+ return;
+ }
+ if (!isDimmable()) {
+ return;
+ }
+ int widthHalf = mBackgroundNormal.getWidth()/2;
+ int heightHalf = mBackgroundNormal.getActualHeight()/2;
+ float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf);
+ Animator animator;
+ if (reverse) {
+ animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
+ widthHalf, heightHalf, radius, 0);
+ } else {
+ animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
+ widthHalf, heightHalf, 0, radius);
+ }
+ mBackgroundNormal.setVisibility(View.VISIBLE);
+ Interpolator interpolator;
+ Interpolator alphaInterpolator;
+ if (!reverse) {
+ interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+ alphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+ } else {
+ interpolator = ACTIVATE_INVERSE_INTERPOLATOR;
+ alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR;
+ }
+ animator.setInterpolator(interpolator);
+ animator.setDuration(ACTIVATE_ANIMATION_LENGTH);
+ if (reverse) {
+ mBackgroundNormal.setAlpha(1f);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ updateBackground();
+ }
+ });
+ animator.start();
+ } else {
+ mBackgroundNormal.setAlpha(0.4f);
+ animator.start();
+ }
+ mBackgroundNormal.animate()
+ .alpha(reverse ? 0f : 1f)
+ .setInterpolator(alphaInterpolator)
+ .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float animatedFraction = animation.getAnimatedFraction();
+ if (reverse) {
+ animatedFraction = 1.0f - animatedFraction;
+ }
+ setNormalBackgroundVisibilityAmount(animatedFraction);
+ }
+ })
+ .setDuration(ACTIVATE_ANIMATION_LENGTH);
+ }
+
+ /**
+ * Cancels the hotspot and makes the notification inactive.
+ */
+ public void makeInactive(boolean animate) {
+ if (mActivated) {
+ mActivated = false;
+ if (mDimmed) {
+ if (animate) {
+ startActivateAnimation(true /* reverse */);
+ } else {
+ updateBackground();
+ }
+ }
+ }
+ if (mOnActivatedListener != null) {
+ mOnActivatedListener.onActivationReset(this);
+ }
+ removeCallbacks(mTapTimeoutRunnable);
+ }
+
+ public void setDimmed(boolean dimmed, boolean fade) {
+ mNeedsDimming = dimmed;
+ dimmed &= isDimmable();
+ if (mDimmed != dimmed) {
+ mDimmed = dimmed;
+ resetBackgroundAlpha();
+ if (fade) {
+ fadeDimmedBackground();
+ } else {
+ updateBackground();
+ }
+ }
+ }
+
+ public boolean isDimmable() {
+ return true;
+ }
+
+ public void setDark(boolean dark, boolean fade, long delay) {
+ super.setDark(dark, fade, delay);
+ if (mDark == dark) {
+ return;
+ }
+ mDark = dark;
+ updateBackground();
+ updateBackgroundTint(false);
+ if (!dark && fade && !shouldHideBackground()) {
+ fadeInFromDark(delay);
+ }
+ updateOutlineAlpha();
+ }
+
+ private void updateOutlineAlpha() {
+ if (mDark) {
+ setOutlineAlpha(0f);
+ return;
+ }
+ float alpha = NotificationStackScrollLayout.BACKGROUND_ALPHA_DIMMED;
+ alpha = (alpha + (1.0f - alpha) * mNormalBackgroundVisibilityAmount);
+ alpha *= mShadowAlpha;
+ if (mFadeInFromDarkAnimator != null) {
+ alpha *= mFadeInFromDarkAnimator.getAnimatedFraction();
+ }
+ setOutlineAlpha(alpha);
+ }
+
+ public void setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount) {
+ mNormalBackgroundVisibilityAmount = normalBackgroundVisibilityAmount;
+ updateOutlineAlpha();
+ }
+
+ @Override
+ public void setBelowSpeedBump(boolean below) {
+ super.setBelowSpeedBump(below);
+ if (below != mIsBelowSpeedBump) {
+ mIsBelowSpeedBump = below;
+ updateBackgroundTint();
+ onBelowSpeedBumpChanged();
+ }
+ }
+
+ protected void onBelowSpeedBumpChanged() {
+ }
+
+ /**
+ * @return whether we are below the speed bump
+ */
+ public boolean isBelowSpeedBump() {
+ return mIsBelowSpeedBump;
+ }
+
+ /**
+ * Sets the tint color of the background
+ */
+ public void setTintColor(int color) {
+ setTintColor(color, false);
+ }
+
+ /**
+ * Sets the tint color of the background
+ */
+ public void setTintColor(int color, boolean animated) {
+ if (color != mBgTint) {
+ mBgTint = color;
+ updateBackgroundTint(animated);
+ }
+ }
+
+ /**
+ * Set an override tint color that is used for the background.
+ *
+ * @param color the color that should be used to tint the background.
+ * This can be {@link #NO_COLOR} if the tint should be normally computed.
+ * @param overrideAmount a value from 0 to 1 how much the override tint should be used. The
+ * background color will then be the interpolation between this and the
+ * regular background color, where 1 means the overrideTintColor is fully
+ * used and the background color not at all.
+ */
+ public void setOverrideTintColor(int color, float overrideAmount) {
+ if (mDark) {
+ color = NO_COLOR;
+ overrideAmount = 0;
+ }
+ mOverrideTint = color;
+ mOverrideAmount = overrideAmount;
+ int newColor = calculateBgColor();
+ setBackgroundTintColor(newColor);
+ if (!isDimmable() && mNeedsDimming) {
+ mBackgroundNormal.setDrawableAlpha((int) NotificationUtils.interpolate(255,
+ mDimmedAlpha,
+ overrideAmount));
+ } else {
+ mBackgroundNormal.setDrawableAlpha(255);
+ }
+ }
+
+ protected void updateBackgroundTint() {
+ updateBackgroundTint(false /* animated */);
+ }
+
+ private void updateBackgroundTint(boolean animated) {
+ if (mBackgroundColorAnimator != null) {
+ mBackgroundColorAnimator.cancel();
+ }
+ int rippleColor = getRippleColor();
+ mBackgroundDimmed.setRippleColor(rippleColor);
+ mBackgroundNormal.setRippleColor(rippleColor);
+ int color = calculateBgColor();
+ if (!animated) {
+ setBackgroundTintColor(color);
+ } else if (color != mCurrentBackgroundTint) {
+ mStartTint = mCurrentBackgroundTint;
+ mTargetTint = color;
+ mBackgroundColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+ mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int newColor = NotificationUtils.interpolateColors(mStartTint, mTargetTint,
+ animation.getAnimatedFraction());
+ setBackgroundTintColor(newColor);
+ }
+ });
+ mBackgroundColorAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ mBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR);
+ mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mBackgroundColorAnimator = null;
+ }
+ });
+ mBackgroundColorAnimator.start();
+ }
+ }
+
+ private void setBackgroundTintColor(int color) {
+ if (color != mCurrentBackgroundTint) {
+ mCurrentBackgroundTint = color;
+ if (color == mNormalColor) {
+ // We don't need to tint a normal notification
+ color = 0;
+ }
+ mBackgroundDimmed.setTint(color);
+ mBackgroundNormal.setTint(color);
+ }
+ }
+
+ /**
+ * Fades in the background when exiting dark mode.
+ */
+ private void fadeInFromDark(long delay) {
+ final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal;
+ background.setAlpha(0f);
+ mBackgroundVisibilityUpdater.onAnimationUpdate(null);
+ background.animate()
+ .alpha(1f)
+ .setDuration(DARK_ANIMATION_LENGTH)
+ .setStartDelay(delay)
+ .setInterpolator(Interpolators.ALPHA_IN)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ // Jump state if we are cancelled
+ background.setAlpha(1f);
+ }
+ })
+ .setUpdateListener(mBackgroundVisibilityUpdater)
+ .start();
+ mFadeInFromDarkAnimator = TimeAnimator.ofFloat(0.0f, 1.0f);
+ mFadeInFromDarkAnimator.setDuration(DARK_ANIMATION_LENGTH);
+ mFadeInFromDarkAnimator.setStartDelay(delay);
+ mFadeInFromDarkAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ mFadeInFromDarkAnimator.addListener(mFadeInEndListener);
+ mFadeInFromDarkAnimator.addUpdateListener(mUpdateOutlineListener);
+ mFadeInFromDarkAnimator.start();
+ }
+
+ /**
+ * Fades the background when the dimmed state changes.
+ */
+ private void fadeDimmedBackground() {
+ mBackgroundDimmed.animate().cancel();
+ mBackgroundNormal.animate().cancel();
+ if (mActivated) {
+ updateBackground();
+ return;
+ }
+ if (!shouldHideBackground()) {
+ if (mDimmed) {
+ mBackgroundDimmed.setVisibility(View.VISIBLE);
+ } else {
+ mBackgroundNormal.setVisibility(View.VISIBLE);
+ }
+ }
+ float startAlpha = mDimmed ? 1f : 0;
+ float endAlpha = mDimmed ? 0 : 1f;
+ int duration = BACKGROUND_ANIMATION_LENGTH_MS;
+ // Check whether there is already a background animation running.
+ if (mBackgroundAnimator != null) {
+ startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
+ duration = (int) mBackgroundAnimator.getCurrentPlayTime();
+ mBackgroundAnimator.removeAllListeners();
+ mBackgroundAnimator.cancel();
+ if (duration <= 0) {
+ updateBackground();
+ return;
+ }
+ }
+ mBackgroundNormal.setAlpha(startAlpha);
+ mBackgroundAnimator =
+ ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
+ mBackgroundAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mBackgroundAnimator.setDuration(duration);
+ mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ updateBackground();
+ mBackgroundAnimator = null;
+ if (mFadeInFromDarkAnimator == null) {
+ mDimmedBackgroundFadeInAmount = -1;
+ }
+ }
+ });
+ mBackgroundAnimator.addUpdateListener(mBackgroundVisibilityUpdater);
+ mBackgroundAnimator.start();
+ }
+
+ protected void updateBackgroundAlpha(float transformationAmount) {
+ mBgAlpha = isChildInGroup() && mDimmed ? transformationAmount : 1f;
+ if (mDimmedBackgroundFadeInAmount != -1) {
+ mBgAlpha *= mDimmedBackgroundFadeInAmount;
+ }
+ mBackgroundDimmed.setAlpha(mBgAlpha);
+ }
+
+ protected void resetBackgroundAlpha() {
+ updateBackgroundAlpha(0f /* transformationAmount */);
+ }
+
+ protected void updateBackground() {
+ cancelFadeAnimations();
+ if (shouldHideBackground()) {
+ mBackgroundDimmed.setVisibility(INVISIBLE);
+ mBackgroundNormal.setVisibility(mActivated ? VISIBLE : INVISIBLE);
+ } else if (mDimmed) {
+ // When groups are animating to the expanded state from the lockscreen, show the
+ // normal background instead of the dimmed background
+ final boolean dontShowDimmed = isGroupExpansionChanging() && isChildInGroup();
+ mBackgroundDimmed.setVisibility(dontShowDimmed ? View.INVISIBLE : View.VISIBLE);
+ mBackgroundNormal.setVisibility((mActivated || dontShowDimmed)
+ ? View.VISIBLE
+ : View.INVISIBLE);
+ } else {
+ mBackgroundDimmed.setVisibility(View.INVISIBLE);
+ mBackgroundNormal.setVisibility(View.VISIBLE);
+ mBackgroundNormal.setAlpha(1f);
+ removeCallbacks(mTapTimeoutRunnable);
+ // make in inactive to avoid it sticking around active
+ makeInactive(false /* animate */);
+ }
+ setNormalBackgroundVisibilityAmount(
+ mBackgroundNormal.getVisibility() == View.VISIBLE ? 1.0f : 0.0f);
+ }
+
+ protected boolean shouldHideBackground() {
+ return mDark;
+ }
+
+ private void cancelFadeAnimations() {
+ if (mBackgroundAnimator != null) {
+ mBackgroundAnimator.cancel();
+ }
+ mBackgroundDimmed.animate().cancel();
+ mBackgroundNormal.animate().cancel();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ setPivotX(getWidth() / 2);
+ }
+
+ @Override
+ public void setActualHeight(int actualHeight, boolean notifyListeners) {
+ super.setActualHeight(actualHeight, notifyListeners);
+ setPivotY(actualHeight / 2);
+ mBackgroundNormal.setActualHeight(actualHeight);
+ mBackgroundDimmed.setActualHeight(actualHeight);
+ }
+
+ @Override
+ public void setClipTopAmount(int clipTopAmount) {
+ super.setClipTopAmount(clipTopAmount);
+ mBackgroundNormal.setClipTopAmount(clipTopAmount);
+ mBackgroundDimmed.setClipTopAmount(clipTopAmount);
+ }
+
+ @Override
+ public void setClipBottomAmount(int clipBottomAmount) {
+ super.setClipBottomAmount(clipBottomAmount);
+ mBackgroundNormal.setClipBottomAmount(clipBottomAmount);
+ mBackgroundDimmed.setClipBottomAmount(clipBottomAmount);
+ }
+
+ @Override
+ public void performRemoveAnimation(long duration, float translationDirection,
+ Runnable onFinishedRunnable) {
+ enableAppearDrawing(true);
+ if (mDrawingAppearAnimation) {
+ startAppearAnimation(false /* isAppearing */, translationDirection,
+ 0, duration, onFinishedRunnable);
+ } else if (onFinishedRunnable != null) {
+ onFinishedRunnable.run();
+ }
+ }
+
+ @Override
+ public void performAddAnimation(long delay, long duration) {
+ enableAppearDrawing(true);
+ if (mDrawingAppearAnimation) {
+ startAppearAnimation(true /* isAppearing */, -1.0f, delay, duration, null);
+ }
+ }
+
+ private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay,
+ long duration, final Runnable onFinishedRunnable) {
+ cancelAppearAnimation();
+ mAnimationTranslationY = translationDirection * getActualHeight();
+ if (mAppearAnimationFraction == -1.0f) {
+ // not initialized yet, we start anew
+ if (isAppearing) {
+ mAppearAnimationFraction = 0.0f;
+ mAppearAnimationTranslation = mAnimationTranslationY;
+ } else {
+ mAppearAnimationFraction = 1.0f;
+ mAppearAnimationTranslation = 0;
+ }
+ }
+
+ float targetValue;
+ if (isAppearing) {
+ mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
+ mCurrentAlphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+ targetValue = 1.0f;
+ } else {
+ mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN;
+ mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
+ targetValue = 0.0f;
+ }
+ mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
+ targetValue);
+ mAppearAnimator.setInterpolator(Interpolators.LINEAR);
+ mAppearAnimator.setDuration(
+ (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
+ mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mAppearAnimationFraction = (float) animation.getAnimatedValue();
+ updateAppearAnimationAlpha();
+ updateAppearRect();
+ invalidate();
+ }
+ });
+ if (delay > 0) {
+ // we need to apply the initial state already to avoid drawn frames in the wrong state
+ updateAppearAnimationAlpha();
+ updateAppearRect();
+ mAppearAnimator.setStartDelay(delay);
+ }
+ mAppearAnimator.addListener(new AnimatorListenerAdapter() {
+ private boolean mWasCancelled;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (onFinishedRunnable != null) {
+ onFinishedRunnable.run();
+ }
+ if (!mWasCancelled) {
+ enableAppearDrawing(false);
+ onAppearAnimationFinished(isAppearing);
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mWasCancelled = false;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mWasCancelled = true;
+ }
+ });
+ mAppearAnimator.start();
+ }
+
+ protected void onAppearAnimationFinished(boolean wasAppearing) {
+ }
+
+ private void cancelAppearAnimation() {
+ if (mAppearAnimator != null) {
+ mAppearAnimator.cancel();
+ mAppearAnimator = null;
+ }
+ }
+
+ public void cancelAppearDrawing() {
+ cancelAppearAnimation();
+ enableAppearDrawing(false);
+ }
+
+ private void updateAppearRect() {
+ float inverseFraction = (1.0f - mAppearAnimationFraction);
+ float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
+ float translateYTotalAmount = translationFraction * mAnimationTranslationY;
+ mAppearAnimationTranslation = translateYTotalAmount;
+
+ // handle width animation
+ float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
+ / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
+ widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
+ widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
+ float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) *
+ widthFraction);
+ float right = getWidth() - left;
+
+ // handle top animation
+ float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
+ VERTICAL_ANIMATION_START;
+ heightFraction = Math.max(0.0f, heightFraction);
+ heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
+
+ float top;
+ float bottom;
+ final int actualHeight = getActualHeight();
+ if (mAnimationTranslationY > 0.0f) {
+ bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f
+ - translateYTotalAmount;
+ top = bottom * heightFraction;
+ } else {
+ top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f -
+ translateYTotalAmount;
+ bottom = actualHeight * (1 - heightFraction) + top * heightFraction;
+ }
+ mAppearAnimationRect.set(left, top, right, bottom);
+ setOutlineRect(left, top + mAppearAnimationTranslation, right,
+ bottom + mAppearAnimationTranslation);
+ }
+
+ private void updateAppearAnimationAlpha() {
+ float contentAlphaProgress = mAppearAnimationFraction;
+ contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
+ contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
+ contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
+ setContentAlpha(contentAlphaProgress);
+ }
+
+ private void setContentAlpha(float contentAlpha) {
+ View contentView = getContentView();
+ if (contentView.hasOverlappingRendering()) {
+ int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE
+ : LAYER_TYPE_HARDWARE;
+ int currentLayerType = contentView.getLayerType();
+ if (currentLayerType != layerType) {
+ contentView.setLayerType(layerType, null);
+ }
+ }
+ contentView.setAlpha(contentAlpha);
+ }
+
+ protected abstract View getContentView();
+
+ public int calculateBgColor() {
+ return calculateBgColor(true /* withTint */, true /* withOverRide */);
+ }
+
+ /**
+ * @param withTint should a possible tint be factored in?
+ * @param withOverRide should the value be interpolated with {@link #mOverrideTint}
+ * @return the calculated background color
+ */
+ private int calculateBgColor(boolean withTint, boolean withOverRide) {
+ if (withTint && mDark) {
+ return getContext().getColor(R.color.notification_material_background_dark_color);
+ }
+ if (withOverRide && mOverrideTint != NO_COLOR) {
+ int defaultTint = calculateBgColor(withTint, false);
+ return NotificationUtils.interpolateColors(defaultTint, mOverrideTint, mOverrideAmount);
+ }
+ if (withTint && mBgTint != NO_COLOR) {
+ return mBgTint;
+ } else if (mIsBelowSpeedBump) {
+ return mLowPriorityColor;
+ } else {
+ return mNormalColor;
+ }
+ }
+
+ protected int getRippleColor() {
+ if (mBgTint != 0) {
+ return mTintedRippleColor;
+ } else if (mIsBelowSpeedBump) {
+ return mLowPriorityRippleColor;
+ } else {
+ return mNormalRippleColor;
+ }
+ }
+
+ /**
+ * When we draw the appear animation, we render the view in a bitmap and render this bitmap
+ * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
+ * such that the normal drawing of the views does not happen anymore.
+ *
+ * @param enable Should it be enabled.
+ */
+ private void enableAppearDrawing(boolean enable) {
+ if (enable != mDrawingAppearAnimation) {
+ mDrawingAppearAnimation = enable;
+ if (!enable) {
+ setContentAlpha(1.0f);
+ mAppearAnimationFraction = -1;
+ setOutlineRect(null);
+ }
+ invalidate();
+ }
+ }
+
+ public boolean isDrawingAppearAnimation() {
+ return mDrawingAppearAnimation;
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (mDrawingAppearAnimation) {
+ canvas.save();
+ canvas.translate(0, mAppearAnimationTranslation);
+ }
+ super.dispatchDraw(canvas);
+ if (mDrawingAppearAnimation) {
+ canvas.restore();
+ }
+ }
+
+ public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
+ mOnActivatedListener = onActivatedListener;
+ }
+
+ public boolean hasSameBgColor(ActivatableNotificationView otherView) {
+ return calculateBgColor() == otherView.calculateBgColor();
+ }
+
+ @Override
+ public float getShadowAlpha() {
+ return mShadowAlpha;
+ }
+
+ @Override
+ public void setShadowAlpha(float shadowAlpha) {
+ if (shadowAlpha != mShadowAlpha) {
+ mShadowAlpha = shadowAlpha;
+ updateOutlineAlpha();
+ }
+ }
+
+ @Override
+ public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
+ int outlineTranslation) {
+ boolean hiddenBefore = mShadowHidden;
+ mShadowHidden = shadowIntensity == 0.0f;
+ if (!mShadowHidden || !hiddenBefore) {
+ mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (getTranslationZ()
+ + FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd,
+ outlineTranslation);
+ }
+ }
+
+ public int getBackgroundColorWithoutTint() {
+ return calculateBgColor(false /* withTint */, false /* withOverride */);
+ }
+
+ public interface OnActivatedListener {
+ void onActivated(ActivatableNotificationView view);
+ void onActivationReset(ActivatableNotificationView view);
+ }
+}
diff --git a/com/android/systemui/statusbar/AlphaImageView.java b/com/android/systemui/statusbar/AlphaImageView.java
new file mode 100644
index 0000000..06dc4e6
--- /dev/null
+++ b/com/android/systemui/statusbar/AlphaImageView.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.statusbar;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * An ImageView which does not have overlapping renderings commands and therefore does not need a
+ * layer when alpha is changed.
+ */
+public class AlphaImageView extends ImageView {
+ public AlphaImageView(Context context) {
+ super(context);
+ }
+
+ public AlphaImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AlphaImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public AlphaImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+}
diff --git a/com/android/systemui/statusbar/AlphaOptimizedButton.java b/com/android/systemui/statusbar/AlphaOptimizedButton.java
new file mode 100644
index 0000000..87c12c2
--- /dev/null
+++ b/com/android/systemui/statusbar/AlphaOptimizedButton.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 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.widget.Button;
+
+/**
+ * A Button which doesn't have overlapping drawing commands
+ */
+public class AlphaOptimizedButton extends Button {
+ public AlphaOptimizedButton(Context context) {
+ super(context);
+ }
+
+ public AlphaOptimizedButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AlphaOptimizedButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public AlphaOptimizedButton(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+}
diff --git a/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java b/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java
new file mode 100644
index 0000000..359272e
--- /dev/null
+++ b/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java
@@ -0,0 +1,50 @@
+/*
+ * 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.widget.FrameLayout;
+
+/**
+ * A frame layout which does not have overlapping renderings commands and therefore does not need a
+ * layer when alpha is changed.
+ */
+public class AlphaOptimizedFrameLayout extends FrameLayout
+{
+ public AlphaOptimizedFrameLayout(Context context) {
+ super(context);
+ }
+
+ public AlphaOptimizedFrameLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AlphaOptimizedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public AlphaOptimizedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+}
diff --git a/com/android/systemui/statusbar/AlphaOptimizedImageView.java b/com/android/systemui/statusbar/AlphaOptimizedImageView.java
new file mode 100644
index 0000000..ef03d5f
--- /dev/null
+++ b/com/android/systemui/statusbar/AlphaOptimizedImageView.java
@@ -0,0 +1,50 @@
+/*
+ * 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.widget.ImageView;
+
+/**
+ * An ImageView which supports an attribute specifying whether it has overlapping rendering
+ * commands and therefore does not need a layer when alpha is changed.
+ */
+public class AlphaOptimizedImageView extends ImageView {
+
+ public AlphaOptimizedImageView(Context context) {
+ this(context, null /* attrs */);
+ }
+
+ public AlphaOptimizedImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /* defStyleAttr */);
+ }
+
+ public AlphaOptimizedImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+ }
+
+ public AlphaOptimizedImageView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+}
diff --git a/com/android/systemui/statusbar/AlphaOptimizedView.java b/com/android/systemui/statusbar/AlphaOptimizedView.java
new file mode 100644
index 0000000..d2fe858
--- /dev/null
+++ b/com/android/systemui/statusbar/AlphaOptimizedView.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+/**
+ * A View which does not have overlapping renderings commands and therefore does not need a
+ * layer when alpha is changed.
+ */
+public class AlphaOptimizedView extends View
+{
+ public AlphaOptimizedView(Context context) {
+ super(context);
+ }
+
+ public AlphaOptimizedView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AlphaOptimizedView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public AlphaOptimizedView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+}
diff --git a/com/android/systemui/statusbar/AnimatedImageView.java b/com/android/systemui/statusbar/AnimatedImageView.java
new file mode 100644
index 0000000..ba92c45
--- /dev/null
+++ b/com/android/systemui/statusbar/AnimatedImageView.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.systemui.R;
+
+@RemoteView
+public class AnimatedImageView extends ImageView {
+ private final boolean mHasOverlappingRendering;
+ AnimationDrawable mAnim;
+ boolean mAttached;
+ private boolean mAllowAnimation = true;
+
+ // Tracks the last image that was set, so that we don't refresh the image if it is exactly
+ // the same as the previous one. If this is a resid, we track that. If it's a drawable, we
+ // track the hashcode of the drawable.
+ int mDrawableId;
+
+ public AnimatedImageView(Context context) {
+ this(context, null);
+ }
+
+ public AnimatedImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
+ R.styleable.AnimatedImageView, 0, 0);
+
+ try {
+ // Default to true, which is what View.java defaults toA
+ mHasOverlappingRendering = a.getBoolean(
+ R.styleable.AnimatedImageView_hasOverlappingRendering, true);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ public void setAllowAnimation(boolean allowAnimation) {
+ if (mAllowAnimation != allowAnimation) {
+ mAllowAnimation = allowAnimation;
+ updateAnim();
+ if (!mAllowAnimation && mAnim != null) {
+ // Reset drawable such that we show the first frame whenever we're not animating.
+ mAnim.setVisible(getVisibility() == VISIBLE, true /* restart */);
+ }
+ }
+ }
+
+ private void updateAnim() {
+ Drawable drawable = getDrawable();
+ if (mAttached && mAnim != null) {
+ mAnim.stop();
+ }
+ if (drawable instanceof AnimationDrawable) {
+ mAnim = (AnimationDrawable) drawable;
+ if (isShown() && mAllowAnimation) {
+ mAnim.start();
+ }
+ } else {
+ mAnim = null;
+ }
+ }
+
+ @Override
+ public void setImageDrawable(Drawable drawable) {
+ if (drawable != null) {
+ if (mDrawableId == drawable.hashCode()) return;
+
+ mDrawableId = drawable.hashCode();
+ } else {
+ mDrawableId = 0;
+ }
+ super.setImageDrawable(drawable);
+ updateAnim();
+ }
+
+ @Override
+ @android.view.RemotableViewMethod
+ public void setImageResource(int resid) {
+ if (mDrawableId == resid) return;
+
+ mDrawableId = resid;
+ super.setImageResource(resid);
+ updateAnim();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mAttached = true;
+ updateAnim();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mAnim != null) {
+ mAnim.stop();
+ }
+ mAttached = false;
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int vis) {
+ super.onVisibilityChanged(changedView, vis);
+ if (mAnim != null) {
+ if (isShown() && mAllowAnimation) {
+ mAnim.start();
+ } else {
+ mAnim.stop();
+ }
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return mHasOverlappingRendering;
+ }
+}
+
diff --git a/com/android/systemui/statusbar/BackDropView.java b/com/android/systemui/statusbar/BackDropView.java
new file mode 100644
index 0000000..f1eb9fe
--- /dev/null
+++ b/com/android/systemui/statusbar/BackDropView.java
@@ -0,0 +1,65 @@
+/*
+ * 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.FrameLayout;
+
+/**
+ * A view who contains media artwork.
+ */
+public class BackDropView extends FrameLayout
+{
+ private Runnable mOnVisibilityChangedRunnable;
+
+ public BackDropView(Context context) {
+ super(context);
+ }
+
+ public BackDropView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public BackDropView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public BackDropView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ if (changedView == this && mOnVisibilityChangedRunnable != null) {
+ mOnVisibilityChangedRunnable.run();
+ }
+ }
+
+ public void setOnVisibilityChangedRunnable(Runnable runnable) {
+ mOnVisibilityChangedRunnable = runnable;
+ }
+
+}
diff --git a/com/android/systemui/statusbar/CommandQueue.java b/com/android/systemui/statusbar/CommandQueue.java
new file mode 100644
index 0000000..6349275
--- /dev/null
+++ b/com/android/systemui/statusbar/CommandQueue.java
@@ -0,0 +1,670 @@
+/*
+ * Copyright (C) 2010 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.ComponentName;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.VisibleForTesting;
+import android.util.Pair;
+
+import com.android.internal.os.SomeArgs;
+import com.android.internal.statusbar.IStatusBar;
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.SystemUI;
+
+import java.util.ArrayList;
+
+/**
+ * This class takes the functions from IStatusBar that come in on
+ * binder pool threads and posts messages to get them onto the main
+ * thread, and calls onto Callbacks. It also takes care of
+ * coalescing these calls so they don't stack up. For the calls
+ * are coalesced, note that they are all idempotent.
+ */
+public class CommandQueue extends IStatusBar.Stub {
+ private static final int INDEX_MASK = 0xffff;
+ private static final int MSG_SHIFT = 16;
+ private static final int MSG_MASK = 0xffff << MSG_SHIFT;
+
+ private static final int OP_SET_ICON = 1;
+ private static final int OP_REMOVE_ICON = 2;
+
+ private static final int MSG_ICON = 1 << MSG_SHIFT;
+ private static final int MSG_DISABLE = 2 << MSG_SHIFT;
+ private static final int MSG_EXPAND_NOTIFICATIONS = 3 << MSG_SHIFT;
+ private static final int MSG_COLLAPSE_PANELS = 4 << MSG_SHIFT;
+ private static final int MSG_EXPAND_SETTINGS = 5 << MSG_SHIFT;
+ private static final int MSG_SET_SYSTEMUI_VISIBILITY = 6 << MSG_SHIFT;
+ private static final int MSG_TOP_APP_WINDOW_CHANGED = 7 << MSG_SHIFT;
+ private static final int MSG_SHOW_IME_BUTTON = 8 << MSG_SHIFT;
+ private static final int MSG_TOGGLE_RECENT_APPS = 9 << MSG_SHIFT;
+ private static final int MSG_PRELOAD_RECENT_APPS = 10 << MSG_SHIFT;
+ private static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 11 << MSG_SHIFT;
+ private static final int MSG_SET_WINDOW_STATE = 12 << MSG_SHIFT;
+ private static final int MSG_SHOW_RECENT_APPS = 13 << MSG_SHIFT;
+ private static final int MSG_HIDE_RECENT_APPS = 14 << MSG_SHIFT;
+ private static final int MSG_SHOW_SCREEN_PIN_REQUEST = 18 << MSG_SHIFT;
+ private static final int MSG_APP_TRANSITION_PENDING = 19 << MSG_SHIFT;
+ private static final int MSG_APP_TRANSITION_CANCELLED = 20 << MSG_SHIFT;
+ private static final int MSG_APP_TRANSITION_STARTING = 21 << MSG_SHIFT;
+ private static final int MSG_ASSIST_DISCLOSURE = 22 << MSG_SHIFT;
+ private static final int MSG_START_ASSIST = 23 << MSG_SHIFT;
+ private static final int MSG_CAMERA_LAUNCH_GESTURE = 24 << MSG_SHIFT;
+ private static final int MSG_TOGGLE_KEYBOARD_SHORTCUTS = 25 << MSG_SHIFT;
+ private static final int MSG_SHOW_PICTURE_IN_PICTURE_MENU = 26 << MSG_SHIFT;
+ private static final int MSG_ADD_QS_TILE = 27 << MSG_SHIFT;
+ private static final int MSG_REMOVE_QS_TILE = 28 << MSG_SHIFT;
+ private static final int MSG_CLICK_QS_TILE = 29 << MSG_SHIFT;
+ private static final int MSG_TOGGLE_APP_SPLIT_SCREEN = 30 << MSG_SHIFT;
+ private static final int MSG_APP_TRANSITION_FINISHED = 31 << MSG_SHIFT;
+ private static final int MSG_DISMISS_KEYBOARD_SHORTCUTS = 32 << MSG_SHIFT;
+ private static final int MSG_HANDLE_SYSTEM_KEY = 33 << MSG_SHIFT;
+ private static final int MSG_SHOW_GLOBAL_ACTIONS = 34 << MSG_SHIFT;
+ private static final int MSG_TOGGLE_PANEL = 35 << MSG_SHIFT;
+ private static final int MSG_SHOW_SHUTDOWN_UI = 36 << MSG_SHIFT;
+ private static final int MSG_SET_TOP_APP_HIDES_STATUS_BAR = 37 << MSG_SHIFT;
+
+ public static final int FLAG_EXCLUDE_NONE = 0;
+ public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
+ public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1;
+ public static final int FLAG_EXCLUDE_NOTIFICATION_PANEL = 1 << 2;
+ public static final int FLAG_EXCLUDE_INPUT_METHODS_PANEL = 1 << 3;
+ public static final int FLAG_EXCLUDE_COMPAT_MODE_PANEL = 1 << 4;
+
+ private static final String SHOW_IME_SWITCHER_KEY = "showImeSwitcherKey";
+
+ private final Object mLock = new Object();
+ private ArrayList<Callbacks> mCallbacks = new ArrayList<>();
+ private Handler mHandler = new H(Looper.getMainLooper());
+ private int mDisable1;
+ private int mDisable2;
+
+ /**
+ * These methods are called back on the main thread.
+ */
+ public interface Callbacks {
+ default void setIcon(String slot, StatusBarIcon icon) { }
+ default void removeIcon(String slot) { }
+ default void disable(int state1, int state2, boolean animate) { }
+ default void animateExpandNotificationsPanel() { }
+ default void animateCollapsePanels(int flags) { }
+ default void togglePanel() { }
+ default void animateExpandSettingsPanel(String obj) { }
+ default void setSystemUiVisibility(int vis, int fullscreenStackVis,
+ int dockedStackVis, int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
+ }
+ default void topAppWindowChanged(boolean visible) { }
+ default void setImeWindowStatus(IBinder token, int vis, int backDisposition,
+ boolean showImeSwitcher) { }
+ default void showRecentApps(boolean triggeredFromAltTab, boolean fromHome) { }
+ default void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { }
+ default void toggleRecentApps() { }
+ default void toggleSplitScreen() { }
+ default void preloadRecentApps() { }
+ default void dismissKeyboardShortcutsMenu() { }
+ default void toggleKeyboardShortcutsMenu(int deviceId) { }
+ default void cancelPreloadRecentApps() { }
+ default void setWindowState(int window, int state) { }
+ default void showScreenPinningRequest(int taskId) { }
+ default void appTransitionPending(boolean forced) { }
+ default void appTransitionCancelled() { }
+ default void appTransitionStarting(long startTime, long duration, boolean forced) { }
+ default void appTransitionFinished() { }
+ default void showAssistDisclosure() { }
+ default void startAssist(Bundle args) { }
+ default void onCameraLaunchGestureDetected(int source) { }
+ default void showPictureInPictureMenu() { }
+ default void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) { }
+
+ default void addQsTile(ComponentName tile) { }
+ default void remQsTile(ComponentName tile) { }
+ default void clickTile(ComponentName tile) { }
+
+ default void handleSystemKey(int arg1) { }
+ default void handleShowGlobalActionsMenu() { }
+ default void handleShowShutdownUi(boolean isReboot, String reason) { }
+ }
+
+ @VisibleForTesting
+ protected CommandQueue() {
+ }
+
+ public void addCallbacks(Callbacks callbacks) {
+ mCallbacks.add(callbacks);
+ callbacks.disable(mDisable1, mDisable2, false /* animate */);
+ }
+
+ public void removeCallbacks(Callbacks callbacks) {
+ mCallbacks.remove(callbacks);
+ }
+
+ public void setIcon(String slot, StatusBarIcon icon) {
+ synchronized (mLock) {
+ // don't coalesce these
+ mHandler.obtainMessage(MSG_ICON, OP_SET_ICON, 0,
+ new Pair<String, StatusBarIcon>(slot, icon)).sendToTarget();
+ }
+ }
+
+ public void removeIcon(String slot) {
+ synchronized (mLock) {
+ // don't coalesce these
+ mHandler.obtainMessage(MSG_ICON, OP_REMOVE_ICON, 0, slot).sendToTarget();
+ }
+ }
+
+ public void disable(int state1, int state2, boolean animate) {
+ synchronized (mLock) {
+ mDisable1 = state1;
+ mDisable2 = state2;
+ mHandler.removeMessages(MSG_DISABLE);
+ Message msg = mHandler.obtainMessage(MSG_DISABLE, state1, state2, animate);
+ if (Looper.myLooper() == mHandler.getLooper()) {
+ // If its the right looper execute immediately so hides can be handled quickly.
+ mHandler.handleMessage(msg);
+ msg.recycle();
+ } else {
+ msg.sendToTarget();
+ }
+ }
+ }
+
+ public void disable(int state1, int state2) {
+ disable(state1, state2, true);
+ }
+
+ public void recomputeDisableFlags(boolean animate) {
+ disable(mDisable1, mDisable2, animate);
+ }
+
+ public void animateExpandNotificationsPanel() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_EXPAND_NOTIFICATIONS);
+ mHandler.sendEmptyMessage(MSG_EXPAND_NOTIFICATIONS);
+ }
+ }
+
+ public void animateCollapsePanels() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_COLLAPSE_PANELS);
+ mHandler.obtainMessage(MSG_COLLAPSE_PANELS, 0, 0).sendToTarget();
+ }
+ }
+
+ public void animateCollapsePanels(int flags) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_COLLAPSE_PANELS);
+ mHandler.obtainMessage(MSG_COLLAPSE_PANELS, flags, 0).sendToTarget();
+ }
+ }
+
+ public void togglePanel() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_TOGGLE_PANEL);
+ mHandler.obtainMessage(MSG_TOGGLE_PANEL, 0, 0).sendToTarget();
+ }
+ }
+
+ public void animateExpandSettingsPanel(String subPanel) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_EXPAND_SETTINGS);
+ mHandler.obtainMessage(MSG_EXPAND_SETTINGS, subPanel).sendToTarget();
+ }
+ }
+
+ public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
+ int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
+ synchronized (mLock) {
+ // Don't coalesce these, since it might have one time flags set such as
+ // STATUS_BAR_UNHIDE which might get lost.
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = vis;
+ args.argi2 = fullscreenStackVis;
+ args.argi3 = dockedStackVis;
+ args.argi4 = mask;
+ args.arg1 = fullscreenStackBounds;
+ args.arg2 = dockedStackBounds;
+ mHandler.obtainMessage(MSG_SET_SYSTEMUI_VISIBILITY, args).sendToTarget();
+ }
+ }
+
+ public void topAppWindowChanged(boolean menuVisible) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_TOP_APP_WINDOW_CHANGED);
+ mHandler.obtainMessage(MSG_TOP_APP_WINDOW_CHANGED, menuVisible ? 1 : 0, 0,
+ null).sendToTarget();
+ }
+ }
+
+ public void setImeWindowStatus(IBinder token, int vis, int backDisposition,
+ boolean showImeSwitcher) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_SHOW_IME_BUTTON);
+ Message m = mHandler.obtainMessage(MSG_SHOW_IME_BUTTON, vis, backDisposition, token);
+ m.getData().putBoolean(SHOW_IME_SWITCHER_KEY, showImeSwitcher);
+ m.sendToTarget();
+ }
+ }
+
+ public void showRecentApps(boolean triggeredFromAltTab, boolean fromHome) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_SHOW_RECENT_APPS);
+ mHandler.obtainMessage(MSG_SHOW_RECENT_APPS,
+ triggeredFromAltTab ? 1 : 0, fromHome ? 1 : 0, null).sendToTarget();
+ }
+ }
+
+ public void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_HIDE_RECENT_APPS);
+ mHandler.obtainMessage(MSG_HIDE_RECENT_APPS,
+ triggeredFromAltTab ? 1 : 0, triggeredFromHomeKey ? 1 : 0,
+ null).sendToTarget();
+ }
+ }
+
+ public void toggleSplitScreen() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_TOGGLE_APP_SPLIT_SCREEN);
+ mHandler.obtainMessage(MSG_TOGGLE_APP_SPLIT_SCREEN, 0, 0, null).sendToTarget();
+ }
+ }
+
+ public void toggleRecentApps() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_TOGGLE_RECENT_APPS);
+ Message msg = mHandler.obtainMessage(MSG_TOGGLE_RECENT_APPS, 0, 0, null);
+ msg.setAsynchronous(true);
+ msg.sendToTarget();
+ }
+ }
+
+ public void preloadRecentApps() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_PRELOAD_RECENT_APPS);
+ mHandler.obtainMessage(MSG_PRELOAD_RECENT_APPS, 0, 0, null).sendToTarget();
+ }
+ }
+
+ public void cancelPreloadRecentApps() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_CANCEL_PRELOAD_RECENT_APPS);
+ mHandler.obtainMessage(MSG_CANCEL_PRELOAD_RECENT_APPS, 0, 0, null).sendToTarget();
+ }
+ }
+
+ @Override
+ public void dismissKeyboardShortcutsMenu() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_DISMISS_KEYBOARD_SHORTCUTS);
+ mHandler.obtainMessage(MSG_DISMISS_KEYBOARD_SHORTCUTS).sendToTarget();
+ }
+ }
+
+ @Override
+ public void toggleKeyboardShortcutsMenu(int deviceId) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_TOGGLE_KEYBOARD_SHORTCUTS);
+ mHandler.obtainMessage(MSG_TOGGLE_KEYBOARD_SHORTCUTS, deviceId, 0).sendToTarget();
+ }
+ }
+
+ @Override
+ public void showPictureInPictureMenu() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_SHOW_PICTURE_IN_PICTURE_MENU);
+ mHandler.obtainMessage(MSG_SHOW_PICTURE_IN_PICTURE_MENU).sendToTarget();
+ }
+ }
+
+ public void setWindowState(int window, int state) {
+ synchronized (mLock) {
+ // don't coalesce these
+ mHandler.obtainMessage(MSG_SET_WINDOW_STATE, window, state, null).sendToTarget();
+ }
+ }
+
+ public void showScreenPinningRequest(int taskId) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_SHOW_SCREEN_PIN_REQUEST, taskId, 0, null)
+ .sendToTarget();
+ }
+ }
+
+ public void appTransitionPending() {
+ appTransitionPending(false /* forced */);
+ }
+
+ public void appTransitionPending(boolean forced) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_APP_TRANSITION_PENDING, forced ? 1 : 0, 0).sendToTarget();
+ }
+ }
+
+ public void appTransitionCancelled() {
+ synchronized (mLock) {
+ mHandler.sendEmptyMessage(MSG_APP_TRANSITION_CANCELLED);
+ }
+ }
+
+ public void appTransitionStarting(long startTime, long duration) {
+ appTransitionStarting(startTime, duration, false /* forced */);
+ }
+
+ public void appTransitionStarting(long startTime, long duration, boolean forced) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_APP_TRANSITION_STARTING, forced ? 1 : 0, 0,
+ Pair.create(startTime, duration)).sendToTarget();
+ }
+ }
+
+ @Override
+ public void appTransitionFinished() {
+ synchronized (mLock) {
+ mHandler.sendEmptyMessage(MSG_APP_TRANSITION_FINISHED);
+ }
+ }
+
+ public void showAssistDisclosure() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_ASSIST_DISCLOSURE);
+ mHandler.obtainMessage(MSG_ASSIST_DISCLOSURE).sendToTarget();
+ }
+ }
+
+ public void startAssist(Bundle args) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_START_ASSIST);
+ mHandler.obtainMessage(MSG_START_ASSIST, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void onCameraLaunchGestureDetected(int source) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_CAMERA_LAUNCH_GESTURE);
+ mHandler.obtainMessage(MSG_CAMERA_LAUNCH_GESTURE, source, 0).sendToTarget();
+ }
+ }
+
+ @Override
+ public void addQsTile(ComponentName tile) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget();
+ }
+ }
+
+ @Override
+ public void remQsTile(ComponentName tile) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_REMOVE_QS_TILE, tile).sendToTarget();
+ }
+ }
+
+ @Override
+ public void clickQsTile(ComponentName tile) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_CLICK_QS_TILE, tile).sendToTarget();
+ }
+ }
+
+ @Override
+ public void handleSystemKey(int key) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_HANDLE_SYSTEM_KEY, key, 0).sendToTarget();
+ }
+ }
+
+ @Override
+ public void showGlobalActionsMenu() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_SHOW_GLOBAL_ACTIONS);
+ mHandler.obtainMessage(MSG_SHOW_GLOBAL_ACTIONS).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setTopAppHidesStatusBar(boolean hidesStatusBar) {
+ mHandler.removeMessages(MSG_SET_TOP_APP_HIDES_STATUS_BAR);
+ mHandler.obtainMessage(MSG_SET_TOP_APP_HIDES_STATUS_BAR, hidesStatusBar ? 1 : 0, 0)
+ .sendToTarget();
+ }
+
+ @Override
+ public void showShutdownUi(boolean isReboot, String reason) {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_SHOW_SHUTDOWN_UI);
+ mHandler.obtainMessage(MSG_SHOW_SHUTDOWN_UI, isReboot ? 1 : 0, 0, reason)
+ .sendToTarget();
+ }
+ }
+
+ private final class H extends Handler {
+ private H(Looper l) {
+ super(l);
+ }
+
+ public void handleMessage(Message msg) {
+ final int what = msg.what & MSG_MASK;
+ switch (what) {
+ case MSG_ICON: {
+ switch (msg.arg1) {
+ case OP_SET_ICON: {
+ Pair<String, StatusBarIcon> p = (Pair<String, StatusBarIcon>) msg.obj;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).setIcon(p.first, p.second);
+ }
+ break;
+ }
+ case OP_REMOVE_ICON:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).removeIcon((String) msg.obj);
+ }
+ break;
+ }
+ break;
+ }
+ case MSG_DISABLE:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).disable(msg.arg1, msg.arg2, (Boolean) msg.obj);
+ }
+ break;
+ case MSG_EXPAND_NOTIFICATIONS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).animateExpandNotificationsPanel();
+ }
+ break;
+ case MSG_COLLAPSE_PANELS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).animateCollapsePanels(msg.arg1);
+ }
+ break;
+ case MSG_TOGGLE_PANEL:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).togglePanel();
+ }
+ break;
+ case MSG_EXPAND_SETTINGS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).animateExpandSettingsPanel((String) msg.obj);
+ }
+ break;
+ case MSG_SET_SYSTEMUI_VISIBILITY:
+ SomeArgs args = (SomeArgs) msg.obj;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).setSystemUiVisibility(args.argi1, args.argi2, args.argi3,
+ args.argi4, (Rect) args.arg1, (Rect) args.arg2);
+ }
+ args.recycle();
+ break;
+ case MSG_TOP_APP_WINDOW_CHANGED:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).topAppWindowChanged(msg.arg1 != 0);
+ }
+ break;
+ case MSG_SHOW_IME_BUTTON:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).setImeWindowStatus((IBinder) msg.obj, msg.arg1, msg.arg2,
+ msg.getData().getBoolean(SHOW_IME_SWITCHER_KEY, false));
+ }
+ break;
+ case MSG_SHOW_RECENT_APPS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).showRecentApps(msg.arg1 != 0, msg.arg2 != 0);
+ }
+ break;
+ case MSG_HIDE_RECENT_APPS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).hideRecentApps(msg.arg1 != 0, msg.arg2 != 0);
+ }
+ break;
+ case MSG_TOGGLE_RECENT_APPS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).toggleRecentApps();
+ }
+ break;
+ case MSG_PRELOAD_RECENT_APPS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).preloadRecentApps();
+ }
+ break;
+ case MSG_CANCEL_PRELOAD_RECENT_APPS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).cancelPreloadRecentApps();
+ }
+ break;
+ case MSG_DISMISS_KEYBOARD_SHORTCUTS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).dismissKeyboardShortcutsMenu();
+ }
+ break;
+ case MSG_TOGGLE_KEYBOARD_SHORTCUTS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).toggleKeyboardShortcutsMenu(msg.arg1);
+ }
+ break;
+ case MSG_SET_WINDOW_STATE:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).setWindowState(msg.arg1, msg.arg2);
+ }
+ break;
+ case MSG_SHOW_SCREEN_PIN_REQUEST:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).showScreenPinningRequest(msg.arg1);
+ }
+ break;
+ case MSG_APP_TRANSITION_PENDING:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).appTransitionPending(msg.arg1 != 0);
+ }
+ break;
+ case MSG_APP_TRANSITION_CANCELLED:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).appTransitionCancelled();
+ }
+ break;
+ case MSG_APP_TRANSITION_STARTING:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ Pair<Long, Long> data = (Pair<Long, Long>) msg.obj;
+ mCallbacks.get(i).appTransitionStarting(data.first, data.second,
+ msg.arg1 != 0);
+ }
+ break;
+ case MSG_APP_TRANSITION_FINISHED:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).appTransitionFinished();
+ }
+ break;
+ case MSG_ASSIST_DISCLOSURE:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).showAssistDisclosure();
+ }
+ break;
+ case MSG_START_ASSIST:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).startAssist((Bundle) msg.obj);
+ }
+ break;
+ case MSG_CAMERA_LAUNCH_GESTURE:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).onCameraLaunchGestureDetected(msg.arg1);
+ }
+ break;
+ case MSG_SHOW_PICTURE_IN_PICTURE_MENU:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).showPictureInPictureMenu();
+ }
+ break;
+ case MSG_ADD_QS_TILE:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).addQsTile((ComponentName) msg.obj);
+ }
+ break;
+ case MSG_REMOVE_QS_TILE:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).remQsTile((ComponentName) msg.obj);
+ }
+ break;
+ case MSG_CLICK_QS_TILE:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).clickTile((ComponentName) msg.obj);
+ }
+ break;
+ case MSG_TOGGLE_APP_SPLIT_SCREEN:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).toggleSplitScreen();
+ }
+ break;
+ case MSG_HANDLE_SYSTEM_KEY:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).handleSystemKey(msg.arg1);
+ }
+ break;
+ case MSG_SHOW_GLOBAL_ACTIONS:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).handleShowGlobalActionsMenu();
+ }
+ break;
+ case MSG_SHOW_SHUTDOWN_UI:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).handleShowShutdownUi(msg.arg1 != 0, (String) msg.obj);
+ }
+ break;
+ case MSG_SET_TOP_APP_HIDES_STATUS_BAR:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).setTopAppHidesStatusBar(msg.arg1 != 0);
+ }
+ break;
+ }
+ }
+ }
+
+ // Need this class since CommandQueue already extends IStatusBar.Stub, so CommandQueueStart
+ // is needed so it can extend SystemUI.
+ public static class CommandQueueStart extends SystemUI {
+ @Override
+ public void start() {
+ putComponent(CommandQueue.class, new CommandQueue());
+ }
+ }
+}
+
diff --git a/com/android/systemui/statusbar/CrossFadeHelper.java b/com/android/systemui/statusbar/CrossFadeHelper.java
new file mode 100644
index 0000000..e8f0925
--- /dev/null
+++ b/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2016 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.Interpolators;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * A helper to fade views in and out.
+ */
+public class CrossFadeHelper {
+ public static final long ANIMATION_DURATION_LENGTH = 210;
+
+ public static void fadeOut(final View view, final Runnable endRunnable) {
+ view.animate().cancel();
+ view.animate()
+ .alpha(0f)
+ .setDuration(ANIMATION_DURATION_LENGTH)
+ .setInterpolator(Interpolators.ALPHA_OUT)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
+ view.setVisibility(View.INVISIBLE);
+ }
+ });
+ if (view.hasOverlappingRendering()) {
+ view.animate().withLayer();
+ }
+ }
+
+ public static void fadeOut(View view, float fadeOutAmount) {
+ fadeOut(view, fadeOutAmount, true /* remap */);
+ }
+
+ /**
+ * Fade out a view by a given progress amount
+ * @param view the view to fade out
+ * @param fadeOutAmount how much the view is faded out. 0 means not at all and 1 means fully
+ * faded out
+ * @param remap whether the fade amount should be remapped to the shorter duration
+ * {@link #ANIMATION_DURATION_LENGTH} from the normal fade duration
+ * {@link StackStateAnimator#ANIMATION_DURATION_STANDARD} in order to have a faster fading.
+ *
+ * @see #fadeIn(View, float, boolean)
+ */
+ public static void fadeOut(View view, float fadeOutAmount, boolean remap) {
+ view.animate().cancel();
+ if (fadeOutAmount == 1.0f) {
+ view.setVisibility(View.INVISIBLE);
+ } else if (view.getVisibility() == View.INVISIBLE) {
+ view.setVisibility(View.VISIBLE);
+ }
+ if (remap) {
+ fadeOutAmount = mapToFadeDuration(fadeOutAmount);
+ }
+ float alpha = Interpolators.ALPHA_OUT.getInterpolation(1.0f - fadeOutAmount);
+ view.setAlpha(alpha);
+ updateLayerType(view, alpha);
+ }
+
+ private static float mapToFadeDuration(float fadeOutAmount) {
+ // Assuming a linear interpolator, we can easily map it to our new duration
+ float endPoint = (float) ANIMATION_DURATION_LENGTH
+ / (float) StackStateAnimator.ANIMATION_DURATION_STANDARD;
+ return Math.min(fadeOutAmount / endPoint, 1.0f);
+ }
+
+ private static void updateLayerType(View view, float alpha) {
+ if (view.hasOverlappingRendering() && alpha > 0.0f && alpha < 1.0f) {
+ view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ } else if (view.getLayerType() == View.LAYER_TYPE_HARDWARE) {
+ view.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ }
+
+ public static void fadeIn(final View view) {
+ view.animate().cancel();
+ if (view.getVisibility() == View.INVISIBLE) {
+ view.setAlpha(0.0f);
+ view.setVisibility(View.VISIBLE);
+ }
+ view.animate()
+ .alpha(1f)
+ .setDuration(ANIMATION_DURATION_LENGTH)
+ .setInterpolator(Interpolators.ALPHA_IN)
+ .withEndAction(null);
+ if (view.hasOverlappingRendering()) {
+ view.animate().withLayer();
+ }
+ }
+
+ public static void fadeIn(View view, float fadeInAmount) {
+ fadeIn(view, fadeInAmount, true /* remap */);
+ }
+
+ /**
+ * Fade in a view by a given progress amount
+ * @param view the view to fade in
+ * @param fadeInAmount how much the view is faded in. 0 means not at all and 1 means fully
+ * faded in.
+ * @param remap whether the fade amount should be remapped to the shorter duration
+ * {@link #ANIMATION_DURATION_LENGTH} from the normal fade duration
+ * {@link StackStateAnimator#ANIMATION_DURATION_STANDARD} in order to have a faster fading.
+ *
+ * @see #fadeOut(View, float, boolean)
+ */
+ public static void fadeIn(View view, float fadeInAmount, boolean remap) {
+ view.animate().cancel();
+ if (view.getVisibility() == View.INVISIBLE) {
+ view.setVisibility(View.VISIBLE);
+ }
+ if (remap) {
+ fadeInAmount = mapToFadeDuration(fadeInAmount);
+ }
+ float alpha = Interpolators.ALPHA_IN.getInterpolation(fadeInAmount);
+ view.setAlpha(alpha);
+ updateLayerType(view, alpha);
+ }
+}
diff --git a/com/android/systemui/statusbar/DismissView.java b/com/android/systemui/statusbar/DismissView.java
new file mode 100644
index 0000000..d7c6443
--- /dev/null
+++ b/com/android/systemui/statusbar/DismissView.java
@@ -0,0 +1,93 @@
+/*
+ * 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.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.StackScrollState;
+
+public class DismissView extends StackScrollerDecorView {
+ private final int mClearAllTopPadding;
+ private DismissViewButton mDismissButton;
+
+ public DismissView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mClearAllTopPadding = context.getResources().getDimensionPixelSize(
+ R.dimen.clear_all_padding_top);
+ }
+
+ @Override
+ protected View findContentView() {
+ return findViewById(R.id.dismiss_text);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mDismissButton = (DismissViewButton) findContentView();
+ }
+
+ public void setTextColor(@ColorInt int color) {
+ mDismissButton.setTextColor(color);
+ }
+
+ 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();
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mDismissButton.setText(R.string.clear_all_notifications_text);
+ mDismissButton.setContentDescription(
+ mContext.getString(R.string.accessibility_clear_all));
+ }
+
+ public boolean isButtonVisible() {
+ return mDismissButton.getAlpha() != 0.0f;
+ }
+
+ @Override
+ public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+ return new DismissViewState();
+ }
+
+ public class DismissViewState extends ExpandableViewState {
+ @Override
+ public void applyToView(View view) {
+ super.applyToView(view);
+ if (view instanceof DismissView) {
+ DismissView dismissView = (DismissView) view;
+ boolean visible = this.clipTopAmount < mClearAllTopPadding;
+ dismissView.performVisibilityAnimation(visible && !dismissView.willBeGone());
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/DismissViewButton.java b/com/android/systemui/statusbar/DismissViewButton.java
new file mode 100644
index 0000000..b608d67
--- /dev/null
+++ b/com/android/systemui/statusbar/DismissViewButton.java
@@ -0,0 +1,63 @@
+/*
+ * 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.ViewGroup;
+
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+public class DismissViewButton extends AlphaOptimizedButton {
+
+ 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);
+ }
+
+ /**
+ * 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/com/android/systemui/statusbar/DragDownHelper.java b/com/android/systemui/statusbar/DragDownHelper.java
new file mode 100644
index 0000000..1bbf848
--- /dev/null
+++ b/com/android/systemui/statusbar/DragDownHelper.java
@@ -0,0 +1,258 @@
+/*
+ * 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.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.ExpandHelper;
+import com.android.systemui.Gefingerpoken;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+
+/**
+ * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
+ * the notification where the drag started.
+ */
+public class DragDownHelper implements Gefingerpoken {
+
+ private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f;
+ private static final float RUBBERBAND_FACTOR_STATIC = 0.15f;
+
+ private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375;
+
+ private int mMinDragDistance;
+ private ExpandHelper.Callback mCallback;
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+ private boolean mDraggingDown;
+ private float mTouchSlop;
+ private DragDownCallback mDragDownCallback;
+ private View mHost;
+ private final int[] mTemp2 = new int[2];
+ private boolean mDraggedFarEnough;
+ private ExpandableView mStartingChild;
+ private float mLastHeight;
+ private FalsingManager mFalsingManager;
+
+ public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
+ DragDownCallback dragDownCallback) {
+ mMinDragDistance = context.getResources().getDimensionPixelSize(
+ R.dimen.keyguard_drag_down_min_distance);
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mCallback = callback;
+ mDragDownCallback = dragDownCallback;
+ mHost = host;
+ mFalsingManager = FalsingManager.getInstance(context);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ final float x = event.getX();
+ final float y = event.getY();
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mDraggedFarEnough = false;
+ mDraggingDown = false;
+ mStartingChild = null;
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final float h = y - mInitialTouchY;
+ if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
+ mFalsingManager.onNotificatonStartDraggingDown();
+ mDraggingDown = true;
+ captureStartingChild(mInitialTouchX, mInitialTouchY);
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ mDragDownCallback.onTouchSlopExceeded();
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!mDraggingDown) {
+ return false;
+ }
+ final float x = event.getX();
+ final float y = event.getY();
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_MOVE:
+ mLastHeight = y - mInitialTouchY;
+ captureStartingChild(mInitialTouchX, mInitialTouchY);
+ if (mStartingChild != null) {
+ handleExpansion(mLastHeight, mStartingChild);
+ } else {
+ mDragDownCallback.setEmptyDragAmount(mLastHeight);
+ }
+ if (mLastHeight > mMinDragDistance) {
+ if (!mDraggedFarEnough) {
+ mDraggedFarEnough = true;
+ mDragDownCallback.onCrossedThreshold(true);
+ }
+ } else {
+ if (mDraggedFarEnough) {
+ mDraggedFarEnough = false;
+ mDragDownCallback.onCrossedThreshold(false);
+ }
+ }
+ return true;
+ case MotionEvent.ACTION_UP:
+ if (!isFalseTouch() && mDragDownCallback.onDraggedDown(mStartingChild,
+ (int) (y - mInitialTouchY))) {
+ if (mStartingChild == null) {
+ mDragDownCallback.setEmptyDragAmount(0f);
+ } else {
+ mCallback.setUserLockedChild(mStartingChild, false);
+ mStartingChild = null;
+ }
+ mDraggingDown = false;
+ } else {
+ stopDragging();
+ return false;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ stopDragging();
+ return false;
+ }
+ return false;
+ }
+
+ private boolean isFalseTouch() {
+ if (!mDragDownCallback.isFalsingCheckNeeded()) {
+ return false;
+ }
+ return mFalsingManager.isFalseTouch() || !mDraggedFarEnough;
+ }
+
+ private void captureStartingChild(float x, float y) {
+ if (mStartingChild == null) {
+ mStartingChild = findView(x, y);
+ if (mStartingChild != null) {
+ mCallback.setUserLockedChild(mStartingChild, true);
+ }
+ }
+ }
+
+ private void handleExpansion(float heightDelta, ExpandableView child) {
+ if (heightDelta < 0) {
+ heightDelta = 0;
+ }
+ boolean expandable = child.isContentExpandable();
+ float rubberbandFactor = expandable
+ ? RUBBERBAND_FACTOR_EXPANDABLE
+ : RUBBERBAND_FACTOR_STATIC;
+ float rubberband = heightDelta * rubberbandFactor;
+ if (expandable
+ && (rubberband + child.getCollapsedHeight()) > child.getMaxContentHeight()) {
+ float overshoot =
+ (rubberband + child.getCollapsedHeight()) - child.getMaxContentHeight();
+ overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
+ rubberband -= overshoot;
+ }
+ child.setActualHeight((int) (child.getCollapsedHeight() + rubberband));
+ }
+
+ private void cancelExpansion(final ExpandableView child) {
+ if (child.getActualHeight() == child.getCollapsedHeight()) {
+ mCallback.setUserLockedChild(child, false);
+ return;
+ }
+ ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
+ child.getActualHeight(), child.getCollapsedHeight());
+ anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCallback.setUserLockedChild(child, false);
+ }
+ });
+ anim.start();
+ }
+
+ private void cancelExpansion() {
+ ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0);
+ anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
+ anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue());
+ }
+ });
+ anim.start();
+ }
+
+ private void stopDragging() {
+ mFalsingManager.onNotificatonStopDraggingDown();
+ if (mStartingChild != null) {
+ cancelExpansion(mStartingChild);
+ mStartingChild = null;
+ } else {
+ cancelExpansion();
+ }
+ mDraggingDown = false;
+ mDragDownCallback.onDragDownReset();
+ }
+
+ private ExpandableView findView(float x, float y) {
+ mHost.getLocationOnScreen(mTemp2);
+ x += mTemp2[0];
+ y += mTemp2[1];
+ return mCallback.getChildAtRawPosition(x, y);
+ }
+
+ public boolean isDraggingDown() {
+ return mDraggingDown;
+ }
+
+ public interface DragDownCallback {
+
+ /**
+ * @return true if the interaction is accepted, false if it should be cancelled
+ */
+ boolean onDraggedDown(View startingChild, int dragLengthY);
+ void onDragDownReset();
+
+ /**
+ * The user has dragged either above or below the threshold
+ * @param above whether he dragged above it
+ */
+ void onCrossedThreshold(boolean above);
+ void onTouchSlopExceeded();
+ void setEmptyDragAmount(float amount);
+ boolean isFalsingCheckNeeded();
+ }
+}
diff --git a/com/android/systemui/statusbar/EmptyShadeView.java b/com/android/systemui/statusbar/EmptyShadeView.java
new file mode 100644
index 0000000..58adde2
--- /dev/null
+++ b/com/android/systemui/statusbar/EmptyShadeView.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.statusbar;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.StackScrollState;
+
+public class EmptyShadeView extends StackScrollerDecorView {
+
+ private TextView mEmptyText;
+
+ public EmptyShadeView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mEmptyText.setText(R.string.empty_shade_text);
+ }
+
+ @Override
+ protected View findContentView() {
+ return findViewById(R.id.no_notifications);
+ }
+
+ public void setTextColor(@ColorInt int color) {
+ mEmptyText.setTextColor(color);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mEmptyText = (TextView) findContentView();
+ }
+
+ @Override
+ public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+ return new EmptyShadeViewState();
+ }
+
+ public class EmptyShadeViewState extends ExpandableViewState {
+ @Override
+ public void applyToView(View view) {
+ super.applyToView(view);
+ if (view instanceof EmptyShadeView) {
+ EmptyShadeView emptyShadeView = (EmptyShadeView) view;
+ boolean visible = this.clipTopAmount <= mEmptyText.getPaddingTop() * 0.6f;
+ emptyShadeView.performVisibilityAnimation(
+ visible && !emptyShadeView.willBeGone());
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/ExpandableNotificationRow.java b/com/android/systemui/statusbar/ExpandableNotificationRow.java
new file mode 100644
index 0000000..7067bc1
--- /dev/null
+++ b/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -0,0 +1,2334 @@
+/*
+ * 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;
+
+import static com.android.systemui.statusbar.notification.NotificationInflater.InflationCallback;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Configuration;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Property;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Chronometer;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.internal.widget.CachingIconView;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
+import com.android.systemui.statusbar.NotificationGuts.GutsContent;
+import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
+import com.android.systemui.statusbar.notification.HybridNotificationView;
+import com.android.systemui.statusbar.notification.NotificationInflater;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackScrollState;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BooleanSupplier;
+
+public class ExpandableNotificationRow extends ActivatableNotificationView
+ implements PluginListener<NotificationMenuRowPlugin> {
+
+ private static final int DEFAULT_DIVIDER_ALPHA = 0x29;
+ private static final int COLORED_DIVIDER_ALPHA = 0x7B;
+ private static final int MENU_VIEW_INDEX = 0;
+
+ public interface LayoutListener {
+ public void onLayout();
+ }
+
+ private LayoutListener mLayoutListener;
+ private boolean mDark;
+ private boolean mLowPriorityStateUpdated;
+ private final NotificationInflater mNotificationInflater;
+ private int mIconTransformContentShift;
+ private int mIconTransformContentShiftNoIcon;
+ private int mNotificationMinHeightLegacy;
+ private int mMaxHeadsUpHeightLegacy;
+ private int mMaxHeadsUpHeight;
+ private int mMaxHeadsUpHeightIncreased;
+ private int mNotificationMinHeight;
+ private int mNotificationMinHeightLarge;
+ private int mNotificationMaxHeight;
+ private int mNotificationAmbientHeight;
+ private int mIncreasedPaddingBetweenElements;
+
+ /** Does this row contain layouts that can adapt to row expansion */
+ private boolean mExpandable;
+ /** Has the user actively changed the expansion state of this row */
+ private boolean mHasUserChangedExpansion;
+ /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
+ private boolean mUserExpanded;
+
+ /**
+ * Has this notification been expanded while it was pinned
+ */
+ private boolean mExpandedWhenPinned;
+ /** Is the user touching this row */
+ private boolean mUserLocked;
+ /** Are we showing the "public" version */
+ private boolean mShowingPublic;
+ private boolean mSensitive;
+ private boolean mSensitiveHiddenInGeneral;
+ private boolean mShowingPublicInitialized;
+ private boolean mHideSensitiveForIntrinsicHeight;
+
+ /**
+ * Is this notification expanded by the system. The expansion state can be overridden by the
+ * user expansion.
+ */
+ private boolean mIsSystemExpanded;
+
+ /**
+ * Whether the notification is on the keyguard and the expansion is disabled.
+ */
+ private boolean mOnKeyguard;
+
+ private Animator mTranslateAnim;
+ private ArrayList<View> mTranslateableViews;
+ private NotificationContentView mPublicLayout;
+ private NotificationContentView mPrivateLayout;
+ private NotificationContentView[] mLayouts;
+ private int mMaxExpandHeight;
+ private int mHeadsUpHeight;
+ private int mNotificationColor;
+ private ExpansionLogger mLogger;
+ private String mLoggingKey;
+ private NotificationGuts mGuts;
+ private NotificationData.Entry mEntry;
+ private StatusBarNotification mStatusBarNotification;
+ private String mAppName;
+ private boolean mIsHeadsUp;
+ private boolean mLastChronometerRunning = true;
+ private ViewStub mChildrenContainerStub;
+ private NotificationGroupManager mGroupManager;
+ private boolean mChildrenExpanded;
+ private boolean mIsSummaryWithChildren;
+ private NotificationChildrenContainer mChildrenContainer;
+ private NotificationMenuRowPlugin mMenuRow;
+ private ViewStub mGutsStub;
+ private boolean mIsSystemChildExpanded;
+ private boolean mIsPinned;
+ private FalsingManager mFalsingManager;
+ private AboveShelfChangedListener mAboveShelfChangedListener;
+ private HeadsUpManager mHeadsUpManager;
+
+ private boolean mJustClicked;
+ private boolean mIconAnimationRunning;
+ private boolean mShowNoBackground;
+ private ExpandableNotificationRow mNotificationParent;
+ private OnExpandClickListener mOnExpandClickListener;
+ private boolean mGroupExpansionChanging;
+
+ /**
+ * A supplier that returns true if keyguard is secure.
+ */
+ private BooleanSupplier mSecureStateProvider;
+
+ /**
+ * Whether or not a notification that is not part of a group of notifications can be manually
+ * expanded by the user.
+ */
+ private boolean mEnableNonGroupedNotificationExpand;
+
+ /**
+ * Whether or not to update the background of the header of the notification when its expanded.
+ * If {@code true}, the header background will disappear when expanded.
+ */
+ private boolean mShowGroupBackgroundWhenExpanded;
+
+ private OnClickListener mExpandClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!mShowingPublic && (!mIsLowPriority || isExpanded())
+ && mGroupManager.isSummaryOfGroup(mStatusBarNotification)) {
+ mGroupExpansionChanging = true;
+ final boolean wasExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification);
+ boolean nowExpanded = mGroupManager.toggleGroupExpansion(mStatusBarNotification);
+ mOnExpandClickListener.onExpandClicked(mEntry, nowExpanded);
+ MetricsLogger.action(mContext, MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER,
+ nowExpanded);
+ onExpansionChanged(true /* userAction */, wasExpanded);
+ } else if (mEnableNonGroupedNotificationExpand) {
+ if (v.isAccessibilityFocused()) {
+ mPrivateLayout.setFocusOnVisibilityChange();
+ }
+ boolean nowExpanded;
+ if (isPinned()) {
+ nowExpanded = !mExpandedWhenPinned;
+ mExpandedWhenPinned = nowExpanded;
+ } else {
+ nowExpanded = !isExpanded();
+ setUserExpanded(nowExpanded);
+ }
+ notifyHeightChanged(true);
+ mOnExpandClickListener.onExpandClicked(mEntry, nowExpanded);
+ MetricsLogger.action(mContext, MetricsEvent.ACTION_NOTIFICATION_EXPANDER,
+ nowExpanded);
+ }
+ }
+ };
+ private boolean mForceUnlocked;
+ private boolean mDismissed;
+ private boolean mKeepInParent;
+ private boolean mRemoved;
+ private static final Property<ExpandableNotificationRow, Float> TRANSLATE_CONTENT =
+ new FloatProperty<ExpandableNotificationRow>("translate") {
+ @Override
+ public void setValue(ExpandableNotificationRow object, float value) {
+ object.setTranslation(value);
+ }
+
+ @Override
+ public Float get(ExpandableNotificationRow object) {
+ return object.getTranslation();
+ }
+ };
+ private OnClickListener mOnClickListener;
+ private boolean mHeadsupDisappearRunning;
+ private View mChildAfterViewWhenDismissed;
+ private View mGroupParentWhenDismissed;
+ private boolean mRefocusOnDismiss;
+ private float mContentTransformationAmount;
+ private boolean mIconsVisible = true;
+ private boolean mAboveShelf;
+ private boolean mShowAmbient;
+ private boolean mIsLastChild;
+ private Runnable mOnDismissRunnable;
+ private boolean mIsLowPriority;
+ private boolean mIsColorized;
+ private boolean mUseIncreasedCollapsedHeight;
+ private boolean mUseIncreasedHeadsUpHeight;
+ private float mTranslationWhenRemoved;
+ private boolean mWasChildInGroupWhenRemoved;
+ private int mNotificationColorAmbient;
+
+ @Override
+ public boolean isGroupExpansionChanging() {
+ if (isChildInGroup()) {
+ return mNotificationParent.isGroupExpansionChanging();
+ }
+ return mGroupExpansionChanging;
+ }
+
+ public void setGroupExpansionChanging(boolean changing) {
+ mGroupExpansionChanging = changing;
+ }
+
+ @Override
+ public void setActualHeightAnimating(boolean animating) {
+ if (mPrivateLayout != null) {
+ mPrivateLayout.setContentHeightAnimating(animating);
+ }
+ }
+
+ public NotificationContentView getPrivateLayout() {
+ return mPrivateLayout;
+ }
+
+ public NotificationContentView getPublicLayout() {
+ return mPublicLayout;
+ }
+
+ public void setIconAnimationRunning(boolean running) {
+ for (NotificationContentView l : mLayouts) {
+ setIconAnimationRunning(running, l);
+ }
+ if (mIsSummaryWithChildren) {
+ setIconAnimationRunningForChild(running, mChildrenContainer.getHeaderView());
+ setIconAnimationRunningForChild(running, mChildrenContainer.getLowPriorityHeaderView());
+ List<ExpandableNotificationRow> notificationChildren =
+ mChildrenContainer.getNotificationChildren();
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow child = notificationChildren.get(i);
+ child.setIconAnimationRunning(running);
+ }
+ }
+ mIconAnimationRunning = running;
+ }
+
+ private void setIconAnimationRunning(boolean running, NotificationContentView layout) {
+ if (layout != null) {
+ View contractedChild = layout.getContractedChild();
+ View expandedChild = layout.getExpandedChild();
+ View headsUpChild = layout.getHeadsUpChild();
+ setIconAnimationRunningForChild(running, contractedChild);
+ setIconAnimationRunningForChild(running, expandedChild);
+ setIconAnimationRunningForChild(running, headsUpChild);
+ }
+ }
+
+ private void setIconAnimationRunningForChild(boolean running, View child) {
+ if (child != null) {
+ ImageView icon = (ImageView) child.findViewById(com.android.internal.R.id.icon);
+ setIconRunning(icon, running);
+ ImageView rightIcon = (ImageView) child.findViewById(
+ com.android.internal.R.id.right_icon);
+ setIconRunning(rightIcon, running);
+ }
+ }
+
+ private void setIconRunning(ImageView imageView, boolean running) {
+ if (imageView != null) {
+ Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AnimationDrawable) {
+ AnimationDrawable animationDrawable = (AnimationDrawable) drawable;
+ if (running) {
+ animationDrawable.start();
+ } else {
+ animationDrawable.stop();
+ }
+ } else if (drawable instanceof AnimatedVectorDrawable) {
+ AnimatedVectorDrawable animationDrawable = (AnimatedVectorDrawable) drawable;
+ if (running) {
+ animationDrawable.start();
+ } else {
+ animationDrawable.stop();
+ }
+ }
+ }
+ }
+
+ public void updateNotification(NotificationData.Entry entry) {
+ mEntry = entry;
+ mStatusBarNotification = entry.notification;
+ mNotificationInflater.inflateNotificationViews();
+ }
+
+ public void onNotificationUpdated() {
+ for (NotificationContentView l : mLayouts) {
+ l.onNotificationUpdated(mEntry);
+ }
+ mIsColorized = mStatusBarNotification.getNotification().isColorized();
+ mShowingPublicInitialized = false;
+ updateNotificationColor();
+ if (mMenuRow != null) {
+ mMenuRow.onNotificationUpdated(mStatusBarNotification);
+ }
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.recreateNotificationHeader(mExpandClickListener);
+ mChildrenContainer.onNotificationUpdated();
+ }
+ if (mIconAnimationRunning) {
+ setIconAnimationRunning(true);
+ }
+ if (mNotificationParent != null) {
+ mNotificationParent.updateChildrenHeaderAppearance();
+ }
+ onChildrenCountChanged();
+ // The public layouts expand button is always visible
+ mPublicLayout.updateExpandButtons(true);
+ updateLimits();
+ updateIconVisibilities();
+ updateShelfIconColor();
+ }
+
+ @VisibleForTesting
+ void updateShelfIconColor() {
+ StatusBarIconView expandedIcon = mEntry.expandedIcon;
+ boolean isPreL = Boolean.TRUE.equals(expandedIcon.getTag(R.id.icon_is_pre_L));
+ boolean colorize = !isPreL || NotificationUtils.isGrayscale(expandedIcon,
+ NotificationColorUtil.getInstance(mContext));
+ int color = StatusBarIconView.NO_COLOR;
+ if (colorize) {
+ NotificationHeaderView header = getVisibleNotificationHeader();
+ if (header != null) {
+ color = header.getOriginalIconColor();
+ } else {
+ color = mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded(),
+ getBackgroundColorWithoutTint());
+ }
+ }
+ expandedIcon.setStaticDrawableColor(color);
+ }
+
+ public void setAboveShelfChangedListener(AboveShelfChangedListener aboveShelfChangedListener) {
+ mAboveShelfChangedListener = aboveShelfChangedListener;
+ }
+
+ /**
+ * Sets a supplier that can determine whether the keyguard is secure or not.
+ * @param secureStateProvider A function that returns true if keyguard is secure.
+ */
+ public void setSecureStateProvider(BooleanSupplier secureStateProvider) {
+ mSecureStateProvider = secureStateProvider;
+ }
+
+ @Override
+ public boolean isDimmable() {
+ if (!getShowingLayout().isDimmable()) {
+ return false;
+ }
+ return super.isDimmable();
+ }
+
+ private void updateLimits() {
+ for (NotificationContentView l : mLayouts) {
+ updateLimitsForView(l);
+ }
+ }
+
+ private void updateLimitsForView(NotificationContentView layout) {
+ boolean customView = layout.getContractedChild().getId()
+ != com.android.internal.R.id.status_bar_latest_event_content;
+ boolean beforeN = mEntry.targetSdk < Build.VERSION_CODES.N;
+ int minHeight;
+ if (customView && beforeN && !mIsSummaryWithChildren) {
+ minHeight = mNotificationMinHeightLegacy;
+ } else if (mUseIncreasedCollapsedHeight && layout == mPrivateLayout) {
+ minHeight = mNotificationMinHeightLarge;
+ } else {
+ minHeight = mNotificationMinHeight;
+ }
+ boolean headsUpCustom = layout.getHeadsUpChild() != null &&
+ layout.getHeadsUpChild().getId()
+ != com.android.internal.R.id.status_bar_latest_event_content;
+ int headsUpheight;
+ if (headsUpCustom && beforeN) {
+ headsUpheight = mMaxHeadsUpHeightLegacy;
+ } else if (mUseIncreasedHeadsUpHeight && layout == mPrivateLayout) {
+ headsUpheight = mMaxHeadsUpHeightIncreased;
+ } else {
+ headsUpheight = mMaxHeadsUpHeight;
+ }
+ layout.setHeights(minHeight, headsUpheight, mNotificationMaxHeight,
+ mNotificationAmbientHeight);
+ }
+
+ public StatusBarNotification getStatusBarNotification() {
+ return mStatusBarNotification;
+ }
+
+ public NotificationData.Entry getEntry() {
+ return mEntry;
+ }
+
+ public boolean isHeadsUp() {
+ return mIsHeadsUp;
+ }
+
+ public void setHeadsUp(boolean isHeadsUp) {
+ boolean wasAboveShelf = isAboveShelf();
+ int intrinsicBefore = getIntrinsicHeight();
+ mIsHeadsUp = isHeadsUp;
+ mPrivateLayout.setHeadsUp(isHeadsUp);
+ if (mIsSummaryWithChildren) {
+ // The overflow might change since we allow more lines as HUN.
+ mChildrenContainer.updateGroupOverflow();
+ }
+ if (intrinsicBefore != getIntrinsicHeight()) {
+ notifyHeightChanged(false /* needsAnimation */);
+ }
+ if (isHeadsUp) {
+ setAboveShelf(true);
+ } else if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+ }
+ }
+
+ public void setGroupManager(NotificationGroupManager groupManager) {
+ mGroupManager = groupManager;
+ mPrivateLayout.setGroupManager(groupManager);
+ }
+
+ public void setRemoteInputController(RemoteInputController r) {
+ mPrivateLayout.setRemoteInputController(r);
+ }
+
+ public void setAppName(String appName) {
+ mAppName = appName;
+ if (mMenuRow != null && mMenuRow.getMenuView() != null) {
+ mMenuRow.setAppName(mAppName);
+ }
+ }
+
+ public void addChildNotification(ExpandableNotificationRow row) {
+ addChildNotification(row, -1);
+ }
+
+ /**
+ * Add a child notification to this view.
+ *
+ * @param row the row to add
+ * @param childIndex the index to add it at, if -1 it will be added at the end
+ */
+ public void addChildNotification(ExpandableNotificationRow row, int childIndex) {
+ if (mChildrenContainer == null) {
+ mChildrenContainerStub.inflate();
+ }
+ mChildrenContainer.addNotification(row, childIndex);
+ onChildrenCountChanged();
+ row.setIsChildInGroup(true, this);
+ }
+
+ public void removeChildNotification(ExpandableNotificationRow row) {
+ if (mChildrenContainer != null) {
+ mChildrenContainer.removeNotification(row);
+ }
+ onChildrenCountChanged();
+ row.setIsChildInGroup(false, null);
+ }
+
+ @Override
+ public boolean isChildInGroup() {
+ return mNotificationParent != null;
+ }
+
+ public ExpandableNotificationRow getNotificationParent() {
+ return mNotificationParent;
+ }
+
+ /**
+ * @param isChildInGroup Is this notification now in a group
+ * @param parent the new parent notification
+ */
+ public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) {;
+ boolean childInGroup = StatusBar.ENABLE_CHILD_NOTIFICATIONS && isChildInGroup;
+ mNotificationParent = childInGroup ? parent : null;
+ mPrivateLayout.setIsChildInGroup(childInGroup);
+ mNotificationInflater.setIsChildInGroup(childInGroup);
+ resetBackgroundAlpha();
+ updateBackgroundForGroupState();
+ updateClickAndFocus();
+ if (mNotificationParent != null) {
+ setOverrideTintColor(NO_COLOR, 0.0f);
+ mNotificationParent.updateBackgroundForGroupState();
+ }
+ updateIconVisibilities();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() != MotionEvent.ACTION_DOWN
+ || !isChildInGroup() || isGroupExpanded()) {
+ return super.onTouchEvent(event);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected boolean handleSlideBack() {
+ if (mMenuRow != null && mMenuRow.isMenuVisible()) {
+ animateTranslateNotification(0 /* targetLeft */);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean shouldHideBackground() {
+ return super.shouldHideBackground() || mShowNoBackground;
+ }
+
+ @Override
+ public boolean isSummaryWithChildren() {
+ return mIsSummaryWithChildren;
+ }
+
+ @Override
+ public boolean areChildrenExpanded() {
+ return mChildrenExpanded;
+ }
+
+ public List<ExpandableNotificationRow> getNotificationChildren() {
+ return mChildrenContainer == null ? null : mChildrenContainer.getNotificationChildren();
+ }
+
+ public int getNumberOfNotificationChildren() {
+ if (mChildrenContainer == null) {
+ return 0;
+ }
+ return mChildrenContainer.getNotificationChildren().size();
+ }
+
+ /**
+ * Apply the order given in the list to the children.
+ *
+ * @param childOrder the new list order
+ * @param visualStabilityManager
+ * @param callback the callback to invoked in case it is not allowed
+ * @return whether the list order has changed
+ */
+ public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
+ VisualStabilityManager visualStabilityManager,
+ VisualStabilityManager.Callback callback) {
+ return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder,
+ visualStabilityManager, callback);
+ }
+
+ public void getChildrenStates(StackScrollState resultState) {
+ if (mIsSummaryWithChildren) {
+ ExpandableViewState parentState = resultState.getViewStateForView(this);
+ mChildrenContainer.getState(resultState, parentState);
+ }
+ }
+
+ public void applyChildrenState(StackScrollState state) {
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.applyState(state);
+ }
+ }
+
+ public void prepareExpansionChanged(StackScrollState state) {
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.prepareExpansionChanged(state);
+ }
+ }
+
+ public void startChildAnimation(StackScrollState finalState, AnimationProperties properties) {
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.startAnimationToState(finalState, properties);
+ }
+ }
+
+ public ExpandableNotificationRow getViewAtPosition(float y) {
+ if (!mIsSummaryWithChildren || !mChildrenExpanded) {
+ return this;
+ } else {
+ ExpandableNotificationRow view = mChildrenContainer.getViewAtPosition(y);
+ return view == null ? this : view;
+ }
+ }
+
+ public NotificationGuts getGuts() {
+ return mGuts;
+ }
+
+ /**
+ * Set this notification to be pinned to the top if {@link #isHeadsUp()} is true. By doing this
+ * the notification will be rendered on top of the screen.
+ *
+ * @param pinned whether it is pinned
+ */
+ public void setPinned(boolean pinned) {
+ int intrinsicHeight = getIntrinsicHeight();
+ boolean wasAboveShelf = isAboveShelf();
+ mIsPinned = pinned;
+ if (intrinsicHeight != getIntrinsicHeight()) {
+ notifyHeightChanged(false /* needsAnimation */);
+ }
+ if (pinned) {
+ setIconAnimationRunning(true);
+ mExpandedWhenPinned = false;
+ } else if (mExpandedWhenPinned) {
+ setUserExpanded(true);
+ }
+ setChronometerRunning(mLastChronometerRunning);
+ if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+ }
+ }
+
+ public boolean isPinned() {
+ return mIsPinned;
+ }
+
+ @Override
+ public int getPinnedHeadsUpHeight() {
+ return getPinnedHeadsUpHeight(true /* atLeastMinHeight */);
+ }
+
+ /**
+ * @param atLeastMinHeight should the value returned be at least the minimum height.
+ * Used to avoid cyclic calls
+ * @return the height of the heads up notification when pinned
+ */
+ private int getPinnedHeadsUpHeight(boolean atLeastMinHeight) {
+ if (mIsSummaryWithChildren) {
+ return mChildrenContainer.getIntrinsicHeight();
+ }
+ if(mExpandedWhenPinned) {
+ return Math.max(getMaxExpandHeight(), mHeadsUpHeight);
+ } else if (atLeastMinHeight) {
+ return Math.max(getCollapsedHeight(), mHeadsUpHeight);
+ } else {
+ return mHeadsUpHeight;
+ }
+ }
+
+ /**
+ * Mark whether this notification was just clicked, i.e. the user has just clicked this
+ * notification in this frame.
+ */
+ public void setJustClicked(boolean justClicked) {
+ mJustClicked = justClicked;
+ }
+
+ /**
+ * @return true if this notification has been clicked in this frame, false otherwise
+ */
+ public boolean wasJustClicked() {
+ return mJustClicked;
+ }
+
+ public void setChronometerRunning(boolean running) {
+ mLastChronometerRunning = running;
+ setChronometerRunning(running, mPrivateLayout);
+ setChronometerRunning(running, mPublicLayout);
+ if (mChildrenContainer != null) {
+ List<ExpandableNotificationRow> notificationChildren =
+ mChildrenContainer.getNotificationChildren();
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow child = notificationChildren.get(i);
+ child.setChronometerRunning(running);
+ }
+ }
+ }
+
+ private void setChronometerRunning(boolean running, NotificationContentView layout) {
+ if (layout != null) {
+ running = running || isPinned();
+ View contractedChild = layout.getContractedChild();
+ View expandedChild = layout.getExpandedChild();
+ View headsUpChild = layout.getHeadsUpChild();
+ setChronometerRunningForChild(running, contractedChild);
+ setChronometerRunningForChild(running, expandedChild);
+ setChronometerRunningForChild(running, headsUpChild);
+ }
+ }
+
+ private void setChronometerRunningForChild(boolean running, View child) {
+ if (child != null) {
+ View chronometer = child.findViewById(com.android.internal.R.id.chronometer);
+ if (chronometer instanceof Chronometer) {
+ ((Chronometer) chronometer).setStarted(running);
+ }
+ }
+ }
+
+ public NotificationHeaderView getNotificationHeader() {
+ if (mIsSummaryWithChildren) {
+ return mChildrenContainer.getHeaderView();
+ }
+ return mPrivateLayout.getNotificationHeader();
+ }
+
+ /**
+ * @return the currently visible notification header. This can be different from
+ * {@link #getNotificationHeader()} in case it is a low-priority group.
+ */
+ public NotificationHeaderView getVisibleNotificationHeader() {
+ if (mIsSummaryWithChildren && !mShowingPublic) {
+ return mChildrenContainer.getVisibleHeader();
+ }
+ return getShowingLayout().getVisibleNotificationHeader();
+ }
+
+
+ /**
+ * @return the contracted notification header. This can be different from
+ * {@link #getNotificationHeader()} and also {@link #getVisibleNotificationHeader()} and only
+ * returns the contracted version.
+ */
+ public NotificationHeaderView getContractedNotificationHeader() {
+ if (mIsSummaryWithChildren) {
+ return mChildrenContainer.getHeaderView();
+ }
+ return mPrivateLayout.getContractedNotificationHeader();
+ }
+
+ public void setOnExpandClickListener(OnExpandClickListener onExpandClickListener) {
+ mOnExpandClickListener = onExpandClickListener;
+ }
+
+ @Override
+ public void setOnClickListener(@Nullable OnClickListener l) {
+ super.setOnClickListener(l);
+ mOnClickListener = l;
+ updateClickAndFocus();
+ }
+
+ private void updateClickAndFocus() {
+ boolean normalChild = !isChildInGroup() || isGroupExpanded();
+ boolean clickable = mOnClickListener != null && normalChild;
+ if (isFocusable() != normalChild) {
+ setFocusable(normalChild);
+ }
+ if (isClickable() != clickable) {
+ setClickable(clickable);
+ }
+ }
+
+ public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ mHeadsUpManager = headsUpManager;
+ }
+
+ public void setGutsView(MenuItem item) {
+ if (mGuts != null && item.getGutsView() instanceof GutsContent) {
+ ((GutsContent) item.getGutsView()).setGutsParent(mGuts);
+ mGuts.setGutsContent((GutsContent) item.getGutsView());
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ Dependency.get(PluginManager.class).addPluginListener(this,
+ NotificationMenuRowPlugin.class, false /* Allow multiple */);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ Dependency.get(PluginManager.class).removePluginListener(this);
+ }
+
+ @Override
+ public void onPluginConnected(NotificationMenuRowPlugin plugin, Context pluginContext) {
+ boolean existed = mMenuRow.getMenuView() != null;
+ if (existed) {
+ removeView(mMenuRow.getMenuView());
+ }
+ mMenuRow = plugin;
+ if (mMenuRow.useDefaultMenuItems()) {
+ ArrayList<MenuItem> items = new ArrayList<>();
+ items.add(NotificationMenuRow.createInfoItem(mContext));
+ items.add(NotificationMenuRow.createSnoozeItem(mContext));
+ mMenuRow.setMenuItems(items);
+ }
+ if (existed) {
+ createMenu();
+ }
+ }
+
+ @Override
+ public void onPluginDisconnected(NotificationMenuRowPlugin plugin) {
+ boolean existed = mMenuRow.getMenuView() != null;
+ mMenuRow = new NotificationMenuRow(mContext); // Back to default
+ if (existed) {
+ createMenu();
+ }
+ }
+
+ public NotificationMenuRowPlugin createMenu() {
+ if (mMenuRow.getMenuView() == null) {
+ mMenuRow.createMenu(this, mStatusBarNotification);
+ mMenuRow.setAppName(mAppName);
+ FrameLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ addView(mMenuRow.getMenuView(), MENU_VIEW_INDEX, lp);
+ }
+ return mMenuRow;
+ }
+
+ public NotificationMenuRowPlugin getProvider() {
+ return mMenuRow;
+ }
+
+ @Override
+ public void onDensityOrFontScaleChanged() {
+ super.onDensityOrFontScaleChanged();
+ initDimens();
+ initBackground();
+ // Let's update our childrencontainer. This is intentionally not guarded with
+ // mIsSummaryWithChildren since we might have had children but not anymore.
+ if (mChildrenContainer != null) {
+ mChildrenContainer.reInflateViews(mExpandClickListener, mEntry.notification);
+ }
+ if (mGuts != null) {
+ View oldGuts = mGuts;
+ int index = indexOfChild(oldGuts);
+ removeView(oldGuts);
+ mGuts = (NotificationGuts) LayoutInflater.from(mContext).inflate(
+ R.layout.notification_guts, this, false);
+ mGuts.setVisibility(oldGuts.getVisibility());
+ addView(mGuts, index);
+ }
+ View oldMenu = mMenuRow.getMenuView();
+ if (oldMenu != null) {
+ int menuIndex = indexOfChild(oldMenu);
+ removeView(oldMenu);
+ mMenuRow.createMenu(ExpandableNotificationRow.this, mStatusBarNotification);
+ mMenuRow.setAppName(mAppName);
+ addView(mMenuRow.getMenuView(), menuIndex);
+ }
+ for (NotificationContentView l : mLayouts) {
+ l.reInflateViews();
+ }
+ mNotificationInflater.onDensityOrFontScaleChanged();
+ onNotificationUpdated();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (mMenuRow.getMenuView() != null) {
+ mMenuRow.onConfigurationChanged();
+ }
+ }
+
+ public void setContentBackground(int customBackgroundColor, boolean animate,
+ NotificationContentView notificationContentView) {
+ if (getShowingLayout() == notificationContentView) {
+ setTintColor(customBackgroundColor, animate);
+ }
+ }
+
+ public void closeRemoteInput() {
+ for (NotificationContentView l : mLayouts) {
+ l.closeRemoteInput();
+ }
+ }
+
+ /**
+ * Set by how much the single line view should be indented.
+ */
+ public void setSingleLineWidthIndention(int indention) {
+ mPrivateLayout.setSingleLineWidthIndention(indention);
+ }
+
+ public int getNotificationColor() {
+ return mNotificationColor;
+ }
+
+ private void updateNotificationColor() {
+ mNotificationColor = NotificationColorUtil.resolveContrastColor(mContext,
+ getStatusBarNotification().getNotification().color,
+ getBackgroundColorWithoutTint());
+ mNotificationColorAmbient = NotificationColorUtil.resolveAmbientColor(mContext,
+ getStatusBarNotification().getNotification().color);
+ }
+
+ public HybridNotificationView getSingleLineView() {
+ return mPrivateLayout.getSingleLineView();
+ }
+
+ public HybridNotificationView getAmbientSingleLineView() {
+ return getShowingLayout().getAmbientSingleLineChild();
+ }
+
+ public boolean isOnKeyguard() {
+ return mOnKeyguard;
+ }
+
+ public void removeAllChildren() {
+ List<ExpandableNotificationRow> notificationChildren
+ = mChildrenContainer.getNotificationChildren();
+ ArrayList<ExpandableNotificationRow> clonedList = new ArrayList<>(notificationChildren);
+ for (int i = 0; i < clonedList.size(); i++) {
+ ExpandableNotificationRow row = clonedList.get(i);
+ if (row.keepInParent()) {
+ continue;
+ }
+ mChildrenContainer.removeNotification(row);
+ row.setIsChildInGroup(false, null);
+ }
+ onChildrenCountChanged();
+ }
+
+ public void setForceUnlocked(boolean forceUnlocked) {
+ mForceUnlocked = forceUnlocked;
+ if (mIsSummaryWithChildren) {
+ List<ExpandableNotificationRow> notificationChildren = getNotificationChildren();
+ for (ExpandableNotificationRow child : notificationChildren) {
+ child.setForceUnlocked(forceUnlocked);
+ }
+ }
+ }
+
+ public void setDismissed(boolean dismissed, boolean fromAccessibility) {
+ mDismissed = dismissed;
+ mGroupParentWhenDismissed = mNotificationParent;
+ mRefocusOnDismiss = fromAccessibility;
+ mChildAfterViewWhenDismissed = null;
+ if (isChildInGroup()) {
+ List<ExpandableNotificationRow> notificationChildren =
+ mNotificationParent.getNotificationChildren();
+ int i = notificationChildren.indexOf(this);
+ if (i != -1 && i < notificationChildren.size() - 1) {
+ mChildAfterViewWhenDismissed = notificationChildren.get(i + 1);
+ }
+ }
+ }
+
+ public boolean isDismissed() {
+ return mDismissed;
+ }
+
+ public boolean keepInParent() {
+ return mKeepInParent;
+ }
+
+ public void setKeepInParent(boolean keepInParent) {
+ mKeepInParent = keepInParent;
+ }
+
+ public boolean isRemoved() {
+ return mRemoved;
+ }
+
+ public void setRemoved() {
+ mRemoved = true;
+ mTranslationWhenRemoved = getTranslationY();
+ mWasChildInGroupWhenRemoved = isChildInGroup();
+ if (isChildInGroup()) {
+ mTranslationWhenRemoved += getNotificationParent().getTranslationY();
+ }
+ mPrivateLayout.setRemoved();
+ }
+
+ public boolean wasChildInGroupWhenRemoved() {
+ return mWasChildInGroupWhenRemoved;
+ }
+
+ public float getTranslationWhenRemoved() {
+ return mTranslationWhenRemoved;
+ }
+
+ public NotificationChildrenContainer getChildrenContainer() {
+ return mChildrenContainer;
+ }
+
+ public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
+ boolean wasAboveShelf = isAboveShelf();
+ mHeadsupDisappearRunning = headsUpAnimatingAway;
+ mPrivateLayout.setHeadsUpAnimatingAway(headsUpAnimatingAway);
+ if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+ }
+ }
+
+ /**
+ * @return if the view was just heads upped and is now animating away. During such a time the
+ * layout needs to be kept consistent
+ */
+ public boolean isHeadsUpAnimatingAway() {
+ return mHeadsupDisappearRunning;
+ }
+
+ public View getChildAfterViewWhenDismissed() {
+ return mChildAfterViewWhenDismissed;
+ }
+
+ public View getGroupParentWhenDismissed() {
+ return mGroupParentWhenDismissed;
+ }
+
+ public void performDismiss() {
+ if (mOnDismissRunnable != null) {
+ mOnDismissRunnable.run();
+ }
+ }
+
+ public void setOnDismissRunnable(Runnable onDismissRunnable) {
+ mOnDismissRunnable = onDismissRunnable;
+ }
+
+ public View getNotificationIcon() {
+ NotificationHeaderView notificationHeader = getVisibleNotificationHeader();
+ if (notificationHeader != null) {
+ return notificationHeader.getIcon();
+ }
+ return null;
+ }
+
+ /**
+ * @return whether the notification is currently showing a view with an icon.
+ */
+ public boolean isShowingIcon() {
+ if (areGutsExposed()) {
+ return false;
+ }
+ return getVisibleNotificationHeader() != null;
+ }
+
+ /**
+ * Set how much this notification is transformed into an icon.
+ *
+ * @param contentTransformationAmount A value from 0 to 1 indicating how much we are transformed
+ * to the content away
+ * @param isLastChild is this the last child in the list. If true, then the transformation is
+ * different since it's content fades out.
+ */
+ public void setContentTransformationAmount(float contentTransformationAmount,
+ boolean isLastChild) {
+ boolean changeTransformation = isLastChild != mIsLastChild;
+ changeTransformation |= mContentTransformationAmount != contentTransformationAmount;
+ mIsLastChild = isLastChild;
+ mContentTransformationAmount = contentTransformationAmount;
+ if (changeTransformation) {
+ updateContentTransformation();
+ }
+ }
+
+ /**
+ * Set the icons to be visible of this notification.
+ */
+ public void setIconsVisible(boolean iconsVisible) {
+ if (iconsVisible != mIconsVisible) {
+ mIconsVisible = iconsVisible;
+ updateIconVisibilities();
+ }
+ }
+
+ @Override
+ protected void onBelowSpeedBumpChanged() {
+ updateIconVisibilities();
+ }
+
+ private void updateContentTransformation() {
+ float contentAlpha;
+ float translationY = -mContentTransformationAmount * mIconTransformContentShift;
+ if (mIsLastChild) {
+ contentAlpha = 1.0f - mContentTransformationAmount;
+ contentAlpha = Math.min(contentAlpha / 0.5f, 1.0f);
+ contentAlpha = Interpolators.ALPHA_OUT.getInterpolation(contentAlpha);
+ translationY *= 0.4f;
+ } else {
+ contentAlpha = 1.0f;
+ }
+ for (NotificationContentView l : mLayouts) {
+ l.setAlpha(contentAlpha);
+ l.setTranslationY(translationY);
+ }
+ if (mChildrenContainer != null) {
+ mChildrenContainer.setAlpha(contentAlpha);
+ mChildrenContainer.setTranslationY(translationY);
+ // TODO: handle children fade out better
+ }
+ }
+
+ private void updateIconVisibilities() {
+ boolean visible = isChildInGroup()
+ || (isBelowSpeedBump() && !NotificationShelf.SHOW_AMBIENT_ICONS)
+ || mIconsVisible;
+ for (NotificationContentView l : mLayouts) {
+ l.setIconsVisible(visible);
+ }
+ if (mChildrenContainer != null) {
+ mChildrenContainer.setIconsVisible(visible);
+ }
+ }
+
+ /**
+ * Get the relative top padding of a view relative to this view. This recursively walks up the
+ * hierarchy and does the corresponding measuring.
+ *
+ * @param view the view to the the padding for. The requested view has to be a child of this
+ * notification.
+ * @return the toppadding
+ */
+ public int getRelativeTopPadding(View view) {
+ int topPadding = 0;
+ while (view.getParent() instanceof ViewGroup) {
+ topPadding += view.getTop();
+ view = (View) view.getParent();
+ if (view instanceof ExpandableNotificationRow) {
+ return topPadding;
+ }
+ }
+ return topPadding;
+ }
+
+ public float getContentTranslation() {
+ return mPrivateLayout.getTranslationY();
+ }
+
+ public void setIsLowPriority(boolean isLowPriority) {
+ mIsLowPriority = isLowPriority;
+ mPrivateLayout.setIsLowPriority(isLowPriority);
+ mNotificationInflater.setIsLowPriority(mIsLowPriority);
+ if (mChildrenContainer != null) {
+ mChildrenContainer.setIsLowPriority(isLowPriority);
+ }
+ }
+
+
+ public void setLowPriorityStateUpdated(boolean lowPriorityStateUpdated) {
+ mLowPriorityStateUpdated = lowPriorityStateUpdated;
+ }
+
+ public boolean hasLowPriorityStateUpdated() {
+ return mLowPriorityStateUpdated;
+ }
+
+ public boolean isLowPriority() {
+ return mIsLowPriority;
+ }
+
+ public void setUseIncreasedCollapsedHeight(boolean use) {
+ mUseIncreasedCollapsedHeight = use;
+ mNotificationInflater.setUsesIncreasedHeight(use);
+ }
+
+ public void setUseIncreasedHeadsUpHeight(boolean use) {
+ mUseIncreasedHeadsUpHeight = use;
+ mNotificationInflater.setUsesIncreasedHeadsUpHeight(use);
+ }
+
+ public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) {
+ mNotificationInflater.setRemoteViewClickHandler(remoteViewClickHandler);
+ }
+
+ public void setInflationCallback(InflationCallback callback) {
+ mNotificationInflater.setInflationCallback(callback);
+ }
+
+ public void setNeedsRedaction(boolean needsRedaction) {
+ mNotificationInflater.setRedactAmbient(needsRedaction);
+ }
+
+ @VisibleForTesting
+ public NotificationInflater getNotificationInflater() {
+ return mNotificationInflater;
+ }
+
+ public int getNotificationColorAmbient() {
+ return mNotificationColorAmbient;
+ }
+
+ public interface ExpansionLogger {
+ void logNotificationExpansion(String key, boolean userAction, boolean expanded);
+ }
+
+ public ExpandableNotificationRow(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mFalsingManager = FalsingManager.getInstance(context);
+ mNotificationInflater = new NotificationInflater(this);
+ mMenuRow = new NotificationMenuRow(mContext);
+ initDimens();
+ }
+
+ private void initDimens() {
+ mNotificationMinHeightLegacy = getFontScaledHeight(R.dimen.notification_min_height_legacy);
+ mNotificationMinHeight = getFontScaledHeight(R.dimen.notification_min_height);
+ mNotificationMinHeightLarge = getFontScaledHeight(
+ R.dimen.notification_min_height_increased);
+ mNotificationMaxHeight = getFontScaledHeight(R.dimen.notification_max_height);
+ mNotificationAmbientHeight = getFontScaledHeight(R.dimen.notification_ambient_height);
+ mMaxHeadsUpHeightLegacy = getFontScaledHeight(
+ R.dimen.notification_max_heads_up_height_legacy);
+ mMaxHeadsUpHeight = getFontScaledHeight(R.dimen.notification_max_heads_up_height);
+ mMaxHeadsUpHeightIncreased = getFontScaledHeight(
+ R.dimen.notification_max_heads_up_height_increased);
+
+ Resources res = getResources();
+ mIncreasedPaddingBetweenElements = res.getDimensionPixelSize(
+ R.dimen.notification_divider_height_increased);
+ mIconTransformContentShiftNoIcon = res.getDimensionPixelSize(
+ R.dimen.notification_icon_transform_content_shift);
+ mEnableNonGroupedNotificationExpand =
+ res.getBoolean(R.bool.config_enableNonGroupedNotificationExpand);
+ mShowGroupBackgroundWhenExpanded =
+ res.getBoolean(R.bool.config_showGroupNotificationBgWhenExpanded);
+ }
+
+ /**
+ * @param dimenId the dimen to look up
+ * @return the font scaled dimen as if it were in sp but doesn't shrink sizes below dp
+ */
+ private int getFontScaledHeight(int dimenId) {
+ int dimensionPixelSize = getResources().getDimensionPixelSize(dimenId);
+ float factor = Math.max(1.0f, getResources().getDisplayMetrics().scaledDensity /
+ getResources().getDisplayMetrics().density);
+ return (int) (dimensionPixelSize * factor);
+ }
+
+ /**
+ * Resets this view so it can be re-used for an updated notification.
+ */
+ public void reset() {
+ mShowingPublicInitialized = false;
+ onHeightReset();
+ requestLayout();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic);
+ mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
+ mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout};
+
+ for (NotificationContentView l : mLayouts) {
+ l.setExpandClickListener(mExpandClickListener);
+ l.setContainingNotification(this);
+ }
+ mGutsStub = (ViewStub) findViewById(R.id.notification_guts_stub);
+ mGutsStub.setOnInflateListener(new ViewStub.OnInflateListener() {
+ @Override
+ public void onInflate(ViewStub stub, View inflated) {
+ mGuts = (NotificationGuts) inflated;
+ mGuts.setClipTopAmount(getClipTopAmount());
+ mGuts.setActualHeight(getActualHeight());
+ mGutsStub = null;
+ }
+ });
+ mChildrenContainerStub = (ViewStub) findViewById(R.id.child_container_stub);
+ mChildrenContainerStub.setOnInflateListener(new ViewStub.OnInflateListener() {
+
+ @Override
+ public void onInflate(ViewStub stub, View inflated) {
+ mChildrenContainer = (NotificationChildrenContainer) inflated;
+ mChildrenContainer.setIsLowPriority(mIsLowPriority);
+ mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
+ mChildrenContainer.onNotificationUpdated();
+
+ if (mShouldTranslateContents) {
+ mTranslateableViews.add(mChildrenContainer);
+ }
+ }
+ });
+
+ if (mShouldTranslateContents) {
+ // Add the views that we translate to reveal the menu
+ mTranslateableViews = new ArrayList<>();
+ for (int i = 0; i < getChildCount(); i++) {
+ mTranslateableViews.add(getChildAt(i));
+ }
+ // Remove views that don't translate
+ mTranslateableViews.remove(mChildrenContainerStub);
+ mTranslateableViews.remove(mGutsStub);
+ }
+ }
+
+ public void resetTranslation() {
+ if (mTranslateAnim != null) {
+ mTranslateAnim.cancel();
+ }
+
+ if (!mShouldTranslateContents) {
+ setTranslationX(0);
+ } else if (mTranslateableViews != null) {
+ for (int i = 0; i < mTranslateableViews.size(); i++) {
+ mTranslateableViews.get(i).setTranslationX(0);
+ }
+ invalidateOutline();
+ }
+
+ mMenuRow.resetMenu();
+ }
+
+ public void animateTranslateNotification(final float leftTarget) {
+ if (mTranslateAnim != null) {
+ mTranslateAnim.cancel();
+ }
+ mTranslateAnim = getTranslateViewAnimator(leftTarget, null /* updateListener */);
+ if (mTranslateAnim != null) {
+ mTranslateAnim.start();
+ }
+ }
+
+ @Override
+ public void setTranslation(float translationX) {
+ if (areGutsExposed()) {
+ // Don't translate if guts are showing.
+ return;
+ }
+ if (!mShouldTranslateContents) {
+ setTranslationX(translationX);
+ } else if (mTranslateableViews != null) {
+ // Translate the group of views
+ for (int i = 0; i < mTranslateableViews.size(); i++) {
+ if (mTranslateableViews.get(i) != null) {
+ mTranslateableViews.get(i).setTranslationX(translationX);
+ }
+ }
+ invalidateOutline();
+ }
+ if (mMenuRow.getMenuView() != null) {
+ mMenuRow.onTranslationUpdate(translationX);
+ }
+ }
+
+ @Override
+ public float getTranslation() {
+ if (!mShouldTranslateContents) {
+ return getTranslationX();
+ }
+
+ if (mTranslateableViews != null && mTranslateableViews.size() > 0) {
+ // All of the views in the list should have same translation, just use first one.
+ return mTranslateableViews.get(0).getTranslationX();
+ }
+
+ return 0;
+ }
+
+ public Animator getTranslateViewAnimator(final float leftTarget,
+ AnimatorUpdateListener listener) {
+ if (mTranslateAnim != null) {
+ mTranslateAnim.cancel();
+ }
+ if (areGutsExposed()) {
+ // No translation if guts are exposed.
+ return null;
+ }
+ final ObjectAnimator translateAnim = ObjectAnimator.ofFloat(this, TRANSLATE_CONTENT,
+ leftTarget);
+ if (listener != null) {
+ translateAnim.addUpdateListener(listener);
+ }
+ translateAnim.addListener(new AnimatorListenerAdapter() {
+ boolean cancelled = false;
+
+ @Override
+ public void onAnimationCancel(Animator anim) {
+ cancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ if (!cancelled && leftTarget == 0) {
+ mMenuRow.resetMenu();
+ mTranslateAnim = null;
+ }
+ }
+ });
+ mTranslateAnim = translateAnim;
+ return translateAnim;
+ }
+
+ public void inflateGuts() {
+ if (mGuts == null) {
+ mGutsStub.inflate();
+ }
+ }
+
+ private void updateChildrenVisibility() {
+ mPrivateLayout.setVisibility(!mShowingPublic && !mIsSummaryWithChildren ? VISIBLE
+ : INVISIBLE);
+ if (mChildrenContainer != null) {
+ mChildrenContainer.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE
+ : INVISIBLE);
+ }
+ // The limits might have changed if the view suddenly became a group or vice versa
+ updateLimits();
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
+ if (super.onRequestSendAccessibilityEventInternal(child, event)) {
+ // Add a record for the entire layout since its content is somehow small.
+ // The event comes from a leaf view that is interacted with.
+ AccessibilityEvent record = AccessibilityEvent.obtain();
+ onInitializeAccessibilityEvent(record);
+ dispatchPopulateAccessibilityEvent(record);
+ event.appendRecord(record);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void setDark(boolean dark, boolean fade, long delay) {
+ super.setDark(dark, fade, delay);
+ mDark = dark;
+ if (!mIsHeadsUp) {
+ // Only fade the showing view of the pulsing notification.
+ fade = false;
+ }
+ final NotificationContentView showing = getShowingLayout();
+ if (showing != null) {
+ showing.setDark(dark, fade, delay);
+ }
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.setDark(dark, fade, delay);
+ }
+ updateShelfIconColor();
+ }
+
+ /**
+ * Tap sounds should not be played when we're unlocking.
+ * Doing so would cause audio collision and the system would feel unpolished.
+ */
+ @Override
+ public boolean isSoundEffectsEnabled() {
+ final boolean mute = mDark && mSecureStateProvider != null &&
+ !mSecureStateProvider.getAsBoolean();
+ return !mute && super.isSoundEffectsEnabled();
+ }
+
+ public boolean isExpandable() {
+ if (mIsSummaryWithChildren && !mShowingPublic) {
+ return !mChildrenExpanded;
+ }
+ return mEnableNonGroupedNotificationExpand && mExpandable;
+ }
+
+ public void setExpandable(boolean expandable) {
+ mExpandable = expandable;
+ mPrivateLayout.updateExpandButtons(isExpandable());
+ }
+
+ @Override
+ public void setClipToActualHeight(boolean clipToActualHeight) {
+ super.setClipToActualHeight(clipToActualHeight || isUserLocked());
+ getShowingLayout().setClipToActualHeight(clipToActualHeight || isUserLocked());
+ }
+
+ /**
+ * @return whether the user has changed the expansion state
+ */
+ public boolean hasUserChangedExpansion() {
+ return mHasUserChangedExpansion;
+ }
+
+ public boolean isUserExpanded() {
+ return mUserExpanded;
+ }
+
+ /**
+ * Set this notification to be expanded by the user
+ *
+ * @param userExpanded whether the user wants this notification to be expanded
+ */
+ public void setUserExpanded(boolean userExpanded) {
+ setUserExpanded(userExpanded, false /* allowChildExpansion */);
+ }
+
+ /**
+ * Set this notification to be expanded by the user
+ *
+ * @param userExpanded whether the user wants this notification to be expanded
+ * @param allowChildExpansion whether a call to this method allows expanding children
+ */
+ public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
+ mFalsingManager.setNotificationExpanded();
+ if (mIsSummaryWithChildren && !mShowingPublic && allowChildExpansion
+ && !mChildrenContainer.showingAsLowPriority()) {
+ final boolean wasExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification);
+ mGroupManager.setGroupExpanded(mStatusBarNotification, userExpanded);
+ onExpansionChanged(true /* userAction */, wasExpanded);
+ return;
+ }
+ if (userExpanded && !mExpandable) return;
+ final boolean wasExpanded = isExpanded();
+ mHasUserChangedExpansion = true;
+ mUserExpanded = userExpanded;
+ onExpansionChanged(true /* userAction */, wasExpanded);
+ if (!wasExpanded && isExpanded()
+ && getActualHeight() != getIntrinsicHeight()) {
+ notifyHeightChanged(true /* needsAnimation */);
+ }
+ }
+
+ public void resetUserExpansion() {
+ boolean changed = mUserExpanded;
+ mHasUserChangedExpansion = false;
+ mUserExpanded = false;
+ if (changed && mIsSummaryWithChildren) {
+ mChildrenContainer.onExpansionChanged();
+ }
+ updateShelfIconColor();
+ }
+
+ public boolean isUserLocked() {
+ return mUserLocked && !mForceUnlocked;
+ }
+
+ public void setUserLocked(boolean userLocked) {
+ mUserLocked = userLocked;
+ mPrivateLayout.setUserExpanding(userLocked);
+ // This is intentionally not guarded with mIsSummaryWithChildren since we might have had
+ // children but not anymore.
+ if (mChildrenContainer != null) {
+ mChildrenContainer.setUserLocked(userLocked);
+ if (mIsSummaryWithChildren && (userLocked || !isGroupExpanded())) {
+ updateBackgroundForGroupState();
+ }
+ }
+ }
+
+ /**
+ * @return has the system set this notification to be expanded
+ */
+ public boolean isSystemExpanded() {
+ return mIsSystemExpanded;
+ }
+
+ /**
+ * Set this notification to be expanded by the system.
+ *
+ * @param expand whether the system wants this notification to be expanded.
+ */
+ public void setSystemExpanded(boolean expand) {
+ if (expand != mIsSystemExpanded) {
+ final boolean wasExpanded = isExpanded();
+ mIsSystemExpanded = expand;
+ notifyHeightChanged(false /* needsAnimation */);
+ onExpansionChanged(false /* userAction */, wasExpanded);
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.updateGroupOverflow();
+ }
+ }
+ }
+
+ /**
+ * @param onKeyguard whether to prevent notification expansion
+ */
+ public void setOnKeyguard(boolean onKeyguard) {
+ if (onKeyguard != mOnKeyguard) {
+ boolean wasAboveShelf = isAboveShelf();
+ final boolean wasExpanded = isExpanded();
+ mOnKeyguard = onKeyguard;
+ onExpansionChanged(false /* userAction */, wasExpanded);
+ if (wasExpanded != isExpanded()) {
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.updateGroupOverflow();
+ }
+ notifyHeightChanged(false /* needsAnimation */);
+ }
+ if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+ }
+ }
+ }
+
+ /**
+ * @return Can the underlying notification be cleared? This can be different from whether the
+ * notification can be dismissed in case notifications are sensitive on the lockscreen.
+ * @see #canViewBeDismissed()
+ */
+ public boolean isClearable() {
+ if (mStatusBarNotification == null || !mStatusBarNotification.isClearable()) {
+ return false;
+ }
+ if (mIsSummaryWithChildren) {
+ List<ExpandableNotificationRow> notificationChildren =
+ mChildrenContainer.getNotificationChildren();
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow child = notificationChildren.get(i);
+ if (!child.isClearable()) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ if (isUserLocked()) {
+ return getActualHeight();
+ }
+ if (mGuts != null && mGuts.isExposed()) {
+ return mGuts.getIntrinsicHeight();
+ } else if ((isChildInGroup() && !isGroupExpanded())) {
+ return mPrivateLayout.getMinHeight();
+ } else if (mSensitive && mHideSensitiveForIntrinsicHeight) {
+ return getMinHeight();
+ } else if (mIsSummaryWithChildren && (!mOnKeyguard || mShowAmbient)) {
+ return mChildrenContainer.getIntrinsicHeight();
+ } else if (isHeadsUpAllowed() && (mIsHeadsUp || mHeadsupDisappearRunning)) {
+ if (isPinned() || mHeadsupDisappearRunning) {
+ return getPinnedHeadsUpHeight(true /* atLeastMinHeight */);
+ } else if (isExpanded()) {
+ return Math.max(getMaxExpandHeight(), mHeadsUpHeight);
+ } else {
+ return Math.max(getCollapsedHeight(), mHeadsUpHeight);
+ }
+ } else if (isExpanded()) {
+ return getMaxExpandHeight();
+ } else {
+ return getCollapsedHeight();
+ }
+ }
+
+ private boolean isHeadsUpAllowed() {
+ return !mOnKeyguard && !mShowAmbient;
+ }
+
+ @Override
+ public boolean isGroupExpanded() {
+ return mGroupManager.isGroupExpanded(mStatusBarNotification);
+ }
+
+ private void onChildrenCountChanged() {
+ mIsSummaryWithChildren = StatusBar.ENABLE_CHILD_NOTIFICATIONS
+ && mChildrenContainer != null && mChildrenContainer.getNotificationChildCount() > 0;
+ if (mIsSummaryWithChildren && mChildrenContainer.getHeaderView() == null) {
+ mChildrenContainer.recreateNotificationHeader(mExpandClickListener
+ );
+ }
+ getShowingLayout().updateBackgroundColor(false /* animate */);
+ mPrivateLayout.updateExpandButtons(isExpandable());
+ updateChildrenHeaderAppearance();
+ updateChildrenVisibility();
+ }
+
+ public void updateChildrenHeaderAppearance() {
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.updateChildrenHeaderAppearance();
+ }
+ }
+
+ /**
+ * Check whether the view state is currently expanded. This is given by the system in {@link
+ * #setSystemExpanded(boolean)} and can be overridden by user expansion or
+ * collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this
+ * view can differ from this state, if layout params are modified from outside.
+ *
+ * @return whether the view state is currently expanded.
+ */
+ public boolean isExpanded() {
+ return isExpanded(false /* allowOnKeyguard */);
+ }
+
+ public boolean isExpanded(boolean allowOnKeyguard) {
+ return (!mOnKeyguard || allowOnKeyguard)
+ && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded())
+ || isUserExpanded());
+ }
+
+ private boolean isSystemChildExpanded() {
+ return mIsSystemChildExpanded;
+ }
+
+ public void setSystemChildExpanded(boolean expanded) {
+ mIsSystemChildExpanded = expanded;
+ }
+
+ public void setLayoutListener(LayoutListener listener) {
+ mLayoutListener = listener;
+ }
+
+ public void removeListener() {
+ mLayoutListener = null;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ updateMaxHeights();
+ if (mMenuRow.getMenuView() != null) {
+ mMenuRow.onHeightUpdate();
+ }
+ updateContentShiftHeight();
+ if (mLayoutListener != null) {
+ mLayoutListener.onLayout();
+ }
+ }
+
+ /**
+ * Updates the content shift height such that the header is completely hidden when coming from
+ * the top.
+ */
+ private void updateContentShiftHeight() {
+ NotificationHeaderView notificationHeader = getVisibleNotificationHeader();
+ if (notificationHeader != null) {
+ CachingIconView icon = notificationHeader.getIcon();
+ mIconTransformContentShift = getRelativeTopPadding(icon) + icon.getHeight();
+ } else {
+ mIconTransformContentShift = mIconTransformContentShiftNoIcon;
+ }
+ }
+
+ private void updateMaxHeights() {
+ int intrinsicBefore = getIntrinsicHeight();
+ View expandedChild = mPrivateLayout.getExpandedChild();
+ if (expandedChild == null) {
+ expandedChild = mPrivateLayout.getContractedChild();
+ }
+ mMaxExpandHeight = expandedChild.getHeight();
+ View headsUpChild = mPrivateLayout.getHeadsUpChild();
+ if (headsUpChild == null) {
+ headsUpChild = mPrivateLayout.getContractedChild();
+ }
+ mHeadsUpHeight = headsUpChild.getHeight();
+ if (intrinsicBefore != getIntrinsicHeight()) {
+ notifyHeightChanged(true /* needsAnimation */);
+ }
+ }
+
+ @Override
+ public void notifyHeightChanged(boolean needsAnimation) {
+ super.notifyHeightChanged(needsAnimation);
+ getShowingLayout().requestSelectLayout(needsAnimation || isUserLocked());
+ }
+
+ public void setSensitive(boolean sensitive, boolean hideSensitive) {
+ mSensitive = sensitive;
+ mSensitiveHiddenInGeneral = hideSensitive;
+ }
+
+ @Override
+ public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
+ mHideSensitiveForIntrinsicHeight = hideSensitive;
+ if (mIsSummaryWithChildren) {
+ List<ExpandableNotificationRow> notificationChildren =
+ mChildrenContainer.getNotificationChildren();
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow child = notificationChildren.get(i);
+ child.setHideSensitiveForIntrinsicHeight(hideSensitive);
+ }
+ }
+ }
+
+ @Override
+ public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
+ long duration) {
+ boolean oldShowingPublic = mShowingPublic;
+ mShowingPublic = mSensitive && hideSensitive;
+ if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) {
+ return;
+ }
+
+ // bail out if no public version
+ if (mPublicLayout.getChildCount() == 0) return;
+
+ if (!animated) {
+ mPublicLayout.animate().cancel();
+ mPrivateLayout.animate().cancel();
+ if (mChildrenContainer != null) {
+ mChildrenContainer.animate().cancel();
+ mChildrenContainer.setAlpha(1f);
+ }
+ mPublicLayout.setAlpha(1f);
+ mPrivateLayout.setAlpha(1f);
+ mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE);
+ updateChildrenVisibility();
+ } else {
+ animateShowingPublic(delay, duration);
+ }
+ NotificationContentView showingLayout = getShowingLayout();
+ showingLayout.updateBackgroundColor(animated);
+ mPrivateLayout.updateExpandButtons(isExpandable());
+ updateShelfIconColor();
+ showingLayout.setDark(isDark(), false /* animate */, 0 /* delay */);
+ mShowingPublicInitialized = true;
+ }
+
+ private void animateShowingPublic(long delay, long duration) {
+ View[] privateViews = mIsSummaryWithChildren
+ ? new View[] {mChildrenContainer}
+ : new View[] {mPrivateLayout};
+ View[] publicViews = new View[] {mPublicLayout};
+ View[] hiddenChildren = mShowingPublic ? privateViews : publicViews;
+ View[] shownChildren = mShowingPublic ? publicViews : privateViews;
+ for (final View hiddenView : hiddenChildren) {
+ hiddenView.setVisibility(View.VISIBLE);
+ hiddenView.animate().cancel();
+ hiddenView.animate()
+ .alpha(0f)
+ .setStartDelay(delay)
+ .setDuration(duration)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ hiddenView.setVisibility(View.INVISIBLE);
+ }
+ });
+ }
+ for (View showView : shownChildren) {
+ showView.setVisibility(View.VISIBLE);
+ showView.setAlpha(0f);
+ showView.animate().cancel();
+ showView.animate()
+ .alpha(1f)
+ .setStartDelay(delay)
+ .setDuration(duration);
+ }
+ }
+
+ @Override
+ public boolean mustStayOnScreen() {
+ return mIsHeadsUp;
+ }
+
+ /**
+ * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as
+ * otherwise some state might not be updated. To request about the general clearability
+ * see {@link #isClearable()}.
+ */
+ public boolean canViewBeDismissed() {
+ return isClearable() && (!mShowingPublic || !mSensitiveHiddenInGeneral);
+ }
+
+ public void makeActionsVisibile() {
+ setUserExpanded(true, true);
+ if (isChildInGroup()) {
+ mGroupManager.setGroupExpanded(mStatusBarNotification, true);
+ }
+ notifyHeightChanged(false /* needsAnimation */);
+ }
+
+ public void setChildrenExpanded(boolean expanded, boolean animate) {
+ mChildrenExpanded = expanded;
+ if (mChildrenContainer != null) {
+ mChildrenContainer.setChildrenExpanded(expanded);
+ }
+ updateBackgroundForGroupState();
+ updateClickAndFocus();
+ }
+
+ public static void applyTint(View v, int color) {
+ int alpha;
+ if (color != 0) {
+ alpha = COLORED_DIVIDER_ALPHA;
+ } else {
+ color = 0xff000000;
+ alpha = DEFAULT_DIVIDER_ALPHA;
+ }
+ if (v.getBackground() instanceof ColorDrawable) {
+ ColorDrawable background = (ColorDrawable) v.getBackground();
+ background.mutate();
+ background.setColor(color);
+ background.setAlpha(alpha);
+ }
+ }
+
+ public int getMaxExpandHeight() {
+ return mMaxExpandHeight;
+ }
+
+ public boolean areGutsExposed() {
+ return (mGuts != null && mGuts.isExposed());
+ }
+
+ @Override
+ public boolean isContentExpandable() {
+ if (mIsSummaryWithChildren && !mShowingPublic) {
+ return true;
+ }
+ NotificationContentView showingLayout = getShowingLayout();
+ return showingLayout.isContentExpandable();
+ }
+
+ @Override
+ protected View getContentView() {
+ if (mIsSummaryWithChildren && !mShowingPublic) {
+ return mChildrenContainer;
+ }
+ return getShowingLayout();
+ }
+
+ @Override
+ protected void onAppearAnimationFinished(boolean wasAppearing) {
+ super.onAppearAnimationFinished(wasAppearing);
+ if (wasAppearing) {
+ // During the animation the visible view might have changed, so let's make sure all
+ // alphas are reset
+ if (mChildrenContainer != null) {
+ mChildrenContainer.setAlpha(1.0f);
+ mChildrenContainer.setLayerType(LAYER_TYPE_NONE, null);
+ }
+ for (NotificationContentView l : mLayouts) {
+ l.setAlpha(1.0f);
+ l.setLayerType(LAYER_TYPE_NONE, null);
+ }
+ }
+ }
+
+ @Override
+ public int getExtraBottomPadding() {
+ if (mIsSummaryWithChildren && isGroupExpanded()) {
+ return mIncreasedPaddingBetweenElements;
+ }
+ return 0;
+ }
+
+ @Override
+ public void setActualHeight(int height, boolean notifyListeners) {
+ boolean changed = height != getActualHeight();
+ super.setActualHeight(height, notifyListeners);
+ if (changed && isRemoved()) {
+ // TODO: remove this once we found the gfx bug for this.
+ // This is a hack since a removed view sometimes would just stay blank. it occured
+ // when sending yourself a message and then clicking on it.
+ ViewGroup parent = (ViewGroup) getParent();
+ if (parent != null) {
+ parent.invalidate();
+ }
+ }
+ if (mGuts != null && mGuts.isExposed()) {
+ mGuts.setActualHeight(height);
+ return;
+ }
+ int contentHeight = Math.max(getMinHeight(), height);
+ for (NotificationContentView l : mLayouts) {
+ l.setContentHeight(contentHeight);
+ }
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.setActualHeight(height);
+ }
+ if (mGuts != null) {
+ mGuts.setActualHeight(height);
+ }
+ if (mMenuRow.getMenuView() != null) {
+ mMenuRow.onHeightUpdate();
+ }
+ }
+
+ @Override
+ public int getMaxContentHeight() {
+ if (mIsSummaryWithChildren && !mShowingPublic) {
+ return mChildrenContainer.getMaxContentHeight();
+ }
+ NotificationContentView showingLayout = getShowingLayout();
+ return showingLayout.getMaxHeight();
+ }
+
+ @Override
+ public int getMinHeight() {
+ if (mGuts != null && mGuts.isExposed()) {
+ return mGuts.getIntrinsicHeight();
+ } else if (isHeadsUpAllowed() && mIsHeadsUp && mHeadsUpManager.isTrackingHeadsUp()) {
+ return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
+ } else if (mIsSummaryWithChildren && !isGroupExpanded() && !mShowingPublic) {
+ return mChildrenContainer.getMinHeight();
+ } else if (isHeadsUpAllowed() && mIsHeadsUp) {
+ return mHeadsUpHeight;
+ }
+ NotificationContentView showingLayout = getShowingLayout();
+ return showingLayout.getMinHeight();
+ }
+
+ @Override
+ public int getCollapsedHeight() {
+ if (mIsSummaryWithChildren && !mShowingPublic) {
+ return mChildrenContainer.getCollapsedHeight();
+ }
+ return getMinHeight();
+ }
+
+ @Override
+ public void setClipTopAmount(int clipTopAmount) {
+ super.setClipTopAmount(clipTopAmount);
+ for (NotificationContentView l : mLayouts) {
+ l.setClipTopAmount(clipTopAmount);
+ }
+ if (mGuts != null) {
+ mGuts.setClipTopAmount(clipTopAmount);
+ }
+ }
+
+ @Override
+ public void setClipBottomAmount(int clipBottomAmount) {
+ if (clipBottomAmount != mClipBottomAmount) {
+ super.setClipBottomAmount(clipBottomAmount);
+ for (NotificationContentView l : mLayouts) {
+ l.setClipBottomAmount(clipBottomAmount);
+ }
+ if (mGuts != null) {
+ mGuts.setClipBottomAmount(clipBottomAmount);
+ }
+ }
+ if (mChildrenContainer != null) {
+ // We have to update this even if it hasn't changed, since the children locations can
+ // have changed
+ mChildrenContainer.setClipBottomAmount(clipBottomAmount);
+ }
+ }
+
+ public boolean isMaxExpandHeightInitialized() {
+ return mMaxExpandHeight != 0;
+ }
+
+ public NotificationContentView getShowingLayout() {
+ return mShowingPublic ? mPublicLayout : mPrivateLayout;
+ }
+
+ public void setLegacy(boolean legacy) {
+ for (NotificationContentView l : mLayouts) {
+ l.setLegacy(legacy);
+ }
+ }
+
+ @Override
+ protected void updateBackgroundTint() {
+ super.updateBackgroundTint();
+ updateBackgroundForGroupState();
+ if (mIsSummaryWithChildren) {
+ List<ExpandableNotificationRow> notificationChildren =
+ mChildrenContainer.getNotificationChildren();
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow child = notificationChildren.get(i);
+ child.updateBackgroundForGroupState();
+ }
+ }
+ }
+
+ /**
+ * Called when a group has finished animating from collapsed or expanded state.
+ */
+ public void onFinishedExpansionChange() {
+ mGroupExpansionChanging = false;
+ updateBackgroundForGroupState();
+ }
+
+ /**
+ * Updates the parent and children backgrounds in a group based on the expansion state.
+ */
+ public void updateBackgroundForGroupState() {
+ if (mIsSummaryWithChildren) {
+ // Only when the group has finished expanding do we hide its background.
+ mShowNoBackground = !mShowGroupBackgroundWhenExpanded && isGroupExpanded()
+ && !isGroupExpansionChanging() && !isUserLocked();
+ mChildrenContainer.updateHeaderForExpansion(mShowNoBackground);
+ List<ExpandableNotificationRow> children = mChildrenContainer.getNotificationChildren();
+ for (int i = 0; i < children.size(); i++) {
+ children.get(i).updateBackgroundForGroupState();
+ }
+ } else if (isChildInGroup()) {
+ final int childColor = getShowingLayout().getBackgroundColorForExpansionState();
+ // Only show a background if the group is expanded OR if it is expanding / collapsing
+ // and has a custom background color.
+ final boolean showBackground = isGroupExpanded()
+ || ((mNotificationParent.isGroupExpansionChanging()
+ || mNotificationParent.isUserLocked()) && childColor != 0);
+ mShowNoBackground = !showBackground;
+ } else {
+ // Only children or parents ever need no background.
+ mShowNoBackground = false;
+ }
+ updateOutline();
+ updateBackground();
+ }
+
+ public int getPositionOfChild(ExpandableNotificationRow childRow) {
+ if (mIsSummaryWithChildren) {
+ return mChildrenContainer.getPositionInLinearLayout(childRow);
+ }
+ return 0;
+ }
+
+ public void setExpansionLogger(ExpansionLogger logger, String key) {
+ mLogger = logger;
+ mLoggingKey = key;
+ }
+
+ public void onExpandedByGesture(boolean userExpanded) {
+ int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER;
+ if (mGroupManager.isSummaryOfGroup(getStatusBarNotification())) {
+ event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER;
+ }
+ MetricsLogger.action(mContext, event, userExpanded);
+ }
+
+ @Override
+ public float getIncreasedPaddingAmount() {
+ if (mIsSummaryWithChildren) {
+ if (isGroupExpanded()) {
+ return 1.0f;
+ } else if (isUserLocked()) {
+ return mChildrenContainer.getIncreasedPaddingAmount();
+ }
+ } else if (isColorized() && (!mIsLowPriority || isExpanded())) {
+ return -1.0f;
+ }
+ return 0.0f;
+ }
+
+ private boolean isColorized() {
+ return mIsColorized && mBgTint != NO_COLOR;
+ }
+
+ @Override
+ protected boolean disallowSingleClick(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ NotificationHeaderView header = getVisibleNotificationHeader();
+ if (header != null && header.isInTouchRect(x - getTranslation(), y)) {
+ return true;
+ }
+ if ((!mIsSummaryWithChildren || mShowingPublic)
+ && getShowingLayout().disallowSingleClick(x, y)) {
+ return true;
+ }
+ return super.disallowSingleClick(event);
+ }
+
+ private void onExpansionChanged(boolean userAction, boolean wasExpanded) {
+ boolean nowExpanded = isExpanded();
+ if (mIsSummaryWithChildren && (!mIsLowPriority || wasExpanded)) {
+ nowExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification);
+ }
+ if (nowExpanded != wasExpanded) {
+ updateShelfIconColor();
+ if (mLogger != null) {
+ mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded);
+ }
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.onExpansionChanged();
+ }
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ if (canViewBeDismissed()) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS);
+ }
+ boolean expandable = mShowingPublic;
+ boolean isExpanded = false;
+ if (!expandable) {
+ if (mIsSummaryWithChildren) {
+ expandable = true;
+ if (!mIsLowPriority || isExpanded()) {
+ isExpanded = isGroupExpanded();
+ }
+ } else {
+ expandable = mPrivateLayout.isContentExpandable();
+ isExpanded = isExpanded();
+ }
+ }
+ if (expandable) {
+ if (isExpanded) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
+ } else {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+ }
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_DISMISS:
+ NotificationStackScrollLayout.performDismiss(this, mGroupManager,
+ true /* fromAccessibility */);
+ return true;
+ case AccessibilityNodeInfo.ACTION_COLLAPSE:
+ case AccessibilityNodeInfo.ACTION_EXPAND:
+ mExpandClickListener.onClick(this);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean shouldRefocusOnDismiss() {
+ return mRefocusOnDismiss || isAccessibilityFocused();
+ }
+
+ public interface OnExpandClickListener {
+ void onExpandClicked(NotificationData.Entry clickedEntry, boolean nowExpanded);
+ }
+
+ @Override
+ public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+ return new NotificationViewState(stackScrollState);
+ }
+
+ @Override
+ public boolean isAboveShelf() {
+ return !isOnKeyguard()
+ && (mIsPinned || mHeadsupDisappearRunning || (mIsHeadsUp && mAboveShelf));
+ }
+
+ public void setShowAmbient(boolean showAmbient) {
+ if (showAmbient != mShowAmbient) {
+ mShowAmbient = showAmbient;
+ if (mChildrenContainer != null) {
+ mChildrenContainer.notifyShowAmbientChanged();
+ }
+ notifyHeightChanged(false /* needsAnimation */);
+ }
+ }
+
+ public boolean isShowingAmbient() {
+ return mShowAmbient;
+ }
+
+ public void setAboveShelf(boolean aboveShelf) {
+ boolean wasAboveShelf = isAboveShelf();
+ mAboveShelf = aboveShelf;
+ if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+ }
+ }
+
+ public static class NotificationViewState extends ExpandableViewState {
+
+ private final StackScrollState mOverallState;
+
+
+ private NotificationViewState(StackScrollState stackScrollState) {
+ mOverallState = stackScrollState;
+ }
+
+ @Override
+ public void applyToView(View view) {
+ super.applyToView(view);
+ if (view instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ row.applyChildrenState(mOverallState);
+ }
+ }
+
+ @Override
+ protected void onYTranslationAnimationFinished(View view) {
+ super.onYTranslationAnimationFinished(view);
+ if (view instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ if (row.isHeadsUpAnimatingAway()) {
+ row.setHeadsUpAnimatingAway(false);
+ }
+ }
+ }
+
+ @Override
+ public void animateTo(View child, AnimationProperties properties) {
+ super.animateTo(child, properties);
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ row.startChildAnimation(mOverallState, properties);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ protected void setChildrenContainer(NotificationChildrenContainer childrenContainer) {
+ mChildrenContainer = childrenContainer;
+ }
+}
diff --git a/com/android/systemui/statusbar/ExpandableOutlineView.java b/com/android/systemui/statusbar/ExpandableOutlineView.java
new file mode 100644
index 0000000..2556890
--- /dev/null
+++ b/com/android/systemui/statusbar/ExpandableOutlineView.java
@@ -0,0 +1,165 @@
+/*
+ * 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.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import com.android.systemui.R;
+
+/**
+ * Like {@link ExpandableView}, but setting an outline for the height and clipping.
+ */
+public abstract class ExpandableOutlineView extends ExpandableView {
+
+ private final Rect mOutlineRect = new Rect();
+ private boolean mCustomOutline;
+ private float mOutlineAlpha = -1f;
+ private float mOutlineRadius;
+
+ /**
+ * {@code true} if the children views of the {@link ExpandableOutlineView} are translated when
+ * it is moved. Otherwise, the translation is set on the {@code ExpandableOutlineView} itself.
+ */
+ protected boolean mShouldTranslateContents;
+
+ private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ int translation = mShouldTranslateContents ? (int) getTranslation() : 0;
+ if (!mCustomOutline) {
+ outline.setRoundRect(translation,
+ mClipTopAmount,
+ getWidth() + translation,
+ Math.max(getActualHeight() - mClipBottomAmount, mClipTopAmount),
+ mOutlineRadius);
+ } else {
+ outline.setRoundRect(mOutlineRect, mOutlineRadius);
+ }
+ outline.setAlpha(mOutlineAlpha);
+ }
+ };
+
+ public ExpandableOutlineView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setOutlineProvider(mProvider);
+ initDimens();
+ }
+
+ private void initDimens() {
+ Resources res = getResources();
+ mShouldTranslateContents =
+ res.getBoolean(R.bool.config_translateNotificationContentsOnSwipe);
+ mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius);
+ setClipToOutline(res.getBoolean(R.bool.config_clipNotificationsToOutline));
+ }
+
+ public void onDensityOrFontScaleChanged() {
+ initDimens();
+ invalidateOutline();
+ }
+
+ @Override
+ public void setActualHeight(int actualHeight, boolean notifyListeners) {
+ super.setActualHeight(actualHeight, notifyListeners);
+ invalidateOutline();
+ }
+
+ @Override
+ public void setClipTopAmount(int clipTopAmount) {
+ super.setClipTopAmount(clipTopAmount);
+ invalidateOutline();
+ }
+
+ @Override
+ public void setClipBottomAmount(int clipBottomAmount) {
+ super.setClipBottomAmount(clipBottomAmount);
+ invalidateOutline();
+ }
+
+ protected void setOutlineAlpha(float alpha) {
+ if (alpha != mOutlineAlpha) {
+ mOutlineAlpha = alpha;
+ invalidateOutline();
+ }
+ }
+
+ @Override
+ public float getOutlineAlpha() {
+ return mOutlineAlpha;
+ }
+
+ protected void setOutlineRect(RectF rect) {
+ if (rect != null) {
+ setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
+ } else {
+ mCustomOutline = false;
+ setClipToOutline(false);
+ invalidateOutline();
+ }
+ }
+
+ @Override
+ public int getOutlineTranslation() {
+ return mCustomOutline ? mOutlineRect.left : (int) getTranslation();
+ }
+
+ public void updateOutline() {
+ if (mCustomOutline) {
+ return;
+ }
+ boolean hasOutline = needsOutline();
+ setOutlineProvider(hasOutline ? mProvider : null);
+ }
+
+ /**
+ * @return Whether the view currently needs an outline. This is usually {@code false} in case
+ * it doesn't have a background.
+ */
+ protected boolean needsOutline() {
+ if (isChildInGroup()) {
+ return isGroupExpanded() && !isGroupExpansionChanging();
+ } else if (isSummaryWithChildren()) {
+ return !isGroupExpanded() || isGroupExpansionChanging();
+ }
+ return true;
+ }
+
+ public boolean isOutlineShowing() {
+ ViewOutlineProvider op = getOutlineProvider();
+ return op != null;
+ }
+
+ protected void setOutlineRect(float left, float top, float right, float bottom) {
+ mCustomOutline = true;
+ setClipToOutline(true);
+
+ mOutlineRect.set((int) left, (int) top, (int) right, (int) bottom);
+
+ // Outlines need to be at least 1 dp
+ mOutlineRect.bottom = (int) Math.max(top, mOutlineRect.bottom);
+ mOutlineRect.right = (int) Math.max(left, mOutlineRect.right);
+
+ invalidateOutline();
+ }
+
+}
diff --git a/com/android/systemui/statusbar/ExpandableView.java b/com/android/systemui/statusbar/ExpandableView.java
new file mode 100644
index 0000000..efe5e0c
--- /dev/null
+++ b/com/android/systemui/statusbar/ExpandableView.java
@@ -0,0 +1,530 @@
+/*
+ * 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.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackScrollState;
+
+import java.util.ArrayList;
+
+/**
+ * An abstract view for expandable views.
+ */
+public abstract class ExpandableView extends FrameLayout {
+
+ protected OnHeightChangedListener mOnHeightChangedListener;
+ private int mActualHeight;
+ protected int mClipTopAmount;
+ protected int mClipBottomAmount;
+ private boolean mDark;
+ private ArrayList<View> mMatchParentViews = new ArrayList<View>();
+ private static Rect mClipRect = new Rect();
+ private boolean mWillBeGone;
+ private int mMinClipTopAmount = 0;
+ private boolean mClipToActualHeight = true;
+ private boolean mChangingPosition = false;
+ private ViewGroup mTransientContainer;
+ private boolean mInShelf;
+ private boolean mTransformingInShelf;
+
+ public ExpandableView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int givenSize = MeasureSpec.getSize(heightMeasureSpec);
+ int ownMaxHeight = Integer.MAX_VALUE;
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode != MeasureSpec.UNSPECIFIED && givenSize != 0) {
+ ownMaxHeight = Math.min(givenSize, ownMaxHeight);
+ }
+ int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
+ int maxChildHeight = 0;
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ int childHeightSpec = newHeightSpec;
+ ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
+ if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
+ if (layoutParams.height >= 0) {
+ // An actual height is set
+ childHeightSpec = layoutParams.height > ownMaxHeight
+ ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
+ : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
+ }
+ child.measure(
+ getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
+ childHeightSpec);
+ int childHeight = child.getMeasuredHeight();
+ maxChildHeight = Math.max(maxChildHeight, childHeight);
+ } else {
+ mMatchParentViews.add(child);
+ }
+ }
+ int ownHeight = heightMode == MeasureSpec.EXACTLY
+ ? givenSize : Math.min(ownMaxHeight, maxChildHeight);
+ newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
+ for (View child : mMatchParentViews) {
+ child.measure(getChildMeasureSpec(
+ widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
+ newHeightSpec);
+ }
+ mMatchParentViews.clear();
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ setMeasuredDimension(width, ownHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ updateClipping();
+ }
+
+ @Override
+ public boolean pointInView(float localX, float localY, float slop) {
+ float top = mClipTopAmount;
+ float bottom = mActualHeight;
+ return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
+ localY < (bottom + slop);
+ }
+
+ /**
+ * Sets the actual height of this notification. This is different than the laid out
+ * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
+ *
+ * @param actualHeight The height of this notification.
+ * @param notifyListeners Whether the listener should be informed about the change.
+ */
+ public void setActualHeight(int actualHeight, boolean notifyListeners) {
+ mActualHeight = actualHeight;
+ updateClipping();
+ if (notifyListeners) {
+ notifyHeightChanged(false /* needsAnimation */);
+ }
+ }
+
+ public void setActualHeight(int actualHeight) {
+ setActualHeight(actualHeight, true /* notifyListeners */);
+ }
+
+ /**
+ * See {@link #setActualHeight}.
+ *
+ * @return The current actual height of this notification.
+ */
+ public int getActualHeight() {
+ return mActualHeight;
+ }
+
+ /**
+ * @return The maximum height of this notification.
+ */
+ public int getMaxContentHeight() {
+ return getHeight();
+ }
+
+ /**
+ * @return The minimum content height of this notification.
+ */
+ public int getMinHeight() {
+ return getHeight();
+ }
+
+ /**
+ * @return The collapsed height of this view. Note that this might be different
+ * than {@link #getMinHeight()} because some elements like groups may have different sizes when
+ * they are system expanded.
+ */
+ public int getCollapsedHeight() {
+ return getHeight();
+ }
+
+ /**
+ * Sets the notification as dimmed. The default implementation does nothing.
+ *
+ * @param dimmed Whether the notification should be dimmed.
+ * @param fade Whether an animation should be played to change the state.
+ */
+ public void setDimmed(boolean dimmed, boolean fade) {
+ }
+
+ /**
+ * Sets the notification as dark. The default implementation does nothing.
+ *
+ * @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, long delay) {
+ mDark = dark;
+ }
+
+ public boolean isDark() {
+ return mDark;
+ }
+
+ /**
+ * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
+ * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
+ * of a stack scroller update such that the updated intrinsic height (which is dependent on
+ * whether private or public layout is showing) gets taken into account into all layout
+ * calculations.
+ */
+ public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
+ }
+
+ /**
+ * Sets whether the notification should hide its private contents if it is sensitive.
+ */
+ public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
+ long duration) {
+ }
+
+ /**
+ * @return The desired notification height.
+ */
+ public int getIntrinsicHeight() {
+ return getHeight();
+ }
+
+ /**
+ * Sets the amount this view should be clipped from the top. This is used when an expanded
+ * notification is scrolling in the top or bottom stack.
+ *
+ * @param clipTopAmount The amount of pixels this view should be clipped from top.
+ */
+ public void setClipTopAmount(int clipTopAmount) {
+ mClipTopAmount = clipTopAmount;
+ updateClipping();
+ }
+
+ /**
+ * Set the amount the the notification is clipped on the bottom in addition to the regular
+ * clipping. This is mainly used to clip something in a non-animated way without changing the
+ * actual height of the notification and is purely visual.
+ *
+ * @param clipBottomAmount the amount to clip.
+ */
+ public void setClipBottomAmount(int clipBottomAmount) {
+ mClipBottomAmount = clipBottomAmount;
+ updateClipping();
+ }
+
+ public int getClipTopAmount() {
+ return mClipTopAmount;
+ }
+
+ public int getClipBottomAmount() {
+ return mClipBottomAmount;
+ }
+
+ public void setOnHeightChangedListener(OnHeightChangedListener listener) {
+ mOnHeightChangedListener = listener;
+ }
+
+ /**
+ * @return Whether we can expand this views content.
+ */
+ public boolean isContentExpandable() {
+ return false;
+ }
+
+ public void notifyHeightChanged(boolean needsAnimation) {
+ if (mOnHeightChangedListener != null) {
+ mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
+ }
+ }
+
+ public boolean isTransparent() {
+ return false;
+ }
+
+ /**
+ * Perform a remove animation on this view.
+ *
+ * @param duration The duration of the remove animation.
+ * @param translationDirection The direction value from [-1 ... 1] indicating in which the
+ * animation should be performed. A value of -1 means that The
+ * remove animation should be performed upwards,
+ * such that the child appears to be going away to the top. 1
+ * Should mean the opposite.
+ * @param onFinishedRunnable A runnable which should be run when the animation is finished.
+ */
+ public abstract void performRemoveAnimation(long duration, float translationDirection,
+ Runnable onFinishedRunnable);
+
+ public abstract void performAddAnimation(long delay, long duration);
+
+ /**
+ * Set the notification appearance to be below the speed bump.
+ * @param below true if it is below.
+ */
+ public void setBelowSpeedBump(boolean below) {
+ }
+
+ public int getPinnedHeadsUpHeight() {
+ return getIntrinsicHeight();
+ }
+
+
+ /**
+ * Sets the translation of the view.
+ */
+ public void setTranslation(float translation) {
+ setTranslationX(translation);
+ }
+
+ /**
+ * Gets the translation of the view.
+ */
+ public float getTranslation() {
+ return getTranslationX();
+ }
+
+ public void onHeightReset() {
+ if (mOnHeightChangedListener != null) {
+ mOnHeightChangedListener.onReset(this);
+ }
+ }
+
+ /**
+ * 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. Since we are manually clipping this
+ * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
+ * ensure that accessibility and focusing work correctly.
+ *
+ * @param outRect The (scrolled) drawing bounds of the view.
+ */
+ @Override
+ public void getDrawingRect(Rect outRect) {
+ super.getDrawingRect(outRect);
+ outRect.left += getTranslationX();
+ outRect.right += getTranslationX();
+ outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
+ outRect.top += getTranslationY() + getClipTopAmount();
+ }
+
+ @Override
+ public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
+ super.getBoundsOnScreen(outRect, clipToParent);
+ if (getTop() + getTranslationY() < 0) {
+ // We got clipped to the parent here - make sure we undo that.
+ outRect.top += getTop() + getTranslationY();
+ }
+ outRect.bottom = outRect.top + getActualHeight();
+ outRect.top += getClipTopAmount();
+ }
+
+ public boolean isSummaryWithChildren() {
+ return false;
+ }
+
+ public boolean areChildrenExpanded() {
+ return false;
+ }
+
+ private void updateClipping() {
+ if (mClipToActualHeight) {
+ int top = getClipTopAmount();
+ mClipRect.set(0, top, getWidth(), Math.max(getActualHeight() + getExtraBottomPadding()
+ - mClipBottomAmount, top));
+ setClipBounds(mClipRect);
+ } else {
+ setClipBounds(null);
+ }
+ }
+
+ public void setClipToActualHeight(boolean clipToActualHeight) {
+ mClipToActualHeight = clipToActualHeight;
+ updateClipping();
+ }
+
+ public boolean willBeGone() {
+ return mWillBeGone;
+ }
+
+ public void setWillBeGone(boolean willBeGone) {
+ mWillBeGone = willBeGone;
+ }
+
+ public int getMinClipTopAmount() {
+ return mMinClipTopAmount;
+ }
+
+ public void setMinClipTopAmount(int minClipTopAmount) {
+ mMinClipTopAmount = minClipTopAmount;
+ }
+
+ @Override
+ public void setLayerType(int layerType, Paint paint) {
+ if (hasOverlappingRendering()) {
+ super.setLayerType(layerType, paint);
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ // Otherwise it will be clipped
+ return super.hasOverlappingRendering() && getActualHeight() <= getHeight();
+ }
+
+ public float getShadowAlpha() {
+ return 0.0f;
+ }
+
+ public void setShadowAlpha(float shadowAlpha) {
+ }
+
+ /**
+ * @return an amount between -1 and 1 of increased padding that this child needs. 1 means it
+ * needs a full increased padding while -1 means it needs no padding at all. For 0.0f the normal
+ * padding is applied.
+ */
+ public float getIncreasedPaddingAmount() {
+ return 0.0f;
+ }
+
+ public boolean mustStayOnScreen() {
+ return false;
+ }
+
+ public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
+ int outlineTranslation) {
+ }
+
+ public float getOutlineAlpha() {
+ return 0.0f;
+ }
+
+ public int getOutlineTranslation() {
+ return 0;
+ }
+
+ public void setChangingPosition(boolean changingPosition) {
+ mChangingPosition = changingPosition;
+ }
+
+ public boolean isChangingPosition() {
+ return mChangingPosition;
+ }
+
+ public void setTransientContainer(ViewGroup transientContainer) {
+ mTransientContainer = transientContainer;
+ }
+
+ public ViewGroup getTransientContainer() {
+ return mTransientContainer;
+ }
+
+ /**
+ * @return padding used to alter how much of the view is clipped.
+ */
+ public int getExtraBottomPadding() {
+ return 0;
+ }
+
+ /**
+ * @return true if the group's expansion state is changing, false otherwise.
+ */
+ public boolean isGroupExpansionChanging() {
+ return false;
+ }
+
+ public boolean isGroupExpanded() {
+ return false;
+ }
+
+ public boolean isChildInGroup() {
+ return false;
+ }
+
+ public void setActualHeightAnimating(boolean animating) {}
+
+ public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+ return new ExpandableViewState();
+ }
+
+ /**
+ * @return whether the current view doesn't add height to the overall content. This means that
+ * if it is added to a list of items, it's content will still have the same height.
+ * An example is the notification shelf, that is always placed on top of another view.
+ */
+ public boolean hasNoContentHeight() {
+ return false;
+ }
+
+ /**
+ * @param inShelf whether the view is currently fully in the notification shelf.
+ */
+ public void setInShelf(boolean inShelf) {
+ mInShelf = inShelf;
+ }
+
+ public boolean isInShelf() {
+ return mInShelf;
+ }
+
+ /**
+ * @param transformingInShelf whether the view is currently transforming into the shelf in an
+ * animated way
+ */
+ public void setTransformingInShelf(boolean transformingInShelf) {
+ mTransformingInShelf = transformingInShelf;
+ }
+
+ public boolean isTransformingIntoShelf() {
+ return mTransformingInShelf;
+ }
+
+ public boolean isAboveShelf() {
+ return false;
+ }
+
+ /**
+ * A listener notifying when {@link #getActualHeight} changes.
+ */
+ public interface OnHeightChangedListener {
+
+ /**
+ * @param view the view for which the height changed, or {@code null} if just the top
+ * padding or the padding between the elements changed
+ * @param needsAnimation whether the view height needs to be animated
+ */
+ void onHeightChanged(ExpandableView view, boolean needsAnimation);
+
+ /**
+ * Called when the view is reset and therefore the height will change abruptly
+ *
+ * @param view The view which was reset.
+ */
+ void onReset(ExpandableView view);
+ }
+}
diff --git a/com/android/systemui/statusbar/FlingAnimationUtils.java b/com/android/systemui/statusbar/FlingAnimationUtils.java
new file mode 100644
index 0000000..758fb7a
--- /dev/null
+++ b/com/android/systemui/statusbar/FlingAnimationUtils.java
@@ -0,0 +1,353 @@
+/*
+ * 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.content.Context;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+
+/**
+ * Utility class to calculate general fling animation when the finger is released.
+ */
+public class FlingAnimationUtils {
+
+ private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f;
+ private static final float LINEAR_OUT_SLOW_IN_X2_MAX = 0.68f;
+ private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f;
+ private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f;
+ private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f;
+ private static final float MIN_VELOCITY_DP_PER_SECOND = 250;
+ private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000;
+
+ private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 0.75f;
+ private final float mSpeedUpFactor;
+ private final float mY2;
+
+ private float mMinVelocityPxPerSecond;
+ private float mMaxLengthSeconds;
+ private float mHighVelocityPxPerSecond;
+ private float mLinearOutSlowInX2;
+
+ private AnimatorProperties mAnimatorProperties = new AnimatorProperties();
+ private PathInterpolator mInterpolator;
+ private float mCachedStartGradient = -1;
+ private float mCachedVelocityFactor = -1;
+
+ public FlingAnimationUtils(Context ctx, float maxLengthSeconds) {
+ this(ctx, maxLengthSeconds, 0.0f);
+ }
+
+ /**
+ * @param maxLengthSeconds the longest duration an animation can become in seconds
+ * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
+ * the end of the animation. 0 means it's at the beginning and no
+ * acceleration will take place.
+ */
+ public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor) {
+ this(ctx, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f);
+ }
+
+ /**
+ * @param maxLengthSeconds the longest duration an animation can become in seconds
+ * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
+ * the end of the animation. 0 means it's at the beginning and no
+ * acceleration will take place.
+ * @param x2 the x value to take for the second point of the bezier spline. If a value below 0
+ * is provided, the value is automatically calculated.
+ * @param y2 the y value to take for the second point of the bezier spline
+ */
+ public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor, float x2,
+ float y2) {
+ mMaxLengthSeconds = maxLengthSeconds;
+ mSpeedUpFactor = speedUpFactor;
+ if (x2 < 0) {
+ mLinearOutSlowInX2 = NotificationUtils.interpolate(LINEAR_OUT_SLOW_IN_X2,
+ LINEAR_OUT_SLOW_IN_X2_MAX,
+ mSpeedUpFactor);
+ } else {
+ mLinearOutSlowInX2 = x2;
+ }
+ mY2 = y2;
+
+ mMinVelocityPxPerSecond
+ = MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+ mHighVelocityPxPerSecond
+ = HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ */
+ public void apply(Animator animator, float currValue, float endValue, float velocity) {
+ apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ */
+ public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
+ float velocity) {
+ apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length
+ * gets multiplied by the ratio between the actual distance and this value
+ */
+ public void apply(Animator animator, float currValue, float endValue, float velocity,
+ float maxDistance) {
+ AnimatorProperties properties = getProperties(currValue, endValue, velocity,
+ maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length
+ * gets multiplied by the ratio between the actual distance and this value
+ */
+ public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
+ float velocity, float maxDistance) {
+ AnimatorProperties properties = getProperties(currValue, endValue, velocity,
+ maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ private AnimatorProperties getProperties(float currValue,
+ float endValue, float velocity, float maxDistance) {
+ float maxLengthSeconds = (float) (mMaxLengthSeconds
+ * Math.sqrt(Math.abs(endValue - currValue) / maxDistance));
+ float diff = Math.abs(endValue - currValue);
+ float velAbs = Math.abs(velocity);
+ float velocityFactor = mSpeedUpFactor == 0.0f
+ ? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f);
+ float startGradient = NotificationUtils.interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT,
+ mY2 / mLinearOutSlowInX2, velocityFactor);
+ float durationSeconds = startGradient * diff / velAbs;
+ Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor);
+ if (durationSeconds <= maxLengthSeconds) {
+ mAnimatorProperties.interpolator = slowInInterpolator;
+ } else if (velAbs >= mMinVelocityPxPerSecond) {
+
+ // Cross fade between fast-out-slow-in and linear interpolator with current velocity.
+ durationSeconds = maxLengthSeconds;
+ VelocityInterpolator velocityInterpolator
+ = new VelocityInterpolator(durationSeconds, velAbs, diff);
+ InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
+ velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN);
+ mAnimatorProperties.interpolator = superInterpolator;
+ } else {
+
+ // Just use a normal interpolator which doesn't take the velocity into account.
+ durationSeconds = maxLengthSeconds;
+ mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN;
+ }
+ mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+ return mAnimatorProperties;
+ }
+
+ private Interpolator getInterpolator(float startGradient, float velocityFactor) {
+ if (startGradient != mCachedStartGradient
+ || velocityFactor != mCachedVelocityFactor) {
+ float speedup = mSpeedUpFactor * (1.0f - velocityFactor);
+ mInterpolator = new PathInterpolator(speedup,
+ speedup * startGradient,
+ mLinearOutSlowInX2, mY2);
+ mCachedStartGradient = startGradient;
+ mCachedVelocityFactor = velocityFactor;
+ }
+ return mInterpolator;
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion for the case when the animation is making something
+ * disappear.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length
+ * gets multiplied by the ratio between the actual distance and this value
+ */
+ public void applyDismissing(Animator animator, float currValue, float endValue,
+ float velocity, float maxDistance) {
+ AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
+ maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion for the case when the animation is making something
+ * disappear.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length
+ * gets multiplied by the ratio between the actual distance and this value
+ */
+ public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue,
+ float velocity, float maxDistance) {
+ AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
+ maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ private AnimatorProperties getDismissingProperties(float currValue, float endValue,
+ float velocity, float maxDistance) {
+ float maxLengthSeconds = (float) (mMaxLengthSeconds
+ * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f));
+ float diff = Math.abs(endValue - currValue);
+ float velAbs = Math.abs(velocity);
+ float y2 = calculateLinearOutFasterInY2(velAbs);
+
+ float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2;
+ Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2);
+ float durationSeconds = startGradient * diff / velAbs;
+ if (durationSeconds <= maxLengthSeconds) {
+ mAnimatorProperties.interpolator = mLinearOutFasterIn;
+ } else if (velAbs >= mMinVelocityPxPerSecond) {
+
+ // Cross fade between linear-out-faster-in and linear interpolator with current
+ // velocity.
+ durationSeconds = maxLengthSeconds;
+ VelocityInterpolator velocityInterpolator
+ = new VelocityInterpolator(durationSeconds, velAbs, diff);
+ InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
+ velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN);
+ mAnimatorProperties.interpolator = superInterpolator;
+ } else {
+
+ // Just use a normal interpolator which doesn't take the velocity into account.
+ durationSeconds = maxLengthSeconds;
+ mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+ }
+ mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+ return mAnimatorProperties;
+ }
+
+ /**
+ * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
+ * velocity. The faster the velocity, the more "linear" the interpolator gets.
+ *
+ * @param velocity the velocity of the gesture.
+ * @return the y2 control point for a cubic bezier path interpolator
+ */
+ private float calculateLinearOutFasterInY2(float velocity) {
+ float t = (velocity - mMinVelocityPxPerSecond)
+ / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond);
+ t = Math.max(0, Math.min(1, t));
+ return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
+ }
+
+ /**
+ * @return the minimum velocity a gesture needs to have to be considered a fling
+ */
+ public float getMinVelocityPxPerSecond() {
+ return mMinVelocityPxPerSecond;
+ }
+
+ /**
+ * An interpolator which interpolates two interpolators with an interpolator.
+ */
+ private static final class InterpolatorInterpolator implements Interpolator {
+
+ private Interpolator mInterpolator1;
+ private Interpolator mInterpolator2;
+ private Interpolator mCrossfader;
+
+ InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2,
+ Interpolator crossfader) {
+ mInterpolator1 = interpolator1;
+ mInterpolator2 = interpolator2;
+ mCrossfader = crossfader;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ float t = mCrossfader.getInterpolation(input);
+ return (1 - t) * mInterpolator1.getInterpolation(input)
+ + t * mInterpolator2.getInterpolation(input);
+ }
+ }
+
+ /**
+ * An interpolator which interpolates with a fixed velocity.
+ */
+ private static final class VelocityInterpolator implements Interpolator {
+
+ private float mDurationSeconds;
+ private float mVelocity;
+ private float mDiff;
+
+ private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
+ mDurationSeconds = durationSeconds;
+ mVelocity = velocity;
+ mDiff = diff;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ float time = input * mDurationSeconds;
+ return time * mVelocity / mDiff;
+ }
+ }
+
+ private static class AnimatorProperties {
+ Interpolator interpolator;
+ long duration;
+ }
+
+}
diff --git a/com/android/systemui/statusbar/GestureRecorder.java b/com/android/systemui/statusbar/GestureRecorder.java
new file mode 100644
index 0000000..f2adaf0
--- /dev/null
+++ b/com/android/systemui/statusbar/GestureRecorder.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2012 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.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import java.io.BufferedWriter;
+import java.io.FileDescriptor;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.LinkedList;
+
+/**
+ * Convenience class for capturing gestures for later analysis.
+ */
+public class GestureRecorder {
+ public static final boolean DEBUG = true; // for now
+ public static final String TAG = GestureRecorder.class.getSimpleName();
+
+ public class Gesture {
+ public abstract class Record {
+ long time;
+ public abstract String toJson();
+ }
+ public class MotionEventRecord extends Record {
+ public MotionEvent event;
+ public MotionEventRecord(long when, MotionEvent event) {
+ this.time = when;
+ this.event = MotionEvent.obtain(event);
+ }
+ String actionName(int action) {
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ return "down";
+ case MotionEvent.ACTION_UP:
+ return "up";
+ case MotionEvent.ACTION_MOVE:
+ return "move";
+ case MotionEvent.ACTION_CANCEL:
+ return "cancel";
+ default:
+ return String.valueOf(action);
+ }
+ }
+ public String toJson() {
+ return String.format(
+ ("{\"type\":\"motion\", \"time\":%d, \"action\":\"%s\", "
+ + "\"x\":%.2f, \"y\":%.2f, \"s\":%.2f, \"p\":%.2f}"),
+ this.time,
+ actionName(this.event.getAction()),
+ this.event.getRawX(),
+ this.event.getRawY(),
+ this.event.getSize(),
+ this.event.getPressure()
+ );
+ }
+ }
+ public class TagRecord extends Record {
+ public String tag, info;
+ public TagRecord(long when, String tag, String info) {
+ this.time = when;
+ this.tag = tag;
+ this.info = info;
+ }
+ public String toJson() {
+ return String.format("{\"type\":\"tag\", \"time\":%d, \"tag\":\"%s\", \"info\":\"%s\"}",
+ this.time,
+ this.tag,
+ this.info
+ );
+ }
+ }
+ private LinkedList<Record> mRecords = new LinkedList<Record>();
+ private HashSet<String> mTags = new HashSet<String>();
+ long mDownTime = -1;
+ boolean mComplete = false;
+
+ public void add(MotionEvent ev) {
+ mRecords.add(new MotionEventRecord(ev.getEventTime(), ev));
+ if (mDownTime < 0) {
+ mDownTime = ev.getDownTime();
+ } else {
+ if (mDownTime != ev.getDownTime()) {
+ Log.w(TAG, "Assertion failure in GestureRecorder: event downTime ("
+ +ev.getDownTime()+") does not match gesture downTime ("+mDownTime+")");
+ }
+ }
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mComplete = true;
+ }
+ }
+ public void tag(long when, String tag, String info) {
+ mRecords.add(new TagRecord(when, tag, info));
+ mTags.add(tag);
+ }
+ public boolean isComplete() {
+ return mComplete;
+ }
+ public String toJson() {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ sb.append("[");
+ for (Record r : mRecords) {
+ if (!first) sb.append(", ");
+ first = false;
+ sb.append(r.toJson());
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+ }
+
+ // -=-=-=-=-=-=-=-=-=-=-=-
+
+ static final long SAVE_DELAY = 5000; // ms
+ static final int SAVE_MESSAGE = 6351;
+
+ private LinkedList<Gesture> mGestures;
+ private Gesture mCurrentGesture;
+ private int mLastSaveLen = -1;
+ private String mLogfile;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == SAVE_MESSAGE) {
+ save();
+ }
+ }
+ };
+
+ public GestureRecorder(String filename) {
+ mLogfile = filename;
+ mGestures = new LinkedList<Gesture>();
+ mCurrentGesture = null;
+ }
+
+ public void add(MotionEvent ev) {
+ synchronized (mGestures) {
+ if (mCurrentGesture == null || mCurrentGesture.isComplete()) {
+ mCurrentGesture = new Gesture();
+ mGestures.add(mCurrentGesture);
+ }
+ mCurrentGesture.add(ev);
+ }
+ saveLater();
+ }
+
+ public void tag(long when, String tag, String info) {
+ synchronized (mGestures) {
+ if (mCurrentGesture == null) {
+ mCurrentGesture = new Gesture();
+ mGestures.add(mCurrentGesture);
+ }
+ mCurrentGesture.tag(when, tag, info);
+ }
+ saveLater();
+ }
+
+ public void tag(long when, String tag) {
+ tag(when, tag, null);
+ }
+
+ public void tag(String tag) {
+ tag(SystemClock.uptimeMillis(), tag, null);
+ }
+
+ public void tag(String tag, String info) {
+ tag(SystemClock.uptimeMillis(), tag, info);
+ }
+
+ /**
+ * Generates a JSON string capturing all completed gestures.
+ * Not threadsafe; call with a lock.
+ */
+ public String toJsonLocked() {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ sb.append("[");
+ int count = 0;
+ for (Gesture g : mGestures) {
+ if (!g.isComplete()) continue;
+ if (!first) sb.append("," );
+ first = false;
+ sb.append(g.toJson());
+ count++;
+ }
+ mLastSaveLen = count;
+ sb.append("]");
+ return sb.toString();
+ }
+
+ public String toJson() {
+ String s;
+ synchronized (mGestures) {
+ s = toJsonLocked();
+ }
+ return s;
+ }
+
+ public void saveLater() {
+ mHandler.removeMessages(SAVE_MESSAGE);
+ mHandler.sendEmptyMessageDelayed(SAVE_MESSAGE, SAVE_DELAY);
+ }
+
+ public void save() {
+ synchronized (mGestures) {
+ try {
+ BufferedWriter w = new BufferedWriter(new FileWriter(mLogfile, /*append=*/ true));
+ w.append(toJsonLocked() + "\n");
+ w.close();
+ mGestures.clear();
+ // If we have a pending gesture, push it back
+ if (mCurrentGesture != null && !mCurrentGesture.isComplete()) {
+ mGestures.add(mCurrentGesture);
+ }
+ if (DEBUG) {
+ Log.v(TAG, String.format("Wrote %d complete gestures to %s", mLastSaveLen, mLogfile));
+ }
+ } catch (IOException e) {
+ Log.e(TAG, String.format("Couldn't write gestures to %s", mLogfile), e);
+ mLastSaveLen = -1;
+ }
+ }
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ save();
+ if (mLastSaveLen >= 0) {
+ pw.println(String.valueOf(mLastSaveLen) + " gestures written to " + mLogfile);
+ } else {
+ pw.println("error writing gestures");
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/InflationTask.java b/com/android/systemui/statusbar/InflationTask.java
new file mode 100644
index 0000000..22fd37c
--- /dev/null
+++ b/com/android/systemui/statusbar/InflationTask.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * An interface for running inflation tasks that allows aborting and superseding existing
+ * operations.
+ */
+public interface InflationTask {
+ void abort();
+
+ /**
+ * Supersedes an existing task. i.e another task was superceeded by this.
+ *
+ * @param task the task that was previously running
+ */
+ default void supersedeTask(InflationTask task) {}
+}
diff --git a/com/android/systemui/statusbar/KeyboardShortcutAppItemLayout.java b/com/android/systemui/statusbar/KeyboardShortcutAppItemLayout.java
new file mode 100644
index 0000000..5377dee
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyboardShortcutAppItemLayout.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+
+/**
+ * Layout used for displaying keyboard shortcut items inside an alert dialog.
+ * The layout sets the maxWidth of shortcuts keyword textview to 70% of available space.
+ */
+public class KeyboardShortcutAppItemLayout extends RelativeLayout {
+
+ private static final double MAX_WIDTH_PERCENT_FOR_KEYWORDS = 0.70;
+
+ public KeyboardShortcutAppItemLayout(Context context) {
+ super(context);
+ }
+
+ public KeyboardShortcutAppItemLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+ ImageView shortcutIcon = findViewById(R.id.keyboard_shortcuts_icon);
+ TextView shortcutKeyword = findViewById(R.id.keyboard_shortcuts_keyword);
+ int totalMeasuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int totalPadding = getPaddingLeft() + getPaddingRight();
+ int availableWidth = totalMeasuredWidth - totalPadding;
+ if (shortcutIcon.getVisibility() == View.VISIBLE) {
+ availableWidth = availableWidth - shortcutIcon.getMeasuredWidth();
+ }
+ shortcutKeyword.setMaxWidth((int)
+ Math.round(availableWidth * MAX_WIDTH_PERCENT_FOR_KEYWORDS));
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/com/android/systemui/statusbar/KeyboardShortcutKeysLayout.java b/com/android/systemui/statusbar/KeyboardShortcutKeysLayout.java
new file mode 100644
index 0000000..6746a67
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyboardShortcutKeysLayout.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2016 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.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Layout used as a container for keyboard shortcut keys. It's children are wrapped and right
+ * aligned.
+ */
+public final class KeyboardShortcutKeysLayout extends ViewGroup {
+ private int mLineHeight;
+ private final Context mContext;
+
+ public KeyboardShortcutKeysLayout(Context context) {
+ super(context);
+ this.mContext = context;
+ }
+
+ public KeyboardShortcutKeysLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ this.mContext = context;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
+ int childCount = getChildCount();
+ int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
+ int lineHeight = 0;
+ int xPos = getPaddingLeft();
+ int yPos = getPaddingTop();
+
+ int childHeightMeasureSpec;
+ if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+ } else {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+ child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+ childHeightMeasureSpec);
+ int childWidth = child.getMeasuredWidth();
+ lineHeight = Math.max(lineHeight,
+ child.getMeasuredHeight() + layoutParams.mVerticalSpacing);
+
+ if (xPos + childWidth > width) {
+ xPos = getPaddingLeft();
+ yPos += lineHeight;
+ }
+ xPos += childWidth + layoutParams.mHorizontalSpacing;
+ }
+ }
+ this.mLineHeight = lineHeight;
+
+ if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
+ height = yPos + lineHeight;
+ } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
+ if (yPos + lineHeight < height) {
+ height = yPos + lineHeight;
+ }
+ }
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ int spacing = getHorizontalVerticalSpacing();
+ return new LayoutParams(spacing, spacing);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams layoutParams) {
+ int spacing = getHorizontalVerticalSpacing();
+ return new LayoutParams(spacing, spacing, layoutParams);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return (p instanceof LayoutParams);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int childCount = getChildCount();
+ int fullRowWidth = r - l;
+ int xPos = isRTL()
+ ? fullRowWidth - getPaddingRight()
+ : getPaddingLeft();
+ int yPos = getPaddingTop();
+ int lastHorizontalSpacing = 0;
+ // The index of the child which starts the current row.
+ int rowStartIdx = 0;
+
+ // Go through all the children.
+ for (int i = 0; i < childCount; i++) {
+ View currentChild = getChildAt(i);
+ if (currentChild.getVisibility() != GONE) {
+ int currentChildWidth = currentChild.getMeasuredWidth();
+ LayoutParams lp = (LayoutParams) currentChild.getLayoutParams();
+
+ boolean childDoesNotFitOnRow = isRTL()
+ ? xPos - getPaddingLeft() - currentChildWidth < 0
+ : xPos + currentChildWidth > fullRowWidth;
+
+ if (childDoesNotFitOnRow) {
+ // Layout all the children on this row but the current one.
+ layoutChildrenOnRow(rowStartIdx, i, fullRowWidth, xPos, yPos,
+ lastHorizontalSpacing);
+ // Update the positions for starting on the new row.
+ xPos = isRTL()
+ ? fullRowWidth - getPaddingRight()
+ : getPaddingLeft();
+ yPos += mLineHeight;
+ rowStartIdx = i;
+ }
+
+ xPos = isRTL()
+ ? xPos - currentChildWidth - lp.mHorizontalSpacing
+ : xPos + currentChildWidth + lp.mHorizontalSpacing;
+ lastHorizontalSpacing = lp.mHorizontalSpacing;
+ }
+ }
+
+ // Lay out the children on the last row.
+ if (rowStartIdx < childCount) {
+ layoutChildrenOnRow(rowStartIdx, childCount, fullRowWidth, xPos, yPos,
+ lastHorizontalSpacing);
+ }
+ }
+
+ private int getHorizontalVerticalSpacing() {
+ DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ return (int) TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 4, displayMetrics);
+ }
+
+ private void layoutChildrenOnRow(int startIndex, int endIndex, int fullRowWidth, int xPos,
+ int yPos, int lastHorizontalSpacing) {
+ if (!isRTL()) {
+ xPos = getPaddingLeft() + fullRowWidth - xPos + lastHorizontalSpacing;
+ }
+
+ for (int j = startIndex; j < endIndex; ++j) {
+ View currentChild = getChildAt(j);
+ int currentChildWidth = currentChild.getMeasuredWidth();
+ LayoutParams lp = (LayoutParams) currentChild.getLayoutParams();
+ if (isRTL() && j == startIndex) {
+ xPos = fullRowWidth - xPos - getPaddingRight() - currentChildWidth
+ - lp.mHorizontalSpacing;
+ }
+
+ currentChild.layout(
+ xPos,
+ yPos,
+ xPos + currentChildWidth,
+ yPos + currentChild.getMeasuredHeight());
+
+ if (isRTL()) {
+ int nextChildWidth = j < endIndex - 1
+ ? getChildAt(j + 1).getMeasuredWidth()
+ : 0;
+ xPos -= nextChildWidth + lp.mHorizontalSpacing;
+ } else {
+ xPos += currentChildWidth + lp.mHorizontalSpacing;
+ }
+ }
+ }
+
+ private boolean isRTL() {
+ return mContext.getResources().getConfiguration().getLayoutDirection()
+ == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ public final int mHorizontalSpacing;
+ public final int mVerticalSpacing;
+
+ public LayoutParams(int horizontalSpacing, int verticalSpacing,
+ ViewGroup.LayoutParams viewGroupLayout) {
+ super(viewGroupLayout);
+ this.mHorizontalSpacing = horizontalSpacing;
+ this.mVerticalSpacing = verticalSpacing;
+ }
+
+ public LayoutParams(int mHorizontalSpacing, int verticalSpacing) {
+ super(0, 0);
+ this.mHorizontalSpacing = mHorizontalSpacing;
+ this.mVerticalSpacing = verticalSpacing;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/KeyboardShortcuts.java b/com/android/systemui/statusbar/KeyboardShortcuts.java
new file mode 100644
index 0000000..d370a63
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -0,0 +1,781 @@
+/*
+ * Copyright (C) 2016 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlertDialog;
+import android.app.AppGlobals;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Icon;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.KeyboardShortcutInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager.KeyboardShortcutsReceiver;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.internal.app.AssistUtils;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import static android.content.Context.LAYOUT_INFLATER_SERVICE;
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+
+/**
+ * Contains functionality for handling keyboard shortcuts.
+ */
+public final class KeyboardShortcuts {
+ private static final String TAG = KeyboardShortcuts.class.getSimpleName();
+ private static final Object sLock = new Object();
+ private static KeyboardShortcuts sInstance;
+
+ private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
+ private final SparseArray<String> mModifierNames = new SparseArray<>();
+ private final SparseArray<Drawable> mSpecialCharacterDrawables = new SparseArray<>();
+ private final SparseArray<Drawable> mModifierDrawables = new SparseArray<>();
+ // Ordered list of modifiers that are supported. All values in this array must exist in
+ // mModifierNames.
+ private final int[] mModifierList = new int[] {
+ KeyEvent.META_META_ON, KeyEvent.META_CTRL_ON, KeyEvent.META_ALT_ON,
+ KeyEvent.META_SHIFT_ON, KeyEvent.META_SYM_ON, KeyEvent.META_FUNCTION_ON
+ };
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Context mContext;
+ private final IPackageManager mPackageManager;
+ private final OnClickListener mDialogCloseListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dismissKeyboardShortcuts();
+ }
+ };
+ private final Comparator<KeyboardShortcutInfo> mApplicationItemsComparator =
+ new Comparator<KeyboardShortcutInfo>() {
+ @Override
+ public int compare(KeyboardShortcutInfo ksh1, KeyboardShortcutInfo ksh2) {
+ boolean ksh1ShouldBeLast = ksh1.getLabel() == null
+ || ksh1.getLabel().toString().isEmpty();
+ boolean ksh2ShouldBeLast = ksh2.getLabel() == null
+ || ksh2.getLabel().toString().isEmpty();
+ if (ksh1ShouldBeLast && ksh2ShouldBeLast) {
+ return 0;
+ }
+ if (ksh1ShouldBeLast) {
+ return 1;
+ }
+ if (ksh2ShouldBeLast) {
+ return -1;
+ }
+ return (ksh1.getLabel().toString()).compareToIgnoreCase(
+ ksh2.getLabel().toString());
+ }
+ };
+
+ private Dialog mKeyboardShortcutsDialog;
+ private KeyCharacterMap mKeyCharacterMap;
+ private KeyCharacterMap mBackupKeyCharacterMap;
+
+ private KeyboardShortcuts(Context context) {
+ this.mContext = new ContextThemeWrapper(context, android.R.style.Theme_DeviceDefault_Light);
+ this.mPackageManager = AppGlobals.getPackageManager();
+ loadResources(context);
+ }
+
+ private static KeyboardShortcuts getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new KeyboardShortcuts(context);
+ }
+ return sInstance;
+ }
+
+ public static void show(Context context, int deviceId) {
+ MetricsLogger.visible(context,
+ MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER);
+ synchronized (sLock) {
+ if (sInstance != null && !sInstance.mContext.equals(context)) {
+ dismiss();
+ }
+ getInstance(context).showKeyboardShortcuts(deviceId);
+ }
+ }
+
+ public static void toggle(Context context, int deviceId) {
+ synchronized (sLock) {
+ if (isShowing()) {
+ dismiss();
+ } else {
+ show(context, deviceId);
+ }
+ }
+ }
+
+ public static void dismiss() {
+ synchronized (sLock) {
+ if (sInstance != null) {
+ MetricsLogger.hidden(sInstance.mContext,
+ MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER);
+ sInstance.dismissKeyboardShortcuts();
+ sInstance = null;
+ }
+ }
+ }
+
+ private static boolean isShowing() {
+ return sInstance != null && sInstance.mKeyboardShortcutsDialog != null
+ && sInstance.mKeyboardShortcutsDialog.isShowing();
+ }
+
+ private void loadResources(Context context) {
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_HOME, context.getString(R.string.keyboard_key_home));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_BACK, context.getString(R.string.keyboard_key_back));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_UP, context.getString(R.string.keyboard_key_dpad_up));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_DOWN, context.getString(R.string.keyboard_key_dpad_down));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_LEFT, context.getString(R.string.keyboard_key_dpad_left));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_RIGHT, context.getString(R.string.keyboard_key_dpad_right));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_CENTER, context.getString(R.string.keyboard_key_dpad_center));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_PERIOD, ".");
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_TAB, context.getString(R.string.keyboard_key_tab));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_SPACE, context.getString(R.string.keyboard_key_space));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_ENTER, context.getString(R.string.keyboard_key_enter));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DEL, context.getString(R.string.keyboard_key_backspace));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
+ context.getString(R.string.keyboard_key_media_play_pause));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_MEDIA_STOP, context.getString(R.string.keyboard_key_media_stop));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_MEDIA_NEXT, context.getString(R.string.keyboard_key_media_next));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PREVIOUS,
+ context.getString(R.string.keyboard_key_media_previous));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_REWIND,
+ context.getString(R.string.keyboard_key_media_rewind));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
+ context.getString(R.string.keyboard_key_media_fast_forward));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_PAGE_UP, context.getString(R.string.keyboard_key_page_up));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_PAGE_DOWN, context.getString(R.string.keyboard_key_page_down));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_A,
+ context.getString(R.string.keyboard_key_button_template, "A"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_B,
+ context.getString(R.string.keyboard_key_button_template, "B"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_C,
+ context.getString(R.string.keyboard_key_button_template, "C"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_X,
+ context.getString(R.string.keyboard_key_button_template, "X"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Y,
+ context.getString(R.string.keyboard_key_button_template, "Y"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Z,
+ context.getString(R.string.keyboard_key_button_template, "Z"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L1,
+ context.getString(R.string.keyboard_key_button_template, "L1"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R1,
+ context.getString(R.string.keyboard_key_button_template, "R1"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L2,
+ context.getString(R.string.keyboard_key_button_template, "L2"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R2,
+ context.getString(R.string.keyboard_key_button_template, "R2"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_START,
+ context.getString(R.string.keyboard_key_button_template, "Start"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_SELECT,
+ context.getString(R.string.keyboard_key_button_template, "Select"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_MODE,
+ context.getString(R.string.keyboard_key_button_template, "Mode"));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_FORWARD_DEL, context.getString(R.string.keyboard_key_forward_del));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_ESCAPE, "Esc");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_SYSRQ, "SysRq");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BREAK, "Break");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_SCROLL_LOCK, "Scroll Lock");
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_MOVE_HOME, context.getString(R.string.keyboard_key_move_home));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_MOVE_END, context.getString(R.string.keyboard_key_move_end));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_INSERT, context.getString(R.string.keyboard_key_insert));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F1, "F1");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F2, "F2");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F3, "F3");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F4, "F4");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F5, "F5");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F6, "F6");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F7, "F7");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F8, "F8");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F9, "F9");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F10, "F10");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F11, "F11");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F12, "F12");
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_NUM_LOCK, context.getString(R.string.keyboard_key_num_lock));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_0,
+ context.getString(R.string.keyboard_key_numpad_template, "0"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_1,
+ context.getString(R.string.keyboard_key_numpad_template, "1"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_2,
+ context.getString(R.string.keyboard_key_numpad_template, "2"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_3,
+ context.getString(R.string.keyboard_key_numpad_template, "3"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_4,
+ context.getString(R.string.keyboard_key_numpad_template, "4"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_5,
+ context.getString(R.string.keyboard_key_numpad_template, "5"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_6,
+ context.getString(R.string.keyboard_key_numpad_template, "6"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_7,
+ context.getString(R.string.keyboard_key_numpad_template, "7"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_8,
+ context.getString(R.string.keyboard_key_numpad_template, "8"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_9,
+ context.getString(R.string.keyboard_key_numpad_template, "9"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DIVIDE,
+ context.getString(R.string.keyboard_key_numpad_template, "/"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
+ context.getString(R.string.keyboard_key_numpad_template, "*"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
+ context.getString(R.string.keyboard_key_numpad_template, "-"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ADD,
+ context.getString(R.string.keyboard_key_numpad_template, "+"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DOT,
+ context.getString(R.string.keyboard_key_numpad_template, "."));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_COMMA,
+ context.getString(R.string.keyboard_key_numpad_template, ","));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ENTER,
+ context.getString(R.string.keyboard_key_numpad_template,
+ context.getString(R.string.keyboard_key_enter)));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_EQUALS,
+ context.getString(R.string.keyboard_key_numpad_template, "="));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_LEFT_PAREN,
+ context.getString(R.string.keyboard_key_numpad_template, "("));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN,
+ context.getString(R.string.keyboard_key_numpad_template, ")"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_ZENKAKU_HANKAKU, "半角/全角");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_EISU, "英数");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変換");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変換");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "かな");
+
+ mModifierNames.put(KeyEvent.META_META_ON, "Meta");
+ mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl");
+ mModifierNames.put(KeyEvent.META_ALT_ON, "Alt");
+ mModifierNames.put(KeyEvent.META_SHIFT_ON, "Shift");
+ mModifierNames.put(KeyEvent.META_SYM_ON, "Sym");
+ mModifierNames.put(KeyEvent.META_FUNCTION_ON, "Fn");
+
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DEL, context.getDrawable(R.drawable.ic_ksh_key_backspace));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_ENTER, context.getDrawable(R.drawable.ic_ksh_key_enter));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DPAD_UP, context.getDrawable(R.drawable.ic_ksh_key_up));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DPAD_RIGHT, context.getDrawable(R.drawable.ic_ksh_key_right));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DPAD_DOWN, context.getDrawable(R.drawable.ic_ksh_key_down));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DPAD_LEFT, context.getDrawable(R.drawable.ic_ksh_key_left));
+
+ mModifierDrawables.put(
+ KeyEvent.META_META_ON, context.getDrawable(R.drawable.ic_ksh_key_meta));
+ }
+
+ /**
+ * Retrieves a {@link KeyCharacterMap} and assigns it to mKeyCharacterMap. If the given id is an
+ * existing device, that device's map is used. Otherwise, it checks first all available devices
+ * and if there is a full keyboard it uses that map, otherwise falls back to the Virtual
+ * Keyboard with its default map.
+ */
+ private void retrieveKeyCharacterMap(int deviceId) {
+ final InputManager inputManager = InputManager.getInstance();
+ mBackupKeyCharacterMap = inputManager.getInputDevice(-1).getKeyCharacterMap();
+ if (deviceId != -1) {
+ final InputDevice inputDevice = inputManager.getInputDevice(deviceId);
+ if (inputDevice != null) {
+ mKeyCharacterMap = inputDevice.getKeyCharacterMap();
+ return;
+ }
+ }
+ final int[] deviceIds = inputManager.getInputDeviceIds();
+ for (int i = 0; i < deviceIds.length; ++i) {
+ final InputDevice inputDevice = inputManager.getInputDevice(deviceIds[i]);
+ // -1 is the Virtual Keyboard, with the default key map. Use that one only as last
+ // resort.
+ if (inputDevice.getId() != -1 && inputDevice.isFullKeyboard()) {
+ mKeyCharacterMap = inputDevice.getKeyCharacterMap();
+ return;
+ }
+ }
+ // Fall back to -1, the virtual keyboard.
+ mKeyCharacterMap = mBackupKeyCharacterMap;
+ }
+
+ private void showKeyboardShortcuts(int deviceId) {
+ retrieveKeyCharacterMap(deviceId);
+ SystemServicesProxy.getInstance(mContext).requestKeyboardShortcuts(mContext,
+ new KeyboardShortcutsReceiver() {
+ @Override
+ public void onKeyboardShortcutsReceived(
+ final List<KeyboardShortcutGroup> result) {
+ result.add(getSystemShortcuts());
+ final KeyboardShortcutGroup appShortcuts = getDefaultApplicationShortcuts();
+ if (appShortcuts != null) {
+ result.add(appShortcuts);
+ }
+ showKeyboardShortcutsDialog(result);
+ }
+ }, deviceId);
+ }
+
+ private void dismissKeyboardShortcuts() {
+ if (mKeyboardShortcutsDialog != null) {
+ mKeyboardShortcutsDialog.dismiss();
+ mKeyboardShortcutsDialog = null;
+ }
+ }
+
+ private KeyboardShortcutGroup getSystemShortcuts() {
+ final KeyboardShortcutGroup systemGroup = new KeyboardShortcutGroup(
+ mContext.getString(R.string.keyboard_shortcut_group_system), true);
+ systemGroup.addItem(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_system_home),
+ KeyEvent.KEYCODE_ENTER,
+ KeyEvent.META_META_ON));
+ systemGroup.addItem(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_system_back),
+ KeyEvent.KEYCODE_DEL,
+ KeyEvent.META_META_ON));
+ systemGroup.addItem(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_system_recents),
+ KeyEvent.KEYCODE_TAB,
+ KeyEvent.META_ALT_ON));
+ systemGroup.addItem(new KeyboardShortcutInfo(
+ mContext.getString(
+ R.string.keyboard_shortcut_group_system_notifications),
+ KeyEvent.KEYCODE_N,
+ KeyEvent.META_META_ON));
+ systemGroup.addItem(new KeyboardShortcutInfo(
+ mContext.getString(
+ R.string.keyboard_shortcut_group_system_shortcuts_helper),
+ KeyEvent.KEYCODE_SLASH,
+ KeyEvent.META_META_ON));
+ systemGroup.addItem(new KeyboardShortcutInfo(
+ mContext.getString(
+ R.string.keyboard_shortcut_group_system_switch_input),
+ KeyEvent.KEYCODE_SPACE,
+ KeyEvent.META_META_ON));
+ return systemGroup;
+ }
+
+ private KeyboardShortcutGroup getDefaultApplicationShortcuts() {
+ final int userId = mContext.getUserId();
+ List<KeyboardShortcutInfo> keyboardShortcutInfoAppItems = new ArrayList<>();
+
+ // Assist.
+ final AssistUtils assistUtils = new AssistUtils(mContext);
+ final ComponentName assistComponent = assistUtils.getAssistComponentForUser(userId);
+ PackageInfo assistPackageInfo = null;
+ try {
+ assistPackageInfo = mPackageManager.getPackageInfo(
+ assistComponent.getPackageName(), 0, userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "PackageManagerService is dead");
+ }
+
+ if (assistPackageInfo != null) {
+ final Icon assistIcon = Icon.createWithResource(
+ assistPackageInfo.applicationInfo.packageName,
+ assistPackageInfo.applicationInfo.icon);
+
+ keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_applications_assist),
+ assistIcon,
+ KeyEvent.KEYCODE_UNKNOWN,
+ KeyEvent.META_META_ON));
+ }
+
+ // Browser.
+ final Icon browserIcon = getIconForIntentCategory(Intent.CATEGORY_APP_BROWSER, userId);
+ if (browserIcon != null) {
+ keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+ browserIcon,
+ KeyEvent.KEYCODE_B,
+ KeyEvent.META_META_ON));
+ }
+
+
+ // Contacts.
+ final Icon contactsIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CONTACTS, userId);
+ if (contactsIcon != null) {
+ keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
+ contactsIcon,
+ KeyEvent.KEYCODE_C,
+ KeyEvent.META_META_ON));
+ }
+
+ // Email.
+ final Icon emailIcon = getIconForIntentCategory(Intent.CATEGORY_APP_EMAIL, userId);
+ if (emailIcon != null) {
+ keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_applications_email),
+ emailIcon,
+ KeyEvent.KEYCODE_E,
+ KeyEvent.META_META_ON));
+ }
+
+ // Messaging.
+ final Icon messagingIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MESSAGING, userId);
+ if (messagingIcon != null) {
+ keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
+ messagingIcon,
+ KeyEvent.KEYCODE_S,
+ KeyEvent.META_META_ON));
+ }
+
+ // Music.
+ final Icon musicIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MUSIC, userId);
+ if (musicIcon != null) {
+ keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_applications_music),
+ musicIcon,
+ KeyEvent.KEYCODE_P,
+ KeyEvent.META_META_ON));
+ }
+
+ // Calendar.
+ final Icon calendarIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CALENDAR, userId);
+ if (calendarIcon != null) {
+ keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+ mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
+ calendarIcon,
+ KeyEvent.KEYCODE_L,
+ KeyEvent.META_META_ON));
+ }
+
+ final int itemsSize = keyboardShortcutInfoAppItems.size();
+ if (itemsSize == 0) {
+ return null;
+ }
+
+ // Sorts by label, case insensitive with nulls and/or empty labels last.
+ Collections.sort(keyboardShortcutInfoAppItems, mApplicationItemsComparator);
+ return new KeyboardShortcutGroup(
+ mContext.getString(R.string.keyboard_shortcut_group_applications),
+ keyboardShortcutInfoAppItems,
+ true);
+ }
+
+ private Icon getIconForIntentCategory(String intentCategory, int userId) {
+ final Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(intentCategory);
+
+ final PackageInfo packageInfo = getPackageInfoForIntent(intent, userId);
+ if (packageInfo != null && packageInfo.applicationInfo.icon != 0) {
+ return Icon.createWithResource(
+ packageInfo.applicationInfo.packageName,
+ packageInfo.applicationInfo.icon);
+ }
+ return null;
+ }
+
+ private PackageInfo getPackageInfoForIntent(Intent intent, int userId) {
+ try {
+ ResolveInfo handler;
+ handler = mPackageManager.resolveIntent(
+ intent, intent.resolveTypeIfNeeded(mContext.getContentResolver()), 0, userId);
+ if (handler == null || handler.activityInfo == null) {
+ return null;
+ }
+ return mPackageManager.getPackageInfo(handler.activityInfo.packageName, 0, userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "PackageManagerService is dead", e);
+ return null;
+ }
+ }
+
+ private void showKeyboardShortcutsDialog(
+ final List<KeyboardShortcutGroup> keyboardShortcutGroups) {
+ // Need to post on the main thread.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ handleShowKeyboardShortcuts(keyboardShortcutGroups);
+ }
+ });
+ }
+
+ private void handleShowKeyboardShortcuts(List<KeyboardShortcutGroup> keyboardShortcutGroups) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+ LAYOUT_INFLATER_SERVICE);
+ final View keyboardShortcutsView = inflater.inflate(
+ R.layout.keyboard_shortcuts_view, null);
+ populateKeyboardShortcuts((LinearLayout) keyboardShortcutsView.findViewById(
+ R.id.keyboard_shortcuts_container), keyboardShortcutGroups);
+ dialogBuilder.setView(keyboardShortcutsView);
+ dialogBuilder.setPositiveButton(R.string.quick_settings_done, mDialogCloseListener);
+ mKeyboardShortcutsDialog = dialogBuilder.create();
+ mKeyboardShortcutsDialog.setCanceledOnTouchOutside(true);
+ Window keyboardShortcutsWindow = mKeyboardShortcutsDialog.getWindow();
+ keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
+ synchronized (sLock) {
+ // showKeyboardShortcutsDialog only if it has not been dismissed already
+ if (sInstance != null) {
+ mKeyboardShortcutsDialog.show();
+ }
+ }
+ }
+
+ private void populateKeyboardShortcuts(LinearLayout keyboardShortcutsLayout,
+ List<KeyboardShortcutGroup> keyboardShortcutGroups) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ final int keyboardShortcutGroupsSize = keyboardShortcutGroups.size();
+ TextView shortcutsKeyView = (TextView) inflater.inflate(
+ R.layout.keyboard_shortcuts_key_view, null, false);
+ shortcutsKeyView.measure(
+ View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ final int shortcutKeyTextItemMinWidth = shortcutsKeyView.getMeasuredHeight();
+ // Needed to be able to scale the image items to the same height as the text items.
+ final int shortcutKeyIconItemHeightWidth = shortcutsKeyView.getMeasuredHeight()
+ - shortcutsKeyView.getPaddingTop()
+ - shortcutsKeyView.getPaddingBottom();
+ for (int i = 0; i < keyboardShortcutGroupsSize; i++) {
+ KeyboardShortcutGroup group = keyboardShortcutGroups.get(i);
+ TextView categoryTitle = (TextView) inflater.inflate(
+ R.layout.keyboard_shortcuts_category_title, keyboardShortcutsLayout, false);
+ categoryTitle.setText(group.getLabel());
+ categoryTitle.setTextColor(group.isSystemGroup()
+ ? Utils.getColorAccent(mContext)
+ : mContext.getColor(R.color.ksh_application_group_color));
+ keyboardShortcutsLayout.addView(categoryTitle);
+
+ LinearLayout shortcutContainer = (LinearLayout) inflater.inflate(
+ R.layout.keyboard_shortcuts_container, keyboardShortcutsLayout, false);
+ final int itemsSize = group.getItems().size();
+ for (int j = 0; j < itemsSize; j++) {
+ KeyboardShortcutInfo info = group.getItems().get(j);
+ List<StringDrawableContainer> shortcutKeys = getHumanReadableShortcutKeys(info);
+ if (shortcutKeys == null) {
+ // Ignore shortcuts we can't display keys for.
+ Log.w(TAG, "Keyboard Shortcut contains unsupported keys, skipping.");
+ continue;
+ }
+ View shortcutView = inflater.inflate(R.layout.keyboard_shortcut_app_item,
+ shortcutContainer, false);
+
+ if (info.getIcon() != null) {
+ ImageView shortcutIcon = (ImageView) shortcutView
+ .findViewById(R.id.keyboard_shortcuts_icon);
+ shortcutIcon.setImageIcon(info.getIcon());
+ shortcutIcon.setVisibility(View.VISIBLE);
+ }
+
+ TextView shortcutKeyword = (TextView) shortcutView
+ .findViewById(R.id.keyboard_shortcuts_keyword);
+ shortcutKeyword.setText(info.getLabel());
+ if (info.getIcon() != null) {
+ RelativeLayout.LayoutParams lp =
+ (RelativeLayout.LayoutParams) shortcutKeyword.getLayoutParams();
+ lp.removeRule(RelativeLayout.ALIGN_PARENT_START);
+ shortcutKeyword.setLayoutParams(lp);
+ }
+
+ ViewGroup shortcutItemsContainer = (ViewGroup) shortcutView
+ .findViewById(R.id.keyboard_shortcuts_item_container);
+ final int shortcutKeysSize = shortcutKeys.size();
+ for (int k = 0; k < shortcutKeysSize; k++) {
+ StringDrawableContainer shortcutRepresentation = shortcutKeys.get(k);
+ if (shortcutRepresentation.mDrawable != null) {
+ ImageView shortcutKeyIconView = (ImageView) inflater.inflate(
+ R.layout.keyboard_shortcuts_key_icon_view, shortcutItemsContainer,
+ false);
+ Bitmap bitmap = Bitmap.createBitmap(shortcutKeyIconItemHeightWidth,
+ shortcutKeyIconItemHeightWidth, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ shortcutRepresentation.mDrawable.setBounds(0, 0, canvas.getWidth(),
+ canvas.getHeight());
+ shortcutRepresentation.mDrawable.draw(canvas);
+ shortcutKeyIconView.setImageBitmap(bitmap);
+ shortcutKeyIconView.setImportantForAccessibility(
+ IMPORTANT_FOR_ACCESSIBILITY_YES);
+ shortcutKeyIconView.setAccessibilityDelegate(
+ new ShortcutKeyAccessibilityDelegate(
+ shortcutRepresentation.mString));
+ shortcutItemsContainer.addView(shortcutKeyIconView);
+ } else if (shortcutRepresentation.mString != null) {
+ TextView shortcutKeyTextView = (TextView) inflater.inflate(
+ R.layout.keyboard_shortcuts_key_view, shortcutItemsContainer,
+ false);
+ shortcutKeyTextView.setMinimumWidth(shortcutKeyTextItemMinWidth);
+ shortcutKeyTextView.setText(shortcutRepresentation.mString);
+ shortcutKeyTextView.setAccessibilityDelegate(
+ new ShortcutKeyAccessibilityDelegate(
+ shortcutRepresentation.mString));
+ shortcutItemsContainer.addView(shortcutKeyTextView);
+ }
+ }
+ shortcutContainer.addView(shortcutView);
+ }
+ keyboardShortcutsLayout.addView(shortcutContainer);
+ if (i < keyboardShortcutGroupsSize - 1) {
+ View separator = inflater.inflate(
+ R.layout.keyboard_shortcuts_category_separator, keyboardShortcutsLayout,
+ false);
+ keyboardShortcutsLayout.addView(separator);
+ }
+ }
+ }
+
+ private List<StringDrawableContainer> getHumanReadableShortcutKeys(KeyboardShortcutInfo info) {
+ List<StringDrawableContainer> shortcutKeys = getHumanReadableModifiers(info);
+ if (shortcutKeys == null) {
+ return null;
+ }
+ String shortcutKeyString = null;
+ Drawable shortcutKeyDrawable = null;
+ if (info.getBaseCharacter() > Character.MIN_VALUE) {
+ shortcutKeyString = String.valueOf(info.getBaseCharacter());
+ } else if (mSpecialCharacterDrawables.get(info.getKeycode()) != null) {
+ shortcutKeyDrawable = mSpecialCharacterDrawables.get(info.getKeycode());
+ shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
+ } else if (mSpecialCharacterNames.get(info.getKeycode()) != null) {
+ shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
+ } else {
+ // Special case for shortcuts with no base key or keycode.
+ if (info.getKeycode() == KeyEvent.KEYCODE_UNKNOWN) {
+ return shortcutKeys;
+ }
+ char displayLabel = mKeyCharacterMap.getDisplayLabel(info.getKeycode());
+ if (displayLabel != 0) {
+ shortcutKeyString = String.valueOf(displayLabel);
+ } else {
+ displayLabel = mBackupKeyCharacterMap.getDisplayLabel(info.getKeycode());
+ if (displayLabel != 0) {
+ shortcutKeyString = String.valueOf(displayLabel);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ if (shortcutKeyString != null) {
+ shortcutKeys.add(new StringDrawableContainer(shortcutKeyString, shortcutKeyDrawable));
+ } else {
+ Log.w(TAG, "Keyboard Shortcut does not have a text representation, skipping.");
+ }
+
+ return shortcutKeys;
+ }
+
+ private List<StringDrawableContainer> getHumanReadableModifiers(KeyboardShortcutInfo info) {
+ final List<StringDrawableContainer> shortcutKeys = new ArrayList<>();
+ int modifiers = info.getModifiers();
+ if (modifiers == 0) {
+ return shortcutKeys;
+ }
+ for(int i = 0; i < mModifierList.length; ++i) {
+ final int supportedModifier = mModifierList[i];
+ if ((modifiers & supportedModifier) != 0) {
+ shortcutKeys.add(new StringDrawableContainer(
+ mModifierNames.get(supportedModifier),
+ mModifierDrawables.get(supportedModifier)));
+ modifiers &= ~supportedModifier;
+ }
+ }
+ if (modifiers != 0) {
+ // Remaining unsupported modifiers, don't show anything.
+ return null;
+ }
+ return shortcutKeys;
+ }
+
+ private final class ShortcutKeyAccessibilityDelegate extends AccessibilityDelegate {
+ private String mContentDescription;
+
+ ShortcutKeyAccessibilityDelegate(String contentDescription) {
+ mContentDescription = contentDescription;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (mContentDescription != null) {
+ info.setContentDescription(mContentDescription.toLowerCase());
+ }
+ }
+ }
+
+ private static final class StringDrawableContainer {
+ @NonNull
+ public String mString;
+ @Nullable
+ public Drawable mDrawable;
+
+ StringDrawableContainer(String string, Drawable drawable) {
+ mString = string;
+ mDrawable = drawable;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java b/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
new file mode 100644
index 0000000..8a5bece
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Receiver for the Keyboard Shortcuts Helper.
+ */
+public class KeyboardShortcutsReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
+ KeyboardShortcuts.show(context, -1 /* deviceId unknown */);
+ } else if (Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
+ KeyboardShortcuts.dismiss();
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/KeyguardAffordanceView.java b/com/android/systemui/statusbar/KeyguardAffordanceView.java
new file mode 100644
index 0000000..e12b574
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyguardAffordanceView.java
@@ -0,0 +1,564 @@
+/*
+ * 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.ArgbEvaluator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.DisplayListCanvas;
+import android.view.RenderNodeAnimator;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.KeyguardAffordanceHelper;
+
+/**
+ * An ImageView which does not have overlapping renderings commands and therefore does not need a
+ * layer when alpha is changed.
+ */
+public class KeyguardAffordanceView extends ImageView {
+
+ private static final long CIRCLE_APPEAR_DURATION = 80;
+ private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200;
+ private static final long NORMAL_ANIMATION_DURATION = 200;
+ public static final float MAX_ICON_SCALE_AMOUNT = 1.5f;
+ public static final float MIN_ICON_SCALE_AMOUNT = 0.8f;
+
+ private final int mMinBackgroundRadius;
+ private final Paint mCirclePaint;
+ private final int mDarkIconColor;
+ private final int mNormalColor;
+ private final ArgbEvaluator mColorInterpolator;
+ private final FlingAnimationUtils mFlingAnimationUtils;
+ private float mCircleRadius;
+ private int mCenterX;
+ private int mCenterY;
+ private ValueAnimator mCircleAnimator;
+ private ValueAnimator mAlphaAnimator;
+ private ValueAnimator mScaleAnimator;
+ private float mCircleStartValue;
+ private boolean mCircleWillBeHidden;
+ private int[] mTempPoint = new int[2];
+ private float mImageScale = 1f;
+ private int mCircleColor;
+ private boolean mIsLeft;
+ private View mPreviewView;
+ private float mCircleStartRadius;
+ private float mMaxCircleSize;
+ private Animator mPreviewClipper;
+ private float mRestingAlpha = KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT;
+ private boolean mSupportHardware;
+ private boolean mFinishing;
+ private boolean mLaunchingAffordance;
+ private boolean mShouldTint = true;
+
+ private CanvasProperty<Float> mHwCircleRadius;
+ private CanvasProperty<Float> mHwCenterX;
+ private CanvasProperty<Float> mHwCenterY;
+ private CanvasProperty<Paint> mHwCirclePaint;
+
+ private AnimatorListenerAdapter mClipEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPreviewClipper = null;
+ }
+ };
+ private AnimatorListenerAdapter mCircleEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCircleAnimator = null;
+ }
+ };
+ private AnimatorListenerAdapter mScaleEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mScaleAnimator = null;
+ }
+ };
+ private AnimatorListenerAdapter mAlphaEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAlphaAnimator = null;
+ }
+ };
+
+ public KeyguardAffordanceView(Context context) {
+ this(context, null);
+ }
+
+ public KeyguardAffordanceView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ TypedArray a = context.obtainStyledAttributes(attrs, android.R.styleable.ImageView);
+
+ mCirclePaint = new Paint();
+ mCirclePaint.setAntiAlias(true);
+ mCircleColor = 0xffffffff;
+ mCirclePaint.setColor(mCircleColor);
+
+ mNormalColor = a.getColor(android.R.styleable.ImageView_tint, 0xffffffff);
+ mDarkIconColor = 0xff000000;
+ mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
+ R.dimen.keyguard_affordance_min_background_radius);
+ mColorInterpolator = new ArgbEvaluator();
+ mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.3f);
+
+ a.recycle();
+ }
+
+ public void setImageDrawable(@Nullable Drawable drawable, boolean tint) {
+ super.setImageDrawable(drawable);
+ mShouldTint = tint;
+ updateIconColor();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mCenterX = getWidth() / 2;
+ mCenterY = getHeight() / 2;
+ mMaxCircleSize = getMaxCircleSize();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ mSupportHardware = canvas.isHardwareAccelerated();
+ drawBackgroundCircle(canvas);
+ canvas.save();
+ canvas.scale(mImageScale, mImageScale, getWidth() / 2, getHeight() / 2);
+ super.onDraw(canvas);
+ canvas.restore();
+ }
+
+ public void setPreviewView(View v) {
+ View oldPreviewView = mPreviewView;
+ mPreviewView = v;
+ if (mPreviewView != null) {
+ mPreviewView.setVisibility(mLaunchingAffordance
+ ? oldPreviewView.getVisibility() : INVISIBLE);
+ }
+ }
+
+ private void updateIconColor() {
+ if (!mShouldTint) return;
+ Drawable drawable = getDrawable().mutate();
+ float alpha = mCircleRadius / mMinBackgroundRadius;
+ alpha = Math.min(1.0f, alpha);
+ int color = (int) mColorInterpolator.evaluate(alpha, mNormalColor, mDarkIconColor);
+ drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+ }
+
+ private void drawBackgroundCircle(Canvas canvas) {
+ if (mCircleRadius > 0 || mFinishing) {
+ if (mFinishing && mSupportHardware && mHwCenterX != null) {
+ // Our hardware drawing proparties can be null if the finishing started but we have
+ // never drawn before. In that case we are not doing a render thread animation
+ // anyway, so we need to use the normal drawing.
+ DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas;
+ displayListCanvas.drawCircle(mHwCenterX, mHwCenterY, mHwCircleRadius,
+ mHwCirclePaint);
+ } else {
+ updateCircleColor();
+ canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint);
+ }
+ }
+ }
+
+ private void updateCircleColor() {
+ float fraction = 0.5f + 0.5f * Math.max(0.0f, Math.min(1.0f,
+ (mCircleRadius - mMinBackgroundRadius) / (0.5f * mMinBackgroundRadius)));
+ if (mPreviewView != null && mPreviewView.getVisibility() == VISIBLE) {
+ float finishingFraction = 1 - Math.max(0, mCircleRadius - mCircleStartRadius)
+ / (mMaxCircleSize - mCircleStartRadius);
+ fraction *= finishingFraction;
+ }
+ int color = Color.argb((int) (Color.alpha(mCircleColor) * fraction),
+ Color.red(mCircleColor),
+ Color.green(mCircleColor), Color.blue(mCircleColor));
+ mCirclePaint.setColor(color);
+ }
+
+ public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) {
+ cancelAnimator(mCircleAnimator);
+ cancelAnimator(mPreviewClipper);
+ mFinishing = true;
+ mCircleStartRadius = mCircleRadius;
+ final float maxCircleSize = getMaxCircleSize();
+ Animator animatorToRadius;
+ if (mSupportHardware) {
+ initHwProperties();
+ animatorToRadius = getRtAnimatorToRadius(maxCircleSize);
+ startRtAlphaFadeIn();
+ } else {
+ animatorToRadius = getAnimatorToRadius(maxCircleSize);
+ }
+ mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize,
+ velocity, maxCircleSize);
+ animatorToRadius.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimationEndRunnable.run();
+ mFinishing = false;
+ mCircleRadius = maxCircleSize;
+ invalidate();
+ }
+ });
+ animatorToRadius.start();
+ setImageAlpha(0, true);
+ if (mPreviewView != null) {
+ mPreviewView.setVisibility(View.VISIBLE);
+ mPreviewClipper = ViewAnimationUtils.createCircularReveal(
+ mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
+ maxCircleSize);
+ mFlingAnimationUtils.applyDismissing(mPreviewClipper, mCircleRadius, maxCircleSize,
+ velocity, maxCircleSize);
+ mPreviewClipper.addListener(mClipEndListener);
+ mPreviewClipper.start();
+ if (mSupportHardware) {
+ startRtCircleFadeOut(animatorToRadius.getDuration());
+ }
+ }
+ }
+
+ /**
+ * Fades in the Circle on the RenderThread. It's used when finishing the circle when it had
+ * alpha 0 in the beginning.
+ */
+ private void startRtAlphaFadeIn() {
+ if (mCircleRadius == 0 && mPreviewView == null) {
+ Paint modifiedPaint = new Paint(mCirclePaint);
+ modifiedPaint.setColor(mCircleColor);
+ modifiedPaint.setAlpha(0);
+ mHwCirclePaint = CanvasProperty.createPaint(modifiedPaint);
+ RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
+ RenderNodeAnimator.PAINT_ALPHA, 255);
+ animator.setTarget(this);
+ animator.setInterpolator(Interpolators.ALPHA_IN);
+ animator.setDuration(250);
+ animator.start();
+ }
+ }
+
+ public void instantFinishAnimation() {
+ cancelAnimator(mPreviewClipper);
+ if (mPreviewView != null) {
+ mPreviewView.setClipBounds(null);
+ mPreviewView.setVisibility(View.VISIBLE);
+ }
+ mCircleRadius = getMaxCircleSize();
+ setImageAlpha(0, false);
+ invalidate();
+ }
+
+ private void startRtCircleFadeOut(long duration) {
+ RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
+ RenderNodeAnimator.PAINT_ALPHA, 0);
+ animator.setDuration(duration);
+ animator.setInterpolator(Interpolators.ALPHA_OUT);
+ animator.setTarget(this);
+ animator.start();
+ }
+
+ private Animator getRtAnimatorToRadius(float circleRadius) {
+ RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius);
+ animator.setTarget(this);
+ return animator;
+ }
+
+ private void initHwProperties() {
+ mHwCenterX = CanvasProperty.createFloat(mCenterX);
+ mHwCenterY = CanvasProperty.createFloat(mCenterY);
+ mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint);
+ mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius);
+ }
+
+ private float getMaxCircleSize() {
+ getLocationInWindow(mTempPoint);
+ float rootWidth = getRootView().getWidth();
+ float width = mTempPoint[0] + mCenterX;
+ width = Math.max(rootWidth - width, width);
+ float height = mTempPoint[1] + mCenterY;
+ return (float) Math.hypot(width, height);
+ }
+
+ public void setCircleRadius(float circleRadius) {
+ setCircleRadius(circleRadius, false, false);
+ }
+
+ public void setCircleRadius(float circleRadius, boolean slowAnimation) {
+ setCircleRadius(circleRadius, slowAnimation, false);
+ }
+
+ public void setCircleRadiusWithoutAnimation(float circleRadius) {
+ cancelAnimator(mCircleAnimator);
+ setCircleRadius(circleRadius, false ,true);
+ }
+
+ private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
+
+ // Check if we need a new animation
+ boolean radiusHidden = (mCircleAnimator != null && mCircleWillBeHidden)
+ || (mCircleAnimator == null && mCircleRadius == 0.0f);
+ boolean nowHidden = circleRadius == 0.0f;
+ boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
+ if (!radiusNeedsAnimation) {
+ if (mCircleAnimator == null) {
+ mCircleRadius = circleRadius;
+ updateIconColor();
+ invalidate();
+ if (nowHidden) {
+ if (mPreviewView != null) {
+ mPreviewView.setVisibility(View.INVISIBLE);
+ }
+ }
+ } else if (!mCircleWillBeHidden) {
+
+ // We just update the end value
+ float diff = circleRadius - mMinBackgroundRadius;
+ PropertyValuesHolder[] values = mCircleAnimator.getValues();
+ values[0].setFloatValues(mCircleStartValue + diff, circleRadius);
+ mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime());
+ }
+ } else {
+ cancelAnimator(mCircleAnimator);
+ cancelAnimator(mPreviewClipper);
+ ValueAnimator animator = getAnimatorToRadius(circleRadius);
+ Interpolator interpolator = circleRadius == 0.0f
+ ? Interpolators.FAST_OUT_LINEAR_IN
+ : Interpolators.LINEAR_OUT_SLOW_IN;
+ animator.setInterpolator(interpolator);
+ long duration = 250;
+ if (!slowAnimation) {
+ float durationFactor = Math.abs(mCircleRadius - circleRadius)
+ / (float) mMinBackgroundRadius;
+ duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
+ duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
+ }
+ animator.setDuration(duration);
+ animator.start();
+ if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) {
+ mPreviewView.setVisibility(View.VISIBLE);
+ mPreviewClipper = ViewAnimationUtils.createCircularReveal(
+ mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
+ circleRadius);
+ mPreviewClipper.setInterpolator(interpolator);
+ mPreviewClipper.setDuration(duration);
+ mPreviewClipper.addListener(mClipEndListener);
+ mPreviewClipper.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPreviewView.setVisibility(View.INVISIBLE);
+ }
+ });
+ mPreviewClipper.start();
+ }
+ }
+ }
+
+ private ValueAnimator getAnimatorToRadius(float circleRadius) {
+ ValueAnimator animator = ValueAnimator.ofFloat(mCircleRadius, circleRadius);
+ mCircleAnimator = animator;
+ mCircleStartValue = mCircleRadius;
+ mCircleWillBeHidden = circleRadius == 0.0f;
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mCircleRadius = (float) animation.getAnimatedValue();
+ updateIconColor();
+ invalidate();
+ }
+ });
+ animator.addListener(mCircleEndListener);
+ return animator;
+ }
+
+ private void cancelAnimator(Animator animator) {
+ if (animator != null) {
+ animator.cancel();
+ }
+ }
+
+ public void setImageScale(float imageScale, boolean animate) {
+ setImageScale(imageScale, animate, -1, null);
+ }
+
+ /**
+ * Sets the scale of the containing image
+ *
+ * @param imageScale The new Scale.
+ * @param animate Should an animation be performed
+ * @param duration If animate, whats the duration? When -1 we take the default duration
+ * @param interpolator If animate, whats the interpolator? When null we take the default
+ * interpolator.
+ */
+ public void setImageScale(float imageScale, boolean animate, long duration,
+ Interpolator interpolator) {
+ cancelAnimator(mScaleAnimator);
+ if (!animate) {
+ mImageScale = imageScale;
+ invalidate();
+ } else {
+ ValueAnimator animator = ValueAnimator.ofFloat(mImageScale, imageScale);
+ mScaleAnimator = animator;
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mImageScale = (float) animation.getAnimatedValue();
+ invalidate();
+ }
+ });
+ animator.addListener(mScaleEndListener);
+ if (interpolator == null) {
+ interpolator = imageScale == 0.0f
+ ? Interpolators.FAST_OUT_LINEAR_IN
+ : Interpolators.LINEAR_OUT_SLOW_IN;
+ }
+ animator.setInterpolator(interpolator);
+ if (duration == -1) {
+ float durationFactor = Math.abs(mImageScale - imageScale)
+ / (1.0f - MIN_ICON_SCALE_AMOUNT);
+ durationFactor = Math.min(1.0f, durationFactor);
+ duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
+ }
+ animator.setDuration(duration);
+ animator.start();
+ }
+ }
+
+ public void setRestingAlpha(float alpha) {
+ mRestingAlpha = alpha;
+
+ // TODO: Handle the case an animation is playing.
+ setImageAlpha(alpha, false);
+ }
+
+ public float getRestingAlpha() {
+ return mRestingAlpha;
+ }
+
+ public void setImageAlpha(float alpha, boolean animate) {
+ setImageAlpha(alpha, animate, -1, null, null);
+ }
+
+ /**
+ * Sets the alpha of the containing image
+ *
+ * @param alpha The new alpha.
+ * @param animate Should an animation be performed
+ * @param duration If animate, whats the duration? When -1 we take the default duration
+ * @param interpolator If animate, whats the interpolator? When null we take the default
+ * interpolator.
+ */
+ public void setImageAlpha(float alpha, boolean animate, long duration,
+ Interpolator interpolator, Runnable runnable) {
+ cancelAnimator(mAlphaAnimator);
+ alpha = mLaunchingAffordance ? 0 : alpha;
+ int endAlpha = (int) (alpha * 255);
+ final Drawable background = getBackground();
+ if (!animate) {
+ if (background != null) background.mutate().setAlpha(endAlpha);
+ setImageAlpha(endAlpha);
+ } else {
+ int currentAlpha = getImageAlpha();
+ ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
+ mAlphaAnimator = animator;
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int alpha = (int) animation.getAnimatedValue();
+ if (background != null) background.mutate().setAlpha(alpha);
+ setImageAlpha(alpha);
+ }
+ });
+ animator.addListener(mAlphaEndListener);
+ if (interpolator == null) {
+ interpolator = alpha == 0.0f
+ ? Interpolators.FAST_OUT_LINEAR_IN
+ : Interpolators.LINEAR_OUT_SLOW_IN;
+ }
+ animator.setInterpolator(interpolator);
+ if (duration == -1) {
+ float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
+ durationFactor = Math.min(1.0f, durationFactor);
+ duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
+ }
+ animator.setDuration(duration);
+ if (runnable != null) {
+ animator.addListener(getEndListener(runnable));
+ }
+ animator.start();
+ }
+ }
+
+ private Animator.AnimatorListener getEndListener(final Runnable runnable) {
+ return new AnimatorListenerAdapter() {
+ boolean mCancelled;
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCancelled) {
+ runnable.run();
+ }
+ }
+ };
+ }
+
+ public float getCircleRadius() {
+ return mCircleRadius;
+ }
+
+ @Override
+ public boolean performClick() {
+ if (isClickable()) {
+ return super.performClick();
+ } else {
+ return false;
+ }
+ }
+
+ public void setLaunchingAffordance(boolean launchingAffordance) {
+ mLaunchingAffordance = launchingAffordance;
+ }
+}
diff --git a/com/android/systemui/statusbar/KeyguardIndicationController.java b/com/android/systemui/statusbar/KeyguardIndicationController.java
new file mode 100644
index 0000000..569e58d
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -0,0 +1,529 @@
+/*
+ * 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.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.BatteryManager;
+import android.os.BatteryStats;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.text.format.Formatter;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IBatteryStats;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
+import com.android.systemui.statusbar.phone.LockIcon;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.util.wakelock.SettableWakeLock;
+import com.android.systemui.util.wakelock.WakeLock;
+
+/**
+ * Controls the indications and error messages shown on the Keyguard
+ */
+public class KeyguardIndicationController {
+
+ private static final String TAG = "KeyguardIndication";
+ private static final boolean DEBUG_CHARGING_SPEED = false;
+
+ private static final int MSG_HIDE_TRANSIENT = 1;
+ private static final int MSG_CLEAR_FP_MSG = 2;
+ private static final long TRANSIENT_FP_ERROR_TIMEOUT = 1300;
+
+ private final Context mContext;
+ private ViewGroup mIndicationArea;
+ private KeyguardIndicationTextView mTextView;
+ private KeyguardIndicationTextView mDisclosure;
+ private final UserManager mUserManager;
+ private final IBatteryStats mBatteryInfo;
+ private final SettableWakeLock mWakeLock;
+
+ private final int mSlowThreshold;
+ private final int mFastThreshold;
+ private LockIcon mLockIcon;
+ private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+
+ private String mRestingIndication;
+ private String mTransientIndication;
+ private int mTransientTextColor;
+ private int mInitialTextColor;
+ private boolean mVisible;
+
+ private boolean mPowerPluggedIn;
+ private boolean mPowerCharged;
+ private int mChargingSpeed;
+ private int mChargingWattage;
+ private String mMessageToShowOnScreenOn;
+
+ private KeyguardUpdateMonitorCallback mUpdateMonitorCallback;
+
+ private final DevicePolicyManager mDevicePolicyManager;
+ private boolean mDozing;
+
+ /**
+ * Creates a new KeyguardIndicationController and registers callbacks.
+ */
+ public KeyguardIndicationController(Context context, ViewGroup indicationArea,
+ LockIcon lockIcon) {
+ this(context, indicationArea, lockIcon,
+ WakeLock.createPartial(context, "Doze:KeyguardIndication"));
+
+ registerCallbacks(KeyguardUpdateMonitor.getInstance(context));
+ }
+
+ /**
+ * Creates a new KeyguardIndicationController for testing. Does *not* register callbacks.
+ */
+ @VisibleForTesting
+ KeyguardIndicationController(Context context, ViewGroup indicationArea, LockIcon lockIcon,
+ WakeLock wakeLock) {
+ mContext = context;
+ mIndicationArea = indicationArea;
+ mTextView = (KeyguardIndicationTextView) indicationArea.findViewById(
+ R.id.keyguard_indication_text);
+ mInitialTextColor = mTextView != null ? mTextView.getCurrentTextColor() : Color.WHITE;
+ mDisclosure = (KeyguardIndicationTextView) indicationArea.findViewById(
+ R.id.keyguard_indication_enterprise_disclosure);
+ mLockIcon = lockIcon;
+ mWakeLock = new SettableWakeLock(wakeLock);
+
+ Resources res = context.getResources();
+ mSlowThreshold = res.getInteger(R.integer.config_chargingSlowlyThreshold);
+ mFastThreshold = res.getInteger(R.integer.config_chargingFastThreshold);
+
+ mUserManager = context.getSystemService(UserManager.class);
+ mBatteryInfo = IBatteryStats.Stub.asInterface(
+ ServiceManager.getService(BatteryStats.SERVICE_NAME));
+
+ mDevicePolicyManager = (DevicePolicyManager) context.getSystemService(
+ Context.DEVICE_POLICY_SERVICE);
+
+ updateDisclosure();
+ }
+
+ private void registerCallbacks(KeyguardUpdateMonitor monitor) {
+ monitor.registerCallback(getKeyguardCallback());
+
+ mContext.registerReceiverAsUser(mTickReceiver, UserHandle.SYSTEM,
+ new IntentFilter(Intent.ACTION_TIME_TICK), null,
+ Dependency.get(Dependency.TIME_TICK_HANDLER));
+ }
+
+ /**
+ * Gets the {@link KeyguardUpdateMonitorCallback} instance associated with this
+ * {@link KeyguardIndicationController}.
+ *
+ * <p>Subclasses may override this method to extend or change the callback behavior by extending
+ * the {@link BaseKeyguardCallback}.
+ *
+ * @return A KeyguardUpdateMonitorCallback. Multiple calls to this method <b>must</b> return the
+ * same instance.
+ */
+ protected KeyguardUpdateMonitorCallback getKeyguardCallback() {
+ if (mUpdateMonitorCallback == null) {
+ mUpdateMonitorCallback = new BaseKeyguardCallback();
+ }
+ return mUpdateMonitorCallback;
+ }
+
+ private void updateDisclosure() {
+ if (mDevicePolicyManager == null) {
+ return;
+ }
+
+ if (!mDozing && mDevicePolicyManager.isDeviceManaged()) {
+ final CharSequence organizationName =
+ mDevicePolicyManager.getDeviceOwnerOrganizationName();
+ if (organizationName != null) {
+ mDisclosure.switchIndication(mContext.getResources().getString(
+ R.string.do_disclosure_with_name, organizationName));
+ } else {
+ mDisclosure.switchIndication(R.string.do_disclosure_generic);
+ }
+ mDisclosure.setVisibility(View.VISIBLE);
+ } else {
+ mDisclosure.setVisibility(View.GONE);
+ }
+ }
+
+ public void setVisible(boolean visible) {
+ mVisible = visible;
+ mIndicationArea.setVisibility(visible ? View.VISIBLE : View.GONE);
+ if (visible) {
+ // If this is called after an error message was already shown, we should not clear it.
+ // Otherwise the error message won't be shown
+ if (!mHandler.hasMessages(MSG_HIDE_TRANSIENT)) {
+ hideTransientIndication();
+ }
+ updateIndication();
+ } else if (!visible) {
+ // If we unlock and return to keyguard quickly, previous error should not be shown
+ hideTransientIndication();
+ }
+ }
+
+ /**
+ * Sets the indication that is shown if nothing else is showing.
+ */
+ public void setRestingIndication(String restingIndication) {
+ mRestingIndication = restingIndication;
+ updateIndication();
+ }
+
+ /**
+ * Sets the active controller managing changes and callbacks to user information.
+ */
+ public void setUserInfoController(UserInfoController userInfoController) {
+ }
+
+ /**
+ * Returns the indication text indicating that trust has been granted.
+ *
+ * @return {@code null} or an empty string if a trust indication text should not be shown.
+ */
+ protected String getTrustGrantedIndication() {
+ return null;
+ }
+
+ /**
+ * Returns the indication text indicating that trust is currently being managed.
+ *
+ * @return {@code null} or an empty string if a trust managed text should not be shown.
+ */
+ protected String getTrustManagedIndication() {
+ return null;
+ }
+
+ /**
+ * Hides transient indication in {@param delayMs}.
+ */
+ public void hideTransientIndicationDelayed(long delayMs) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_HIDE_TRANSIENT), delayMs);
+ }
+
+ /**
+ * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
+ */
+ public void showTransientIndication(int transientIndication) {
+ showTransientIndication(mContext.getResources().getString(transientIndication));
+ }
+
+ /**
+ * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
+ */
+ public void showTransientIndication(String transientIndication) {
+ showTransientIndication(transientIndication, mInitialTextColor);
+ }
+
+ /**
+ * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
+ */
+ public void showTransientIndication(String transientIndication, int textColor) {
+ mTransientIndication = transientIndication;
+ mTransientTextColor = textColor;
+ mHandler.removeMessages(MSG_HIDE_TRANSIENT);
+ if (mDozing && !TextUtils.isEmpty(mTransientIndication)) {
+ // Make sure this doesn't get stuck and burns in. Acquire wakelock until its cleared.
+ mWakeLock.setAcquired(true);
+ hideTransientIndicationDelayed(BaseKeyguardCallback.HIDE_DELAY_MS);
+ }
+ updateIndication();
+ }
+
+ /**
+ * Hides transient indication.
+ */
+ public void hideTransientIndication() {
+ if (mTransientIndication != null) {
+ mTransientIndication = null;
+ mHandler.removeMessages(MSG_HIDE_TRANSIENT);
+ updateIndication();
+ }
+ }
+
+ protected final void updateIndication() {
+ if (TextUtils.isEmpty(mTransientIndication)) {
+ mWakeLock.setAcquired(false);
+ }
+
+ if (mVisible) {
+ // Walk down a precedence-ordered list of what indication
+ // should be shown based on user or device state
+ if (mDozing) {
+ // If we're dozing, never show a persistent indication.
+ if (!TextUtils.isEmpty(mTransientIndication)) {
+ // When dozing we ignore any text color and use white instead, because
+ // colors can be hard to read in low brightness.
+ mTextView.setTextColor(Color.WHITE);
+ mTextView.switchIndication(mTransientIndication);
+ } else {
+ mTextView.switchIndication(null);
+ }
+ return;
+ }
+
+ KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+ int userId = KeyguardUpdateMonitor.getCurrentUser();
+ String trustGrantedIndication = getTrustGrantedIndication();
+ String trustManagedIndication = getTrustManagedIndication();
+ if (!mUserManager.isUserUnlocked(userId)) {
+ mTextView.switchIndication(com.android.internal.R.string.lockscreen_storage_locked);
+ mTextView.setTextColor(mInitialTextColor);
+ } else if (!TextUtils.isEmpty(mTransientIndication)) {
+ mTextView.switchIndication(mTransientIndication);
+ mTextView.setTextColor(mTransientTextColor);
+ } else if (!TextUtils.isEmpty(trustGrantedIndication)
+ && updateMonitor.getUserHasTrust(userId)) {
+ mTextView.switchIndication(trustGrantedIndication);
+ mTextView.setTextColor(mInitialTextColor);
+ } else if (mPowerPluggedIn) {
+ String indication = computePowerIndication();
+ if (DEBUG_CHARGING_SPEED) {
+ indication += ", " + (mChargingWattage / 1000) + " mW";
+ }
+ mTextView.switchIndication(indication);
+ mTextView.setTextColor(mInitialTextColor);
+ } else if (!TextUtils.isEmpty(trustManagedIndication)
+ && updateMonitor.getUserTrustIsManaged(userId)
+ && !updateMonitor.getUserHasTrust(userId)) {
+ mTextView.switchIndication(trustManagedIndication);
+ mTextView.setTextColor(mInitialTextColor);
+ } else {
+ mTextView.switchIndication(mRestingIndication);
+ mTextView.setTextColor(mInitialTextColor);
+ }
+ }
+ }
+
+ private String computePowerIndication() {
+ if (mPowerCharged) {
+ return mContext.getResources().getString(R.string.keyguard_charged);
+ }
+
+ // Try fetching charging time from battery stats.
+ long chargingTimeRemaining = 0;
+ try {
+ chargingTimeRemaining = mBatteryInfo.computeChargeTimeRemaining();
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling IBatteryStats: ", e);
+ }
+ final boolean hasChargingTime = chargingTimeRemaining > 0;
+
+ int chargingId;
+ switch (mChargingSpeed) {
+ case KeyguardUpdateMonitor.BatteryStatus.CHARGING_FAST:
+ chargingId = hasChargingTime
+ ? R.string.keyguard_indication_charging_time_fast
+ : R.string.keyguard_plugged_in_charging_fast;
+ break;
+ case KeyguardUpdateMonitor.BatteryStatus.CHARGING_SLOWLY:
+ chargingId = hasChargingTime
+ ? R.string.keyguard_indication_charging_time_slowly
+ : R.string.keyguard_plugged_in_charging_slowly;
+ break;
+ default:
+ chargingId = hasChargingTime
+ ? R.string.keyguard_indication_charging_time
+ : R.string.keyguard_plugged_in;
+ break;
+ }
+
+ if (hasChargingTime) {
+ String chargingTimeFormatted = Formatter.formatShortElapsedTimeRoundingUpToMinutes(
+ mContext, chargingTimeRemaining);
+ return mContext.getResources().getString(chargingId, chargingTimeFormatted);
+ } else {
+ return mContext.getResources().getString(chargingId);
+ }
+ }
+
+ public void setStatusBarKeyguardViewManager(
+ StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+ mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+ }
+
+ private final BroadcastReceiver mTickReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mHandler.post(() -> {
+ if (mVisible) {
+ updateIndication();
+ }
+ });
+ }
+ };
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_HIDE_TRANSIENT) {
+ hideTransientIndication();
+ } else if (msg.what == MSG_CLEAR_FP_MSG) {
+ mLockIcon.setTransientFpError(false);
+ }
+ }
+ };
+
+ public void setDozing(boolean dozing) {
+ if (mDozing == dozing) {
+ return;
+ }
+ mDozing = dozing;
+ updateIndication();
+ updateDisclosure();
+ }
+
+ protected class BaseKeyguardCallback extends KeyguardUpdateMonitorCallback {
+ public static final int HIDE_DELAY_MS = 5000;
+ private int mLastSuccessiveErrorMessage = -1;
+
+ @Override
+ public void onRefreshBatteryInfo(KeyguardUpdateMonitor.BatteryStatus status) {
+ boolean isChargingOrFull = status.status == BatteryManager.BATTERY_STATUS_CHARGING
+ || status.status == BatteryManager.BATTERY_STATUS_FULL;
+ boolean wasPluggedIn = mPowerPluggedIn;
+ mPowerPluggedIn = status.isPluggedIn() && isChargingOrFull;
+ mPowerCharged = status.isCharged();
+ mChargingWattage = status.maxChargingWattage;
+ mChargingSpeed = status.getChargingSpeed(mSlowThreshold, mFastThreshold);
+ updateIndication();
+ if (mDozing) {
+ if (!wasPluggedIn && mPowerPluggedIn) {
+ showTransientIndication(computePowerIndication());
+ hideTransientIndicationDelayed(HIDE_DELAY_MS);
+ } else if (wasPluggedIn && !mPowerPluggedIn) {
+ hideTransientIndication();
+ }
+ }
+ }
+
+ @Override
+ public void onKeyguardVisibilityChanged(boolean showing) {
+ if (showing) {
+ updateDisclosure();
+ }
+ }
+
+ @Override
+ public void onFingerprintHelp(int msgId, String helpString) {
+ KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+ if (!updateMonitor.isUnlockingWithFingerprintAllowed()) {
+ return;
+ }
+ int errorColor = Utils.getColorError(mContext);
+ if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
+ mStatusBarKeyguardViewManager.showBouncerMessage(helpString, errorColor);
+ } else if (updateMonitor.isScreenOn()) {
+ mLockIcon.setTransientFpError(true);
+ showTransientIndication(helpString, errorColor);
+ hideTransientIndicationDelayed(TRANSIENT_FP_ERROR_TIMEOUT);
+ mHandler.removeMessages(MSG_CLEAR_FP_MSG);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CLEAR_FP_MSG),
+ TRANSIENT_FP_ERROR_TIMEOUT);
+ }
+ // Help messages indicate that there was actually a try since the last error, so those
+ // are not two successive error messages anymore.
+ mLastSuccessiveErrorMessage = -1;
+ }
+
+ @Override
+ public void onFingerprintError(int msgId, String errString) {
+ KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+ if ((!updateMonitor.isUnlockingWithFingerprintAllowed()
+ && msgId != FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
+ || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
+ return;
+ }
+ int errorColor = Utils.getColorError(mContext);
+ if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
+ // When swiping up right after receiving a fingerprint error, the bouncer calls
+ // authenticate leading to the same message being shown again on the bouncer.
+ // We want to avoid this, as it may confuse the user when the message is too
+ // generic.
+ if (mLastSuccessiveErrorMessage != msgId) {
+ mStatusBarKeyguardViewManager.showBouncerMessage(errString, errorColor);
+ }
+ } else if (updateMonitor.isScreenOn()) {
+ showTransientIndication(errString, errorColor);
+ // We want to keep this message around in case the screen was off
+ hideTransientIndicationDelayed(HIDE_DELAY_MS);
+ } else {
+ mMessageToShowOnScreenOn = errString;
+ }
+ mLastSuccessiveErrorMessage = msgId;
+ }
+
+ @Override
+ public void onScreenTurnedOn() {
+ if (mMessageToShowOnScreenOn != null) {
+ int errorColor = Utils.getColorError(mContext);
+ showTransientIndication(mMessageToShowOnScreenOn, errorColor);
+ // We want to keep this message around in case the screen was off
+ hideTransientIndicationDelayed(HIDE_DELAY_MS);
+ mMessageToShowOnScreenOn = null;
+ }
+ }
+
+ @Override
+ public void onFingerprintRunningStateChanged(boolean running) {
+ if (running) {
+ mMessageToShowOnScreenOn = null;
+ }
+ }
+
+ @Override
+ public void onFingerprintAuthenticated(int userId) {
+ super.onFingerprintAuthenticated(userId);
+ mLastSuccessiveErrorMessage = -1;
+ }
+
+ @Override
+ public void onFingerprintAuthFailed() {
+ super.onFingerprintAuthFailed();
+ mLastSuccessiveErrorMessage = -1;
+ }
+
+ @Override
+ public void onUserUnlocked() {
+ if (mVisible) {
+ updateIndication();
+ }
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/NotificationBackgroundView.java b/com/android/systemui/statusbar/NotificationBackgroundView.java
new file mode 100644
index 0000000..81a99bc
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationBackgroundView.java
@@ -0,0 +1,155 @@
+/*
+ * 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.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RippleDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * A view that can be used for both the dimmed and normal background of an notification.
+ */
+public class NotificationBackgroundView extends View {
+
+ private Drawable mBackground;
+ private int mClipTopAmount;
+ private int mActualHeight;
+ private int mClipBottomAmount;
+ private int mTintColor;
+
+ public NotificationBackgroundView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ draw(canvas, mBackground);
+ }
+
+ private void draw(Canvas canvas, Drawable drawable) {
+ int bottom = mActualHeight - mClipBottomAmount;
+ if (drawable != null && bottom > mClipTopAmount) {
+ drawable.setBounds(0, mClipTopAmount, getWidth(), bottom);
+ drawable.draw(canvas);
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || who == mBackground;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ drawableStateChanged(mBackground);
+ }
+
+ private void drawableStateChanged(Drawable d) {
+ if (d != null && d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ if (mBackground != null) {
+ mBackground.setHotspot(x, y);
+ }
+ }
+
+ /**
+ * Sets a background drawable. As we need to change our bounds independently of layout, we need
+ * the notion of a background independently of the regular View background..
+ */
+ public void setCustomBackground(Drawable background) {
+ if (mBackground != null) {
+ mBackground.setCallback(null);
+ unscheduleDrawable(mBackground);
+ }
+ mBackground = background;
+ if (mBackground != null) {
+ mBackground.setCallback(this);
+ setTint(mTintColor);
+ }
+ if (mBackground instanceof RippleDrawable) {
+ ((RippleDrawable) mBackground).setForceSoftware(true);
+ }
+ invalidate();
+ }
+
+ public void setCustomBackground(int drawableResId) {
+ final Drawable d = mContext.getDrawable(drawableResId);
+ setCustomBackground(d);
+ }
+
+ public void setTint(int tintColor) {
+ if (tintColor != 0) {
+ mBackground.setColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP);
+ } else {
+ mBackground.clearColorFilter();
+ }
+ mTintColor = tintColor;
+ invalidate();
+ }
+
+ public void setActualHeight(int actualHeight) {
+ mActualHeight = actualHeight;
+ invalidate();
+ }
+
+ public int getActualHeight() {
+ return mActualHeight;
+ }
+
+ public void setClipTopAmount(int clipTopAmount) {
+ mClipTopAmount = clipTopAmount;
+ invalidate();
+ }
+
+ public void setClipBottomAmount(int clipBottomAmount) {
+ mClipBottomAmount = clipBottomAmount;
+ invalidate();
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+
+ // Prevents this view from creating a layer when alpha is animating.
+ return false;
+ }
+
+ public void setState(int[] drawableState) {
+ mBackground.setState(drawableState);
+ }
+
+ public void setRippleColor(int color) {
+ if (mBackground instanceof RippleDrawable) {
+ RippleDrawable ripple = (RippleDrawable) mBackground;
+ ripple.setColor(ColorStateList.valueOf(color));
+ }
+ }
+
+ public void setDrawableAlpha(int drawableAlpha) {
+ mBackground.setAlpha(drawableAlpha);
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationContentView.java b/com/android/systemui/statusbar/NotificationContentView.java
new file mode 100644
index 0000000..9e059c8
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationContentView.java
@@ -0,0 +1,1462 @@
+/*
+ * 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.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Build;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.HybridNotificationView;
+import com.android.systemui.statusbar.notification.HybridGroupManager;
+import com.android.systemui.statusbar.notification.NotificationCustomViewWrapper;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.NotificationViewWrapper;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.policy.RemoteInputView;
+
+/**
+ * A frame layout containing the actual payload of the notification, including the contracted,
+ * expanded and heads up layout. This class is responsible for clipping the content and and
+ * switching between the expanded, contracted and the heads up view depending on its clipped size.
+ */
+public class NotificationContentView extends FrameLayout {
+
+ public static final int VISIBLE_TYPE_CONTRACTED = 0;
+ public static final int VISIBLE_TYPE_EXPANDED = 1;
+ public static final int VISIBLE_TYPE_HEADSUP = 2;
+ private static final int VISIBLE_TYPE_SINGLELINE = 3;
+ public static final int VISIBLE_TYPE_AMBIENT = 4;
+ private static final int VISIBLE_TYPE_AMBIENT_SINGLELINE = 5;
+ public static final int UNDEFINED = -1;
+
+ private final Rect mClipBounds = new Rect();
+ private final int mMinContractedHeight;
+ private final int mNotificationContentMarginEnd;
+
+ private View mContractedChild;
+ private View mExpandedChild;
+ private View mHeadsUpChild;
+ private HybridNotificationView mSingleLineView;
+ private View mAmbientChild;
+ private HybridNotificationView mAmbientSingleLineChild;
+
+ private RemoteInputView mExpandedRemoteInput;
+ private RemoteInputView mHeadsUpRemoteInput;
+
+ private NotificationViewWrapper mContractedWrapper;
+ private NotificationViewWrapper mExpandedWrapper;
+ private NotificationViewWrapper mHeadsUpWrapper;
+ private NotificationViewWrapper mAmbientWrapper;
+ private HybridGroupManager mHybridGroupManager;
+ private int mClipTopAmount;
+ private int mContentHeight;
+ private int mVisibleType = VISIBLE_TYPE_CONTRACTED;
+ private boolean mDark;
+ private boolean mAnimate;
+ private boolean mIsHeadsUp;
+ private boolean mLegacy;
+ private boolean mIsChildInGroup;
+ private int mSmallHeight;
+ private int mHeadsUpHeight;
+ private int mNotificationMaxHeight;
+ private int mNotificationAmbientHeight;
+ private StatusBarNotification mStatusBarNotification;
+ private NotificationGroupManager mGroupManager;
+ private RemoteInputController mRemoteInputController;
+ private Runnable mExpandedVisibleListener;
+
+ private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener
+ = new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ // We need to post since we don't want the notification to animate on the very first
+ // frame
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mAnimate = true;
+ }
+ });
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ return true;
+ }
+ };
+
+ private OnClickListener mExpandClickListener;
+ private boolean mBeforeN;
+ private boolean mExpandable;
+ private boolean mClipToActualHeight = true;
+ private ExpandableNotificationRow mContainingNotification;
+ /** The visible type at the start of a touch driven transformation */
+ private int mTransformationStartVisibleType;
+ /** The visible type at the start of an animation driven transformation */
+ private int mAnimationStartVisibleType = UNDEFINED;
+ private boolean mUserExpanding;
+ private int mSingleLineWidthIndention;
+ private boolean mForceSelectNextLayout = true;
+ private PendingIntent mPreviousExpandedRemoteInputIntent;
+ private PendingIntent mPreviousHeadsUpRemoteInputIntent;
+ private RemoteInputView mCachedExpandedRemoteInput;
+ private RemoteInputView mCachedHeadsUpRemoteInput;
+
+ private int mContentHeightAtAnimationStart = UNDEFINED;
+ private boolean mFocusOnVisibilityChange;
+ private boolean mHeadsUpAnimatingAway;
+ private boolean mIconsVisible;
+ private int mClipBottomAmount;
+ private boolean mIsLowPriority;
+ private boolean mIsContentExpandable;
+
+
+ public NotificationContentView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mHybridGroupManager = new HybridGroupManager(getContext(), this);
+ mMinContractedHeight = getResources().getDimensionPixelSize(
+ R.dimen.min_notification_layout_height);
+ mNotificationContentMarginEnd = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin_end);
+ }
+
+ public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight,
+ int ambientHeight) {
+ mSmallHeight = smallHeight;
+ mHeadsUpHeight = headsUpMaxHeight;
+ mNotificationMaxHeight = maxHeight;
+ mNotificationAmbientHeight = ambientHeight;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
+ boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
+ int maxSize = Integer.MAX_VALUE;
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ if (hasFixedHeight || isHeightLimited) {
+ maxSize = MeasureSpec.getSize(heightMeasureSpec);
+ }
+ int maxChildHeight = 0;
+ if (mExpandedChild != null) {
+ int size = Math.min(maxSize, mNotificationMaxHeight);
+ ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams();
+ boolean useExactly = false;
+ if (layoutParams.height >= 0) {
+ // An actual height is set
+ size = Math.min(maxSize, layoutParams.height);
+ useExactly = true;
+ }
+ int spec = size == Integer.MAX_VALUE
+ ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+ : MeasureSpec.makeMeasureSpec(size, useExactly
+ ? MeasureSpec.EXACTLY
+ : MeasureSpec.AT_MOST);
+ mExpandedChild.measure(widthMeasureSpec, spec);
+ maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight());
+ }
+ if (mContractedChild != null) {
+ int heightSpec;
+ int size = Math.min(maxSize, mSmallHeight);
+ ViewGroup.LayoutParams layoutParams = mContractedChild.getLayoutParams();
+ boolean useExactly = false;
+ if (layoutParams.height >= 0) {
+ // An actual height is set
+ size = Math.min(size, layoutParams.height);
+ useExactly = true;
+ }
+ if (shouldContractedBeFixedSize() || useExactly) {
+ heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
+ } else {
+ heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
+ }
+ mContractedChild.measure(widthMeasureSpec, heightSpec);
+ int measuredHeight = mContractedChild.getMeasuredHeight();
+ if (measuredHeight < mMinContractedHeight) {
+ heightSpec = MeasureSpec.makeMeasureSpec(mMinContractedHeight, MeasureSpec.EXACTLY);
+ mContractedChild.measure(widthMeasureSpec, heightSpec);
+ }
+ maxChildHeight = Math.max(maxChildHeight, measuredHeight);
+ if (updateContractedHeaderWidth()) {
+ mContractedChild.measure(widthMeasureSpec, heightSpec);
+ }
+ if (mExpandedChild != null
+ && mContractedChild.getMeasuredHeight() > mExpandedChild.getMeasuredHeight()) {
+ // the Expanded child is smaller then the collapsed. Let's remeasure it.
+ heightSpec = MeasureSpec.makeMeasureSpec(mContractedChild.getMeasuredHeight(),
+ MeasureSpec.EXACTLY);
+ mExpandedChild.measure(widthMeasureSpec, heightSpec);
+ }
+ }
+ if (mHeadsUpChild != null) {
+ int size = Math.min(maxSize, mHeadsUpHeight);
+ ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams();
+ boolean useExactly = false;
+ if (layoutParams.height >= 0) {
+ // An actual height is set
+ size = Math.min(size, layoutParams.height);
+ useExactly = true;
+ }
+ mHeadsUpChild.measure(widthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY
+ : MeasureSpec.AT_MOST));
+ maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight());
+ }
+ if (mSingleLineView != null) {
+ int singleLineWidthSpec = widthMeasureSpec;
+ if (mSingleLineWidthIndention != 0
+ && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+ singleLineWidthSpec = MeasureSpec.makeMeasureSpec(
+ width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(),
+ MeasureSpec.EXACTLY);
+ }
+ mSingleLineView.measure(singleLineWidthSpec,
+ MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST));
+ maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight());
+ }
+ if (mAmbientChild != null) {
+ int size = Math.min(maxSize, mNotificationAmbientHeight);
+ ViewGroup.LayoutParams layoutParams = mAmbientChild.getLayoutParams();
+ boolean useExactly = false;
+ if (layoutParams.height >= 0) {
+ // An actual height is set
+ size = Math.min(size, layoutParams.height);
+ useExactly = true;
+ }
+ mAmbientChild.measure(widthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY
+ : MeasureSpec.AT_MOST));
+ maxChildHeight = Math.max(maxChildHeight, mAmbientChild.getMeasuredHeight());
+ }
+ if (mAmbientSingleLineChild != null) {
+ int size = Math.min(maxSize, mNotificationAmbientHeight);
+ ViewGroup.LayoutParams layoutParams = mAmbientSingleLineChild.getLayoutParams();
+ boolean useExactly = false;
+ if (layoutParams.height >= 0) {
+ // An actual height is set
+ size = Math.min(size, layoutParams.height);
+ useExactly = true;
+ }
+ int ambientSingleLineWidthSpec = widthMeasureSpec;
+ if (mSingleLineWidthIndention != 0
+ && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+ ambientSingleLineWidthSpec = MeasureSpec.makeMeasureSpec(
+ width - mSingleLineWidthIndention + mAmbientSingleLineChild.getPaddingEnd(),
+ MeasureSpec.EXACTLY);
+ }
+ mAmbientSingleLineChild.measure(ambientSingleLineWidthSpec,
+ MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY
+ : MeasureSpec.AT_MOST));
+ maxChildHeight = Math.max(maxChildHeight, mAmbientSingleLineChild.getMeasuredHeight());
+ }
+ int ownHeight = Math.min(maxChildHeight, maxSize);
+ setMeasuredDimension(width, ownHeight);
+ }
+
+ private boolean updateContractedHeaderWidth() {
+ // We need to update the expanded and the collapsed header to have exactly the same with to
+ // have the expand buttons laid out at the same location.
+ NotificationHeaderView contractedHeader = mContractedWrapper.getNotificationHeader();
+ if (contractedHeader != null) {
+ if (mExpandedChild != null
+ && mExpandedWrapper.getNotificationHeader() != null) {
+ NotificationHeaderView expandedHeader = mExpandedWrapper.getNotificationHeader();
+ int expandedSize = expandedHeader.getMeasuredWidth()
+ - expandedHeader.getPaddingEnd();
+ int collapsedSize = contractedHeader.getMeasuredWidth()
+ - expandedHeader.getPaddingEnd();
+ if (expandedSize != collapsedSize) {
+ int paddingEnd = contractedHeader.getMeasuredWidth() - expandedSize;
+ contractedHeader.setPadding(
+ contractedHeader.isLayoutRtl()
+ ? paddingEnd
+ : contractedHeader.getPaddingLeft(),
+ contractedHeader.getPaddingTop(),
+ contractedHeader.isLayoutRtl()
+ ? contractedHeader.getPaddingLeft()
+ : paddingEnd,
+ contractedHeader.getPaddingBottom());
+ contractedHeader.setShowWorkBadgeAtEnd(true);
+ return true;
+ }
+ } else {
+ int paddingEnd = mNotificationContentMarginEnd;
+ if (contractedHeader.getPaddingEnd() != paddingEnd) {
+ contractedHeader.setPadding(
+ contractedHeader.isLayoutRtl()
+ ? paddingEnd
+ : contractedHeader.getPaddingLeft(),
+ contractedHeader.getPaddingTop(),
+ contractedHeader.isLayoutRtl()
+ ? contractedHeader.getPaddingLeft()
+ : paddingEnd,
+ contractedHeader.getPaddingBottom());
+ contractedHeader.setShowWorkBadgeAtEnd(false);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean shouldContractedBeFixedSize() {
+ return mBeforeN && mContractedWrapper instanceof NotificationCustomViewWrapper;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int previousHeight = 0;
+ if (mExpandedChild != null) {
+ previousHeight = mExpandedChild.getHeight();
+ }
+ super.onLayout(changed, left, top, right, bottom);
+ if (previousHeight != 0 && mExpandedChild.getHeight() != previousHeight) {
+ mContentHeightAtAnimationStart = previousHeight;
+ }
+ updateClipping();
+ invalidateOutline();
+ selectLayout(false /* animate */, mForceSelectNextLayout /* force */);
+ mForceSelectNextLayout = false;
+ updateExpandButtons(mExpandable);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ updateVisibility();
+ }
+
+ public View getContractedChild() {
+ return mContractedChild;
+ }
+
+ public View getExpandedChild() {
+ return mExpandedChild;
+ }
+
+ public View getHeadsUpChild() {
+ return mHeadsUpChild;
+ }
+
+ public View getAmbientChild() {
+ return mAmbientChild;
+ }
+
+ public HybridNotificationView getAmbientSingleLineChild() {
+ return mAmbientSingleLineChild;
+ }
+
+ public void setContractedChild(View child) {
+ if (mContractedChild != null) {
+ mContractedChild.animate().cancel();
+ removeView(mContractedChild);
+ }
+ addView(child);
+ mContractedChild = child;
+ mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child,
+ mContainingNotification);
+ mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
+ }
+
+ public void setExpandedChild(View child) {
+ if (mExpandedChild != null) {
+ mPreviousExpandedRemoteInputIntent = null;
+ if (mExpandedRemoteInput != null) {
+ mExpandedRemoteInput.onNotificationUpdateOrReset();
+ if (mExpandedRemoteInput.isActive()) {
+ mPreviousExpandedRemoteInputIntent = mExpandedRemoteInput.getPendingIntent();
+ mCachedExpandedRemoteInput = mExpandedRemoteInput;
+ mExpandedRemoteInput.dispatchStartTemporaryDetach();
+ ((ViewGroup)mExpandedRemoteInput.getParent()).removeView(mExpandedRemoteInput);
+ }
+ }
+ mExpandedChild.animate().cancel();
+ removeView(mExpandedChild);
+ mExpandedRemoteInput = null;
+ }
+ if (child == null) {
+ mExpandedChild = null;
+ mExpandedWrapper = null;
+ if (mVisibleType == VISIBLE_TYPE_EXPANDED) {
+ mVisibleType = VISIBLE_TYPE_CONTRACTED;
+ }
+ if (mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED) {
+ mTransformationStartVisibleType = UNDEFINED;
+ }
+ return;
+ }
+ addView(child);
+ mExpandedChild = child;
+ mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child,
+ mContainingNotification);
+ }
+
+ public void setHeadsUpChild(View child) {
+ if (mHeadsUpChild != null) {
+ mPreviousHeadsUpRemoteInputIntent = null;
+ if (mHeadsUpRemoteInput != null) {
+ mHeadsUpRemoteInput.onNotificationUpdateOrReset();
+ if (mHeadsUpRemoteInput.isActive()) {
+ mPreviousHeadsUpRemoteInputIntent = mHeadsUpRemoteInput.getPendingIntent();
+ mCachedHeadsUpRemoteInput = mHeadsUpRemoteInput;
+ mHeadsUpRemoteInput.dispatchStartTemporaryDetach();
+ ((ViewGroup)mHeadsUpRemoteInput.getParent()).removeView(mHeadsUpRemoteInput);
+ }
+ }
+ mHeadsUpChild.animate().cancel();
+ removeView(mHeadsUpChild);
+ mHeadsUpRemoteInput = null;
+ }
+ if (child == null) {
+ mHeadsUpChild = null;
+ mHeadsUpWrapper = null;
+ if (mVisibleType == VISIBLE_TYPE_HEADSUP) {
+ mVisibleType = VISIBLE_TYPE_CONTRACTED;
+ }
+ if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) {
+ mTransformationStartVisibleType = UNDEFINED;
+ }
+ return;
+ }
+ addView(child);
+ mHeadsUpChild = child;
+ mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child,
+ mContainingNotification);
+ }
+
+ public void setAmbientChild(View child) {
+ if (mAmbientChild != null) {
+ mAmbientChild.animate().cancel();
+ removeView(mAmbientChild);
+ }
+ if (child == null) {
+ return;
+ }
+ addView(child);
+ mAmbientChild = child;
+ mAmbientWrapper = NotificationViewWrapper.wrap(getContext(), child,
+ mContainingNotification);
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ updateVisibility();
+ }
+
+ private void updateVisibility() {
+ setVisible(isShown());
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
+ }
+
+ private void setVisible(final boolean isVisible) {
+ if (isVisible) {
+ // This call can happen multiple times, but removing only removes a single one.
+ // We therefore need to remove the old one.
+ getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
+ // 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;
+ }
+ }
+
+ private void focusExpandButtonIfNecessary() {
+ if (mFocusOnVisibilityChange) {
+ NotificationHeaderView header = getVisibleNotificationHeader();
+ if (header != null) {
+ ImageView expandButton = header.getExpandButton();
+ if (expandButton != null) {
+ expandButton.requestAccessibilityFocus();
+ }
+ }
+ mFocusOnVisibilityChange = false;
+ }
+ }
+
+ public void setContentHeight(int contentHeight) {
+ mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());
+ selectLayout(mAnimate /* animate */, false /* force */);
+
+ int minHeightHint = getMinContentHeightHint();
+
+ NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
+ if (wrapper != null) {
+ wrapper.setContentHeight(mContentHeight, minHeightHint);
+ }
+
+ wrapper = getVisibleWrapper(mTransformationStartVisibleType);
+ if (wrapper != null) {
+ wrapper.setContentHeight(mContentHeight, minHeightHint);
+ }
+
+ updateClipping();
+ invalidateOutline();
+ }
+
+ /**
+ * @return the minimum apparent height that the wrapper should allow for the purpose
+ * of aligning elements at the bottom edge. If this is larger than the content
+ * height, the notification is clipped instead of being further shrunk.
+ */
+ private int getMinContentHeightHint() {
+ if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) {
+ return mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_action_list_height);
+ }
+
+ // Transition between heads-up & expanded, or pinned.
+ if (mHeadsUpChild != null && mExpandedChild != null) {
+ boolean transitioningBetweenHunAndExpanded =
+ isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) ||
+ isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP);
+ boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED)
+ && (mIsHeadsUp || mHeadsUpAnimatingAway)
+ && !mContainingNotification.isOnKeyguard();
+ if (transitioningBetweenHunAndExpanded || pinned) {
+ return Math.min(mHeadsUpChild.getHeight(), mExpandedChild.getHeight());
+ }
+ }
+
+ // Size change of the expanded version
+ if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart >= 0
+ && mExpandedChild != null) {
+ return Math.min(mContentHeightAtAnimationStart, mExpandedChild.getHeight());
+ }
+
+ int hint;
+ if (mAmbientChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_AMBIENT)) {
+ hint = mAmbientChild.getHeight();
+ } else if (mAmbientSingleLineChild != null && isVisibleOrTransitioning(
+ VISIBLE_TYPE_AMBIENT_SINGLELINE)) {
+ hint = mAmbientSingleLineChild.getHeight();
+ } else if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) {
+ hint = mHeadsUpChild.getHeight();
+ } else if (mExpandedChild != null) {
+ hint = mExpandedChild.getHeight();
+ } else {
+ hint = mContractedChild.getHeight() + mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_action_list_height);
+ }
+
+ if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) {
+ hint = Math.min(hint, mExpandedChild.getHeight());
+ }
+ return hint;
+ }
+
+ private boolean isTransitioningFromTo(int from, int to) {
+ return (mTransformationStartVisibleType == from || mAnimationStartVisibleType == from)
+ && mVisibleType == to;
+ }
+
+ private boolean isVisibleOrTransitioning(int type) {
+ return mVisibleType == type || mTransformationStartVisibleType == type
+ || mAnimationStartVisibleType == type;
+ }
+
+ private void updateContentTransformation() {
+ int visibleType = calculateVisibleType();
+ if (visibleType != mVisibleType) {
+ // A new transformation starts
+ mTransformationStartVisibleType = mVisibleType;
+ final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
+ final TransformableView hiddenView = getTransformableViewForVisibleType(
+ mTransformationStartVisibleType);
+ shownView.transformFrom(hiddenView, 0.0f);
+ getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
+ hiddenView.transformTo(shownView, 0.0f);
+ mVisibleType = visibleType;
+ updateBackgroundColor(true /* animate */);
+ }
+ if (mForceSelectNextLayout) {
+ forceUpdateVisibilities();
+ }
+ if (mTransformationStartVisibleType != UNDEFINED
+ && mVisibleType != mTransformationStartVisibleType
+ && getViewForVisibleType(mTransformationStartVisibleType) != null) {
+ final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType);
+ final TransformableView hiddenView = getTransformableViewForVisibleType(
+ mTransformationStartVisibleType);
+ float transformationAmount = calculateTransformationAmount();
+ shownView.transformFrom(hiddenView, transformationAmount);
+ hiddenView.transformTo(shownView, transformationAmount);
+ updateBackgroundTransformation(transformationAmount);
+ } else {
+ updateViewVisibilities(visibleType);
+ updateBackgroundColor(false);
+ }
+ }
+
+ private void updateBackgroundTransformation(float transformationAmount) {
+ int endColor = getBackgroundColor(mVisibleType);
+ int startColor = getBackgroundColor(mTransformationStartVisibleType);
+ if (endColor != startColor) {
+ if (startColor == 0) {
+ startColor = mContainingNotification.getBackgroundColorWithoutTint();
+ }
+ if (endColor == 0) {
+ endColor = mContainingNotification.getBackgroundColorWithoutTint();
+ }
+ endColor = NotificationUtils.interpolateColors(startColor, endColor,
+ transformationAmount);
+ }
+ mContainingNotification.updateBackgroundAlpha(transformationAmount);
+ mContainingNotification.setContentBackground(endColor, false, this);
+ }
+
+ private float calculateTransformationAmount() {
+ int startHeight = getViewForVisibleType(mTransformationStartVisibleType).getHeight();
+ int endHeight = getViewForVisibleType(mVisibleType).getHeight();
+ int progress = Math.abs(mContentHeight - startHeight);
+ int totalDistance = Math.abs(endHeight - startHeight);
+ float amount = (float) progress / (float) totalDistance;
+ return Math.min(1.0f, amount);
+ }
+
+ public int getContentHeight() {
+ return mContentHeight;
+ }
+
+ public int getMaxHeight() {
+ if (mContainingNotification.isShowingAmbient()) {
+ return getShowingAmbientView().getHeight();
+ } else if (mExpandedChild != null) {
+ return mExpandedChild.getHeight();
+ } else if (mIsHeadsUp && mHeadsUpChild != null && !mContainingNotification.isOnKeyguard()) {
+ return mHeadsUpChild.getHeight();
+ }
+ return mContractedChild.getHeight();
+ }
+
+ public int getMinHeight() {
+ return getMinHeight(false /* likeGroupExpanded */);
+ }
+
+ public int getMinHeight(boolean likeGroupExpanded) {
+ if (mContainingNotification.isShowingAmbient()) {
+ return getShowingAmbientView().getHeight();
+ } else if (likeGroupExpanded || !mIsChildInGroup || isGroupExpanded()) {
+ return mContractedChild.getHeight();
+ } else {
+ return mSingleLineView.getHeight();
+ }
+ }
+
+ public View getShowingAmbientView() {
+ View v = mIsChildInGroup ? mAmbientSingleLineChild : mAmbientChild;
+ if (v != null) {
+ return v;
+ } else {
+ return mContractedChild;
+ }
+ }
+
+ private boolean isGroupExpanded() {
+ return mGroupManager.isGroupExpanded(mStatusBarNotification);
+ }
+
+ public void setClipTopAmount(int clipTopAmount) {
+ mClipTopAmount = clipTopAmount;
+ updateClipping();
+ }
+
+
+ public void setClipBottomAmount(int clipBottomAmount) {
+ mClipBottomAmount = clipBottomAmount;
+ updateClipping();
+ }
+
+ @Override
+ public void setTranslationY(float translationY) {
+ super.setTranslationY(translationY);
+ updateClipping();
+ }
+
+ private void updateClipping() {
+ if (mClipToActualHeight) {
+ int top = (int) (mClipTopAmount - getTranslationY());
+ int bottom = (int) (mContentHeight - mClipBottomAmount - getTranslationY());
+ bottom = Math.max(top, bottom);
+ mClipBounds.set(0, top, getWidth(), bottom);
+ setClipBounds(mClipBounds);
+ } else {
+ setClipBounds(null);
+ }
+ }
+
+ public void setClipToActualHeight(boolean clipToActualHeight) {
+ mClipToActualHeight = clipToActualHeight;
+ updateClipping();
+ }
+
+ private void selectLayout(boolean animate, boolean force) {
+ if (mContractedChild == null) {
+ return;
+ }
+ if (mUserExpanding) {
+ updateContentTransformation();
+ } else {
+ int visibleType = calculateVisibleType();
+ boolean changedType = visibleType != mVisibleType;
+ if (changedType || force) {
+ View visibleView = getViewForVisibleType(visibleType);
+ if (visibleView != null) {
+ visibleView.setVisibility(VISIBLE);
+ transferRemoteInputFocus(visibleType);
+ }
+
+ if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null)
+ || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null)
+ || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null)
+ || visibleType == VISIBLE_TYPE_CONTRACTED)) {
+ animateToVisibleType(visibleType);
+ } else {
+ updateViewVisibilities(visibleType);
+ }
+ mVisibleType = visibleType;
+ if (changedType) {
+ focusExpandButtonIfNecessary();
+ }
+ NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType);
+ if (visibleWrapper != null) {
+ visibleWrapper.setContentHeight(mContentHeight, getMinContentHeightHint());
+ }
+ updateBackgroundColor(animate);
+ }
+ }
+ }
+
+ private void forceUpdateVisibilities() {
+ forceUpdateVisibility(VISIBLE_TYPE_CONTRACTED, mContractedChild, mContractedWrapper);
+ forceUpdateVisibility(VISIBLE_TYPE_EXPANDED, mExpandedChild, mExpandedWrapper);
+ forceUpdateVisibility(VISIBLE_TYPE_HEADSUP, mHeadsUpChild, mHeadsUpWrapper);
+ forceUpdateVisibility(VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView);
+ forceUpdateVisibility(VISIBLE_TYPE_AMBIENT, mAmbientChild, mAmbientWrapper);
+ forceUpdateVisibility(VISIBLE_TYPE_AMBIENT_SINGLELINE, mAmbientSingleLineChild,
+ mAmbientSingleLineChild);
+ fireExpandedVisibleListenerIfVisible();
+ // forceUpdateVisibilities cancels outstanding animations without updating the
+ // mAnimationStartVisibleType. Do so here instead.
+ mAnimationStartVisibleType = UNDEFINED;
+ }
+
+ private void fireExpandedVisibleListenerIfVisible() {
+ if (mExpandedVisibleListener != null && mExpandedChild != null && isShown()
+ && mExpandedChild.getVisibility() == VISIBLE) {
+ Runnable listener = mExpandedVisibleListener;
+ mExpandedVisibleListener = null;
+ listener.run();
+ }
+ }
+
+ private void forceUpdateVisibility(int type, View view, TransformableView wrapper) {
+ if (view == null) {
+ return;
+ }
+ boolean visible = mVisibleType == type
+ || mTransformationStartVisibleType == type;
+ if (!visible) {
+ view.setVisibility(INVISIBLE);
+ } else {
+ wrapper.setVisible(true);
+ }
+ }
+
+ public void updateBackgroundColor(boolean animate) {
+ int customBackgroundColor = getBackgroundColor(mVisibleType);
+ mContainingNotification.resetBackgroundAlpha();
+ mContainingNotification.setContentBackground(customBackgroundColor, animate, this);
+ }
+
+ public int getVisibleType() {
+ return mVisibleType;
+ }
+
+ public int getBackgroundColorForExpansionState() {
+ // When expanding or user locked we want the new type, when collapsing we want
+ // the original type
+ final int visibleType = (mContainingNotification.isGroupExpanded()
+ || mContainingNotification.isUserLocked())
+ ? calculateVisibleType()
+ : getVisibleType();
+ return getBackgroundColor(visibleType);
+ }
+
+ public int getBackgroundColor(int visibleType) {
+ NotificationViewWrapper currentVisibleWrapper = getVisibleWrapper(visibleType);
+ int customBackgroundColor = 0;
+ if (currentVisibleWrapper != null) {
+ customBackgroundColor = currentVisibleWrapper.getCustomBackgroundColor();
+ }
+ return customBackgroundColor;
+ }
+
+ private void updateViewVisibilities(int visibleType) {
+ updateViewVisibility(visibleType, VISIBLE_TYPE_CONTRACTED,
+ mContractedChild, mContractedWrapper);
+ updateViewVisibility(visibleType, VISIBLE_TYPE_EXPANDED,
+ mExpandedChild, mExpandedWrapper);
+ updateViewVisibility(visibleType, VISIBLE_TYPE_HEADSUP,
+ mHeadsUpChild, mHeadsUpWrapper);
+ updateViewVisibility(visibleType, VISIBLE_TYPE_SINGLELINE,
+ mSingleLineView, mSingleLineView);
+ updateViewVisibility(visibleType, VISIBLE_TYPE_AMBIENT,
+ mAmbientChild, mAmbientWrapper);
+ updateViewVisibility(visibleType, VISIBLE_TYPE_AMBIENT_SINGLELINE,
+ mAmbientSingleLineChild, mAmbientSingleLineChild);
+ fireExpandedVisibleListenerIfVisible();
+ // updateViewVisibilities cancels outstanding animations without updating the
+ // mAnimationStartVisibleType. Do so here instead.
+ mAnimationStartVisibleType = UNDEFINED;
+ }
+
+ private void updateViewVisibility(int visibleType, int type, View view,
+ TransformableView wrapper) {
+ if (view != null) {
+ wrapper.setVisible(visibleType == type);
+ }
+ }
+
+ private void animateToVisibleType(int visibleType) {
+ final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
+ final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType);
+ if (shownView == hiddenView || hiddenView == null) {
+ shownView.setVisible(true);
+ return;
+ }
+ mAnimationStartVisibleType = mVisibleType;
+ shownView.transformFrom(hiddenView);
+ getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
+ hiddenView.transformTo(shownView, new Runnable() {
+ @Override
+ public void run() {
+ if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) {
+ hiddenView.setVisible(false);
+ }
+ mAnimationStartVisibleType = UNDEFINED;
+ }
+ });
+ fireExpandedVisibleListenerIfVisible();
+ }
+
+ private void transferRemoteInputFocus(int visibleType) {
+ if (visibleType == VISIBLE_TYPE_HEADSUP
+ && mHeadsUpRemoteInput != null
+ && (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive())) {
+ mHeadsUpRemoteInput.stealFocusFrom(mExpandedRemoteInput);
+ }
+ if (visibleType == VISIBLE_TYPE_EXPANDED
+ && mExpandedRemoteInput != null
+ && (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive())) {
+ mExpandedRemoteInput.stealFocusFrom(mHeadsUpRemoteInput);
+ }
+ }
+
+ /**
+ * @param visibleType one of the static enum types in this view
+ * @return the corresponding transformable view according to the given visible type
+ */
+ private TransformableView getTransformableViewForVisibleType(int visibleType) {
+ switch (visibleType) {
+ case VISIBLE_TYPE_EXPANDED:
+ return mExpandedWrapper;
+ case VISIBLE_TYPE_HEADSUP:
+ return mHeadsUpWrapper;
+ case VISIBLE_TYPE_SINGLELINE:
+ return mSingleLineView;
+ case VISIBLE_TYPE_AMBIENT:
+ return mAmbientWrapper;
+ case VISIBLE_TYPE_AMBIENT_SINGLELINE:
+ return mAmbientSingleLineChild;
+ default:
+ return mContractedWrapper;
+ }
+ }
+
+ /**
+ * @param visibleType one of the static enum types in this view
+ * @return the corresponding view according to the given visible type
+ */
+ private View getViewForVisibleType(int visibleType) {
+ switch (visibleType) {
+ case VISIBLE_TYPE_EXPANDED:
+ return mExpandedChild;
+ case VISIBLE_TYPE_HEADSUP:
+ return mHeadsUpChild;
+ case VISIBLE_TYPE_SINGLELINE:
+ return mSingleLineView;
+ case VISIBLE_TYPE_AMBIENT:
+ return mAmbientChild;
+ case VISIBLE_TYPE_AMBIENT_SINGLELINE:
+ return mAmbientSingleLineChild;
+ default:
+ return mContractedChild;
+ }
+ }
+
+ public NotificationViewWrapper getVisibleWrapper(int visibleType) {
+ switch (visibleType) {
+ case VISIBLE_TYPE_EXPANDED:
+ return mExpandedWrapper;
+ case VISIBLE_TYPE_HEADSUP:
+ return mHeadsUpWrapper;
+ case VISIBLE_TYPE_CONTRACTED:
+ return mContractedWrapper;
+ case VISIBLE_TYPE_AMBIENT:
+ return mAmbientWrapper;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * @return one of the static enum types in this view, calculated form the current state
+ */
+ public int calculateVisibleType() {
+ if (mContainingNotification.isShowingAmbient()) {
+ if (mIsChildInGroup && mAmbientSingleLineChild != null) {
+ return VISIBLE_TYPE_AMBIENT_SINGLELINE;
+ } else if (mAmbientChild != null) {
+ return VISIBLE_TYPE_AMBIENT;
+ } else {
+ return VISIBLE_TYPE_CONTRACTED;
+ }
+ }
+ if (mUserExpanding) {
+ int height = !mIsChildInGroup || isGroupExpanded()
+ || mContainingNotification.isExpanded(true /* allowOnKeyguard */)
+ ? mContainingNotification.getMaxContentHeight()
+ : mContainingNotification.getShowingLayout().getMinHeight();
+ if (height == 0) {
+ height = mContentHeight;
+ }
+ int expandedVisualType = getVisualTypeForHeight(height);
+ int collapsedVisualType = mIsChildInGroup && !isGroupExpanded()
+ ? VISIBLE_TYPE_SINGLELINE
+ : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight());
+ return mTransformationStartVisibleType == collapsedVisualType
+ ? expandedVisualType
+ : collapsedVisualType;
+ }
+ int intrinsicHeight = mContainingNotification.getIntrinsicHeight();
+ int viewHeight = mContentHeight;
+ if (intrinsicHeight != 0) {
+ // the intrinsicHeight might be 0 because it was just reset.
+ viewHeight = Math.min(mContentHeight, intrinsicHeight);
+ }
+ return getVisualTypeForHeight(viewHeight);
+ }
+
+ private int getVisualTypeForHeight(float viewHeight) {
+ boolean noExpandedChild = mExpandedChild == null;
+ if (!noExpandedChild && viewHeight == mExpandedChild.getHeight()) {
+ return VISIBLE_TYPE_EXPANDED;
+ }
+ if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) {
+ return VISIBLE_TYPE_SINGLELINE;
+ }
+
+ if ((mIsHeadsUp || mHeadsUpAnimatingAway) && mHeadsUpChild != null
+ && !mContainingNotification.isOnKeyguard()) {
+ if (viewHeight <= mHeadsUpChild.getHeight() || noExpandedChild) {
+ return VISIBLE_TYPE_HEADSUP;
+ } else {
+ return VISIBLE_TYPE_EXPANDED;
+ }
+ } else {
+ if (noExpandedChild || (viewHeight <= mContractedChild.getHeight()
+ && (!mIsChildInGroup || isGroupExpanded()
+ || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
+ return VISIBLE_TYPE_CONTRACTED;
+ } else {
+ return VISIBLE_TYPE_EXPANDED;
+ }
+ }
+ }
+
+ public boolean isContentExpandable() {
+ return mIsContentExpandable;
+ }
+
+ public void setDark(boolean dark, boolean fade, long delay) {
+ if (mContractedChild == null) {
+ return;
+ }
+ mDark = dark;
+ if (mVisibleType == VISIBLE_TYPE_CONTRACTED || !dark) {
+ mContractedWrapper.setDark(dark, fade, delay);
+ }
+ if (mVisibleType == VISIBLE_TYPE_EXPANDED || (mExpandedChild != null && !dark)) {
+ mExpandedWrapper.setDark(dark, fade, delay);
+ }
+ if (mVisibleType == VISIBLE_TYPE_HEADSUP || (mHeadsUpChild != null && !dark)) {
+ mHeadsUpWrapper.setDark(dark, fade, delay);
+ }
+ if (mSingleLineView != null && (mVisibleType == VISIBLE_TYPE_SINGLELINE || !dark)) {
+ mSingleLineView.setDark(dark, fade, delay);
+ }
+ selectLayout(!dark && fade /* animate */, false /* force */);
+ }
+
+ public void setHeadsUp(boolean headsUp) {
+ mIsHeadsUp = headsUp;
+ selectLayout(false /* animate */, true /* force */);
+ updateExpandButtons(mExpandable);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+
+ // This is not really true, but good enough when fading from the contracted to the expanded
+ // layout, and saves us some layers.
+ return false;
+ }
+
+ public void setLegacy(boolean legacy) {
+ mLegacy = legacy;
+ updateLegacy();
+ }
+
+ private void updateLegacy() {
+ if (mContractedChild != null) {
+ mContractedWrapper.setLegacy(mLegacy);
+ }
+ if (mExpandedChild != null) {
+ mExpandedWrapper.setLegacy(mLegacy);
+ }
+ if (mHeadsUpChild != null) {
+ mHeadsUpWrapper.setLegacy(mLegacy);
+ }
+ }
+
+ public void setIsChildInGroup(boolean isChildInGroup) {
+ mIsChildInGroup = isChildInGroup;
+ if (mContractedChild != null) {
+ mContractedWrapper.setIsChildInGroup(mIsChildInGroup);
+ }
+ if (mExpandedChild != null) {
+ mExpandedWrapper.setIsChildInGroup(mIsChildInGroup);
+ }
+ if (mHeadsUpChild != null) {
+ mHeadsUpWrapper.setIsChildInGroup(mIsChildInGroup);
+ }
+ if (mAmbientChild != null) {
+ mAmbientWrapper.setIsChildInGroup(mIsChildInGroup);
+ }
+ updateAllSingleLineViews();
+ }
+
+ public void onNotificationUpdated(NotificationData.Entry entry) {
+ mStatusBarNotification = entry.notification;
+ mBeforeN = entry.targetSdk < Build.VERSION_CODES.N;
+ updateAllSingleLineViews();
+ if (mContractedChild != null) {
+ mContractedWrapper.onContentUpdated(entry.row);
+ }
+ if (mExpandedChild != null) {
+ mExpandedWrapper.onContentUpdated(entry.row);
+ }
+ if (mHeadsUpChild != null) {
+ mHeadsUpWrapper.onContentUpdated(entry.row);
+ }
+ if (mAmbientChild != null) {
+ mAmbientWrapper.onContentUpdated(entry.row);
+ }
+ applyRemoteInput(entry);
+ updateLegacy();
+ mForceSelectNextLayout = true;
+ setDark(mDark, false /* animate */, 0 /* delay */);
+ mPreviousExpandedRemoteInputIntent = null;
+ mPreviousHeadsUpRemoteInputIntent = null;
+ }
+
+ private void updateAllSingleLineViews() {
+ updateSingleLineView();
+ updateAmbientSingleLineView();
+ }
+ private void updateSingleLineView() {
+ if (mIsChildInGroup) {
+ mSingleLineView = mHybridGroupManager.bindFromNotification(
+ mSingleLineView, mStatusBarNotification.getNotification());
+ } else if (mSingleLineView != null) {
+ removeView(mSingleLineView);
+ mSingleLineView = null;
+ }
+ }
+
+ private void updateAmbientSingleLineView() {
+ if (mIsChildInGroup) {
+ mAmbientSingleLineChild = mHybridGroupManager.bindAmbientFromNotification(
+ mAmbientSingleLineChild, mStatusBarNotification.getNotification());
+ } else if (mAmbientSingleLineChild != null) {
+ removeView(mAmbientSingleLineChild);
+ mAmbientSingleLineChild = null;
+ }
+ }
+
+ private void applyRemoteInput(final NotificationData.Entry entry) {
+ if (mRemoteInputController == null) {
+ return;
+ }
+
+ boolean hasRemoteInput = false;
+
+ Notification.Action[] actions = entry.notification.getNotification().actions;
+ if (actions != null) {
+ for (Notification.Action a : actions) {
+ if (a.getRemoteInputs() != null) {
+ for (RemoteInput ri : a.getRemoteInputs()) {
+ if (ri.getAllowFreeFormInput()) {
+ hasRemoteInput = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ View bigContentView = mExpandedChild;
+ if (bigContentView != null) {
+ mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasRemoteInput,
+ mPreviousExpandedRemoteInputIntent, mCachedExpandedRemoteInput,
+ mExpandedWrapper);
+ } else {
+ mExpandedRemoteInput = null;
+ }
+ if (mCachedExpandedRemoteInput != null
+ && mCachedExpandedRemoteInput != mExpandedRemoteInput) {
+ // We had a cached remote input but didn't reuse it. Clean up required.
+ mCachedExpandedRemoteInput.dispatchFinishTemporaryDetach();
+ }
+ mCachedExpandedRemoteInput = null;
+
+ View headsUpContentView = mHeadsUpChild;
+ if (headsUpContentView != null) {
+ mHeadsUpRemoteInput = applyRemoteInput(headsUpContentView, entry, hasRemoteInput,
+ mPreviousHeadsUpRemoteInputIntent, mCachedHeadsUpRemoteInput, mHeadsUpWrapper);
+ } else {
+ mHeadsUpRemoteInput = null;
+ }
+ if (mCachedHeadsUpRemoteInput != null
+ && mCachedHeadsUpRemoteInput != mHeadsUpRemoteInput) {
+ // We had a cached remote input but didn't reuse it. Clean up required.
+ mCachedHeadsUpRemoteInput.dispatchFinishTemporaryDetach();
+ }
+ mCachedHeadsUpRemoteInput = null;
+ }
+
+ private RemoteInputView applyRemoteInput(View view, NotificationData.Entry entry,
+ boolean hasRemoteInput, PendingIntent existingPendingIntent,
+ RemoteInputView cachedView, NotificationViewWrapper wrapper) {
+ View actionContainerCandidate = view.findViewById(
+ com.android.internal.R.id.actions_container);
+ if (actionContainerCandidate instanceof FrameLayout) {
+ RemoteInputView existing = (RemoteInputView)
+ view.findViewWithTag(RemoteInputView.VIEW_TAG);
+
+ if (existing != null) {
+ existing.onNotificationUpdateOrReset();
+ }
+
+ if (existing == null && hasRemoteInput) {
+ ViewGroup actionContainer = (FrameLayout) actionContainerCandidate;
+ if (cachedView == null) {
+ RemoteInputView riv = RemoteInputView.inflate(
+ mContext, actionContainer, entry, mRemoteInputController);
+
+ riv.setVisibility(View.INVISIBLE);
+ actionContainer.addView(riv, new LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT)
+ );
+ existing = riv;
+ } else {
+ actionContainer.addView(cachedView);
+ cachedView.dispatchFinishTemporaryDetach();
+ cachedView.requestFocus();
+ existing = cachedView;
+ }
+ }
+ if (hasRemoteInput) {
+ int color = entry.notification.getNotification().color;
+ if (color == Notification.COLOR_DEFAULT) {
+ color = mContext.getColor(R.color.default_remote_input_background);
+ }
+ existing.setBackgroundColor(NotificationColorUtil.ensureTextBackgroundColor(color,
+ mContext.getColor(R.color.remote_input_text_enabled),
+ mContext.getColor(R.color.remote_input_hint)));
+
+ existing.setWrapper(wrapper);
+
+ if (existingPendingIntent != null || existing.isActive()) {
+ // The current action could be gone, or the pending intent no longer valid.
+ // If we find a matching action in the new notification, focus, otherwise close.
+ Notification.Action[] actions = entry.notification.getNotification().actions;
+ if (existingPendingIntent != null) {
+ existing.setPendingIntent(existingPendingIntent);
+ }
+ if (existing.updatePendingIntentFromActions(actions)) {
+ if (!existing.isActive()) {
+ existing.focus();
+ }
+ } else {
+ if (existing.isActive()) {
+ existing.close();
+ }
+ }
+ }
+ }
+ return existing;
+ }
+ return null;
+ }
+
+ public void closeRemoteInput() {
+ if (mHeadsUpRemoteInput != null) {
+ mHeadsUpRemoteInput.close();
+ }
+ if (mExpandedRemoteInput != null) {
+ mExpandedRemoteInput.close();
+ }
+ }
+
+ public void setGroupManager(NotificationGroupManager groupManager) {
+ mGroupManager = groupManager;
+ }
+
+ public void setRemoteInputController(RemoteInputController r) {
+ mRemoteInputController = r;
+ }
+
+ public void setExpandClickListener(OnClickListener expandClickListener) {
+ mExpandClickListener = expandClickListener;
+ }
+
+ public void updateExpandButtons(boolean expandable) {
+ mExpandable = expandable;
+ // if the expanded child has the same height as the collapsed one we hide it.
+ if (mExpandedChild != null && mExpandedChild.getHeight() != 0) {
+ if ((!mIsHeadsUp && !mHeadsUpAnimatingAway)
+ || mHeadsUpChild == null || mContainingNotification.isOnKeyguard()) {
+ if (mExpandedChild.getHeight() <= mContractedChild.getHeight()) {
+ expandable = false;
+ }
+ } else if (mExpandedChild.getHeight() <= mHeadsUpChild.getHeight()) {
+ expandable = false;
+ }
+ }
+ if (mExpandedChild != null) {
+ mExpandedWrapper.updateExpandability(expandable, mExpandClickListener);
+ }
+ if (mContractedChild != null) {
+ mContractedWrapper.updateExpandability(expandable, mExpandClickListener);
+ }
+ if (mHeadsUpChild != null) {
+ mHeadsUpWrapper.updateExpandability(expandable, mExpandClickListener);
+ }
+ mIsContentExpandable = expandable;
+ }
+
+ public NotificationHeaderView getNotificationHeader() {
+ NotificationHeaderView header = null;
+ if (mContractedChild != null) {
+ header = mContractedWrapper.getNotificationHeader();
+ }
+ if (header == null && mExpandedChild != null) {
+ header = mExpandedWrapper.getNotificationHeader();
+ }
+ if (header == null && mHeadsUpChild != null) {
+ header = mHeadsUpWrapper.getNotificationHeader();
+ }
+ if (header == null && mAmbientChild != null) {
+ header = mAmbientWrapper.getNotificationHeader();
+ }
+ return header;
+ }
+
+
+ public NotificationHeaderView getContractedNotificationHeader() {
+ if (mContractedChild != null) {
+ return mContractedWrapper.getNotificationHeader();
+ }
+ return null;
+ }
+
+ public NotificationHeaderView getVisibleNotificationHeader() {
+ NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
+ return wrapper == null ? null : wrapper.getNotificationHeader();
+ }
+
+ public void setContainingNotification(ExpandableNotificationRow containingNotification) {
+ mContainingNotification = containingNotification;
+ }
+
+ public void requestSelectLayout(boolean needsAnimation) {
+ selectLayout(needsAnimation, false);
+ }
+
+ public void reInflateViews() {
+ if (mIsChildInGroup && mSingleLineView != null) {
+ removeView(mSingleLineView);
+ mSingleLineView = null;
+ updateAllSingleLineViews();
+ }
+ }
+
+ public void setUserExpanding(boolean userExpanding) {
+ mUserExpanding = userExpanding;
+ if (userExpanding) {
+ mTransformationStartVisibleType = mVisibleType;
+ } else {
+ mTransformationStartVisibleType = UNDEFINED;
+ mVisibleType = calculateVisibleType();
+ updateViewVisibilities(mVisibleType);
+ updateBackgroundColor(false);
+ }
+ }
+
+ /**
+ * Set by how much the single line view should be indented. Used when a overflow indicator is
+ * present and only during measuring
+ */
+ public void setSingleLineWidthIndention(int singleLineWidthIndention) {
+ if (singleLineWidthIndention != mSingleLineWidthIndention) {
+ mSingleLineWidthIndention = singleLineWidthIndention;
+ mContainingNotification.forceLayout();
+ forceLayout();
+ }
+ }
+
+ public HybridNotificationView getSingleLineView() {
+ return mSingleLineView;
+ }
+
+ public void setRemoved() {
+ if (mExpandedRemoteInput != null) {
+ mExpandedRemoteInput.setRemoved();
+ }
+ if (mHeadsUpRemoteInput != null) {
+ mHeadsUpRemoteInput.setRemoved();
+ }
+ }
+
+ public void setContentHeightAnimating(boolean animating) {
+ if (!animating) {
+ mContentHeightAtAnimationStart = UNDEFINED;
+ }
+ }
+
+ @VisibleForTesting
+ boolean isAnimatingVisibleType() {
+ return mAnimationStartVisibleType != UNDEFINED;
+ }
+
+ public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
+ mHeadsUpAnimatingAway = headsUpAnimatingAway;
+ selectLayout(false /* animate */, true /* force */);
+ }
+
+ public void setFocusOnVisibilityChange() {
+ mFocusOnVisibilityChange = true;
+ }
+
+ public void setIconsVisible(boolean iconsVisible) {
+ mIconsVisible = iconsVisible;
+ updateIconVisibilities();
+ }
+
+ private void updateIconVisibilities() {
+ if (mContractedWrapper != null) {
+ NotificationHeaderView header = mContractedWrapper.getNotificationHeader();
+ if (header != null) {
+ header.getIcon().setForceHidden(!mIconsVisible);
+ }
+ }
+ if (mHeadsUpWrapper != null) {
+ NotificationHeaderView header = mHeadsUpWrapper.getNotificationHeader();
+ if (header != null) {
+ header.getIcon().setForceHidden(!mIconsVisible);
+ }
+ }
+ if (mExpandedWrapper != null) {
+ NotificationHeaderView header = mExpandedWrapper.getNotificationHeader();
+ if (header != null) {
+ header.getIcon().setForceHidden(!mIconsVisible);
+ }
+ }
+ }
+
+ @Override
+ public void onVisibilityAggregated(boolean isVisible) {
+ super.onVisibilityAggregated(isVisible);
+ if (isVisible) {
+ fireExpandedVisibleListenerIfVisible();
+ }
+ }
+
+ /**
+ * Sets a one-shot listener for when the expanded view becomes visible.
+ *
+ * This will fire the listener immediately if the expanded view is already visible.
+ */
+ public void setOnExpandedVisibleListener(Runnable r) {
+ mExpandedVisibleListener = r;
+ fireExpandedVisibleListenerIfVisible();
+ }
+
+ public void setIsLowPriority(boolean isLowPriority) {
+ mIsLowPriority = isLowPriority;
+ }
+
+ public boolean isDimmable() {
+ if (!mContractedWrapper.isDimmable()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Should a single click be disallowed on this view when on the keyguard?
+ */
+ public boolean disallowSingleClick(float x, float y) {
+ NotificationViewWrapper visibleWrapper = getVisibleWrapper(getVisibleType());
+ if (visibleWrapper != null) {
+ return visibleWrapper.disallowSingleClick(x, y);
+ }
+ return false;
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationData.java b/com/android/systemui/statusbar/NotificationData.java
new file mode 100644
index 0000000..ddc7dd0
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationData.java
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import android.app.AppGlobals;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationListenerService.Ranking;
+import android.service.notification.NotificationListenerService.RankingMap;
+import android.service.notification.SnoozeCriterion;
+import android.service.notification.StatusBarNotification;
+import android.util.ArrayMap;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+import android.Manifest;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.Dependency;
+import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.statusbar.notification.InflationException;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The list of currently displaying notifications.
+ */
+public class NotificationData {
+
+ private final Environment mEnvironment;
+ private HeadsUpManager mHeadsUpManager;
+
+ public static final class Entry {
+ private static final long LAUNCH_COOLDOWN = 2000;
+ private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN;
+ private static final int COLOR_INVALID = 1;
+ public String key;
+ public StatusBarNotification notification;
+ public NotificationChannel channel;
+ public StatusBarIconView icon;
+ public StatusBarIconView expandedIcon;
+ public ExpandableNotificationRow row; // the outer expanded view
+ private boolean interruption;
+ public boolean autoRedacted; // whether the redacted notification was generated by us
+ public int targetSdk;
+ private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
+ public RemoteViews cachedContentView;
+ public RemoteViews cachedBigContentView;
+ public RemoteViews cachedHeadsUpContentView;
+ public RemoteViews cachedPublicContentView;
+ public RemoteViews cachedAmbientContentView;
+ public CharSequence remoteInputText;
+ public List<SnoozeCriterion> snoozeCriteria;
+ private int mCachedContrastColor = COLOR_INVALID;
+ private int mCachedContrastColorIsFor = COLOR_INVALID;
+ private InflationTask mRunningTask = null;
+ private Throwable mDebugThrowable;
+
+ public Entry(StatusBarNotification n) {
+ this.key = n.getKey();
+ this.notification = n;
+ }
+
+ public void setInterruption() {
+ interruption = true;
+ }
+
+ public boolean hasInterrupted() {
+ return interruption;
+ }
+
+ /**
+ * Resets the notification entry to be re-used.
+ */
+ public void reset() {
+ if (row != null) {
+ row.reset();
+ }
+ }
+
+ public View getExpandedContentView() {
+ return row.getPrivateLayout().getExpandedChild();
+ }
+
+ public View getPublicContentView() {
+ return row.getPublicLayout().getContractedChild();
+ }
+
+ public void notifyFullScreenIntentLaunched() {
+ setInterruption();
+ lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
+ }
+
+ public boolean hasJustLaunchedFullScreenIntent() {
+ return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
+ }
+
+ /**
+ * Create the icons for a notification
+ * @param context the context to create the icons with
+ * @param sbn the notification
+ * @throws InflationException
+ */
+ public void createIcons(Context context, StatusBarNotification sbn)
+ throws InflationException {
+ Notification n = sbn.getNotification();
+ final Icon smallIcon = n.getSmallIcon();
+ if (smallIcon == null) {
+ throw new InflationException("No small icon in notification from "
+ + sbn.getPackageName());
+ }
+
+ // Construct the icon.
+ icon = new StatusBarIconView(context,
+ sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
+ icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+
+ // Construct the expanded icon.
+ expandedIcon = new StatusBarIconView(context,
+ sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
+ expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+ final StatusBarIcon ic = new StatusBarIcon(
+ sbn.getUser(),
+ sbn.getPackageName(),
+ smallIcon,
+ n.iconLevel,
+ n.number,
+ StatusBarIconView.contentDescForNotification(context, n));
+ if (!icon.set(ic) || !expandedIcon.set(ic)) {
+ icon = null;
+ expandedIcon = null;
+ throw new InflationException("Couldn't create icon: " + ic);
+ }
+ expandedIcon.setVisibility(View.INVISIBLE);
+ expandedIcon.setOnVisibilityChangedListener(
+ newVisibility -> {
+ if (row != null) {
+ row.setIconsVisible(newVisibility != View.VISIBLE);
+ }
+ });
+ }
+
+ public void setIconTag(int key, Object tag) {
+ if (icon != null) {
+ icon.setTag(key, tag);
+ expandedIcon.setTag(key, tag);
+ }
+ }
+
+ /**
+ * Update the notification icons.
+ * @param context the context to create the icons with.
+ * @param n the notification to read the icon from.
+ * @throws InflationException
+ */
+ public void updateIcons(Context context, StatusBarNotification sbn)
+ throws InflationException {
+ if (icon != null) {
+ // Update the icon
+ Notification n = sbn.getNotification();
+ final StatusBarIcon ic = new StatusBarIcon(
+ notification.getUser(),
+ notification.getPackageName(),
+ n.getSmallIcon(),
+ n.iconLevel,
+ n.number,
+ StatusBarIconView.contentDescForNotification(context, n));
+ icon.setNotification(sbn);
+ expandedIcon.setNotification(sbn);
+ if (!icon.set(ic) || !expandedIcon.set(ic)) {
+ throw new InflationException("Couldn't update icon: " + ic);
+ }
+ }
+ }
+
+ public int getContrastedColor(Context context, boolean isLowPriority,
+ int backgroundColor) {
+ int rawColor = isLowPriority ? Notification.COLOR_DEFAULT :
+ notification.getNotification().color;
+ if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) {
+ return mCachedContrastColor;
+ }
+ final int contrasted = NotificationColorUtil.resolveContrastColor(context, rawColor,
+ backgroundColor);
+ mCachedContrastColorIsFor = rawColor;
+ mCachedContrastColor = contrasted;
+ return mCachedContrastColor;
+ }
+
+ /**
+ * Abort all existing inflation tasks
+ */
+ public void abortTask() {
+ if (mRunningTask != null) {
+ mRunningTask.abort();
+ mRunningTask = null;
+ }
+ }
+
+ public void setInflationTask(InflationTask abortableTask) {
+ // abort any existing inflation
+ InflationTask existing = mRunningTask;
+ abortTask();
+ mRunningTask = abortableTask;
+ if (existing != null && mRunningTask != null) {
+ mRunningTask.supersedeTask(existing);
+ }
+ }
+
+ public void onInflationTaskFinished() {
+ mRunningTask = null;
+ }
+
+ @VisibleForTesting
+ public InflationTask getRunningTask() {
+ return mRunningTask;
+ }
+
+ /**
+ * Set a throwable that is used for debugging
+ *
+ * @param debugThrowable the throwable to save
+ */
+ public void setDebugThrowable(Throwable debugThrowable) {
+ mDebugThrowable = debugThrowable;
+ }
+
+ public Throwable getDebugThrowable() {
+ return mDebugThrowable;
+ }
+ }
+
+ private final ArrayMap<String, Entry> mEntries = new ArrayMap<>();
+ private final ArrayList<Entry> mSortedAndFiltered = new ArrayList<>();
+
+ private NotificationGroupManager mGroupManager;
+
+ private RankingMap mRankingMap;
+ private final Ranking mTmpRanking = new Ranking();
+
+ public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ mHeadsUpManager = headsUpManager;
+ }
+
+ private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() {
+ private final Ranking mRankingA = new Ranking();
+ private final Ranking mRankingB = new Ranking();
+
+ @Override
+ public int compare(Entry a, Entry b) {
+ final StatusBarNotification na = a.notification;
+ final StatusBarNotification nb = b.notification;
+ int aImportance = NotificationManager.IMPORTANCE_DEFAULT;
+ int bImportance = NotificationManager.IMPORTANCE_DEFAULT;
+ int aRank = 0;
+ int bRank = 0;
+
+ if (mRankingMap != null) {
+ // RankingMap as received from NoMan
+ mRankingMap.getRanking(a.key, mRankingA);
+ mRankingMap.getRanking(b.key, mRankingB);
+ aImportance = mRankingA.getImportance();
+ bImportance = mRankingB.getImportance();
+ aRank = mRankingA.getRank();
+ bRank = mRankingB.getRank();
+ }
+
+ String mediaNotification = mEnvironment.getCurrentMediaNotificationKey();
+
+ // IMPORTANCE_MIN media streams are allowed to drift to the bottom
+ final boolean aMedia = a.key.equals(mediaNotification)
+ && aImportance > NotificationManager.IMPORTANCE_MIN;
+ final boolean bMedia = b.key.equals(mediaNotification)
+ && bImportance > NotificationManager.IMPORTANCE_MIN;
+
+ boolean aSystemMax = aImportance >= NotificationManager.IMPORTANCE_HIGH &&
+ isSystemNotification(na);
+ boolean bSystemMax = bImportance >= NotificationManager.IMPORTANCE_HIGH &&
+ isSystemNotification(nb);
+
+ boolean isHeadsUp = a.row.isHeadsUp();
+ if (isHeadsUp != b.row.isHeadsUp()) {
+ return isHeadsUp ? -1 : 1;
+ } else if (isHeadsUp) {
+ // Provide consistent ranking with headsUpManager
+ return mHeadsUpManager.compare(a, b);
+ } else if (aMedia != bMedia) {
+ // Upsort current media notification.
+ return aMedia ? -1 : 1;
+ } else if (aSystemMax != bSystemMax) {
+ // Upsort PRIORITY_MAX system notifications
+ return aSystemMax ? -1 : 1;
+ } else if (aRank != bRank) {
+ return aRank - bRank;
+ } else {
+ return Long.compare(nb.getNotification().when, na.getNotification().when);
+ }
+ }
+ };
+
+ public NotificationData(Environment environment) {
+ mEnvironment = environment;
+ mGroupManager = environment.getGroupManager();
+ }
+
+ /**
+ * Returns the sorted list of active notifications (depending on {@link Environment}
+ *
+ * <p>
+ * This call doesn't update the list of active notifications. Call {@link #filterAndSort()}
+ * when the environment changes.
+ * <p>
+ * Don't hold on to or modify the returned list.
+ */
+ public ArrayList<Entry> getActiveNotifications() {
+ return mSortedAndFiltered;
+ }
+
+ public Entry get(String key) {
+ return mEntries.get(key);
+ }
+
+ public void add(Entry entry) {
+ synchronized (mEntries) {
+ mEntries.put(entry.notification.getKey(), entry);
+ }
+ mGroupManager.onEntryAdded(entry);
+
+ updateRankingAndSort(mRankingMap);
+ }
+
+ public Entry remove(String key, RankingMap ranking) {
+ Entry removed = null;
+ synchronized (mEntries) {
+ removed = mEntries.remove(key);
+ }
+ if (removed == null) return null;
+ mGroupManager.onEntryRemoved(removed);
+ updateRankingAndSort(ranking);
+ return removed;
+ }
+
+ public void updateRanking(RankingMap ranking) {
+ updateRankingAndSort(ranking);
+ }
+
+ public boolean isAmbient(String key) {
+ if (mRankingMap != null) {
+ mRankingMap.getRanking(key, mTmpRanking);
+ return mTmpRanking.isAmbient();
+ }
+ return false;
+ }
+
+ public int getVisibilityOverride(String key) {
+ if (mRankingMap != null) {
+ mRankingMap.getRanking(key, mTmpRanking);
+ return mTmpRanking.getVisibilityOverride();
+ }
+ return Ranking.VISIBILITY_NO_OVERRIDE;
+ }
+
+ public boolean shouldSuppressScreenOff(String key) {
+ if (mRankingMap != null) {
+ mRankingMap.getRanking(key, mTmpRanking);
+ return (mTmpRanking.getSuppressedVisualEffects()
+ & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF) != 0;
+ }
+ return false;
+ }
+
+ public boolean shouldSuppressScreenOn(String key) {
+ if (mRankingMap != null) {
+ mRankingMap.getRanking(key, mTmpRanking);
+ return (mTmpRanking.getSuppressedVisualEffects()
+ & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON) != 0;
+ }
+ return false;
+ }
+
+ public int getImportance(String key) {
+ if (mRankingMap != null) {
+ mRankingMap.getRanking(key, mTmpRanking);
+ return mTmpRanking.getImportance();
+ }
+ return NotificationManager.IMPORTANCE_UNSPECIFIED;
+ }
+
+ public String getOverrideGroupKey(String key) {
+ if (mRankingMap != null) {
+ mRankingMap.getRanking(key, mTmpRanking);
+ return mTmpRanking.getOverrideGroupKey();
+ }
+ return null;
+ }
+
+ public List<SnoozeCriterion> getSnoozeCriteria(String key) {
+ if (mRankingMap != null) {
+ mRankingMap.getRanking(key, mTmpRanking);
+ return mTmpRanking.getSnoozeCriteria();
+ }
+ return null;
+ }
+
+ public NotificationChannel getChannel(String key) {
+ if (mRankingMap != null) {
+ mRankingMap.getRanking(key, mTmpRanking);
+ return mTmpRanking.getChannel();
+ }
+ return null;
+ }
+
+ private void updateRankingAndSort(RankingMap ranking) {
+ if (ranking != null) {
+ mRankingMap = ranking;
+ synchronized (mEntries) {
+ final int N = mEntries.size();
+ for (int i = 0; i < N; i++) {
+ Entry entry = mEntries.valueAt(i);
+ final StatusBarNotification oldSbn = entry.notification.cloneLight();
+ final String overrideGroupKey = getOverrideGroupKey(entry.key);
+ if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
+ entry.notification.setOverrideGroupKey(overrideGroupKey);
+ mGroupManager.onEntryUpdated(entry, oldSbn);
+ }
+ entry.channel = getChannel(entry.key);
+ entry.snoozeCriteria = getSnoozeCriteria(entry.key);
+ }
+ }
+ }
+ filterAndSort();
+ }
+
+ // TODO: This should not be public. Instead the Environment should notify this class when
+ // anything changed, and this class should call back the UI so it updates itself.
+ public void filterAndSort() {
+ mSortedAndFiltered.clear();
+
+ synchronized (mEntries) {
+ final int N = mEntries.size();
+ for (int i = 0; i < N; i++) {
+ Entry entry = mEntries.valueAt(i);
+ StatusBarNotification sbn = entry.notification;
+
+ if (shouldFilterOut(sbn)) {
+ continue;
+ }
+
+ mSortedAndFiltered.add(entry);
+ }
+ }
+
+ Collections.sort(mSortedAndFiltered, mRankingComparator);
+ }
+
+ /**
+ * @param sbn
+ * @return true if this notification should NOT be shown right now
+ */
+ public boolean shouldFilterOut(StatusBarNotification sbn) {
+ if (!(mEnvironment.isDeviceProvisioned() ||
+ showNotificationEvenIfUnprovisioned(sbn))) {
+ return true;
+ }
+
+ if (!mEnvironment.isNotificationForCurrentProfiles(sbn)) {
+ return true;
+ }
+
+ if (mEnvironment.isSecurelyLocked(sbn.getUserId()) &&
+ (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET
+ || mEnvironment.shouldHideNotifications(sbn.getUserId())
+ || mEnvironment.shouldHideNotifications(sbn.getKey()))) {
+ return true;
+ }
+
+ if (!StatusBar.ENABLE_CHILD_NOTIFICATIONS
+ && mGroupManager.isChildInGroupWithSummary(sbn)) {
+ return true;
+ }
+
+ final ForegroundServiceController fsc = Dependency.get(ForegroundServiceController.class);
+ if (fsc.isDungeonNotification(sbn) && !fsc.isDungeonNeededForUser(sbn.getUserId())) {
+ // this is a foreground-service disclosure for a user that does not need to show one
+ return true;
+ }
+
+ return false;
+ }
+
+ // Q: What kinds of notifications should show during setup?
+ // A: Almost none! Only things coming from packages with permission
+ // android.permission.NOTIFICATION_DURING_SETUP that also have special "kind" tags marking them
+ // as relevant for setup (see below).
+ public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) {
+ return showNotificationEvenIfUnprovisioned(AppGlobals.getPackageManager(), sbn);
+ }
+
+ @VisibleForTesting
+ static boolean showNotificationEvenIfUnprovisioned(IPackageManager packageManager,
+ StatusBarNotification sbn) {
+ return checkUidPermission(packageManager, Manifest.permission.NOTIFICATION_DURING_SETUP,
+ sbn.getUid()) == PackageManager.PERMISSION_GRANTED
+ && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP);
+ }
+
+ private static int checkUidPermission(IPackageManager packageManager, String permission,
+ int uid) {
+ try {
+ return packageManager.checkUidPermission(permission, uid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ public void dump(PrintWriter pw, String indent) {
+ int N = mSortedAndFiltered.size();
+ pw.print(indent);
+ pw.println("active notifications: " + N);
+ int active;
+ for (active = 0; active < N; active++) {
+ NotificationData.Entry e = mSortedAndFiltered.get(active);
+ dumpEntry(pw, indent, active, e);
+ }
+ synchronized (mEntries) {
+ int M = mEntries.size();
+ pw.print(indent);
+ pw.println("inactive notifications: " + (M - active));
+ int inactiveCount = 0;
+ for (int i = 0; i < M; i++) {
+ Entry entry = mEntries.valueAt(i);
+ if (!mSortedAndFiltered.contains(entry)) {
+ dumpEntry(pw, indent, inactiveCount, entry);
+ inactiveCount++;
+ }
+ }
+ }
+ }
+
+ private void dumpEntry(PrintWriter pw, String indent, int i, Entry e) {
+ mRankingMap.getRanking(e.key, mTmpRanking);
+ pw.print(indent);
+ pw.println(" [" + i + "] key=" + e.key + " icon=" + e.icon);
+ StatusBarNotification n = e.notification;
+ pw.print(indent);
+ pw.println(" pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" +
+ mTmpRanking.getImportance());
+ pw.print(indent);
+ pw.println(" notification=" + n.getNotification());
+ }
+
+ private static boolean isSystemNotification(StatusBarNotification sbn) {
+ String sbnPackage = sbn.getPackageName();
+ return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage);
+ }
+
+ /**
+ * Provides access to keyguard state and user settings dependent data.
+ */
+ public interface Environment {
+ public boolean isSecurelyLocked(int userId);
+ public boolean shouldHideNotifications(int userid);
+ public boolean shouldHideNotifications(String key);
+ public boolean isDeviceProvisioned();
+ public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
+ public String getCurrentMediaNotificationKey();
+ public NotificationGroupManager getGroupManager();
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationGuts.java b/com/android/systemui/statusbar/NotificationGuts.java
new file mode 100644
index 0000000..54d622b
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationGuts.java
@@ -0,0 +1,336 @@
+/*
+ * 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.app.INotificationManager;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settingslib.Utils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+import java.util.Set;
+
+/**
+ * The guts of a notification revealed when performing a long press.
+ */
+public class NotificationGuts extends FrameLayout {
+ private static final String TAG = "NotificationGuts";
+ private static final long CLOSE_GUTS_DELAY = 8000;
+
+ private Drawable mBackground;
+ private int mClipTopAmount;
+ private int mClipBottomAmount;
+ private int mActualHeight;
+ private boolean mExposed;
+
+ private Handler mHandler;
+ private Runnable mFalsingCheck;
+ private boolean mNeedsFalsingProtection;
+ private OnGutsClosedListener mClosedListener;
+ private OnHeightChangedListener mHeightListener;
+
+ private GutsContent mGutsContent;
+
+ public interface GutsContent {
+
+ public void setGutsParent(NotificationGuts listener);
+
+ /**
+ * @return the view to be shown in the notification guts.
+ */
+ public View getContentView();
+
+ /**
+ * @return the actual height of the content.
+ */
+ public int getActualHeight();
+
+ /**
+ * Called when the guts view have been told to close, typically after an outside
+ * interaction.
+ *
+ * @param save whether the state should be saved.
+ * @param force whether the guts view should be forced closed regardless of state.
+ * @return if closing the view has been handled.
+ */
+ public boolean handleCloseControls(boolean save, boolean force);
+
+ /**
+ * @return whether the notification associated with these guts is set to be removed.
+ */
+ public boolean willBeRemoved();
+
+ /**
+ * @return whether these guts are a leavebehind (e.g. {@link NotificationSnooze}).
+ */
+ public default boolean isLeavebehind() {
+ return false;
+ }
+ }
+
+ public interface OnGutsClosedListener {
+ public void onGutsClosed(NotificationGuts guts);
+ }
+
+ public interface OnHeightChangedListener {
+ public void onHeightChanged(NotificationGuts guts);
+ }
+
+ interface OnSettingsClickListener {
+ void onClick(View v, int appUid);
+ }
+
+ public NotificationGuts(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setWillNotDraw(false);
+ mHandler = new Handler();
+ mFalsingCheck = new Runnable() {
+ @Override
+ public void run() {
+ if (mNeedsFalsingProtection && mExposed) {
+ closeControls(-1 /* x */, -1 /* y */, false /* save */, false /* force */);
+ }
+ }
+ };
+ final TypedArray ta = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.Theme, 0, 0);
+ ta.recycle();
+ }
+
+ public NotificationGuts(Context context) {
+ this(context, null);
+ }
+
+ public void setGutsContent(GutsContent content) {
+ mGutsContent = content;
+ removeAllViews();
+ addView(mGutsContent.getContentView());
+ }
+
+ public GutsContent getGutsContent() {
+ return mGutsContent;
+ }
+
+ public void resetFalsingCheck() {
+ mHandler.removeCallbacks(mFalsingCheck);
+ if (mNeedsFalsingProtection && mExposed) {
+ mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ draw(canvas, mBackground);
+ }
+
+ private void draw(Canvas canvas, Drawable drawable) {
+ int top = mClipTopAmount;
+ int bottom = mActualHeight - mClipBottomAmount;
+ if (drawable != null && top < bottom) {
+ drawable.setBounds(0, top, getWidth(), bottom);
+ drawable.draw(canvas);
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mBackground = mContext.getDrawable(R.drawable.notification_guts_bg);
+ if (mBackground != null) {
+ mBackground.setCallback(this);
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || who == mBackground;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ drawableStateChanged(mBackground);
+ }
+
+ private void drawableStateChanged(Drawable d) {
+ if (d != null && d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ if (mBackground != null) {
+ mBackground.setHotspot(x, y);
+ }
+ }
+
+ public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) {
+ if (mGutsContent != null) {
+ if (mGutsContent.isLeavebehind() && leavebehinds) {
+ closeControls(x, y, true /* save */, force);
+ } else if (!mGutsContent.isLeavebehind() && controls) {
+ closeControls(x, y, true /* save */, force);
+ }
+ }
+ }
+
+ public void closeControls(int x, int y, boolean save, boolean force) {
+ if (getWindowToken() == null) {
+ if (mClosedListener != null) {
+ mClosedListener.onGutsClosed(this);
+ }
+ return;
+ }
+
+ if (mGutsContent == null || !mGutsContent.handleCloseControls(save, force)) {
+ animateClose(x, y);
+ setExposed(false, mNeedsFalsingProtection);
+ if (mClosedListener != null) {
+ mClosedListener.onGutsClosed(this);
+ }
+ }
+ }
+
+ private void animateClose(int x, int y) {
+ if (x == -1 || y == -1) {
+ x = (getLeft() + getRight()) / 2;
+ y = (getTop() + getHeight() / 2);
+ }
+ final double horz = Math.max(getWidth() - x, x);
+ final double vert = Math.max(getHeight() - y, y);
+ final float r = (float) Math.hypot(horz, vert);
+ final Animator a = ViewAnimationUtils.createCircularReveal(this,
+ x, y, r, 0);
+ a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+ a.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ setVisibility(View.GONE);
+ }
+ });
+ a.start();
+ }
+
+ public void setActualHeight(int actualHeight) {
+ mActualHeight = actualHeight;
+ invalidate();
+ }
+
+ public int getActualHeight() {
+ return mActualHeight;
+ }
+
+ public int getIntrinsicHeight() {
+ return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
+ }
+
+ public void setClipTopAmount(int clipTopAmount) {
+ mClipTopAmount = clipTopAmount;
+ invalidate();
+ }
+
+ public void setClipBottomAmount(int clipBottomAmount) {
+ mClipBottomAmount = clipBottomAmount;
+ invalidate();
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ // Prevents this view from creating a layer when alpha is animating.
+ return false;
+ }
+
+ public void setClosedListener(OnGutsClosedListener listener) {
+ mClosedListener = listener;
+ }
+
+ public void setHeightChangedListener(OnHeightChangedListener listener) {
+ mHeightListener = listener;
+ }
+
+ protected void onHeightChanged() {
+ if (mHeightListener != null) {
+ mHeightListener.onHeightChanged(this);
+ }
+ }
+
+ public void setExposed(boolean exposed, boolean needsFalsingProtection) {
+ final boolean wasExposed = mExposed;
+ mExposed = exposed;
+ mNeedsFalsingProtection = needsFalsingProtection;
+ if (mExposed && mNeedsFalsingProtection) {
+ resetFalsingCheck();
+ } else {
+ mHandler.removeCallbacks(mFalsingCheck);
+ }
+ if (wasExposed != mExposed && mGutsContent != null) {
+ final View contentView = mGutsContent.getContentView();
+ contentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ if (mExposed) {
+ contentView.requestAccessibilityFocus();
+ }
+ }
+ }
+
+ public boolean willBeRemoved() {
+ return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
+ }
+
+ public boolean isExposed() {
+ return mExposed;
+ }
+
+ public boolean isLeavebehind() {
+ return mGutsContent != null && mGutsContent.isLeavebehind();
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationHeaderUtil.java b/com/android/systemui/statusbar/NotificationHeaderUtil.java
new file mode 100644
index 0000000..4301817
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationHeaderUtil.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2015 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.app.Notification;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Icon;
+import android.text.TextUtils;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * A Util to manage {@link android.view.NotificationHeaderView} objects and their redundancies.
+ */
+public class NotificationHeaderUtil {
+
+ private static final TextViewComparator sTextViewComparator = new TextViewComparator();
+ private static final VisibilityApplicator sVisibilityApplicator = new VisibilityApplicator();
+ private static final DataExtractor sIconExtractor = new DataExtractor() {
+ @Override
+ public Object extractData(ExpandableNotificationRow row) {
+ return row.getStatusBarNotification().getNotification();
+ }
+ };
+ private static final IconComparator sIconVisibilityComparator = new IconComparator() {
+ public boolean compare(View parent, View child, Object parentData,
+ Object childData) {
+ return hasSameIcon(parentData, childData)
+ && hasSameColor(parentData, childData);
+ }
+ };
+ private static final IconComparator sGreyComparator = new IconComparator() {
+ public boolean compare(View parent, View child, Object parentData,
+ Object childData) {
+ return !hasSameIcon(parentData, childData)
+ || hasSameColor(parentData, childData);
+ }
+ };
+ private final static ResultApplicator mGreyApplicator = new ResultApplicator() {
+ @Override
+ public void apply(View view, boolean apply) {
+ NotificationHeaderView header = (NotificationHeaderView) view;
+ ImageView icon = (ImageView) view.findViewById(
+ com.android.internal.R.id.icon);
+ ImageView expand = (ImageView) view.findViewById(
+ com.android.internal.R.id.expand_button);
+ applyToChild(icon, apply, header.getOriginalIconColor());
+ applyToChild(expand, apply, header.getOriginalNotificationColor());
+ }
+
+ private void applyToChild(View view, boolean shouldApply, int originalColor) {
+ if (originalColor != NotificationHeaderView.NO_COLOR) {
+ ImageView imageView = (ImageView) view;
+ imageView.getDrawable().mutate();
+ if (shouldApply) {
+ // lets gray it out
+ int grey = view.getContext().getColor(
+ com.android.internal.R.color.notification_icon_default_color);
+ imageView.getDrawable().setColorFilter(grey, PorterDuff.Mode.SRC_ATOP);
+ } else {
+ // lets reset it
+ imageView.getDrawable().setColorFilter(originalColor,
+ PorterDuff.Mode.SRC_ATOP);
+ }
+ }
+ }
+ };
+
+ private final ExpandableNotificationRow mRow;
+ private final ArrayList<HeaderProcessor> mComparators = new ArrayList<>();
+ private final HashSet<Integer> mDividers = new HashSet<>();
+
+ public NotificationHeaderUtil(ExpandableNotificationRow row) {
+ mRow = row;
+ // To hide the icons if they are the same and the color is the same
+ mComparators.add(new HeaderProcessor(mRow,
+ com.android.internal.R.id.icon,
+ sIconExtractor,
+ sIconVisibilityComparator,
+ sVisibilityApplicator));
+ // To grey them out the icons and expand button when the icons are not the same
+ mComparators.add(new HeaderProcessor(mRow,
+ com.android.internal.R.id.notification_header,
+ sIconExtractor,
+ sGreyComparator,
+ mGreyApplicator));
+ mComparators.add(new HeaderProcessor(mRow,
+ com.android.internal.R.id.profile_badge,
+ null /* Extractor */,
+ new ViewComparator() {
+ @Override
+ public boolean compare(View parent, View child, Object parentData,
+ Object childData) {
+ return parent.getVisibility() != View.GONE;
+ }
+
+ @Override
+ public boolean isEmpty(View view) {
+ if (view instanceof ImageView) {
+ return ((ImageView) view).getDrawable() == null;
+ }
+ return false;
+ }
+ },
+ sVisibilityApplicator));
+ mComparators.add(HeaderProcessor.forTextView(mRow,
+ com.android.internal.R.id.app_name_text));
+ mComparators.add(HeaderProcessor.forTextView(mRow,
+ com.android.internal.R.id.header_text));
+ mDividers.add(com.android.internal.R.id.header_text_divider);
+ mDividers.add(com.android.internal.R.id.time_divider);
+ }
+
+ public void updateChildrenHeaderAppearance() {
+ List<ExpandableNotificationRow> notificationChildren = mRow.getNotificationChildren();
+ if (notificationChildren == null) {
+ return;
+ }
+ // Initialize the comparators
+ for (int compI = 0; compI < mComparators.size(); compI++) {
+ mComparators.get(compI).init();
+ }
+
+ // Compare all notification headers
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow row = notificationChildren.get(i);
+ for (int compI = 0; compI < mComparators.size(); compI++) {
+ mComparators.get(compI).compareToHeader(row);
+ }
+ }
+
+ // Apply the comparison to the row
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow row = notificationChildren.get(i);
+ for (int compI = 0; compI < mComparators.size(); compI++) {
+ mComparators.get(compI).apply(row);
+ }
+ // We need to sanitize the dividers since they might be off-balance now
+ sanitizeHeaderViews(row);
+ }
+ }
+
+ private void sanitizeHeaderViews(ExpandableNotificationRow row) {
+ if (row.isSummaryWithChildren()) {
+ sanitizeHeader(row.getNotificationHeader());
+ return;
+ }
+ final NotificationContentView layout = row.getPrivateLayout();
+ sanitizeChild(layout.getContractedChild());
+ sanitizeChild(layout.getHeadsUpChild());
+ sanitizeChild(layout.getExpandedChild());
+ }
+
+ private void sanitizeChild(View child) {
+ if (child != null) {
+ NotificationHeaderView header = (NotificationHeaderView) child.findViewById(
+ com.android.internal.R.id.notification_header);
+ sanitizeHeader(header);
+ }
+ }
+
+ private void sanitizeHeader(NotificationHeaderView rowHeader) {
+ if (rowHeader == null) {
+ return;
+ }
+ final int childCount = rowHeader.getChildCount();
+ View time = rowHeader.findViewById(com.android.internal.R.id.time);
+ boolean hasVisibleText = false;
+ for (int i = 1; i < childCount - 1 ; i++) {
+ View child = rowHeader.getChildAt(i);
+ if (child instanceof TextView
+ && child.getVisibility() != View.GONE
+ && !mDividers.contains(Integer.valueOf(child.getId()))
+ && child != time) {
+ hasVisibleText = true;
+ break;
+ }
+ }
+ // in case no view is visible we make sure the time is visible
+ int timeVisibility = !hasVisibleText
+ || mRow.getStatusBarNotification().getNotification().showsTime()
+ ? View.VISIBLE : View.GONE;
+ time.setVisibility(timeVisibility);
+ View left = null;
+ View right;
+ for (int i = 1; i < childCount - 1 ; i++) {
+ View child = rowHeader.getChildAt(i);
+ if (mDividers.contains(Integer.valueOf(child.getId()))) {
+ boolean visible = false;
+ // Lets find the item to the right
+ for (i++; i < childCount - 1; i++) {
+ right = rowHeader.getChildAt(i);
+ if (mDividers.contains(Integer.valueOf(right.getId()))) {
+ // A divider was found, this needs to be hidden
+ i--;
+ break;
+ } else if (right.getVisibility() != View.GONE && right instanceof TextView) {
+ visible = left != null;
+ left = right;
+ break;
+ }
+ }
+ child.setVisibility(visible ? View.VISIBLE : View.GONE);
+ } else if (child.getVisibility() != View.GONE && child instanceof TextView) {
+ left = child;
+ }
+ }
+ }
+
+ public void restoreNotificationHeader(ExpandableNotificationRow row) {
+ for (int compI = 0; compI < mComparators.size(); compI++) {
+ mComparators.get(compI).apply(row, true /* reset */);
+ }
+ sanitizeHeaderViews(row);
+ }
+
+ private static class HeaderProcessor {
+ private final int mId;
+ private final DataExtractor mExtractor;
+ private final ResultApplicator mApplicator;
+ private final ExpandableNotificationRow mParentRow;
+ private boolean mApply;
+ private View mParentView;
+ private ViewComparator mComparator;
+ private Object mParentData;
+
+ public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) {
+ return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator);
+ }
+
+ HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor,
+ ViewComparator comparator,
+ ResultApplicator applicator) {
+ mId = id;
+ mExtractor = extractor;
+ mApplicator = applicator;
+ mComparator = comparator;
+ mParentRow = row;
+ }
+
+ public void init() {
+ mParentView = mParentRow.getNotificationHeader().findViewById(mId);
+ mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
+ mApply = !mComparator.isEmpty(mParentView);
+ }
+ public void compareToHeader(ExpandableNotificationRow row) {
+ if (!mApply) {
+ return;
+ }
+ NotificationHeaderView header = row.getContractedNotificationHeader();
+ if (header == null) {
+ // No header found. We still consider this to be the same to avoid weird flickering
+ // when for example showing an undo notification
+ return;
+ }
+ Object childData = mExtractor == null ? null : mExtractor.extractData(row);
+ mApply = mComparator.compare(mParentView, header.findViewById(mId),
+ mParentData, childData);
+ }
+
+ public void apply(ExpandableNotificationRow row) {
+ apply(row, false /* reset */);
+ }
+
+ public void apply(ExpandableNotificationRow row, boolean reset) {
+ boolean apply = mApply && !reset;
+ if (row.isSummaryWithChildren()) {
+ applyToView(apply, row.getNotificationHeader());
+ return;
+ }
+ applyToView(apply, row.getPrivateLayout().getContractedChild());
+ applyToView(apply, row.getPrivateLayout().getHeadsUpChild());
+ applyToView(apply, row.getPrivateLayout().getExpandedChild());
+ }
+
+ private void applyToView(boolean apply, View parent) {
+ if (parent != null) {
+ View view = parent.findViewById(mId);
+ if (view != null && !mComparator.isEmpty(view)) {
+ mApplicator.apply(view, apply);
+ }
+ }
+ }
+ }
+
+ private interface ViewComparator {
+ /**
+ * @param parent the parent view
+ * @param child the child view
+ * @param parentData optional data for the parent
+ * @param childData optional data for the child
+ * @return whether to views are the same
+ */
+ boolean compare(View parent, View child, Object parentData, Object childData);
+ boolean isEmpty(View view);
+ }
+
+ private interface DataExtractor {
+ Object extractData(ExpandableNotificationRow row);
+ }
+
+ private static class TextViewComparator implements ViewComparator {
+ @Override
+ public boolean compare(View parent, View child, Object parentData, Object childData) {
+ TextView parentView = (TextView) parent;
+ TextView childView = (TextView) child;
+ return parentView.getText().equals(childView.getText());
+ }
+
+ @Override
+ public boolean isEmpty(View view) {
+ return TextUtils.isEmpty(((TextView) view).getText());
+ }
+ }
+
+ private static abstract class IconComparator implements ViewComparator {
+ @Override
+ public boolean compare(View parent, View child, Object parentData, Object childData) {
+ return false;
+ }
+
+ protected boolean hasSameIcon(Object parentData, Object childData) {
+ Icon parentIcon = ((Notification) parentData).getSmallIcon();
+ Icon childIcon = ((Notification) childData).getSmallIcon();
+ return parentIcon.sameAs(childIcon);
+ }
+
+ /**
+ * @return whether two ImageViews have the same colorFilterSet or none at all
+ */
+ protected boolean hasSameColor(Object parentData, Object childData) {
+ int parentColor = ((Notification) parentData).color;
+ int childColor = ((Notification) childData).color;
+ return parentColor == childColor;
+ }
+
+ @Override
+ public boolean isEmpty(View view) {
+ return false;
+ }
+ }
+
+ private interface ResultApplicator {
+ void apply(View view, boolean apply);
+ }
+
+ private static class VisibilityApplicator implements ResultApplicator {
+
+ @Override
+ public void apply(View view, boolean apply) {
+ view.setVisibility(apply ? View.GONE : View.VISIBLE);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationInfo.java b/com/android/systemui/statusbar/NotificationInfo.java
new file mode 100644
index 0000000..3b23a0c
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationInfo.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2017 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 static android.app.NotificationManager.IMPORTANCE_NONE;
+
+import android.app.INotificationManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+
+import java.lang.IllegalArgumentException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The guts of a notification revealed when performing a long press.
+ */
+public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent {
+ private static final String TAG = "InfoGuts";
+
+ private INotificationManager mINotificationManager;
+ private String mPkg;
+ private String mAppName;
+ private int mAppUid;
+ private List<NotificationChannel> mNotificationChannels;
+ private NotificationChannel mSingleNotificationChannel;
+ private boolean mIsSingleDefaultChannel;
+ private StatusBarNotification mSbn;
+ private int mStartingUserImportance;
+
+ private TextView mNumChannelsView;
+ private View mChannelDisabledView;
+ private TextView mSettingsLinkView;
+ private Switch mChannelEnabledSwitch;
+ private CheckSaveListener mCheckSaveListener;
+ private OnAppSettingsClickListener mAppSettingsClickListener;
+ private PackageManager mPm;
+
+ private NotificationGuts mGutsContainer;
+
+ public NotificationInfo(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ // Specify a CheckSaveListener to override when/if the user's changes are committed.
+ public interface CheckSaveListener {
+ // Invoked when importance has changed and the NotificationInfo wants to try to save it.
+ // Listener should run saveImportance unless the change should be canceled.
+ void checkSave(Runnable saveImportance);
+ }
+
+ public interface OnSettingsClickListener {
+ void onClick(View v, NotificationChannel channel, int appUid);
+ }
+
+ public interface OnAppSettingsClickListener {
+ void onClick(View v, Intent intent);
+ }
+
+ public void bindNotification(final PackageManager pm,
+ final INotificationManager iNotificationManager,
+ final String pkg,
+ final List<NotificationChannel> notificationChannels,
+ int startingUserImportance,
+ final StatusBarNotification sbn,
+ OnSettingsClickListener onSettingsClick,
+ OnAppSettingsClickListener onAppSettingsClick,
+ OnClickListener onDoneClick,
+ CheckSaveListener checkSaveListener,
+ final Set<String> nonBlockablePkgs)
+ throws RemoteException {
+ mINotificationManager = iNotificationManager;
+ mPkg = pkg;
+ mNotificationChannels = notificationChannels;
+ mCheckSaveListener = checkSaveListener;
+ mSbn = sbn;
+ mPm = pm;
+ mAppSettingsClickListener = onAppSettingsClick;
+ mStartingUserImportance = startingUserImportance;
+ mAppName = mPkg;
+ Drawable pkgicon = null;
+ CharSequence channelNameText = "";
+ ApplicationInfo info = null;
+ try {
+ info = pm.getApplicationInfo(mPkg,
+ PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+ if (info != null) {
+ mAppUid = sbn.getUid();
+ mAppName = String.valueOf(pm.getApplicationLabel(info));
+ pkgicon = pm.getApplicationIcon(info);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // app is gone, just show package name and generic icon
+ pkgicon = pm.getDefaultActivityIcon();
+ }
+ ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
+
+ int numTotalChannels = iNotificationManager.getNumNotificationChannelsForPackage(
+ pkg, mAppUid, false /* includeDeleted */);
+ if (mNotificationChannels.isEmpty()) {
+ throw new IllegalArgumentException("bindNotification requires at least one channel");
+ } else {
+ if (mNotificationChannels.size() == 1) {
+ mSingleNotificationChannel = mNotificationChannels.get(0);
+ // Special behavior for the Default channel if no other channels have been defined.
+ mIsSingleDefaultChannel =
+ (mSingleNotificationChannel.getId()
+ .equals(NotificationChannel.DEFAULT_CHANNEL_ID) &&
+ numTotalChannels <= 1);
+ } else {
+ mSingleNotificationChannel = null;
+ mIsSingleDefaultChannel = false;
+ }
+ }
+
+ boolean nonBlockable = false;
+ try {
+ final PackageInfo pkgInfo = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES);
+ if (Utils.isSystemPackage(getResources(), pm, pkgInfo)) {
+ final int numChannels = mNotificationChannels.size();
+ for (int i = 0; i < numChannels; i++) {
+ final NotificationChannel notificationChannel = mNotificationChannels.get(i);
+ // If any of the system channels is not blockable, the bundle is nonblockable
+ if (!notificationChannel.isBlockableSystem()) {
+ nonBlockable = true;
+ break;
+ }
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // unlikely.
+ }
+ if (nonBlockablePkgs != null) {
+ nonBlockable |= nonBlockablePkgs.contains(pkg);
+ }
+
+ String channelsDescText;
+ mNumChannelsView = findViewById(R.id.num_channels_desc);
+ if (nonBlockable) {
+ channelsDescText = mContext.getString(R.string.notification_unblockable_desc);
+ } else if (mIsSingleDefaultChannel) {
+ channelsDescText = mContext.getString(R.string.notification_default_channel_desc);
+ } else {
+ switch (mNotificationChannels.size()) {
+ case 1:
+ channelsDescText = String.format(mContext.getResources().getQuantityString(
+ R.plurals.notification_num_channels_desc, numTotalChannels),
+ numTotalChannels);
+ break;
+ case 2:
+ channelsDescText = mContext.getString(
+ R.string.notification_channels_list_desc_2,
+ mNotificationChannels.get(0).getName(),
+ mNotificationChannels.get(1).getName());
+ break;
+ default:
+ final int numOthers = mNotificationChannels.size() - 2;
+ channelsDescText = String.format(
+ mContext.getResources().getQuantityString(
+ R.plurals.notification_channels_list_desc_2_and_others,
+ numOthers),
+ mNotificationChannels.get(0).getName(),
+ mNotificationChannels.get(1).getName(),
+ numOthers);
+ }
+ }
+ mNumChannelsView.setText(channelsDescText);
+
+ if (mSingleNotificationChannel == null) {
+ // Multiple channels don't use a channel name for the title.
+ channelNameText = mContext.getString(R.string.notification_num_channels,
+ mNotificationChannels.size());
+ } else if (mIsSingleDefaultChannel || nonBlockable) {
+ // If this is the default channel or the app is unblockable,
+ // don't use our channel-specific text.
+ channelNameText = mContext.getString(R.string.notification_header_default_channel);
+ } else {
+ channelNameText = mSingleNotificationChannel.getName();
+ }
+ ((TextView) findViewById(R.id.pkgname)).setText(mAppName);
+ ((TextView) findViewById(R.id.channel_name)).setText(channelNameText);
+
+ // Set group information if this channel has an associated group.
+ CharSequence groupName = null;
+ if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) {
+ final NotificationChannelGroup notificationChannelGroup =
+ iNotificationManager.getNotificationChannelGroupForPackage(
+ mSingleNotificationChannel.getGroup(), pkg, mAppUid);
+ if (notificationChannelGroup != null) {
+ groupName = notificationChannelGroup.getName();
+ }
+ }
+ TextView groupNameView = ((TextView) findViewById(R.id.group_name));
+ TextView groupDividerView = ((TextView) findViewById(R.id.pkg_group_divider));
+ if (groupName != null) {
+ groupNameView.setText(groupName);
+ groupNameView.setVisibility(View.VISIBLE);
+ groupDividerView.setVisibility(View.VISIBLE);
+ } else {
+ groupNameView.setVisibility(View.GONE);
+ groupDividerView.setVisibility(View.GONE);
+ }
+
+ bindButtons(nonBlockable);
+
+ // Top-level importance group
+ mChannelDisabledView = findViewById(R.id.channel_disabled);
+ updateSecondaryText();
+
+ // Settings button.
+ final TextView settingsButton = (TextView) findViewById(R.id.more_settings);
+ if (mAppUid >= 0 && onSettingsClick != null) {
+ settingsButton.setVisibility(View.VISIBLE);
+ final int appUidF = mAppUid;
+ settingsButton.setOnClickListener(
+ (View view) -> {
+ onSettingsClick.onClick(view, mSingleNotificationChannel, appUidF);
+ });
+ if (numTotalChannels <= 1 || nonBlockable) {
+ settingsButton.setText(R.string.notification_more_settings);
+ } else {
+ settingsButton.setText(R.string.notification_all_categories);
+ }
+ } else {
+ settingsButton.setVisibility(View.GONE);
+ }
+
+ // Done button.
+ final TextView doneButton = (TextView) findViewById(R.id.done);
+ doneButton.setText(R.string.notification_done);
+ doneButton.setOnClickListener(onDoneClick);
+
+ // Optional settings link
+ updateAppSettingsLink();
+ }
+
+ private boolean hasImportanceChanged() {
+ return mSingleNotificationChannel != null &&
+ mChannelEnabledSwitch != null &&
+ mStartingUserImportance != getSelectedImportance();
+ }
+
+ private void saveImportance() {
+ if (!hasImportanceChanged()) {
+ return;
+ }
+ final int selectedImportance = getSelectedImportance();
+ MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
+ selectedImportance - mStartingUserImportance);
+ mSingleNotificationChannel.setImportance(selectedImportance);
+ mSingleNotificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
+ try {
+ mINotificationManager.updateNotificationChannelForPackage(
+ mPkg, mAppUid, mSingleNotificationChannel);
+ } catch (RemoteException e) {
+ // :(
+ }
+ }
+
+ private int getSelectedImportance() {
+ if (!mChannelEnabledSwitch.isChecked()) {
+ return IMPORTANCE_NONE;
+ } else {
+ return mStartingUserImportance;
+ }
+ }
+
+ private void bindButtons(final boolean nonBlockable) {
+ // Enabled Switch
+ mChannelEnabledSwitch = (Switch) findViewById(R.id.channel_enabled_switch);
+ mChannelEnabledSwitch.setChecked(
+ mStartingUserImportance != IMPORTANCE_NONE);
+ final boolean visible = !nonBlockable && mSingleNotificationChannel != null;
+ mChannelEnabledSwitch.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+
+ // Callback when checked.
+ mChannelEnabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (mGutsContainer != null) {
+ mGutsContainer.resetFalsingCheck();
+ }
+ updateSecondaryText();
+ updateAppSettingsLink();
+ });
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ if (mGutsContainer != null &&
+ event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ if (mGutsContainer.isExposed()) {
+ event.getText().add(mContext.getString(
+ R.string.notification_channel_controls_opened_accessibility, mAppName));
+ } else {
+ event.getText().add(mContext.getString(
+ R.string.notification_channel_controls_closed_accessibility, mAppName));
+ }
+ }
+ }
+
+ private void updateSecondaryText() {
+ final boolean disabled = mSingleNotificationChannel != null &&
+ getSelectedImportance() == IMPORTANCE_NONE;
+ if (disabled) {
+ mChannelDisabledView.setVisibility(View.VISIBLE);
+ mNumChannelsView.setVisibility(View.GONE);
+ } else {
+ mChannelDisabledView.setVisibility(View.GONE);
+ mNumChannelsView.setVisibility(mIsSingleDefaultChannel ? View.INVISIBLE : View.VISIBLE);
+ }
+ }
+
+ private void updateAppSettingsLink() {
+ mSettingsLinkView = findViewById(R.id.app_settings);
+ Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel,
+ mSbn.getId(), mSbn.getTag());
+ if (settingsIntent != null && getSelectedImportance() != IMPORTANCE_NONE
+ && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
+ mSettingsLinkView.setVisibility(View.VISIBLE);
+ mSettingsLinkView.setText(mContext.getString(R.string.notification_app_settings,
+ mSbn.getNotification().getSettingsText()));
+ mSettingsLinkView.setOnClickListener((View view) -> {
+ mAppSettingsClickListener.onClick(view, settingsIntent);
+ });
+ } else {
+ mSettingsLinkView.setVisibility(View.GONE);
+ }
+ }
+
+ private Intent getAppSettingsIntent(PackageManager pm, String packageName,
+ NotificationChannel channel, int id, String tag) {
+ Intent intent = new Intent(Intent.ACTION_MAIN)
+ .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
+ .setPackage(packageName);
+ final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
+ intent,
+ PackageManager.MATCH_DEFAULT_ONLY
+ );
+ if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
+ return null;
+ }
+ final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
+ intent.setClassName(activityInfo.packageName, activityInfo.name);
+ if (channel != null) {
+ intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId());
+ }
+ intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id);
+ intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag);
+ return intent;
+ }
+
+ @Override
+ public void setGutsParent(NotificationGuts guts) {
+ mGutsContainer = guts;
+ }
+
+ @Override
+ public boolean willBeRemoved() {
+ return mChannelEnabledSwitch != null && !mChannelEnabledSwitch.isChecked();
+ }
+
+ @Override
+ public View getContentView() {
+ return this;
+ }
+
+ @Override
+ public boolean handleCloseControls(boolean save, boolean force) {
+ if (save && hasImportanceChanged()) {
+ if (mCheckSaveListener != null) {
+ mCheckSaveListener.checkSave(() -> { saveImportance(); });
+ } else {
+ saveImportance();
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int getActualHeight() {
+ return getHeight();
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationMenuRow.java b/com/android/systemui/statusbar/NotificationMenuRow.java
new file mode 100644
index 0000000..99b4b07
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationMenuRow.java
@@ -0,0 +1,664 @@
+/*
+ * Copyright (C) 2016 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 static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
+
+import java.util.ArrayList;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.statusbar.NotificationGuts.GutsContent;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.app.Notification;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.service.notification.StatusBarNotification;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+
+public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener,
+ ExpandableNotificationRow.LayoutListener {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "swipe";
+
+ private static final int ICON_ALPHA_ANIM_DURATION = 200;
+ private static final long SHOW_MENU_DELAY = 60;
+ private static final long SWIPE_MENU_TIMING = 200;
+
+ // Notification must be swiped at least this fraction of a single menu item to show menu
+ private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f;
+ private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f;
+
+ // When the menu is displayed, the notification must be swiped within this fraction of a single
+ // menu item to snap back to menu (else it will cover the menu or it'll be dismissed)
+ private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f;
+
+ private ExpandableNotificationRow mParent;
+
+ private Context mContext;
+ private FrameLayout mMenuContainer;
+ private MenuItem mInfoItem;
+ private ArrayList<MenuItem> mMenuItems;
+ private OnMenuEventListener mMenuListener;
+
+ private ValueAnimator mFadeAnimator;
+ private boolean mAnimating;
+ private boolean mMenuFadedIn;
+
+ private boolean mOnLeft;
+ private boolean mIconsPlaced;
+
+ private boolean mDismissing;
+ private boolean mSnapping;
+ private float mTranslation;
+
+ private int[] mIconLocation = new int[2];
+ private int[] mParentLocation = new int[2];
+
+ private float mHorizSpaceForIcon = -1;
+ private int mVertSpaceForIcons = -1;
+ private int mIconPadding = -1;
+
+ private float mAlpha = 0f;
+ private float mPrevX;
+
+ private CheckForDrag mCheckForDrag;
+ private Handler mHandler;
+
+ private boolean mMenuSnappedTo;
+ private boolean mMenuSnappedOnLeft;
+ private boolean mShouldShowMenu;
+
+ private NotificationSwipeActionHelper mSwipeHelper;
+ private boolean mIsUserTouching;
+
+ public NotificationMenuRow(Context context) {
+ mContext = context;
+ mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear);
+ mHandler = new Handler(Looper.getMainLooper());
+ mMenuItems = new ArrayList<>();
+ }
+
+ @Override
+ public ArrayList<MenuItem> getMenuItems(Context context) {
+ return mMenuItems;
+ }
+
+ @Override
+ public MenuItem getLongpressMenuItem(Context context) {
+ return mInfoItem;
+ }
+
+ @Override
+ public void setSwipeActionHelper(NotificationSwipeActionHelper helper) {
+ mSwipeHelper = helper;
+ }
+
+ @Override
+ public void setMenuClickListener(OnMenuEventListener listener) {
+ mMenuListener = listener;
+ }
+
+ @Override
+ public void createMenu(ViewGroup parent, StatusBarNotification sbn) {
+ mParent = (ExpandableNotificationRow) parent;
+ createMenuViews(true /* resetState */);
+ }
+
+ @Override
+ public boolean isMenuVisible() {
+ return mAlpha > 0;
+ }
+
+ @Override
+ public View getMenuView() {
+ return mMenuContainer;
+ }
+
+ @Override
+ public void resetMenu() {
+ resetState(true);
+ }
+
+ @Override
+ public void onNotificationUpdated(StatusBarNotification sbn) {
+ if (mMenuContainer == null) {
+ // Menu hasn't been created yet, no need to do anything.
+ return;
+ }
+ createMenuViews(!isMenuVisible() /* resetState */);
+ }
+
+ @Override
+ public void onConfigurationChanged() {
+ mParent.setLayoutListener(this);
+ }
+
+ @Override
+ public void onLayout() {
+ mIconsPlaced = false; // Force icons to be re-placed
+ setMenuLocation();
+ mParent.removeListener();
+ }
+
+ private void createMenuViews(boolean resetState) {
+ final Resources res = mContext.getResources();
+ mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size);
+ mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
+ mIconPadding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
+ mMenuItems.clear();
+ // Construct the menu items based on the notification
+ if (mParent != null && mParent.getStatusBarNotification() != null) {
+ int flags = mParent.getStatusBarNotification().getNotification().flags;
+ boolean isForeground = (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
+ if (!isForeground) {
+ // Only show snooze for non-foreground notifications
+ mMenuItems.add(createSnoozeItem(mContext));
+ }
+ }
+ mInfoItem = createInfoItem(mContext);
+ mMenuItems.add(mInfoItem);
+
+ // Construct the menu views
+ if (mMenuContainer != null) {
+ mMenuContainer.removeAllViews();
+ } else {
+ mMenuContainer = new FrameLayout(mContext);
+ }
+ for (int i = 0; i < mMenuItems.size(); i++) {
+ addMenuView(mMenuItems.get(i), mMenuContainer);
+ }
+ if (resetState) {
+ resetState(false /* notify */);
+ } else {
+ mIconsPlaced = false;
+ setMenuLocation();
+ if (!mIsUserTouching) {
+ // If the # of items showing changed we need to update the snap position
+ showMenu(mParent, mOnLeft ? getSpaceForMenu() : -getSpaceForMenu(),
+ 0 /* velocity */);
+ }
+ }
+ }
+
+ private void resetState(boolean notify) {
+ setMenuAlpha(0f);
+ mIconsPlaced = false;
+ mMenuFadedIn = false;
+ mAnimating = false;
+ mSnapping = false;
+ mDismissing = false;
+ mMenuSnappedTo = false;
+ setMenuLocation();
+ if (mMenuListener != null && notify) {
+ mMenuListener.onMenuReset(mParent);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(View view, MotionEvent ev, float velocity) {
+ final int action = ev.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mSnapping = false;
+ if (mFadeAnimator != null) {
+ mFadeAnimator.cancel();
+ }
+ mHandler.removeCallbacks(mCheckForDrag);
+ mCheckForDrag = null;
+ mPrevX = ev.getRawX();
+ mIsUserTouching = true;
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ mSnapping = false;
+ float diffX = ev.getRawX() - mPrevX;
+ mPrevX = ev.getRawX();
+ if (!isTowardsMenu(diffX) && isMenuLocationChange()) {
+ // Don't consider it "snapped" if location has changed.
+ mMenuSnappedTo = false;
+
+ // Changed directions, make sure we check to fade in icon again.
+ if (!mHandler.hasCallbacks(mCheckForDrag)) {
+ // No check scheduled, set null to schedule a new one.
+ mCheckForDrag = null;
+ } else {
+ // Check scheduled, reset alpha and update location; check will fade it in
+ setMenuAlpha(0f);
+ setMenuLocation();
+ }
+ }
+ if (mShouldShowMenu
+ && !NotificationStackScrollLayout.isPinnedHeadsUp(view)
+ && !mParent.areGutsExposed()
+ && !mParent.isDark()
+ && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) {
+ // Only show the menu if we're not a heads up view and guts aren't exposed.
+ mCheckForDrag = new CheckForDrag();
+ mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ mIsUserTouching = false;
+ return handleUpEvent(ev, view, velocity);
+ case MotionEvent.ACTION_CANCEL:
+ mIsUserTouching = false;
+ cancelDrag();
+ return false;
+ }
+ return false;
+ }
+
+ private boolean handleUpEvent(MotionEvent ev, View animView, float velocity) {
+ // If the menu should not be shown, then there is no need to check if the a swipe
+ // should result in a snapping to the menu. As a result, just check if the swipe
+ // was enough to dismiss the notification.
+ if (!mShouldShowMenu) {
+ if (mSwipeHelper.isDismissGesture(ev)) {
+ dismiss(animView, velocity);
+ } else {
+ snapBack(animView, velocity);
+ }
+ return true;
+ }
+
+ final boolean gestureTowardsMenu = isTowardsMenu(velocity);
+ final boolean gestureFastEnough =
+ mSwipeHelper.getMinDismissVelocity() <= Math.abs(velocity);
+ final boolean gestureFarEnough =
+ mSwipeHelper.swipedFarEnough(mTranslation, mParent.getWidth());
+ final double timeForGesture = ev.getEventTime() - ev.getDownTime();
+ final boolean showMenuForSlowOnGoing = !mParent.canViewBeDismissed()
+ && timeForGesture >= SWIPE_MENU_TIMING;
+ final float menuSnapTarget = mOnLeft ? getSpaceForMenu() : -getSpaceForMenu();
+
+ if (DEBUG) {
+ Log.d(TAG, "mTranslation= " + mTranslation
+ + " mAlpha= " + mAlpha
+ + " velocity= " + velocity
+ + " mMenuSnappedTo= " + mMenuSnappedTo
+ + " mMenuSnappedOnLeft= " + mMenuSnappedOnLeft
+ + " mOnLeft= " + mOnLeft
+ + " minDismissVel= " + mSwipeHelper.getMinDismissVelocity()
+ + " isDismissGesture= " + mSwipeHelper.isDismissGesture(ev)
+ + " gestureTowardsMenu= " + gestureTowardsMenu
+ + " gestureFastEnough= " + gestureFastEnough
+ + " gestureFarEnough= " + gestureFarEnough);
+ }
+
+ if (mMenuSnappedTo && isMenuVisible() && mMenuSnappedOnLeft == mOnLeft) {
+ // Menu was snapped to previously and we're on the same side, figure out if
+ // we should stick to the menu, snap back into place, or dismiss
+ final float maximumSwipeDistance = mHorizSpaceForIcon
+ * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION;
+ final float targetLeft = getSpaceForMenu() - maximumSwipeDistance;
+ final float targetRight = mParent.getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION;
+ boolean withinSnapMenuThreshold = mOnLeft
+ ? mTranslation > targetLeft && mTranslation < targetRight
+ : mTranslation < -targetLeft && mTranslation > -targetRight;
+ boolean shouldSnapTo = mOnLeft ? mTranslation < targetLeft : mTranslation > -targetLeft;
+ if (DEBUG) {
+ Log.d(TAG, " withinSnapMenuThreshold= " + withinSnapMenuThreshold
+ + " shouldSnapTo= " + shouldSnapTo
+ + " targetLeft= " + targetLeft
+ + " targetRight= " + targetRight);
+ }
+ if (withinSnapMenuThreshold && !mSwipeHelper.isDismissGesture(ev)) {
+ // Haven't moved enough to unsnap from the menu
+ showMenu(animView, menuSnapTarget, velocity);
+ } else if (mSwipeHelper.isDismissGesture(ev) && !shouldSnapTo) {
+ // Only dismiss if we're not moving towards the menu
+ dismiss(animView, velocity);
+ } else {
+ snapBack(animView, velocity);
+ }
+ } else if (!mSwipeHelper.isFalseGesture(ev)
+ && (swipedEnoughToShowMenu() && (!gestureFastEnough || showMenuForSlowOnGoing))
+ || (gestureTowardsMenu && !mSwipeHelper.isDismissGesture(ev))) {
+ // Menu has not been snapped to previously and this is menu revealing gesture
+ showMenu(animView, menuSnapTarget, velocity);
+ } else if (mSwipeHelper.isDismissGesture(ev) && !gestureTowardsMenu) {
+ dismiss(animView, velocity);
+ } else {
+ snapBack(animView, velocity);
+ }
+ return true;
+ }
+
+ private void showMenu(View animView, float targetLeft, float velocity) {
+ mMenuSnappedTo = true;
+ mMenuSnappedOnLeft = mOnLeft;
+ mMenuListener.onMenuShown(animView);
+ mSwipeHelper.snap(animView, targetLeft, velocity);
+ }
+
+ private void snapBack(View animView, float velocity) {
+ cancelDrag();
+ mMenuSnappedTo = false;
+ mSnapping = true;
+ mSwipeHelper.snap(animView, 0 /* leftTarget */, velocity);
+ }
+
+ private void dismiss(View animView, float velocity) {
+ cancelDrag();
+ mMenuSnappedTo = false;
+ mDismissing = true;
+ mSwipeHelper.dismiss(animView, velocity);
+ }
+
+ private void cancelDrag() {
+ if (mFadeAnimator != null) {
+ mFadeAnimator.cancel();
+ }
+ mHandler.removeCallbacks(mCheckForDrag);
+ }
+
+ /**
+ * @return whether the notification has been translated enough to show the menu and not enough
+ * to be dismissed.
+ */
+ private boolean swipedEnoughToShowMenu() {
+ final float multiplier = mParent.canViewBeDismissed()
+ ? SWIPED_FAR_ENOUGH_MENU_FRACTION
+ : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION;
+ final float minimumSwipeDistance = mHorizSpaceForIcon * multiplier;
+ return !mSwipeHelper.swipedFarEnough(0, 0) && isMenuVisible()
+ && (mOnLeft ? mTranslation > minimumSwipeDistance
+ : mTranslation < -minimumSwipeDistance);
+ }
+
+ /**
+ * Returns whether the gesture is towards the menu location or not.
+ */
+ private boolean isTowardsMenu(float movement) {
+ return isMenuVisible()
+ && ((mOnLeft && movement <= 0)
+ || (!mOnLeft && movement >= 0));
+ }
+
+ @Override
+ public void setAppName(String appName) {
+ if (appName == null) {
+ return;
+ }
+ Resources res = mContext.getResources();
+ final int count = mMenuItems.size();
+ for (int i = 0; i < count; i++) {
+ MenuItem item = mMenuItems.get(i);
+ String description = String.format(
+ res.getString(R.string.notification_menu_accessibility),
+ appName, item.getContentDescription());
+ View menuView = item.getMenuView();
+ if (menuView != null) {
+ menuView.setContentDescription(description);
+ }
+ }
+ }
+
+ @Override
+ public void onHeightUpdate() {
+ if (mParent == null || mMenuItems.size() == 0 || mMenuContainer == null) {
+ return;
+ }
+ int parentHeight = mParent.getActualHeight();
+ float translationY;
+ if (parentHeight < mVertSpaceForIcons) {
+ translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2);
+ } else {
+ translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2;
+ }
+ mMenuContainer.setTranslationY(translationY);
+ }
+
+ @Override
+ public void onTranslationUpdate(float translation) {
+ mTranslation = translation;
+ if (mAnimating || !mMenuFadedIn) {
+ // Don't adjust when animating, or if the menu hasn't been shown yet.
+ return;
+ }
+ final float fadeThreshold = mParent.getWidth() * 0.3f;
+ final float absTrans = Math.abs(translation);
+ float desiredAlpha = 0;
+ if (absTrans == 0) {
+ desiredAlpha = 0;
+ } else if (absTrans <= fadeThreshold) {
+ desiredAlpha = 1;
+ } else {
+ desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold));
+ }
+ setMenuAlpha(desiredAlpha);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mMenuListener == null) {
+ // Nothing to do
+ return;
+ }
+ v.getLocationOnScreen(mIconLocation);
+ mParent.getLocationOnScreen(mParentLocation);
+ final int centerX = (int) (mHorizSpaceForIcon / 2);
+ final int centerY = v.getHeight() / 2;
+ final int x = mIconLocation[0] - mParentLocation[0] + centerX;
+ final int y = mIconLocation[1] - mParentLocation[1] + centerY;
+ final int index = mMenuContainer.indexOfChild(v);
+ mMenuListener.onMenuClicked(mParent, x, y, mMenuItems.get(index));
+ }
+
+ private boolean isMenuLocationChange() {
+ boolean onLeft = mTranslation > mIconPadding;
+ boolean onRight = mTranslation < -mIconPadding;
+ if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) {
+ return true;
+ }
+ return false;
+ }
+
+ private void setMenuLocation() {
+ boolean showOnLeft = mTranslation > 0;
+ if ((mIconsPlaced && showOnLeft == mOnLeft) || mSnapping || mMenuContainer == null
+ || !mMenuContainer.isAttachedToWindow()) {
+ // Do nothing
+ return;
+ }
+ final int count = mMenuContainer.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View v = mMenuContainer.getChildAt(i);
+ final float left = i * mHorizSpaceForIcon;
+ final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1));
+ v.setX(showOnLeft ? left : right);
+ }
+ mOnLeft = showOnLeft;
+ mIconsPlaced = true;
+ }
+
+ private void setMenuAlpha(float alpha) {
+ mAlpha = alpha;
+ if (mMenuContainer == null) {
+ return;
+ }
+ if (alpha == 0) {
+ mMenuFadedIn = false; // Can fade in again once it's gone.
+ mMenuContainer.setVisibility(View.INVISIBLE);
+ } else {
+ mMenuContainer.setVisibility(View.VISIBLE);
+ }
+ final int count = mMenuContainer.getChildCount();
+ for (int i = 0; i < count; i++) {
+ mMenuContainer.getChildAt(i).setAlpha(mAlpha);
+ }
+ }
+
+ /**
+ * Returns the horizontal space in pixels required to display the menu.
+ */
+ private float getSpaceForMenu() {
+ return mHorizSpaceForIcon * mMenuContainer.getChildCount();
+ }
+
+ private final class CheckForDrag implements Runnable {
+ @Override
+ public void run() {
+ final float absTransX = Math.abs(mTranslation);
+ final float bounceBackToMenuWidth = getSpaceForMenu();
+ final float notiThreshold = mParent.getWidth() * 0.4f;
+ if ((!isMenuVisible() || isMenuLocationChange())
+ && absTransX >= bounceBackToMenuWidth * 0.4
+ && absTransX < notiThreshold) {
+ fadeInMenu(notiThreshold);
+ }
+ }
+ }
+
+ private void fadeInMenu(final float notiThreshold) {
+ if (mDismissing || mAnimating) {
+ return;
+ }
+ if (isMenuLocationChange()) {
+ setMenuAlpha(0f);
+ }
+ final float transX = mTranslation;
+ final boolean fromLeft = mTranslation > 0;
+ setMenuLocation();
+ mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1);
+ mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float absTrans = Math.abs(transX);
+
+ boolean pastMenu = (fromLeft && transX <= notiThreshold)
+ || (!fromLeft && absTrans <= notiThreshold);
+ if (pastMenu && !mMenuFadedIn) {
+ setMenuAlpha((float) animation.getAnimatedValue());
+ }
+ }
+ });
+ mFadeAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mAnimating = true;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ // TODO should animate back to 0f from current alpha
+ setMenuAlpha(0f);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimating = false;
+ mMenuFadedIn = mAlpha == 1;
+ }
+ });
+ mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
+ mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION);
+ mFadeAnimator.start();
+ }
+
+ @Override
+ public void setMenuItems(ArrayList<MenuItem> items) {
+ // Do nothing we use our own for now.
+ // TODO -- handle / allow custom menu items!
+ }
+
+ public static MenuItem createSnoozeItem(Context context) {
+ Resources res = context.getResources();
+ NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context)
+ .inflate(R.layout.notification_snooze, null, false);
+ String snoozeDescription = res.getString(R.string.notification_menu_snooze_description);
+ MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content,
+ R.drawable.ic_snooze);
+ return snooze;
+ }
+
+ public static MenuItem createInfoItem(Context context) {
+ Resources res = context.getResources();
+ String infoDescription = res.getString(R.string.notification_menu_gear_description);
+ NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
+ R.layout.notification_info, null, false);
+ MenuItem info = new NotificationMenuItem(context, infoDescription, infoContent,
+ R.drawable.ic_settings);
+ return info;
+ }
+
+ private void addMenuView(MenuItem item, ViewGroup parent) {
+ View menuView = item.getMenuView();
+ if (menuView != null) {
+ parent.addView(menuView);
+ menuView.setOnClickListener(this);
+ FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams();
+ lp.width = (int) mHorizSpaceForIcon;
+ lp.height = (int) mHorizSpaceForIcon;
+ menuView.setLayoutParams(lp);
+ }
+ }
+
+ public static class NotificationMenuItem implements MenuItem {
+ View mMenuView;
+ GutsContent mGutsContent;
+ String mContentDescription;
+
+ public NotificationMenuItem(Context context, String s, GutsContent content, int iconResId) {
+ Resources res = context.getResources();
+ int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
+ int tint = res.getColor(R.color.notification_gear_color);
+ AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context);
+ iv.setPadding(padding, padding, padding, padding);
+ Drawable icon = context.getResources().getDrawable(iconResId);
+ iv.setImageDrawable(icon);
+ iv.setColorFilter(tint);
+ iv.setAlpha(1f);
+ mMenuView = iv;
+ mContentDescription = s;
+ mGutsContent = content;
+ }
+
+ @Override
+ public View getMenuView() {
+ return mMenuView;
+ }
+
+ @Override
+ public View getGutsView() {
+ return mGutsContent.getContentView();
+ }
+
+ @Override
+ public String getContentDescription() {
+ return mContentDescription;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationShelf.java b/com/android/systemui/statusbar/NotificationShelf.java
new file mode 100644
index 0000000..5557dde
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationShelf.java
@@ -0,0 +1,825 @@
+/*
+ * Copyright (C) 2016 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 static com.android.systemui.statusbar.phone.NotificationIconContainer.IconState.NO_VALUE;
+import static com.android.systemui.statusbar.phone.NotificationIconContainer.OVERFLOW_EARLY_AMOUNT;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.SystemProperties;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.phone.NotificationIconContainer;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+import com.android.systemui.statusbar.stack.AmbientState;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackScrollState;
+import com.android.systemui.statusbar.stack.ViewState;
+
+/**
+ * A notification shelf view that is placed inside the notification scroller. It manages the
+ * overflow icons that don't fit into the regular list anymore.
+ */
+public class NotificationShelf extends ActivatableNotificationView implements
+ View.OnLayoutChangeListener {
+
+ public static final boolean SHOW_AMBIENT_ICONS = true;
+ private static final boolean USE_ANIMATIONS_WHEN_OPENING =
+ SystemProperties.getBoolean("debug.icon_opening_animations", true);
+ private static final boolean ICON_ANMATIONS_WHILE_SCROLLING
+ = SystemProperties.getBoolean("debug.icon_scroll_animations", true);
+ private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
+ private ViewInvertHelper mViewInvertHelper;
+ private boolean mDark;
+ private NotificationIconContainer mShelfIcons;
+ private ShelfState mShelfState;
+ private int[] mTmp = new int[2];
+ private boolean mHideBackground;
+ private int mIconAppearTopPadding;
+ private int mStatusBarHeight;
+ private int mStatusBarPaddingStart;
+ private AmbientState mAmbientState;
+ private NotificationStackScrollLayout mHostLayout;
+ private int mMaxLayoutHeight;
+ private int mPaddingBetweenElements;
+ private int mNotGoneIndex;
+ private boolean mHasItemsInStableShelf;
+ private NotificationIconContainer mCollapsedIcons;
+ private int mScrollFastThreshold;
+ private int mIconSize;
+ private int mStatusBarState;
+ private float mMaxShelfEnd;
+ private int mRelativeOffset;
+ private boolean mInteractive;
+ private float mOpenedAmount;
+ private boolean mNoAnimationsInThisFrame;
+ private boolean mAnimationsEnabled = true;
+ private boolean mShowNotificationShelf;
+ private boolean mVibrationOnAnimation;
+ private boolean mUserTouchingScreen;
+ private boolean mTouchActive;
+
+ public NotificationShelf(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mShelfIcons = findViewById(R.id.content);
+ mShelfIcons.setClipChildren(false);
+ mShelfIcons.setClipToPadding(false);
+
+ setClipToActualHeight(false);
+ setClipChildren(false);
+ setClipToPadding(false);
+ mShelfIcons.setShowAllIcons(false);
+ mVibrationOnAnimation = mContext.getResources().getBoolean(
+ R.bool.config_vibrateOnIconAnimation);
+ updateVibrationOnAnimation();
+ mViewInvertHelper = new ViewInvertHelper(mShelfIcons,
+ NotificationPanelView.DOZE_ANIMATION_DURATION);
+ mShelfState = new ShelfState();
+ initDimens();
+ }
+
+ private void updateVibrationOnAnimation() {
+ mShelfIcons.setVibrateOnAnimation(mVibrationOnAnimation && mTouchActive);
+ }
+
+ public void setTouchActive(boolean touchActive) {
+ mTouchActive = touchActive;
+ updateVibrationOnAnimation();
+ }
+
+ public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout) {
+ mAmbientState = ambientState;
+ mHostLayout = hostLayout;
+ }
+
+ private void initDimens() {
+ Resources res = getResources();
+ mIconAppearTopPadding = res.getDimensionPixelSize(R.dimen.notification_icon_appear_padding);
+ mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height);
+ mStatusBarPaddingStart = res.getDimensionPixelOffset(R.dimen.status_bar_padding_start);
+ mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
+
+ ViewGroup.LayoutParams layoutParams = getLayoutParams();
+ layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
+ setLayoutParams(layoutParams);
+
+ int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
+ mShelfIcons.setPadding(padding, 0, padding, 0);
+ mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold);
+ mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf);
+ mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
+
+ if (!mShowNotificationShelf) {
+ setVisibility(GONE);
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ initDimens();
+ }
+
+ @Override
+ public void setDark(boolean dark, boolean fade, long delay) {
+ super.setDark(dark, fade, delay);
+ if (mDark == dark) return;
+ mDark = dark;
+ mShelfIcons.setDark(dark, fade, delay);
+ updateInteractiveness();
+ }
+
+ @Override
+ protected View getContentView() {
+ return mShelfIcons;
+ }
+
+ public NotificationIconContainer getShelfIcons() {
+ return mShelfIcons;
+ }
+
+ @Override
+ public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+ return mShelfState;
+ }
+
+ public void updateState(StackScrollState resultState,
+ AmbientState ambientState) {
+ View lastView = ambientState.getLastVisibleBackgroundChild();
+ if (mShowNotificationShelf && lastView != null) {
+ float maxShelfEnd = ambientState.getInnerHeight() + ambientState.getTopPadding()
+ + ambientState.getStackTranslation();
+ ExpandableViewState lastViewState = resultState.getViewStateForView(lastView);
+ float viewEnd = lastViewState.yTranslation + lastViewState.height;
+ mShelfState.copyFrom(lastViewState);
+ mShelfState.height = getIntrinsicHeight();
+ mShelfState.yTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - mShelfState.height,
+ getFullyClosedTranslation());
+ mShelfState.zTranslation = ambientState.getBaseZHeight();
+ float openedAmount = (mShelfState.yTranslation - getFullyClosedTranslation())
+ / (getIntrinsicHeight() * 2);
+ openedAmount = Math.min(1.0f, openedAmount);
+ mShelfState.openedAmount = openedAmount;
+ mShelfState.clipTopAmount = 0;
+ mShelfState.alpha = mAmbientState.hasPulsingNotifications() ? 0 : 1;
+ mShelfState.belowSpeedBump = mAmbientState.getSpeedBumpIndex() == 0;
+ mShelfState.shadowAlpha = 1.0f;
+ mShelfState.hideSensitive = false;
+ mShelfState.xTranslation = getTranslationX();
+ if (mNotGoneIndex != -1) {
+ mShelfState.notGoneIndex = Math.min(mShelfState.notGoneIndex, mNotGoneIndex);
+ }
+ mShelfState.hasItemsInStableShelf = lastViewState.inShelf;
+ mShelfState.hidden = !mAmbientState.isShadeExpanded()
+ || mAmbientState.isQsCustomizerShowing();
+ mShelfState.maxShelfEnd = maxShelfEnd;
+ } else {
+ mShelfState.hidden = true;
+ mShelfState.location = ExpandableViewState.LOCATION_GONE;
+ mShelfState.hasItemsInStableShelf = false;
+ }
+ }
+
+ /**
+ * Update the shelf appearance based on the other notifications around it. This transforms
+ * the icons from the notification area into the shelf.
+ */
+ public void updateAppearance() {
+ // If the shelf should not be shown, then there is no need to update anything.
+ if (!mShowNotificationShelf) {
+ return;
+ }
+
+ mShelfIcons.resetViewStates();
+ float shelfStart = getTranslationY();
+ float numViewsInShelf = 0.0f;
+ View lastChild = mAmbientState.getLastVisibleBackgroundChild();
+ mNotGoneIndex = -1;
+ float interpolationStart = mMaxLayoutHeight - getIntrinsicHeight() * 2;
+ float expandAmount = 0.0f;
+ if (shelfStart >= interpolationStart) {
+ expandAmount = (shelfStart - interpolationStart) / getIntrinsicHeight();
+ expandAmount = Math.min(1.0f, expandAmount);
+ }
+ // find the first view that doesn't overlap with the shelf
+ int notificationIndex = 0;
+ int notGoneIndex = 0;
+ int colorOfViewBeforeLast = NO_COLOR;
+ boolean backgroundForceHidden = false;
+ if (mHideBackground && !mShelfState.hasItemsInStableShelf) {
+ backgroundForceHidden = true;
+ }
+ int colorTwoBefore = NO_COLOR;
+ int previousColor = NO_COLOR;
+ float transitionAmount = 0.0f;
+ float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity();
+ boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold
+ || (mAmbientState.isExpansionChanging()
+ && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold);
+ boolean scrolling = currentScrollVelocity > 0;
+ boolean expandingAnimated = mAmbientState.isExpansionChanging()
+ && !mAmbientState.isPanelTracking();
+ int baseZHeight = mAmbientState.getBaseZHeight();
+ while (notificationIndex < mHostLayout.getChildCount()) {
+ ExpandableView child = (ExpandableView) mHostLayout.getChildAt(notificationIndex);
+ notificationIndex++;
+ if (!(child instanceof ExpandableNotificationRow)
+ || child.getVisibility() == GONE) {
+ continue;
+ }
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ float notificationClipEnd;
+ boolean aboveShelf = ViewState.getFinalTranslationZ(row) > baseZHeight;
+ boolean isLastChild = child == lastChild;
+ float rowTranslationY = row.getTranslationY();
+ if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) {
+ notificationClipEnd = shelfStart + getIntrinsicHeight();
+ } else {
+ notificationClipEnd = shelfStart - mPaddingBetweenElements;
+ float height = notificationClipEnd - rowTranslationY;
+ if (!row.isBelowSpeedBump() && height <= getNotificationMergeSize()) {
+ // We want the gap to close when we reached the minimum size and only shrink
+ // before
+ notificationClipEnd = Math.min(shelfStart,
+ rowTranslationY + getNotificationMergeSize());
+ }
+ }
+ updateNotificationClipHeight(row, notificationClipEnd);
+ float inShelfAmount = updateIconAppearance(row, expandAmount, scrolling, scrollingFast,
+ expandingAnimated, isLastChild);
+ numViewsInShelf += inShelfAmount;
+ int ownColorUntinted = row.getBackgroundColorWithoutTint();
+ if (rowTranslationY >= shelfStart && mNotGoneIndex == -1) {
+ mNotGoneIndex = notGoneIndex;
+ setTintColor(previousColor);
+ setOverrideTintColor(colorTwoBefore, transitionAmount);
+
+ } else if (mNotGoneIndex == -1) {
+ colorTwoBefore = previousColor;
+ transitionAmount = inShelfAmount;
+ }
+ if (isLastChild) {
+ if (colorOfViewBeforeLast == NO_COLOR) {
+ colorOfViewBeforeLast = ownColorUntinted;
+ }
+ row.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount);
+ } else {
+ colorOfViewBeforeLast = ownColorUntinted;
+ row.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */);
+ }
+ if (notGoneIndex != 0 || !aboveShelf) {
+ row.setAboveShelf(false);
+ }
+ notGoneIndex++;
+ previousColor = ownColorUntinted;
+ }
+ mShelfIcons.setSpeedBumpIndex(mAmbientState.getSpeedBumpIndex());
+ mShelfIcons.calculateIconTranslations();
+ mShelfIcons.applyIconStates();
+ for (int i = 0; i < mHostLayout.getChildCount(); i++) {
+ View child = mHostLayout.getChildAt(i);
+ if (!(child instanceof ExpandableNotificationRow)
+ || child.getVisibility() == GONE) {
+ continue;
+ }
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ updateIconClipAmount(row);
+ updateContinuousClipping(row);
+ }
+ boolean hideBackground = numViewsInShelf < 1.0f;
+ setHideBackground(hideBackground || backgroundForceHidden);
+ if (mNotGoneIndex == -1) {
+ mNotGoneIndex = notGoneIndex;
+ }
+ }
+
+ private void updateIconClipAmount(ExpandableNotificationRow row) {
+ float maxTop = row.getTranslationY();
+ StatusBarIconView icon = row.getEntry().expandedIcon;
+ float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY();
+ if (shelfIconPosition < maxTop) {
+ int top = (int) (maxTop - shelfIconPosition);
+ Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight()));
+ icon.setClipBounds(clipRect);
+ } else {
+ icon.setClipBounds(null);
+ }
+ }
+
+ private void updateContinuousClipping(final ExpandableNotificationRow row) {
+ StatusBarIconView icon = row.getEntry().expandedIcon;
+ boolean needsContinuousClipping = ViewState.isAnimatingY(icon);
+ boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null;
+ if (needsContinuousClipping && !isContinuousClipping) {
+ ViewTreeObserver.OnPreDrawListener predrawListener =
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ boolean animatingY = ViewState.isAnimatingY(icon);
+ if (!animatingY || !icon.isAttachedToWindow()) {
+ icon.getViewTreeObserver().removeOnPreDrawListener(this);
+ icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
+ return true;
+ }
+ updateIconClipAmount(row);
+ return true;
+ }
+ };
+ icon.getViewTreeObserver().addOnPreDrawListener(predrawListener);
+ icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener);
+ }
+ }
+
+ private void updateNotificationClipHeight(ExpandableNotificationRow row,
+ float notificationClipEnd) {
+ float viewEnd = row.getTranslationY() + row.getActualHeight();
+ boolean isPinned = (row.isPinned() || row.isHeadsUpAnimatingAway())
+ && !mAmbientState.isDozingAndNotPulsing(row);
+ if (viewEnd > notificationClipEnd
+ && (mAmbientState.isShadeExpanded() || !isPinned)) {
+ int clipBottomAmount = (int) (viewEnd - notificationClipEnd);
+ if (isPinned) {
+ clipBottomAmount = Math.min(row.getIntrinsicHeight() - row.getCollapsedHeight(),
+ clipBottomAmount);
+ }
+ row.setClipBottomAmount(clipBottomAmount);
+ } else {
+ row.setClipBottomAmount(0);
+ }
+ }
+
+ @Override
+ public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
+ int outlineTranslation) {
+ if (!mHasItemsInStableShelf) {
+ shadowIntensity = 0.0f;
+ }
+ super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation);
+ }
+
+ /**
+ * @return the icon amount how much this notification is in the shelf;
+ */
+ private float updateIconAppearance(ExpandableNotificationRow row, float expandAmount,
+ boolean scrolling, boolean scrollingFast, boolean expandingAnimated,
+ boolean isLastChild) {
+ StatusBarIconView icon = row.getEntry().expandedIcon;
+ NotificationIconContainer.IconState iconState = getIconState(icon);
+ if (iconState == null) {
+ return 0.0f;
+ }
+
+ // Let calculate how much the view is in the shelf
+ float viewStart = row.getTranslationY();
+ int fullHeight = row.getActualHeight() + mPaddingBetweenElements;
+ float iconTransformDistance = getIntrinsicHeight() * 1.5f;
+ iconTransformDistance *= NotificationUtils.interpolate(1.f, 1.5f, expandAmount);
+ iconTransformDistance = Math.min(iconTransformDistance, fullHeight);
+ if (isLastChild) {
+ fullHeight = Math.min(fullHeight, row.getMinHeight() - getIntrinsicHeight());
+ iconTransformDistance = Math.min(iconTransformDistance, row.getMinHeight()
+ - getIntrinsicHeight());
+ }
+ float viewEnd = viewStart + fullHeight;
+ if (expandingAnimated && mAmbientState.getScrollY() == 0
+ && !mAmbientState.isOnKeyguard() && !iconState.isLastExpandIcon) {
+ // We are expanding animated. Because we switch to a linear interpolation in this case,
+ // the last icon may be stuck in between the shelf position and the notification
+ // position, which looks pretty bad. We therefore optimize this case by applying a
+ // shorter transition such that the icon is either fully in the notification or we clamp
+ // it into the shelf if it's close enough.
+ // We need to persist this, since after the expansion, the behavior should still be the
+ // same.
+ float position = mAmbientState.getIntrinsicPadding()
+ + mHostLayout.getPositionInLinearLayout(row);
+ int maxShelfStart = mMaxLayoutHeight - getIntrinsicHeight();
+ if (position < maxShelfStart && position + row.getIntrinsicHeight() >= maxShelfStart
+ && row.getTranslationY() < position) {
+ iconState.isLastExpandIcon = true;
+ iconState.customTransformHeight = NO_VALUE;
+ // Let's check if we're close enough to snap into the shelf
+ boolean forceInShelf = mMaxLayoutHeight - getIntrinsicHeight() - position
+ < getIntrinsicHeight();
+ if (!forceInShelf) {
+ // We are overlapping the shelf but not enough, so the icon needs to be
+ // repositioned
+ iconState.customTransformHeight = (int) (mMaxLayoutHeight
+ - getIntrinsicHeight() - position);
+ }
+ }
+ }
+ float fullTransitionAmount;
+ float iconTransitionAmount;
+ float shelfStart = getTranslationY();
+ if (iconState.hasCustomTransformHeight()) {
+ fullHeight = iconState.customTransformHeight;
+ iconTransformDistance = iconState.customTransformHeight;
+ }
+ boolean fullyInOrOut = true;
+ if (viewEnd >= shelfStart && (!mAmbientState.isUnlockHintRunning() || row.isInShelf())
+ && (mAmbientState.isShadeExpanded()
+ || (!row.isPinned() && !row.isHeadsUpAnimatingAway()))) {
+ if (viewStart < shelfStart) {
+ float fullAmount = (shelfStart - viewStart) / fullHeight;
+ fullAmount = Math.min(1.0f, fullAmount);
+ float interpolatedAmount = Interpolators.ACCELERATE_DECELERATE.getInterpolation(
+ fullAmount);
+ interpolatedAmount = NotificationUtils.interpolate(
+ interpolatedAmount, fullAmount, expandAmount);
+ fullTransitionAmount = 1.0f - interpolatedAmount;
+
+ iconTransitionAmount = (shelfStart - viewStart) / iconTransformDistance;
+ iconTransitionAmount = Math.min(1.0f, iconTransitionAmount);
+ iconTransitionAmount = 1.0f - iconTransitionAmount;
+ fullyInOrOut = false;
+ } else {
+ fullTransitionAmount = 1.0f;
+ iconTransitionAmount = 1.0f;
+ }
+ } else {
+ fullTransitionAmount = 0.0f;
+ iconTransitionAmount = 0.0f;
+ }
+ if (fullyInOrOut && !expandingAnimated && iconState.isLastExpandIcon) {
+ iconState.isLastExpandIcon = false;
+ iconState.customTransformHeight = NO_VALUE;
+ }
+ updateIconPositioning(row, iconTransitionAmount, fullTransitionAmount,
+ iconTransformDistance, scrolling, scrollingFast, expandingAnimated, isLastChild);
+ return fullTransitionAmount;
+ }
+
+ private void updateIconPositioning(ExpandableNotificationRow row, float iconTransitionAmount,
+ float fullTransitionAmount, float iconTransformDistance, boolean scrolling,
+ boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) {
+ StatusBarIconView icon = row.getEntry().expandedIcon;
+ NotificationIconContainer.IconState iconState = getIconState(icon);
+ if (iconState == null) {
+ return;
+ }
+ boolean forceInShelf = iconState.isLastExpandIcon && !iconState.hasCustomTransformHeight();
+ float clampedAmount = iconTransitionAmount > 0.5f ? 1.0f : 0.0f;
+ if (clampedAmount == fullTransitionAmount) {
+ iconState.noAnimations = (scrollingFast || expandingAnimated) && !forceInShelf;
+ iconState.useFullTransitionAmount = iconState.noAnimations
+ || (!ICON_ANMATIONS_WHILE_SCROLLING && fullTransitionAmount == 0.0f && scrolling);
+ iconState.useLinearTransitionAmount = !ICON_ANMATIONS_WHILE_SCROLLING
+ && fullTransitionAmount == 0.0f && !mAmbientState.isExpansionChanging();
+ iconState.translateContent = mMaxLayoutHeight - getTranslationY()
+ - getIntrinsicHeight() > 0;
+ }
+ if (!forceInShelf && (scrollingFast || (expandingAnimated
+ && iconState.useFullTransitionAmount && !ViewState.isAnimatingY(icon)))) {
+ iconState.cancelAnimations(icon);
+ iconState.useFullTransitionAmount = true;
+ iconState.noAnimations = true;
+ }
+ if (iconState.hasCustomTransformHeight()) {
+ iconState.useFullTransitionAmount = true;
+ }
+ if (iconState.isLastExpandIcon) {
+ iconState.translateContent = false;
+ }
+ float transitionAmount;
+ if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount
+ || iconState.useLinearTransitionAmount) {
+ transitionAmount = iconTransitionAmount;
+ } else {
+ // We take the clamped position instead
+ transitionAmount = clampedAmount;
+ iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount
+ && !mNoAnimationsInThisFrame;
+ }
+ iconState.iconAppearAmount = !USE_ANIMATIONS_WHEN_OPENING
+ || iconState.useFullTransitionAmount
+ ? fullTransitionAmount
+ : transitionAmount;
+ iconState.clampedAppearAmount = clampedAmount;
+ float contentTransformationAmount = !mAmbientState.isAboveShelf(row)
+ && (isLastChild || iconState.translateContent)
+ ? iconTransitionAmount
+ : 0.0f;
+ row.setContentTransformationAmount(contentTransformationAmount, isLastChild);
+ setIconTransformationAmount(row, transitionAmount, iconTransformDistance,
+ clampedAmount != transitionAmount, isLastChild);
+ }
+
+ private void setIconTransformationAmount(ExpandableNotificationRow row,
+ float transitionAmount, float iconTransformDistance, boolean usingLinearInterpolation,
+ boolean isLastChild) {
+ StatusBarIconView icon = row.getEntry().expandedIcon;
+ NotificationIconContainer.IconState iconState = getIconState(icon);
+
+ View rowIcon = row.getNotificationIcon();
+ float notificationIconPosition = row.getTranslationY() + row.getContentTranslation();
+ boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf();
+ if (usingLinearInterpolation && !stayingInShelf) {
+ // If we interpolate from the notification position, this might lead to a slightly
+ // odd interpolation, since the notification position changes as well. Let's interpolate
+ // from a fixed distance. We can only do this if we don't animate and the icon is
+ // always in the interpolated positon.
+ notificationIconPosition = getTranslationY() - iconTransformDistance;
+ }
+ float notificationIconSize = 0.0f;
+ int iconTopPadding;
+ if (rowIcon != null) {
+ iconTopPadding = row.getRelativeTopPadding(rowIcon);
+ notificationIconSize = rowIcon.getHeight();
+ } else {
+ iconTopPadding = mIconAppearTopPadding;
+ }
+ notificationIconPosition += iconTopPadding;
+ float shelfIconPosition = getTranslationY() + icon.getTop();
+ shelfIconPosition += (icon.getHeight() - icon.getIconScale() * mIconSize) / 2.0f;
+ float iconYTranslation = NotificationUtils.interpolate(
+ notificationIconPosition - shelfIconPosition,
+ 0,
+ transitionAmount);
+ float shelfIconSize = mIconSize * icon.getIconScale();
+ float alpha = 1.0f;
+ boolean noIcon = !row.isShowingIcon();
+ if (noIcon) {
+ // The view currently doesn't have an icon, lets transform it in!
+ alpha = transitionAmount;
+ notificationIconSize = shelfIconSize / 2.0f;
+ }
+ // The notification size is different from the size in the shelf / statusbar
+ float newSize = NotificationUtils.interpolate(notificationIconSize, shelfIconSize,
+ transitionAmount);
+ if (iconState != null) {
+ iconState.scaleX = newSize / shelfIconSize;
+ iconState.scaleY = iconState.scaleX;
+ iconState.hidden = transitionAmount == 0.0f && !iconState.isAnimating(icon);
+ boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf();
+ if (isAppearing) {
+ iconState.hidden = true;
+ iconState.iconAppearAmount = 0.0f;
+ }
+ iconState.alpha = alpha;
+ iconState.yTranslation = iconYTranslation;
+ if (stayingInShelf) {
+ iconState.iconAppearAmount = 1.0f;
+ iconState.alpha = 1.0f;
+ iconState.scaleX = 1.0f;
+ iconState.scaleY = 1.0f;
+ iconState.hidden = false;
+ }
+ if (mAmbientState.isAboveShelf(row) || (!row.isInShelf() && (isLastChild && row.areGutsExposed()
+ || row.getTranslationZ() > mAmbientState.getBaseZHeight()))) {
+ iconState.hidden = true;
+ }
+ int backgroundColor = getBackgroundColorWithoutTint();
+ int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor);
+ if (!noIcon && shelfColor != StatusBarIconView.NO_COLOR) {
+ int iconColor = row.getVisibleNotificationHeader().getOriginalIconColor();
+ shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor,
+ iconState.iconAppearAmount);
+ }
+ iconState.iconColor = shelfColor;
+ }
+ }
+
+ private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) {
+ return mShelfIcons.getIconState(icon);
+ }
+
+ private float getFullyClosedTranslation() {
+ return - (getIntrinsicHeight() - mStatusBarHeight) / 2;
+ }
+
+ public int getNotificationMergeSize() {
+ return getIntrinsicHeight();
+ }
+
+ @Override
+ public boolean hasNoContentHeight() {
+ return true;
+ }
+
+ private void setHideBackground(boolean hideBackground) {
+ if (mHideBackground != hideBackground) {
+ mHideBackground = hideBackground;
+ updateBackground();
+ updateOutline();
+ }
+ }
+
+ public boolean hidesBackground() {
+ return mHideBackground;
+ }
+
+ @Override
+ protected boolean needsOutline() {
+ return !mHideBackground && super.needsOutline();
+ }
+
+ @Override
+ protected boolean shouldHideBackground() {
+ return super.shouldHideBackground() || mHideBackground;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ updateRelativeOffset();
+ }
+
+ private void updateRelativeOffset() {
+ mCollapsedIcons.getLocationOnScreen(mTmp);
+ mRelativeOffset = mTmp[0];
+ getLocationOnScreen(mTmp);
+ mRelativeOffset -= mTmp[0];
+ }
+
+ private void setOpenedAmount(float openedAmount) {
+ mNoAnimationsInThisFrame = openedAmount == 1.0f && mOpenedAmount == 0.0f;
+ mOpenedAmount = openedAmount;
+ if (!mAmbientState.isPanelFullWidth()) {
+ // We don't do a transformation at all, lets just assume we are fully opened
+ openedAmount = 1.0f;
+ }
+ int start = mRelativeOffset;
+ if (isLayoutRtl()) {
+ start = getWidth() - start - mCollapsedIcons.getWidth();
+ }
+ int width = (int) NotificationUtils.interpolate(start + mCollapsedIcons.getWidth(),
+ mShelfIcons.getWidth(),
+ openedAmount);
+ mShelfIcons.setActualLayoutWidth(width);
+ boolean hasOverflow = mCollapsedIcons.hasOverflow();
+ int collapsedPadding = mCollapsedIcons.getPaddingEnd();
+ if (!hasOverflow) {
+ // we have to ensure that adding the low priority notification won't lead to an
+ // overflow
+ collapsedPadding -= (1.0f + OVERFLOW_EARLY_AMOUNT) * mCollapsedIcons.getIconSize();
+ }
+ float padding = NotificationUtils.interpolate(collapsedPadding,
+ mShelfIcons.getPaddingEnd(),
+ openedAmount);
+ mShelfIcons.setActualPaddingEnd(padding);
+ float paddingStart = NotificationUtils.interpolate(start,
+ mShelfIcons.getPaddingStart(), openedAmount);
+ mShelfIcons.setActualPaddingStart(paddingStart);
+ mShelfIcons.setOpenedAmount(openedAmount);
+ mShelfIcons.setVisualOverflowAdaption(mCollapsedIcons.getVisualOverflowAdaption());
+ }
+
+ public void setMaxLayoutHeight(int maxLayoutHeight) {
+ mMaxLayoutHeight = maxLayoutHeight;
+ }
+
+ /**
+ * @return the index of the notification at which the shelf visually resides
+ */
+ public int getNotGoneIndex() {
+ return mNotGoneIndex;
+ }
+
+ private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) {
+ if (mHasItemsInStableShelf != hasItemsInStableShelf) {
+ mHasItemsInStableShelf = hasItemsInStableShelf;
+ updateInteractiveness();
+ }
+ }
+
+ /**
+ * @return whether the shelf has any icons in it when a potential animation has finished, i.e
+ * if the current state would be applied right now
+ */
+ public boolean hasItemsInStableShelf() {
+ return mHasItemsInStableShelf;
+ }
+
+ public void setCollapsedIcons(NotificationIconContainer collapsedIcons) {
+ mCollapsedIcons = collapsedIcons;
+ mCollapsedIcons.addOnLayoutChangeListener(this);
+ }
+
+ public void setStatusBarState(int statusBarState) {
+ if (mStatusBarState != statusBarState) {
+ mStatusBarState = statusBarState;
+ updateInteractiveness();
+ }
+ }
+
+ private void updateInteractiveness() {
+ mInteractive = mStatusBarState == StatusBarState.KEYGUARD && mHasItemsInStableShelf
+ && !mDark;
+ setClickable(mInteractive);
+ setFocusable(mInteractive);
+ setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ }
+
+ @Override
+ protected boolean isInteractive() {
+ return mInteractive;
+ }
+
+ public void setMaxShelfEnd(float maxShelfEnd) {
+ mMaxShelfEnd = maxShelfEnd;
+ }
+
+ public void setAnimationsEnabled(boolean enabled) {
+ mAnimationsEnabled = enabled;
+ mCollapsedIcons.setAnimationsEnabled(enabled);
+ if (!enabled) {
+ // we need to wait with enabling the animations until the first frame has passed
+ mShelfIcons.setAnimationsEnabled(false);
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false; // Shelf only uses alpha for transitions where the difference can't be seen.
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ if (mInteractive) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+ AccessibilityNodeInfo.AccessibilityAction unlock
+ = new AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_CLICK,
+ getContext().getString(R.string.accessibility_overflow_action));
+ info.addAction(unlock);
+ }
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ updateRelativeOffset();
+ }
+
+ public void setDarkOffsetX(int offsetX) {
+ mShelfIcons.setDarkOffsetX(offsetX);
+ }
+
+ private class ShelfState extends ExpandableViewState {
+ private float openedAmount;
+ private boolean hasItemsInStableShelf;
+ private float maxShelfEnd;
+
+ @Override
+ public void applyToView(View view) {
+ if (!mShowNotificationShelf) {
+ return;
+ }
+
+ super.applyToView(view);
+ setMaxShelfEnd(maxShelfEnd);
+ setOpenedAmount(openedAmount);
+ updateAppearance();
+ setHasItemsInStableShelf(hasItemsInStableShelf);
+ mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
+ }
+
+ @Override
+ public void animateTo(View child, AnimationProperties properties) {
+ if (!mShowNotificationShelf) {
+ return;
+ }
+
+ super.animateTo(child, properties);
+ setMaxShelfEnd(maxShelfEnd);
+ setOpenedAmount(openedAmount);
+ updateAppearance();
+ setHasItemsInStableShelf(hasItemsInStableShelf);
+ mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationSnooze.java b/com/android/systemui/statusbar/NotificationSnooze.java
new file mode 100644
index 0000000..c45ca54
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationSnooze.java
@@ -0,0 +1,399 @@
+package com.android.systemui.statusbar;
+/*
+ * Copyright (C) 2017 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
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.service.notification.SnoozeCriterion;
+import android.service.notification.StatusBarNotification;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+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;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+public class NotificationSnooze extends LinearLayout
+ implements NotificationGuts.GutsContent, View.OnClickListener {
+
+ /**
+ * If this changes more number increases, more assistant action resId's should be defined for
+ * accessibility purposes, see {@link #setSnoozeOptions(List)}
+ */
+ private static final int MAX_ASSISTANT_SUGGESTIONS = 1;
+ private NotificationGuts mGutsContainer;
+ private NotificationSwipeActionHelper mSnoozeListener;
+ private StatusBarNotification mSbn;
+
+ private TextView mSelectedOptionText;
+ private TextView mUndoButton;
+ private ImageView mExpandButton;
+ private View mDivider;
+ private ViewGroup mSnoozeOptionContainer;
+ private List<SnoozeOption> mSnoozeOptions;
+ private int mCollapsedHeight;
+ private SnoozeOption mDefaultOption;
+ private SnoozeOption mSelectedOption;
+ private boolean mSnoozing;
+ private boolean mExpanded;
+ private AnimatorSet mExpandAnimation;
+
+ public NotificationSnooze(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mCollapsedHeight = getResources().getDimensionPixelSize(R.dimen.snooze_snackbar_min_height);
+ findViewById(R.id.notification_snooze).setOnClickListener(this);
+ mSelectedOptionText = (TextView) findViewById(R.id.snooze_option_default);
+ mUndoButton = (TextView) findViewById(R.id.undo);
+ mUndoButton.setOnClickListener(this);
+ mExpandButton = (ImageView) findViewById(R.id.expand_button);
+ mDivider = findViewById(R.id.divider);
+ mDivider.setAlpha(0f);
+ mSnoozeOptionContainer = (ViewGroup) findViewById(R.id.snooze_options);
+ mSnoozeOptionContainer.setVisibility(View.INVISIBLE);
+ mSnoozeOptionContainer.setAlpha(0f);
+
+ // Create the different options based on list
+ mSnoozeOptions = getDefaultSnoozeOptions();
+ createOptionViews();
+
+ setSelected(mDefaultOption);
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ if (mGutsContainer != null && mGutsContainer.isExposed()) {
+ if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ event.getText().add(mSelectedOptionText.getText());
+ }
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.addAction(new AccessibilityAction(R.id.action_snooze_undo,
+ getResources().getString(R.string.snooze_undo)));
+ int count = mSnoozeOptions.size();
+ for (int i = 0; i < count; i++) {
+ AccessibilityAction action = mSnoozeOptions.get(i).getAccessibilityAction();
+ if (action != null) {
+ info.addAction(action);
+ }
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ if (action == R.id.action_snooze_undo) {
+ undoSnooze(mUndoButton);
+ return true;
+ }
+ for (int i = 0; i < mSnoozeOptions.size(); i++) {
+ SnoozeOption so = mSnoozeOptions.get(i);
+ if (so.getAccessibilityAction() != null
+ && so.getAccessibilityAction().getId() == action) {
+ setSelected(so);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setSnoozeOptions(final List<SnoozeCriterion> snoozeList) {
+ if (snoozeList == null) {
+ return;
+ }
+ mSnoozeOptions.clear();
+ mSnoozeOptions = getDefaultSnoozeOptions();
+ final int count = Math.min(MAX_ASSISTANT_SUGGESTIONS, snoozeList.size());
+ for (int i = 0; i < count; i++) {
+ SnoozeCriterion sc = snoozeList.get(i);
+ AccessibilityAction action = new AccessibilityAction(
+ R.id.action_snooze_assistant_suggestion_1, sc.getExplanation());
+ mSnoozeOptions.add(new NotificationSnoozeOption(sc, 0, sc.getExplanation(),
+ sc.getConfirmation(), action));
+ }
+ createOptionViews();
+ }
+
+ public boolean isExpanded() {
+ return mExpanded;
+ }
+
+ public void setSnoozeListener(NotificationSwipeActionHelper listener) {
+ mSnoozeListener = listener;
+ }
+
+ public void setStatusBarNotification(StatusBarNotification sbn) {
+ mSbn = sbn;
+ }
+
+ private ArrayList<SnoozeOption> getDefaultSnoozeOptions() {
+ ArrayList<SnoozeOption> options = new ArrayList<>();
+
+ options.add(createOption(15 /* minutes */, R.id.action_snooze_15_min));
+ options.add(createOption(30 /* minutes */, R.id.action_snooze_30_min));
+ mDefaultOption = createOption(60 /* minutes */, R.id.action_snooze_1_hour);
+ options.add(mDefaultOption);
+ options.add(createOption(60 * 2 /* minutes */, R.id.action_snooze_2_hours));
+ return options;
+ }
+
+ private SnoozeOption createOption(int minutes, int accessibilityActionId) {
+ Resources res = getResources();
+ boolean showInHours = minutes >= 60;
+ int pluralResId = showInHours
+ ? R.plurals.snoozeHourOptions
+ : R.plurals.snoozeMinuteOptions;
+ int count = showInHours ? (minutes / 60) : minutes;
+ String description = res.getQuantityString(pluralResId, count, count);
+ String resultText = String.format(res.getString(R.string.snoozed_for_time), description);
+ SpannableString string = new SpannableString(resultText);
+ string.setSpan(new StyleSpan(Typeface.BOLD),
+ resultText.length() - description.length(), resultText.length(), 0 /* flags */);
+ AccessibilityAction action = new AccessibilityAction(accessibilityActionId, description);
+ return new NotificationSnoozeOption(null, minutes, description, string,
+ action);
+ }
+
+ private void createOptionViews() {
+ mSnoozeOptionContainer.removeAllViews();
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ for (int i = 0; i < mSnoozeOptions.size(); i++) {
+ SnoozeOption option = mSnoozeOptions.get(i);
+ TextView tv = (TextView) inflater.inflate(R.layout.notification_snooze_option,
+ mSnoozeOptionContainer, false);
+ mSnoozeOptionContainer.addView(tv);
+ tv.setText(option.getDescription());
+ tv.setTag(option);
+ tv.setOnClickListener(this);
+ }
+ }
+
+ private void hideSelectedOption() {
+ final int childCount = mSnoozeOptionContainer.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = mSnoozeOptionContainer.getChildAt(i);
+ child.setVisibility(child.getTag() == mSelectedOption ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ private void showSnoozeOptions(boolean show) {
+ int drawableId = show ? com.android.internal.R.drawable.ic_collapse_notification
+ : com.android.internal.R.drawable.ic_expand_notification;
+ mExpandButton.setImageResource(drawableId);
+ if (mExpanded != show) {
+ mExpanded = show;
+ animateSnoozeOptions(show);
+ if (mGutsContainer != null) {
+ mGutsContainer.onHeightChanged();
+ }
+ }
+ }
+
+ private void animateSnoozeOptions(boolean show) {
+ if (mExpandAnimation != null) {
+ mExpandAnimation.cancel();
+ }
+ ObjectAnimator dividerAnim = ObjectAnimator.ofFloat(mDivider, View.ALPHA,
+ mDivider.getAlpha(), show ? 1f : 0f);
+ ObjectAnimator optionAnim = ObjectAnimator.ofFloat(mSnoozeOptionContainer, View.ALPHA,
+ mSnoozeOptionContainer.getAlpha(), show ? 1f : 0f);
+ mSnoozeOptionContainer.setVisibility(View.VISIBLE);
+ mExpandAnimation = new AnimatorSet();
+ mExpandAnimation.playTogether(dividerAnim, optionAnim);
+ mExpandAnimation.setDuration(150);
+ mExpandAnimation.setInterpolator(show ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT);
+ mExpandAnimation.addListener(new AnimatorListenerAdapter() {
+ boolean cancelled = false;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ cancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!show && !cancelled) {
+ mSnoozeOptionContainer.setVisibility(View.INVISIBLE);
+ mSnoozeOptionContainer.setAlpha(0f);
+ }
+ }
+ });
+ mExpandAnimation.start();
+ }
+
+ private void setSelected(SnoozeOption option) {
+ mSelectedOption = option;
+ mSelectedOptionText.setText(option.getConfirmation());
+ showSnoozeOptions(false);
+ hideSelectedOption();
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mGutsContainer != null) {
+ mGutsContainer.resetFalsingCheck();
+ }
+ final int id = v.getId();
+ final SnoozeOption tag = (SnoozeOption) v.getTag();
+ if (tag != null) {
+ setSelected(tag);
+ } else if (id == R.id.notification_snooze) {
+ // Toggle snooze options
+ showSnoozeOptions(!mExpanded);
+ } else {
+ // Undo snooze was selected
+ undoSnooze(v);
+ }
+ }
+
+ private void undoSnooze(View v) {
+ mSelectedOption = null;
+ int[] parentLoc = new int[2];
+ int[] targetLoc = new int[2];
+ mGutsContainer.getLocationOnScreen(parentLoc);
+ v.getLocationOnScreen(targetLoc);
+ final int centerX = v.getWidth() / 2;
+ final int centerY = v.getHeight() / 2;
+ final int x = targetLoc[0] - parentLoc[0] + centerX;
+ final int y = targetLoc[1] - parentLoc[1] + centerY;
+ showSnoozeOptions(false);
+ mGutsContainer.closeControls(x, y, false /* save */, false /* force */);
+ }
+
+ @Override
+ public int getActualHeight() {
+ return mExpanded ? getHeight() : mCollapsedHeight;
+ }
+
+ @Override
+ public boolean willBeRemoved() {
+ return mSnoozing;
+ }
+
+ @Override
+ public View getContentView() {
+ // Reset the view before use
+ setSelected(mDefaultOption);
+ return this;
+ }
+
+ @Override
+ public void setGutsParent(NotificationGuts guts) {
+ mGutsContainer = guts;
+ }
+
+ @Override
+ public boolean handleCloseControls(boolean save, boolean force) {
+ if (mExpanded && !force) {
+ // Collapse expanded state on outside touch
+ showSnoozeOptions(false);
+ return true;
+ } else if (mSnoozeListener != null && mSelectedOption != null) {
+ // Snooze option selected so commit it
+ mSnoozing = true;
+ mSnoozeListener.snooze(mSbn, mSelectedOption);
+ return true;
+ } else {
+ // The view should actually be closed
+ setSelected(mSnoozeOptions.get(0));
+ return false; // Return false here so that guts handles closing the view
+ }
+ }
+
+ @Override
+ public boolean isLeavebehind() {
+ return true;
+ }
+
+ public class NotificationSnoozeOption implements SnoozeOption {
+ private SnoozeCriterion mCriterion;
+ private int mMinutesToSnoozeFor;
+ private CharSequence mDescription;
+ private CharSequence mConfirmation;
+ private AccessibilityAction mAction;
+
+ public NotificationSnoozeOption(SnoozeCriterion sc, int minToSnoozeFor,
+ CharSequence description,
+ CharSequence confirmation, AccessibilityAction action) {
+ mCriterion = sc;
+ mMinutesToSnoozeFor = minToSnoozeFor;
+ mDescription = description;
+ mConfirmation = confirmation;
+ mAction = action;
+ }
+
+ @Override
+ public SnoozeCriterion getSnoozeCriterion() {
+ return mCriterion;
+ }
+
+ @Override
+ public CharSequence getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public CharSequence getConfirmation() {
+ return mConfirmation;
+ }
+
+ @Override
+ public int getMinutesToSnoozeFor() {
+ return mMinutesToSnoozeFor;
+ }
+
+ @Override
+ public AccessibilityAction getAccessibilityAction() {
+ return mAction;
+ }
+
+ }
+}
diff --git a/com/android/systemui/statusbar/RemoteInputController.java b/com/android/systemui/statusbar/RemoteInputController.java
new file mode 100644
index 0000000..7f28c4c
--- /dev/null
+++ b/com/android/systemui/statusbar/RemoteInputController.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2015 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 com.android.internal.util.Preconditions;
+import com.android.systemui.Dependency;
+import com.android.systemui.statusbar.phone.StatusBarWindowManager;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.RemoteInputView;
+
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Keeps track of the currently active {@link RemoteInputView}s.
+ */
+public class RemoteInputController {
+
+ private final ArrayList<Pair<WeakReference<NotificationData.Entry>, Object>> mOpen
+ = new ArrayList<>();
+ private final ArrayMap<String, Object> mSpinning = new ArrayMap<>();
+ private final ArrayList<Callback> mCallbacks = new ArrayList<>(3);
+ private final HeadsUpManager mHeadsUpManager;
+
+ public RemoteInputController(HeadsUpManager headsUpManager) {
+ addCallback(Dependency.get(StatusBarWindowManager.class));
+ mHeadsUpManager = headsUpManager;
+ }
+
+ /**
+ * Adds a currently active remote input.
+ *
+ * @param entry the entry for which a remote input is now active.
+ * @param token a token identifying the view that is managing the remote input
+ */
+ public void addRemoteInput(NotificationData.Entry entry, Object token) {
+ Preconditions.checkNotNull(entry);
+ Preconditions.checkNotNull(token);
+
+ boolean found = pruneWeakThenRemoveAndContains(
+ entry /* contains */, null /* remove */, token /* removeToken */);
+ if (!found) {
+ mOpen.add(new Pair<>(new WeakReference<>(entry), token));
+ }
+
+ apply(entry);
+ }
+
+ /**
+ * Removes a currently active remote input.
+ *
+ * @param entry the entry for which a remote input should be removed.
+ * @param token a token identifying the view that is requesting the removal. If non-null,
+ * the entry is only removed if the token matches the last added token for this
+ * entry. If null, the entry is removed regardless.
+ */
+ public void removeRemoteInput(NotificationData.Entry entry, Object token) {
+ Preconditions.checkNotNull(entry);
+
+ pruneWeakThenRemoveAndContains(null /* contains */, entry /* remove */, token);
+
+ apply(entry);
+ }
+
+ /**
+ * Adds a currently spinning (i.e. sending) remote input.
+ *
+ * @param key the key of the entry that's spinning.
+ * @param token the token of the view managing the remote input.
+ */
+ public void addSpinning(String key, Object token) {
+ Preconditions.checkNotNull(key);
+ Preconditions.checkNotNull(token);
+
+ mSpinning.put(key, token);
+ }
+
+ /**
+ * Removes a currently spinning remote input.
+ *
+ * @param key the key of the entry for which a remote input should be removed.
+ * @param token a token identifying the view that is requesting the removal. If non-null,
+ * the entry is only removed if the token matches the last added token for this
+ * entry. If null, the entry is removed regardless.
+ */
+ public void removeSpinning(String key, Object token) {
+ Preconditions.checkNotNull(key);
+
+ if (token == null || mSpinning.get(key) == token) {
+ mSpinning.remove(key);
+ }
+ }
+
+ public boolean isSpinning(String key) {
+ return mSpinning.containsKey(key);
+ }
+
+ private void apply(NotificationData.Entry entry) {
+ mHeadsUpManager.setRemoteInputActive(entry, isRemoteInputActive(entry));
+ boolean remoteInputActive = isRemoteInputActive();
+ int N = mCallbacks.size();
+ for (int i = 0; i < N; i++) {
+ mCallbacks.get(i).onRemoteInputActive(remoteInputActive);
+ }
+ }
+
+ /**
+ * @return true if {@param entry} has an active RemoteInput
+ */
+ public boolean isRemoteInputActive(NotificationData.Entry entry) {
+ return pruneWeakThenRemoveAndContains(entry /* contains */, null /* remove */,
+ null /* removeToken */);
+ }
+
+ /**
+ * @return true if any entry has an active RemoteInput
+ */
+ public boolean isRemoteInputActive() {
+ pruneWeakThenRemoveAndContains(null /* contains */, null /* remove */,
+ null /* removeToken */);
+ return !mOpen.isEmpty();
+ }
+
+ /**
+ * Prunes dangling weak references, removes entries referring to {@param remove} and returns
+ * whether {@param contains} is part of the array in a single loop.
+ * @param remove if non-null, removes this entry from the active remote inputs
+ * @param removeToken if non-null, only removes an entry if this matches the token when the
+ * entry was added.
+ * @return true if {@param contains} is in the set of active remote inputs
+ */
+ private boolean pruneWeakThenRemoveAndContains(
+ NotificationData.Entry contains, NotificationData.Entry remove, Object removeToken) {
+ boolean found = false;
+ for (int i = mOpen.size() - 1; i >= 0; i--) {
+ NotificationData.Entry item = mOpen.get(i).first.get();
+ Object itemToken = mOpen.get(i).second;
+ boolean removeTokenMatches = (removeToken == null || itemToken == removeToken);
+
+ if (item == null || (item == remove && removeTokenMatches)) {
+ mOpen.remove(i);
+ } else if (item == contains) {
+ if (removeToken != null && removeToken != itemToken) {
+ // We need to update the token. Remove here and let caller reinsert it.
+ mOpen.remove(i);
+ } else {
+ found = true;
+ }
+ }
+ }
+ return found;
+ }
+
+
+ public void addCallback(Callback callback) {
+ Preconditions.checkNotNull(callback);
+ mCallbacks.add(callback);
+ }
+
+ public void remoteInputSent(NotificationData.Entry entry) {
+ int N = mCallbacks.size();
+ for (int i = 0; i < N; i++) {
+ mCallbacks.get(i).onRemoteInputSent(entry);
+ }
+ }
+
+ public void closeRemoteInputs() {
+ if (mOpen.size() == 0) {
+ return;
+ }
+
+ // Make a copy because closing the remote inputs will modify mOpen.
+ ArrayList<NotificationData.Entry> list = new ArrayList<>(mOpen.size());
+ for (int i = mOpen.size() - 1; i >= 0; i--) {
+ NotificationData.Entry item = mOpen.get(i).first.get();
+ if (item != null && item.row != null) {
+ list.add(item);
+ }
+ }
+
+ for (int i = list.size() - 1; i >= 0; i--) {
+ NotificationData.Entry item = list.get(i);
+ if (item.row != null) {
+ item.row.closeRemoteInput();
+ }
+ }
+ }
+
+ public interface Callback {
+ default void onRemoteInputActive(boolean active) {}
+
+ default void onRemoteInputSent(NotificationData.Entry entry) {}
+ }
+}
diff --git a/com/android/systemui/statusbar/ScalingDrawableWrapper.java b/com/android/systemui/statusbar/ScalingDrawableWrapper.java
new file mode 100644
index 0000000..53cbf7f
--- /dev/null
+++ b/com/android/systemui/statusbar/ScalingDrawableWrapper.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 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.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableWrapper;
+
+/**
+ * An extension of {@link DrawableWrapper} that will take a given Drawable and scale it by
+ * the given factor.
+ */
+public class ScalingDrawableWrapper extends DrawableWrapper {
+ private float mScaleFactor;
+
+ public ScalingDrawableWrapper(Drawable drawable, float scaleFactor) {
+ super(drawable);
+ mScaleFactor = scaleFactor;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return (int) (super.getIntrinsicWidth() * mScaleFactor);
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return (int) (super.getIntrinsicHeight() * mScaleFactor);
+ }
+}
diff --git a/com/android/systemui/statusbar/ScrimView.java b/com/android/systemui/statusbar/ScrimView.java
new file mode 100644
index 0000000..a53e348
--- /dev/null
+++ b/com/android/systemui/statusbar/ScrimView.java
@@ -0,0 +1,326 @@
+/*
+ * 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.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v4.graphics.ColorUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Display;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.colorextraction.drawable.GradientDrawable;
+import com.android.systemui.Dependency;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+
+/**
+ * A view which can draw a scrim
+ */
+public class ScrimView extends View implements ConfigurationController.ConfigurationListener {
+ private static final String TAG = "ScrimView";
+ private final ColorExtractor.GradientColors mColors;
+ private boolean mDrawAsSrc;
+ private float mViewAlpha = 1.0f;
+ private ValueAnimator mAlphaAnimator;
+ private Rect mExcludedRect = new Rect();
+ private boolean mHasExcludedArea;
+ private Drawable mDrawable;
+ private PorterDuffColorFilter mColorFilter;
+ private int mTintColor;
+ private ValueAnimator.AnimatorUpdateListener mAlphaUpdateListener = animation -> {
+ if (mDrawable == null) {
+ Log.w(TAG, "Trying to animate null drawable");
+ return;
+ }
+ mDrawable.setAlpha((int) (255 * (float) animation.getAnimatedValue()));
+ };
+ private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAlphaAnimator = null;
+ }
+ };
+ private Runnable mChangeRunnable;
+
+ public ScrimView(Context context) {
+ this(context, null);
+ }
+
+ public ScrimView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ScrimView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ScrimView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mDrawable = new GradientDrawable(context);
+ mDrawable.setCallback(this);
+ mColors = new ColorExtractor.GradientColors();
+ updateScreenSize();
+ updateColorWithTint(false);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // We need to know about configuration changes to update the gradient size
+ // since it's independent from view bounds.
+ ConfigurationController config = Dependency.get(ConfigurationController.class);
+ config.addCallback(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ ConfigurationController config = Dependency.get(ConfigurationController.class);
+ config.removeCallback(this);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mDrawAsSrc || mDrawable.getAlpha() > 0) {
+ if (!mHasExcludedArea) {
+ mDrawable.draw(canvas);
+ } else {
+ if (mExcludedRect.top > 0) {
+ canvas.save();
+ canvas.clipRect(0, 0, getWidth(), mExcludedRect.top);
+ mDrawable.draw(canvas);
+ canvas.restore();
+ }
+ if (mExcludedRect.left > 0) {
+ canvas.save();
+ canvas.clipRect(0, mExcludedRect.top, mExcludedRect.left,
+ mExcludedRect.bottom);
+ mDrawable.draw(canvas);
+ canvas.restore();
+ }
+ if (mExcludedRect.right < getWidth()) {
+ canvas.save();
+ canvas.clipRect(mExcludedRect.right, mExcludedRect.top, getWidth(),
+ mExcludedRect.bottom);
+ mDrawable.draw(canvas);
+ canvas.restore();
+ }
+ if (mExcludedRect.bottom < getHeight()) {
+ canvas.save();
+ canvas.clipRect(0, mExcludedRect.bottom, getWidth(), getHeight());
+ mDrawable.draw(canvas);
+ canvas.restore();
+ }
+ }
+ }
+ }
+
+ public void setDrawable(Drawable drawable) {
+ mDrawable = drawable;
+ mDrawable.setCallback(this);
+ mDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom());
+ mDrawable.setAlpha((int) (255 * mViewAlpha));
+ setDrawAsSrc(mDrawAsSrc);
+ updateScreenSize();
+ invalidate();
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable drawable) {
+ super.invalidateDrawable(drawable);
+ if (drawable == mDrawable) {
+ invalidate();
+ }
+ }
+
+ public void setDrawAsSrc(boolean asSrc) {
+ mDrawAsSrc = asSrc;
+ PorterDuff.Mode mode = asSrc ? PorterDuff.Mode.SRC : PorterDuff.Mode.SRC_OVER;
+ mDrawable.setXfermode(new PorterDuffXfermode(mode));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed) {
+ mDrawable.setBounds(left, top, right, bottom);
+ invalidate();
+ }
+ }
+
+ public void setColors(@NonNull ColorExtractor.GradientColors colors) {
+ setColors(colors, false);
+ }
+
+ public void setColors(@NonNull ColorExtractor.GradientColors colors, boolean animated) {
+ if (colors == null) {
+ throw new IllegalArgumentException("Colors cannot be null");
+ }
+ if (mColors.equals(colors)) {
+ return;
+ }
+ mColors.set(colors);
+ updateColorWithTint(animated);
+ }
+
+ @VisibleForTesting
+ Drawable getDrawable() {
+ return mDrawable;
+ }
+
+ public ColorExtractor.GradientColors getColors() {
+ return mColors;
+ }
+
+ public void setTint(int color) {
+ setTint(color, false);
+ }
+
+ public void setTint(int color, boolean animated) {
+ if (mTintColor == color) {
+ return;
+ }
+ mTintColor = color;
+ updateColorWithTint(animated);
+ }
+
+ private void updateColorWithTint(boolean animated) {
+ if (mDrawable instanceof GradientDrawable) {
+ // Optimization to blend colors and avoid a color filter
+ GradientDrawable drawable = (GradientDrawable) mDrawable;
+ float tintAmount = Color.alpha(mTintColor) / 255f;
+ int mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor,
+ tintAmount);
+ int secondaryTinted = ColorUtils.blendARGB(mColors.getSecondaryColor(), mTintColor,
+ tintAmount);
+ drawable.setColors(mainTinted, secondaryTinted, animated);
+ } else {
+ if (mColorFilter == null) {
+ mColorFilter = new PorterDuffColorFilter(mTintColor, PorterDuff.Mode.SRC_OVER);
+ } else {
+ mColorFilter.setColor(mTintColor);
+ }
+ mDrawable.setColorFilter(Color.alpha(mTintColor) == 0 ? null : mColorFilter);
+ mDrawable.invalidateSelf();
+ }
+
+ if (mChangeRunnable != null) {
+ mChangeRunnable.run();
+ }
+ }
+
+ public int getTint() {
+ return mTintColor;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ public void setViewAlpha(float alpha) {
+ if (alpha != mViewAlpha) {
+ mViewAlpha = alpha;
+
+ if (mAlphaAnimator != null) {
+ mAlphaAnimator.cancel();
+ }
+
+ mDrawable.setAlpha((int) (255 * alpha));
+ if (mChangeRunnable != null) {
+ mChangeRunnable.run();
+ }
+ }
+ }
+
+ public float getViewAlpha() {
+ return mViewAlpha;
+ }
+
+ public void animateViewAlpha(float alpha, long durationOut, Interpolator interpolator) {
+ if (mAlphaAnimator != null) {
+ mAlphaAnimator.cancel();
+ }
+ mAlphaAnimator = ValueAnimator.ofFloat(getViewAlpha(), alpha);
+ mAlphaAnimator.addUpdateListener(mAlphaUpdateListener);
+ mAlphaAnimator.addListener(mClearAnimatorListener);
+ mAlphaAnimator.setInterpolator(interpolator);
+ mAlphaAnimator.setDuration(durationOut);
+ mAlphaAnimator.start();
+ }
+
+ public void setExcludedArea(Rect area) {
+ if (area == null) {
+ mHasExcludedArea = false;
+ invalidate();
+ return;
+ }
+
+ int left = Math.max(area.left, 0);
+ int top = Math.max(area.top, 0);
+ int right = Math.min(area.right, getWidth());
+ int bottom = Math.min(area.bottom, getHeight());
+ mExcludedRect.set(left, top, right, bottom);
+ mHasExcludedArea = left < right && top < bottom;
+ invalidate();
+ }
+
+ public void setChangeRunnable(Runnable changeRunnable) {
+ mChangeRunnable = changeRunnable;
+ }
+
+ @Override
+ public void onConfigChanged(Configuration newConfig) {
+ updateScreenSize();
+ }
+
+ private void updateScreenSize() {
+ if (mDrawable instanceof GradientDrawable) {
+ WindowManager wm = mContext.getSystemService(WindowManager.class);
+ if (wm == null) {
+ Log.w(TAG, "Can't resize gradient drawable to fit the screen");
+ return;
+ }
+ Display display = wm.getDefaultDisplay();
+ if (display != null) {
+ Point size = new Point();
+ display.getRealSize(size);
+ ((GradientDrawable) mDrawable).setScreenSize(size.x, size.y);
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/SignalClusterView.java b/com/android/systemui/statusbar/SignalClusterView.java
new file mode 100644
index 0000000..759d2cf
--- /dev/null
+++ b/com/android/systemui/statusbar/SignalClusterView.java
@@ -0,0 +1,723 @@
+/*
+ * 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;
+
+import android.annotation.DrawableRes;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.telephony.SubscriptionInfo;
+import android.util.ArraySet;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.SignalDrawable;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.policy.IconLogger;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkControllerImpl;
+import com.android.systemui.statusbar.policy.SecurityController;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+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 implements NetworkControllerImpl.SignalCallback,
+ SecurityController.SecurityControllerCallback, Tunable,
+ DarkReceiver {
+
+ static final String TAG = "SignalClusterView";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final String SLOT_AIRPLANE = "airplane";
+ private static final String SLOT_MOBILE = "mobile";
+ private static final String SLOT_WIFI = "wifi";
+ private static final String SLOT_ETHERNET = "ethernet";
+ private static final String SLOT_VPN = "vpn";
+
+ private final NetworkController mNetworkController;
+ private final SecurityController mSecurityController;
+
+ private boolean mNoSimsVisible = false;
+ private boolean mVpnVisible = false;
+ private int mVpnIconId = 0;
+ private int mLastVpnIconId = -1;
+ private boolean mEthernetVisible = false;
+ private int mEthernetIconId = 0;
+ private int mLastEthernetIconId = -1;
+ private boolean mWifiVisible = false;
+ private int mWifiStrengthId = 0;
+ private int mLastWifiStrengthId = -1;
+ private boolean mWifiIn;
+ private boolean mWifiOut;
+ private int mLastWifiActivityId = -1;
+ private boolean mIsAirplaneMode = false;
+ private int mAirplaneIconId = 0;
+ private int mLastAirplaneIconId = -1;
+ private String mAirplaneContentDescription;
+ private String mWifiDescription;
+ private String mEthernetDescription;
+ private ArrayList<PhoneState> mPhoneStates = new ArrayList<PhoneState>();
+ private int mIconTint = Color.WHITE;
+ private float mDarkIntensity;
+ private final Rect mTintArea = new Rect();
+
+ ViewGroup mEthernetGroup, mWifiGroup;
+ View mNoSimsCombo;
+ ImageView mVpn, mEthernet, mWifi, mAirplane, mNoSims, mEthernetDark, mWifiDark, mNoSimsDark;
+ ImageView mWifiActivityIn;
+ ImageView mWifiActivityOut;
+ View mWifiAirplaneSpacer;
+ View mWifiSignalSpacer;
+ LinearLayout mMobileSignalGroup;
+
+ private final int mMobileSignalGroupEndPadding;
+ private final int mMobileDataIconStartPadding;
+ private final int mWideTypeIconStartPadding;
+ private final int mSecondaryTelephonyPadding;
+ private final int mEndPadding;
+ private final int mEndPaddingNothingVisible;
+ private final float mIconScaleFactor;
+
+ private boolean mBlockAirplane;
+ private boolean mBlockMobile;
+ private boolean mBlockWifi;
+ private boolean mBlockEthernet;
+ private boolean mActivityEnabled;
+ private boolean mForceBlockWifi;
+
+ private final IconLogger mIconLogger = Dependency.get(IconLogger.class);
+
+ public SignalClusterView(Context context) {
+ this(context, null);
+ }
+
+ public SignalClusterView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SignalClusterView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ Resources res = getResources();
+ mMobileSignalGroupEndPadding =
+ res.getDimensionPixelSize(R.dimen.mobile_signal_group_end_padding);
+ mMobileDataIconStartPadding =
+ res.getDimensionPixelSize(R.dimen.mobile_data_icon_start_padding);
+ mWideTypeIconStartPadding = res.getDimensionPixelSize(R.dimen.wide_type_icon_start_padding);
+ mSecondaryTelephonyPadding = res.getDimensionPixelSize(R.dimen.secondary_telephony_padding);
+ mEndPadding = res.getDimensionPixelSize(R.dimen.signal_cluster_battery_padding);
+ mEndPaddingNothingVisible = res.getDimensionPixelSize(
+ R.dimen.no_signal_cluster_battery_padding);
+
+ TypedValue typedValue = new TypedValue();
+ res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+ mIconScaleFactor = typedValue.getFloat();
+ mNetworkController = Dependency.get(NetworkController.class);
+ mSecurityController = Dependency.get(SecurityController.class);
+ updateActivityEnabled();
+ }
+
+ public void setForceBlockWifi() {
+ mForceBlockWifi = true;
+ mBlockWifi = true;
+ if (isAttachedToWindow()) {
+ // Re-register to get new callbacks.
+ mNetworkController.removeCallback(this);
+ mNetworkController.addCallback(this);
+ }
+ }
+
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ if (!StatusBarIconController.ICON_BLACKLIST.equals(key)) {
+ return;
+ }
+ ArraySet<String> blockList = StatusBarIconController.getIconBlacklist(newValue);
+ boolean blockAirplane = blockList.contains(SLOT_AIRPLANE);
+ boolean blockMobile = blockList.contains(SLOT_MOBILE);
+ boolean blockWifi = blockList.contains(SLOT_WIFI);
+ boolean blockEthernet = blockList.contains(SLOT_ETHERNET);
+
+ if (blockAirplane != mBlockAirplane || blockMobile != mBlockMobile
+ || blockEthernet != mBlockEthernet || blockWifi != mBlockWifi) {
+ mBlockAirplane = blockAirplane;
+ mBlockMobile = blockMobile;
+ mBlockEthernet = blockEthernet;
+ mBlockWifi = blockWifi || mForceBlockWifi;
+ // Re-register to get new callbacks.
+ mNetworkController.removeCallback(this);
+ mNetworkController.addCallback(this);
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mVpn = findViewById(R.id.vpn);
+ mEthernetGroup = findViewById(R.id.ethernet_combo);
+ mEthernet = findViewById(R.id.ethernet);
+ mEthernetDark = findViewById(R.id.ethernet_dark);
+ mWifiGroup = findViewById(R.id.wifi_combo);
+ mWifi = findViewById(R.id.wifi_signal);
+ mWifiDark = findViewById(R.id.wifi_signal_dark);
+ mWifiActivityIn = findViewById(R.id.wifi_in);
+ mWifiActivityOut= findViewById(R.id.wifi_out);
+ mAirplane = findViewById(R.id.airplane);
+ mNoSims = findViewById(R.id.no_sims);
+ mNoSimsDark = findViewById(R.id.no_sims_dark);
+ mNoSimsCombo = findViewById(R.id.no_sims_combo);
+ mWifiAirplaneSpacer = findViewById(R.id.wifi_airplane_spacer);
+ mWifiSignalSpacer = findViewById(R.id.wifi_signal_spacer);
+ mMobileSignalGroup = findViewById(R.id.mobile_signal_group);
+
+ maybeScaleVpnAndNoSimsIcons();
+ }
+
+ /**
+ * Extracts the icon off of the VPN and no sims views and maybe scale them by
+ * {@link #mIconScaleFactor}. Note that the other icons are not scaled here because they are
+ * dynamic. As such, they need to be scaled each time the icon changes in {@link #apply()}.
+ */
+ private void maybeScaleVpnAndNoSimsIcons() {
+ if (mIconScaleFactor == 1.f) {
+ return;
+ }
+
+ mVpn.setImageDrawable(new ScalingDrawableWrapper(mVpn.getDrawable(), mIconScaleFactor));
+
+ mNoSims.setImageDrawable(
+ new ScalingDrawableWrapper(mNoSims.getDrawable(), mIconScaleFactor));
+ mNoSimsDark.setImageDrawable(
+ new ScalingDrawableWrapper(mNoSimsDark.getDrawable(), mIconScaleFactor));
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mVpnVisible = mSecurityController.isVpnEnabled();
+ mVpnIconId = currentVpnIconId(mSecurityController.isVpnBranded());
+
+ for (PhoneState state : mPhoneStates) {
+ if (state.mMobileGroup.getParent() == null) {
+ mMobileSignalGroup.addView(state.mMobileGroup);
+ }
+ }
+
+ int endPadding = mMobileSignalGroup.getChildCount() > 0 ? mMobileSignalGroupEndPadding : 0;
+ mMobileSignalGroup.setPaddingRelative(0, 0, endPadding, 0);
+
+ Dependency.get(TunerService.class).addTunable(this, StatusBarIconController.ICON_BLACKLIST);
+
+ apply();
+ applyIconTint();
+ mNetworkController.addCallback(this);
+ mSecurityController.addCallback(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mMobileSignalGroup.removeAllViews();
+ Dependency.get(TunerService.class).removeTunable(this);
+ mSecurityController.removeCallback(this);
+ mNetworkController.removeCallback(this);
+
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+
+ // Re-run all checks against the tint area for all icons
+ applyIconTint();
+ }
+
+ // From SecurityController.
+ @Override
+ public void onStateChanged() {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mVpnVisible = mSecurityController.isVpnEnabled();
+ mVpnIconId = currentVpnIconId(mSecurityController.isVpnBranded());
+ apply();
+ }
+ });
+ }
+
+ private void updateActivityEnabled() {
+ mActivityEnabled = mContext.getResources().getBoolean(R.bool.config_showActivity);
+ }
+
+ @Override
+ public void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon,
+ boolean activityIn, boolean activityOut, String description, boolean isTransient) {
+ mWifiVisible = statusIcon.visible && !mBlockWifi;
+ mWifiStrengthId = statusIcon.icon;
+ mWifiDescription = statusIcon.contentDescription;
+ mWifiIn = activityIn && mActivityEnabled && mWifiVisible;
+ mWifiOut = activityOut && mActivityEnabled && mWifiVisible;
+
+ apply();
+ }
+
+ @Override
+ public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType,
+ int qsType, boolean activityIn, boolean activityOut, String typeContentDescription,
+ String description, boolean isWide, int subId, boolean roaming) {
+ PhoneState state = getState(subId);
+ if (state == null) {
+ return;
+ }
+ state.mMobileVisible = statusIcon.visible && !mBlockMobile;
+ state.mMobileStrengthId = statusIcon.icon;
+ state.mMobileTypeId = statusType;
+ state.mMobileDescription = statusIcon.contentDescription;
+ state.mMobileTypeDescription = typeContentDescription;
+ state.mIsMobileTypeIconWide = statusType != 0 && isWide;
+ state.mRoaming = roaming;
+ state.mActivityIn = activityIn && mActivityEnabled;
+ state.mActivityOut = activityOut && mActivityEnabled;
+
+ apply();
+ }
+
+ @Override
+ public void setEthernetIndicators(IconState state) {
+ mEthernetVisible = state.visible && !mBlockEthernet;
+ mEthernetIconId = state.icon;
+ mEthernetDescription = state.contentDescription;
+
+ apply();
+ }
+
+ @Override
+ public void setNoSims(boolean show) {
+ mNoSimsVisible = show && !mBlockMobile;
+ apply();
+ }
+
+ @Override
+ public void setSubs(List<SubscriptionInfo> subs) {
+ if (hasCorrectSubs(subs)) {
+ return;
+ }
+ mPhoneStates.clear();
+ if (mMobileSignalGroup != null) {
+ mMobileSignalGroup.removeAllViews();
+ }
+ final int n = subs.size();
+ for (int i = 0; i < n; i++) {
+ inflatePhoneState(subs.get(i).getSubscriptionId());
+ }
+ if (isAttachedToWindow()) {
+ applyIconTint();
+ }
+ }
+
+ private boolean hasCorrectSubs(List<SubscriptionInfo> subs) {
+ final int N = subs.size();
+ if (N != mPhoneStates.size()) {
+ return false;
+ }
+ for (int i = 0; i < N; i++) {
+ if (mPhoneStates.get(i).mSubId != subs.get(i).getSubscriptionId()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private PhoneState getState(int subId) {
+ for (PhoneState state : mPhoneStates) {
+ if (state.mSubId == subId) {
+ return state;
+ }
+ }
+ Log.e(TAG, "Unexpected subscription " + subId);
+ return null;
+ }
+
+ 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(IconState icon) {
+ mIsAirplaneMode = icon.visible && !mBlockAirplane;
+ mAirplaneIconId = icon.icon;
+ mAirplaneContentDescription = icon.contentDescription;
+
+ apply();
+ }
+
+ @Override
+ public void setMobileDataEnabled(boolean enabled) {
+ // Don't care.
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ // Standard group layout onPopulateAccessibilityEvent() implementations
+ // ignore content description, so populate manually
+ if (mEthernetVisible && mEthernetGroup != null &&
+ mEthernetGroup.getContentDescription() != null)
+ event.getText().add(mEthernetGroup.getContentDescription());
+ if (mWifiVisible && mWifiGroup != null && mWifiGroup.getContentDescription() != null)
+ event.getText().add(mWifiGroup.getContentDescription());
+ for (PhoneState state : mPhoneStates) {
+ state.populateAccessibilityEvent(event);
+ }
+ return super.dispatchPopulateAccessibilityEventInternal(event);
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ if (mEthernet != null) {
+ mEthernet.setImageDrawable(null);
+ mEthernetDark.setImageDrawable(null);
+ mLastEthernetIconId = -1;
+ }
+
+ if (mWifi != null) {
+ mWifi.setImageDrawable(null);
+ mWifiDark.setImageDrawable(null);
+ mLastWifiStrengthId = -1;
+ }
+
+ for (PhoneState state : mPhoneStates) {
+ if (state.mMobileType != null) {
+ state.mMobileType.setImageDrawable(null);
+ state.mLastMobileTypeId = -1;
+ }
+ }
+
+ if (mAirplane != null) {
+ mAirplane.setImageDrawable(null);
+ mLastAirplaneIconId = -1;
+ }
+
+ apply();
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ // Run after each indicator change.
+ private void apply() {
+ if (mWifiGroup == null) return;
+
+ if (mVpnVisible) {
+ if (mLastVpnIconId != mVpnIconId) {
+ setIconForView(mVpn, mVpnIconId);
+ mLastVpnIconId = mVpnIconId;
+ }
+ mIconLogger.onIconShown(SLOT_VPN);
+ mVpn.setVisibility(View.VISIBLE);
+ } else {
+ mIconLogger.onIconHidden(SLOT_VPN);
+ mVpn.setVisibility(View.GONE);
+ }
+ if (DEBUG) Log.d(TAG, String.format("vpn: %s", mVpnVisible ? "VISIBLE" : "GONE"));
+
+ if (mEthernetVisible) {
+ if (mLastEthernetIconId != mEthernetIconId) {
+ setIconForView(mEthernet, mEthernetIconId);
+ setIconForView(mEthernetDark, mEthernetIconId);
+ mLastEthernetIconId = mEthernetIconId;
+ }
+ mEthernetGroup.setContentDescription(mEthernetDescription);
+ mIconLogger.onIconShown(SLOT_ETHERNET);
+ mEthernetGroup.setVisibility(View.VISIBLE);
+ } else {
+ mIconLogger.onIconHidden(SLOT_ETHERNET);
+ mEthernetGroup.setVisibility(View.GONE);
+ }
+
+ if (DEBUG) Log.d(TAG,
+ String.format("ethernet: %s",
+ (mEthernetVisible ? "VISIBLE" : "GONE")));
+
+ if (mWifiVisible) {
+ if (mWifiStrengthId != mLastWifiStrengthId) {
+ setIconForView(mWifi, mWifiStrengthId);
+ setIconForView(mWifiDark, mWifiStrengthId);
+ mLastWifiStrengthId = mWifiStrengthId;
+ }
+ mIconLogger.onIconShown(SLOT_WIFI);
+ mWifiGroup.setContentDescription(mWifiDescription);
+ mWifiGroup.setVisibility(View.VISIBLE);
+ } else {
+ mIconLogger.onIconHidden(SLOT_WIFI);
+ mWifiGroup.setVisibility(View.GONE);
+ }
+
+ if (DEBUG) Log.d(TAG,
+ String.format("wifi: %s sig=%d",
+ (mWifiVisible ? "VISIBLE" : "GONE"),
+ mWifiStrengthId));
+
+ mWifiActivityIn.setVisibility(mWifiIn ? View.VISIBLE : View.GONE);
+ mWifiActivityOut.setVisibility(mWifiOut ? View.VISIBLE : View.GONE);
+
+ boolean anyMobileVisible = false;
+ int firstMobileTypeId = 0;
+ for (PhoneState state : mPhoneStates) {
+ if (state.apply(anyMobileVisible)) {
+ if (!anyMobileVisible) {
+ firstMobileTypeId = state.mMobileTypeId;
+ anyMobileVisible = true;
+ }
+ }
+ }
+ if (anyMobileVisible) {
+ mIconLogger.onIconShown(SLOT_MOBILE);
+ } else {
+ mIconLogger.onIconHidden(SLOT_MOBILE);
+ }
+
+ if (mIsAirplaneMode) {
+ if (mLastAirplaneIconId != mAirplaneIconId) {
+ setIconForView(mAirplane, mAirplaneIconId);
+ mLastAirplaneIconId = mAirplaneIconId;
+ }
+ mAirplane.setContentDescription(mAirplaneContentDescription);
+ mIconLogger.onIconShown(SLOT_AIRPLANE);
+ mAirplane.setVisibility(View.VISIBLE);
+ } else {
+ mIconLogger.onIconHidden(SLOT_AIRPLANE);
+ mAirplane.setVisibility(View.GONE);
+ }
+
+ if (mIsAirplaneMode && mWifiVisible) {
+ mWifiAirplaneSpacer.setVisibility(View.VISIBLE);
+ } else {
+ mWifiAirplaneSpacer.setVisibility(View.GONE);
+ }
+
+ if (((anyMobileVisible && firstMobileTypeId != 0) || mNoSimsVisible) && mWifiVisible) {
+ mWifiSignalSpacer.setVisibility(View.VISIBLE);
+ } else {
+ mWifiSignalSpacer.setVisibility(View.GONE);
+ }
+
+ if (mNoSimsVisible) {
+ mIconLogger.onIconShown(SLOT_MOBILE);
+ mNoSimsCombo.setVisibility(View.VISIBLE);
+ } else {
+ mIconLogger.onIconHidden(SLOT_MOBILE);
+ mNoSimsCombo.setVisibility(View.GONE);
+ }
+
+ boolean anythingVisible = mNoSimsVisible || mWifiVisible || mIsAirplaneMode
+ || anyMobileVisible || mVpnVisible || mEthernetVisible;
+ setPaddingRelative(0, 0, anythingVisible ? mEndPadding : mEndPaddingNothingVisible, 0);
+ }
+
+ /**
+ * Sets the given drawable id on the view. This method will also scale the icon by
+ * {@link #mIconScaleFactor} if appropriate.
+ */
+ private void setIconForView(ImageView imageView, @DrawableRes int iconId) {
+ // Using the imageView's context to retrieve the Drawable so that theme is preserved.
+ Drawable icon = imageView.getContext().getDrawable(iconId);
+
+ if (mIconScaleFactor == 1.f) {
+ imageView.setImageDrawable(icon);
+ } else {
+ imageView.setImageDrawable(new ScalingDrawableWrapper(icon, mIconScaleFactor));
+ }
+ }
+
+
+ @Override
+ public void onDarkChanged(Rect tintArea, float darkIntensity, int tint) {
+ boolean changed = tint != mIconTint || darkIntensity != mDarkIntensity
+ || !mTintArea.equals(tintArea);
+ mIconTint = tint;
+ mDarkIntensity = darkIntensity;
+ mTintArea.set(tintArea);
+ if (changed && isAttachedToWindow()) {
+ applyIconTint();
+ }
+ }
+
+ private void applyIconTint() {
+ setTint(mVpn, DarkIconDispatcher.getTint(mTintArea, mVpn, mIconTint));
+ setTint(mAirplane, DarkIconDispatcher.getTint(mTintArea, mAirplane, mIconTint));
+ applyDarkIntensity(
+ DarkIconDispatcher.getDarkIntensity(mTintArea, mNoSims, mDarkIntensity),
+ mNoSims, mNoSimsDark);
+ applyDarkIntensity(
+ DarkIconDispatcher.getDarkIntensity(mTintArea, mWifi, mDarkIntensity),
+ mWifi, mWifiDark);
+ setTint(mWifiActivityIn,
+ DarkIconDispatcher.getTint(mTintArea, mWifiActivityIn, mIconTint));
+ setTint(mWifiActivityOut,
+ DarkIconDispatcher.getTint(mTintArea, mWifiActivityOut, mIconTint));
+ applyDarkIntensity(
+ DarkIconDispatcher.getDarkIntensity(mTintArea, mEthernet, mDarkIntensity),
+ mEthernet, mEthernetDark);
+ for (int i = 0; i < mPhoneStates.size(); i++) {
+ mPhoneStates.get(i).setIconTint(mIconTint, mDarkIntensity, mTintArea);
+ }
+ }
+
+ private void applyDarkIntensity(float darkIntensity, View lightIcon, View darkIcon) {
+ lightIcon.setAlpha(1 - darkIntensity);
+ darkIcon.setAlpha(darkIntensity);
+ }
+
+ private void setTint(ImageView v, int tint) {
+ v.setImageTintList(ColorStateList.valueOf(tint));
+ }
+
+ private int currentVpnIconId(boolean isBranded) {
+ return isBranded ? R.drawable.stat_sys_branded_vpn : R.drawable.stat_sys_vpn_ic;
+ }
+
+ private class PhoneState {
+ private final int mSubId;
+ private boolean mMobileVisible = false;
+ private int mMobileStrengthId = 0, mMobileTypeId = 0;
+ private int mLastMobileStrengthId = -1;
+ private int mLastMobileTypeId = -1;
+ private boolean mIsMobileTypeIconWide;
+ private String mMobileDescription, mMobileTypeDescription;
+
+ private ViewGroup mMobileGroup;
+ private ImageView mMobile, mMobileDark, mMobileType, mMobileRoaming;
+ public boolean mRoaming;
+ private ImageView mMobileActivityIn;
+ private ImageView mMobileActivityOut;
+ public boolean mActivityIn;
+ public boolean mActivityOut;
+
+ 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 = root.findViewById(R.id.mobile_signal);
+ mMobileDark = root.findViewById(R.id.mobile_signal_dark);
+ mMobileType = root.findViewById(R.id.mobile_type);
+ mMobileRoaming = root.findViewById(R.id.mobile_roaming);
+ mMobileActivityIn = root.findViewById(R.id.mobile_in);
+ mMobileActivityOut = root.findViewById(R.id.mobile_out);
+ // TODO: Remove the 2 instances because now the drawable can handle darkness.
+ mMobile.setImageDrawable(new SignalDrawable(mMobile.getContext()));
+ SignalDrawable drawable = new SignalDrawable(mMobileDark.getContext());
+ drawable.setDarkIntensity(1);
+ mMobileDark.setImageDrawable(drawable);
+ }
+
+ public boolean apply(boolean isSecondaryIcon) {
+ if (mMobileVisible && !mIsAirplaneMode) {
+ if (mLastMobileStrengthId != mMobileStrengthId) {
+ mMobile.getDrawable().setLevel(mMobileStrengthId);
+ mMobileDark.getDrawable().setLevel(mMobileStrengthId);
+ mLastMobileStrengthId = mMobileStrengthId;
+ }
+
+ if (mLastMobileTypeId != mMobileTypeId) {
+ mMobileType.setImageResource(mMobileTypeId);
+ mLastMobileTypeId = 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 : mMobileDataIconStartPadding,
+ 0, 0, 0);
+ mMobileDark.setPaddingRelative(
+ mIsMobileTypeIconWide ? mWideTypeIconStartPadding : mMobileDataIconStartPadding,
+ 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);
+ mMobileRoaming.setVisibility(mRoaming ? View.VISIBLE : View.GONE);
+ mMobileActivityIn.setVisibility(mActivityIn ? View.VISIBLE : View.GONE);
+ mMobileActivityOut.setVisibility(mActivityOut ? View.VISIBLE : View.GONE);
+
+ return mMobileVisible;
+ }
+
+ public void populateAccessibilityEvent(AccessibilityEvent event) {
+ if (mMobileVisible && mMobileGroup != null
+ && mMobileGroup.getContentDescription() != null) {
+ event.getText().add(mMobileGroup.getContentDescription());
+ }
+ }
+
+ public void setIconTint(int tint, float darkIntensity, Rect tintArea) {
+ applyDarkIntensity(
+ DarkIconDispatcher.getDarkIntensity(tintArea, mMobile, darkIntensity),
+ mMobile, mMobileDark);
+ setTint(mMobileType, DarkIconDispatcher.getTint(tintArea, mMobileType, tint));
+ setTint(mMobileRoaming, DarkIconDispatcher.getTint(tintArea, mMobileRoaming,
+ tint));
+ setTint(mMobileActivityIn,
+ DarkIconDispatcher.getTint(tintArea, mMobileActivityIn, tint));
+ setTint(mMobileActivityOut,
+ DarkIconDispatcher.getTint(tintArea, mMobileActivityOut, tint));
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/StackScrollerDecorView.java b/com/android/systemui/statusbar/StackScrollerDecorView.java
new file mode 100644
index 0000000..0a7ee51
--- /dev/null
+++ b/com/android/systemui/statusbar/StackScrollerDecorView.java
@@ -0,0 +1,137 @@
+/*
+ * 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.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+
+/**
+ * A common base class for all views in the notification stack scroller which don't have a
+ * background.
+ */
+public abstract class StackScrollerDecorView extends ExpandableView {
+
+ protected View mContent;
+ private boolean mIsVisible;
+ private boolean mAnimating;
+
+ public StackScrollerDecorView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mContent = findContentView();
+ setInvisible();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ setOutlineProvider(null);
+ }
+
+ @Override
+ public boolean isTransparent() {
+ return true;
+ }
+
+ public void performVisibilityAnimation(boolean nowVisible) {
+ animateText(nowVisible, null /* onFinishedRunnable */);
+ }
+
+ public void performVisibilityAnimation(boolean nowVisible, Runnable onFinishedRunnable) {
+ animateText(nowVisible, onFinishedRunnable);
+ }
+
+ public boolean isVisible() {
+ return mIsVisible || mAnimating;
+ }
+
+ /**
+ * Animate the text to a new visibility.
+ *
+ * @param nowVisible should it now be visible
+ * @param onFinishedRunnable A runnable which should be run when the animation is
+ * finished.
+ */
+ private void animateText(boolean nowVisible, final Runnable onFinishedRunnable) {
+ if (nowVisible != mIsVisible) {
+ // Animate text
+ float endValue = nowVisible ? 1.0f : 0.0f;
+ Interpolator interpolator;
+ if (nowVisible) {
+ interpolator = Interpolators.ALPHA_IN;
+ } else {
+ interpolator = Interpolators.ALPHA_OUT;
+ }
+ mAnimating = true;
+ mContent.animate()
+ .alpha(endValue)
+ .setInterpolator(interpolator)
+ .setDuration(260)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mAnimating = false;
+ if (onFinishedRunnable != null) {
+ onFinishedRunnable.run();
+ }
+ }
+ });
+ mIsVisible = nowVisible;
+ } else {
+ if (onFinishedRunnable != null) {
+ onFinishedRunnable.run();
+ }
+ }
+ }
+
+ public void setInvisible() {
+ mContent.setAlpha(0.0f);
+ mIsVisible = false;
+ }
+
+ @Override
+ public void performRemoveAnimation(long duration, float translationDirection,
+ Runnable onFinishedRunnable) {
+ // TODO: Use duration
+ performVisibilityAnimation(false);
+ }
+
+ @Override
+ public void performAddAnimation(long delay, long duration) {
+ // TODO: use delay and duration
+ performVisibilityAnimation(true);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ public void cancelAnimation() {
+ mContent.animate().cancel();
+ }
+
+ protected abstract View findContentView();
+}
diff --git a/com/android/systemui/statusbar/StatusBarIconView.java b/com/android/systemui/statusbar/StatusBarIconView.java
new file mode 100644
index 0000000..2cff79d
--- /dev/null
+++ b/com/android/systemui/statusbar/StatusBarIconView.java
@@ -0,0 +1,810 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.Notification;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Parcelable;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.support.v4.graphics.ColorUtils;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Log;
+import android.util.Property;
+import android.util.TypedValue;
+import android.view.ViewDebug;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.Interpolator;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.NotificationIconDozeHelper;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+
+import java.text.NumberFormat;
+import java.util.Arrays;
+
+public class StatusBarIconView extends AnimatedImageView {
+ public static final int NO_COLOR = 0;
+
+ /**
+ * Multiply alpha values with (1+DARK_ALPHA_BOOST) when dozing. The chosen value boosts
+ * everything above 30% to 50%, making it appear on 1bit color depths.
+ */
+ private static final float DARK_ALPHA_BOOST = 0.67f;
+ private final int ANIMATION_DURATION_FAST = 100;
+
+ public static final int STATE_ICON = 0;
+ public static final int STATE_DOT = 1;
+ public static final int STATE_HIDDEN = 2;
+
+ private static final String TAG = "StatusBarIconView";
+ private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT
+ = new FloatProperty<StatusBarIconView>("iconAppearAmount") {
+
+ @Override
+ public void setValue(StatusBarIconView object, float value) {
+ object.setIconAppearAmount(value);
+ }
+
+ @Override
+ public Float get(StatusBarIconView object) {
+ return object.getIconAppearAmount();
+ }
+ };
+ private static final Property<StatusBarIconView, Float> DOT_APPEAR_AMOUNT
+ = new FloatProperty<StatusBarIconView>("dot_appear_amount") {
+
+ @Override
+ public void setValue(StatusBarIconView object, float value) {
+ object.setDotAppearAmount(value);
+ }
+
+ @Override
+ public Float get(StatusBarIconView object) {
+ return object.getDotAppearAmount();
+ }
+ };
+
+ private boolean mAlwaysScaleIcon;
+ private int mStatusBarIconDrawingSizeDark = 1;
+ private int mStatusBarIconDrawingSize = 1;
+ private int mStatusBarIconSize = 1;
+ private StatusBarIcon mIcon;
+ @ViewDebug.ExportedProperty private String mSlot;
+ private Drawable mNumberBackground;
+ private Paint mNumberPain;
+ private int mNumberX;
+ private int mNumberY;
+ private String mNumberText;
+ private StatusBarNotification mNotification;
+ private final boolean mBlocked;
+ private int mDensity;
+ private float mIconScale = 1.0f;
+ private final Paint mDotPaint = new Paint();
+ private float mDotRadius;
+ private int mStaticDotRadius;
+ private int mVisibleState = STATE_ICON;
+ private float mIconAppearAmount = 1.0f;
+ private ObjectAnimator mIconAppearAnimator;
+ private ObjectAnimator mDotAnimator;
+ private float mDotAppearAmount;
+ private OnVisibilityChangedListener mOnVisibilityChangedListener;
+ private int mDrawableColor;
+ private int mIconColor;
+ private int mDecorColor;
+ private float mDarkAmount;
+ private ValueAnimator mColorAnimator;
+ private int mCurrentSetColor = NO_COLOR;
+ private int mAnimationStartColor = NO_COLOR;
+ private final ValueAnimator.AnimatorUpdateListener mColorUpdater
+ = animation -> {
+ int newColor = NotificationUtils.interpolateColors(mAnimationStartColor, mIconColor,
+ animation.getAnimatedFraction());
+ setColorInternal(newColor);
+ };
+ private final NotificationIconDozeHelper mDozer;
+ private int mContrastedDrawableColor;
+ private int mCachedContrastBackgroundColor = NO_COLOR;
+ private float[] mMatrix;
+ private ColorMatrixColorFilter mMatrixColorFilter;
+ private boolean mIsInShelf;
+
+ public StatusBarIconView(Context context, String slot, StatusBarNotification sbn) {
+ this(context, slot, sbn, false);
+ }
+
+ public StatusBarIconView(Context context, String slot, StatusBarNotification sbn,
+ boolean blocked) {
+ super(context);
+ mDozer = new NotificationIconDozeHelper(context);
+ mBlocked = blocked;
+ mSlot = slot;
+ mNumberPain = new Paint();
+ mNumberPain.setTextAlign(Paint.Align.CENTER);
+ mNumberPain.setColor(context.getColor(R.drawable.notification_number_text_color));
+ mNumberPain.setAntiAlias(true);
+ setNotification(sbn);
+ maybeUpdateIconScaleDimens();
+ setScaleType(ScaleType.CENTER);
+ mDensity = context.getResources().getDisplayMetrics().densityDpi;
+ if (mNotification != null) {
+ setDecorColor(getContext().getColor(
+ com.android.internal.R.color.notification_icon_default_color));
+ }
+ reloadDimens();
+ }
+
+ private void maybeUpdateIconScaleDimens() {
+ // We do not resize and scale system icons (on the right), only notification icons (on the
+ // left).
+ if (mNotification != null || mAlwaysScaleIcon) {
+ updateIconScaleDimens();
+ }
+ }
+
+ private void updateIconScaleDimens() {
+ Resources res = mContext.getResources();
+ mStatusBarIconSize = res.getDimensionPixelSize(R.dimen.status_bar_icon_size);
+ mStatusBarIconDrawingSizeDark =
+ res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size_dark);
+ mStatusBarIconDrawingSize =
+ res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size);
+ updateIconScale();
+ }
+
+ private void updateIconScale() {
+ final float imageBounds = NotificationUtils.interpolate(
+ mStatusBarIconDrawingSize,
+ mStatusBarIconDrawingSizeDark,
+ mDarkAmount);
+ final int outerBounds = mStatusBarIconSize;
+ mIconScale = (float)imageBounds / (float)outerBounds;
+ }
+
+ public float getIconScaleFullyDark() {
+ return (float) mStatusBarIconDrawingSizeDark / mStatusBarIconDrawingSize;
+ }
+
+ public float getIconScale() {
+ return mIconScale;
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ int density = newConfig.densityDpi;
+ if (density != mDensity) {
+ mDensity = density;
+ maybeUpdateIconScaleDimens();
+ updateDrawable();
+ reloadDimens();
+ }
+ }
+
+ private void reloadDimens() {
+ boolean applyRadius = mDotRadius == mStaticDotRadius;
+ mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
+ if (applyRadius) {
+ mDotRadius = mStaticDotRadius;
+ }
+ }
+
+ public void setNotification(StatusBarNotification notification) {
+ mNotification = notification;
+ if (notification != null) {
+ setContentDescription(notification.getNotification());
+ }
+ }
+
+ public StatusBarIconView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mDozer = new NotificationIconDozeHelper(context);
+ mBlocked = false;
+ mAlwaysScaleIcon = true;
+ updateIconScaleDimens();
+ mDensity = context.getResources().getDisplayMetrics().densityDpi;
+ }
+
+ private static boolean streq(String a, String b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null && b != null) {
+ return false;
+ }
+ if (a != null && b == null) {
+ return false;
+ }
+ return a.equals(b);
+ }
+
+ public boolean equalIcons(Icon a, Icon b) {
+ if (a == b) return true;
+ if (a.getType() != b.getType()) return false;
+ switch (a.getType()) {
+ case Icon.TYPE_RESOURCE:
+ return a.getResPackage().equals(b.getResPackage()) && a.getResId() == b.getResId();
+ case Icon.TYPE_URI:
+ return a.getUriString().equals(b.getUriString());
+ default:
+ return false;
+ }
+ }
+ /**
+ * Returns whether the set succeeded.
+ */
+ public boolean set(StatusBarIcon icon) {
+ final boolean iconEquals = mIcon != null && equalIcons(mIcon.icon, icon.icon);
+ final boolean levelEquals = iconEquals
+ && mIcon.iconLevel == icon.iconLevel;
+ final boolean visibilityEquals = mIcon != null
+ && mIcon.visible == icon.visible;
+ final boolean numberEquals = mIcon != null
+ && mIcon.number == icon.number;
+ mIcon = icon.clone();
+ setContentDescription(icon.contentDescription);
+ if (!iconEquals) {
+ if (!updateDrawable(false /* no clear */)) return false;
+ // we have to clear the grayscale tag since it may have changed
+ setTag(R.id.icon_is_grayscale, null);
+ }
+ if (!levelEquals) {
+ setImageLevel(icon.iconLevel);
+ }
+
+ if (!numberEquals) {
+ if (icon.number > 0 && getContext().getResources().getBoolean(
+ R.bool.config_statusBarShowNumber)) {
+ if (mNumberBackground == null) {
+ mNumberBackground = getContext().getResources().getDrawable(
+ R.drawable.ic_notification_overlay);
+ }
+ placeNumber();
+ } else {
+ mNumberBackground = null;
+ mNumberText = null;
+ }
+ invalidate();
+ }
+ if (!visibilityEquals) {
+ setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE);
+ }
+ return true;
+ }
+
+ public void updateDrawable() {
+ updateDrawable(true /* with clear */);
+ }
+
+ private boolean updateDrawable(boolean withClear) {
+ if (mIcon == null) {
+ return false;
+ }
+ Drawable drawable;
+ try {
+ drawable = getIcon(mIcon);
+ } catch (OutOfMemoryError e) {
+ Log.w(TAG, "OOM while inflating " + mIcon.icon + " for slot " + mSlot);
+ return false;
+ }
+
+ if (drawable == null) {
+ Log.w(TAG, "No icon for slot " + mSlot + "; " + mIcon.icon);
+ return false;
+ }
+ if (withClear) {
+ setImageDrawable(null);
+ }
+ setImageDrawable(drawable);
+ return true;
+ }
+
+ public Icon getSourceIcon() {
+ return mIcon.icon;
+ }
+
+ private Drawable getIcon(StatusBarIcon icon) {
+ return getIcon(getContext(), icon);
+ }
+
+ /**
+ * Returns the right icon to use for this item
+ *
+ * @param context Context to use to get resources
+ * @return Drawable for this item, or null if the package or item could not
+ * be found
+ */
+ public static Drawable getIcon(Context context, StatusBarIcon statusBarIcon) {
+ int userId = statusBarIcon.user.getIdentifier();
+ if (userId == UserHandle.USER_ALL) {
+ userId = UserHandle.USER_SYSTEM;
+ }
+
+ Drawable icon = statusBarIcon.icon.loadDrawableAsUser(context, userId);
+
+ TypedValue typedValue = new TypedValue();
+ context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+ float scaleFactor = typedValue.getFloat();
+
+ // No need to scale the icon, so return it as is.
+ if (scaleFactor == 1.f) {
+ return icon;
+ }
+
+ return new ScalingDrawableWrapper(icon, scaleFactor);
+ }
+
+ public StatusBarIcon getStatusBarIcon() {
+ return mIcon;
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ if (mNotification != null) {
+ event.setParcelableData(mNotification.getNotification());
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (mNumberBackground != null) {
+ placeNumber();
+ }
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+ updateDrawable();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mIconAppearAmount > 0.0f) {
+ canvas.save();
+ canvas.scale(mIconScale * mIconAppearAmount, mIconScale * mIconAppearAmount,
+ getWidth() / 2, getHeight() / 2);
+ super.onDraw(canvas);
+ canvas.restore();
+ }
+
+ if (mNumberBackground != null) {
+ mNumberBackground.draw(canvas);
+ canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain);
+ }
+ if (mDotAppearAmount != 0.0f) {
+ float radius;
+ float alpha;
+ if (mDotAppearAmount <= 1.0f) {
+ radius = mDotRadius * mDotAppearAmount;
+ alpha = 1.0f;
+ } else {
+ float fadeOutAmount = mDotAppearAmount - 1.0f;
+ alpha = 1.0f - fadeOutAmount;
+ radius = NotificationUtils.interpolate(mDotRadius, getWidth() / 4, fadeOutAmount);
+ }
+ mDotPaint.setAlpha((int) (alpha * 255));
+ canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mDotPaint);
+ }
+ }
+
+ @Override
+ protected void debug(int depth) {
+ super.debug(depth);
+ Log.d("View", debugIndent(depth) + "slot=" + mSlot);
+ Log.d("View", debugIndent(depth) + "icon=" + mIcon);
+ }
+
+ void placeNumber() {
+ final String str;
+ final int tooBig = getContext().getResources().getInteger(
+ android.R.integer.status_bar_notification_info_maxnum);
+ if (mIcon.number > tooBig) {
+ str = getContext().getResources().getString(
+ android.R.string.status_bar_notification_info_overflow);
+ } else {
+ NumberFormat f = NumberFormat.getIntegerInstance();
+ str = f.format(mIcon.number);
+ }
+ mNumberText = str;
+
+ final int w = getWidth();
+ final int h = getHeight();
+ final Rect r = new Rect();
+ mNumberPain.getTextBounds(str, 0, str.length(), r);
+ final int tw = r.right - r.left;
+ final int th = r.bottom - r.top;
+ mNumberBackground.getPadding(r);
+ int dw = r.left + tw + r.right;
+ if (dw < mNumberBackground.getMinimumWidth()) {
+ dw = mNumberBackground.getMinimumWidth();
+ }
+ mNumberX = w-r.right-((dw-r.right-r.left)/2);
+ int dh = r.top + th + r.bottom;
+ if (dh < mNumberBackground.getMinimumWidth()) {
+ dh = mNumberBackground.getMinimumWidth();
+ }
+ mNumberY = h-r.bottom-((dh-r.top-th-r.bottom)/2);
+ mNumberBackground.setBounds(w-dw, h-dh, w, h);
+ }
+
+ private void setContentDescription(Notification notification) {
+ if (notification != null) {
+ String d = contentDescForNotification(mContext, notification);
+ if (!TextUtils.isEmpty(d)) {
+ setContentDescription(d);
+ }
+ }
+ }
+
+ public String toString() {
+ return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon
+ + " notification=" + mNotification + ")";
+ }
+
+ public StatusBarNotification getNotification() {
+ return mNotification;
+ }
+
+ public String getSlot() {
+ return mSlot;
+ }
+
+
+ public static String contentDescForNotification(Context c, Notification n) {
+ String appName = "";
+ try {
+ Notification.Builder builder = Notification.Builder.recoverBuilder(c, n);
+ appName = builder.loadHeaderAppName();
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Unable to recover builder", e);
+ // Trying to get the app name from the app info instead.
+ Parcelable appInfo = n.extras.getParcelable(
+ Notification.EXTRA_BUILDER_APPLICATION_INFO);
+ if (appInfo instanceof ApplicationInfo) {
+ appName = String.valueOf(((ApplicationInfo) appInfo).loadLabel(
+ c.getPackageManager()));
+ }
+ }
+
+ CharSequence title = n.extras.getCharSequence(Notification.EXTRA_TITLE);
+ CharSequence text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
+ CharSequence ticker = n.tickerText;
+
+ // Some apps just put the app name into the title
+ CharSequence titleOrText = TextUtils.equals(title, appName) ? text : title;
+
+ CharSequence desc = !TextUtils.isEmpty(titleOrText) ? titleOrText
+ : !TextUtils.isEmpty(ticker) ? ticker : "";
+
+ return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
+ }
+
+ /**
+ * Set the color that is used to draw decoration like the overflow dot. This will not be applied
+ * to the drawable.
+ */
+ public void setDecorColor(int iconTint) {
+ mDecorColor = iconTint;
+ updateDecorColor();
+ }
+
+ private void updateDecorColor() {
+ int color = NotificationUtils.interpolateColors(mDecorColor, Color.WHITE, mDarkAmount);
+ if (mDotPaint.getColor() != color) {
+ mDotPaint.setColor(color);
+
+ if (mDotAppearAmount != 0) {
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Set the static color that should be used for the drawable of this icon if it's not
+ * transitioning this also immediately sets the color.
+ */
+ public void setStaticDrawableColor(int color) {
+ mDrawableColor = color;
+ setColorInternal(color);
+ updateContrastedStaticColor();
+ mIconColor = color;
+ mDozer.setColor(color);
+ }
+
+ private void setColorInternal(int color) {
+ mCurrentSetColor = color;
+ updateIconColor();
+ }
+
+ private void updateIconColor() {
+ if (mCurrentSetColor != NO_COLOR) {
+ if (mMatrixColorFilter == null) {
+ mMatrix = new float[4 * 5];
+ mMatrixColorFilter = new ColorMatrixColorFilter(mMatrix);
+ }
+ int color = NotificationUtils.interpolateColors(
+ mCurrentSetColor, Color.WHITE, mDarkAmount);
+ updateTintMatrix(mMatrix, color, DARK_ALPHA_BOOST * mDarkAmount);
+ mMatrixColorFilter.setColorMatrixArray(mMatrix);
+ setColorFilter(mMatrixColorFilter);
+ invalidate(); // setColorFilter only invalidates if the filter instance changed.
+ } else {
+ mDozer.updateGrayscale(this, mDarkAmount);
+ }
+ }
+
+ /**
+ * Updates {@param array} such that it represents a matrix that changes RGB to {@param color}
+ * and multiplies the alpha channel with the color's alpha+{@param alphaBoost}.
+ */
+ private static void updateTintMatrix(float[] array, int color, float alphaBoost) {
+ Arrays.fill(array, 0);
+ array[4] = Color.red(color);
+ array[9] = Color.green(color);
+ array[14] = Color.blue(color);
+ array[18] = Color.alpha(color) / 255f + alphaBoost;
+ }
+
+ public void setIconColor(int iconColor, boolean animate) {
+ if (mIconColor != iconColor) {
+ mIconColor = iconColor;
+ if (mColorAnimator != null) {
+ mColorAnimator.cancel();
+ }
+ if (mCurrentSetColor == iconColor) {
+ return;
+ }
+ if (animate && mCurrentSetColor != NO_COLOR) {
+ mAnimationStartColor = mCurrentSetColor;
+ mColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+ mColorAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mColorAnimator.setDuration(ANIMATION_DURATION_FAST);
+ mColorAnimator.addUpdateListener(mColorUpdater);
+ mColorAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mColorAnimator = null;
+ mAnimationStartColor = NO_COLOR;
+ }
+ });
+ mColorAnimator.start();
+ } else {
+ setColorInternal(iconColor);
+ }
+ }
+ }
+
+ public int getStaticDrawableColor() {
+ return mDrawableColor;
+ }
+
+ /**
+ * A drawable color that passes GAR on a specific background.
+ * This value is cached.
+ *
+ * @param backgroundColor Background to test against.
+ * @return GAR safe version of {@link StatusBarIconView#getStaticDrawableColor()}.
+ */
+ int getContrastedStaticDrawableColor(int backgroundColor) {
+ if (mCachedContrastBackgroundColor != backgroundColor) {
+ mCachedContrastBackgroundColor = backgroundColor;
+ updateContrastedStaticColor();
+ }
+ return mContrastedDrawableColor;
+ }
+
+ private void updateContrastedStaticColor() {
+ if (Color.alpha(mCachedContrastBackgroundColor) != 255) {
+ mContrastedDrawableColor = mDrawableColor;
+ return;
+ }
+ // We'll modify the color if it doesn't pass GAR
+ int contrastedColor = mDrawableColor;
+ if (!NotificationColorUtil.satisfiesTextContrast(mCachedContrastBackgroundColor,
+ contrastedColor)) {
+ float[] hsl = new float[3];
+ ColorUtils.colorToHSL(mDrawableColor, hsl);
+ // This is basically a light grey, pushing the color will only distort it.
+ // Best thing to do in here is to fallback to the default color.
+ if (hsl[1] < 0.2f) {
+ contrastedColor = Notification.COLOR_DEFAULT;
+ }
+ contrastedColor = NotificationColorUtil.resolveContrastColor(mContext,
+ contrastedColor, mCachedContrastBackgroundColor);
+ }
+ mContrastedDrawableColor = contrastedColor;
+ }
+
+ public void setVisibleState(int state) {
+ setVisibleState(state, true /* animate */, null /* endRunnable */);
+ }
+
+ public void setVisibleState(int state, boolean animate) {
+ setVisibleState(state, animate, null);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ public void setVisibleState(int visibleState, boolean animate, Runnable endRunnable) {
+ boolean runnableAdded = false;
+ if (visibleState != mVisibleState) {
+ mVisibleState = visibleState;
+ if (mIconAppearAnimator != null) {
+ mIconAppearAnimator.cancel();
+ }
+ if (mDotAnimator != null) {
+ mDotAnimator.cancel();
+ }
+ if (animate) {
+ float targetAmount = 0.0f;
+ Interpolator interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+ if (visibleState == STATE_ICON) {
+ targetAmount = 1.0f;
+ interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+ }
+ float currentAmount = getIconAppearAmount();
+ if (targetAmount != currentAmount) {
+ mIconAppearAnimator = ObjectAnimator.ofFloat(this, ICON_APPEAR_AMOUNT,
+ currentAmount, targetAmount);
+ mIconAppearAnimator.setInterpolator(interpolator);
+ mIconAppearAnimator.setDuration(ANIMATION_DURATION_FAST);
+ mIconAppearAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIconAppearAnimator = null;
+ runRunnable(endRunnable);
+ }
+ });
+ mIconAppearAnimator.start();
+ runnableAdded = true;
+ }
+
+ targetAmount = visibleState == STATE_ICON ? 2.0f : 0.0f;
+ interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+ if (visibleState == STATE_DOT) {
+ targetAmount = 1.0f;
+ interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+ }
+ currentAmount = getDotAppearAmount();
+ if (targetAmount != currentAmount) {
+ mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
+ currentAmount, targetAmount);
+ mDotAnimator.setInterpolator(interpolator);
+ mDotAnimator.setDuration(ANIMATION_DURATION_FAST);
+ final boolean runRunnable = !runnableAdded;
+ mDotAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mDotAnimator = null;
+ if (runRunnable) {
+ runRunnable(endRunnable);
+ }
+ }
+ });
+ mDotAnimator.start();
+ runnableAdded = true;
+ }
+ } else {
+ setIconAppearAmount(visibleState == STATE_ICON ? 1.0f : 0.0f);
+ setDotAppearAmount(visibleState == STATE_DOT ? 1.0f
+ : visibleState == STATE_ICON ? 2.0f
+ : 0.0f);
+ }
+ }
+ if (!runnableAdded) {
+ runRunnable(endRunnable);
+ }
+ }
+
+ private void runRunnable(Runnable runnable) {
+ if (runnable != null) {
+ runnable.run();
+ }
+ }
+
+ public void setIconAppearAmount(float iconAppearAmount) {
+ if (mIconAppearAmount != iconAppearAmount) {
+ mIconAppearAmount = iconAppearAmount;
+ invalidate();
+ }
+ }
+
+ public float getIconAppearAmount() {
+ return mIconAppearAmount;
+ }
+
+ public int getVisibleState() {
+ return mVisibleState;
+ }
+
+ public void setDotAppearAmount(float dotAppearAmount) {
+ if (mDotAppearAmount != dotAppearAmount) {
+ mDotAppearAmount = dotAppearAmount;
+ invalidate();
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ if (mOnVisibilityChangedListener != null) {
+ mOnVisibilityChangedListener.onVisibilityChanged(visibility);
+ }
+ }
+
+ public float getDotAppearAmount() {
+ return mDotAppearAmount;
+ }
+
+ public void setOnVisibilityChangedListener(OnVisibilityChangedListener listener) {
+ mOnVisibilityChangedListener = listener;
+ }
+
+ public void setDark(boolean dark, boolean fade, long delay) {
+ mDozer.setIntensityDark(f -> {
+ mDarkAmount = f;
+ updateIconScale();
+ updateDecorColor();
+ updateIconColor();
+ updateAllowAnimation();
+ }, dark, fade, delay);
+ }
+
+ private void updateAllowAnimation() {
+ if (mDarkAmount == 0 || mDarkAmount == 1) {
+ setAllowAnimation(mDarkAmount == 0);
+ }
+ }
+
+ public void setIsInShelf(boolean isInShelf) {
+ mIsInShelf = isInShelf;
+ }
+
+ public boolean isInShelf() {
+ return mIsInShelf;
+ }
+
+ public interface OnVisibilityChangedListener {
+ void onVisibilityChanged(int newVisibility);
+ }
+}
diff --git a/com/android/systemui/statusbar/StatusBarState.java b/com/android/systemui/statusbar/StatusBarState.java
new file mode 100644
index 0000000..c0148c0
--- /dev/null
+++ b/com/android/systemui/statusbar/StatusBarState.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+/**
+ * Class to encapsulate all possible status bar states regarding Keyguard.
+ */
+public class StatusBarState {
+
+ /**
+ * The status bar is in the "normal" shade mode.
+ */
+ public static final int SHADE = 0;
+
+ /**
+ * Status bar is currently the Keyguard.
+ */
+ public static final int KEYGUARD = 1;
+
+ /**
+ * Status bar is in the special mode, where it is fully interactive but still locked. So
+ * dismissing the shade will still show the bouncer.
+ */
+ public static final int SHADE_LOCKED = 2;
+
+ /**
+ * Status bar is locked and shows the full screen user switcher.
+ */
+ public static final int FULLSCREEN_USER_SWITCHER = 3;
+
+
+ public static String toShortString(int x) {
+ switch (x) {
+ case SHADE:
+ return "SHD";
+ case SHADE_LOCKED:
+ return "SHD_LCK";
+ case KEYGUARD:
+ return "KGRD";
+ case FULLSCREEN_USER_SWITCHER:
+ return "FS_USRSW";
+ default:
+ return "bad_value_" + x;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/TransformableView.java b/com/android/systemui/statusbar/TransformableView.java
new file mode 100644
index 0000000..063252f
--- /dev/null
+++ b/com/android/systemui/statusbar/TransformableView.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 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 com.android.systemui.statusbar.notification.TransformState;
+
+/**
+ * A view that can be transformed to and from.
+ */
+public interface TransformableView {
+ int TRANSFORMING_VIEW_ICON = 0;
+ int TRANSFORMING_VIEW_TITLE = 1;
+ int TRANSFORMING_VIEW_TEXT = 2;
+ int TRANSFORMING_VIEW_IMAGE = 3;
+ int TRANSFORMING_VIEW_PROGRESS = 4;
+ int TRANSFORMING_VIEW_ACTIONS = 5;
+
+ /**
+ * Get the current state of a view in a transform animation
+ *
+ * @param fadingView which view we are interested in
+ * @return the current transform state of this viewtype
+ */
+ TransformState getCurrentState(int fadingView);
+
+ /**
+ * Transform to the given view
+ *
+ * @param notification the view to transform to
+ */
+ void transformTo(TransformableView notification, Runnable endRunnable);
+
+ /**
+ * Transform to the given view by a specified amount.
+ *
+ * @param notification the view to transform to
+ * @param transformationAmount how much transformation should be done
+ */
+ void transformTo(TransformableView notification, float transformationAmount);
+
+ /**
+ * Transform to this view from the given view
+ *
+ * @param notification the view to transform from
+ */
+ void transformFrom(TransformableView notification);
+
+ /**
+ * Transform to this view from the given view by a specified amount.
+ *
+ * @param notification the view to transform from
+ * @param transformationAmount how much transformation should be done
+ */
+ void transformFrom(TransformableView notification, float transformationAmount);
+
+ /**
+ * Set this view to be fully visible or gone
+ *
+ * @param visible
+ */
+ void setVisible(boolean visible);
+}
diff --git a/com/android/systemui/statusbar/UserUtil.java b/com/android/systemui/statusbar/UserUtil.java
new file mode 100644
index 0000000..f9afc7c
--- /dev/null
+++ b/com/android/systemui/statusbar/UserUtil.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 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 com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+import android.content.Context;
+import android.content.DialogInterface;
+
+import com.android.systemui.R;
+
+public class UserUtil {
+ public static void deleteUserWithPrompt(Context context, int userId,
+ UserSwitcherController userSwitcherController) {
+ new RemoveUserDialog(context, userId, userSwitcherController).show();
+ }
+
+ private final static class RemoveUserDialog extends SystemUIDialog implements
+ DialogInterface.OnClickListener {
+
+ private final int mUserId;
+ private final UserSwitcherController mUserSwitcherController;
+
+ public RemoveUserDialog(Context context, int userId,
+ UserSwitcherController userSwitcherController) {
+ super(context);
+ setTitle(R.string.user_remove_user_title);
+ setMessage(context.getString(R.string.user_remove_user_message));
+ setButton(DialogInterface.BUTTON_NEGATIVE,
+ context.getString(android.R.string.cancel), this);
+ setButton(DialogInterface.BUTTON_POSITIVE,
+ context.getString(R.string.user_remove_user_remove), this);
+ setCanceledOnTouchOutside(false);
+ mUserId = userId;
+ mUserSwitcherController = userSwitcherController;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == BUTTON_NEGATIVE) {
+ cancel();
+ } else {
+ dismiss();
+ mUserSwitcherController.removeUserId(mUserId);
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/ViewTransformationHelper.java b/com/android/systemui/statusbar/ViewTransformationHelper.java
new file mode 100644
index 0000000..5353005
--- /dev/null
+++ b/com/android/systemui/statusbar/ViewTransformationHelper.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2016 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.util.ArrayMap;
+import android.util.ArraySet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.TransformState;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+import java.util.Stack;
+
+/**
+ * A view that can be transformed to and from.
+ */
+public class ViewTransformationHelper implements TransformableView {
+
+ private static final int TAG_CONTAINS_TRANSFORMED_VIEW = R.id.contains_transformed_view;
+
+ private ArrayMap<Integer, View> mTransformedViews = new ArrayMap<>();
+ private ArrayMap<Integer, CustomTransformation> mCustomTransformations = new ArrayMap<>();
+ private ValueAnimator mViewTransformationAnimation;
+
+ public void addTransformedView(int key, View transformedView) {
+ mTransformedViews.put(key, transformedView);
+ }
+
+ public void reset() {
+ mTransformedViews.clear();
+ }
+
+ public void setCustomTransformation(CustomTransformation transformation, int viewType) {
+ mCustomTransformations.put(viewType, transformation);
+ }
+
+ @Override
+ public TransformState getCurrentState(int fadingView) {
+ View view = mTransformedViews.get(fadingView);
+ if (view != null && view.getVisibility() != View.GONE) {
+ return TransformState.createFrom(view);
+ }
+ return null;
+ }
+
+ @Override
+ public void transformTo(final TransformableView notification, final Runnable endRunnable) {
+ if (mViewTransformationAnimation != null) {
+ mViewTransformationAnimation.cancel();
+ }
+ mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+ mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ transformTo(notification, animation.getAnimatedFraction());
+ }
+ });
+ mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR);
+ mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() {
+ public boolean mCancelled;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCancelled) {
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
+ setVisible(false);
+ } else {
+ abortTransformations();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+ });
+ mViewTransformationAnimation.start();
+ }
+
+ @Override
+ public void transformTo(TransformableView notification, float transformationAmount) {
+ for (Integer viewType : mTransformedViews.keySet()) {
+ TransformState ownState = getCurrentState(viewType);
+ if (ownState != null) {
+ CustomTransformation customTransformation = mCustomTransformations.get(viewType);
+ if (customTransformation != null && customTransformation.transformTo(
+ ownState, notification, transformationAmount)) {
+ ownState.recycle();
+ continue;
+ }
+ TransformState otherState = notification.getCurrentState(viewType);
+ if (otherState != null) {
+ ownState.transformViewTo(otherState, transformationAmount);
+ otherState.recycle();
+ } else {
+ ownState.disappear(transformationAmount, notification);
+ }
+ ownState.recycle();
+ }
+ }
+ }
+
+ @Override
+ public void transformFrom(final TransformableView notification) {
+ if (mViewTransformationAnimation != null) {
+ mViewTransformationAnimation.cancel();
+ }
+ mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+ mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ transformFrom(notification, animation.getAnimatedFraction());
+ }
+ });
+ mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() {
+ public boolean mCancelled;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCancelled) {
+ setVisible(true);
+ } else {
+ abortTransformations();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+ });
+ mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR);
+ mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ mViewTransformationAnimation.start();
+ }
+
+ @Override
+ public void transformFrom(TransformableView notification, float transformationAmount) {
+ for (Integer viewType : mTransformedViews.keySet()) {
+ TransformState ownState = getCurrentState(viewType);
+ if (ownState != null) {
+ CustomTransformation customTransformation = mCustomTransformations.get(viewType);
+ if (customTransformation != null && customTransformation.transformFrom(
+ ownState, notification, transformationAmount)) {
+ ownState.recycle();
+ continue;
+ }
+ TransformState otherState = notification.getCurrentState(viewType);
+ if (otherState != null) {
+ ownState.transformViewFrom(otherState, transformationAmount);
+ otherState.recycle();
+ } else {
+ ownState.appear(transformationAmount, notification);
+ }
+ ownState.recycle();
+ }
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (mViewTransformationAnimation != null) {
+ mViewTransformationAnimation.cancel();
+ }
+ for (Integer viewType : mTransformedViews.keySet()) {
+ TransformState ownState = getCurrentState(viewType);
+ if (ownState != null) {
+ ownState.setVisible(visible, false /* force */);
+ ownState.recycle();
+ }
+ }
+ }
+
+ private void abortTransformations() {
+ for (Integer viewType : mTransformedViews.keySet()) {
+ TransformState ownState = getCurrentState(viewType);
+ if (ownState != null) {
+ ownState.abortTransformation();
+ ownState.recycle();
+ }
+ }
+ }
+
+ /**
+ * Add the remaining transformation views such that all views are being transformed correctly
+ * @param viewRoot the root below which all elements need to be transformed
+ */
+ public void addRemainingTransformTypes(View viewRoot) {
+ // lets now tag the right views
+ int numValues = mTransformedViews.size();
+ for (int i = 0; i < numValues; i++) {
+ View view = mTransformedViews.valueAt(i);
+ while (view != viewRoot.getParent()) {
+ view.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, true);
+ view = (View) view.getParent();
+ }
+ }
+ Stack<View> stack = new Stack<>();
+ // Add the right views now
+ stack.push(viewRoot);
+ while (!stack.isEmpty()) {
+ View child = stack.pop();
+ Boolean containsView = (Boolean) child.getTag(TAG_CONTAINS_TRANSFORMED_VIEW);
+ if (containsView == null) {
+ // This one is unhandled, let's add it to our list.
+ int id = child.getId();
+ if (id != View.NO_ID) {
+ // We only fade views with an id
+ addTransformedView(id, child);
+ continue;
+ }
+ }
+ child.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, null);
+ if (child instanceof ViewGroup && !mTransformedViews.containsValue(child)){
+ ViewGroup group = (ViewGroup) child;
+ for (int i = 0; i < group.getChildCount(); i++) {
+ stack.push(group.getChildAt(i));
+ }
+ }
+ }
+ }
+
+ public void resetTransformedView(View view) {
+ TransformState state = TransformState.createFrom(view);
+ state.setVisible(true /* visible */, true /* force */);
+ state.recycle();
+ }
+
+ /**
+ * @return a set of all views are being transformed.
+ */
+ public ArraySet<View> getAllTransformingViews() {
+ return new ArraySet<>(mTransformedViews.values());
+ }
+
+ public static abstract class CustomTransformation {
+ /**
+ * Transform a state to the given view
+ * @param ownState the state to transform
+ * @param notification the view to transform to
+ * @param transformationAmount how much transformation should be done
+ * @return whether a custom transformation is performed
+ */
+ public abstract boolean transformTo(TransformState ownState,
+ TransformableView notification,
+ float transformationAmount);
+
+ /**
+ * Transform to this state from the given view
+ * @param ownState the state to transform to
+ * @param notification the view to transform from
+ * @param transformationAmount how much transformation should be done
+ * @return whether a custom transformation is performed
+ */
+ public abstract boolean transformFrom(TransformState ownState,
+ TransformableView notification,
+ float transformationAmount);
+
+ /**
+ * Perform a custom initialisation before transforming.
+ *
+ * @param ownState our own state
+ * @param otherState the other state
+ * @return whether a custom initialization is done
+ */
+ public boolean initTransformation(TransformState ownState,
+ TransformState otherState) {
+ return false;
+ }
+
+ public boolean customTransformTarget(TransformState ownState,
+ TransformState otherState) {
+ return false;
+ }
+
+ /**
+ * Get a custom interpolator for this animation
+ * @param interpolationType the type of the interpolation, i.e TranslationX / TranslationY
+ * @param isFrom true if this transformation from the other view
+ */
+ public Interpolator getCustomInterpolator(int interpolationType, boolean isFrom) {
+ return null;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/car/CarBatteryController.java b/com/android/systemui/statusbar/car/CarBatteryController.java
new file mode 100644
index 0000000..fc39648
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarBatteryController.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2016 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.car;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadsetClient;
+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.Bundle;
+import android.util.Log;
+
+import com.android.systemui.statusbar.policy.BatteryController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * A {@link BatteryController} that is specific to the Auto use-case. For Auto, the battery icon
+ * displays the battery status of a device that is connected via bluetooth and not the system's
+ * battery.
+ */
+public class CarBatteryController extends BroadcastReceiver implements BatteryController {
+ private static final String TAG = "CarBatteryController";
+
+ // According to the Bluetooth HFP 1.5 specification, battery levels are indicated by a
+ // value from 1-5, where these values represent the following:
+ // 0%% - 0, 1-25%% - 1, 26-50%% - 2, 51-75%% - 3, 76-99%% - 4, 100%% - 5
+ // As a result, set the level as the average within that range.
+ private static final int BATTERY_LEVEL_EMPTY = 0;
+ private static final int BATTERY_LEVEL_1 = 12;
+ private static final int BATTERY_LEVEL_2 = 28;
+ private static final int BATTERY_LEVEL_3 = 63;
+ private static final int BATTERY_LEVEL_4 = 87;
+ private static final int BATTERY_LEVEL_FULL = 100;
+
+ private static final int INVALID_BATTERY_LEVEL = -1;
+
+ private final Context mContext;
+
+ private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+ private BluetoothHeadsetClient mBluetoothHeadsetClient;
+
+ private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>();
+
+ private int mLevel;
+
+ /**
+ * An interface indicating the container of a View that will display what the information
+ * in the {@link CarBatteryController}.
+ */
+ public interface BatteryViewHandler {
+ void hideBatteryView();
+ void showBatteryView();
+ }
+
+ private BatteryViewHandler mBatteryViewHandler;
+
+ public CarBatteryController(Context context) {
+ mContext = context;
+
+ if (mAdapter == null) {
+ return;
+ }
+
+ mAdapter.getProfileProxy(context.getApplicationContext(), mHfpServiceListener,
+ BluetoothProfile.HEADSET_CLIENT);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("CarBatteryController state:");
+ pw.print(" mLevel=");
+ pw.println(mLevel);
+ }
+
+ @Override
+ public void setPowerSaveMode(boolean powerSave) {
+ // No-op. No power save mode for the car.
+ }
+
+ @Override
+ public void addCallback(BatteryController.BatteryStateChangeCallback cb) {
+ mChangeCallbacks.add(cb);
+
+ // There is no way to know if the phone is plugged in or charging via bluetooth, so pass
+ // false for these values.
+ cb.onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
+ cb.onPowerSaveChanged(false /* isPowerSave */);
+ }
+
+ @Override
+ public void removeCallback(BatteryController.BatteryStateChangeCallback cb) {
+ mChangeCallbacks.remove(cb);
+ }
+
+ public void addBatteryViewHandler(BatteryViewHandler batteryViewHandler) {
+ mBatteryViewHandler = batteryViewHandler;
+ }
+
+ public void startListening() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
+ mContext.registerReceiver(this, filter);
+ }
+
+ public void stopListening() {
+ mContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onReceive(). action: " + action);
+ }
+
+ if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Received ACTION_AG_EVENT");
+ }
+
+ int batteryLevel = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
+ INVALID_BATTERY_LEVEL);
+
+ updateBatteryLevel(batteryLevel);
+
+ if (batteryLevel != INVALID_BATTERY_LEVEL && mBatteryViewHandler != null) {
+ mBatteryViewHandler.showBatteryView();
+ }
+ } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+ int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+ Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
+ + oldState + " -> " + newState);
+
+ }
+ BluetoothDevice device =
+ (BluetoothDevice)intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
+ updateBatteryIcon(device, newState);
+ }
+ }
+
+ /**
+ * Converts the battery level to a percentage that can be displayed on-screen and notifies
+ * any {@link BatteryStateChangeCallback}s of this.
+ */
+ private void updateBatteryLevel(int batteryLevel) {
+ if (batteryLevel == INVALID_BATTERY_LEVEL) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Battery level invalid. Ignoring.");
+ }
+ return;
+ }
+
+ // The battery level is a value between 0-5. Let the default battery level be 0.
+ switch (batteryLevel) {
+ case 5:
+ mLevel = BATTERY_LEVEL_FULL;
+ break;
+ case 4:
+ mLevel = BATTERY_LEVEL_4;
+ break;
+ case 3:
+ mLevel = BATTERY_LEVEL_3;
+ break;
+ case 2:
+ mLevel = BATTERY_LEVEL_2;
+ break;
+ case 1:
+ mLevel = BATTERY_LEVEL_1;
+ break;
+ case 0:
+ default:
+ mLevel = BATTERY_LEVEL_EMPTY;
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Battery level: " + batteryLevel + "; setting mLevel as: " + mLevel);
+ }
+
+ notifyBatteryLevelChanged();
+ }
+
+ /**
+ * Updates the display of the battery icon depending on the given connection state from the
+ * given {@link BluetoothDevice}.
+ */
+ private void updateBatteryIcon(BluetoothDevice device, int newState) {
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Device connected");
+ }
+
+ if (mBatteryViewHandler != null) {
+ mBatteryViewHandler.showBatteryView();
+ }
+
+ if (mBluetoothHeadsetClient == null || device == null) {
+ return;
+ }
+
+ // Check if battery information is available and immediately update.
+ Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device);
+ if (featuresBundle == null) {
+ return;
+ }
+
+ int batteryLevel = featuresBundle.getInt(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
+ INVALID_BATTERY_LEVEL);
+ updateBatteryLevel(batteryLevel);
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Device disconnected");
+ }
+
+ if (mBatteryViewHandler != null) {
+ mBatteryViewHandler.hideBatteryView();
+ }
+ }
+ }
+
+ @Override
+ public void dispatchDemoCommand(String command, Bundle args) {
+ // TODO: Car demo mode.
+ }
+
+ @Override
+ public boolean isPowerSave() {
+ // Power save is not valid for the car, so always return false.
+ return false;
+ }
+
+ private void notifyBatteryLevelChanged() {
+ for (int i = 0, size = mChangeCallbacks.size(); i < size; i++) {
+ mChangeCallbacks.get(i)
+ .onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
+ }
+ }
+
+ private final ServiceListener mHfpServiceListener = new ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ if (profile == BluetoothProfile.HEADSET_CLIENT) {
+ mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ if (profile == BluetoothProfile.HEADSET_CLIENT) {
+ mBluetoothHeadsetClient = null;
+ }
+ }
+ };
+
+}
diff --git a/com/android/systemui/statusbar/car/CarNavigationBarController.java b/com/android/systemui/statusbar/car/CarNavigationBarController.java
new file mode 100644
index 0000000..7e08d56
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarNavigationBarController.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2015 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.car;
+
+import android.app.ActivityManager.StackId;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.v4.util.SimpleArrayMap;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.view.View;
+import android.widget.LinearLayout;
+import com.android.systemui.R;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A controller to populate data for CarNavigationBarView and handle user interactions.
+ *
+ * <p>Each button inside the navigation bar is defined by data in arrays_car.xml. OEMs can
+ * customize the navigation buttons by updating arrays_car.xml appropriately in an overlay.
+ */
+class CarNavigationBarController {
+ private static final String TAG = "CarNavBarController";
+
+ private static final String EXTRA_FACET_CATEGORIES = "categories";
+ private static final String EXTRA_FACET_PACKAGES = "packages";
+ private static final String EXTRA_FACET_ID = "filter_id";
+ private static final String EXTRA_FACET_LAUNCH_PICKER = "launch_picker";
+
+ /**
+ * Each facet of the navigation bar maps to a set of package names or categories defined in
+ * arrays_car.xml. Package names for a given facet are delimited by ";".
+ */
+ private static final String FACET_FILTER_DELIMITER = ";";
+
+ private final Context mContext;
+ private final CarNavigationBarView mNavBar;
+ private final CarStatusBar mStatusBar;
+
+ /**
+ * Set of categories each facet will filter on.
+ */
+ private final List<String[]> mFacetCategories = new ArrayList<>();
+
+ /**
+ * Set of package names each facet will filter on.
+ */
+ private final List<String[]> mFacetPackages = new ArrayList<>();
+
+ private final SimpleArrayMap<String, Integer> mFacetCategoryMap = new SimpleArrayMap<>();
+ private final SimpleArrayMap<String, Integer> mFacetPackageMap = new SimpleArrayMap<>();
+
+ private final List<CarNavigationButton> mNavButtons = new ArrayList<>();
+
+ private final SparseBooleanArray mFacetHasMultipleAppsCache = new SparseBooleanArray();
+
+ private int mCurrentFacetIndex;
+ private Intent mPersistentTaskIntent;
+
+ public CarNavigationBarController(Context context, CarNavigationBarView navBar,
+ CarStatusBar activityStarter) {
+ mContext = context;
+ mNavBar = navBar;
+ mStatusBar = activityStarter;
+ bind();
+
+ if (context.getResources().getBoolean(R.bool.config_enablePersistentDockedActivity)) {
+ setupPersistentDockedTask();
+ }
+ }
+
+ private void setupPersistentDockedTask() {
+ try {
+ mPersistentTaskIntent = Intent.parseUri(
+ mContext.getString(R.string.config_persistentDockedActivityIntentUri),
+ Intent.URI_INTENT_SCHEME);
+ } catch (URISyntaxException e) {
+ Log.e(TAG, "Malformed persistent task intent.");
+ }
+ }
+
+ public void taskChanged(String packageName, int stackId) {
+ // If the package name belongs to a filter, then highlight appropriate button in
+ // the navigation bar.
+ if (mFacetPackageMap.containsKey(packageName)) {
+ setCurrentFacet(mFacetPackageMap.get(packageName));
+ }
+
+ // Check if the package matches any of the categories for the facets
+ String category = getPackageCategory(packageName);
+ if (category != null) {
+ setCurrentFacet(mFacetCategoryMap.get(category));
+ }
+
+ // Set up the persistent docked task if needed.
+ if (mPersistentTaskIntent != null && !mStatusBar.hasDockedTask()
+ && stackId != StackId.HOME_STACK_ID) {
+ mStatusBar.startActivityOnStack(mPersistentTaskIntent, StackId.DOCKED_STACK_ID);
+ }
+ }
+
+ public void onPackageChange(String packageName) {
+ if (mFacetPackageMap.containsKey(packageName)) {
+ int index = mFacetPackageMap.get(packageName);
+ mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index));
+ // No need to check categories because we've already refreshed the cache.
+ return;
+ }
+
+ String category = getPackageCategory(packageName);
+ if (mFacetCategoryMap.containsKey(category)) {
+ int index = mFacetCategoryMap.get(category);
+ mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index));
+ }
+ }
+
+ /**
+ * Iterates through the items in arrays_car.xml and sets up the facet bar buttons to
+ * perform the task in that configuration file when clicked or long-pressed.
+ */
+ private void bind() {
+ Resources res = mContext.getResources();
+
+ TypedArray icons = res.obtainTypedArray(R.array.car_facet_icons);
+ TypedArray intents = res.obtainTypedArray(R.array.car_facet_intent_uris);
+ TypedArray longPressIntents = res.obtainTypedArray(R.array.car_facet_longpress_intent_uris);
+ TypedArray facetPackageNames = res.obtainTypedArray(R.array.car_facet_package_filters);
+ TypedArray facetCategories = res.obtainTypedArray(R.array.car_facet_category_filters);
+
+ try {
+ if (icons.length() != intents.length()
+ || icons.length() != longPressIntents.length()
+ || icons.length() != facetPackageNames.length()
+ || icons.length() != facetCategories.length()) {
+ throw new RuntimeException("car_facet array lengths do not match");
+ }
+
+ for (int i = 0, size = icons.length(); i < size; i++) {
+ Drawable icon = icons.getDrawable(i);
+ CarNavigationButton button = createNavButton(icon);
+ initClickListeners(button, i, intents.getString(i), longPressIntents.getString(i));
+
+ mNavButtons.add(button);
+ mNavBar.addButton(button, createNavButton(icon) /* lightsOutButton */);
+
+ initFacetFilterMaps(i, facetPackageNames.getString(i).split(FACET_FILTER_DELIMITER),
+ facetCategories.getString(i).split(FACET_FILTER_DELIMITER));
+ mFacetHasMultipleAppsCache.put(i, facetHasMultiplePackages(i));
+ }
+ } finally {
+ // Clean up all the TypedArrays.
+ icons.recycle();
+ intents.recycle();
+ longPressIntents.recycle();
+ facetPackageNames.recycle();
+ facetCategories.recycle();
+ }
+ }
+
+ /**
+ * Recreates each of the buttons on a density or font scale change. This manual process is
+ * necessary since this class is not part of an activity that automatically gets recreated.
+ */
+ public void onDensityOrFontScaleChanged() {
+ TypedArray icons = mContext.getResources().obtainTypedArray(R.array.car_facet_icons);
+
+ try {
+ int length = icons.length();
+ if (length != mNavButtons.size()) {
+ // This should not happen since the mNavButtons list is created from the length
+ // of the icons array in bind().
+ throw new RuntimeException("car_facet array lengths do not match number of "
+ + "created buttons.");
+ }
+
+ for (int i = 0; i < length; i++) {
+ Drawable icon = icons.getDrawable(i);
+
+ // Setting a new icon will trigger a requestLayout() call if necessary.
+ mNavButtons.get(i).setResources(icon);
+ }
+ } finally {
+ icons.recycle();
+ }
+ }
+
+ private void initFacetFilterMaps(int id, String[] packageNames, String[] categories) {
+ mFacetCategories.add(categories);
+ for (String category : categories) {
+ mFacetCategoryMap.put(category, id);
+ }
+
+ mFacetPackages.add(packageNames);
+ for (String packageName : packageNames) {
+ mFacetPackageMap.put(packageName, id);
+ }
+ }
+
+ private String getPackageCategory(String packageName) {
+ PackageManager pm = mContext.getPackageManager();
+ int size = mFacetCategories.size();
+ // For each facet, check if the given package name matches one of its categories
+ for (int i = 0; i < size; i++) {
+ String[] categories = mFacetCategories.get(i);
+ for (int j = 0; j < categories.length; j++) {
+ String category = categories[j];
+ Intent intent = new Intent();
+ intent.setPackage(packageName);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addCategory(category);
+ List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+ if (list.size() > 0) {
+ // Cache this package name into facetPackageMap, so we won't have to query
+ // all categories next time this package name shows up.
+ mFacetPackageMap.put(packageName, mFacetCategoryMap.get(category));
+ return category;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to check if a given facet has multiple packages associated with it. This can
+ * be resource defined package names or package names filtered by facet category.
+ *
+ * @return {@code true} if the facet at the given index has more than one package.
+ */
+ private boolean facetHasMultiplePackages(int index) {
+ PackageManager pm = mContext.getPackageManager();
+
+ // Check if the packages defined for the filter actually exists on the device
+ String[] packages = mFacetPackages.get(index);
+ if (packages.length > 1) {
+ int count = 0;
+ for (int i = 0; i < packages.length; i++) {
+ count += pm.getLaunchIntentForPackage(packages[i]) != null ? 1 : 0;
+ if (count > 1) {
+ return true;
+ }
+ }
+ }
+
+ // If there weren't multiple packages defined for the facet, check the categories
+ // and see if they resolve to multiple package names
+ String categories[] = mFacetCategories.get(index);
+
+ int count = 0;
+ for (int i = 0; i < categories.length; i++) {
+ String category = categories[i];
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addCategory(category);
+ count += pm.queryIntentActivities(intent, 0).size();
+ if (count > 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the facet at the given index to be the facet that is currently active. The button will
+ * be highlighted appropriately.
+ */
+ private void setCurrentFacet(int index) {
+ if (index == mCurrentFacetIndex) {
+ return;
+ }
+
+ if (mNavButtons.get(mCurrentFacetIndex) != null) {
+ mNavButtons.get(mCurrentFacetIndex)
+ .setSelected(false /* selected */, false /* showMoreIcon */);
+ }
+
+ if (mNavButtons.get(index) != null) {
+ mNavButtons.get(index).setSelected(true /* selected */,
+ mFacetHasMultipleAppsCache.get(index) /* showMoreIcon */);
+ }
+
+ mCurrentFacetIndex = index;
+ }
+
+ /**
+ * Creates the View that is used for the buttons along the navigation bar.
+ *
+ * @param icon The icon to be used for the button.
+ */
+ private CarNavigationButton createNavButton(Drawable icon) {
+ CarNavigationButton button = (CarNavigationButton) View.inflate(mContext,
+ R.layout.car_navigation_button, null);
+ button.setResources(icon);
+ LinearLayout.LayoutParams lp =
+ new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1);
+ button.setLayoutParams(lp);
+
+ return button;
+ }
+
+ /**
+ * Initializes the click and long click listeners that correspond to the given command string.
+ * The click listeners are attached to the given button.
+ */
+ private void initClickListeners(View button, int index, String clickString,
+ String longPressString) {
+ // Each button at least have an action when pressed.
+ if (TextUtils.isEmpty(clickString)) {
+ throw new RuntimeException("Facet at index " + index + " does not have click action.");
+ }
+
+ try {
+ Intent intent = Intent.parseUri(clickString, Intent.URI_INTENT_SCHEME);
+ button.setOnClickListener(v -> onFacetClicked(intent, index));
+ } catch (URISyntaxException e) {
+ throw new RuntimeException("Malformed intent uri", e);
+ }
+
+ if (TextUtils.isEmpty(longPressString)) {
+ button.setLongClickable(false);
+ return;
+ }
+
+ try {
+ Intent intent = Intent.parseUri(longPressString, Intent.URI_INTENT_SCHEME);
+ button.setOnLongClickListener(v -> {
+ onFacetLongClicked(intent, index);
+ return true;
+ });
+ } catch (URISyntaxException e) {
+ throw new RuntimeException("Malformed long-press intent uri", e);
+ }
+ }
+
+ /**
+ * Handles a click on a facet. A click will trigger the given Intent.
+ *
+ * @param index The index of the facet that was clicked.
+ */
+ private void onFacetClicked(Intent intent, int index) {
+ String packageName = intent.getPackage();
+
+ if (packageName == null) {
+ return;
+ }
+
+ intent.putExtra(EXTRA_FACET_CATEGORIES, mFacetCategories.get(index));
+ intent.putExtra(EXTRA_FACET_PACKAGES, mFacetPackages.get(index));
+ // The facet is identified by the index in which it was added to the nav bar.
+ // This value can be used to determine which facet was selected
+ intent.putExtra(EXTRA_FACET_ID, Integer.toString(index));
+
+ // If the current facet is clicked, we want to launch the picker by default
+ // rather than the "preferred/last run" app.
+ intent.putExtra(EXTRA_FACET_LAUNCH_PICKER, index == mCurrentFacetIndex);
+
+ int stackId = StackId.FULLSCREEN_WORKSPACE_STACK_ID;
+ if (intent.getCategories().contains(Intent.CATEGORY_HOME)) {
+ stackId = StackId.HOME_STACK_ID;
+ }
+
+ setCurrentFacet(index);
+ mStatusBar.startActivityOnStack(intent, stackId);
+ }
+
+ /**
+ * Handles a long-press on a facet. The long-press will trigger the given Intent.
+ *
+ * @param index The index of the facet that was clicked.
+ */
+ private void onFacetLongClicked(Intent intent, int index) {
+ setCurrentFacet(index);
+ mStatusBar.startActivityOnStack(intent, StackId.FULLSCREEN_WORKSPACE_STACK_ID);
+ }
+}
diff --git a/com/android/systemui/statusbar/car/CarNavigationBarView.java b/com/android/systemui/statusbar/car/CarNavigationBarView.java
new file mode 100644
index 0000000..6cbbd6c
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarNavigationBarView.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2015 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.car;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.NavigationBarView;
+
+/**
+ * A custom navigation bar for the automotive use case.
+ * <p>
+ * The navigation bar in the automotive use case is more like a list of shortcuts, rendered
+ * in a linear layout.
+ */
+class CarNavigationBarView extends NavigationBarView {
+ private LinearLayout mNavButtons;
+ private LinearLayout mLightsOutButtons;
+
+ public CarNavigationBarView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ mNavButtons = findViewById(R.id.nav_buttons);
+ mLightsOutButtons = findViewById(R.id.lights_out);
+ }
+
+ public void addButton(CarNavigationButton button, CarNavigationButton lightsOutButton){
+ mNavButtons.addView(button);
+ mLightsOutButtons.addView(lightsOutButton);
+ }
+
+ @Override
+ public void setDisabledFlags(int disabledFlags, boolean force) {
+ // TODO: Populate.
+ }
+
+ @Override
+ public void reorient() {
+ // We expect the car head unit to always have a fixed rotation so we ignore this. The super
+ // class implentation expects mRotatedViews to be populated, so if you call into it, there
+ // is a possibility of a NullPointerException.
+ }
+
+ @Override
+ public View getCurrentView() {
+ return this;
+ }
+
+ @Override
+ public void setNavigationIconHints(int hints, boolean force) {
+ // We do not need to set the navigation icon hints for a vehicle
+ // Calling setNavigationIconHints in the base class will result in a NPE as the car
+ // navigation bar does not have a back button.
+ }
+}
diff --git a/com/android/systemui/statusbar/car/CarNavigationButton.java b/com/android/systemui/statusbar/car/CarNavigationButton.java
new file mode 100644
index 0000000..2de358f
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarNavigationButton.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 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.car;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import com.android.keyguard.AlphaOptimizedImageButton;
+import com.android.systemui.R;
+
+/**
+ * A wrapper view for a car navigation facet, which includes a button icon and a drop down icon.
+ */
+public class CarNavigationButton extends RelativeLayout {
+ private static final float SELECTED_ALPHA = 1;
+ private static final float UNSELECTED_ALPHA = 0.7f;
+
+ private AlphaOptimizedImageButton mIcon;
+ private AlphaOptimizedImageButton mMoreIcon;
+
+ public CarNavigationButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ super.onFinishInflate();
+ mIcon = findViewById(R.id.car_nav_button_icon);
+ mIcon.setScaleType(ImageView.ScaleType.CENTER);
+ mIcon.setClickable(false);
+ mIcon.setBackgroundColor(android.R.color.transparent);
+ mIcon.setAlpha(UNSELECTED_ALPHA);
+
+ mMoreIcon = findViewById(R.id.car_nav_button_more_icon);
+ mMoreIcon.setClickable(false);
+ mMoreIcon.setBackgroundColor(android.R.color.transparent);
+ mMoreIcon.setVisibility(INVISIBLE);
+ mMoreIcon.setImageDrawable(getContext().getDrawable(R.drawable.car_ic_arrow));
+ mMoreIcon.setAlpha(UNSELECTED_ALPHA);
+ }
+
+ public void setResources(Drawable icon) {
+ mIcon.setImageDrawable(icon);
+ }
+
+ public void setSelected(boolean selected, boolean showMoreIcon) {
+ if (selected) {
+ mMoreIcon.setVisibility(showMoreIcon ? VISIBLE : INVISIBLE);
+ mMoreIcon.setAlpha(SELECTED_ALPHA);
+ mIcon.setAlpha(SELECTED_ALPHA);
+ } else {
+ mMoreIcon.setVisibility(INVISIBLE);
+ mIcon.setAlpha(UNSELECTED_ALPHA);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/car/CarStatusBar.java b/com/android/systemui/statusbar/car/CarStatusBar.java
new file mode 100644
index 0000000..680f693
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2015 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.car;
+
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewStub;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+import com.android.systemui.BatteryMeterView;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
+import com.android.systemui.statusbar.phone.NavigationBarView;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.classifier.FalsingLog;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.Prefs;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Map;
+/**
+ * A status bar (and navigation bar) tailored for the automotive use case.
+ */
+public class CarStatusBar extends StatusBar implements
+ CarBatteryController.BatteryViewHandler {
+ private static final String TAG = "CarStatusBar";
+
+ private TaskStackListenerImpl mTaskStackListener;
+
+ private CarNavigationBarController mController;
+ private FullscreenUserSwitcher mFullscreenUserSwitcher;
+
+ private CarBatteryController mCarBatteryController;
+ private BatteryMeterView mBatteryMeterView;
+ private Drawable mNotificationPanelBackground;
+
+ private ConnectedDeviceSignalController mConnectedDeviceSignalController;
+ private ViewGroup mNavigationBarWindow;
+ private CarNavigationBarView mNavigationBarView;
+
+ private final Object mQueueLock = new Object();
+ @Override
+ public void start() {
+ super.start();
+ mTaskStackListener = new TaskStackListenerImpl();
+ SystemServicesProxy.getInstance(mContext).registerTaskStackListener(mTaskStackListener);
+ registerPackageChangeReceivers();
+
+ mStackScroller.setScrollingEnabled(true);
+
+ createBatteryController();
+ mCarBatteryController.startListening();
+ }
+
+ @Override
+ public void destroy() {
+ mCarBatteryController.stopListening();
+ mConnectedDeviceSignalController.stopListening();
+
+ if (mNavigationBarWindow != null) {
+ mWindowManager.removeViewImmediate(mNavigationBarWindow);
+ mNavigationBarView = null;
+ }
+
+ super.destroy();
+ }
+
+ @Override
+ protected void makeStatusBarView() {
+ super.makeStatusBarView();
+
+ mNotificationPanelBackground = getDefaultWallpaper();
+ mScrimController.setScrimBehindDrawable(mNotificationPanelBackground);
+
+ FragmentHostManager manager = FragmentHostManager.get(mStatusBarWindow);
+ manager.addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {
+ mBatteryMeterView = fragment.getView().findViewById(R.id.battery);
+
+ // By default, the BatteryMeterView should not be visible. It will be toggled
+ // when a device has connected by bluetooth.
+ mBatteryMeterView.setVisibility(View.GONE);
+
+ ViewStub stub = fragment.getView().findViewById(R.id.connected_device_signals_stub);
+ View signalsView = stub.inflate();
+
+ // When a ViewStub if inflated, it does not respect the margins on the
+ // inflated view.
+ // As a result, manually add the ending margin.
+ ((LinearLayout.LayoutParams) signalsView.getLayoutParams()).setMarginEnd(
+ mContext.getResources().getDimensionPixelOffset(
+ R.dimen.status_bar_connected_device_signal_margin_end));
+
+ if (mConnectedDeviceSignalController != null) {
+ mConnectedDeviceSignalController.stopListening();
+ }
+ mConnectedDeviceSignalController = new ConnectedDeviceSignalController(mContext,
+ signalsView);
+ mConnectedDeviceSignalController.startListening();
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "makeStatusBarView(). mBatteryMeterView: " + mBatteryMeterView);
+ }
+ });
+ }
+
+ private BatteryController createBatteryController() {
+ mCarBatteryController = new CarBatteryController(mContext);
+ mCarBatteryController.addBatteryViewHandler(this);
+ return mCarBatteryController;
+ }
+
+ @Override
+ protected void createNavigationBar() {
+ if (mNavigationBarView != null) {
+ return;
+ }
+
+ // SystemUI requires that the navigation bar view have a parent. Since the regular
+ // StatusBar inflates navigation_bar_window as this parent view, use the same view for the
+ // CarNavigationBarView.
+ mNavigationBarWindow = (ViewGroup) View.inflate(mContext,
+ R.layout.navigation_bar_window, null);
+ if (mNavigationBarWindow == null) {
+ Log.e(TAG, "CarStatusBar failed inflate for R.layout.navigation_bar_window");
+ }
+
+
+ View.inflate(mContext, R.layout.car_navigation_bar, mNavigationBarWindow);
+ mNavigationBarView = (CarNavigationBarView) mNavigationBarWindow.getChildAt(0);
+ if (mNavigationBarView == null) {
+ Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar");
+ }
+
+
+ mController = new CarNavigationBarController(mContext, mNavigationBarView,
+ this /* ActivityStarter*/);
+ mNavigationBarView.getBarTransitions().setAlwaysOpaque(true);
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
+ WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+ PixelFormat.TRANSLUCENT);
+ lp.setTitle("CarNavigationBar");
+ lp.windowAnimations = 0;
+
+ mWindowManager.addView(mNavigationBarWindow, lp);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ //When executing dump() funciton simultaneously, we need to serialize them
+ //to get mStackScroller's position correctly.
+ synchronized (mQueueLock) {
+ pw.println(" mStackScroller: " + viewInfo(mStackScroller));
+ pw.println(" mStackScroller: " + viewInfo(mStackScroller)
+ + " scroll " + mStackScroller.getScrollX()
+ + "," + mStackScroller.getScrollY());
+ }
+
+ pw.print(" mTaskStackListener="); pw.println(mTaskStackListener);
+ pw.print(" mController=");
+ pw.println(mController);
+ pw.print(" mFullscreenUserSwitcher="); pw.println(mFullscreenUserSwitcher);
+ pw.print(" mCarBatteryController=");
+ pw.println(mCarBatteryController);
+ pw.print(" mBatteryMeterView=");
+ pw.println(mBatteryMeterView);
+ pw.print(" mConnectedDeviceSignalController=");
+ pw.println(mConnectedDeviceSignalController);
+ pw.print(" mNavigationBarView=");
+ pw.println(mNavigationBarView);
+
+ if (KeyguardUpdateMonitor.getInstance(mContext) != null) {
+ KeyguardUpdateMonitor.getInstance(mContext).dump(fd, pw, args);
+ }
+
+ FalsingManager.getInstance(mContext).dump(pw);
+ FalsingLog.dump(pw);
+
+ pw.println("SharedPreferences:");
+ for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) {
+ pw.print(" "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
+ }
+ }
+
+ @Override
+ public NavigationBarView getNavigationBarView() {
+ return mNavigationBarView;
+ }
+
+ @Override
+ public View getNavigationBarWindow() {
+ return mNavigationBarWindow;
+ }
+
+ @Override
+ protected View.OnTouchListener getStatusBarWindowTouchListener() {
+ // Usually, a touch on the background window will dismiss the notification shade. However,
+ // for the car use-case, the shade should remain unless the user switches to a different
+ // facet (e.g. phone).
+ return null;
+ }
+
+ /**
+ * Returns the {@link com.android.systemui.SwipeHelper.LongPressListener} that will be
+ * triggered when a notification card is long-pressed.
+ */
+ @Override
+ protected SwipeHelper.LongPressListener getNotificationLongClicker() {
+ // For the automative use case, we do not want to the user to be able to interact with
+ // a notification other than a regular click. As a result, just return null for the
+ // long click listener.
+ return null;
+ }
+
+ @Override
+ public void showBatteryView() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "showBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
+ }
+
+ if (mBatteryMeterView != null) {
+ mBatteryMeterView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void hideBatteryView() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "hideBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
+ }
+
+ if (mBatteryMeterView != null) {
+ mBatteryMeterView.setVisibility(View.GONE);
+ }
+ }
+
+ private BroadcastReceiver mPackageChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getData() == null || mController == null) {
+ return;
+ }
+ String packageName = intent.getData().getSchemeSpecificPart();
+ mController.onPackageChange(packageName);
+ }
+ };
+
+ private void registerPackageChangeReceivers() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mPackageChangeReceiver, filter);
+ }
+
+ public boolean hasDockedTask() {
+ return Recents.getSystemServices().hasDockedTask();
+ }
+
+ /**
+ * An implementation of TaskStackListener, that listens for changes in the system task
+ * stack and notifies the navigation bar.
+ */
+ private class TaskStackListenerImpl extends TaskStackListener {
+ @Override
+ public void onTaskStackChanged() {
+ SystemServicesProxy ssp = Recents.getSystemServices();
+ ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getRunningTask();
+ if (runningTaskInfo != null && runningTaskInfo.baseActivity != null) {
+ mController.taskChanged(runningTaskInfo.baseActivity.getPackageName(),
+ runningTaskInfo.stackId);
+ }
+ }
+ }
+
+ @Override
+ protected void createUserSwitcher() {
+ UserSwitcherController userSwitcherController =
+ Dependency.get(UserSwitcherController.class);
+ if (userSwitcherController.useFullscreenUserSwitcher()) {
+ mFullscreenUserSwitcher = new FullscreenUserSwitcher(this,
+ userSwitcherController,
+ mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub));
+ } else {
+ super.createUserSwitcher();
+ }
+ }
+
+ @Override
+ public void userSwitched(int newUserId) {
+ super.userSwitched(newUserId);
+ if (mFullscreenUserSwitcher != null) {
+ mFullscreenUserSwitcher.onUserSwitched(newUserId);
+ }
+ }
+
+ @Override
+ public void updateKeyguardState(boolean goingToFullShade, boolean fromShadeLocked) {
+ super.updateKeyguardState(goingToFullShade, fromShadeLocked);
+ if (mFullscreenUserSwitcher != null) {
+ if (mState == StatusBarState.FULLSCREEN_USER_SWITCHER) {
+ mFullscreenUserSwitcher.show();
+ } else {
+ mFullscreenUserSwitcher.hide();
+ }
+ }
+ }
+
+ @Override
+ public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) {
+ // Do nothing, we don't want to display media art in the lock screen for a car.
+ }
+
+ private int startActivityWithOptions(Intent intent, Bundle options) {
+ int result = ActivityManager.START_CANCELED;
+ try {
+ result = ActivityManager.getService().startActivityAsUser(null /* caller */,
+ mContext.getBasePackageName(),
+ intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ null /* resultTo*/,
+ null /* resultWho*/,
+ 0 /* requestCode*/,
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP,
+ null /* profilerInfo*/,
+ options,
+ UserHandle.CURRENT.getIdentifier());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to start activity", e);
+ }
+
+ return result;
+ }
+
+ public int startActivityOnStack(Intent intent, int stackId) {
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchStackId(stackId);
+ return startActivityWithOptions(intent, options.toBundle());
+ }
+
+ @Override
+ protected boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn) {
+ // Because space is usually constrained in the auto use-case, there should not be a
+ // pinned notification when the shade has been expanded. Ensure this by not pinning any
+ // notification if the shade is already opened.
+ if (mPanelExpanded) {
+ return false;
+ }
+
+ return super.shouldPeek(entry, sbn);
+ }
+
+ @Override
+ public void animateExpandNotificationsPanel() {
+ // Because space is usually constrained in the auto use-case, there should not be a
+ // pinned notification when the shade has been expanded. Ensure this by removing all heads-
+ // up notifications.
+ mHeadsUpManager.removeAllHeadsUpEntries();
+ super.animateExpandNotificationsPanel();
+ }
+
+ /**
+ * Ensures that relevant child views are appropriately recreated when the device's density
+ * changes.
+ */
+ @Override
+ public void onDensityOrFontScaleChanged() {
+ super.onDensityOrFontScaleChanged();
+ mController.onDensityOrFontScaleChanged();
+
+ // Need to update the background on density changed in case the change was due to night
+ // mode.
+ mNotificationPanelBackground = getDefaultWallpaper();
+ mScrimController.setScrimBehindDrawable(mNotificationPanelBackground);
+ }
+
+ /**
+ * Returns the {@link Drawable} that represents the wallpaper that the user has currently set.
+ */
+ private Drawable getDefaultWallpaper() {
+ return mContext.getDrawable(com.android.internal.R.drawable.default_wallpaper);
+ }
+}
diff --git a/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java b/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java
new file mode 100644
index 0000000..677fa81
--- /dev/null
+++ b/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java
@@ -0,0 +1,255 @@
+package com.android.systemui.statusbar.car;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadsetClient;
+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.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.telephony.SignalStrength;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.ImageView;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ScalingDrawableWrapper;
+import com.android.systemui.statusbar.phone.SignalDrawable;
+import com.android.systemui.statusbar.policy.BluetoothController;
+
+import static com.android.systemui.statusbar.phone.StatusBar.DEBUG;
+
+/**
+ * Controller that monitors signal strength for a device that is connected via bluetooth.
+ */
+public class ConnectedDeviceSignalController extends BroadcastReceiver implements
+ BluetoothController.Callback {
+ private final static String TAG = "DeviceSignalCtlr";
+
+ /**
+ * The value that indicates if a network is unavailable. This value is according ot the
+ * Bluetooth HFP 1.5 spec, which indicates this value is one of two: 0 or 1. These stand
+ * for network unavailable and available respectively.
+ */
+ private static final int NETWORK_UNAVAILABLE = 0;
+ private static final int NETWORK_UNAVAILABLE_ICON_ID = R.drawable.stat_sys_signal_null;
+
+ /**
+ * All possible signal strength icons. According to the Bluetooth HFP 1.5 specification,
+ * signal strength is indicated by a value from 1-5, where these values represent the following:
+ *
+ * <p>0%% - 0, 1-25%% - 1, 26-50%% - 2, 51-75%% - 3, 76-99%% - 4, 100%% - 5
+ *
+ * <p>As a result, these are treated as an index into this array for the corresponding icon.
+ * Note that the icon is the same for 0 and 1.
+ */
+ private static final int[] SIGNAL_STRENGTH_ICONS = {
+ 0,
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ };
+
+ private static final int INVALID_SIGNAL = -1;
+
+ private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+ private final Context mContext;
+ private final BluetoothController mController;
+
+ private final View mSignalsView;
+ private final ImageView mNetworkSignalView;
+
+ private final float mIconScaleFactor;
+ private final SignalDrawable mSignalDrawable;
+
+ private BluetoothHeadsetClient mBluetoothHeadsetClient;
+
+ public ConnectedDeviceSignalController(Context context, View signalsView) {
+ mContext = context;
+ mController = Dependency.get(BluetoothController.class);
+
+ mSignalsView = signalsView;
+ mNetworkSignalView = (ImageView)
+ mSignalsView.findViewById(R.id.connected_device_network_signal);
+
+ TypedValue typedValue = new TypedValue();
+ context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+ mIconScaleFactor = typedValue.getFloat();
+ mSignalDrawable = new SignalDrawable(mNetworkSignalView.getContext());
+ mNetworkSignalView.setImageDrawable(
+ new ScalingDrawableWrapper(mSignalDrawable, mIconScaleFactor));
+
+ if (mAdapter == null) {
+ return;
+ }
+
+ mAdapter.getProfileProxy(context.getApplicationContext(), mHfpServiceListener,
+ BluetoothProfile.HEADSET_CLIENT);
+ }
+
+ public void startListening() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
+ mContext.registerReceiver(this, filter);
+
+ mController.addCallback(this);
+ }
+
+ public void stopListening() {
+ mContext.unregisterReceiver(this);
+ mController.removeCallback(this);
+ }
+
+ @Override
+ public void onBluetoothDevicesChanged() {
+ // Nothing to do here because this Controller is not displaying a list of possible
+ // bluetooth devices.
+ }
+
+ @Override
+ public void onBluetoothStateChange(boolean enabled) {
+ if (DEBUG) {
+ Log.d(TAG, "onBluetoothStateChange(). enabled: " + enabled);
+ }
+
+ // Only need to handle the case if bluetooth has been disabled, in which case the
+ // signal indicators are hidden. If bluetooth has been enabled, then this class should
+ // receive updates to the connection state via onReceive().
+ if (!enabled) {
+ mNetworkSignalView.setVisibility(View.GONE);
+ mSignalsView.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ if (DEBUG) {
+ Log.d(TAG, "onReceive(). action: " + action);
+ }
+
+ if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) {
+ if (DEBUG) {
+ Log.d(TAG, "Received ACTION_AG_EVENT");
+ }
+
+ processActionAgEvent(intent);
+ } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+ int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+ if (DEBUG) {
+ int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+ Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
+ + oldState + " -> " + newState);
+ }
+ BluetoothDevice device =
+ (BluetoothDevice) intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
+ updateViewVisibility(device, newState);
+ }
+ }
+
+ /**
+ * Processes an {@link Intent} that had an action of
+ * {@link BluetoothHeadsetClient#ACTION_AG_EVENT}.
+ */
+ private void processActionAgEvent(Intent intent) {
+ int networkStatus = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_NETWORK_STATUS,
+ INVALID_SIGNAL);
+ if (networkStatus != INVALID_SIGNAL) {
+ if (DEBUG) {
+ Log.d(TAG, "EXTRA_NETWORK_STATUS: " + " " + networkStatus);
+ }
+
+ if (networkStatus == NETWORK_UNAVAILABLE) {
+ setNetworkSignalIcon(NETWORK_UNAVAILABLE_ICON_ID);
+ }
+ }
+
+ int signalStrength = intent.getIntExtra(
+ BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH, INVALID_SIGNAL);
+ if (signalStrength != INVALID_SIGNAL) {
+ if (DEBUG) {
+ Log.d(TAG, "EXTRA_NETWORK_SIGNAL_STRENGTH: " + signalStrength);
+ }
+
+ setNetworkSignalIcon(SIGNAL_STRENGTH_ICONS[signalStrength]);
+ }
+
+ int roamingStatus = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_NETWORK_ROAMING,
+ INVALID_SIGNAL);
+ if (roamingStatus != INVALID_SIGNAL) {
+ if (DEBUG) {
+ Log.d(TAG, "EXTRA_NETWORK_ROAMING: " + roamingStatus);
+ }
+ }
+ }
+
+ private void setNetworkSignalIcon(int level) {
+ // Setting the icon on a child view of mSignalView, so toggle this container visible.
+ mSignalsView.setVisibility(View.VISIBLE);
+
+ mSignalDrawable.setLevel(SignalDrawable.getState(level,
+ SignalStrength.NUM_SIGNAL_STRENGTH_BINS, false));
+ mNetworkSignalView.setVisibility(View.VISIBLE);
+ }
+
+ private void updateViewVisibility(BluetoothDevice device, int newState) {
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ if (DEBUG) {
+ Log.d(TAG, "Device connected");
+ }
+
+ if (mBluetoothHeadsetClient == null || device == null) {
+ return;
+ }
+
+ // Check if battery information is available and immediately update.
+ Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device);
+ if (featuresBundle == null) {
+ return;
+ }
+
+ int signalStrength = featuresBundle.getInt(
+ BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH, INVALID_SIGNAL);
+ if (signalStrength != INVALID_SIGNAL) {
+ if (DEBUG) {
+ Log.d(TAG, "EXTRA_NETWORK_SIGNAL_STRENGTH: " + signalStrength);
+ }
+
+ setNetworkSignalIcon(SIGNAL_STRENGTH_ICONS[signalStrength]);
+ }
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+ if (DEBUG) {
+ Log.d(TAG, "Device disconnected");
+ }
+
+ mNetworkSignalView.setVisibility(View.GONE);
+ mSignalsView.setVisibility(View.GONE);
+ }
+ }
+
+ private final ServiceListener mHfpServiceListener = new ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ if (profile == BluetoothProfile.HEADSET_CLIENT) {
+ mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ if (profile == BluetoothProfile.HEADSET_CLIENT) {
+ mBluetoothHeadsetClient = null;
+ }
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
new file mode 100644
index 0000000..172c62a
--- /dev/null
+++ b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2015 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.car;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.res.Resources;
+import android.os.CountDownTimer;
+import android.view.View;
+import android.view.ViewStub;
+import android.widget.ProgressBar;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+
+/**
+ * Manages the fullscreen user switcher.
+ */
+public class FullscreenUserSwitcher {
+ private final View mContainer;
+ private final View mParent;
+ private final UserGridView mUserGridView;
+ private final UserSwitcherController mUserSwitcherController;
+ private final ProgressBar mProgressBar;
+ private final ProgressBar mSwitchingUsers;
+ private final int mLoginTimeoutMs;
+ private final int mAnimUpdateIntervalMs;
+ private final int mShortAnimDuration;
+
+ private boolean mShowing;
+
+ private CountDownTimer mTimer;
+
+ public FullscreenUserSwitcher(StatusBar statusBar,
+ UserSwitcherController userSwitcherController,
+ ViewStub containerStub) {
+ mUserSwitcherController = userSwitcherController;
+ mParent = containerStub.inflate();
+ mContainer = mParent.findViewById(R.id.container);
+ mUserGridView = mContainer.findViewById(R.id.user_grid);
+ mUserGridView.init(statusBar, mUserSwitcherController, true /* showInitially */);
+ mUserGridView.setUserSelectionListener(record -> {
+ if (!record.isCurrent) {
+ toggleSwitchInProgress(true);
+ }
+ });
+
+ PageIndicator pageIndicator = mContainer.findViewById(R.id.user_switcher_page_indicator);
+ pageIndicator.setupWithViewPager(mUserGridView);
+
+ mProgressBar = mContainer.findViewById(R.id.countdown_progress);
+ Resources res = mContainer.getResources();
+ mLoginTimeoutMs = res.getInteger(R.integer.car_user_switcher_timeout_ms);
+ mAnimUpdateIntervalMs = res.getInteger(R.integer.car_user_switcher_anim_update_ms);
+ mShortAnimDuration = res.getInteger(android.R.integer.config_shortAnimTime);
+
+ mContainer.findViewById(R.id.start_driving).setOnClickListener(v -> {
+ cancelTimer();
+ automaticallySelectUser();
+ });
+
+ // Any interaction with the screen should cancel the timer.
+ mContainer.setOnClickListener(v -> {
+ cancelTimer();
+ });
+ mUserGridView.setOnTouchListener((v, e) -> {
+ cancelTimer();
+ return false;
+ });
+
+ mSwitchingUsers = mParent.findViewById(R.id.switching_users);
+ }
+
+ public void onUserSwitched(int newUserId) {
+ mUserGridView.onUserSwitched(newUserId);
+ }
+
+ private void toggleSwitchInProgress(boolean inProgress) {
+ if (inProgress) {
+ crossFade(mSwitchingUsers, mContainer);
+ } else {
+ crossFade(mContainer, mSwitchingUsers);
+ }
+ }
+
+ private void crossFade(View incoming, View outgoing) {
+ incoming.animate()
+ .alpha(1.0f)
+ .setDuration(mShortAnimDuration)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ incoming.setAlpha(0.0f);
+ incoming.setVisibility(View.VISIBLE);
+ }
+ });
+
+ outgoing.animate()
+ .alpha(0.0f)
+ .setDuration(mShortAnimDuration)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ outgoing.setVisibility(View.GONE);
+ }
+ });
+ }
+
+ public void show() {
+ if (mShowing) {
+ return;
+ }
+ mShowing = true;
+ mParent.setVisibility(View.VISIBLE);
+ cancelTimer();
+
+ // This would be the case if we were in the middle of a switch.
+ if (mProgressBar.getVisibility() != View.VISIBLE) {
+ return;
+ }
+
+ mTimer = new CountDownTimer(mLoginTimeoutMs, mAnimUpdateIntervalMs) {
+ @Override
+ public void onTick(long msUntilFinished) {
+ int elapsed = mLoginTimeoutMs - (int) msUntilFinished;
+ mProgressBar.setProgress((int) elapsed, true /* animate */);
+ }
+
+ @Override
+ public void onFinish() {
+ mProgressBar.setProgress(mLoginTimeoutMs, true /* animate */);
+ automaticallySelectUser();
+ }
+ };
+ mTimer.start();
+ }
+
+ public void hide() {
+ mShowing = false;
+ cancelTimer();
+ toggleSwitchInProgress(false);
+ mParent.setVisibility(View.GONE);
+ }
+
+ private void cancelTimer() {
+ if (mTimer != null) {
+ mTimer.cancel();
+ mTimer = null;
+ mProgressBar.setProgress(0, true /* animate */);
+ }
+ }
+
+ private void automaticallySelectUser() {
+ // TODO: Switch according to some policy. This implementation just tries to drop the
+ // keyguard for the current user.
+ mUserGridView.showOfflineAuthUi();
+ }
+}
diff --git a/com/android/systemui/statusbar/car/PageIndicator.java b/com/android/systemui/statusbar/car/PageIndicator.java
new file mode 100644
index 0000000..c830ff8
--- /dev/null
+++ b/com/android/systemui/statusbar/car/PageIndicator.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2017 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.car;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import com.android.systemui.R;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Displays the dots underneath the ViewPager on the lock screen. This is really just a simplified
+ * version of PagerTitleStrip. We don't inherit from there because it's impossible to bypass some
+ * of the overriden logic in that class.
+ */
+public class PageIndicator extends View {
+ private static final String TAG = "PageIndicator";
+ // These can be made a styleable attribute in the future if necessary.
+ private static final int SELECTED_COLOR = 0xFFF5F5F5; // grey 100
+ private static final int UNSELECTED_COLOR = 0xFFBDBDBD; // grey 400
+ private final PageListener mPageListener = new PageListener();
+
+ private ViewPager mPager;
+ private WeakReference<PagerAdapter> mWatchingAdapter;
+
+ private int mPageCount;
+ private int mCurrentPosition;
+ private Paint mPaint;
+ private int mRadius;
+ private int mStep;
+
+ public PageIndicator(Context context) {
+ super(context);
+ init();
+ }
+
+ public PageIndicator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaint.setStyle(Paint.Style.FILL);
+ mRadius = getResources().getDimensionPixelSize(R.dimen.car_page_indicator_dot_diameter) / 2;
+ mStep = mRadius * 3;
+ }
+
+ public void setupWithViewPager(ViewPager pager) {
+ mPager = pager;
+
+ final PagerAdapter adapter = (PagerAdapter) pager.getAdapter();
+ pager.addOnPageChangeListener(mPageListener);
+ pager.addOnAdapterChangeListener(mPageListener);
+ updateAdapter(mWatchingAdapter != null ? mWatchingAdapter.get() : null, adapter);
+ invalidate();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mPager != null) {
+ updateAdapter(mPager.getAdapter(), null);
+ mPager.removeOnPageChangeListener(mPageListener);
+ mPager.removeOnAdapterChangeListener(mPageListener);
+ mPager = null;
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // Don't draw anything unless there's multiple pages to scroll through. No need to clear
+ // any previous dots, since onDraw provides a canvas that's already cleared.
+ if (mPageCount <= 1)
+ return;
+
+ int x = canvas.getWidth() / 2 - (mPageCount / 2) * mStep;
+ int y = canvas.getHeight() / 2;
+
+ for (int i = 0; i < mPageCount; i++) {
+ if (i == mCurrentPosition) {
+ mPaint.setColor(SELECTED_COLOR);
+ } else {
+ mPaint.setColor(UNSELECTED_COLOR);
+ }
+
+ canvas.drawCircle(x, y, mRadius, mPaint);
+ x += mStep;
+ }
+ }
+
+ void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
+ if (oldAdapter != null) {
+ oldAdapter.unregisterDataSetObserver(mPageListener);
+ mWatchingAdapter = null;
+ }
+
+ if (newAdapter != null) {
+ newAdapter.registerDataSetObserver(mPageListener);
+ mWatchingAdapter = new WeakReference<>(newAdapter);
+ }
+
+ updateDots();
+
+ if (mPager != null) {
+ requestLayout();
+ }
+ }
+
+ private <T> T getRef(WeakReference<T> weakRef) {
+ if (weakRef == null) {
+ return null;
+ }
+ return weakRef.get();
+ }
+
+ private void updateDots() {
+ PagerAdapter adapter = getRef(mWatchingAdapter);
+ if (adapter == null) {
+ return;
+ }
+
+ int count = adapter.getCount();
+ if (mPageCount == count) {
+ // Nothing to be done.
+ return;
+ }
+
+ mPageCount = count;
+ mCurrentPosition = 0;
+ invalidate();
+ }
+
+ private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener,
+ ViewPager.OnAdapterChangeListener {
+
+ @Override
+ public void onPageScrolled(int unused1, float unused2, int unused3) { }
+
+ @Override
+ public void onPageSelected(int position) {
+ if (mCurrentPosition == position) {
+ return;
+ }
+
+ if (mPageCount <= position) {
+ Log.e(TAG, "Position out of bounds, position=" + position + " size=" + mPageCount);
+ return;
+ }
+
+ mCurrentPosition = position;
+ invalidate();
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) { }
+
+ @Override
+ public void onAdapterChanged(ViewPager viewPager, PagerAdapter oldAdapter,
+ PagerAdapter newAdapter) {
+ updateAdapter(oldAdapter, newAdapter);
+ }
+
+ @Override
+ public void onChanged() {
+ updateDots();
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/car/UserGridView.java b/com/android/systemui/statusbar/car/UserGridView.java
new file mode 100644
index 0000000..e551801
--- /dev/null
+++ b/com/android/systemui/statusbar/car/UserGridView.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2015 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.car;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+
+/**
+ * Displays a ViewPager with icons for the users in the system to allow switching between users.
+ * One of the uses of this is for the lock screen in auto.
+ */
+public class UserGridView extends ViewPager {
+ private static final int EXPAND_ANIMATION_TIME_MS = 200;
+ private static final int HIDE_ANIMATION_TIME_MS = 133;
+
+ private StatusBar mStatusBar;
+ private UserSwitcherController mUserSwitcherController;
+ private Adapter mAdapter;
+ private UserSelectionListener mUserSelectionListener;
+ private ValueAnimator mHeightAnimator;
+ private int mTargetHeight;
+ private int mHeightChildren;
+ private boolean mShowing;
+
+ public UserGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void init(StatusBar statusBar, UserSwitcherController userSwitcherController,
+ boolean showInitially) {
+ mStatusBar = statusBar;
+ mUserSwitcherController = userSwitcherController;
+ mAdapter = new Adapter(mUserSwitcherController);
+ addOnLayoutChangeListener(mAdapter);
+ setAdapter(mAdapter);
+ mShowing = showInitially;
+ }
+
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ public void show() {
+ mShowing = true;
+ animateHeightChange(getMeasuredHeight(), mHeightChildren);
+ }
+
+ public void hide() {
+ mShowing = false;
+ animateHeightChange(getMeasuredHeight(), 0);
+ }
+
+ public void onUserSwitched(int newUserId) {
+ // Bring up security view after user switch is completed.
+ post(this::showOfflineAuthUi);
+ }
+
+ public void setUserSelectionListener(UserSelectionListener userSelectionListener) {
+ mUserSelectionListener = userSelectionListener;
+ }
+
+ void showOfflineAuthUi() {
+ // TODO: Show keyguard UI in-place.
+ mStatusBar.executeRunnableDismissingKeyguard(null, null, true, true, true);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Wrap content doesn't work in ViewPagers, so simulate the behavior in code.
+ int height = 0;
+ if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
+ height = MeasureSpec.getSize(heightMeasureSpec);
+ } else {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ child.measure(widthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ height = Math.max(child.getMeasuredHeight(), height);
+ }
+
+ mHeightChildren = height;
+
+ // Override the height if it's not showing.
+ if (!mShowing) {
+ height = 0;
+ }
+
+ // Respect the AT_MOST request from parent.
+ if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
+ height = Math.min(MeasureSpec.getSize(heightMeasureSpec), height);
+ }
+ }
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ private void animateHeightChange(int oldHeight, int newHeight) {
+ // If there is no change in height or an animation is already in progress towards the
+ // desired height, then there's no need to make any changes.
+ if (oldHeight == newHeight || newHeight == mTargetHeight) {
+ return;
+ }
+
+ // Animation in progress is not going towards the new target, so cancel it.
+ if (mHeightAnimator != null){
+ mHeightAnimator.cancel();
+ }
+
+ mTargetHeight = newHeight;
+ mHeightAnimator = ValueAnimator.ofInt(oldHeight, mTargetHeight);
+ mHeightAnimator.addUpdateListener(valueAnimator -> {
+ ViewGroup.LayoutParams layoutParams = getLayoutParams();
+ layoutParams.height = (Integer) valueAnimator.getAnimatedValue();
+ requestLayout();
+ });
+ mHeightAnimator.addListener(new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animator) {}
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ // ValueAnimator does not guarantee that the update listener will get an update
+ // to the final value, so here, the final value is set. Though the final calculated
+ // height (mTargetHeight) could be set, WRAP_CONTENT is more appropriate.
+ ViewGroup.LayoutParams layoutParams = getLayoutParams();
+ layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ requestLayout();
+ mHeightAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {}
+ });
+
+ mHeightAnimator.setInterpolator(new FastOutSlowInInterpolator());
+ if (oldHeight < newHeight) {
+ // Expanding
+ mHeightAnimator.setDuration(EXPAND_ANIMATION_TIME_MS);
+ } else {
+ // Hiding
+ mHeightAnimator.setDuration(HIDE_ANIMATION_TIME_MS);
+ }
+ mHeightAnimator.start();
+ }
+
+ /**
+ * This is a ViewPager.PagerAdapter which deletegates the work to a
+ * UserSwitcherController.BaseUserAdapter. Java doesn't support multiple inheritance so we have
+ * to use composition instead to achieve the same goal since both the base classes are abstract
+ * classes and not interfaces.
+ */
+ private final class Adapter extends PagerAdapter implements View.OnLayoutChangeListener {
+ private final int mPodWidth;
+ private final int mPodMarginBetween;
+ private final int mPodImageAvatarWidth;
+ private final int mPodImageAvatarHeight;
+
+ private final WrappedBaseUserAdapter mUserAdapter;
+ private int mContainerWidth;
+
+ public Adapter(UserSwitcherController controller) {
+ super();
+ mUserAdapter = new WrappedBaseUserAdapter(controller, this);
+
+ Resources res = getResources();
+ mPodWidth = res.getDimensionPixelSize(R.dimen.car_fullscreen_user_pod_width);
+ mPodMarginBetween = res.getDimensionPixelSize(
+ R.dimen.car_fullscreen_user_pod_margin_between);
+ mPodImageAvatarWidth = res.getDimensionPixelSize(
+ R.dimen.car_fullscreen_user_pod_image_avatar_width);
+ mPodImageAvatarHeight = res.getDimensionPixelSize(
+ R.dimen.car_fullscreen_user_pod_image_avatar_height);
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ container.removeView((View) object);
+ }
+
+ private int getIconsPerPage() {
+ // We need to know how many pods we need in this page. Each pod has its own width and
+ // a margin between them. We can then divide the measured width of the parent by the
+ // sum of pod width and margin to get the number of pods that will completely fit.
+ // There is one less margin than the number of pods (eg. for 5 pods, there are 4
+ // margins), so need to add the margin to the measured width to account for that.
+ return (mContainerWidth + mPodMarginBetween) /
+ (mPodWidth + mPodMarginBetween);
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ Context context = getContext();
+ LayoutInflater inflater = LayoutInflater.from(context);
+
+ ViewGroup pods = (ViewGroup) inflater.inflate(
+ R.layout.car_fullscreen_user_pod_container, null);
+
+ int iconsPerPage = getIconsPerPage();
+ int limit = Math.min(mUserAdapter.getCount(), (position + 1) * iconsPerPage);
+ for (int i = position * iconsPerPage; i < limit; i++) {
+ View v = makeUserPod(inflater, context, i, pods);
+ pods.addView(v);
+ // This is hacky, but the dividers on the pod container LinearLayout don't seem
+ // to work for whatever reason. Instead, set a right margin on the pod if it's not
+ // the right-most pod and there is more than one pod in the container.
+ if (i < limit - 1 && limit > 1) {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ params.setMargins(0, 0, mPodMarginBetween, 0);
+ v.setLayoutParams(params);
+ }
+ }
+ container.addView(pods);
+ return pods;
+ }
+
+ /**
+ * Returns the default user icon. This icon is a circle with a letter in it. The letter is
+ * the first character in the username.
+ *
+ * @param userName the username of the user for which the icon is to be created
+ */
+ private Bitmap getDefaultUserIcon(CharSequence userName) {
+ CharSequence displayText = userName.subSequence(0, 1);
+ Bitmap out = Bitmap.createBitmap(mPodImageAvatarWidth, mPodImageAvatarHeight,
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(out);
+
+ // Draw the circle background.
+ GradientDrawable shape = new GradientDrawable();
+ shape.setShape(GradientDrawable.RADIAL_GRADIENT);
+ shape.setGradientRadius(1.0f);
+ shape.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_bgcolor));
+ shape.setBounds(0, 0, mPodImageAvatarWidth, mPodImageAvatarHeight);
+ shape.draw(canvas);
+
+ // Draw the letter in the center.
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_fgcolor));
+ paint.setTextAlign(Align.CENTER);
+ paint.setTextSize(getResources().getDimensionPixelSize(
+ R.dimen.car_fullscreen_user_pod_icon_text_size));
+ Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
+ // The Y coordinate is measured by taking half the height of the pod, but that would
+ // draw the character putting the bottom of the font in the middle of the pod. To
+ // correct this, half the difference between the top and bottom distance metrics of the
+ // font gives the offset of the font. Bottom is a positive value, top is negative, so
+ // the different is actually a sum. The "half" operation is then factored out.
+ canvas.drawText(displayText.toString(), mPodImageAvatarWidth / 2,
+ (mPodImageAvatarHeight - (metrics.bottom + metrics.top)) / 2, paint);
+
+ return out;
+ }
+
+ private View makeUserPod(LayoutInflater inflater, Context context,
+ int position, ViewGroup parent) {
+ final UserSwitcherController.UserRecord record = mUserAdapter.getItem(position);
+ View view = inflater.inflate(R.layout.car_fullscreen_user_pod, parent, false);
+
+ TextView nameView = view.findViewById(R.id.user_name);
+ if (record != null) {
+ nameView.setText(mUserAdapter.getName(context, record));
+ view.setActivated(record.isCurrent);
+ } else {
+ nameView.setText(context.getString(R.string.unknown_user_label));
+ }
+
+ ImageView iconView = (ImageView) view.findViewById(R.id.user_avatar);
+ if (record == null || (record.picture == null && !record.isAddUser)) {
+ iconView.setImageBitmap(getDefaultUserIcon(nameView.getText()));
+ } else if (record.isAddUser) {
+ Drawable icon = context.getDrawable(R.drawable.ic_add_circle_qs);
+ icon.setTint(context.getColor(R.color.car_user_switcher_no_user_image_bgcolor));
+ iconView.setImageDrawable(icon);
+ } else {
+ iconView.setImageBitmap(record.picture);
+ }
+
+ iconView.setOnClickListener(v -> {
+ if (record == null) {
+ return;
+ }
+
+ if (mUserSelectionListener != null) {
+ mUserSelectionListener.onUserSelected(record);
+ }
+
+ if (record.isCurrent) {
+ showOfflineAuthUi();
+ } else {
+ mUserSwitcherController.switchTo(record);
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public int getCount() {
+ int iconsPerPage = getIconsPerPage();
+ if (iconsPerPage == 0) {
+ return 0;
+ }
+ return (int) Math.ceil((double) mUserAdapter.getCount() / getIconsPerPage());
+ }
+
+ public void refresh() {
+ mUserAdapter.refresh();
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return view == object;
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mContainerWidth = Math.max(left - right, right - left);
+ notifyDataSetChanged();
+ }
+ }
+
+ private final class WrappedBaseUserAdapter extends UserSwitcherController.BaseUserAdapter {
+ private Adapter mContainer;
+
+ public WrappedBaseUserAdapter(UserSwitcherController controller, Adapter container) {
+ super(controller);
+ mContainer = container;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ throw new UnsupportedOperationException("unused");
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ super.notifyDataSetChanged();
+ mContainer.notifyDataSetChanged();
+ }
+ }
+
+ interface UserSelectionListener {
+ void onUserSelected(UserSwitcherController.UserRecord record);
+ };
+}
diff --git a/com/android/systemui/statusbar/notification/AboveShelfChangedListener.java b/com/android/systemui/statusbar/notification/AboveShelfChangedListener.java
new file mode 100644
index 0000000..07baedc
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/AboveShelfChangedListener.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+/**
+ * A listener for when the above shelf state of notification changes
+ */
+public interface AboveShelfChangedListener {
+
+ /**
+ * Notifies a listener that the above shelf state changed
+ */
+ void onAboveShelfStateChanged(boolean aboveShelf);
+}
diff --git a/com/android/systemui/statusbar/notification/AboveShelfObserver.java b/com/android/systemui/statusbar/notification/AboveShelfObserver.java
new file mode 100644
index 0000000..f10d2d7
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/AboveShelfObserver.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * An observer that listens to the above shelf state and can notify listeners
+ */
+public class AboveShelfObserver implements AboveShelfChangedListener {
+
+ private final ViewGroup mHostLayout;
+ private boolean mHasViewsAboveShelf = false;
+ private HasViewAboveShelfChangedListener mListener;
+
+ public AboveShelfObserver(ViewGroup hostLayout) {
+ mHostLayout = hostLayout;
+ }
+
+ public void setListener(HasViewAboveShelfChangedListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onAboveShelfStateChanged(boolean aboveShelf) {
+ boolean hasViewsAboveShelf = aboveShelf;
+ if (!hasViewsAboveShelf && mHostLayout != null) {
+ int n = mHostLayout.getChildCount();
+ for (int i = 0; i < n; i++) {
+ View child = mHostLayout.getChildAt(i);
+ if (child instanceof ExpandableNotificationRow) {
+ if (((ExpandableNotificationRow) child).isAboveShelf()) {
+ hasViewsAboveShelf = true;
+ break;
+ }
+ }
+ }
+ }
+ if (mHasViewsAboveShelf != hasViewsAboveShelf) {
+ mHasViewsAboveShelf = hasViewsAboveShelf;
+ if (mListener != null) {
+ mListener.onHasViewsAboveShelfChanged(hasViewsAboveShelf);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ boolean hasViewsAboveShelf() {
+ return mHasViewsAboveShelf;
+ }
+
+ public interface HasViewAboveShelfChangedListener {
+ void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf);
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/ActionListTransformState.java b/com/android/systemui/statusbar/notification/ActionListTransformState.java
new file mode 100644
index 0000000..8c72544
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/ActionListTransformState.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.Pools;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A transform state of the action list
+*/
+public class ActionListTransformState extends TransformState {
+
+ private static Pools.SimplePool<ActionListTransformState> sInstancePool
+ = new Pools.SimplePool<>(40);
+
+ @Override
+ protected boolean sameAs(TransformState otherState) {
+ return otherState instanceof ActionListTransformState;
+ }
+
+ public static ActionListTransformState obtain() {
+ ActionListTransformState instance = sInstancePool.acquire();
+ if (instance != null) {
+ return instance;
+ }
+ return new ActionListTransformState();
+ }
+
+ @Override
+ public void transformViewFullyFrom(TransformState otherState, float transformationAmount) {
+ // Don't do Y transform - let the wrapper handle this based on the content height
+ }
+
+ @Override
+ public void transformViewFullyTo(TransformState otherState, float transformationAmount) {
+ // Don't do Y transform - let the wrapper handle this based on the content height
+ }
+
+ @Override
+ protected void resetTransformedView() {
+ // We need to keep the Y transformation, because this is used to keep the action list
+ // aligned at the bottom, unrelated to transforms.
+ float y = getTransformedView().getTranslationY();
+ super.resetTransformedView();
+ getTransformedView().setTranslationY(y);
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ sInstancePool.release(this);
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java b/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
new file mode 100644
index 0000000..de4c312
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+
+import static com.android.systemui.statusbar.TransformableView.TRANSFORMING_VIEW_TITLE;
+import static com.android.systemui.statusbar.notification.TransformState.TRANSFORM_Y;
+
+/**
+ * A custom transformation that modifies the interpolator
+ */
+public abstract class CustomInterpolatorTransformation
+ extends ViewTransformationHelper.CustomTransformation {
+
+ private final int mViewType;
+
+ public CustomInterpolatorTransformation(int viewType) {
+ mViewType = viewType;
+ }
+
+ @Override
+ public boolean transformTo(TransformState ownState, TransformableView notification,
+ float transformationAmount) {
+ if (!hasCustomTransformation()) {
+ return false;
+ }
+ TransformState otherState = notification.getCurrentState(mViewType);
+ if (otherState == null) {
+ return false;
+ }
+ View view = ownState.getTransformedView();
+ CrossFadeHelper.fadeOut(view, transformationAmount);
+ ownState.transformViewFullyTo(otherState, this, transformationAmount);
+ otherState.recycle();
+ return true;
+ }
+
+ protected boolean hasCustomTransformation() {
+ return true;
+ }
+
+ @Override
+ public boolean transformFrom(TransformState ownState,
+ TransformableView notification, float transformationAmount) {
+ if (!hasCustomTransformation()) {
+ return false;
+ }
+ TransformState otherState = notification.getCurrentState(mViewType);
+ if (otherState == null) {
+ return false;
+ }
+ View view = ownState.getTransformedView();
+ CrossFadeHelper.fadeIn(view, transformationAmount);
+ ownState.transformViewFullyFrom(otherState, this, transformationAmount);
+ otherState.recycle();
+ return true;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/FakeShadowView.java b/com/android/systemui/statusbar/notification/FakeShadowView.java
new file mode 100644
index 0000000..0c1891e
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/FakeShadowView.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Outline;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.widget.LinearLayout;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
+
+/**
+ * A view used to cast a shadow of a certain size on another view
+ */
+public class FakeShadowView extends AlphaOptimizedFrameLayout {
+ public static final float SHADOW_SIBLING_TRESHOLD = 0.1f;
+ private final int mShadowMinHeight;
+
+ private View mFakeShadow;
+ private float mOutlineAlpha;
+
+ public FakeShadowView(Context context) {
+ this(context, null);
+ }
+
+ public FakeShadowView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FakeShadowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public FakeShadowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mFakeShadow = new View(context);
+ mFakeShadow.setVisibility(INVISIBLE);
+ mFakeShadow.setLayoutParams(new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ (int) (48 * getResources().getDisplayMetrics().density)));
+ mFakeShadow.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRect(0, 0, getWidth(), mFakeShadow.getHeight());
+ outline.setAlpha(mOutlineAlpha);
+ }
+ });
+ addView(mFakeShadow);
+ mShadowMinHeight = Math.max(1, context.getResources()
+ .getDimensionPixelSize(R.dimen.notification_divider_height));
+ }
+
+ public void setFakeShadowTranslationZ(float fakeShadowTranslationZ, float outlineAlpha,
+ int shadowYEnd, int outlineTranslation) {
+ if (fakeShadowTranslationZ == 0.0f) {
+ mFakeShadow.setVisibility(INVISIBLE);
+ } else {
+ mFakeShadow.setVisibility(VISIBLE);
+ fakeShadowTranslationZ = Math.max(mShadowMinHeight, fakeShadowTranslationZ);
+ mFakeShadow.setTranslationZ(fakeShadowTranslationZ);
+ mFakeShadow.setTranslationX(outlineTranslation);
+ mFakeShadow.setTranslationY(shadowYEnd - mFakeShadow.getHeight());
+ if (outlineAlpha != mOutlineAlpha) {
+ mOutlineAlpha = outlineAlpha;
+ mFakeShadow.invalidateOutline();
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/HybridGroupManager.java b/com/android/systemui/statusbar/notification/HybridGroupManager.java
new file mode 100644
index 0000000..3ed8cce
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/HybridGroupManager.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2015 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.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+
+/**
+ * A class managing hybrid groups that include {@link HybridNotificationView} and the notification
+ * group overflow.
+ */
+public class HybridGroupManager {
+
+ private final Context mContext;
+ private final NotificationDozeHelper mDozer;
+ private final ViewGroup mParent;
+
+ private final float mOverflowNumberSizeDark;
+ private final int mOverflowNumberPaddingDark;
+ private final float mOverflowNumberSize;
+ private final int mOverflowNumberPadding;
+
+ private int mOverflowNumberColor;
+ private int mOverflowNumberColorDark;
+ private float mDarkAmount = 0f;
+
+ public HybridGroupManager(Context ctx, ViewGroup parent) {
+ mContext = ctx;
+ mParent = parent;
+ mDozer = new NotificationDozeHelper();
+
+ Resources res = mContext.getResources();
+ mOverflowNumberSize = res.getDimensionPixelSize(
+ R.dimen.group_overflow_number_size);
+ mOverflowNumberSizeDark = res.getDimensionPixelSize(
+ R.dimen.group_overflow_number_size_dark);
+ mOverflowNumberPadding = res.getDimensionPixelSize(
+ R.dimen.group_overflow_number_padding);
+ mOverflowNumberPaddingDark = mOverflowNumberPadding + res.getDimensionPixelSize(
+ R.dimen.group_overflow_number_extra_padding_dark);
+ }
+
+ private HybridNotificationView inflateHybridViewWithStyle(int style) {
+ LayoutInflater inflater = new ContextThemeWrapper(mContext, style)
+ .getSystemService(LayoutInflater.class);
+ HybridNotificationView hybrid = (HybridNotificationView) inflater.inflate(
+ R.layout.hybrid_notification, mParent, false);
+ mParent.addView(hybrid);
+ return hybrid;
+ }
+
+ private TextView inflateOverflowNumber() {
+ LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
+ TextView numberView = (TextView) inflater.inflate(
+ R.layout.hybrid_overflow_number, mParent, false);
+ mParent.addView(numberView);
+ updateOverFlowNumberColor(numberView);
+ return numberView;
+ }
+
+ private void updateOverFlowNumberColor(TextView numberView) {
+ numberView.setTextColor(NotificationUtils.interpolateColors(
+ mOverflowNumberColor, mOverflowNumberColorDark, mDarkAmount));
+ }
+
+ public void setOverflowNumberColor(TextView numberView, int colorRegular, int colorDark) {
+ mOverflowNumberColor = colorRegular;
+ mOverflowNumberColorDark = colorDark;
+ if (numberView != null) {
+ updateOverFlowNumberColor(numberView);
+ }
+ }
+
+ public HybridNotificationView bindFromNotification(HybridNotificationView reusableView,
+ Notification notification) {
+ return bindFromNotificationWithStyle(reusableView, notification,
+ R.style.HybridNotification);
+ }
+
+ public HybridNotificationView bindAmbientFromNotification(HybridNotificationView reusableView,
+ Notification notification) {
+ return bindFromNotificationWithStyle(reusableView, notification,
+ R.style.HybridNotification_Ambient);
+ }
+
+ private HybridNotificationView bindFromNotificationWithStyle(
+ HybridNotificationView reusableView, Notification notification, int style) {
+ if (reusableView == null) {
+ reusableView = inflateHybridViewWithStyle(style);
+ }
+ CharSequence titleText = resolveTitle(notification);
+ CharSequence contentText = resolveText(notification);
+ reusableView.bind(titleText, contentText);
+ return reusableView;
+ }
+
+ private CharSequence resolveText(Notification notification) {
+ CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
+ if (contentText == null) {
+ contentText = notification.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
+ }
+ return contentText;
+ }
+
+ private CharSequence resolveTitle(Notification notification) {
+ CharSequence titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
+ if (titleText == null) {
+ titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE_BIG);
+ }
+ return titleText;
+ }
+
+ public TextView bindOverflowNumber(TextView reusableView, int number) {
+ if (reusableView == null) {
+ reusableView = inflateOverflowNumber();
+ }
+ String text = mContext.getResources().getString(
+ R.string.notification_group_overflow_indicator, number);
+ if (!text.equals(reusableView.getText())) {
+ reusableView.setText(text);
+ }
+ String contentDescription = String.format(mContext.getResources().getQuantityString(
+ R.plurals.notification_group_overflow_description, number), number);
+
+ reusableView.setContentDescription(contentDescription);
+ return reusableView;
+ }
+
+ public void setOverflowNumberDark(TextView view, boolean dark, boolean fade, long delay) {
+ mDozer.setIntensityDark((f)->{
+ mDarkAmount = f;
+ updateOverFlowNumberColor(view);
+ }, dark, fade, delay);
+ view.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ dark ? mOverflowNumberSizeDark : mOverflowNumberSize);
+ int paddingEnd = dark ? mOverflowNumberPaddingDark : mOverflowNumberPadding;
+ view.setPaddingRelative(view.getPaddingStart(), view.getPaddingTop(), paddingEnd,
+ view.getPaddingBottom());
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/HybridNotificationView.java b/com/android/systemui/statusbar/notification/HybridNotificationView.java
new file mode 100644
index 0000000..0a1795f
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/HybridNotificationView.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2015 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.notification;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.keyguard.AlphaOptimizedLinearLayout;
+import com.android.systemui.R;
+import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+/**
+ * A hybrid view which may contain information about one ore more notifications.
+ */
+public class HybridNotificationView extends AlphaOptimizedLinearLayout
+ implements TransformableView {
+
+ private ViewTransformationHelper mTransformationHelper;
+
+ protected TextView mTitleView;
+ protected TextView mTextView;
+ private ViewInvertHelper mInvertHelper;
+
+ public HybridNotificationView(Context context) {
+ this(context, null);
+ }
+
+ public HybridNotificationView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public HybridNotificationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public HybridNotificationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public TextView getTitleView() {
+ return mTitleView;
+ }
+
+ public TextView getTextView() {
+ return mTextView;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mTitleView = (TextView) findViewById(R.id.notification_title);
+ mTextView = (TextView) findViewById(R.id.notification_text);
+ mInvertHelper = new ViewInvertHelper(this, NotificationPanelView.DOZE_ANIMATION_DURATION);
+ mTransformationHelper = new ViewTransformationHelper();
+ mTransformationHelper.setCustomTransformation(
+ new ViewTransformationHelper.CustomTransformation() {
+ @Override
+ public boolean transformTo(TransformState ownState, TransformableView notification,
+ float transformationAmount) {
+ // We want to transform to the same y location as the title
+ TransformState otherState = notification.getCurrentState(
+ TRANSFORMING_VIEW_TITLE);
+ CrossFadeHelper.fadeOut(mTextView, transformationAmount);
+ if (otherState != null) {
+ ownState.transformViewVerticalTo(otherState, transformationAmount);
+ otherState.recycle();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean transformFrom(TransformState ownState,
+ TransformableView notification, float transformationAmount) {
+ // We want to transform from the same y location as the title
+ TransformState otherState = notification.getCurrentState(
+ TRANSFORMING_VIEW_TITLE);
+ CrossFadeHelper.fadeIn(mTextView, transformationAmount);
+ if (otherState != null) {
+ ownState.transformViewVerticalFrom(otherState, transformationAmount);
+ otherState.recycle();
+ }
+ return true;
+ }
+ }, TRANSFORMING_VIEW_TEXT);
+ mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, mTitleView);
+ mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TEXT, mTextView);
+ }
+
+ public void bind(CharSequence title) {
+ bind(title, null);
+ }
+
+ public void bind(CharSequence title, CharSequence text) {
+ mTitleView.setText(title);
+ mTitleView.setVisibility(TextUtils.isEmpty(title) ? GONE : VISIBLE);
+ if (TextUtils.isEmpty(text)) {
+ mTextView.setVisibility(GONE);
+ mTextView.setText(null);
+ } else {
+ mTextView.setVisibility(VISIBLE);
+ mTextView.setText(text.toString());
+ }
+ requestLayout();
+ }
+
+ public void setDark(boolean dark, boolean fade, long delay) {
+ mInvertHelper.setInverted(dark, fade, delay);
+ }
+
+ @Override
+ public TransformState getCurrentState(int fadingView) {
+ return mTransformationHelper.getCurrentState(fadingView);
+ }
+
+ @Override
+ public void transformTo(TransformableView notification, Runnable endRunnable) {
+ mTransformationHelper.transformTo(notification, endRunnable);
+ }
+
+ @Override
+ public void transformTo(TransformableView notification, float transformationAmount) {
+ mTransformationHelper.transformTo(notification, transformationAmount);
+ }
+
+ @Override
+ public void transformFrom(TransformableView notification) {
+ mTransformationHelper.transformFrom(notification);
+ }
+
+ @Override
+ public void transformFrom(TransformableView notification, float transformationAmount) {
+ mTransformationHelper.transformFrom(notification, transformationAmount);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ mTransformationHelper.setVisible(visible);
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/ImageGradientColorizer.java b/com/android/systemui/statusbar/notification/ImageGradientColorizer.java
new file mode 100644
index 0000000..454edbb
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/ImageGradientColorizer.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+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.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Shader;
+import android.graphics.Xfermode;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A utility class to colorize bitmaps with a color gradient and a special blending mode
+ */
+public class ImageGradientColorizer {
+ public Bitmap colorize(Drawable drawable, int backgroundColor, boolean isRtl) {
+ int width = drawable.getIntrinsicWidth();
+ int height = drawable.getIntrinsicHeight();
+ int size = Math.min(width, height);
+ int widthInset = (width - size) / 2;
+ int heightInset = (height - size) / 2;
+ drawable = drawable.mutate();
+ drawable.setBounds(- widthInset, - heightInset, width - widthInset, height - heightInset);
+ Bitmap newBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(newBitmap);
+
+ // Values to calculate the luminance of a color
+ float lr = 0.2126f;
+ float lg = 0.7152f;
+ float lb = 0.0722f;
+
+ // Extract the red, green, blue components of the color extraction color in
+ // float and int form
+ int tri = Color.red(backgroundColor);
+ int tgi = Color.green(backgroundColor);
+ int tbi = Color.blue(backgroundColor);
+
+ float tr = tri / 255f;
+ float tg = tgi / 255f;
+ float tb = tbi / 255f;
+
+ // Calculate the luminance of the color extraction color
+ float cLum = (tr * lr + tg * lg + tb * lb) * 255;
+
+ ColorMatrix m = new ColorMatrix(new float[] {
+ lr, lg, lb, 0, tri - cLum,
+ lr, lg, lb, 0, tgi - cLum,
+ lr, lg, lb, 0, tbi - cLum,
+ 0, 0, 0, 1, 0,
+ });
+
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ LinearGradient linearGradient = new LinearGradient(0, 0, size, 0,
+ new int[] {0, Color.argb(0.5f, 1, 1, 1), Color.BLACK},
+ new float[] {0.0f, 0.4f, 1.0f}, Shader.TileMode.CLAMP);
+ paint.setShader(linearGradient);
+ Bitmap fadeIn = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas fadeInCanvas = new Canvas(fadeIn);
+ drawable.clearColorFilter();
+ drawable.draw(fadeInCanvas);
+
+ if (isRtl) {
+ // Let's flip the gradient
+ fadeInCanvas.translate(size, 0);
+ fadeInCanvas.scale(-1, 1);
+ }
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
+ fadeInCanvas.drawPaint(paint);
+
+ Paint coloredPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ coloredPaint.setColorFilter(new ColorMatrixColorFilter(m));
+ coloredPaint.setAlpha((int) (0.5f * 255));
+ canvas.drawBitmap(fadeIn, 0, 0, coloredPaint);
+
+ linearGradient = new LinearGradient(0, 0, size, 0,
+ new int[] {0, Color.argb(0.5f, 1, 1, 1), Color.BLACK},
+ new float[] {0.0f, 0.6f, 1.0f}, Shader.TileMode.CLAMP);
+ paint.setShader(linearGradient);
+ fadeInCanvas.drawPaint(paint);
+ canvas.drawBitmap(fadeIn, 0, 0, null);
+
+ return newBitmap;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/ImageTransformState.java b/com/android/systemui/statusbar/notification/ImageTransformState.java
new file mode 100644
index 0000000..92f26d6
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/ImageTransformState.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.graphics.drawable.Icon;
+import android.util.Pools;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * A transform state of a image view.
+*/
+public class ImageTransformState extends TransformState {
+ public static final long ANIMATION_DURATION_LENGTH = 210;
+
+ public static final int ICON_TAG = R.id.image_icon_tag;
+ private static Pools.SimplePool<ImageTransformState> sInstancePool
+ = new Pools.SimplePool<>(40);
+ private Icon mIcon;
+
+ @Override
+ public void initFrom(View view) {
+ super.initFrom(view);
+ if (view instanceof ImageView) {
+ mIcon = (Icon) view.getTag(ICON_TAG);
+ }
+ }
+
+ @Override
+ protected boolean sameAs(TransformState otherState) {
+ if (super.sameAs(otherState)) {
+ return true;
+ }
+ if (otherState instanceof ImageTransformState) {
+ return mIcon != null && mIcon.sameAs(((ImageTransformState) otherState).getIcon());
+ }
+ return false;
+ }
+
+ @Override
+ public void appear(float transformationAmount, TransformableView otherView) {
+ if (otherView instanceof HybridNotificationView) {
+ if (transformationAmount == 0.0f) {
+ mTransformedView.setPivotY(0);
+ mTransformedView.setPivotX(mTransformedView.getWidth() / 2);
+ prepareFadeIn();
+ }
+ transformationAmount = mapToDuration(transformationAmount);
+ CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, false /* remap */);
+ transformationAmount = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(
+ transformationAmount);
+ mTransformedView.setScaleX(transformationAmount);
+ mTransformedView.setScaleY(transformationAmount);
+ } else {
+ super.appear(transformationAmount, otherView);
+ }
+ }
+
+ @Override
+ public void disappear(float transformationAmount, TransformableView otherView) {
+ if (otherView instanceof HybridNotificationView) {
+ if (transformationAmount == 0.0f) {
+ mTransformedView.setPivotY(0);
+ mTransformedView.setPivotX(mTransformedView.getWidth() / 2);
+ }
+ transformationAmount = mapToDuration(1.0f - transformationAmount);
+ CrossFadeHelper.fadeOut(mTransformedView, 1.0f - transformationAmount,
+ false /* remap */);
+ transformationAmount = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(
+ transformationAmount);
+ mTransformedView.setScaleX(transformationAmount);
+ mTransformedView.setScaleY(transformationAmount);
+ } else {
+ super.disappear(transformationAmount, otherView);
+ }
+ }
+
+ private static float mapToDuration(float scaleAmount) {
+ // Assuming a linear interpolator, we can easily map it to our new duration
+ scaleAmount = (scaleAmount * StackStateAnimator.ANIMATION_DURATION_STANDARD
+ - (StackStateAnimator.ANIMATION_DURATION_STANDARD - ANIMATION_DURATION_LENGTH))
+ / ANIMATION_DURATION_LENGTH;
+ return Math.max(Math.min(scaleAmount, 1.0f), 0.0f);
+ }
+
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ public static ImageTransformState obtain() {
+ ImageTransformState instance = sInstancePool.acquire();
+ if (instance != null) {
+ return instance;
+ }
+ return new ImageTransformState();
+ }
+
+ @Override
+ protected boolean transformScale(TransformState otherState) {
+ return true;
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ sInstancePool.release(this);
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ mIcon = null;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/InflationException.java b/com/android/systemui/statusbar/notification/InflationException.java
new file mode 100644
index 0000000..b484138
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/InflationException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+/**
+ * An exception that something went wrong during the inflation
+ */
+public class InflationException extends Exception {
+ public InflationException(String error) {
+ super(error);
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java b/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java
new file mode 100644
index 0000000..80854ec
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.graphics.Palette;
+import android.util.LayoutDirection;
+
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * A class the processes media notifications and extracts the right text and background colors.
+ */
+public class MediaNotificationProcessor {
+
+ /**
+ * The fraction below which we select the vibrant instead of the light/dark vibrant color
+ */
+ private static final float POPULATION_FRACTION_FOR_MORE_VIBRANT = 1.0f;
+
+ /**
+ * Minimum saturation that a muted color must have if there exists if deciding between two
+ * colors
+ */
+ private static final float MIN_SATURATION_WHEN_DECIDING = 0.19f;
+
+ /**
+ * Minimum fraction that any color must have to be picked up as a text color
+ */
+ private static final double MINIMUM_IMAGE_FRACTION = 0.002;
+
+ /**
+ * The population fraction to select the dominant color as the text color over a the colored
+ * ones.
+ */
+ private static final float POPULATION_FRACTION_FOR_DOMINANT = 0.01f;
+
+ /**
+ * The population fraction to select a white or black color as the background over a color.
+ */
+ private static final float POPULATION_FRACTION_FOR_WHITE_OR_BLACK = 2.5f;
+ private static final float BLACK_MAX_LIGHTNESS = 0.08f;
+ private static final float WHITE_MIN_LIGHTNESS = 0.90f;
+ private static final int RESIZE_BITMAP_AREA = 150 * 150;
+ private final ImageGradientColorizer mColorizer;
+ private final Context mContext;
+ private float[] mFilteredBackgroundHsl = null;
+ private Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl);
+
+ /**
+ * The context of the notification. This is the app context of the package posting the
+ * notification.
+ */
+ private final Context mPackageContext;
+ private boolean mIsLowPriority;
+
+ public MediaNotificationProcessor(Context context, Context packageContext) {
+ this(context, packageContext, new ImageGradientColorizer());
+ }
+
+ @VisibleForTesting
+ MediaNotificationProcessor(Context context, Context packageContext,
+ ImageGradientColorizer colorizer) {
+ mContext = context;
+ mPackageContext = packageContext;
+ mColorizer = colorizer;
+ }
+
+ /**
+ * Processes a builder of a media notification and calculates the appropriate colors that should
+ * be used.
+ *
+ * @param notification the notification that is being processed
+ * @param builder the recovered builder for the notification. this will be modified
+ */
+ public void processNotification(Notification notification, Notification.Builder builder) {
+ Icon largeIcon = notification.getLargeIcon();
+ Bitmap bitmap = null;
+ Drawable drawable = null;
+ if (largeIcon != null) {
+ // We're transforming the builder, let's make sure all baked in RemoteViews are
+ // rebuilt!
+ builder.setRebuildStyledRemoteViews(true);
+ drawable = largeIcon.loadDrawable(mPackageContext);
+ int backgroundColor = 0;
+ if (notification.isColorizedMedia()) {
+ int width = drawable.getIntrinsicWidth();
+ int height = drawable.getIntrinsicHeight();
+ int area = width * height;
+ if (area > RESIZE_BITMAP_AREA) {
+ double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area);
+ width = (int) (factor * width);
+ height = (int) (factor * height);
+ }
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, width, height);
+ drawable.draw(canvas);
+
+ // for the background we only take the left side of the image to ensure
+ // a smooth transition
+ Palette.Builder paletteBuilder = Palette.from(bitmap)
+ .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight())
+ .clearFilters() // we want all colors, red / white / black ones too!
+ .resizeBitmapArea(RESIZE_BITMAP_AREA);
+ Palette palette = paletteBuilder.generate();
+ backgroundColor = findBackgroundColorAndFilter(palette);
+ // we want most of the full region again, slightly shifted to the right
+ float textColorStartWidthFraction = 0.4f;
+ paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0,
+ bitmap.getWidth(),
+ bitmap.getHeight());
+ if (mFilteredBackgroundHsl != null) {
+ paletteBuilder.addFilter((rgb, hsl) -> {
+ // at least 10 degrees hue difference
+ float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]);
+ return diff > 10 && diff < 350;
+ });
+ }
+ paletteBuilder.addFilter(mBlackWhiteFilter);
+ palette = paletteBuilder.generate();
+ int foregroundColor = selectForegroundColor(backgroundColor, palette);
+ builder.setColorPalette(backgroundColor, foregroundColor);
+ } else {
+ int id = mIsLowPriority
+ ? R.color.notification_material_background_low_priority_color
+ : R.color.notification_material_background_color;
+ backgroundColor = mContext.getColor(id);
+ }
+ Bitmap colorized = mColorizer.colorize(drawable, backgroundColor,
+ mContext.getResources().getConfiguration().getLayoutDirection() ==
+ LayoutDirection.RTL);
+ builder.setLargeIcon(Icon.createWithBitmap(colorized));
+ }
+ }
+
+ private int selectForegroundColor(int backgroundColor, Palette palette) {
+ if (NotificationColorUtil.isColorLight(backgroundColor)) {
+ return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(),
+ palette.getVibrantSwatch(),
+ palette.getDarkMutedSwatch(),
+ palette.getMutedSwatch(),
+ palette.getDominantSwatch(),
+ Color.BLACK);
+ } else {
+ return selectForegroundColorForSwatches(palette.getLightVibrantSwatch(),
+ palette.getVibrantSwatch(),
+ palette.getLightMutedSwatch(),
+ palette.getMutedSwatch(),
+ palette.getDominantSwatch(),
+ Color.WHITE);
+ }
+ }
+
+ private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant,
+ Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch,
+ Palette.Swatch dominantSwatch, int fallbackColor) {
+ Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant);
+ if (coloredCandidate == null) {
+ coloredCandidate = selectMutedCandidate(mutedSwatch, moreMutedSwatch);
+ }
+ if (coloredCandidate != null) {
+ if (dominantSwatch == coloredCandidate) {
+ return coloredCandidate.getRgb();
+ } else if ((float) coloredCandidate.getPopulation() / dominantSwatch.getPopulation()
+ < POPULATION_FRACTION_FOR_DOMINANT
+ && dominantSwatch.getHsl()[1] > MIN_SATURATION_WHEN_DECIDING) {
+ return dominantSwatch.getRgb();
+ } else {
+ return coloredCandidate.getRgb();
+ }
+ } else if (hasEnoughPopulation(dominantSwatch)) {
+ return dominantSwatch.getRgb();
+ } else {
+ return fallbackColor;
+ }
+ }
+
+ private Palette.Swatch selectMutedCandidate(Palette.Swatch first,
+ Palette.Swatch second) {
+ boolean firstValid = hasEnoughPopulation(first);
+ boolean secondValid = hasEnoughPopulation(second);
+ if (firstValid && secondValid) {
+ float firstSaturation = first.getHsl()[1];
+ float secondSaturation = second.getHsl()[1];
+ float populationFraction = first.getPopulation() / (float) second.getPopulation();
+ if (firstSaturation * populationFraction > secondSaturation) {
+ return first;
+ } else {
+ return second;
+ }
+ } else if (firstValid) {
+ return first;
+ } else if (secondValid) {
+ return second;
+ }
+ return null;
+ }
+
+ private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) {
+ boolean firstValid = hasEnoughPopulation(first);
+ boolean secondValid = hasEnoughPopulation(second);
+ if (firstValid && secondValid) {
+ int firstPopulation = first.getPopulation();
+ int secondPopulation = second.getPopulation();
+ if (firstPopulation / (float) secondPopulation
+ < POPULATION_FRACTION_FOR_MORE_VIBRANT) {
+ return second;
+ } else {
+ return first;
+ }
+ } else if (firstValid) {
+ return first;
+ } else if (secondValid) {
+ return second;
+ }
+ return null;
+ }
+
+ private boolean hasEnoughPopulation(Palette.Swatch swatch) {
+ // We want a fraction that is at least 1% of the image
+ return swatch != null
+ && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION);
+ }
+
+ private int findBackgroundColorAndFilter(Palette palette) {
+ // by default we use the dominant palette
+ Palette.Swatch dominantSwatch = palette.getDominantSwatch();
+ if (dominantSwatch == null) {
+ // We're not filtering on white or black
+ mFilteredBackgroundHsl = null;
+ return Color.WHITE;
+ }
+
+ if (!isWhiteOrBlack(dominantSwatch.getHsl())) {
+ mFilteredBackgroundHsl = dominantSwatch.getHsl();
+ return dominantSwatch.getRgb();
+ }
+ // Oh well, we selected black or white. Lets look at the second color!
+ List<Palette.Swatch> swatches = palette.getSwatches();
+ float highestNonWhitePopulation = -1;
+ Palette.Swatch second = null;
+ for (Palette.Swatch swatch: swatches) {
+ if (swatch != dominantSwatch
+ && swatch.getPopulation() > highestNonWhitePopulation
+ && !isWhiteOrBlack(swatch.getHsl())) {
+ second = swatch;
+ highestNonWhitePopulation = swatch.getPopulation();
+ }
+ }
+ if (second == null) {
+ // We're not filtering on white or black
+ mFilteredBackgroundHsl = null;
+ return dominantSwatch.getRgb();
+ }
+ if (dominantSwatch.getPopulation() / highestNonWhitePopulation
+ > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) {
+ // The dominant swatch is very dominant, lets take it!
+ // We're not filtering on white or black
+ mFilteredBackgroundHsl = null;
+ return dominantSwatch.getRgb();
+ } else {
+ mFilteredBackgroundHsl = second.getHsl();
+ return second.getRgb();
+ }
+ }
+
+ private boolean isWhiteOrBlack(float[] hsl) {
+ return isBlack(hsl) || isWhite(hsl);
+ }
+
+
+ /**
+ * @return true if the color represents a color which is close to black.
+ */
+ private boolean isBlack(float[] hslColor) {
+ return hslColor[2] <= BLACK_MAX_LIGHTNESS;
+ }
+
+ /**
+ * @return true if the color represents a color which is close to white.
+ */
+ private boolean isWhite(float[] hslColor) {
+ return hslColor[2] >= WHITE_MIN_LIGHTNESS;
+ }
+
+ public void setIsLowPriority(boolean isLowPriority) {
+ mIsLowPriority = isLowPriority;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationBigPictureTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationBigPictureTemplateViewWrapper.java
new file mode 100644
index 0000000..cf12e94
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationBigPictureTemplateViewWrapper.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * Wraps a notification containing a big picture template
+ */
+public class NotificationBigPictureTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+ protected NotificationBigPictureTemplateViewWrapper(Context ctx, View view,
+ ExpandableNotificationRow row) {
+ super(ctx, view, row);
+ }
+
+ @Override
+ public void onContentUpdated(ExpandableNotificationRow row) {
+ super.onContentUpdated(row);
+ updateImageTag(row.getStatusBarNotification());
+ }
+
+ private void updateImageTag(StatusBarNotification notification) {
+ final Bundle extras = notification.getNotification().extras;
+ Icon overRiddenIcon = extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG);
+ if (overRiddenIcon != null) {
+ mPicture.setTag(ImageTransformState.ICON_TAG, overRiddenIcon);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationBigTextTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationBigTextTemplateViewWrapper.java
new file mode 100644
index 0000000..20a3d8f
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationBigTextTemplateViewWrapper.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.content.Context;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+
+import com.android.internal.widget.ImageFloatingTextView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+
+/**
+ * Wraps a notification containing a big text template
+ */
+public class NotificationBigTextTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+ private ImageFloatingTextView mBigtext;
+
+ protected NotificationBigTextTemplateViewWrapper(Context ctx, View view,
+ ExpandableNotificationRow row) {
+ super(ctx, view, row);
+ }
+
+ private void resolveViews(StatusBarNotification notification) {
+ mBigtext = (ImageFloatingTextView) mView.findViewById(com.android.internal.R.id.big_text);
+ }
+
+ @Override
+ public void onContentUpdated(ExpandableNotificationRow row) {
+ // Reinspect the notification. Before the super call, because the super call also updates
+ // the transformation types and we need to have our values set by then.
+ resolveViews(row.getStatusBarNotification());
+ super.onContentUpdated(row);
+ }
+
+ @Override
+ protected void updateTransformedTypes() {
+ // This also clears the existing types
+ super.updateTransformedTypes();
+ if (mBigtext != null) {
+ mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT,
+ mBigtext);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
new file mode 100644
index 0000000..bca4b43
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
@@ -0,0 +1,118 @@
+/*
+ * 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+/**
+ * Wraps a notification containing a custom view.
+ */
+public class NotificationCustomViewWrapper extends NotificationViewWrapper {
+
+ private final ViewInvertHelper mInvertHelper;
+ private final Paint mGreyPaint = new Paint();
+ private boolean mIsLegacy;
+ private int mLegacyColor;
+
+ protected NotificationCustomViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
+ super(ctx, view, row);
+ mInvertHelper = new ViewInvertHelper(view, NotificationPanelView.DOZE_ANIMATION_DURATION);
+ mLegacyColor = row.getContext().getColor(R.color.notification_legacy_background_color);
+ }
+
+ @Override
+ public void setDark(boolean dark, boolean fade, long delay) {
+ if (dark == mDark && mDarkInitialized) {
+ return;
+ }
+ super.setDark(dark, fade, delay);
+ if (!mIsLegacy && mShouldInvertDark) {
+ if (fade) {
+ mInvertHelper.fade(dark, delay);
+ } else {
+ mInvertHelper.update(dark);
+ }
+ } else {
+ mView.setLayerType(dark ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null);
+ if (fade) {
+ fadeGrayscale(dark, delay);
+ } else {
+ updateGrayscale(dark);
+ }
+ }
+ }
+
+ protected void fadeGrayscale(final boolean dark, long delay) {
+ getDozer().startIntensityAnimation(animation -> {
+ getDozer().updateGrayscaleMatrix((float) animation.getAnimatedValue());
+ mGreyPaint.setColorFilter(
+ new ColorMatrixColorFilter(getDozer().getGrayscaleColorMatrix()));
+ mView.setLayerPaint(mGreyPaint);
+ }, dark, delay, new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!dark) {
+ mView.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ }
+ });
+ }
+
+ protected void updateGrayscale(boolean dark) {
+ if (dark) {
+ getDozer().updateGrayscaleMatrix(1f);
+ mGreyPaint.setColorFilter(
+ new ColorMatrixColorFilter(getDozer().getGrayscaleColorMatrix()));
+ mView.setLayerPaint(mGreyPaint);
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ mView.setAlpha(visible ? 1.0f : 0.0f);
+ }
+
+ @Override
+ protected boolean shouldClearBackgroundOnReapply() {
+ return false;
+ }
+
+ @Override
+ public int getCustomBackgroundColor() {
+ int customBackgroundColor = super.getCustomBackgroundColor();
+ if (customBackgroundColor == 0 && mIsLegacy) {
+ return mLegacyColor;
+ }
+ return customBackgroundColor;
+ }
+
+ public void setLegacy(boolean legacy) {
+ super.setLegacy(legacy);
+ mIsLegacy = legacy;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationDozeHelper.java b/com/android/systemui/statusbar/notification/NotificationDozeHelper.java
new file mode 100644
index 0000000..0b3b3cb
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationDozeHelper.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.widget.ImageView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+import java.util.function.Consumer;
+
+public class NotificationDozeHelper {
+ private final ColorMatrix mGrayscaleColorMatrix = new ColorMatrix();
+
+ public void fadeGrayscale(final ImageView target, final boolean dark, long delay) {
+ startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ updateGrayscale(target, (float) animation.getAnimatedValue());
+ }
+ }, dark, delay, new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!dark) {
+ target.setColorFilter(null);
+ }
+ }
+ });
+ }
+
+ public void updateGrayscale(ImageView target, boolean dark) {
+ updateGrayscale(target, dark ? 1 : 0);
+ }
+
+ public void updateGrayscale(ImageView target, float darkAmount) {
+ if (darkAmount > 0) {
+ updateGrayscaleMatrix(darkAmount);
+ target.setColorFilter(new ColorMatrixColorFilter(mGrayscaleColorMatrix));
+ } else {
+ target.setColorFilter(null);
+ }
+ }
+
+ public 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(Interpolators.LINEAR_OUT_SLOW_IN);
+ animator.setStartDelay(delay);
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ animator.start();
+ }
+
+ public void setIntensityDark(Consumer<Float> listener, boolean dark,
+ boolean animate, long delay) {
+ if (animate) {
+ startIntensityAnimation(a -> listener.accept((Float) a.getAnimatedValue()), dark, delay,
+ null /* listener */);
+ } else {
+ listener.accept(dark ? 1f : 0f);
+ }
+ }
+
+ public void updateGrayscaleMatrix(float intensity) {
+ mGrayscaleColorMatrix.setSaturation(1 - intensity);
+ }
+
+ public ColorMatrix getGrayscaleColorMatrix() {
+ return mGrayscaleColorMatrix;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java
new file mode 100644
index 0000000..b95b8a3
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2015 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.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuffColorFilter;
+import android.util.ArraySet;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.widget.NotificationExpandButton;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+import java.util.Stack;
+
+import static com.android.systemui.statusbar.notification.TransformState.TRANSFORM_Y;
+
+/**
+ * Wraps a notification header view.
+ */
+public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
+
+ private static final Interpolator LOW_PRIORITY_HEADER_CLOSE
+ = new PathInterpolator(0.4f, 0f, 0.7f, 1f);
+
+ protected final ViewInvertHelper mInvertHelper;
+ protected final ViewTransformationHelper mTransformationHelper;
+
+ protected int mColor;
+ private ImageView mIcon;
+
+ private NotificationExpandButton mExpandButton;
+ private NotificationHeaderView mNotificationHeader;
+ private TextView mHeaderText;
+ private ImageView mWorkProfileImage;
+ private boolean mIsLowPriority;
+ private boolean mTransformLowPriorityTitle;
+ private boolean mShowExpandButtonAtEnd;
+
+ protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
+ super(ctx, view, row);
+ mShowExpandButtonAtEnd = ctx.getResources().getBoolean(
+ R.bool.config_showNotificationExpandButtonAtEnd);
+ mInvertHelper = new ViewInvertHelper(ctx, NotificationPanelView.DOZE_ANIMATION_DURATION);
+ mTransformationHelper = new ViewTransformationHelper();
+
+ // we want to avoid that the header clashes with the other text when transforming
+ // low-priority
+ mTransformationHelper.setCustomTransformation(
+ new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) {
+
+ @Override
+ public Interpolator getCustomInterpolator(int interpolationType,
+ boolean isFrom) {
+ boolean isLowPriority = mView instanceof NotificationHeaderView;
+ if (interpolationType == TRANSFORM_Y) {
+ if (isLowPriority && !isFrom
+ || !isLowPriority && isFrom) {
+ return Interpolators.LINEAR_OUT_SLOW_IN;
+ } else {
+ return LOW_PRIORITY_HEADER_CLOSE;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean hasCustomTransformation() {
+ return mIsLowPriority && mTransformLowPriorityTitle;
+ }
+ }, TRANSFORMING_VIEW_TITLE);
+ resolveHeaderViews();
+ updateInvertHelper();
+ }
+
+ @Override
+ protected NotificationDozeHelper createDozer(Context ctx) {
+ return new NotificationIconDozeHelper(ctx);
+ }
+
+ @Override
+ protected NotificationIconDozeHelper getDozer() {
+ return (NotificationIconDozeHelper) super.getDozer();
+ }
+
+ protected void resolveHeaderViews() {
+ mIcon = mView.findViewById(com.android.internal.R.id.icon);
+ mHeaderText = mView.findViewById(com.android.internal.R.id.header_text);
+ mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button);
+ mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge);
+ mColor = resolveColor(mExpandButton);
+ mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header);
+ mNotificationHeader.setShowExpandButtonAtEnd(mShowExpandButtonAtEnd);
+ getDozer().setColor(mColor);
+ }
+
+ private int resolveColor(ImageView icon) {
+ if (icon != null && icon.getDrawable() != null) {
+ ColorFilter filter = icon.getDrawable().getColorFilter();
+ if (filter instanceof PorterDuffColorFilter) {
+ return ((PorterDuffColorFilter) filter).getColor();
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public void onContentUpdated(ExpandableNotificationRow row) {
+ super.onContentUpdated(row);
+ mIsLowPriority = row.isLowPriority();
+ mTransformLowPriorityTitle = !row.isChildInGroup() && !row.isSummaryWithChildren();
+ ArraySet<View> previousViews = mTransformationHelper.getAllTransformingViews();
+
+ // Reinspect the notification.
+ resolveHeaderViews();
+ updateInvertHelper();
+ updateTransformedTypes();
+ addRemainingTransformTypes();
+ updateCropToPaddingForImageViews();
+ Notification notification = row.getStatusBarNotification().getNotification();
+ mIcon.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon());
+ // The work profile image is always the same lets just set the icon tag for it not to
+ // animate
+ mWorkProfileImage.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon());
+
+ // We need to reset all views that are no longer transforming in case a view was previously
+ // transformed, but now we decided to transform its container instead.
+ ArraySet<View> currentViews = mTransformationHelper.getAllTransformingViews();
+ for (int i = 0; i < previousViews.size(); i++) {
+ View view = previousViews.valueAt(i);
+ if (!currentViews.contains(view)) {
+ mTransformationHelper.resetTransformedView(view);
+ }
+ }
+ }
+
+ /**
+ * Adds the remaining TransformTypes to the TransformHelper. This is done to make sure that each
+ * child is faded automatically and doesn't have to be manually added.
+ * The keys used for the views are the ids.
+ */
+ private void addRemainingTransformTypes() {
+ mTransformationHelper.addRemainingTransformTypes(mView);
+ }
+
+ /**
+ * Since we are deactivating the clipping when transforming the ImageViews don't get clipped
+ * anymore during these transitions. We can avoid that by using
+ * {@link ImageView#setCropToPadding(boolean)} on all ImageViews.
+ */
+ private void updateCropToPaddingForImageViews() {
+ Stack<View> stack = new Stack<>();
+ stack.push(mView);
+ while (!stack.isEmpty()) {
+ View child = stack.pop();
+ if (child instanceof ImageView) {
+ ((ImageView) child).setCropToPadding(true);
+ } else if (child instanceof ViewGroup){
+ ViewGroup group = (ViewGroup) child;
+ for (int i = 0; i < group.getChildCount(); i++) {
+ stack.push(group.getChildAt(i));
+ }
+ }
+ }
+ }
+
+ protected void updateInvertHelper() {
+ mInvertHelper.clearTargets();
+ for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
+ View child = mNotificationHeader.getChildAt(i);
+ if (child != mIcon) {
+ mInvertHelper.addTarget(child);
+ }
+ }
+ }
+
+ protected void updateTransformedTypes() {
+ mTransformationHelper.reset();
+ mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ICON, mIcon);
+ if (mIsLowPriority) {
+ mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE,
+ mHeaderText);
+ }
+ }
+
+ @Override
+ public void setDark(boolean dark, boolean fade, long delay) {
+ if (dark == mDark && mDarkInitialized) {
+ return;
+ }
+ super.setDark(dark, fade, delay);
+ if (fade) {
+ mInvertHelper.fade(dark, delay);
+ } else {
+ mInvertHelper.update(dark);
+ }
+ if (mIcon != null && !mRow.isChildInGroup()) {
+ // We don't update the color for children views / their icon is invisible anyway.
+ // It also may lead to bugs where the icon isn't correctly greyed out.
+ boolean hadColorFilter = mNotificationHeader.getOriginalIconColor()
+ != NotificationHeaderView.NO_COLOR;
+
+ getDozer().setImageDark(mIcon, dark, fade, delay, !hadColorFilter);
+ }
+ }
+
+ @Override
+ public void updateExpandability(boolean expandable, View.OnClickListener onClickListener) {
+ mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE);
+ mNotificationHeader.setOnClickListener(expandable ? onClickListener : null);
+ }
+
+ @Override
+ public NotificationHeaderView getNotificationHeader() {
+ return mNotificationHeader;
+ }
+
+ @Override
+ public TransformState getCurrentState(int fadingView) {
+ return mTransformationHelper.getCurrentState(fadingView);
+ }
+
+ @Override
+ public void transformTo(TransformableView notification, Runnable endRunnable) {
+ mTransformationHelper.transformTo(notification, endRunnable);
+ }
+
+ @Override
+ public void transformTo(TransformableView notification, float transformationAmount) {
+ mTransformationHelper.transformTo(notification, transformationAmount);
+ }
+
+ @Override
+ public void transformFrom(TransformableView notification) {
+ mTransformationHelper.transformFrom(notification);
+ }
+
+ @Override
+ public void transformFrom(TransformableView notification, float transformationAmount) {
+ mTransformationHelper.transformFrom(notification, transformationAmount);
+ }
+
+ @Override
+ public void setIsChildInGroup(boolean isChildInGroup) {
+ super.setIsChildInGroup(isChildInGroup);
+ mTransformLowPriorityTitle = !isChildInGroup;
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ mTransformationHelper.setVisible(visible);
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationIconDozeHelper.java b/com/android/systemui/statusbar/notification/NotificationIconDozeHelper.java
new file mode 100644
index 0000000..9f79ef2
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationIconDozeHelper.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+
+public class NotificationIconDozeHelper extends NotificationDozeHelper {
+
+ private final int mImageDarkAlpha;
+ private final int mImageDarkColor = 0xffffffff;
+ private final PorterDuffColorFilter mImageColorFilter = new PorterDuffColorFilter(
+ 0, PorterDuff.Mode.SRC_ATOP);
+
+ private int mColor = Color.BLACK;
+
+ public NotificationIconDozeHelper(Context ctx) {
+ mImageDarkAlpha = ctx.getResources().getInteger(R.integer.doze_small_icon_alpha);
+ }
+
+ public void setColor(int color) {
+ mColor = color;
+ }
+
+ public void setImageDark(ImageView target, boolean dark, boolean fade, long delay,
+ boolean useGrayscale) {
+ if (fade) {
+ if (!useGrayscale) {
+ fadeImageColorFilter(target, dark, delay);
+ fadeImageAlpha(target, dark, delay);
+ } else {
+ fadeGrayscale(target, dark, delay);
+ }
+ } else {
+ if (!useGrayscale) {
+ updateImageColorFilter(target, dark);
+ updateImageAlpha(target, dark);
+ } else {
+ updateGrayscale(target, dark);
+ }
+ }
+ }
+
+ private void fadeImageColorFilter(final ImageView target, boolean dark, long delay) {
+ startIntensityAnimation(animation -> {
+ updateImageColorFilter(target, (Float) animation.getAnimatedValue());
+ }, dark, delay, null /* listener */);
+ }
+
+ private void fadeImageAlpha(final ImageView target, boolean dark, long delay) {
+ startIntensityAnimation(animation -> {
+ float t = (float) animation.getAnimatedValue();
+ target.setImageAlpha((int) (255 * (1f - t) + mImageDarkAlpha * t));
+ }, dark, delay, null /* listener */);
+ }
+
+ private void updateImageColorFilter(ImageView target, boolean dark) {
+ updateImageColorFilter(target, dark ? 1f : 0f);
+ }
+
+ private void updateImageColorFilter(ImageView target, float intensity) {
+ int color = NotificationUtils.interpolateColors(mColor, mImageDarkColor, intensity);
+ mImageColorFilter.setColor(color);
+ Drawable imageDrawable = target.getDrawable();
+
+ // Also, the notification might have been modified during the animation, so background
+ // might be null here.
+ if (imageDrawable != null) {
+ Drawable d = imageDrawable.mutate();
+ // DrawableContainer ignores the color filter if it's already set, so clear it first to
+ // get it set and invalidated properly.
+ d.setColorFilter(null);
+ d.setColorFilter(mImageColorFilter);
+ }
+ }
+
+ private void updateImageAlpha(ImageView target, boolean dark) {
+ target.setImageAlpha(dark ? mImageDarkAlpha : 255);
+ }
+
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationInflater.java b/com/android/systemui/statusbar/notification/NotificationInflater.java
new file mode 100644
index 0000000..f967118
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationInflater.java
@@ -0,0 +1,711 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.CancellationSignal;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.InflationTask;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationContentView;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.util.Assert;
+
+import java.util.HashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A utility that inflates the right kind of contentView based on the state
+ */
+public class NotificationInflater {
+
+ public static final String TAG = "NotificationInflater";
+ @VisibleForTesting
+ static final int FLAG_REINFLATE_ALL = ~0;
+ private static final int FLAG_REINFLATE_CONTENT_VIEW = 1<<0;
+ @VisibleForTesting
+ static final int FLAG_REINFLATE_EXPANDED_VIEW = 1<<1;
+ private static final int FLAG_REINFLATE_HEADS_UP_VIEW = 1<<2;
+ private static final int FLAG_REINFLATE_PUBLIC_VIEW = 1<<3;
+ private static final int FLAG_REINFLATE_AMBIENT_VIEW = 1<<4;
+ private static final InflationExecutor EXECUTOR = new InflationExecutor();
+
+ private final ExpandableNotificationRow mRow;
+ private boolean mIsLowPriority;
+ private boolean mUsesIncreasedHeight;
+ private boolean mUsesIncreasedHeadsUpHeight;
+ private RemoteViews.OnClickHandler mRemoteViewClickHandler;
+ private boolean mIsChildInGroup;
+ private InflationCallback mCallback;
+ private boolean mRedactAmbient;
+
+ public NotificationInflater(ExpandableNotificationRow row) {
+ mRow = row;
+ }
+
+ public void setIsLowPriority(boolean isLowPriority) {
+ mIsLowPriority = isLowPriority;
+ }
+
+ /**
+ * Set whether the notification is a child in a group
+ *
+ * @return whether the view was re-inflated
+ */
+ public void setIsChildInGroup(boolean childInGroup) {
+ if (childInGroup != mIsChildInGroup) {
+ mIsChildInGroup = childInGroup;
+ if (mIsLowPriority) {
+ int flags = FLAG_REINFLATE_CONTENT_VIEW | FLAG_REINFLATE_EXPANDED_VIEW;
+ inflateNotificationViews(flags);
+ }
+ } ;
+ }
+
+ public void setUsesIncreasedHeight(boolean usesIncreasedHeight) {
+ mUsesIncreasedHeight = usesIncreasedHeight;
+ }
+
+ public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) {
+ mUsesIncreasedHeadsUpHeight = usesIncreasedHeight;
+ }
+
+ public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) {
+ mRemoteViewClickHandler = remoteViewClickHandler;
+ }
+
+ public void setRedactAmbient(boolean redactAmbient) {
+ if (mRedactAmbient != redactAmbient) {
+ mRedactAmbient = redactAmbient;
+ if (mRow.getEntry() == null) {
+ return;
+ }
+ inflateNotificationViews(FLAG_REINFLATE_AMBIENT_VIEW);
+ }
+ }
+
+ /**
+ * Inflate all views of this notification on a background thread. This is asynchronous and will
+ * notify the callback once it's finished.
+ */
+ public void inflateNotificationViews() {
+ inflateNotificationViews(FLAG_REINFLATE_ALL);
+ }
+
+ /**
+ * Reinflate all views for the specified flags on a background thread. This is asynchronous and
+ * will notify the callback once it's finished.
+ *
+ * @param reInflateFlags flags which views should be reinflated. Use {@link #FLAG_REINFLATE_ALL}
+ * to reinflate all of views.
+ */
+ @VisibleForTesting
+ void inflateNotificationViews(int reInflateFlags) {
+ if (mRow.isRemoved()) {
+ // We don't want to reinflate anything for removed notifications. Otherwise views might
+ // be readded to the stack, leading to leaks. This may happen with low-priority groups
+ // where the removal of already removed children can lead to a reinflation.
+ return;
+ }
+ StatusBarNotification sbn = mRow.getEntry().notification;
+ new AsyncInflationTask(sbn, reInflateFlags, mRow, mIsLowPriority,
+ mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
+ mCallback, mRemoteViewClickHandler).execute();
+ }
+
+ @VisibleForTesting
+ InflationProgress inflateNotificationViews(int reInflateFlags,
+ Notification.Builder builder, Context packageContext) {
+ InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority,
+ mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
+ mRedactAmbient, packageContext);
+ apply(result, reInflateFlags, mRow, mRedactAmbient, mRemoteViewClickHandler, null);
+ return result;
+ }
+
+ private static InflationProgress createRemoteViews(int reInflateFlags,
+ Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup,
+ boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
+ Context packageContext) {
+ InflationProgress result = new InflationProgress();
+ isLowPriority = isLowPriority && !isChildInGroup;
+ if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
+ result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);
+ }
+
+ if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
+ result.newExpandedView = createExpandedView(builder, isLowPriority);
+ }
+
+ if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
+ result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);
+ }
+
+ if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
+ result.newPublicView = builder.makePublicContentView();
+ }
+
+ if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
+ result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification()
+ : builder.makeAmbientNotification();
+ }
+ result.packageContext = packageContext;
+ return result;
+ }
+
+ public static CancellationSignal apply(InflationProgress result, int reInflateFlags,
+ ExpandableNotificationRow row, boolean redactAmbient,
+ RemoteViews.OnClickHandler remoteViewClickHandler,
+ @Nullable InflationCallback callback) {
+ NotificationData.Entry entry = row.getEntry();
+ NotificationContentView privateLayout = row.getPrivateLayout();
+ NotificationContentView publicLayout = row.getPublicLayout();
+ final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
+
+ int flag = FLAG_REINFLATE_CONTENT_VIEW;
+ if ((reInflateFlags & flag) != 0) {
+ boolean isNewView = !canReapplyRemoteView(result.newContentView, entry.cachedContentView);
+ ApplyCallback applyCallback = new ApplyCallback() {
+ @Override
+ public void setResultView(View v) {
+ result.inflatedContentView = v;
+ }
+
+ @Override
+ public RemoteViews getRemoteView() {
+ return result.newContentView;
+ }
+ };
+ applyRemoteView(result, reInflateFlags, flag, row, redactAmbient,
+ isNewView, remoteViewClickHandler, callback, entry, privateLayout,
+ privateLayout.getContractedChild(), privateLayout.getVisibleWrapper(
+ NotificationContentView.VISIBLE_TYPE_CONTRACTED),
+ runningInflations, applyCallback);
+ }
+
+ flag = FLAG_REINFLATE_EXPANDED_VIEW;
+ if ((reInflateFlags & flag) != 0) {
+ if (result.newExpandedView != null) {
+ boolean isNewView = !canReapplyRemoteView(result.newExpandedView,
+ entry.cachedBigContentView);
+ ApplyCallback applyCallback = new ApplyCallback() {
+ @Override
+ public void setResultView(View v) {
+ result.inflatedExpandedView = v;
+ }
+
+ @Override
+ public RemoteViews getRemoteView() {
+ return result.newExpandedView;
+ }
+ };
+ applyRemoteView(result, reInflateFlags, flag, row,
+ redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
+ privateLayout, privateLayout.getExpandedChild(),
+ privateLayout.getVisibleWrapper(
+ NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations,
+ applyCallback);
+ }
+ }
+
+ flag = FLAG_REINFLATE_HEADS_UP_VIEW;
+ if ((reInflateFlags & flag) != 0) {
+ if (result.newHeadsUpView != null) {
+ boolean isNewView = !canReapplyRemoteView(result.newHeadsUpView,
+ entry.cachedHeadsUpContentView);
+ ApplyCallback applyCallback = new ApplyCallback() {
+ @Override
+ public void setResultView(View v) {
+ result.inflatedHeadsUpView = v;
+ }
+
+ @Override
+ public RemoteViews getRemoteView() {
+ return result.newHeadsUpView;
+ }
+ };
+ applyRemoteView(result, reInflateFlags, flag, row,
+ redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
+ privateLayout, privateLayout.getHeadsUpChild(),
+ privateLayout.getVisibleWrapper(
+ NotificationContentView.VISIBLE_TYPE_HEADSUP), runningInflations,
+ applyCallback);
+ }
+ }
+
+ flag = FLAG_REINFLATE_PUBLIC_VIEW;
+ if ((reInflateFlags & flag) != 0) {
+ boolean isNewView = !canReapplyRemoteView(result.newPublicView,
+ entry.cachedPublicContentView);
+ ApplyCallback applyCallback = new ApplyCallback() {
+ @Override
+ public void setResultView(View v) {
+ result.inflatedPublicView = v;
+ }
+
+ @Override
+ public RemoteViews getRemoteView() {
+ return result.newPublicView;
+ }
+ };
+ applyRemoteView(result, reInflateFlags, flag, row,
+ redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
+ publicLayout, publicLayout.getContractedChild(),
+ publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),
+ runningInflations, applyCallback);
+ }
+
+ flag = FLAG_REINFLATE_AMBIENT_VIEW;
+ if ((reInflateFlags & flag) != 0) {
+ NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout;
+ boolean isNewView = !canReapplyAmbient(row, redactAmbient) ||
+ !canReapplyRemoteView(result.newAmbientView, entry.cachedAmbientContentView);
+ ApplyCallback applyCallback = new ApplyCallback() {
+ @Override
+ public void setResultView(View v) {
+ result.inflatedAmbientView = v;
+ }
+
+ @Override
+ public RemoteViews getRemoteView() {
+ return result.newAmbientView;
+ }
+ };
+ applyRemoteView(result, reInflateFlags, flag, row,
+ redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
+ newParent, newParent.getAmbientChild(), newParent.getVisibleWrapper(
+ NotificationContentView.VISIBLE_TYPE_AMBIENT), runningInflations,
+ applyCallback);
+ }
+
+ // Let's try to finish, maybe nobody is even inflating anything
+ finishIfDone(result, reInflateFlags, runningInflations, callback, row,
+ redactAmbient);
+ CancellationSignal cancellationSignal = new CancellationSignal();
+ cancellationSignal.setOnCancelListener(
+ () -> runningInflations.values().forEach(CancellationSignal::cancel));
+ return cancellationSignal;
+ }
+
+ @VisibleForTesting
+ static void applyRemoteView(final InflationProgress result,
+ final int reInflateFlags, int inflationId,
+ final ExpandableNotificationRow row,
+ final boolean redactAmbient, boolean isNewView,
+ RemoteViews.OnClickHandler remoteViewClickHandler,
+ @Nullable final InflationCallback callback, NotificationData.Entry entry,
+ NotificationContentView parentLayout, View existingView,
+ NotificationViewWrapper existingWrapper,
+ final HashMap<Integer, CancellationSignal> runningInflations,
+ ApplyCallback applyCallback) {
+ RemoteViews newContentView = applyCallback.getRemoteView();
+ RemoteViews.OnViewAppliedListener listener
+ = new RemoteViews.OnViewAppliedListener() {
+
+ @Override
+ public void onViewApplied(View v) {
+ if (isNewView) {
+ v.setIsRootNamespace(true);
+ applyCallback.setResultView(v);
+ } else if (existingWrapper != null) {
+ existingWrapper.onReinflated();
+ }
+ runningInflations.remove(inflationId);
+ finishIfDone(result, reInflateFlags, runningInflations, callback, row,
+ redactAmbient);
+ }
+
+ @Override
+ public void onError(Exception e) {
+ // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could
+ // actually also be a system issue, so let's try on the UI thread again to be safe.
+ try {
+ View newView = existingView;
+ if (isNewView) {
+ newView = newContentView.apply(
+ result.packageContext,
+ parentLayout,
+ remoteViewClickHandler);
+ } else {
+ newContentView.reapply(
+ result.packageContext,
+ existingView,
+ remoteViewClickHandler);
+ }
+ Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.",
+ e);
+ onViewApplied(newView);
+ } catch (Exception anotherException) {
+ runningInflations.remove(inflationId);
+ handleInflationError(runningInflations, e, entry.notification, callback);
+ }
+ }
+ };
+ CancellationSignal cancellationSignal;
+ if (isNewView) {
+ cancellationSignal = newContentView.applyAsync(
+ result.packageContext,
+ parentLayout,
+ EXECUTOR,
+ listener,
+ remoteViewClickHandler);
+ } else {
+ cancellationSignal = newContentView.reapplyAsync(
+ result.packageContext,
+ existingView,
+ EXECUTOR,
+ listener,
+ remoteViewClickHandler);
+ }
+ runningInflations.put(inflationId, cancellationSignal);
+ }
+
+ private static void handleInflationError(HashMap<Integer, CancellationSignal> runningInflations,
+ Exception e, StatusBarNotification notification, @Nullable InflationCallback callback) {
+ Assert.isMainThread();
+ runningInflations.values().forEach(CancellationSignal::cancel);
+ if (callback != null) {
+ callback.handleInflationException(notification, e);
+ }
+ }
+
+ /**
+ * Finish the inflation of the views
+ *
+ * @return true if the inflation was finished
+ */
+ private static boolean finishIfDone(InflationProgress result, int reInflateFlags,
+ HashMap<Integer, CancellationSignal> runningInflations,
+ @Nullable InflationCallback endListener, ExpandableNotificationRow row,
+ boolean redactAmbient) {
+ Assert.isMainThread();
+ NotificationData.Entry entry = row.getEntry();
+ NotificationContentView privateLayout = row.getPrivateLayout();
+ NotificationContentView publicLayout = row.getPublicLayout();
+ if (runningInflations.isEmpty()) {
+ if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
+ if (result.inflatedContentView != null) {
+ privateLayout.setContractedChild(result.inflatedContentView);
+ }
+ entry.cachedContentView = result.newContentView;
+ }
+
+ if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
+ if (result.inflatedExpandedView != null) {
+ privateLayout.setExpandedChild(result.inflatedExpandedView);
+ } else if (result.newExpandedView == null) {
+ privateLayout.setExpandedChild(null);
+ }
+ entry.cachedBigContentView = result.newExpandedView;
+ row.setExpandable(result.newExpandedView != null);
+ }
+
+ if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
+ if (result.inflatedHeadsUpView != null) {
+ privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
+ } else if (result.newHeadsUpView == null) {
+ privateLayout.setHeadsUpChild(null);
+ }
+ entry.cachedHeadsUpContentView = result.newHeadsUpView;
+ }
+
+ if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
+ if (result.inflatedPublicView != null) {
+ publicLayout.setContractedChild(result.inflatedPublicView);
+ }
+ entry.cachedPublicContentView = result.newPublicView;
+ }
+
+ if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
+ if (result.inflatedAmbientView != null) {
+ NotificationContentView newParent = redactAmbient
+ ? publicLayout : privateLayout;
+ NotificationContentView otherParent = !redactAmbient
+ ? publicLayout : privateLayout;
+ newParent.setAmbientChild(result.inflatedAmbientView);
+ otherParent.setAmbientChild(null);
+ }
+ entry.cachedAmbientContentView = result.newAmbientView;
+ }
+ if (endListener != null) {
+ endListener.onAsyncInflationFinished(row.getEntry());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private static RemoteViews createExpandedView(Notification.Builder builder,
+ boolean isLowPriority) {
+ RemoteViews bigContentView = builder.createBigContentView();
+ if (bigContentView != null) {
+ return bigContentView;
+ }
+ if (isLowPriority) {
+ RemoteViews contentView = builder.createContentView();
+ Notification.Builder.makeHeaderExpanded(contentView);
+ return contentView;
+ }
+ return null;
+ }
+
+ private static RemoteViews createContentView(Notification.Builder builder,
+ boolean isLowPriority, boolean useLarge) {
+ if (isLowPriority) {
+ return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
+ }
+ return builder.createContentView(useLarge);
+ }
+
+ /**
+ * @param newView The new view that will be applied
+ * @param oldView The old view that was applied to the existing view before
+ * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply.
+ */
+ @VisibleForTesting
+ static boolean canReapplyRemoteView(final RemoteViews newView,
+ final RemoteViews oldView) {
+ return (newView == null && oldView == null) ||
+ (newView != null && oldView != null
+ && oldView.getPackage() != null
+ && newView.getPackage() != null
+ && newView.getPackage().equals(oldView.getPackage())
+ && newView.getLayoutId() == oldView.getLayoutId()
+ && !oldView.isReapplyDisallowed());
+ }
+
+ public void setInflationCallback(InflationCallback callback) {
+ mCallback = callback;
+ }
+
+ public interface InflationCallback {
+ void handleInflationException(StatusBarNotification notification, Exception e);
+ void onAsyncInflationFinished(NotificationData.Entry entry);
+ }
+
+ public void onDensityOrFontScaleChanged() {
+ NotificationData.Entry entry = mRow.getEntry();
+ entry.cachedAmbientContentView = null;
+ entry.cachedBigContentView = null;
+ entry.cachedContentView = null;
+ entry.cachedHeadsUpContentView = null;
+ entry.cachedPublicContentView = null;
+ inflateNotificationViews();
+ }
+
+ private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) {
+ NotificationContentView ambientView = redactAmbient ? row.getPublicLayout()
+ : row.getPrivateLayout(); ;
+ return ambientView.getAmbientChild() != null;
+ }
+
+ public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
+ implements InflationCallback, InflationTask {
+
+ private final StatusBarNotification mSbn;
+ private final Context mContext;
+ private final boolean mIsLowPriority;
+ private final boolean mIsChildInGroup;
+ private final boolean mUsesIncreasedHeight;
+ private final InflationCallback mCallback;
+ private final boolean mUsesIncreasedHeadsUpHeight;
+ private final boolean mRedactAmbient;
+ private int mReInflateFlags;
+ private ExpandableNotificationRow mRow;
+ private Exception mError;
+ private RemoteViews.OnClickHandler mRemoteViewClickHandler;
+ private CancellationSignal mCancellationSignal;
+
+ private AsyncInflationTask(StatusBarNotification notification,
+ int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority,
+ boolean isChildInGroup, boolean usesIncreasedHeight,
+ boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
+ InflationCallback callback,
+ RemoteViews.OnClickHandler remoteViewClickHandler) {
+ mRow = row;
+ mSbn = notification;
+ mReInflateFlags = reInflateFlags;
+ mContext = mRow.getContext();
+ mIsLowPriority = isLowPriority;
+ mIsChildInGroup = isChildInGroup;
+ mUsesIncreasedHeight = usesIncreasedHeight;
+ mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
+ mRedactAmbient = redactAmbient;
+ mRemoteViewClickHandler = remoteViewClickHandler;
+ mCallback = callback;
+ NotificationData.Entry entry = row.getEntry();
+ entry.setInflationTask(this);
+ }
+
+ @VisibleForTesting
+ public int getReInflateFlags() {
+ return mReInflateFlags;
+ }
+
+ @Override
+ protected InflationProgress doInBackground(Void... params) {
+ try {
+ final Notification.Builder recoveredBuilder
+ = Notification.Builder.recoverBuilder(mContext,
+ mSbn.getNotification());
+ Context packageContext = mSbn.getPackageContext(mContext);
+ Notification notification = mSbn.getNotification();
+ if (mIsLowPriority) {
+ int backgroundColor = mContext.getColor(
+ R.color.notification_material_background_low_priority_color);
+ recoveredBuilder.setBackgroundColorHint(backgroundColor);
+ }
+ if (notification.isMediaNotification()) {
+ MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext,
+ packageContext);
+ processor.setIsLowPriority(mIsLowPriority);
+ processor.processNotification(notification, recoveredBuilder);
+ }
+ return createRemoteViews(mReInflateFlags,
+ recoveredBuilder, mIsLowPriority, mIsChildInGroup,
+ mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
+ packageContext);
+ } catch (Exception e) {
+ mError = e;
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(InflationProgress result) {
+ if (mError == null) {
+ mCancellationSignal = apply(result, mReInflateFlags, mRow, mRedactAmbient,
+ mRemoteViewClickHandler, this);
+ } else {
+ handleError(mError);
+ }
+ }
+
+ private void handleError(Exception e) {
+ mRow.getEntry().onInflationTaskFinished();
+ StatusBarNotification sbn = mRow.getStatusBarNotification();
+ final String ident = sbn.getPackageName() + "/0x"
+ + Integer.toHexString(sbn.getId());
+ Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e);
+ mCallback.handleInflationException(sbn,
+ new InflationException("Couldn't inflate contentViews" + e));
+ }
+
+ @Override
+ public void abort() {
+ cancel(true /* mayInterruptIfRunning */);
+ if (mCancellationSignal != null) {
+ mCancellationSignal.cancel();
+ }
+ }
+
+ @Override
+ public void supersedeTask(InflationTask task) {
+ if (task instanceof AsyncInflationTask) {
+ // We want to inflate all flags of the previous task as well
+ mReInflateFlags |= ((AsyncInflationTask) task).mReInflateFlags;
+ }
+ }
+
+ @Override
+ public void handleInflationException(StatusBarNotification notification, Exception e) {
+ handleError(e);
+ }
+
+ @Override
+ public void onAsyncInflationFinished(NotificationData.Entry entry) {
+ mRow.getEntry().onInflationTaskFinished();
+ mRow.onNotificationUpdated();
+ mCallback.onAsyncInflationFinished(mRow.getEntry());
+ }
+ }
+
+ @VisibleForTesting
+ static class InflationProgress {
+ private RemoteViews newContentView;
+ private RemoteViews newHeadsUpView;
+ private RemoteViews newExpandedView;
+ private RemoteViews newAmbientView;
+ private RemoteViews newPublicView;
+
+ @VisibleForTesting
+ Context packageContext;
+
+ private View inflatedContentView;
+ private View inflatedHeadsUpView;
+ private View inflatedExpandedView;
+ private View inflatedAmbientView;
+ private View inflatedPublicView;
+ }
+
+ @VisibleForTesting
+ abstract static class ApplyCallback {
+ public abstract void setResultView(View v);
+ public abstract RemoteViews getRemoteView();
+ }
+
+ /**
+ * A custom executor that allows more tasks to be queued. Default values are copied from
+ * AsyncTask
+ */
+ private static class InflationExecutor implements Executor {
+ private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+ // We want at least 2 threads and at most 4 threads in the core pool,
+ // preferring to have 1 less than the CPU count to avoid saturating
+ // the CPU with background work
+ private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
+ private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
+ private static final int KEEP_ALIVE_SECONDS = 30;
+
+ private static final ThreadFactory sThreadFactory = new ThreadFactory() {
+ private final AtomicInteger mCount = new AtomicInteger(1);
+
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "InflaterThread #" + mCount.getAndIncrement());
+ }
+ };
+
+ private final ThreadPoolExecutor mExecutor;
+
+ private InflationExecutor() {
+ mExecutor = new ThreadPoolExecutor(
+ CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(), sThreadFactory);
+ mExecutor.allowCoreThreadTimeOut(true);
+ }
+
+ @Override
+ public void execute(Runnable runnable) {
+ mExecutor.execute(runnable);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java
new file mode 100644
index 0000000..eb211a1
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.content.Context;
+import android.view.View;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+
+/**
+ * Wraps a notification containing a media template
+ */
+public class NotificationMediaTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+ protected NotificationMediaTemplateViewWrapper(Context ctx, View view,
+ ExpandableNotificationRow row) {
+ super(ctx, view, row);
+ }
+
+ View mActions;
+
+ private void resolveViews() {
+ mActions = mView.findViewById(com.android.internal.R.id.media_actions);
+ }
+
+ @Override
+ public void onContentUpdated(ExpandableNotificationRow row) {
+ // Reinspect the notification. Before the super call, because the super call also updates
+ // the transformation types and we need to have our values set by then.
+ resolveViews();
+ super.onContentUpdated(row);
+ }
+
+ @Override
+ protected void updateTransformedTypes() {
+ // This also clears the existing types
+ super.updateTransformedTypes();
+ if (mActions != null) {
+ mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ACTIONS,
+ mActions);
+ }
+ }
+
+ @Override
+ public boolean isDimmable() {
+ return false;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
new file mode 100644
index 0000000..f6ee1ca
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import com.android.internal.widget.MessagingLinearLayout;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+/**
+ * Wraps a notification containing a messaging template
+ */
+public class NotificationMessagingTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+ private View mContractedMessage;
+ private ArrayList<View> mHistoricMessages = new ArrayList<View>();
+
+ protected NotificationMessagingTemplateViewWrapper(Context ctx, View view,
+ ExpandableNotificationRow row) {
+ super(ctx, view, row);
+ }
+
+ private void resolveViews() {
+ mContractedMessage = null;
+
+ View container = mView.findViewById(com.android.internal.R.id.notification_messaging);
+ if (container instanceof MessagingLinearLayout
+ && ((MessagingLinearLayout) container).getChildCount() > 0) {
+ MessagingLinearLayout messagingContainer = (MessagingLinearLayout) container;
+
+ int childCount = messagingContainer.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = messagingContainer.getChildAt(i);
+
+ if (child.getVisibility() == View.GONE
+ && child instanceof TextView
+ && !TextUtils.isEmpty(((TextView) child).getText())) {
+ mHistoricMessages.add(child);
+ }
+
+ // Only consider the first visible child - transforming to a position other than the
+ // first looks bad because we have to move across other messages that are fading in.
+ if (child.getId() == messagingContainer.getContractedChildId()) {
+ mContractedMessage = child;
+ } else if (child.getVisibility() == View.VISIBLE) {
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onContentUpdated(ExpandableNotificationRow row) {
+ // Reinspect the notification. Before the super call, because the super call also updates
+ // the transformation types and we need to have our values set by then.
+ resolveViews();
+ super.onContentUpdated(row);
+ }
+
+ @Override
+ protected void updateTransformedTypes() {
+ // This also clears the existing types
+ super.updateTransformedTypes();
+ if (mContractedMessage != null) {
+ mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT,
+ mContractedMessage);
+ }
+ }
+
+ @Override
+ public void setRemoteInputVisible(boolean visible) {
+ for (int i = 0; i < mHistoricMessages.size(); i++) {
+ mHistoricMessages.get(i).setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
new file mode 100644
index 0000000..bb979eb
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
@@ -0,0 +1,275 @@
+/*
+ * 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.notification;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+
+/**
+ * Wraps a notification view inflated from a template.
+ */
+public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapper {
+
+ private static final int mDarkProgressTint = 0xffffffff;
+
+ protected ImageView mPicture;
+ private ProgressBar mProgressBar;
+ private TextView mTitle;
+ private TextView mText;
+ private View mActionsContainer;
+ private View mReplyAction;
+ private Rect mTmpRect = new Rect();
+
+ private int mContentHeight;
+ private int mMinHeightHint;
+
+ protected NotificationTemplateViewWrapper(Context ctx, View view,
+ ExpandableNotificationRow row) {
+ super(ctx, view, row);
+ mTransformationHelper.setCustomTransformation(
+ new ViewTransformationHelper.CustomTransformation() {
+ @Override
+ public boolean transformTo(TransformState ownState,
+ TransformableView notification, final float transformationAmount) {
+ if (!(notification instanceof HybridNotificationView)) {
+ return false;
+ }
+ TransformState otherState = notification.getCurrentState(
+ TRANSFORMING_VIEW_TITLE);
+ final View text = ownState.getTransformedView();
+ CrossFadeHelper.fadeOut(text, transformationAmount);
+ if (otherState != null) {
+ ownState.transformViewVerticalTo(otherState, this,
+ transformationAmount);
+ otherState.recycle();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean customTransformTarget(TransformState ownState,
+ TransformState otherState) {
+ float endY = getTransformationY(ownState, otherState);
+ ownState.setTransformationEndY(endY);
+ return true;
+ }
+
+ @Override
+ public boolean transformFrom(TransformState ownState,
+ TransformableView notification, float transformationAmount) {
+ if (!(notification instanceof HybridNotificationView)) {
+ return false;
+ }
+ TransformState otherState = notification.getCurrentState(
+ TRANSFORMING_VIEW_TITLE);
+ final View text = ownState.getTransformedView();
+ CrossFadeHelper.fadeIn(text, transformationAmount);
+ if (otherState != null) {
+ ownState.transformViewVerticalFrom(otherState, this,
+ transformationAmount);
+ otherState.recycle();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean initTransformation(TransformState ownState,
+ TransformState otherState) {
+ float startY = getTransformationY(ownState, otherState);
+ ownState.setTransformationStartY(startY);
+ return true;
+ }
+
+ private float getTransformationY(TransformState ownState,
+ TransformState otherState) {
+ int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
+ int[] ownStablePosition = ownState.getLaidOutLocationOnScreen();
+ return (otherStablePosition[1]
+ + otherState.getTransformedView().getHeight()
+ - ownStablePosition[1]) * 0.33f;
+ }
+
+ }, TRANSFORMING_VIEW_TEXT);
+ }
+
+ private void resolveTemplateViews(StatusBarNotification notification) {
+ mPicture = (ImageView) mView.findViewById(com.android.internal.R.id.right_icon);
+ if (mPicture != null) {
+ mPicture.setTag(ImageTransformState.ICON_TAG,
+ notification.getNotification().getLargeIcon());
+ }
+ mTitle = (TextView) mView.findViewById(com.android.internal.R.id.title);
+ mText = (TextView) mView.findViewById(com.android.internal.R.id.text);
+ final View progress = mView.findViewById(com.android.internal.R.id.progress);
+ if (progress instanceof ProgressBar) {
+ mProgressBar = (ProgressBar) progress;
+ } else {
+ // It's still a viewstub
+ mProgressBar = null;
+ }
+ mActionsContainer = mView.findViewById(com.android.internal.R.id.actions_container);
+ mReplyAction = mView.findViewById(com.android.internal.R.id.reply_icon_action);
+ }
+
+ @Override
+ public boolean disallowSingleClick(float x, float y) {
+ if (mReplyAction != null && mReplyAction.getVisibility() == View.VISIBLE) {
+ if (isOnView(mReplyAction, x, y) || isOnView(mPicture, x, y)) {
+ return true;
+ }
+ }
+ return super.disallowSingleClick(x, y);
+ }
+
+ private boolean isOnView(View view, float x, float y) {
+ View searchView = (View) view.getParent();
+ while (searchView != null && !(searchView instanceof ExpandableNotificationRow)) {
+ searchView.getHitRect(mTmpRect);
+ x -= mTmpRect.left;
+ y -= mTmpRect.top;
+ searchView = (View) searchView.getParent();
+ }
+ view.getHitRect(mTmpRect);
+ return mTmpRect.contains((int) x,(int) y);
+ }
+
+ @Override
+ public void onContentUpdated(ExpandableNotificationRow row) {
+ // Reinspect the notification. Before the super call, because the super call also updates
+ // the transformation types and we need to have our values set by then.
+ resolveTemplateViews(row.getStatusBarNotification());
+ super.onContentUpdated(row);
+ }
+
+ @Override
+ protected void updateInvertHelper() {
+ super.updateInvertHelper();
+ View mainColumn = mView.findViewById(com.android.internal.R.id.notification_main_column);
+ if (mainColumn != null) {
+ mInvertHelper.addTarget(mainColumn);
+ }
+ }
+
+ @Override
+ protected void updateTransformedTypes() {
+ // This also clears the existing types
+ super.updateTransformedTypes();
+ if (mTitle != null) {
+ mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE,
+ mTitle);
+ }
+ if (mText != null) {
+ mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT,
+ mText);
+ }
+ if (mPicture != null) {
+ mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_IMAGE,
+ mPicture);
+ }
+ if (mProgressBar != null) {
+ mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_PROGRESS,
+ mProgressBar);
+ }
+ }
+
+ @Override
+ public void setDark(boolean dark, boolean fade, long delay) {
+ if (dark == mDark && mDarkInitialized) {
+ return;
+ }
+ super.setDark(dark, fade, delay);
+ setPictureDark(dark, fade, delay);
+ setProgressBarDark(dark, fade, delay);
+ }
+
+ private void setProgressBarDark(boolean dark, boolean fade, long delay) {
+ if (mProgressBar != null) {
+ if (fade) {
+ fadeProgressDark(mProgressBar, dark, delay);
+ } else {
+ updateProgressDark(mProgressBar, dark);
+ }
+ }
+ }
+
+ private void fadeProgressDark(final ProgressBar target, final boolean dark, long delay) {
+ getDozer().startIntensityAnimation(animation -> {
+ float t = (float) animation.getAnimatedValue();
+ updateProgressDark(target, t);
+ }, dark, delay, null /* listener */);
+ }
+
+ private void updateProgressDark(ProgressBar target, float intensity) {
+ int color = interpolateColor(mColor, mDarkProgressTint, intensity);
+ target.getIndeterminateDrawable().mutate().setTint(color);
+ target.getProgressDrawable().mutate().setTint(color);
+ }
+
+ private void updateProgressDark(ProgressBar target, boolean dark) {
+ updateProgressDark(target, dark ? 1f : 0f);
+ }
+
+ private void setPictureDark(boolean dark, boolean fade, long delay) {
+ if (mPicture != null) {
+ getDozer().setImageDark(mPicture, dark, fade, delay, true /* useGrayscale */);
+ }
+ }
+
+ 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));
+ }
+
+ @Override
+ public void setContentHeight(int contentHeight, int minHeightHint) {
+ super.setContentHeight(contentHeight, minHeightHint);
+
+ mContentHeight = contentHeight;
+ mMinHeightHint = minHeightHint;
+ updateActionOffset();
+ }
+
+ private void updateActionOffset() {
+ if (mActionsContainer != null) {
+ // We should never push the actions higher than they are in the headsup view.
+ int constrainedContentHeight = Math.max(mContentHeight, mMinHeightHint);
+ mActionsContainer.setTranslationY(constrainedContentHeight - mView.getHeight());
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationUtils.java b/com/android/systemui/statusbar/notification/NotificationUtils.java
new file mode 100644
index 0000000..3115361
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationUtils.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 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.notification;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
+
+/**
+ * A util class for various reusable functions
+ */
+public class NotificationUtils {
+ private static final int[] sLocationBase = new int[2];
+ private static final int[] sLocationOffset = new int[2];
+ public static boolean isGrayscale(ImageView v, NotificationColorUtil colorUtil) {
+ Object isGrayscale = v.getTag(R.id.icon_is_grayscale);
+ if (isGrayscale != null) {
+ return Boolean.TRUE.equals(isGrayscale);
+ }
+ boolean grayscale = colorUtil.isGrayscaleIcon(v.getDrawable());
+ v.setTag(R.id.icon_is_grayscale, grayscale);
+ return grayscale;
+ }
+
+ public static float interpolate(float start, float end, float amount) {
+ return start * (1.0f - amount) + end * amount;
+ }
+
+ public static int interpolateColors(int startColor, int endColor, float amount) {
+ return Color.argb(
+ (int) interpolate(Color.alpha(startColor), Color.alpha(endColor), amount),
+ (int) interpolate(Color.red(startColor), Color.red(endColor), amount),
+ (int) interpolate(Color.green(startColor), Color.green(endColor), amount),
+ (int) interpolate(Color.blue(startColor), Color.blue(endColor), amount));
+ }
+
+ public static float getRelativeYOffset(View offsetView, View baseView) {
+ baseView.getLocationOnScreen(sLocationBase);
+ offsetView.getLocationOnScreen(sLocationOffset);
+ return sLocationOffset[1] - sLocationBase[1];
+ }
+
+ public static boolean isHapticFeedbackDisabled(Context context) {
+ return Settings.System.getIntForUser(context.getContentResolver(),
+ Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) == 0;
+ }
+
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
new file mode 100644
index 0000000..5200d69
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
@@ -0,0 +1,193 @@
+/*
+ * 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.notification;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.v4.graphics.ColorUtils;
+import android.view.NotificationHeaderView;
+import android.view.View;
+
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+
+/**
+ * Wraps the actual notification content view; used to implement behaviors which are different for
+ * the individual templates and custom views.
+ */
+public abstract class NotificationViewWrapper implements TransformableView {
+
+ protected final View mView;
+ protected final ExpandableNotificationRow mRow;
+ private final NotificationDozeHelper mDozer;
+
+ protected boolean mDark;
+ private int mBackgroundColor = 0;
+ protected boolean mShouldInvertDark;
+ protected boolean mDarkInitialized = false;
+
+ public static NotificationViewWrapper wrap(Context ctx, View v, ExpandableNotificationRow row) {
+ if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) {
+ if ("bigPicture".equals(v.getTag())) {
+ return new NotificationBigPictureTemplateViewWrapper(ctx, v, row);
+ } else if ("bigText".equals(v.getTag())) {
+ return new NotificationBigTextTemplateViewWrapper(ctx, v, row);
+ } else if ("media".equals(v.getTag()) || "bigMediaNarrow".equals(v.getTag())) {
+ return new NotificationMediaTemplateViewWrapper(ctx, v, row);
+ } else if ("messaging".equals(v.getTag())) {
+ return new NotificationMessagingTemplateViewWrapper(ctx, v, row);
+ }
+ return new NotificationTemplateViewWrapper(ctx, v, row);
+ } else if (v instanceof NotificationHeaderView) {
+ return new NotificationHeaderViewWrapper(ctx, v, row);
+ } else {
+ return new NotificationCustomViewWrapper(ctx, v, row);
+ }
+ }
+
+ protected NotificationViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
+ mView = view;
+ mRow = row;
+ mDozer = createDozer(ctx);
+ onReinflated();
+ }
+
+ protected NotificationDozeHelper createDozer(Context ctx) {
+ return new NotificationDozeHelper();
+ }
+
+ protected NotificationDozeHelper getDozer() {
+ return mDozer;
+ }
+
+ /**
+ * 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 void setDark(boolean dark, boolean fade, long delay) {
+ mDark = dark;
+ mDarkInitialized = true;
+ }
+
+ /**
+ * Notifies this wrapper that the content of the view might have changed.
+ * @param row the row this wrapper is attached to
+ */
+ public void onContentUpdated(ExpandableNotificationRow row) {
+ mDarkInitialized = false;
+ }
+
+ public void onReinflated() {
+ if (shouldClearBackgroundOnReapply()) {
+ mBackgroundColor = 0;
+ }
+ Drawable background = mView.getBackground();
+ if (background instanceof ColorDrawable) {
+ mBackgroundColor = ((ColorDrawable) background).getColor();
+ mView.setBackground(null);
+ }
+ mShouldInvertDark = mBackgroundColor == 0 || isColorLight(mBackgroundColor);
+ }
+
+ protected boolean shouldClearBackgroundOnReapply() {
+ return true;
+ }
+
+ private boolean isColorLight(int backgroundColor) {
+ return Color.alpha(backgroundColor) == 0
+ || ColorUtils.calculateLuminance(backgroundColor) > 0.5;
+ }
+
+ /**
+ * Update the appearance of the expand button.
+ *
+ * @param expandable should this view be expandable
+ * @param onClickListener the listener to invoke when the expand affordance is clicked on
+ */
+ public void updateExpandability(boolean expandable, View.OnClickListener onClickListener) {}
+
+ /**
+ * @return the notification header if it exists
+ */
+ public NotificationHeaderView getNotificationHeader() {
+ return null;
+ }
+
+ @Override
+ public TransformState getCurrentState(int fadingView) {
+ return null;
+ }
+
+ @Override
+ public void transformTo(TransformableView notification, Runnable endRunnable) {
+ // By default we are fading out completely
+ CrossFadeHelper.fadeOut(mView, endRunnable);
+ }
+
+ @Override
+ public void transformTo(TransformableView notification, float transformationAmount) {
+ CrossFadeHelper.fadeOut(mView, transformationAmount);
+ }
+
+ @Override
+ public void transformFrom(TransformableView notification) {
+ // By default we are fading in completely
+ CrossFadeHelper.fadeIn(mView);
+ }
+
+ @Override
+ public void transformFrom(TransformableView notification, float transformationAmount) {
+ CrossFadeHelper.fadeIn(mView, transformationAmount);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ mView.animate().cancel();
+ mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ public int getCustomBackgroundColor() {
+ // Parent notifications should always use the normal background color
+ return mRow.isSummaryWithChildren() ? 0 : mBackgroundColor;
+ }
+
+ public void setLegacy(boolean legacy) {
+ }
+
+ public void setContentHeight(int contentHeight, int minHeightHint) {
+ }
+
+ public void setRemoteInputVisible(boolean visible) {
+ }
+
+ public void setIsChildInGroup(boolean isChildInGroup) {
+ }
+
+ public boolean isDimmable() {
+ return true;
+ }
+
+ public boolean disallowSingleClick(float x, float y) {
+ return false;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/ProgressTransformState.java b/com/android/systemui/statusbar/notification/ProgressTransformState.java
new file mode 100644
index 0000000..bf78194
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/ProgressTransformState.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.util.Pools;
+
+/**
+ * A transform state of a progress view.
+*/
+public class ProgressTransformState extends TransformState {
+
+ private static Pools.SimplePool<ProgressTransformState> sInstancePool
+ = new Pools.SimplePool<>(40);
+
+ @Override
+ protected boolean sameAs(TransformState otherState) {
+ if (otherState instanceof ProgressTransformState) {
+ return true;
+ }
+ return super.sameAs(otherState);
+ }
+
+ public static ProgressTransformState obtain() {
+ ProgressTransformState instance = sInstancePool.acquire();
+ if (instance != null) {
+ return instance;
+ }
+ return new ProgressTransformState();
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ sInstancePool.release(this);
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/PropertyAnimator.java b/com/android/systemui/statusbar/notification/PropertyAnimator.java
new file mode 100644
index 0000000..80ba943
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/PropertyAnimator.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.stack.AnimationFilter;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ViewState;
+
+/**
+ * An animator to animate properties
+ */
+public class PropertyAnimator {
+
+ public static <T extends View> void startAnimation(final T view,
+ AnimatableProperty animatableProperty, float newEndValue,
+ AnimationProperties properties) {
+ Property<T, Float> property = animatableProperty.getProperty();
+ int animationStartTag = animatableProperty.getAnimationStartTag();
+ int animationEndTag = animatableProperty.getAnimationEndTag();
+ Float previousStartValue = ViewState.getChildTag(view, animationStartTag);
+ Float previousEndValue = ViewState.getChildTag(view, animationEndTag);
+ if (previousEndValue != null && previousEndValue == newEndValue) {
+ return;
+ }
+ int animatorTag = animatableProperty.getAnimatorTag();
+ ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag);
+ AnimationFilter filter = properties.getAnimationFilter();
+ if (!filter.shouldAnimateProperty(property)) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ float relativeDiff = newEndValue - previousEndValue;
+ float newStartValue = previousStartValue + relativeDiff;
+ values[0].setFloatValues(newStartValue, newEndValue);
+ view.setTag(animationStartTag, newStartValue);
+ view.setTag(animationEndTag, newEndValue);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ property.set(view, newEndValue);
+ return;
+ }
+ }
+
+ Float currentValue = property.get(view);
+ ValueAnimator animator = ValueAnimator.ofFloat(currentValue, newEndValue);
+ animator.addUpdateListener(
+ animation -> property.set(view, (Float) animation.getAnimatedValue()));
+ Interpolator customInterpolator = properties.getCustomInterpolator(view, property);
+ Interpolator interpolator = customInterpolator != null ? customInterpolator
+ : Interpolators.FAST_OUT_SLOW_IN;
+ animator.setInterpolator(interpolator);
+ long newDuration = ViewState.cancelAnimatorAndGetNewDuration(properties.duration,
+ previousAnimator);
+ animator.setDuration(newDuration);
+ if (properties.delay > 0 && (previousAnimator == null
+ || previousAnimator.getAnimatedFraction() == 0)) {
+ animator.setStartDelay(properties.delay);
+ }
+ AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setTag(animatorTag, null);
+ view.setTag(animationStartTag, null);
+ view.setTag(animationEndTag, null);
+ }
+ });
+ ViewState.startAnimator(animator, listener);
+ view.setTag(animatorTag, animator);
+ view.setTag(animationStartTag, currentValue);
+ view.setTag(animationEndTag, newEndValue);
+ }
+
+ public interface AnimatableProperty {
+ int getAnimationStartTag();
+ int getAnimationEndTag();
+ int getAnimatorTag();
+ Property getProperty();
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/RowInflaterTask.java b/com/android/systemui/statusbar/notification/RowInflaterTask.java
new file mode 100644
index 0000000..3491f81
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/RowInflaterTask.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.content.Context;
+import android.support.v4.view.AsyncLayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.InflationTask;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+
+/**
+ * An inflater task that asynchronously inflates a ExpandableNotificationRow
+ */
+public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInflateFinishedListener {
+ private RowInflationFinishedListener mListener;
+ private NotificationData.Entry mEntry;
+ private boolean mCancelled;
+
+ /**
+ * Inflates a new notificationView. This should not be called twice on this object
+ */
+ public void inflate(Context context, ViewGroup parent, NotificationData.Entry entry,
+ RowInflationFinishedListener listener) {
+ mListener = listener;
+ AsyncLayoutInflater inflater = new AsyncLayoutInflater(context);
+ mEntry = entry;
+ entry.setInflationTask(this);
+ inflater.inflate(R.layout.status_bar_notification_row, parent, this);
+ }
+
+ @Override
+ public void abort() {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onInflateFinished(View view, int resid, ViewGroup parent) {
+ if (!mCancelled) {
+ mEntry.onInflationTaskFinished();
+ mListener.onInflationFinished((ExpandableNotificationRow) view);
+ }
+ }
+
+ public interface RowInflationFinishedListener {
+ void onInflationFinished(ExpandableNotificationRow row);
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/TextViewTransformState.java b/com/android/systemui/statusbar/notification/TextViewTransformState.java
new file mode 100644
index 0000000..c4aabe4
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/TextViewTransformState.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.text.Layout;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.Pools;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A transform state of a mText view.
+*/
+public class TextViewTransformState extends TransformState {
+
+ private static Pools.SimplePool<TextViewTransformState> sInstancePool
+ = new Pools.SimplePool<>(40);
+ private TextView mText;
+
+ @Override
+ public void initFrom(View view) {
+ super.initFrom(view);
+ if (view instanceof TextView) {
+ mText = (TextView) view;
+ }
+ }
+
+ @Override
+ protected boolean sameAs(TransformState otherState) {
+ if (super.sameAs(otherState)) {
+ return true;
+ }
+ if (otherState instanceof TextViewTransformState) {
+ TextViewTransformState otherTvs = (TextViewTransformState) otherState;
+ if(TextUtils.equals(otherTvs.mText.getText(), mText.getText())) {
+ int ownEllipsized = getEllipsisCount();
+ int otherEllipsized = otherTvs.getEllipsisCount();
+ return ownEllipsized == otherEllipsized
+ && mText.getLineCount() == otherTvs.mText.getLineCount()
+ && hasSameSpans(otherTvs);
+ }
+ }
+ return false;
+ }
+
+ private boolean hasSameSpans(TextViewTransformState otherTvs) {
+ boolean hasSpans = mText instanceof Spanned;
+ boolean otherHasSpans = otherTvs.mText instanceof Spanned;
+ if (hasSpans != otherHasSpans) {
+ return false;
+ } else if (!hasSpans) {
+ return true;
+ }
+ // Actually both have spans, let's try to compare them
+ Spanned ownSpanned = (Spanned) mText;
+ Object[] spans = ownSpanned.getSpans(0, ownSpanned.length(), Object.class);
+ Spanned otherSpanned = (Spanned) otherTvs.mText;
+ Object[] otherSpans = otherSpanned.getSpans(0, otherSpanned.length(), Object.class);
+ if (spans.length != otherSpans.length) {
+ return false;
+ }
+ for (int i = 0; i < spans.length; i++) {
+ Object span = spans[i];
+ Object otherSpan = otherSpans[i];
+ if (!span.getClass().equals(otherSpan.getClass())) {
+ return false;
+ }
+ if (ownSpanned.getSpanStart(span) != otherSpanned.getSpanStart(otherSpan)
+ || ownSpanned.getSpanEnd(span) != otherSpanned.getSpanEnd(otherSpan)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean transformScale(TransformState otherState) {
+ if (!(otherState instanceof TextViewTransformState)) {
+ return false;
+ }
+ TextViewTransformState otherTvs = (TextViewTransformState) otherState;
+ int lineCount = mText.getLineCount();
+ return lineCount == 1 && lineCount == otherTvs.mText.getLineCount()
+ && getEllipsisCount() == otherTvs.getEllipsisCount()
+ && getViewHeight() != otherTvs.getViewHeight();
+ }
+
+ @Override
+ protected int getViewWidth() {
+ Layout l = mText.getLayout();
+ if (l != null) {
+ return (int) l.getLineWidth(0);
+ }
+ return super.getViewWidth();
+ }
+
+ @Override
+ protected int getViewHeight() {
+ return mText.getLineHeight();
+ }
+
+ private int getInnerHeight(TextView text) {
+ return text.getHeight() - text.getPaddingTop() - text.getPaddingBottom();
+ }
+
+ private int getEllipsisCount() {
+ Layout l = mText.getLayout();
+ if (l != null) {
+ int lines = l.getLineCount();
+ if (lines > 0) {
+ // we only care about the first line
+ return l.getEllipsisCount(0);
+ }
+ }
+ return 0;
+ }
+
+ public static TextViewTransformState obtain() {
+ TextViewTransformState instance = sInstancePool.acquire();
+ if (instance != null) {
+ return instance;
+ }
+ return new TextViewTransformState();
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ sInstancePool.release(this);
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ mText = null;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/TransformState.java b/com/android/systemui/statusbar/notification/TransformState.java
new file mode 100644
index 0000000..bafedff
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/TransformState.java
@@ -0,0 +1,581 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.util.ArraySet;
+import android.util.Pools;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+
+/**
+ * A transform state of a view.
+*/
+public class TransformState {
+
+ public static final int TRANSFORM_X = 0x1;
+ public static final int TRANSFORM_Y = 0x10;
+ public static final int TRANSFORM_ALL = TRANSFORM_X | TRANSFORM_Y;
+
+ private static final float UNDEFINED = -1f;
+ private static final int CLIP_CLIPPING_SET = R.id.clip_children_set_tag;
+ private static final int CLIP_CHILDREN_TAG = R.id.clip_children_tag;
+ private static final int CLIP_TO_PADDING = R.id.clip_to_padding_tag;
+ private static final int TRANSFORMATION_START_X = R.id.transformation_start_x_tag;
+ private static final int TRANSFORMATION_START_Y = R.id.transformation_start_y_tag;
+ private static final int TRANSFORMATION_START_SCLALE_X = R.id.transformation_start_scale_x_tag;
+ private static final int TRANSFORMATION_START_SCLALE_Y = R.id.transformation_start_scale_y_tag;
+ private static Pools.SimplePool<TransformState> sInstancePool = new Pools.SimplePool<>(40);
+
+ protected View mTransformedView;
+ private int[] mOwnPosition = new int[2];
+ private boolean mSameAsAny;
+ private float mTransformationEndY = UNDEFINED;
+ private float mTransformationEndX = UNDEFINED;
+
+ public void initFrom(View view) {
+ mTransformedView = view;
+ }
+
+ /**
+ * Transforms the {@link #mTransformedView} from the given transformviewstate
+ * @param otherState the state to transform from
+ * @param transformationAmount how much to transform
+ */
+ public void transformViewFrom(TransformState otherState, float transformationAmount) {
+ mTransformedView.animate().cancel();
+ if (sameAs(otherState)) {
+ if (mTransformedView.getVisibility() == View.INVISIBLE
+ || mTransformedView.getAlpha() != 1.0f) {
+ // We have the same content, lets show ourselves
+ mTransformedView.setAlpha(1.0f);
+ mTransformedView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
+ }
+ transformViewFullyFrom(otherState, transformationAmount);
+ }
+
+ public void transformViewFullyFrom(TransformState otherState, float transformationAmount) {
+ transformViewFrom(otherState, TRANSFORM_ALL, null, transformationAmount);
+ }
+
+ public void transformViewFullyFrom(TransformState otherState,
+ ViewTransformationHelper.CustomTransformation customTransformation,
+ float transformationAmount) {
+ transformViewFrom(otherState, TRANSFORM_ALL, customTransformation, transformationAmount);
+ }
+
+ public void transformViewVerticalFrom(TransformState otherState,
+ ViewTransformationHelper.CustomTransformation customTransformation,
+ float transformationAmount) {
+ transformViewFrom(otherState, TRANSFORM_Y, customTransformation, transformationAmount);
+ }
+
+ public void transformViewVerticalFrom(TransformState otherState, float transformationAmount) {
+ transformViewFrom(otherState, TRANSFORM_Y, null, transformationAmount);
+ }
+
+ private void transformViewFrom(TransformState otherState, int transformationFlags,
+ ViewTransformationHelper.CustomTransformation customTransformation,
+ float transformationAmount) {
+ final View transformedView = mTransformedView;
+ boolean transformX = (transformationFlags & TRANSFORM_X) != 0;
+ boolean transformY = (transformationFlags & TRANSFORM_Y) != 0;
+ boolean transformScale = transformScale(otherState);
+ // lets animate the positions correctly
+ if (transformationAmount == 0.0f
+ || transformX && getTransformationStartX() == UNDEFINED
+ || transformY && getTransformationStartY() == UNDEFINED
+ || transformScale && getTransformationStartScaleX() == UNDEFINED
+ || transformScale && getTransformationStartScaleY() == UNDEFINED) {
+ int[] otherPosition;
+ if (transformationAmount != 0.0f) {
+ otherPosition = otherState.getLaidOutLocationOnScreen();
+ } else {
+ otherPosition = otherState.getLocationOnScreen();
+ }
+ int[] ownStablePosition = getLaidOutLocationOnScreen();
+ if (customTransformation == null
+ || !customTransformation.initTransformation(this, otherState)) {
+ if (transformX) {
+ setTransformationStartX(otherPosition[0] - ownStablePosition[0]);
+ }
+ if (transformY) {
+ setTransformationStartY(otherPosition[1] - ownStablePosition[1]);
+ }
+ // we also want to animate the scale if we're the same
+ View otherView = otherState.getTransformedView();
+ if (transformScale && otherState.getViewWidth() != getViewWidth()) {
+ setTransformationStartScaleX(otherState.getViewWidth() * otherView.getScaleX()
+ / (float) getViewWidth());
+ transformedView.setPivotX(0);
+ } else {
+ setTransformationStartScaleX(UNDEFINED);
+ }
+ if (transformScale && otherState.getViewHeight() != getViewHeight()) {
+ setTransformationStartScaleY(otherState.getViewHeight() * otherView.getScaleY()
+ / (float) getViewHeight());
+ transformedView.setPivotY(0);
+ } else {
+ setTransformationStartScaleY(UNDEFINED);
+ }
+ }
+ if (!transformX) {
+ setTransformationStartX(UNDEFINED);
+ }
+ if (!transformY) {
+ setTransformationStartY(UNDEFINED);
+ }
+ if (!transformScale) {
+ setTransformationStartScaleX(UNDEFINED);
+ setTransformationStartScaleY(UNDEFINED);
+ }
+ setClippingDeactivated(transformedView, true);
+ }
+ float interpolatedValue = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+ transformationAmount);
+ if (transformX) {
+ float interpolation = interpolatedValue;
+ if (customTransformation != null) {
+ Interpolator customInterpolator =
+ customTransformation.getCustomInterpolator(TRANSFORM_X, true /* isFrom */);
+ if (customInterpolator != null) {
+ interpolation = customInterpolator.getInterpolation(transformationAmount);
+ }
+ }
+ transformedView.setTranslationX(NotificationUtils.interpolate(getTransformationStartX(),
+ 0.0f,
+ interpolation));
+ }
+ if (transformY) {
+ float interpolation = interpolatedValue;
+ if (customTransformation != null) {
+ Interpolator customInterpolator =
+ customTransformation.getCustomInterpolator(TRANSFORM_Y, true /* isFrom */);
+ if (customInterpolator != null) {
+ interpolation = customInterpolator.getInterpolation(transformationAmount);
+ }
+ }
+ transformedView.setTranslationY(NotificationUtils.interpolate(getTransformationStartY(),
+ 0.0f,
+ interpolation));
+ }
+ if (transformScale) {
+ float transformationStartScaleX = getTransformationStartScaleX();
+ if (transformationStartScaleX != UNDEFINED) {
+ transformedView.setScaleX(
+ NotificationUtils.interpolate(transformationStartScaleX,
+ 1.0f,
+ interpolatedValue));
+ }
+ float transformationStartScaleY = getTransformationStartScaleY();
+ if (transformationStartScaleY != UNDEFINED) {
+ transformedView.setScaleY(
+ NotificationUtils.interpolate(transformationStartScaleY,
+ 1.0f,
+ interpolatedValue));
+ }
+ }
+ }
+
+ protected int getViewWidth() {
+ return mTransformedView.getWidth();
+ }
+
+ protected int getViewHeight() {
+ return mTransformedView.getHeight();
+ }
+
+ protected boolean transformScale(TransformState otherState) {
+ return false;
+ }
+
+ /**
+ * Transforms the {@link #mTransformedView} to the given transformviewstate
+ * @param otherState the state to transform from
+ * @param transformationAmount how much to transform
+ * @return whether an animation was started
+ */
+ public boolean transformViewTo(TransformState otherState, float transformationAmount) {
+ mTransformedView.animate().cancel();
+ if (sameAs(otherState)) {
+ // We have the same text, lets show ourselfs
+ if (mTransformedView.getVisibility() == View.VISIBLE) {
+ mTransformedView.setAlpha(0.0f);
+ mTransformedView.setVisibility(View.INVISIBLE);
+ }
+ return false;
+ } else {
+ CrossFadeHelper.fadeOut(mTransformedView, transformationAmount);
+ }
+ transformViewFullyTo(otherState, transformationAmount);
+ return true;
+ }
+
+ public void transformViewFullyTo(TransformState otherState, float transformationAmount) {
+ transformViewTo(otherState, TRANSFORM_ALL, null, transformationAmount);
+ }
+
+ public void transformViewFullyTo(TransformState otherState,
+ ViewTransformationHelper.CustomTransformation customTransformation,
+ float transformationAmount) {
+ transformViewTo(otherState, TRANSFORM_ALL, customTransformation, transformationAmount);
+ }
+
+ public void transformViewVerticalTo(TransformState otherState,
+ ViewTransformationHelper.CustomTransformation customTransformation,
+ float transformationAmount) {
+ transformViewTo(otherState, TRANSFORM_Y, customTransformation, transformationAmount);
+ }
+
+ public void transformViewVerticalTo(TransformState otherState, float transformationAmount) {
+ transformViewTo(otherState, TRANSFORM_Y, null, transformationAmount);
+ }
+
+ private void transformViewTo(TransformState otherState, int transformationFlags,
+ ViewTransformationHelper.CustomTransformation customTransformation,
+ float transformationAmount) {
+ // lets animate the positions correctly
+
+ final View transformedView = mTransformedView;
+ boolean transformX = (transformationFlags & TRANSFORM_X) != 0;
+ boolean transformY = (transformationFlags & TRANSFORM_Y) != 0;
+ boolean transformScale = transformScale(otherState);
+ // lets animate the positions correctly
+ if (transformationAmount == 0.0f) {
+ if (transformX) {
+ float transformationStartX = getTransformationStartX();
+ float start = transformationStartX != UNDEFINED ? transformationStartX
+ : transformedView.getTranslationX();
+ setTransformationStartX(start);
+ }
+ if (transformY) {
+ float transformationStartY = getTransformationStartY();
+ float start = transformationStartY != UNDEFINED ? transformationStartY
+ : transformedView.getTranslationY();
+ setTransformationStartY(start);
+ }
+ View otherView = otherState.getTransformedView();
+ if (transformScale && otherState.getViewWidth() != getViewWidth()) {
+ setTransformationStartScaleX(transformedView.getScaleX());
+ transformedView.setPivotX(0);
+ } else {
+ setTransformationStartScaleX(UNDEFINED);
+ }
+ if (transformScale && otherState.getViewHeight() != getViewHeight()) {
+ setTransformationStartScaleY(transformedView.getScaleY());
+ transformedView.setPivotY(0);
+ } else {
+ setTransformationStartScaleY(UNDEFINED);
+ }
+ setClippingDeactivated(transformedView, true);
+ }
+ float interpolatedValue = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+ transformationAmount);
+ int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
+ int[] ownPosition = getLaidOutLocationOnScreen();
+ if (transformX) {
+ float endX = otherStablePosition[0] - ownPosition[0];
+ float interpolation = interpolatedValue;
+ if (customTransformation != null) {
+ if (customTransformation.customTransformTarget(this, otherState)) {
+ endX = mTransformationEndX;
+ }
+ Interpolator customInterpolator =
+ customTransformation.getCustomInterpolator(TRANSFORM_X, false /* isFrom */);
+ if (customInterpolator != null) {
+ interpolation = customInterpolator.getInterpolation(transformationAmount);
+ }
+ }
+ transformedView.setTranslationX(NotificationUtils.interpolate(getTransformationStartX(),
+ endX,
+ interpolation));
+ }
+ if (transformY) {
+ float endY = otherStablePosition[1] - ownPosition[1];
+ float interpolation = interpolatedValue;
+ if (customTransformation != null) {
+ if (customTransformation.customTransformTarget(this, otherState)) {
+ endY = mTransformationEndY;
+ }
+ Interpolator customInterpolator =
+ customTransformation.getCustomInterpolator(TRANSFORM_Y, false /* isFrom */);
+ if (customInterpolator != null) {
+ interpolation = customInterpolator.getInterpolation(transformationAmount);
+ }
+ }
+ transformedView.setTranslationY(NotificationUtils.interpolate(getTransformationStartY(),
+ endY,
+ interpolation));
+ }
+ if (transformScale) {
+ View otherView = otherState.getTransformedView();
+ float transformationStartScaleX = getTransformationStartScaleX();
+ if (transformationStartScaleX != UNDEFINED) {
+ transformedView.setScaleX(
+ NotificationUtils.interpolate(transformationStartScaleX,
+ (otherState.getViewWidth() / (float) getViewWidth()),
+ interpolatedValue));
+ }
+ float transformationStartScaleY = getTransformationStartScaleY();
+ if (transformationStartScaleY != UNDEFINED) {
+ transformedView.setScaleY(
+ NotificationUtils.interpolate(transformationStartScaleY,
+ (otherState.getViewHeight() / (float) getViewHeight()),
+ interpolatedValue));
+ }
+ }
+ }
+
+ public static void setClippingDeactivated(final View transformedView, boolean deactivated) {
+ if (!(transformedView.getParent() instanceof ViewGroup)) {
+ return;
+ }
+ ViewGroup view = (ViewGroup) transformedView.getParent();
+ while (true) {
+ ArraySet<View> clipSet = (ArraySet<View>) view.getTag(CLIP_CLIPPING_SET);
+ if (clipSet == null) {
+ clipSet = new ArraySet<>();
+ view.setTag(CLIP_CLIPPING_SET, clipSet);
+ }
+ Boolean clipChildren = (Boolean) view.getTag(CLIP_CHILDREN_TAG);
+ if (clipChildren == null) {
+ clipChildren = view.getClipChildren();
+ view.setTag(CLIP_CHILDREN_TAG, clipChildren);
+ }
+ Boolean clipToPadding = (Boolean) view.getTag(CLIP_TO_PADDING);
+ if (clipToPadding == null) {
+ clipToPadding = view.getClipToPadding();
+ view.setTag(CLIP_TO_PADDING, clipToPadding);
+ }
+ ExpandableNotificationRow row = view instanceof ExpandableNotificationRow
+ ? (ExpandableNotificationRow) view
+ : null;
+ if (!deactivated) {
+ clipSet.remove(transformedView);
+ if (clipSet.isEmpty()) {
+ view.setClipChildren(clipChildren);
+ view.setClipToPadding(clipToPadding);
+ view.setTag(CLIP_CLIPPING_SET, null);
+ if (row != null) {
+ row.setClipToActualHeight(true);
+ }
+ }
+ } else {
+ clipSet.add(transformedView);
+ view.setClipChildren(false);
+ view.setClipToPadding(false);
+ if (row != null && row.isChildInGroup()) {
+ // We still want to clip to the parent's height
+ row.setClipToActualHeight(false);
+ }
+ }
+ if (row != null && !row.isChildInGroup()) {
+ return;
+ }
+ final ViewParent parent = view.getParent();
+ if (parent instanceof ViewGroup) {
+ view = (ViewGroup) parent;
+ } else {
+ return;
+ }
+ }
+ }
+
+ public int[] getLaidOutLocationOnScreen() {
+ int[] location = getLocationOnScreen();
+ // remove translation
+ location[0] -= mTransformedView.getTranslationX();
+ location[1] -= mTransformedView.getTranslationY();
+ return location;
+ }
+
+ public int[] getLocationOnScreen() {
+ mTransformedView.getLocationOnScreen(mOwnPosition);
+
+ // remove scale
+ mOwnPosition[0] -= (1.0f - mTransformedView.getScaleX()) * mTransformedView.getPivotX();
+ mOwnPosition[1] -= (1.0f - mTransformedView.getScaleY()) * mTransformedView.getPivotY();
+ return mOwnPosition;
+ }
+
+ protected boolean sameAs(TransformState otherState) {
+ return mSameAsAny;
+ }
+
+ public void appear(float transformationAmount, TransformableView otherView) {
+ // There's no other view, lets fade us in
+ // Certain views need to prepare the fade in and make sure its children are
+ // completely visible. An example is the notification header.
+ if (transformationAmount == 0.0f) {
+ prepareFadeIn();
+ }
+ CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
+ }
+
+ public void disappear(float transformationAmount, TransformableView otherView) {
+ CrossFadeHelper.fadeOut(mTransformedView, transformationAmount);
+ }
+
+ public static TransformState createFrom(View view) {
+ if (view instanceof TextView) {
+ TextViewTransformState result = TextViewTransformState.obtain();
+ result.initFrom(view);
+ return result;
+ }
+ if (view.getId() == com.android.internal.R.id.actions_container) {
+ ActionListTransformState result = ActionListTransformState.obtain();
+ result.initFrom(view);
+ return result;
+ }
+ if (view instanceof ImageView) {
+ ImageTransformState result = ImageTransformState.obtain();
+ result.initFrom(view);
+ if (view.getId() == com.android.internal.R.id.reply_icon_action) {
+ ((TransformState) result).setIsSameAsAnyView(true);
+ }
+ return result;
+ }
+ if (view instanceof ProgressBar) {
+ ProgressTransformState result = ProgressTransformState.obtain();
+ result.initFrom(view);
+ return result;
+ }
+ TransformState result = obtain();
+ result.initFrom(view);
+ return result;
+ }
+
+ private void setIsSameAsAnyView(boolean sameAsAny) {
+ mSameAsAny = sameAsAny;
+ }
+
+ public void recycle() {
+ reset();
+ if (getClass() == TransformState.class) {
+ sInstancePool.release(this);
+ }
+ }
+
+ public void setTransformationEndY(float transformationEndY) {
+ mTransformationEndY = transformationEndY;
+ }
+
+ public void setTransformationEndX(float transformationEndX) {
+ mTransformationEndX = transformationEndX;
+ }
+
+ public float getTransformationStartX() {
+ Object tag = mTransformedView.getTag(TRANSFORMATION_START_X);
+ return tag == null ? UNDEFINED : (float) tag;
+ }
+
+ public float getTransformationStartY() {
+ Object tag = mTransformedView.getTag(TRANSFORMATION_START_Y);
+ return tag == null ? UNDEFINED : (float) tag;
+ }
+
+ public float getTransformationStartScaleX() {
+ Object tag = mTransformedView.getTag(TRANSFORMATION_START_SCLALE_X);
+ return tag == null ? UNDEFINED : (float) tag;
+ }
+
+ public float getTransformationStartScaleY() {
+ Object tag = mTransformedView.getTag(TRANSFORMATION_START_SCLALE_Y);
+ return tag == null ? UNDEFINED : (float) tag;
+ }
+
+ public void setTransformationStartX(float transformationStartX) {
+ mTransformedView.setTag(TRANSFORMATION_START_X, transformationStartX);
+ }
+
+ public void setTransformationStartY(float transformationStartY) {
+ mTransformedView.setTag(TRANSFORMATION_START_Y, transformationStartY);
+ }
+
+ private void setTransformationStartScaleX(float startScaleX) {
+ mTransformedView.setTag(TRANSFORMATION_START_SCLALE_X, startScaleX);
+ }
+
+ private void setTransformationStartScaleY(float startScaleY) {
+ mTransformedView.setTag(TRANSFORMATION_START_SCLALE_Y, startScaleY);
+ }
+
+ protected void reset() {
+ mTransformedView = null;
+ mSameAsAny = false;
+ mTransformationEndX = UNDEFINED;
+ mTransformationEndY = UNDEFINED;
+ }
+
+ public void setVisible(boolean visible, boolean force) {
+ if (!force && mTransformedView.getVisibility() == View.GONE) {
+ return;
+ }
+ if (mTransformedView.getVisibility() != View.GONE) {
+ mTransformedView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ }
+ mTransformedView.animate().cancel();
+ mTransformedView.setAlpha(visible ? 1.0f : 0.0f);
+ resetTransformedView();
+ }
+
+ public void prepareFadeIn() {
+ resetTransformedView();
+ }
+
+ protected void resetTransformedView() {
+ mTransformedView.setTranslationX(0);
+ mTransformedView.setTranslationY(0);
+ mTransformedView.setScaleX(1.0f);
+ mTransformedView.setScaleY(1.0f);
+ setClippingDeactivated(mTransformedView, false);
+ abortTransformation();
+ }
+
+ public void abortTransformation() {
+ mTransformedView.setTag(TRANSFORMATION_START_X, UNDEFINED);
+ mTransformedView.setTag(TRANSFORMATION_START_Y, UNDEFINED);
+ mTransformedView.setTag(TRANSFORMATION_START_SCLALE_X, UNDEFINED);
+ mTransformedView.setTag(TRANSFORMATION_START_SCLALE_Y, UNDEFINED);
+ }
+
+ public static TransformState obtain() {
+ TransformState instance = sInstancePool.acquire();
+ if (instance != null) {
+ return instance;
+ }
+ return new TransformState();
+ }
+
+ public View getTransformedView() {
+ return mTransformedView;
+ }
+}
diff --git a/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java b/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
new file mode 100644
index 0000000..4a52acc
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * An object that can determine the visibility of a Notification.
+ */
+public interface VisibilityLocationProvider {
+
+ /**
+ * @return whether the view is in a visible location right now.
+ */
+ boolean isInVisibleLocation(ExpandableNotificationRow row);
+}
diff --git a/com/android/systemui/statusbar/notification/VisualStabilityManager.java b/com/android/systemui/statusbar/notification/VisualStabilityManager.java
new file mode 100644
index 0000000..09aff1b
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/VisualStabilityManager.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.support.v4.util.ArraySet;
+import android.view.View;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+
+import java.util.ArrayList;
+
+/**
+ * A manager that ensures that notifications are visually stable. It will suppress reorderings
+ * and reorder at the right time when they are out of view.
+ */
+public class VisualStabilityManager implements OnHeadsUpChangedListener {
+
+ private final ArrayList<Callback> mCallbacks = new ArrayList<>();
+
+ private boolean mPanelExpanded;
+ private boolean mScreenOn;
+ private boolean mReorderingAllowed;
+ private VisibilityLocationProvider mVisibilityLocationProvider;
+ private ArraySet<View> mAllowedReorderViews = new ArraySet<>();
+ private ArraySet<View> mLowPriorityReorderingViews = new ArraySet<>();
+ private ArraySet<View> mAddedChildren = new ArraySet<>();
+ private boolean mPulsing;
+
+ /**
+ * Add a callback to invoke when reordering is allowed again.
+ * @param callback
+ */
+ public void addReorderingAllowedCallback(Callback callback) {
+ if (mCallbacks.contains(callback)) {
+ return;
+ }
+ mCallbacks.add(callback);
+ }
+
+ /**
+ * Set the panel to be expanded.
+ */
+ public void setPanelExpanded(boolean expanded) {
+ mPanelExpanded = expanded;
+ updateReorderingAllowed();
+ }
+
+ /**
+ * @param screenOn whether the screen is on
+ */
+ public void setScreenOn(boolean screenOn) {
+ mScreenOn = screenOn;
+ updateReorderingAllowed();
+ }
+
+ /**
+ * @param pulsing whether we are currently pulsing for ambient display.
+ */
+ public void setPulsing(boolean pulsing) {
+ if (mPulsing == pulsing) {
+ return;
+ }
+ mPulsing = pulsing;
+ updateReorderingAllowed();
+ }
+
+ private void updateReorderingAllowed() {
+ boolean reorderingAllowed = (!mScreenOn || !mPanelExpanded) && !mPulsing;
+ boolean changed = reorderingAllowed && !mReorderingAllowed;
+ mReorderingAllowed = reorderingAllowed;
+ if (changed) {
+ notifyCallbacks();
+ }
+ }
+
+ private void notifyCallbacks() {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ Callback callback = mCallbacks.get(i);
+ callback.onReorderingAllowed();
+ }
+ mCallbacks.clear();
+ }
+
+ /**
+ * @return whether reordering is currently allowed in general.
+ */
+ public boolean isReorderingAllowed() {
+ return mReorderingAllowed;
+ }
+
+ /**
+ * @return whether a specific notification is allowed to reorder. Certain notifications are
+ * allowed to reorder even if {@link #isReorderingAllowed()} returns false, like newly added
+ * notifications or heads-up notifications that are out of view.
+ */
+ public boolean canReorderNotification(ExpandableNotificationRow row) {
+ if (mReorderingAllowed) {
+ return true;
+ }
+ if (mAddedChildren.contains(row)) {
+ return true;
+ }
+ if (mLowPriorityReorderingViews.contains(row)) {
+ return true;
+ }
+ if (mAllowedReorderViews.contains(row)
+ && !mVisibilityLocationProvider.isInVisibleLocation(row)) {
+ return true;
+ }
+ return false;
+ }
+
+ public void setVisibilityLocationProvider(
+ VisibilityLocationProvider visibilityLocationProvider) {
+ mVisibilityLocationProvider = visibilityLocationProvider;
+ }
+
+ public void onReorderingFinished() {
+ mAllowedReorderViews.clear();
+ mAddedChildren.clear();
+ mLowPriorityReorderingViews.clear();
+ }
+
+ @Override
+ public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+ if (isHeadsUp) {
+ // Heads up notifications should in general be allowed to reorder if they are out of
+ // view and stay at the current location if they aren't.
+ mAllowedReorderViews.add(entry.row);
+ }
+ }
+
+ public void onLowPriorityUpdated(NotificationData.Entry entry) {
+ mLowPriorityReorderingViews.add(entry.row);
+ }
+
+ /**
+ * Notify the visual stability manager that a new view was added and should be allowed to
+ * reorder next time.
+ */
+ public void notifyViewAddition(View view) {
+ mAddedChildren.add(view);
+ }
+
+ public interface Callback {
+ /**
+ * Called when reordering is allowed again.
+ */
+ void onReorderingAllowed();
+ }
+
+}
diff --git a/com/android/systemui/statusbar/phone/AppButtonData.java b/com/android/systemui/statusbar/phone/AppButtonData.java
new file mode 100644
index 0000000..f6c1062
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/AppButtonData.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 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.app.ActivityManager.RecentTaskInfo;
+
+import java.util.ArrayList;
+
+/**
+ * Data associated with an app button.
+ */
+class AppButtonData {
+ public final AppInfo appInfo;
+ public boolean pinned;
+ // Recent tasks for this app, sorted by lastActiveTime, descending.
+ public ArrayList<RecentTaskInfo> tasks;
+
+ public AppButtonData(AppInfo appInfo, boolean pinned) {
+ this.appInfo = appInfo;
+ this.pinned = pinned;
+ }
+
+ public int getTaskCount() {
+ return tasks == null ? 0 : tasks.size();
+ }
+
+ /**
+ * Returns true if the button contains no useful information and should be removed.
+ */
+ public boolean isEmpty() {
+ return !pinned && getTaskCount() == 0;
+ }
+
+ public void addTask(RecentTaskInfo task) {
+ if (tasks == null) {
+ tasks = new ArrayList<RecentTaskInfo>();
+ }
+ tasks.add(task);
+ }
+
+ public void clearTasks() {
+ if (tasks != null) {
+ tasks.clear();
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/AppIconDragShadowBuilder.java b/com/android/systemui/statusbar/phone/AppIconDragShadowBuilder.java
new file mode 100644
index 0000000..b55b0a5
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/AppIconDragShadowBuilder.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 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.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.ImageView;
+
+/** Creates a scaled-up version of an app icon for dragging. */
+class AppIconDragShadowBuilder extends View.DragShadowBuilder {
+ private final static int ICON_SCALE = 2;
+ final Drawable mDrawable;
+ final int mIconSize; // Height and width in device-pixels.
+
+ public AppIconDragShadowBuilder(ImageView icon) {
+ mDrawable = icon.getDrawable();
+ // The Drawable may not be the same size as the ImageView, so use the ImageView size.
+ // The ImageView is not square because it has additional left and right padding to create
+ // a wider drop target, so use the height to create a square drag shadow.
+ mIconSize = icon.getHeight() * ICON_SCALE;
+ }
+
+ @Override
+ public void onProvideShadowMetrics(Point size, Point touch) {
+ size.set(mIconSize, mIconSize);
+ // Shift the drag shadow up slightly because the apps are at the bottom edge of the
+ // screen.
+ touch.set(mIconSize / 2, mIconSize * 2 / 3);
+ }
+
+ @Override
+ public void onDrawShadow(Canvas canvas) {
+ // The Drawable's native bounds may be different than the source ImageView. Force it
+ // to the correct size.
+ Rect oldBounds = mDrawable.copyBounds();
+ mDrawable.setBounds(0, 0, mIconSize, mIconSize);
+ mDrawable.draw(canvas);
+ mDrawable.setBounds(oldBounds);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/AppInfo.java b/com/android/systemui/statusbar/phone/AppInfo.java
new file mode 100644
index 0000000..8f0b532
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/AppInfo.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 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.os.UserHandle;
+
+/**
+ * Navigation bar app information.
+ */
+class AppInfo {
+ private final ComponentName mComponentName;
+ private final UserHandle mUser;
+
+ public AppInfo(ComponentName componentName, UserHandle user) {
+ if (componentName == null || user == null) throw new IllegalArgumentException();
+ mComponentName = componentName;
+ mUser = user;
+ }
+
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ public UserHandle getUser() {
+ return mUser;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final AppInfo other = (AppInfo) obj;
+ return mComponentName.equals(other.mComponentName) && mUser.equals(other.mUser);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/AutoTileManager.java b/com/android/systemui/statusbar/phone/AutoTileManager.java
new file mode 100644
index 0000000..1bd90fa
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016 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.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings.Secure;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.NightDisplayController;
+import com.android.systemui.Dependency;
+import com.android.systemui.Prefs;
+import com.android.systemui.Prefs.Key;
+import com.android.systemui.qs.AutoAddTracker;
+import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.SecureSetting;
+import com.android.systemui.statusbar.policy.DataSaverController;
+import com.android.systemui.statusbar.policy.DataSaverController.Listener;
+import com.android.systemui.statusbar.policy.HotspotController;
+import com.android.systemui.statusbar.policy.HotspotController.Callback;
+
+/**
+ * Manages which tiles should be automatically added to QS.
+ */
+public class AutoTileManager {
+
+ public static final String HOTSPOT = "hotspot";
+ public static final String SAVER = "saver";
+ public static final String INVERSION = "inversion";
+ public static final String WORK = "work";
+ public static final String NIGHT = "night";
+ private final Context mContext;
+ private final QSTileHost mHost;
+ private final Handler mHandler;
+ private final AutoAddTracker mAutoTracker;
+
+ public AutoTileManager(Context context, QSTileHost host) {
+ mAutoTracker = new AutoAddTracker(context);
+ mContext = context;
+ mHost = host;
+ mHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER));
+ if (!mAutoTracker.isAdded(HOTSPOT)) {
+ Dependency.get(HotspotController.class).addCallback(mHotspotCallback);
+ }
+ if (!mAutoTracker.isAdded(SAVER)) {
+ Dependency.get(DataSaverController.class).addCallback(mDataSaverListener);
+ }
+ if (!mAutoTracker.isAdded(INVERSION)) {
+ mColorsSetting = new SecureSetting(mContext, mHandler,
+ Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) {
+ @Override
+ protected void handleValueChanged(int value, boolean observedChange) {
+ if (mAutoTracker.isAdded(INVERSION)) return;
+ if (value != 0) {
+ mHost.addTile(INVERSION);
+ mAutoTracker.setTileAdded(INVERSION);
+ mHandler.post(() -> mColorsSetting.setListening(false));
+ }
+ }
+ };
+ mColorsSetting.setListening(true);
+ }
+ if (!mAutoTracker.isAdded(WORK)) {
+ Dependency.get(ManagedProfileController.class).addCallback(mProfileCallback);
+ }
+
+ if (!mAutoTracker.isAdded(NIGHT)
+ && NightDisplayController.isAvailable(mContext)) {
+ Dependency.get(NightDisplayController.class).setListener(mNightDisplayCallback);
+ }
+ }
+
+ public void destroy() {
+ mColorsSetting.setListening(false);
+ mAutoTracker.destroy();
+ Dependency.get(HotspotController.class).removeCallback(mHotspotCallback);
+ Dependency.get(DataSaverController.class).removeCallback(mDataSaverListener);
+ Dependency.get(ManagedProfileController.class).removeCallback(mProfileCallback);
+ Dependency.get(NightDisplayController.class).setListener(null);
+ }
+
+ private final ManagedProfileController.Callback mProfileCallback =
+ new ManagedProfileController.Callback() {
+ @Override
+ public void onManagedProfileChanged() {
+ if (mAutoTracker.isAdded(WORK)) return;
+ if (Dependency.get(ManagedProfileController.class).hasActiveProfile()) {
+ mHost.addTile(WORK);
+ mAutoTracker.setTileAdded(WORK);
+ mHandler.post(() -> Dependency.get(ManagedProfileController.class)
+ .removeCallback(mProfileCallback));
+ }
+ }
+
+ @Override
+ public void onManagedProfileRemoved() {
+ }
+ };
+
+ private SecureSetting mColorsSetting;
+
+ private final DataSaverController.Listener mDataSaverListener = new Listener() {
+ @Override
+ public void onDataSaverChanged(boolean isDataSaving) {
+ if (mAutoTracker.isAdded(SAVER)) return;
+ if (isDataSaving) {
+ mHost.addTile(SAVER);
+ mAutoTracker.setTileAdded(SAVER);
+ mHandler.post(() -> Dependency.get(DataSaverController.class).removeCallback(
+ mDataSaverListener));
+ }
+ }
+ };
+
+ private final HotspotController.Callback mHotspotCallback = new Callback() {
+ @Override
+ public void onHotspotChanged(boolean enabled) {
+ if (mAutoTracker.isAdded(HOTSPOT)) return;
+ if (enabled) {
+ mHost.addTile(HOTSPOT);
+ mAutoTracker.setTileAdded(HOTSPOT);
+ mHandler.post(() -> Dependency.get(HotspotController.class)
+ .removeCallback(mHotspotCallback));
+ }
+ }
+ };
+
+ @VisibleForTesting
+ final NightDisplayController.Callback mNightDisplayCallback =
+ new NightDisplayController.Callback() {
+ @Override
+ public void onActivated(boolean activated) {
+ if (activated) {
+ addNightTile();
+ }
+ }
+
+ @Override
+ public void onAutoModeChanged(int autoMode) {
+ if (autoMode == NightDisplayController.AUTO_MODE_CUSTOM
+ || autoMode == NightDisplayController.AUTO_MODE_TWILIGHT) {
+ addNightTile();
+ }
+ }
+
+ private void addNightTile() {
+ if (mAutoTracker.isAdded(NIGHT)) return;
+ mHost.addTile(NIGHT);
+ mAutoTracker.setTileAdded(NIGHT);
+ mHandler.post(() -> Dependency.get(NightDisplayController.class)
+ .setListener(null));
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/phone/BarTransitions.java b/com/android/systemui/statusbar/phone/BarTransitions.java
new file mode 100644
index 0000000..1f44abe
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/BarTransitions.java
@@ -0,0 +1,288 @@
+/*
+ * 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.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.View;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+public class BarTransitions {
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_COLORS = false;
+
+ public static final int MODE_OPAQUE = 0;
+ public static final int MODE_SEMI_TRANSPARENT = 1;
+ public static final int MODE_TRANSLUCENT = 2;
+ 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;
+ public static final int BACKGROUND_DURATION = 200;
+
+ private final String mTag;
+ private final View mView;
+ private final BarBackgroundDrawable mBarBackground;
+
+ private int mMode;
+ private boolean mAlwaysOpaque = false;
+
+ public BarTransitions(View view, int gradientResourceId) {
+ mTag = "BarTransitions." + view.getClass().getSimpleName();
+ mView = view;
+ mBarBackground = new BarBackgroundDrawable(mView.getContext(), gradientResourceId);
+ mView.setBackground(mBarBackground);
+ }
+
+ public int getMode() {
+ return mMode;
+ }
+
+ public void setAutoDim(boolean autoDim) {
+ // Default is don't care.
+ }
+
+ /**
+ * @param alwaysOpaque if {@code true}, the bar's background will always be opaque, regardless
+ * of what mode it is currently set to.
+ */
+ public void setAlwaysOpaque(boolean alwaysOpaque) {
+ mAlwaysOpaque = alwaysOpaque;
+ }
+
+ public boolean isAlwaysOpaque() {
+ // Low-end devices do not support translucent modes, fallback to opaque
+ return mAlwaysOpaque;
+ }
+
+ public void transitionTo(int mode, boolean animate) {
+ if (isAlwaysOpaque() && (mode == MODE_SEMI_TRANSPARENT || mode == MODE_TRANSLUCENT
+ || mode == MODE_TRANSPARENT)) {
+ mode = MODE_OPAQUE;
+ }
+ if (isAlwaysOpaque() && (mode == MODE_LIGHTS_OUT_TRANSPARENT)) {
+ mode = MODE_LIGHTS_OUT;
+ }
+ if (mMode == mode) return;
+ int oldMode = mMode;
+ mMode = mode;
+ if (DEBUG) Log.d(mTag, String.format("%s -> %s animate=%s",
+ modeToString(oldMode), modeToString(mode), animate));
+ onTransition(oldMode, mMode, animate);
+ }
+
+ protected void onTransition(int oldMode, int newMode, boolean animate) {
+ applyModeBackground(oldMode, newMode, animate);
+ }
+
+ protected void applyModeBackground(int oldMode, int newMode, boolean animate) {
+ if (DEBUG) Log.d(mTag, String.format("applyModeBackground oldMode=%s newMode=%s animate=%s",
+ modeToString(oldMode), modeToString(newMode), animate));
+ mBarBackground.applyModeBackground(oldMode, newMode, animate);
+ }
+
+ public static String modeToString(int mode) {
+ if (mode == MODE_OPAQUE) return "MODE_OPAQUE";
+ if (mode == MODE_SEMI_TRANSPARENT) return "MODE_SEMI_TRANSPARENT";
+ if (mode == MODE_TRANSLUCENT) return "MODE_TRANSLUCENT";
+ 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);
+ }
+
+ public void finishAnimations() {
+ 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;
+ private final int mTransparent;
+ private final int mWarning;
+ private final Drawable mGradient;
+
+ private int mMode = -1;
+ private boolean mAnimating;
+ private long mStartTime;
+ private long mEndTime;
+
+ private int mGradientAlpha;
+ private int mColor;
+ private PorterDuffColorFilter mTintFilter;
+ private Paint mPaint = new Paint();
+
+ private int mGradientAlphaStart;
+ private int mColorStart;
+
+
+ public BarBackgroundDrawable(Context context, int gradientResourceId) {
+ final Resources res = context.getResources();
+ if (DEBUG_COLORS) {
+ mOpaque = 0xff0000ff;
+ mSemiTransparent = 0x7f0000ff;
+ mTransparent = 0x2f0000ff;
+ mWarning = 0xffff0000;
+ } else {
+ mOpaque = context.getColor(R.color.system_bar_background_opaque);
+ mSemiTransparent = context.getColor(
+ com.android.internal.R.color.system_bar_background_semi_transparent);
+ mTransparent = context.getColor(R.color.system_bar_background_transparent);
+ mWarning = Utils.getColorAttr(context, android.R.attr.colorError);
+ }
+ mGradient = context.getDrawable(gradientResourceId);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // noop
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ // noop
+ }
+
+ @Override
+ public void setTint(int color) {
+ if (mTintFilter == null) {
+ mTintFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
+ } else {
+ mTintFilter.setColor(color);
+ }
+ invalidateSelf();
+ }
+
+ @Override
+ public void setTintMode(Mode tintMode) {
+ if (mTintFilter == null) {
+ mTintFilter = new PorterDuffColorFilter(0, tintMode);
+ } else {
+ mTintFilter.setMode(tintMode);
+ }
+ invalidateSelf();
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ mGradient.setBounds(bounds);
+ }
+
+ public void applyModeBackground(int oldMode, int newMode, boolean animate) {
+ if (mMode == newMode) return;
+ mMode = newMode;
+ mAnimating = animate;
+ if (animate) {
+ long now = SystemClock.elapsedRealtime();
+ mStartTime = now;
+ mEndTime = now + BACKGROUND_DURATION;
+ mGradientAlphaStart = mGradientAlpha;
+ mColorStart = mColor;
+ }
+ invalidateSelf();
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ public void finishAnimation() {
+ if (mAnimating) {
+ mAnimating = false;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ int targetGradientAlpha = 0, targetColor = 0;
+ if (mMode == MODE_WARNING) {
+ targetColor = mWarning;
+ } else if (mMode == MODE_TRANSLUCENT) {
+ targetColor = mSemiTransparent;
+ } else if (mMode == MODE_SEMI_TRANSPARENT) {
+ targetColor = mSemiTransparent;
+ } else if (mMode == MODE_TRANSPARENT || mMode == MODE_LIGHTS_OUT_TRANSPARENT) {
+ targetColor = mTransparent;
+ } else {
+ targetColor = mOpaque;
+ }
+
+ if (!mAnimating) {
+ mColor = targetColor;
+ mGradientAlpha = targetGradientAlpha;
+ } else {
+ final long now = SystemClock.elapsedRealtime();
+ if (now >= mEndTime) {
+ mAnimating = false;
+ mColor = targetColor;
+ mGradientAlpha = targetGradientAlpha;
+ } else {
+ final float t = (now - mStartTime) / (float)(mEndTime - mStartTime);
+ final float v = Math.max(0, Math.min(
+ Interpolators.LINEAR.getInterpolation(t), 1));
+ mGradientAlpha = (int)(v * targetGradientAlpha + mGradientAlphaStart * (1 - v));
+ mColor = Color.argb(
+ (int)(v * Color.alpha(targetColor) + Color.alpha(mColorStart) * (1 - v)),
+ (int)(v * Color.red(targetColor) + Color.red(mColorStart) * (1 - v)),
+ (int)(v * Color.green(targetColor) + Color.green(mColorStart) * (1 - v)),
+ (int)(v * Color.blue(targetColor) + Color.blue(mColorStart) * (1 - v)));
+ }
+ }
+ if (mGradientAlpha > 0) {
+ mGradient.setAlpha(mGradientAlpha);
+ mGradient.draw(canvas);
+ }
+ if (Color.alpha(mColor) > 0) {
+ mPaint.setColor(mColor);
+ if (mTintFilter != null) {
+ mPaint.setColorFilter(mTintFilter);
+ }
+ canvas.drawPaint(mPaint);
+ }
+ if (mAnimating) {
+ invalidateSelf(); // keep going
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/BounceInterpolator.java b/com/android/systemui/statusbar/phone/BounceInterpolator.java
new file mode 100644
index 0000000..0fdc185
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/BounceInterpolator.java
@@ -0,0 +1,43 @@
+/*
+ * 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.view.animation.Interpolator;
+
+/**
+ * An implementation of a bouncer interpolator optimized for unlock hinting.
+ */
+public class BounceInterpolator implements Interpolator {
+
+ private final static float SCALE_FACTOR = 7.5625f;
+
+ @Override
+ public float getInterpolation(float t) {
+ t *= 11f / 10f;
+ if (t < 4f / 11f) {
+ return SCALE_FACTOR * t * t;
+ } else if (t < 8f / 11f) {
+ float t2 = t - 6f / 11f;
+ return SCALE_FACTOR * t2 * t2 + 3f / 4f;
+ } else if (t < 10f / 11f) {
+ float t2 = t - 9f / 11f;
+ return SCALE_FACTOR * t2 * t2 + 15f / 16f;
+ } else {
+ return 1;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/com/android/systemui/statusbar/phone/ButtonDispatcher.java
new file mode 100644
index 0000000..a83e659
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ButtonDispatcher.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2016 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.graphics.drawable.Drawable;
+import android.view.View;
+
+import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface;
+import com.android.systemui.statusbar.policy.KeyButtonDrawable;
+
+import java.util.ArrayList;
+
+/**
+ * Dispatches common view calls to multiple views. This is used to handle
+ * multiples of the same nav bar icon appearing.
+ */
+public class ButtonDispatcher {
+
+ private final ArrayList<View> mViews = new ArrayList<>();
+
+ private final int mId;
+
+ private View.OnClickListener mClickListener;
+ private View.OnTouchListener mTouchListener;
+ private View.OnLongClickListener mLongClickListener;
+ private Boolean mLongClickable;
+ private Integer mAlpha;
+ private Float mDarkIntensity;
+ private Integer mVisibility = -1;
+ private KeyButtonDrawable mImageDrawable;
+ private View mCurrentView;
+ private boolean mVertical;
+
+ public ButtonDispatcher(int id) {
+ mId = id;
+ }
+
+ void clear() {
+ mViews.clear();
+ }
+
+ void addView(View view) {
+ mViews.add(view);
+ view.setOnClickListener(mClickListener);
+ view.setOnTouchListener(mTouchListener);
+ view.setOnLongClickListener(mLongClickListener);
+ if (mLongClickable != null) {
+ view.setLongClickable(mLongClickable);
+ }
+ if (mAlpha != null) {
+ view.setAlpha(mAlpha);
+ }
+ if (mDarkIntensity != null) {
+ ((ButtonInterface) view).setDarkIntensity(mDarkIntensity);
+ }
+ if (mVisibility != null) {
+ view.setVisibility(mVisibility);
+ }
+ if (mImageDrawable != null) {
+ ((ButtonInterface) view).setImageDrawable(mImageDrawable);
+ }
+
+ if (view instanceof ButtonInterface) {
+ ((ButtonInterface) view).setVertical(mVertical);
+ }
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public int getVisibility() {
+ return mVisibility != null ? mVisibility : View.VISIBLE;
+ }
+
+ public float getAlpha() {
+ return mAlpha != null ? mAlpha : 1;
+ }
+
+ public void setImageDrawable(KeyButtonDrawable drawable) {
+ mImageDrawable = drawable;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ ((ButtonInterface) mViews.get(i)).setImageDrawable(mImageDrawable);
+ }
+ }
+
+ public void setVisibility(int visibility) {
+ if (mVisibility == visibility) return;
+ mVisibility = visibility;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ mViews.get(i).setVisibility(mVisibility);
+ }
+ }
+
+ public void abortCurrentGesture() {
+ // This seems to be an instantaneous thing, so not going to persist it.
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ ((ButtonInterface) mViews.get(i)).abortCurrentGesture();
+ }
+ }
+
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ mViews.get(i).setAlpha(alpha);
+ }
+ }
+
+ public void setDarkIntensity(float darkIntensity) {
+ mDarkIntensity = darkIntensity;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ ((ButtonInterface) mViews.get(i)).setDarkIntensity(darkIntensity);
+ }
+ }
+
+ public void setOnClickListener(View.OnClickListener clickListener) {
+ mClickListener = clickListener;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ mViews.get(i).setOnClickListener(mClickListener);
+ }
+ }
+
+ public void setOnTouchListener(View.OnTouchListener touchListener) {
+ mTouchListener = touchListener;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ mViews.get(i).setOnTouchListener(mTouchListener);
+ }
+ }
+
+ public void setLongClickable(boolean isLongClickable) {
+ mLongClickable = isLongClickable;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ mViews.get(i).setLongClickable(mLongClickable);
+ }
+ }
+
+ public void setOnLongClickListener(View.OnLongClickListener longClickListener) {
+ mLongClickListener = longClickListener;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ mViews.get(i).setOnLongClickListener(mLongClickListener);
+ }
+ }
+
+ public ArrayList<View> getViews() {
+ return mViews;
+ }
+
+ public View getCurrentView() {
+ return mCurrentView;
+ }
+
+ public void setCurrentView(View currentView) {
+ mCurrentView = currentView.findViewById(mId);
+ }
+
+ public void setVertical(boolean vertical) {
+ mVertical = vertical;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ final View view = mViews.get(i);
+ if (view instanceof ButtonInterface) {
+ ((ButtonInterface) view).setVertical(vertical);
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
new file mode 100644
index 0000000..2c3f452
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2017 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 static android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS;
+import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO;
+
+import static com.android.systemui.statusbar.phone.StatusBar.reinflateSignalCluster;
+
+import android.annotation.Nullable;
+import android.app.Fragment;
+import android.app.StatusBarManager;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.LinearLayout;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.SignalClusterView;
+import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.EncryptionHelper;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+/**
+ * Contains the collapsed status bar and handles hiding/showing based on disable flags
+ * and keyguard state. Also manages lifecycle to make sure the views it contains are being
+ * updated by the StatusBarIconController and DarkIconManager while it is attached.
+ */
+public class CollapsedStatusBarFragment extends Fragment implements CommandQueue.Callbacks {
+
+ public static final String TAG = "CollapsedStatusBarFragment";
+ private static final String EXTRA_PANEL_STATE = "panel_state";
+ private PhoneStatusBarView mStatusBar;
+ private KeyguardMonitor mKeyguardMonitor;
+ private NetworkController mNetworkController;
+ private LinearLayout mSystemIconArea;
+ private View mNotificationIconAreaInner;
+ private int mDisabled1;
+ private StatusBar mStatusBarComponent;
+ private DarkIconManager mDarkIconManager;
+ private SignalClusterView mSignalClusterView;
+
+ private SignalCallback mSignalCallback = new SignalCallback() {
+ @Override
+ public void setIsAirplaneMode(NetworkController.IconState icon) {
+ mStatusBarComponent.recomputeDisableFlags(true /* animate */);
+ }
+ };
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
+ mNetworkController = Dependency.get(NetworkController.class);
+ mStatusBarComponent = SysUiServiceProvider.getComponent(getContext(), StatusBar.class);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.status_bar, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mStatusBar = (PhoneStatusBarView) view;
+ if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_PANEL_STATE)) {
+ mStatusBar.go(savedInstanceState.getInt(EXTRA_PANEL_STATE));
+ }
+ mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons));
+ Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager);
+ mSystemIconArea = mStatusBar.findViewById(R.id.system_icon_area);
+ mSignalClusterView = mStatusBar.findViewById(R.id.signal_cluster);
+ Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mSignalClusterView);
+ // Default to showing until we know otherwise.
+ showSystemIconArea(false);
+ initEmergencyCryptkeeperText();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(EXTRA_PANEL_STATE, mStatusBar.getState());
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).removeCallbacks(this);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mSignalClusterView);
+ Dependency.get(StatusBarIconController.class).removeIconGroup(mDarkIconManager);
+ if (mNetworkController.hasEmergencyCryptKeeperText()) {
+ mNetworkController.removeCallback(mSignalCallback);
+ }
+ }
+
+ public void initNotificationIconArea(NotificationIconAreaController
+ notificationIconAreaController) {
+ ViewGroup notificationIconArea = mStatusBar.findViewById(R.id.notification_icon_area);
+ mNotificationIconAreaInner =
+ notificationIconAreaController.getNotificationInnerAreaView();
+ if (mNotificationIconAreaInner.getParent() != null) {
+ ((ViewGroup) mNotificationIconAreaInner.getParent())
+ .removeView(mNotificationIconAreaInner);
+ }
+ notificationIconArea.addView(mNotificationIconAreaInner);
+ // Default to showing until we know otherwise.
+ showNotificationIconArea(false);
+ }
+
+ @Override
+ public void disable(int state1, int state2, boolean animate) {
+ state1 = adjustDisableFlags(state1);
+ final int old1 = mDisabled1;
+ final int diff1 = state1 ^ old1;
+ mDisabled1 = state1;
+ if ((diff1 & DISABLE_SYSTEM_INFO) != 0) {
+ if ((state1 & DISABLE_SYSTEM_INFO) != 0) {
+ hideSystemIconArea(animate);
+ } else {
+ showSystemIconArea(animate);
+ }
+ }
+ if ((diff1 & DISABLE_NOTIFICATION_ICONS) != 0) {
+ if ((state1 & DISABLE_NOTIFICATION_ICONS) != 0) {
+ hideNotificationIconArea(animate);
+ } else {
+ showNotificationIconArea(animate);
+ }
+ }
+ }
+
+ protected int adjustDisableFlags(int state) {
+ if (!mStatusBarComponent.isLaunchTransitionFadingAway()
+ && !mKeyguardMonitor.isKeyguardFadingAway()
+ && shouldHideNotificationIcons()) {
+ state |= DISABLE_NOTIFICATION_ICONS;
+ state |= DISABLE_SYSTEM_INFO;
+ }
+ if (mNetworkController != null && EncryptionHelper.IS_DATA_ENCRYPTED) {
+ if (mNetworkController.hasEmergencyCryptKeeperText()) {
+ state |= DISABLE_NOTIFICATION_ICONS;
+ }
+ if (!mNetworkController.isRadioOn()) {
+ state |= DISABLE_SYSTEM_INFO;
+ }
+ }
+ return state;
+ }
+
+ private boolean shouldHideNotificationIcons() {
+ if (!mStatusBar.isClosed() && mStatusBarComponent.hideStatusBarIconsWhenExpanded()) {
+ return true;
+ }
+ if (mStatusBarComponent.hideStatusBarIconsForBouncer()) {
+ return true;
+ }
+ return false;
+ }
+
+ public void hideSystemIconArea(boolean animate) {
+ animateHide(mSystemIconArea, animate);
+ }
+
+ public void showSystemIconArea(boolean animate) {
+ animateShow(mSystemIconArea, animate);
+ }
+
+ public void hideNotificationIconArea(boolean animate) {
+ animateHide(mNotificationIconAreaInner, animate);
+ }
+
+ public void showNotificationIconArea(boolean animate) {
+ animateShow(mNotificationIconAreaInner, animate);
+ }
+
+ /**
+ * Hides a view.
+ */
+ private void animateHide(final View v, boolean animate) {
+ v.animate().cancel();
+ if (!animate) {
+ v.setAlpha(0f);
+ v.setVisibility(View.INVISIBLE);
+ return;
+ }
+ v.animate()
+ .alpha(0f)
+ .setDuration(160)
+ .setStartDelay(0)
+ .setInterpolator(Interpolators.ALPHA_OUT)
+ .withEndAction(() -> v.setVisibility(View.INVISIBLE));
+ }
+
+ /**
+ * Shows a view, and synchronizes the animation with Keyguard exit animations, if applicable.
+ */
+ private void animateShow(View v, boolean animate) {
+ v.animate().cancel();
+ v.setVisibility(View.VISIBLE);
+ if (!animate) {
+ v.setAlpha(1f);
+ return;
+ }
+ v.animate()
+ .alpha(1f)
+ .setDuration(320)
+ .setInterpolator(Interpolators.ALPHA_IN)
+ .setStartDelay(50)
+
+ // We need to clean up any pending end action from animateHide if we call
+ // both hide and show in the same frame before the animation actually gets started.
+ // cancel() doesn't really remove the end action.
+ .withEndAction(null);
+
+ // Synchronize the motion with the Keyguard fading if necessary.
+ if (mKeyguardMonitor.isKeyguardFadingAway()) {
+ v.animate()
+ .setDuration(mKeyguardMonitor.getKeyguardFadingAwayDuration())
+ .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+ .setStartDelay(mKeyguardMonitor.getKeyguardFadingAwayDelay())
+ .start();
+ }
+ }
+
+ private void initEmergencyCryptkeeperText() {
+ View emergencyViewStub = mStatusBar.findViewById(R.id.emergency_cryptkeeper_text);
+ if (mNetworkController.hasEmergencyCryptKeeperText()) {
+ if (emergencyViewStub != null) {
+ ((ViewStub) emergencyViewStub).inflate();
+ }
+ mNetworkController.addCallback(mSignalCallback);
+ } else if (emergencyViewStub != null) {
+ ViewGroup parent = (ViewGroup) emergencyViewStub.getParent();
+ parent.removeView(emergencyViewStub);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.java b/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.java
new file mode 100644
index 0000000..6f53844
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 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.Context;
+import android.content.om.IOverlayManager;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.os.LocaleList;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+
+import com.android.systemui.ConfigurationChangedReceiver;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ConfigurationControllerImpl implements ConfigurationController,
+ ConfigurationChangedReceiver {
+
+ private final ArrayList<ConfigurationListener> mListeners = new ArrayList<>();
+ private final Configuration mLastConfig = new Configuration();
+ private int mDensity;
+ private float mFontScale;
+ private boolean mInCarMode;
+ private int mUiMode;
+ private LocaleList mLocaleList;
+
+ public ConfigurationControllerImpl(Context context) {
+ Configuration currentConfig = context.getResources().getConfiguration();
+ mFontScale = currentConfig.fontScale;
+ mDensity = currentConfig.densityDpi;
+ mInCarMode = (currentConfig.uiMode & Configuration.UI_MODE_TYPE_MASK)
+ == Configuration.UI_MODE_TYPE_CAR;
+ mUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ mLocaleList = currentConfig.getLocales();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // Avoid concurrent modification exception
+ ArrayList<ConfigurationListener> listeners = new ArrayList<>(mListeners);
+
+ listeners.forEach(l -> {
+ if (mListeners.contains(l)) {
+ l.onConfigChanged(newConfig);
+ }
+ });
+ final float fontScale = newConfig.fontScale;
+ final int density = newConfig.densityDpi;
+ int uiMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ if (density != mDensity || fontScale != mFontScale
+ || (mInCarMode && uiMode != mUiMode)) {
+ listeners.forEach(l -> {
+ if (mListeners.contains(l)) {
+ l.onDensityOrFontScaleChanged();
+ }
+ });
+ mDensity = density;
+ mFontScale = fontScale;
+ mUiMode = uiMode;
+ }
+
+ final LocaleList localeList = newConfig.getLocales();
+ if (!localeList.equals(mLocaleList)) {
+ mLocaleList = localeList;
+ listeners.forEach(l -> {
+ if (mListeners.contains(l)) {
+ l.onLocaleListChanged();
+ }
+ });
+ }
+
+ if ((mLastConfig.updateFrom(newConfig) & ActivityInfo.CONFIG_ASSETS_PATHS) != 0) {
+ listeners.forEach(l -> {
+ if (mListeners.contains(l)) {
+ l.onOverlayChanged();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void addCallback(ConfigurationListener listener) {
+ mListeners.add(listener);
+ listener.onDensityOrFontScaleChanged();
+ }
+
+ @Override
+ public void removeCallback(ConfigurationListener listener) {
+ mListeners.remove(listener);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java b/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
new file mode 100644
index 0000000..3f9ae80
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 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 static com.android.systemui.statusbar.policy.DarkIconDispatcher.getTint;
+
+import android.animation.ArgbEvaluator;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Rect;
+import android.util.ArrayMap;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+
+public class DarkIconDispatcherImpl implements DarkIconDispatcher {
+
+ private final LightBarTransitionsController mTransitionsController;
+ private final Rect mTintArea = new Rect();
+ private final ArrayMap<Object, DarkReceiver> mReceivers = new ArrayMap<>();
+
+ private int mIconTint = DEFAULT_ICON_TINT;
+ private float mDarkIntensity;
+ private int mDarkModeIconColorSingleTone;
+ private int mLightModeIconColorSingleTone;
+
+ public DarkIconDispatcherImpl(Context context) {
+ mDarkModeIconColorSingleTone = context.getColor(R.color.dark_mode_icon_color_single_tone);
+ mLightModeIconColorSingleTone = context.getColor(R.color.light_mode_icon_color_single_tone);
+
+ mTransitionsController = new LightBarTransitionsController(context,
+ this::setIconTintInternal);
+ }
+
+ public LightBarTransitionsController getTransitionsController() {
+ return mTransitionsController;
+ }
+
+ public void addDarkReceiver(DarkReceiver receiver) {
+ mReceivers.put(receiver, receiver);
+ receiver.onDarkChanged(mTintArea, mDarkIntensity, mIconTint);
+ }
+
+ public void addDarkReceiver(ImageView imageView) {
+ DarkReceiver receiver = (area, darkIntensity, tint) -> imageView.setImageTintList(
+ ColorStateList.valueOf(getTint(mTintArea, imageView, mIconTint)));
+ mReceivers.put(imageView, receiver);
+ receiver.onDarkChanged(mTintArea, mDarkIntensity, mIconTint);
+ }
+
+ public void removeDarkReceiver(DarkReceiver object) {
+ mReceivers.remove(object);
+ }
+
+ public void removeDarkReceiver(ImageView object) {
+ mReceivers.remove(object);
+ }
+
+ public void applyDark(ImageView object) {
+ mReceivers.get(object).onDarkChanged(mTintArea, mDarkIntensity, mIconTint);
+ }
+
+ /**
+ * Sets the dark area so {@link #setIconsDark} only affects the icons in the specified area.
+ *
+ * @param darkArea the area in which icons should change it's tint, in logical screen
+ * coordinates
+ */
+ public void setIconsDarkArea(Rect darkArea) {
+ if (darkArea == null && mTintArea.isEmpty()) {
+ return;
+ }
+ if (darkArea == null) {
+ mTintArea.setEmpty();
+ } else {
+ mTintArea.set(darkArea);
+ }
+ applyIconTint();
+ }
+
+ private void setIconTintInternal(float darkIntensity) {
+ mDarkIntensity = darkIntensity;
+ mIconTint = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity,
+ mLightModeIconColorSingleTone, mDarkModeIconColorSingleTone);
+ applyIconTint();
+ }
+
+ private void applyIconTint() {
+ for (int i = 0; i < mReceivers.size(); i++) {
+ mReceivers.valueAt(i).onDarkChanged(mTintArea, mDarkIntensity, mIconTint);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/com/android/systemui/statusbar/phone/DemoStatusIcons.java
new file mode 100644
index 0000000..c499619
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -0,0 +1,158 @@
+/*
+ * 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.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.DemoMode;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.policy.LocationControllerImpl;
+
+public class DemoStatusIcons extends LinearLayout implements DemoMode {
+ private final LinearLayout mStatusIcons;
+ private final int mIconSize;
+
+ private boolean mDemoMode;
+
+ public DemoStatusIcons(LinearLayout statusIcons, int iconSize) {
+ super(statusIcons.getContext());
+ mStatusIcons = statusIcons;
+ mIconSize = iconSize;
+
+ setLayoutParams(mStatusIcons.getLayoutParams());
+ setOrientation(mStatusIcons.getOrientation());
+ setGravity(Gravity.CENTER_VERTICAL); // no LL.getGravity()
+ ViewGroup p = (ViewGroup) mStatusIcons.getParent();
+ p.addView(this, p.indexOfChild(mStatusIcons));
+ }
+
+ @Override
+ public void dispatchDemoCommand(String command, Bundle args) {
+ if (!mDemoMode && command.equals(COMMAND_ENTER)) {
+ mDemoMode = true;
+ mStatusIcons.setVisibility(View.GONE);
+ setVisibility(View.VISIBLE);
+ } else if (mDemoMode && command.equals(COMMAND_EXIT)) {
+ mDemoMode = false;
+ mStatusIcons.setVisibility(View.VISIBLE);
+ setVisibility(View.GONE);
+ } else if (mDemoMode && command.equals(COMMAND_STATUS)) {
+ String volume = args.getString("volume");
+ if (volume != null) {
+ int iconId = volume.equals("vibrate") ? R.drawable.stat_sys_ringer_vibrate
+ : 0;
+ updateSlot("volume", null, iconId);
+ }
+ String zen = args.getString("zen");
+ if (zen != null) {
+ int iconId = zen.equals("important") ? R.drawable.stat_sys_zen_important
+ : zen.equals("none") ? R.drawable.stat_sys_zen_none
+ : 0;
+ updateSlot("zen", null, iconId);
+ }
+ String bt = args.getString("bluetooth");
+ if (bt != null) {
+ int iconId = bt.equals("disconnected") ? R.drawable.stat_sys_data_bluetooth
+ : bt.equals("connected") ? R.drawable.stat_sys_data_bluetooth_connected
+ : 0;
+ updateSlot("bluetooth", null, iconId);
+ }
+ String location = args.getString("location");
+ if (location != null) {
+ int iconId = location.equals("show") ? PhoneStatusBarPolicy.LOCATION_STATUS_ICON_ID
+ : 0;
+ updateSlot("location", null, iconId);
+ }
+ String alarm = args.getString("alarm");
+ if (alarm != null) {
+ int iconId = alarm.equals("show") ? R.drawable.stat_sys_alarm
+ : 0;
+ updateSlot("alarm_clock", null, iconId);
+ }
+ String tty = args.getString("tty");
+ if (tty != null) {
+ int iconId = tty.equals("show") ? R.drawable.stat_sys_tty_mode
+ : 0;
+ updateSlot("tty", null, iconId);
+ }
+ String mute = args.getString("mute");
+ if (mute != null) {
+ int iconId = mute.equals("show") ? android.R.drawable.stat_notify_call_mute
+ : 0;
+ updateSlot("mute", null, iconId);
+ }
+ String speakerphone = args.getString("speakerphone");
+ if (speakerphone != null) {
+ int iconId = speakerphone.equals("show") ? android.R.drawable.stat_sys_speakerphone
+ : 0;
+ updateSlot("speakerphone", null, iconId);
+ }
+ String cast = args.getString("cast");
+ if (cast != null) {
+ 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);
+ }
+ }
+ }
+
+ private void updateSlot(String slot, String iconPkg, int iconId) {
+ if (!mDemoMode) return;
+ if (iconPkg == null) {
+ iconPkg = mContext.getPackageName();
+ }
+ int removeIndex = -1;
+ for (int i = 0; i < getChildCount(); i++) {
+ StatusBarIconView v = (StatusBarIconView) getChildAt(i);
+ if (slot.equals(v.getTag())) {
+ if (iconId == 0) {
+ removeIndex = i;
+ break;
+ } else {
+ StatusBarIcon icon = v.getStatusBarIcon();
+ icon.icon = Icon.createWithResource(icon.icon.getResPackage(), iconId);
+ v.set(icon);
+ v.updateDrawable();
+ return;
+ }
+ }
+ }
+ if (iconId == 0) {
+ if (removeIndex != -1) {
+ removeViewAt(removeIndex);
+ }
+ return;
+ }
+ StatusBarIcon icon = new StatusBarIcon(iconPkg, UserHandle.SYSTEM, iconId, 0, 0, "Demo");
+ StatusBarIconView v = new StatusBarIconView(getContext(), null, null);
+ v.setTag(slot);
+ v.set(icon);
+ addView(v, 0, new LinearLayout.LayoutParams(mIconSize, mIconSize));
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/DoubleTapHelper.java b/com/android/systemui/statusbar/phone/DoubleTapHelper.java
new file mode 100644
index 0000000..dcb6a38
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DoubleTapHelper.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2017 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.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.R;
+
+/**
+ * Detects a double tap.
+ */
+public class DoubleTapHelper {
+
+ private static final long DOUBLETAP_TIMEOUT_MS = 1200;
+
+ private final View mView;
+ private final ActivationListener mActivationListener;
+ private final DoubleTapListener mDoubleTapListener;
+ private final SlideBackListener mSlideBackListener;
+ private final DoubleTapLogListener mDoubleTapLogListener;
+
+ private float mTouchSlop;
+ private float mDoubleTapSlop;
+
+ private boolean mActivated;
+
+ private float mDownX;
+ private float mDownY;
+ private boolean mTrackTouch;
+
+ private float mActivationX;
+ private float mActivationY;
+ private Runnable mTapTimeoutRunnable = this::makeInactive;
+
+ public DoubleTapHelper(View view, ActivationListener activationListener,
+ DoubleTapListener doubleTapListener, SlideBackListener slideBackListener,
+ DoubleTapLogListener doubleTapLogListener) {
+ mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
+ mDoubleTapSlop = view.getResources().getDimension(R.dimen.double_tap_slop);
+ mView = view;
+
+ mActivationListener = activationListener;
+ mDoubleTapListener = doubleTapListener;
+ mSlideBackListener = slideBackListener;
+ mDoubleTapLogListener = doubleTapLogListener;
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ return onTouchEvent(event, Integer.MAX_VALUE);
+ }
+
+ public boolean onTouchEvent(MotionEvent event, int maxTouchableHeight) {
+ int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mDownX = event.getX();
+ mDownY = event.getY();
+ mTrackTouch = true;
+ if (mDownY > maxTouchableHeight) {
+ mTrackTouch = false;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!isWithinTouchSlop(event)) {
+ makeInactive();
+ mTrackTouch = false;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (isWithinTouchSlop(event)) {
+ if (mSlideBackListener != null && mSlideBackListener.onSlideBack()) {
+ return true;
+ }
+ if (!mActivated) {
+ makeActive();
+ mView.postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
+ mActivationX = event.getX();
+ mActivationY = event.getY();
+ } else {
+ boolean withinDoubleTapSlop = isWithinDoubleTapSlop(event);
+ if (mDoubleTapLogListener != null) {
+ mDoubleTapLogListener.onDoubleTapLog(withinDoubleTapSlop,
+ event.getX() - mActivationX,
+ event.getY() - mActivationY);
+ }
+ if (withinDoubleTapSlop) {
+ if (!mDoubleTapListener.onDoubleTap()) {
+ return false;
+ }
+ } else {
+ makeInactive();
+ mTrackTouch = false;
+ }
+ }
+ } else {
+ makeInactive();
+ mTrackTouch = false;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ makeInactive();
+ mTrackTouch = false;
+ break;
+ default:
+ break;
+ }
+ return mTrackTouch;
+ }
+
+ private void makeActive() {
+ if (!mActivated) {
+ mActivated = true;
+ mActivationListener.onActiveChanged(true);
+ }
+ }
+
+ private void makeInactive() {
+ if (mActivated) {
+ mActivated = false;
+ mActivationListener.onActiveChanged(false);
+ }
+ }
+
+ private boolean isWithinTouchSlop(MotionEvent event) {
+ return Math.abs(event.getX() - mDownX) < mTouchSlop
+ && Math.abs(event.getY() - mDownY) < mTouchSlop;
+ }
+
+ private boolean isWithinDoubleTapSlop(MotionEvent event) {
+ if (!mActivated) {
+ // If we're not activated there's no double tap slop to satisfy.
+ return true;
+ }
+
+ return Math.abs(event.getX() - mActivationX) < mDoubleTapSlop
+ && Math.abs(event.getY() - mActivationY) < mDoubleTapSlop;
+ }
+
+ @FunctionalInterface
+ public interface ActivationListener {
+ void onActiveChanged(boolean active);
+ }
+
+ @FunctionalInterface
+ public interface DoubleTapListener {
+ boolean onDoubleTap();
+ }
+
+ @FunctionalInterface
+ public interface SlideBackListener {
+ boolean onSlideBack();
+ }
+
+ @FunctionalInterface
+ public interface DoubleTapLogListener {
+ void onDoubleTapLog(boolean accepted, float dx, float dy);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/DozeParameters.java b/com/android/systemui/statusbar/phone/DozeParameters.java
new file mode 100644
index 0000000..6b7397b
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -0,0 +1,241 @@
+/*
+ * 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.content.Context;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.MathUtils;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.hardware.AmbientDisplayConfiguration;
+import com.android.systemui.R;
+
+import java.io.PrintWriter;
+
+public class DozeParameters {
+ private static final int MAX_DURATION = 60 * 1000;
+ public static final String DOZE_SENSORS_WAKE_UP_FULLY = "doze_sensors_wake_up_fully";
+
+ private final Context mContext;
+ private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
+
+ private static IntInOutMatcher sPickupSubtypePerformsProxMatcher;
+
+ public DozeParameters(Context context) {
+ mContext = context;
+ mAmbientDisplayConfiguration = new AmbientDisplayConfiguration(mContext);
+ }
+
+ public void dump(PrintWriter pw) {
+ pw.println(" DozeParameters:");
+ pw.print(" getDisplayStateSupported(): "); pw.println(getDisplayStateSupported());
+ 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(" getPulseInVisibleDuration(): "); pw.println(getPulseVisibleDuration());
+ pw.print(" getPulseOutDuration(): "); pw.println(getPulseOutDuration());
+ pw.print(" getPulseOnSigMotion(): "); pw.println(getPulseOnSigMotion());
+ pw.print(" getVibrateOnSigMotion(): "); pw.println(getVibrateOnSigMotion());
+ pw.print(" getVibrateOnPickup(): "); pw.println(getVibrateOnPickup());
+ pw.print(" getProxCheckBeforePulse(): "); pw.println(getProxCheckBeforePulse());
+ pw.print(" getPickupVibrationThreshold(): "); pw.println(getPickupVibrationThreshold());
+ pw.print(" getPickupSubtypePerformsProxCheck(): ");pw.println(
+ dumpPickupSubtypePerformsProxCheck());
+ }
+
+ private String dumpPickupSubtypePerformsProxCheck() {
+ // Refresh sPickupSubtypePerformsProxMatcher
+ getPickupSubtypePerformsProxCheck(0);
+
+ if (sPickupSubtypePerformsProxMatcher == null) {
+ return "fallback: " + mContext.getResources().getBoolean(
+ R.bool.doze_pickup_performs_proximity_check);
+ } else {
+ return "spec: " + sPickupSubtypePerformsProxMatcher.mSpec;
+ }
+ }
+
+ public boolean getDisplayStateSupported() {
+ return getBoolean("doze.display.supported", R.bool.doze_display_state_supported);
+ }
+
+ public boolean getDozeSuspendDisplayStateSupported() {
+ return mContext.getResources().getBoolean(R.bool.doze_suspend_display_state_supported);
+ }
+
+ public int getPulseDuration(boolean pickup) {
+ return getPulseInDuration(pickup) + getPulseVisibleDuration() + getPulseOutDuration();
+ }
+
+ public int getPulseInDuration(boolean pickupOrDoubleTap) {
+ return pickupOrDoubleTap
+ ? 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 getPulseVisibleDuration() {
+ return getInt("doze.pulse.duration.visible", R.integer.doze_pulse_duration_visible);
+ }
+
+ public int getPulseOutDuration() {
+ return getInt("doze.pulse.duration.out", R.integer.doze_pulse_duration_out);
+ }
+
+ public boolean getPulseOnSigMotion() {
+ return getBoolean("doze.pulse.sigmotion", R.bool.doze_pulse_on_significant_motion);
+ }
+
+ public boolean getVibrateOnSigMotion() {
+ return SystemProperties.getBoolean("doze.vibrate.sigmotion", false);
+ }
+
+ public boolean getVibrateOnPickup() {
+ return SystemProperties.getBoolean("doze.vibrate.pickup", false);
+ }
+
+ public boolean getProxCheckBeforePulse() {
+ return getBoolean("doze.pulse.proxcheck", R.bool.doze_proximity_check_before_pulse);
+ }
+
+ public int getPickupVibrationThreshold() {
+ return getInt("doze.pickup.vibration.threshold", R.integer.doze_pickup_vibration_threshold);
+ }
+
+ public boolean getAlwaysOn() {
+ return mAmbientDisplayConfiguration.alwaysOnEnabled(UserHandle.USER_CURRENT);
+ }
+
+ private boolean getBoolean(String propName, int resId) {
+ return SystemProperties.getBoolean(propName, mContext.getResources().getBoolean(resId));
+ }
+
+ private int getInt(String propName, int resId) {
+ int value = SystemProperties.getInt(propName, mContext.getResources().getInteger(resId));
+ return MathUtils.constrain(value, 0, MAX_DURATION);
+ }
+
+ private String getString(String propName, int resId) {
+ return SystemProperties.get(propName, mContext.getString(resId));
+ }
+
+ public boolean getPickupSubtypePerformsProxCheck(int subType) {
+ String spec = getString("doze.pickup.proxcheck",
+ R.string.doze_pickup_subtype_performs_proximity_check);
+
+ if (TextUtils.isEmpty(spec)) {
+ // Fall back to non-subtype based property.
+ return mContext.getResources().getBoolean(R.bool.doze_pickup_performs_proximity_check);
+ }
+
+ if (sPickupSubtypePerformsProxMatcher == null
+ || !TextUtils.equals(spec, sPickupSubtypePerformsProxMatcher.mSpec)) {
+ sPickupSubtypePerformsProxMatcher = new IntInOutMatcher(spec);
+ }
+
+ return sPickupSubtypePerformsProxMatcher.isIn(subType);
+ }
+
+ public int getPulseVisibleDurationExtended() {
+ return 2 * getPulseVisibleDuration();
+ }
+
+ public boolean doubleTapReportsTouchCoordinates() {
+ return mContext.getResources().getBoolean(R.bool.doze_double_tap_reports_touch_coordinates);
+ }
+
+
+ /**
+ * Parses a spec of the form `1,2,3,!5,*`. The resulting object will match numbers that are
+ * listed, will not match numbers that are listed with a ! prefix, and will match / not match
+ * unlisted numbers depending on whether * or !* is present.
+ *
+ * * -> match any numbers that are not explicitly listed
+ * !* -> don't match any numbers that are not explicitly listed
+ * 2 -> match 2
+ * !3 -> don't match 3
+ *
+ * It is illegal to specify:
+ * - an empty spec
+ * - a spec containing that are empty, or a lone !
+ * - a spec for anything other than numbers or *
+ * - multiple terms for the same number / multiple *s
+ */
+ public static class IntInOutMatcher {
+ private static final String WILDCARD = "*";
+ private static final char OUT_PREFIX = '!';
+
+ private final SparseBooleanArray mIsIn;
+ private final boolean mDefaultIsIn;
+ final String mSpec;
+
+ public IntInOutMatcher(String spec) {
+ if (TextUtils.isEmpty(spec)) {
+ throw new IllegalArgumentException("Spec must not be empty");
+ }
+
+ boolean defaultIsIn = false;
+ boolean foundWildcard = false;
+
+ mSpec = spec;
+ mIsIn = new SparseBooleanArray();
+
+ for (String itemPrefixed : spec.split(",", -1)) {
+ if (itemPrefixed.length() == 0) {
+ throw new IllegalArgumentException(
+ "Illegal spec, must not have zero-length items: `" + spec + "`");
+ }
+ boolean isIn = itemPrefixed.charAt(0) != OUT_PREFIX;
+ String item = isIn ? itemPrefixed : itemPrefixed.substring(1);
+
+ if (itemPrefixed.length() == 0) {
+ throw new IllegalArgumentException(
+ "Illegal spec, must not have zero-length items: `" + spec + "`");
+ }
+
+ if (WILDCARD.equals(item)) {
+ if (foundWildcard) {
+ throw new IllegalArgumentException("Illegal spec, `" + WILDCARD +
+ "` must not appear multiple times in `" + spec + "`");
+ }
+ defaultIsIn = isIn;
+ foundWildcard = true;
+ } else {
+ int key = Integer.parseInt(item);
+ if (mIsIn.indexOfKey(key) >= 0) {
+ throw new IllegalArgumentException("Illegal spec, `" + key +
+ "` must not appear multiple times in `" + spec + "`");
+ }
+ mIsIn.put(key, isIn);
+ }
+ }
+
+ if (!foundWildcard) {
+ throw new IllegalArgumentException("Illegal spec, must specify either * or !*");
+ }
+
+ mDefaultIsIn = defaultIsIn;
+ }
+
+ public boolean isIn(int value) {
+ return (mIsIn.get(value, mDefaultIsIn));
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/DozeScrimController.java b/com/android/systemui/statusbar/phone/DozeScrimController.java
new file mode 100644
index 0000000..021b451
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DozeScrimController.java
@@ -0,0 +1,391 @@
+/*
+ * 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.Interpolator;
+
+import com.android.systemui.Interpolators;
+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 Handler mHandler = new Handler();
+ private final ScrimController mScrimController;
+
+ private final Context mContext;
+
+ private boolean mDozing;
+ private DozeHost.PulseCallback mPulseCallback;
+ private int mPulseReason;
+ private Animator mInFrontAnimator;
+ private Animator mBehindAnimator;
+ private float mInFrontTarget;
+ private float mBehindTarget;
+ private boolean mDozingAborted;
+ private boolean mWakeAndUnlocking;
+ private boolean mFullyPulsing;
+
+ private float mAodFrontScrimOpacity = 0;
+ private Runnable mSetDozeInFrontAlphaDelayed;
+
+ public DozeScrimController(ScrimController scrimController, Context context) {
+ mContext = context;
+ mScrimController = scrimController;
+ mDozeParameters = new DozeParameters(context);
+ }
+
+ public void setDozing(boolean dozing, boolean animate) {
+ if (mDozing == dozing) return;
+ mDozing = dozing;
+ mWakeAndUnlocking = false;
+ if (mDozing) {
+ mDozingAborted = false;
+ abortAnimations();
+ mScrimController.setDozeBehindAlpha(1f);
+ setDozeInFrontAlpha(mDozeParameters.getAlwaysOn() ? mAodFrontScrimOpacity : 1f);
+ } else {
+ cancelPulsing();
+ if (animate) {
+ startScrimAnimation(false /* inFront */, 0f /* target */,
+ NotificationPanelView.DOZE_ANIMATION_DURATION,
+ Interpolators.LINEAR_OUT_SLOW_IN);
+ startScrimAnimation(true /* inFront */, 0f /* target */,
+ NotificationPanelView.DOZE_ANIMATION_DURATION,
+ Interpolators.LINEAR_OUT_SLOW_IN);
+ } else {
+ abortAnimations();
+ mScrimController.setDozeBehindAlpha(0f);
+ setDozeInFrontAlpha(0f);
+ }
+ }
+ }
+
+ /**
+ * Set the opacity of the front scrim when showing AOD1
+ *
+ * Used to emulate lower brightness values than the hardware supports natively.
+ */
+ public void setAodDimmingScrim(float scrimOpacity) {
+ mAodFrontScrimOpacity = scrimOpacity;
+ if (mDozing && !isPulsing() && !mDozingAborted && !mWakeAndUnlocking
+ && mDozeParameters.getAlwaysOn()) {
+ setDozeInFrontAlpha(mAodFrontScrimOpacity);
+ }
+ }
+
+ public void setWakeAndUnlocking() {
+ // Immediately abort the doze scrims in case of wake-and-unlock
+ // for pulsing so the Keyguard fade-out animation scrim can take over.
+ if (!mWakeAndUnlocking) {
+ mWakeAndUnlocking = true;
+ mScrimController.setDozeBehindAlpha(0f);
+ 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;
+ setDozeInFrontAlpha(1f);
+ mHandler.post(mPulseIn);
+ }
+
+ /**
+ * Aborts pulsing immediately.
+ */
+ public void abortPulsing() {
+ cancelPulsing();
+ if (mDozing && !mWakeAndUnlocking) {
+ mScrimController.setDozeBehindAlpha(1f);
+ setDozeInFrontAlpha(mDozeParameters.getAlwaysOn() && !mDozingAborted
+ ? mAodFrontScrimOpacity : 1f);
+ }
+ }
+
+ /**
+ * Aborts dozing immediately.
+ */
+ public void abortDoze() {
+ mDozingAborted = true;
+ abortPulsing();
+ }
+
+ public void pulseOutNow() {
+ if (mPulseCallback != null && mFullyPulsing) {
+ mPulseOut.run();
+ }
+ }
+
+ public void onScreenTurnedOn() {
+ if (isPulsing()) {
+ final boolean pickupOrDoubleTap = mPulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP
+ || mPulseReason == DozeLog.PULSE_REASON_SENSOR_DOUBLE_TAP;
+ startScrimAnimation(true /* inFront */, 0f,
+ mDozeParameters.getPulseInDuration(pickupOrDoubleTap),
+ pickupOrDoubleTap ? Interpolators.LINEAR_OUT_SLOW_IN : Interpolators.ALPHA_OUT,
+ mPulseInFinished);
+ }
+ }
+
+ public boolean isPulsing() {
+ return mPulseCallback != null;
+ }
+
+ public boolean isDozing() {
+ return mDozing;
+ }
+
+ public void extendPulse() {
+ mHandler.removeCallbacks(mPulseOut);
+ }
+
+ private void cancelPulsing() {
+ if (DEBUG) Log.d(TAG, "Cancel pulsing");
+
+ if (mPulseCallback != null) {
+ mFullyPulsing = false;
+ mHandler.removeCallbacks(mPulseIn);
+ mHandler.removeCallbacks(mPulseOut);
+ mHandler.removeCallbacks(mPulseOutExtended);
+ 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, null /* endRunnable */);
+ }
+
+ private void startScrimAnimation(final boolean inFront, float target, long duration,
+ Interpolator interpolator, 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.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 (mWakeAndUnlocking) {
+ return;
+ }
+ if (inFront) {
+ mScrimController.setDozeInFrontAlpha(alpha);
+ } else {
+ mScrimController.setDozeBehindAlpha(alpha);
+ }
+ }
+
+ private float getDozeAlpha(boolean inFront) {
+ return inFront
+ ? mScrimController.getDozeInFrontAlpha()
+ : mScrimController.getDozeBehindAlpha();
+ }
+
+ private void setDozeInFrontAlpha(float opacity) {
+ setDozeInFrontAlphaDelayed(opacity, 0 /* delay */);
+
+ }
+
+ private void setDozeInFrontAlphaDelayed(float opacity, long delayMs) {
+ if (mSetDozeInFrontAlphaDelayed != null) {
+ mHandler.removeCallbacks(mSetDozeInFrontAlphaDelayed);
+ mSetDozeInFrontAlphaDelayed = null;
+ }
+ if (delayMs <= 0) {
+ mScrimController.setDozeInFrontAlpha(opacity);
+ } else {
+ mHandler.postDelayed(mSetDozeInFrontAlphaDelayed = () -> {
+ setDozeInFrontAlpha(opacity);
+ }, delayMs);
+ }
+ }
+
+ 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);
+
+ // 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());
+ mHandler.postDelayed(mPulseOutExtended,
+ mDozeParameters.getPulseVisibleDurationExtended());
+ mFullyPulsing = true;
+ }
+ };
+
+ private final Runnable mPulseOutExtended = new Runnable() {
+ @Override
+ public void run() {
+ mHandler.removeCallbacks(mPulseOut);
+ mPulseOut.run();
+ }
+ };
+
+ private final Runnable mPulseOut = new Runnable() {
+ @Override
+ public void run() {
+ mFullyPulsing = false;
+ mHandler.removeCallbacks(mPulseOut);
+ mHandler.removeCallbacks(mPulseOutExtended);
+ if (DEBUG) Log.d(TAG, "Pulse out, mDozing=" + mDozing);
+ if (!mDozing) return;
+ startScrimAnimation(true /* inFront */, 1,
+ mDozeParameters.getPulseOutDuration(),
+ Interpolators.ALPHA_IN, mPulseOutFinishing);
+ }
+ };
+
+ private final Runnable mPulseOutFinishing = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.d(TAG, "Pulse out finished");
+ DozeLog.tracePulseFinish();
+ if (mDozeParameters.getAlwaysOn() && mDozing) {
+ // Setting power states can block rendering. For AOD, delay finishing the pulse and
+ // setting the power state until the fully black scrim had time to hit the
+ // framebuffer.
+ mHandler.postDelayed(mPulseOutFinished, 30);
+ } else {
+ mPulseOutFinished.run();
+ }
+ }
+ };
+
+ private final Runnable mPulseOutFinished = new Runnable() {
+ @Override
+ public void run() {
+ // Signal that the pulse is all finished so we can turn the screen off now.
+ DozeScrimController.this.pulseFinished();
+ if (mDozeParameters.getAlwaysOn()) {
+ // Setting power states can happen after we push out the frame. Make sure we
+ // stay fully opaque until the power state request reaches the lower levels.
+ setDozeInFrontAlphaDelayed(mAodFrontScrimOpacity, 30);
+ }
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/phone/ExpandableIndicator.java b/com/android/systemui/statusbar/phone/ExpandableIndicator.java
new file mode 100644
index 0000000..8f49c85
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ExpandableIndicator.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 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.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import com.android.systemui.R;
+
+public class ExpandableIndicator extends ImageView {
+
+ private boolean mExpanded;
+ private boolean mIsDefaultDirection = true;
+
+ public ExpandableIndicator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ updateIndicatorDrawable();
+ setContentDescription(getContentDescription(mExpanded));
+ }
+
+ public void setExpanded(boolean expanded) {
+ if (expanded == mExpanded) return;
+ mExpanded = expanded;
+ final int res = getDrawableResourceId(!mExpanded);
+ // workaround to reset drawable
+ final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) getContext()
+ .getDrawable(res).getConstantState().newDrawable();
+ setImageDrawable(avd);
+ avd.forceAnimationOnUI();
+ avd.start();
+ setContentDescription(getContentDescription(expanded));
+ }
+
+ /** Whether the icons are using the default direction or the opposite */
+ public void setDefaultDirection(boolean isDefaultDirection) {
+ mIsDefaultDirection = isDefaultDirection;
+ updateIndicatorDrawable();
+ }
+
+ private int getDrawableResourceId(boolean expanded) {
+ if (mIsDefaultDirection) {
+ return expanded ? R.drawable.ic_volume_collapse_animation
+ : R.drawable.ic_volume_expand_animation;
+ } else {
+ return expanded ? R.drawable.ic_volume_expand_animation
+ : R.drawable.ic_volume_collapse_animation;
+ }
+ }
+
+ private String getContentDescription(boolean expanded) {
+ return expanded ? mContext.getString(R.string.accessibility_quick_settings_collapse)
+ : mContext.getString(R.string.accessibility_quick_settings_expand);
+ }
+
+ private void updateIndicatorDrawable() {
+ final int res = getDrawableResourceId(mExpanded);
+ setImageResource(res);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/FingerprintUnlockController.java b/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
new file mode 100644
index 0000000..91369db
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2015 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.Context;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+
+import com.android.keyguard.KeyguardConstants;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.LatencyTracker;
+import com.android.systemui.Dependency;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+
+import java.io.PrintWriter;
+
+/**
+ * Controller which coordinates all the fingerprint unlocking actions with the UI.
+ */
+public class FingerprintUnlockController extends KeyguardUpdateMonitorCallback {
+
+ private static final String TAG = "FingerprintController";
+ private static final boolean DEBUG_FP_WAKELOCK = KeyguardConstants.DEBUG_FP_WAKELOCK;
+ private static final long FINGERPRINT_WAKELOCK_TIMEOUT_MS = 15 * 1000;
+ private static final String FINGERPRINT_WAKE_LOCK_NAME = "wake-and-unlock wakelock";
+
+ /**
+ * Mode in which we don't need to wake up the device when we get a fingerprint.
+ */
+ public static final int MODE_NONE = 0;
+
+ /**
+ * Mode in which we wake up the device, and directly dismiss Keyguard. Active when we acquire
+ * a fingerprint while the screen is off and the device was sleeping.
+ */
+ public static final int MODE_WAKE_AND_UNLOCK = 1;
+
+ /**
+ * Mode in which we wake the device up, and fade out the Keyguard contents because they were
+ * already visible while pulsing in doze mode.
+ */
+ public static final int MODE_WAKE_AND_UNLOCK_PULSING = 2;
+
+ /**
+ * Mode in which we wake up the device, but play the normal dismiss animation. Active when we
+ * acquire a fingerprint pulsing in doze mode.
+ */
+ public static final int MODE_SHOW_BOUNCER = 3;
+
+ /**
+ * Mode in which we only wake up the device, and keyguard was not showing when we acquired a
+ * fingerprint.
+ * */
+ public static final int MODE_ONLY_WAKE = 4;
+
+ /**
+ * Mode in which fingerprint unlocks the device.
+ */
+ public static final int MODE_UNLOCK = 5;
+
+ /**
+ * Mode in which fingerprint brings up the bouncer because fingerprint unlocking is currently
+ * not allowed.
+ */
+ public static final int MODE_DISMISS_BOUNCER = 6;
+
+ /**
+ * Mode in which fingerprint wakes and unlocks the device from a dream.
+ */
+ public static final int MODE_WAKE_AND_UNLOCK_FROM_DREAM = 7;
+
+ /**
+ * How much faster we collapse the lockscreen when authenticating with fingerprint.
+ */
+ private static final float FINGERPRINT_COLLAPSE_SPEEDUP_FACTOR = 1.1f;
+
+ private PowerManager mPowerManager;
+ private Handler mHandler = new Handler();
+ private PowerManager.WakeLock mWakeLock;
+ private KeyguardUpdateMonitor mUpdateMonitor;
+ private int mMode;
+ private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ private StatusBarWindowManager mStatusBarWindowManager;
+ private DozeScrimController mDozeScrimController;
+ private KeyguardViewMediator mKeyguardViewMediator;
+ private ScrimController mScrimController;
+ private StatusBar mStatusBar;
+ private final UnlockMethodCache mUnlockMethodCache;
+ private final Context mContext;
+ private int mPendingAuthenticatedUserId = -1;
+ private boolean mPendingShowBouncer;
+ private boolean mHasScreenTurnedOnSinceAuthenticating;
+
+ public FingerprintUnlockController(Context context,
+ DozeScrimController dozeScrimController,
+ KeyguardViewMediator keyguardViewMediator,
+ ScrimController scrimController,
+ StatusBar statusBar,
+ UnlockMethodCache unlockMethodCache) {
+ mContext = context;
+ mPowerManager = context.getSystemService(PowerManager.class);
+ mUpdateMonitor = KeyguardUpdateMonitor.getInstance(context);
+ mUpdateMonitor.registerCallback(this);
+ Dependency.get(WakefulnessLifecycle.class).addObserver(mWakefulnessObserver);
+ Dependency.get(ScreenLifecycle.class).addObserver(mScreenObserver);
+ mStatusBarWindowManager = Dependency.get(StatusBarWindowManager.class);
+ mDozeScrimController = dozeScrimController;
+ mKeyguardViewMediator = keyguardViewMediator;
+ mScrimController = scrimController;
+ mStatusBar = statusBar;
+ mUnlockMethodCache = unlockMethodCache;
+ }
+
+ public void setStatusBarKeyguardViewManager(
+ StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+ mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+ }
+
+ private final Runnable mReleaseFingerprintWakeLockRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG_FP_WAKELOCK) {
+ Log.i(TAG, "fp wakelock: TIMEOUT!!");
+ }
+ releaseFingerprintWakeLock();
+ }
+ };
+
+ private void releaseFingerprintWakeLock() {
+ if (mWakeLock != null) {
+ mHandler.removeCallbacks(mReleaseFingerprintWakeLockRunnable);
+ if (DEBUG_FP_WAKELOCK) {
+ Log.i(TAG, "releasing fp wakelock");
+ }
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ }
+
+ @Override
+ public void onFingerprintAcquired() {
+ Trace.beginSection("FingerprintUnlockController#onFingerprintAcquired");
+ releaseFingerprintWakeLock();
+ if (!mUpdateMonitor.isDeviceInteractive()) {
+ if (LatencyTracker.isEnabled(mContext)) {
+ LatencyTracker.getInstance(mContext).onActionStart(
+ LatencyTracker.ACTION_FINGERPRINT_WAKE_AND_UNLOCK);
+ }
+ mWakeLock = mPowerManager.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK, FINGERPRINT_WAKE_LOCK_NAME);
+ Trace.beginSection("acquiring wake-and-unlock");
+ mWakeLock.acquire();
+ Trace.endSection();
+ if (DEBUG_FP_WAKELOCK) {
+ Log.i(TAG, "fingerprint acquired, grabbing fp wakelock");
+ }
+ mHandler.postDelayed(mReleaseFingerprintWakeLockRunnable,
+ FINGERPRINT_WAKELOCK_TIMEOUT_MS);
+ }
+ Trace.endSection();
+ }
+
+ private boolean pulsingOrAod() {
+ boolean pulsing = mDozeScrimController.isPulsing();
+ boolean dozingWithScreenOn = mStatusBar.isDozing() && !mStatusBar.isScreenFullyOff();
+ return pulsing || dozingWithScreenOn;
+ }
+
+ @Override
+ public void onFingerprintAuthenticated(int userId) {
+ Trace.beginSection("FingerprintUnlockController#onFingerprintAuthenticated");
+ if (mUpdateMonitor.isGoingToSleep()) {
+ mPendingAuthenticatedUserId = userId;
+ Trace.endSection();
+ return;
+ }
+ startWakeAndUnlock(calculateMode());
+ }
+
+ public void startWakeAndUnlock(int mode) {
+ // TODO(b/62444020): remove when this bug is fixed
+ Log.v(TAG, "startWakeAndUnlock(" + mode + ")");
+ boolean wasDeviceInteractive = mUpdateMonitor.isDeviceInteractive();
+ mMode = mode;
+ mHasScreenTurnedOnSinceAuthenticating = false;
+ if (mMode == MODE_WAKE_AND_UNLOCK_PULSING && pulsingOrAod()) {
+ // If we are waking the device up while we are pulsing the clock and the
+ // notifications would light up first, creating an unpleasant animation.
+ // Defer changing the screen brightness by forcing doze brightness on our window
+ // until the clock and the notifications are faded out.
+ mStatusBarWindowManager.setForceDozeBrightness(true);
+ }
+ if (!wasDeviceInteractive) {
+ if (DEBUG_FP_WAKELOCK) {
+ Log.i(TAG, "fp wakelock: Authenticated, waking up...");
+ }
+ mPowerManager.wakeUp(SystemClock.uptimeMillis(), "android.policy:FINGERPRINT");
+ }
+ Trace.beginSection("release wake-and-unlock");
+ releaseFingerprintWakeLock();
+ Trace.endSection();
+ switch (mMode) {
+ case MODE_DISMISS_BOUNCER:
+ Trace.beginSection("MODE_DISMISS");
+ mStatusBarKeyguardViewManager.notifyKeyguardAuthenticated(
+ false /* strongAuth */);
+ Trace.endSection();
+ break;
+ case MODE_UNLOCK:
+ case MODE_SHOW_BOUNCER:
+ Trace.beginSection("MODE_UNLOCK or MODE_SHOW_BOUNCER");
+ if (!wasDeviceInteractive) {
+ mStatusBarKeyguardViewManager.notifyDeviceWakeUpRequested();
+ mPendingShowBouncer = true;
+ } else {
+ showBouncer();
+ }
+ Trace.endSection();
+ break;
+ case MODE_WAKE_AND_UNLOCK_FROM_DREAM:
+ case MODE_WAKE_AND_UNLOCK_PULSING:
+ case MODE_WAKE_AND_UNLOCK:
+ if (mMode == MODE_WAKE_AND_UNLOCK_PULSING) {
+ Trace.beginSection("MODE_WAKE_AND_UNLOCK_PULSING");
+ mStatusBar.updateMediaMetaData(false /* metaDataChanged */,
+ true /* allowEnterAnimation */);
+ } else if (mMode == MODE_WAKE_AND_UNLOCK){
+ Trace.beginSection("MODE_WAKE_AND_UNLOCK");
+ mDozeScrimController.abortDoze();
+ } else {
+ Trace.beginSection("MODE_WAKE_AND_UNLOCK_FROM_DREAM");
+ mUpdateMonitor.awakenFromDream();
+ }
+ mStatusBarWindowManager.setStatusBarFocusable(false);
+ mKeyguardViewMediator.onWakeAndUnlocking();
+ mScrimController.setWakeAndUnlocking();
+ mDozeScrimController.setWakeAndUnlocking();
+ if (mStatusBar.getNavigationBarView() != null) {
+ mStatusBar.getNavigationBarView().setWakeAndUnlocking(true);
+ }
+ Trace.endSection();
+ break;
+ case MODE_ONLY_WAKE:
+ case MODE_NONE:
+ break;
+ }
+ mStatusBar.notifyFpAuthModeChanged();
+ Trace.endSection();
+ }
+
+ private void showBouncer() {
+ mStatusBarKeyguardViewManager.animateCollapsePanels(
+ FINGERPRINT_COLLAPSE_SPEEDUP_FACTOR);
+ mPendingShowBouncer = false;
+ }
+
+ @Override
+ public void onStartedGoingToSleep(int why) {
+ resetMode();
+ mPendingAuthenticatedUserId = -1;
+ }
+
+ @Override
+ public void onFinishedGoingToSleep(int why) {
+ Trace.beginSection("FingerprintUnlockController#onFinishedGoingToSleep");
+ if (mPendingAuthenticatedUserId != -1) {
+
+ // Post this to make sure it's executed after the device is fully locked.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onFingerprintAuthenticated(mPendingAuthenticatedUserId);
+ }
+ });
+ }
+ mPendingAuthenticatedUserId = -1;
+ Trace.endSection();
+ }
+
+ public boolean hasPendingAuthentication() {
+ return mPendingAuthenticatedUserId != -1
+ && mUpdateMonitor.isUnlockingWithFingerprintAllowed()
+ && mPendingAuthenticatedUserId == KeyguardUpdateMonitor.getCurrentUser();
+ }
+
+ public int getMode() {
+ return mMode;
+ }
+
+ private int calculateMode() {
+ boolean unlockingAllowed = mUpdateMonitor.isUnlockingWithFingerprintAllowed();
+ boolean deviceDreaming = mUpdateMonitor.isDreaming();
+
+ if (!mUpdateMonitor.isDeviceInteractive()) {
+ if (!mStatusBarKeyguardViewManager.isShowing()) {
+ return MODE_ONLY_WAKE;
+ } else if (mDozeScrimController.isPulsing() && unlockingAllowed) {
+ return MODE_WAKE_AND_UNLOCK_PULSING;
+ } else if (unlockingAllowed || !mUnlockMethodCache.isMethodSecure()) {
+ return MODE_WAKE_AND_UNLOCK;
+ } else {
+ return MODE_SHOW_BOUNCER;
+ }
+ }
+ if (unlockingAllowed && deviceDreaming) {
+ return MODE_WAKE_AND_UNLOCK_FROM_DREAM;
+ }
+ if (mStatusBarKeyguardViewManager.isShowing()) {
+ if (mStatusBarKeyguardViewManager.isBouncerShowing() && unlockingAllowed) {
+ return MODE_DISMISS_BOUNCER;
+ } else if (unlockingAllowed) {
+ return MODE_UNLOCK;
+ } else if (!mStatusBarKeyguardViewManager.isBouncerShowing()) {
+ return MODE_SHOW_BOUNCER;
+ }
+ }
+ return MODE_NONE;
+ }
+
+ @Override
+ public void onFingerprintAuthFailed() {
+ cleanup();
+ }
+
+ @Override
+ public void onFingerprintError(int msgId, String errString) {
+ cleanup();
+ }
+
+ private void cleanup() {
+ releaseFingerprintWakeLock();
+ }
+
+ public void startKeyguardFadingAway() {
+
+ // Disable brightness override when the ambient contents are fully invisible.
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mStatusBarWindowManager.setForceDozeBrightness(false);
+ }
+ }, StatusBar.FADE_KEYGUARD_DURATION_PULSING);
+ }
+
+ public void finishKeyguardFadingAway() {
+ resetMode();
+ }
+
+ private void resetMode() {
+ mMode = MODE_NONE;
+ mStatusBarWindowManager.setForceDozeBrightness(false);
+ if (mStatusBar.getNavigationBarView() != null) {
+ mStatusBar.getNavigationBarView().setWakeAndUnlocking(false);
+ }
+ mStatusBar.notifyFpAuthModeChanged();
+ }
+
+ private final WakefulnessLifecycle.Observer mWakefulnessObserver =
+ new WakefulnessLifecycle.Observer() {
+ @Override
+ public void onFinishedWakingUp() {
+ if (mPendingShowBouncer) {
+ FingerprintUnlockController.this.showBouncer();
+ }
+ }
+ };
+
+ private final ScreenLifecycle.Observer mScreenObserver =
+ new ScreenLifecycle.Observer() {
+ @Override
+ public void onScreenTurnedOn() {
+ mHasScreenTurnedOnSinceAuthenticating = true;
+ }
+ };
+
+ public boolean hasScreenTurnedOnSinceAuthenticating() {
+ return mHasScreenTurnedOnSinceAuthenticating;
+ }
+
+ public void dump(PrintWriter pw) {
+ pw.println(" FingerprintUnlockController:");
+ pw.print(" mMode="); pw.println(mMode);
+ pw.print(" mWakeLock="); pw.println(mWakeLock);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
new file mode 100644
index 0000000..c85571c
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2015 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.Context;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.Gefingerpoken;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+/**
+ * A helper class to handle touches on the heads-up views.
+ */
+public class HeadsUpTouchHelper implements Gefingerpoken {
+
+ private HeadsUpManager mHeadsUpManager;
+ private NotificationStackScrollLayout mStackScroller;
+ private int mTrackingPointer;
+ private float mTouchSlop;
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+ private boolean mTouchingHeadsUpView;
+ private boolean mTrackingHeadsUp;
+ private boolean mCollapseSnoozes;
+ private NotificationPanelView mPanel;
+ private ExpandableNotificationRow mPickedChild;
+
+ public HeadsUpTouchHelper(HeadsUpManager headsUpManager,
+ NotificationStackScrollLayout stackScroller,
+ NotificationPanelView notificationPanelView) {
+ mHeadsUpManager = headsUpManager;
+ mStackScroller = stackScroller;
+ mPanel = notificationPanelView;
+ Context context = stackScroller.getContext();
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ mTouchSlop = configuration.getScaledTouchSlop();
+ }
+
+ public boolean isTrackingHeadsUp() {
+ return mTrackingHeadsUp;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (!mTouchingHeadsUpView && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ setTrackingHeadsUp(false);
+ ExpandableView child = mStackScroller.getChildAtRawPosition(x, y);
+ mTouchingHeadsUpView = false;
+ if (child instanceof ExpandableNotificationRow) {
+ mPickedChild = (ExpandableNotificationRow) child;
+ mTouchingHeadsUpView = !mStackScroller.isExpanded()
+ && mPickedChild.isHeadsUp() && mPickedChild.isPinned();
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ mTrackingPointer = event.getPointerId(newIndex);
+ mInitialTouchX = event.getX(newIndex);
+ mInitialTouchY = event.getY(newIndex);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final float h = y - mInitialTouchY;
+ if (mTouchingHeadsUpView && Math.abs(h) > mTouchSlop
+ && Math.abs(h) > Math.abs(x - mInitialTouchX)) {
+ setTrackingHeadsUp(true);
+ mCollapseSnoozes = h < 0;
+ mInitialTouchX = x;
+ mInitialTouchY = y;
+ int expandedHeight = mPickedChild.getActualHeight();
+ mPanel.setPanelScrimMinFraction((float) expandedHeight
+ / mPanel.getMaxPanelHeight());
+ mPanel.startExpandMotion(x, y, true /* startTracking */, expandedHeight);
+ mPanel.startExpandingFromPeek();
+ // This call needs to be after the expansion start otherwise we will get a
+ // flicker of one frame as it's not expanded yet.
+ mHeadsUpManager.unpinAll();
+ mPanel.clearNotificationEffects();
+ endMotion();
+ return true;
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ if (mPickedChild != null && mTouchingHeadsUpView) {
+ // We may swallow this click if the heads up just came in.
+ if (mHeadsUpManager.shouldSwallowClick(
+ mPickedChild.getStatusBarNotification().getKey())) {
+ endMotion();
+ return true;
+ }
+ }
+ endMotion();
+ break;
+ }
+ return false;
+ }
+
+ private void setTrackingHeadsUp(boolean tracking) {
+ mTrackingHeadsUp = tracking;
+ mHeadsUpManager.setTrackingHeadsUp(tracking);
+ mPanel.setTrackingHeadsUp(tracking);
+ }
+
+ public void notifyFling(boolean collapse) {
+ if (collapse && mCollapseSnoozes) {
+ mHeadsUpManager.snooze();
+ }
+ mCollapseSnoozes = false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!mTrackingHeadsUp) {
+ return false;
+ }
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ endMotion();
+ setTrackingHeadsUp(false);
+ break;
+ }
+ return true;
+ }
+
+ private void endMotion() {
+ mTrackingPointer = -1;
+ mPickedChild = null;
+ mTouchingHeadsUpView = false;
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java b/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
new file mode 100644
index 0000000..df1ffda
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
@@ -0,0 +1,587 @@
+/*
+ * 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.content.Context;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.FlingAnimationUtils;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+
+/**
+ * A touch handler of the keyguard which is responsible for launching phone and camera affordances.
+ */
+public class KeyguardAffordanceHelper {
+
+ public static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.5f;
+ public static final long HINT_PHASE1_DURATION = 200;
+ private static final long HINT_PHASE2_DURATION = 350;
+ private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f;
+ private static final int HINT_CIRCLE_OPEN_DURATION = 500;
+
+ private final Context mContext;
+ private final Callback mCallback;
+
+ private FlingAnimationUtils mFlingAnimationUtils;
+ private VelocityTracker mVelocityTracker;
+ private boolean mSwipingInProgress;
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+ private float mTranslation;
+ private float mTranslationOnDown;
+ private int mTouchSlop;
+ private int mMinTranslationAmount;
+ private int mMinFlingVelocity;
+ private int mHintGrowAmount;
+ private KeyguardAffordanceView mLeftIcon;
+ private KeyguardAffordanceView mCenterIcon;
+ private KeyguardAffordanceView mRightIcon;
+ private Animator mSwipeAnimator;
+ private FalsingManager mFalsingManager;
+ private int mMinBackgroundRadius;
+ private boolean mMotionCancelled;
+ private int mTouchTargetSize;
+ private View mTargetedView;
+ private boolean mTouchSlopExeeded;
+ private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mSwipeAnimator = null;
+ mSwipingInProgress = false;
+ mTargetedView = null;
+ }
+ };
+ private Runnable mAnimationEndRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onAnimationToSideEnded();
+ }
+ };
+
+ KeyguardAffordanceHelper(Callback callback, Context context) {
+ mContext = context;
+ mCallback = callback;
+ initIcons();
+ updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false);
+ updateIcon(mCenterIcon, 0.0f, mCenterIcon.getRestingAlpha(), false, false, true, false);
+ updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false);
+ initDimens();
+ }
+
+ private void initDimens() {
+ final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+ mTouchSlop = configuration.getScaledPagingTouchSlop();
+ mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+ mMinTranslationAmount = mContext.getResources().getDimensionPixelSize(
+ R.dimen.keyguard_min_swipe_amount);
+ mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
+ R.dimen.keyguard_affordance_min_background_radius);
+ mTouchTargetSize = mContext.getResources().getDimensionPixelSize(
+ R.dimen.keyguard_affordance_touch_target_size);
+ mHintGrowAmount =
+ mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways);
+ mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f);
+ mFalsingManager = FalsingManager.getInstance(mContext);
+ }
+
+ private void initIcons() {
+ mLeftIcon = mCallback.getLeftIcon();
+ mCenterIcon = mCallback.getCenterIcon();
+ mRightIcon = mCallback.getRightIcon();
+ updatePreviews();
+ }
+
+ public void updatePreviews() {
+ mLeftIcon.setPreviewView(mCallback.getLeftPreview());
+ mRightIcon.setPreviewView(mCallback.getRightPreview());
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+ if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ final float y = event.getY();
+ final float x = event.getX();
+
+ boolean isUp = false;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ View targetView = getIconAtPosition(x, y);
+ if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) {
+ mMotionCancelled = true;
+ return false;
+ }
+ if (mTargetedView != null) {
+ cancelAnimation();
+ } else {
+ mTouchSlopExeeded = false;
+ }
+ startSwiping(targetView);
+ mInitialTouchX = x;
+ mInitialTouchY = y;
+ mTranslationOnDown = mTranslation;
+ initVelocityTracker();
+ trackMovement(event);
+ mMotionCancelled = false;
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ mMotionCancelled = true;
+ endMotion(true /* forceSnapBack */, x, y);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ trackMovement(event);
+ float xDist = x - mInitialTouchX;
+ float yDist = y - mInitialTouchY;
+ float distance = (float) Math.hypot(xDist, yDist);
+ if (!mTouchSlopExeeded && distance > mTouchSlop) {
+ mTouchSlopExeeded = true;
+ }
+ if (mSwipingInProgress) {
+ if (mTargetedView == mRightIcon) {
+ distance = mTranslationOnDown - distance;
+ distance = Math.min(0, distance);
+ } else {
+ distance = mTranslationOnDown + distance;
+ distance = Math.max(0, distance);
+ }
+ setTranslation(distance, false /* isReset */, false /* animateReset */);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ isUp = true;
+ case MotionEvent.ACTION_CANCEL:
+ boolean hintOnTheRight = mTargetedView == mRightIcon;
+ trackMovement(event);
+ endMotion(!isUp, x, y);
+ if (!mTouchSlopExeeded && isUp) {
+ mCallback.onIconClicked(hintOnTheRight);
+ }
+ break;
+ }
+ return true;
+ }
+
+ private void startSwiping(View targetView) {
+ mCallback.onSwipingStarted(targetView == mRightIcon);
+ mSwipingInProgress = true;
+ mTargetedView = targetView;
+ }
+
+ private View getIconAtPosition(float x, float y) {
+ if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) {
+ return mLeftIcon;
+ }
+ if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) {
+ return mRightIcon;
+ }
+ return null;
+ }
+
+ public boolean isOnAffordanceIcon(float x, float y) {
+ return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y);
+ }
+
+ private boolean isOnIcon(View icon, float x, float y) {
+ float iconX = icon.getX() + icon.getWidth() / 2.0f;
+ float iconY = icon.getY() + icon.getHeight() / 2.0f;
+ double distance = Math.hypot(x - iconX, y - iconY);
+ return distance <= mTouchTargetSize / 2;
+ }
+
+ private void endMotion(boolean forceSnapBack, float lastX, float lastY) {
+ if (mSwipingInProgress) {
+ flingWithCurrentVelocity(forceSnapBack, lastX, lastY);
+ } else {
+ mTargetedView = null;
+ }
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private boolean rightSwipePossible() {
+ return mRightIcon.getVisibility() == View.VISIBLE;
+ }
+
+ private boolean leftSwipePossible() {
+ return mLeftIcon.getVisibility() == View.VISIBLE;
+ }
+
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return false;
+ }
+
+ public void startHintAnimation(boolean right,
+ Runnable onFinishedListener) {
+ cancelAnimation();
+ startHintAnimationPhase1(right, onFinishedListener);
+ }
+
+ private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) {
+ final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
+ ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount);
+ animator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCancelled) {
+ mSwipeAnimator = null;
+ mTargetedView = null;
+ onFinishedListener.run();
+ } else {
+ startUnlockHintAnimationPhase2(right, onFinishedListener);
+ }
+ }
+ });
+ animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ animator.setDuration(HINT_PHASE1_DURATION);
+ animator.start();
+ mSwipeAnimator = animator;
+ mTargetedView = targetView;
+ }
+
+ /**
+ * Phase 2: Move back.
+ */
+ private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) {
+ ValueAnimator animator = getAnimatorToRadius(right, 0);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mSwipeAnimator = null;
+ mTargetedView = null;
+ onFinishedListener.run();
+ }
+ });
+ animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+ animator.setDuration(HINT_PHASE2_DURATION);
+ animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
+ animator.start();
+ mSwipeAnimator = animator;
+ }
+
+ private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
+ final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
+ ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float newRadius = (float) animation.getAnimatedValue();
+ targetView.setCircleRadiusWithoutAnimation(newRadius);
+ float translation = getTranslationFromRadius(newRadius);
+ mTranslation = right ? -translation : translation;
+ updateIconsFromTranslation(targetView);
+ }
+ });
+ return animator;
+ }
+
+ private void cancelAnimation() {
+ if (mSwipeAnimator != null) {
+ mSwipeAnimator.cancel();
+ }
+ }
+
+ private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) {
+ float vel = getCurrentVelocity(lastX, lastY);
+
+ // We snap back if the current translation is not far enough
+ boolean snapBack = false;
+ if (mCallback.needsAntiFalsing()) {
+ snapBack = snapBack || mFalsingManager.isFalseTouch();
+ }
+ snapBack = snapBack || isBelowFalsingThreshold();
+
+ // or if the velocity is in the opposite direction.
+ boolean velIsInWrongDirection = vel * mTranslation < 0;
+ snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection;
+ vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
+ fling(vel, snapBack || forceSnapBack, mTranslation < 0);
+ }
+
+ private boolean isBelowFalsingThreshold() {
+ return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount();
+ }
+
+ private int getMinTranslationAmount() {
+ float factor = mCallback.getAffordanceFalsingFactor();
+ return (int) (mMinTranslationAmount * factor);
+ }
+
+ private void fling(float vel, final boolean snapBack, boolean right) {
+ float target = right ? -mCallback.getMaxTranslationDistance()
+ : mCallback.getMaxTranslationDistance();
+ target = snapBack ? 0 : target;
+
+ ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target);
+ mFlingAnimationUtils.apply(animator, mTranslation, target, vel);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mTranslation = (float) animation.getAnimatedValue();
+ }
+ });
+ animator.addListener(mFlingEndListener);
+ if (!snapBack) {
+ startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable, right);
+ mCallback.onAnimationToSideStarted(right, mTranslation, vel);
+ } else {
+ reset(true);
+ }
+ animator.start();
+ mSwipeAnimator = animator;
+ if (snapBack) {
+ mCallback.onSwipingAborted();
+ }
+ }
+
+ private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable,
+ boolean right) {
+ KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
+ targetView.finishAnimation(velocity, mAnimationEndRunnable);
+ }
+
+ private void setTranslation(float translation, boolean isReset, boolean animateReset) {
+ translation = rightSwipePossible() ? translation : Math.max(0, translation);
+ translation = leftSwipePossible() ? translation : Math.min(0, translation);
+ float absTranslation = Math.abs(translation);
+ if (translation != mTranslation || isReset) {
+ KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon;
+ KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon;
+ float alpha = absTranslation / getMinTranslationAmount();
+
+ // We interpolate the alpha of the other icons to 0
+ float fadeOutAlpha = 1.0f - alpha;
+ fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f);
+
+ boolean animateIcons = isReset && animateReset;
+ boolean forceNoCircleAnimation = isReset && !animateReset;
+ float radius = getRadiusFromTranslation(absTranslation);
+ boolean slowAnimation = isReset && isBelowFalsingThreshold();
+ if (!isReset) {
+ updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(),
+ false, false, false, false);
+ } else {
+ updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(),
+ animateIcons, slowAnimation, true /* isReset */, forceNoCircleAnimation);
+ }
+ updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(),
+ animateIcons, slowAnimation, isReset, forceNoCircleAnimation);
+ updateIcon(mCenterIcon, 0.0f, fadeOutAlpha * mCenterIcon.getRestingAlpha(),
+ animateIcons, slowAnimation, isReset, forceNoCircleAnimation);
+
+ mTranslation = translation;
+ }
+ }
+
+ private void updateIconsFromTranslation(KeyguardAffordanceView targetView) {
+ float absTranslation = Math.abs(mTranslation);
+ float alpha = absTranslation / getMinTranslationAmount();
+
+ // We interpolate the alpha of the other icons to 0
+ float fadeOutAlpha = 1.0f - alpha;
+ fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
+
+ // We interpolate the alpha of the targetView to 1
+ KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon;
+ updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false);
+ updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false);
+ updateIconAlpha(mCenterIcon, fadeOutAlpha * mCenterIcon.getRestingAlpha(), false);
+ }
+
+ private float getTranslationFromRadius(float circleSize) {
+ float translation = (circleSize - mMinBackgroundRadius)
+ / BACKGROUND_RADIUS_SCALE_FACTOR;
+ return translation > 0.0f ? translation + mTouchSlop : 0.0f;
+ }
+
+ private float getRadiusFromTranslation(float translation) {
+ if (translation <= mTouchSlop) {
+ return 0.0f;
+ }
+ return (translation - mTouchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius;
+ }
+
+ public void animateHideLeftRightIcon() {
+ cancelAnimation();
+ updateIcon(mRightIcon, 0f, 0f, true, false, false, false);
+ updateIcon(mLeftIcon, 0f, 0f, true, false, false, false);
+ }
+
+ private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha,
+ boolean animate, boolean slowRadiusAnimation, boolean force,
+ boolean forceNoCircleAnimation) {
+ if (view.getVisibility() != View.VISIBLE && !force) {
+ return;
+ }
+ if (forceNoCircleAnimation) {
+ view.setCircleRadiusWithoutAnimation(circleRadius);
+ } else {
+ view.setCircleRadius(circleRadius, slowRadiusAnimation);
+ }
+ updateIconAlpha(view, alpha, animate);
+ }
+
+ private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) {
+ float scale = getScale(alpha, view);
+ alpha = Math.min(1.0f, alpha);
+ view.setImageAlpha(alpha, animate);
+ view.setImageScale(scale, animate);
+ }
+
+ private float getScale(float alpha, KeyguardAffordanceView icon) {
+ float scale = alpha / icon.getRestingAlpha() * 0.2f +
+ KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT;
+ return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT);
+ }
+
+ private void trackMovement(MotionEvent event) {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.addMovement(event);
+ }
+ }
+
+ private void initVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ }
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ private float getCurrentVelocity(float lastX, float lastY) {
+ if (mVelocityTracker == null) {
+ return 0;
+ }
+ mVelocityTracker.computeCurrentVelocity(1000);
+ float aX = mVelocityTracker.getXVelocity();
+ float aY = mVelocityTracker.getYVelocity();
+ float bX = lastX - mInitialTouchX;
+ float bY = lastY - mInitialTouchY;
+ float bLen = (float) Math.hypot(bX, bY);
+ // Project the velocity onto the distance vector: a * b / |b|
+ float projectedVelocity = (aX * bX + aY * bY) / bLen;
+ if (mTargetedView == mRightIcon) {
+ projectedVelocity = -projectedVelocity;
+ }
+ return projectedVelocity;
+ }
+
+ public void onConfigurationChanged() {
+ initDimens();
+ initIcons();
+ }
+
+ public void onRtlPropertiesChanged() {
+ initIcons();
+ }
+
+ public void reset(boolean animate) {
+ cancelAnimation();
+ setTranslation(0.0f, true /* isReset */, animate);
+ mMotionCancelled = true;
+ if (mSwipingInProgress) {
+ mCallback.onSwipingAborted();
+ mSwipingInProgress = false;
+ }
+ }
+
+ public boolean isSwipingInProgress() {
+ return mSwipingInProgress;
+ }
+
+ public void launchAffordance(boolean animate, boolean left) {
+ if (mSwipingInProgress) {
+ // We don't want to mess with the state if the user is actually swiping already.
+ return;
+ }
+ KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon;
+ KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon;
+ startSwiping(targetView);
+ if (animate) {
+ fling(0, false, !left);
+ updateIcon(otherView, 0.0f, 0, true, false, true, false);
+ updateIcon(mCenterIcon, 0.0f, 0, true, false, true, false);
+ } else {
+ mCallback.onAnimationToSideStarted(!left, mTranslation, 0);
+ mTranslation = left ? mCallback.getMaxTranslationDistance()
+ : mCallback.getMaxTranslationDistance();
+ updateIcon(mCenterIcon, 0.0f, 0.0f, false, false, true, false);
+ updateIcon(otherView, 0.0f, 0.0f, false, false, true, false);
+ targetView.instantFinishAnimation();
+ mFlingEndListener.onAnimationEnd(null);
+ mAnimationEndRunnable.run();
+ }
+ }
+
+ public interface Callback {
+
+ /**
+ * Notifies the callback when an animation to a side page was started.
+ *
+ * @param rightPage Is the page animated to the right page?
+ */
+ void onAnimationToSideStarted(boolean rightPage, float translation, float vel);
+
+ /**
+ * Notifies the callback the animation to a side page has ended.
+ */
+ void onAnimationToSideEnded();
+
+ float getMaxTranslationDistance();
+
+ void onSwipingStarted(boolean rightIcon);
+
+ void onSwipingAborted();
+
+ void onIconClicked(boolean rightIcon);
+
+ KeyguardAffordanceView getLeftIcon();
+
+ KeyguardAffordanceView getCenterIcon();
+
+ KeyguardAffordanceView getRightIcon();
+
+ View getLeftPreview();
+
+ View getRightPreview();
+
+ /**
+ * @return The factor the minimum swipe amount should be multiplied with.
+ */
+ float getAffordanceFalsingFactor();
+
+ boolean needsAntiFalsing();
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
new file mode 100644
index 0000000..f058862
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -0,0 +1,875 @@
+/*
+ * 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 static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_BUTTON;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_UNLOCK;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_BUTTON;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_UNLOCK;
+
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.MediaStore;
+import android.service.media.CameraPrewarmService;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.EventLogTags;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.plugins.IntentButtonProvider;
+import com.android.systemui.plugins.IntentButtonProvider.IntentButton;
+import com.android.systemui.plugins.IntentButtonProvider.IntentButton.IconState;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.policy.AccessibilityController;
+import com.android.systemui.statusbar.policy.ExtensionController;
+import com.android.systemui.statusbar.policy.ExtensionController.Extension;
+import com.android.systemui.statusbar.policy.FlashlightController;
+import com.android.systemui.statusbar.policy.PreviewInflater;
+import com.android.systemui.tuner.LockscreenFragment.LockButtonFactory;
+import com.android.systemui.tuner.TunerService;
+
+/**
+ * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status
+ * text.
+ */
+public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickListener,
+ UnlockMethodCache.OnUnlockMethodChangedListener,
+ AccessibilityController.AccessibilityStateChangedCallback, View.OnLongClickListener {
+
+ final static String TAG = "StatusBar/KeyguardBottomAreaView";
+
+ public static final String CAMERA_LAUNCH_SOURCE_AFFORDANCE = "lockscreen_affordance";
+ public static final String CAMERA_LAUNCH_SOURCE_WIGGLE = "wiggle_gesture";
+ public static final String CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP = "power_double_tap";
+ public static final String CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = "lift_to_launch_ml";
+
+ public static final String EXTRA_CAMERA_LAUNCH_SOURCE
+ = "com.android.systemui.camera_launch_source";
+
+ private static final String LEFT_BUTTON_PLUGIN
+ = "com.android.systemui.action.PLUGIN_LOCKSCREEN_LEFT_BUTTON";
+ private static final String RIGHT_BUTTON_PLUGIN
+ = "com.android.systemui.action.PLUGIN_LOCKSCREEN_RIGHT_BUTTON";
+
+ private static final Intent SECURE_CAMERA_INTENT =
+ new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE)
+ .addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ public 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 mRightAffordanceView;
+ private KeyguardAffordanceView mLeftAffordanceView;
+ private LockIcon mLockIcon;
+ private ViewGroup mIndicationArea;
+ private TextView mEnterpriseDisclosure;
+ private TextView mIndicationText;
+ private ViewGroup mPreviewContainer;
+ private ViewGroup mOverlayContainer;
+
+ private View mLeftPreview;
+ private View mCameraPreview;
+
+ private ActivityStarter mActivityStarter;
+ private UnlockMethodCache mUnlockMethodCache;
+ private LockPatternUtils mLockPatternUtils;
+ private FlashlightController mFlashlightController;
+ private PreviewInflater mPreviewInflater;
+ private KeyguardIndicationController mIndicationController;
+ private AccessibilityController mAccessibilityController;
+ private StatusBar mStatusBar;
+ private KeyguardAffordanceHelper mAffordanceHelper;
+
+ private boolean mUserSetupComplete;
+ private boolean mPrewarmBound;
+ private Messenger mPrewarmMessenger;
+ private final ServiceConnection mPrewarmConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mPrewarmMessenger = new Messenger(service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mPrewarmMessenger = null;
+ }
+ };
+
+ private boolean mLeftIsVoiceAssist;
+ private AssistManager mAssistManager;
+ private Drawable mLeftAssistIcon;
+
+ private IntentButton mRightButton = new DefaultRightButton();
+ private Extension<IntentButton> mRightExtension;
+ private String mRightButtonStr;
+ private IntentButton mLeftButton = new DefaultLeftButton();
+ private Extension<IntentButton> mLeftExtension;
+ private String mLeftButtonStr;
+ private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
+ private boolean mDozing;
+
+ public KeyguardBottomAreaView(Context context) {
+ this(context, null);
+ }
+
+ public KeyguardBottomAreaView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public KeyguardBottomAreaView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public KeyguardBottomAreaView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ String label = null;
+ if (host == mLockIcon) {
+ label = getResources().getString(R.string.unlock_label);
+ } else if (host == mRightAffordanceView) {
+ label = getResources().getString(R.string.camera_label);
+ } else if (host == mLeftAffordanceView) {
+ if (mLeftIsVoiceAssist) {
+ label = getResources().getString(R.string.voice_assist_label);
+ } else {
+ label = getResources().getString(R.string.phone_label);
+ }
+ }
+ info.addAction(new AccessibilityAction(ACTION_CLICK, label));
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == ACTION_CLICK) {
+ if (host == mLockIcon) {
+ mStatusBar.animateCollapsePanels(
+ CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */);
+ return true;
+ } else if (host == mRightAffordanceView) {
+ launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE);
+ return true;
+ } else if (host == mLeftAffordanceView) {
+ launchLeftAffordance();
+ return true;
+ }
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ };
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mLockPatternUtils = new LockPatternUtils(mContext);
+ mPreviewContainer = findViewById(R.id.preview_container);
+ mOverlayContainer = findViewById(R.id.overlay_container);
+ mRightAffordanceView = findViewById(R.id.camera_button);
+ mLeftAffordanceView = findViewById(R.id.left_button);
+ mLockIcon = findViewById(R.id.lock_icon);
+ mIndicationArea = findViewById(R.id.keyguard_indication_area);
+ mEnterpriseDisclosure = findViewById(
+ R.id.keyguard_indication_enterprise_disclosure);
+ mIndicationText = findViewById(R.id.keyguard_indication_text);
+ updateCameraVisibility();
+ mUnlockMethodCache = UnlockMethodCache.getInstance(getContext());
+ mUnlockMethodCache.addListener(this);
+ KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+ mLockIcon.setScreenOn(updateMonitor.isScreenOn());
+ mLockIcon.setDeviceInteractive(updateMonitor.isDeviceInteractive());
+ mLockIcon.update();
+ setClipChildren(false);
+ setClipToPadding(false);
+ mPreviewInflater = new PreviewInflater(mContext, new LockPatternUtils(mContext));
+ inflateCameraPreview();
+ mLockIcon.setOnClickListener(this);
+ mLockIcon.setOnLongClickListener(this);
+ mRightAffordanceView.setOnClickListener(this);
+ mLeftAffordanceView.setOnClickListener(this);
+ initAccessibility();
+ mActivityStarter = Dependency.get(ActivityStarter.class);
+ mFlashlightController = Dependency.get(FlashlightController.class);
+ mAccessibilityController = Dependency.get(AccessibilityController.class);
+ mAssistManager = Dependency.get(AssistManager.class);
+ mLockIcon.setAccessibilityController(mAccessibilityController);
+ updateLeftAffordance();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mAccessibilityController.addStateChangedCallback(this);
+ mRightExtension = Dependency.get(ExtensionController.class).newExtension(IntentButton.class)
+ .withPlugin(IntentButtonProvider.class, RIGHT_BUTTON_PLUGIN,
+ p -> p.getIntentButton())
+ .withTunerFactory(new LockButtonFactory(mContext, LOCKSCREEN_RIGHT_BUTTON))
+ .withDefault(() -> new DefaultRightButton())
+ .withCallback(button -> setRightButton(button))
+ .build();
+ mLeftExtension = Dependency.get(ExtensionController.class).newExtension(IntentButton.class)
+ .withPlugin(IntentButtonProvider.class, LEFT_BUTTON_PLUGIN,
+ p -> p.getIntentButton())
+ .withTunerFactory(new LockButtonFactory(mContext, LOCKSCREEN_LEFT_BUTTON))
+ .withDefault(() -> new DefaultLeftButton())
+ .withCallback(button -> setLeftButton(button))
+ .build();
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
+ getContext().registerReceiverAsUser(mDevicePolicyReceiver,
+ UserHandle.ALL, filter, null, null);
+ KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallback);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mAccessibilityController.removeStateChangedCallback(this);
+ mRightExtension.destroy();
+ mLeftExtension.destroy();
+ getContext().unregisterReceiver(mDevicePolicyReceiver);
+ KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallback);
+ }
+
+ private void initAccessibility() {
+ mLockIcon.setAccessibilityDelegate(mAccessibilityDelegate);
+ mLeftAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate);
+ mRightAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ int indicationBottomMargin = getResources().getDimensionPixelSize(
+ R.dimen.keyguard_indication_margin_bottom);
+ MarginLayoutParams mlp = (MarginLayoutParams) mIndicationArea.getLayoutParams();
+ if (mlp.bottomMargin != indicationBottomMargin) {
+ mlp.bottomMargin = indicationBottomMargin;
+ mIndicationArea.setLayoutParams(mlp);
+ }
+
+ // Respect font size setting.
+ mEnterpriseDisclosure.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.text_size_small_material));
+ mIndicationText.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.text_size_small_material));
+
+ ViewGroup.LayoutParams lp = mRightAffordanceView.getLayoutParams();
+ lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width);
+ lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height);
+ mRightAffordanceView.setLayoutParams(lp);
+ updateRightAffordanceIcon();
+
+ lp = mLockIcon.getLayoutParams();
+ lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width);
+ lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height);
+ mLockIcon.setLayoutParams(lp);
+ mLockIcon.setContentDescription(getContext().getText(R.string.accessibility_unlock_button));
+ mLockIcon.update(true /* force */);
+
+ lp = mLeftAffordanceView.getLayoutParams();
+ lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width);
+ lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height);
+ mLeftAffordanceView.setLayoutParams(lp);
+ updateLeftAffordanceIcon();
+ }
+
+ private void updateRightAffordanceIcon() {
+ IconState state = mRightButton.getIcon();
+ mRightAffordanceView.setVisibility(!mDozing && state.isVisible ? View.VISIBLE : View.GONE);
+ mRightAffordanceView.setImageDrawable(state.drawable, state.tint);
+ mRightAffordanceView.setContentDescription(state.contentDescription);
+ }
+
+ public void setStatusBar(StatusBar statusBar) {
+ mStatusBar = statusBar;
+ updateCameraVisibility(); // in case onFinishInflate() was called too early
+ }
+
+ public void setAffordanceHelper(KeyguardAffordanceHelper affordanceHelper) {
+ mAffordanceHelper = affordanceHelper;
+ }
+
+ public void setUserSetupComplete(boolean userSetupComplete) {
+ mUserSetupComplete = userSetupComplete;
+ updateCameraVisibility();
+ updateLeftAffordanceIcon();
+ }
+
+ private Intent getCameraIntent() {
+ return mRightButton.getIntent();
+ }
+
+ /**
+ * Resolves the intent to launch the camera application.
+ */
+ public ResolveInfo resolveCameraIntent() {
+ return mContext.getPackageManager().resolveActivityAsUser(getCameraIntent(),
+ PackageManager.MATCH_DEFAULT_ONLY,
+ KeyguardUpdateMonitor.getCurrentUser());
+ }
+
+ private void updateCameraVisibility() {
+ if (mRightAffordanceView == null) {
+ // Things are not set up yet; reply hazy, ask again later
+ return;
+ }
+ mRightAffordanceView.setVisibility(!mDozing && mRightButton.getIcon().isVisible
+ ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Set an alternate icon for the left assist affordance (replace the mic icon)
+ */
+ public void setLeftAssistIcon(Drawable drawable) {
+ mLeftAssistIcon = drawable;
+ updateLeftAffordanceIcon();
+ }
+
+ private void updateLeftAffordanceIcon() {
+ IconState state = mLeftButton.getIcon();
+ mLeftAffordanceView.setVisibility(!mDozing && state.isVisible ? View.VISIBLE : View.GONE);
+ mLeftAffordanceView.setImageDrawable(state.drawable, state.tint);
+ mLeftAffordanceView.setContentDescription(state.contentDescription);
+ }
+
+ public boolean isLeftVoiceAssist() {
+ return mLeftIsVoiceAssist;
+ }
+
+ private boolean isPhoneVisible() {
+ PackageManager pm = mContext.getPackageManager();
+ return pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+ && pm.resolveActivity(PHONE_INTENT, 0) != null;
+ }
+
+ @Override
+ public void onStateChanged(boolean accessibilityEnabled, boolean touchExplorationEnabled) {
+ mRightAffordanceView.setClickable(touchExplorationEnabled);
+ mLeftAffordanceView.setClickable(touchExplorationEnabled);
+ mRightAffordanceView.setFocusable(accessibilityEnabled);
+ mLeftAffordanceView.setFocusable(accessibilityEnabled);
+ mLockIcon.update();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mRightAffordanceView) {
+ launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE);
+ } else if (v == mLeftAffordanceView) {
+ launchLeftAffordance();
+ } if (v == mLockIcon) {
+ if (!mAccessibilityController.isAccessibilityEnabled()) {
+ handleTrustCircleClick();
+ } else {
+ mStatusBar.animateCollapsePanels(
+ CommandQueue.FLAG_EXCLUDE_NONE, true /* force */);
+ }
+ }
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ handleTrustCircleClick();
+ return true;
+ }
+
+ private void handleTrustCircleClick() {
+ mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_LOCK, 0 /* lengthDp - N/A */,
+ 0 /* velocityDp - N/A */);
+ mIndicationController.showTransientIndication(
+ R.string.keyguard_indication_trust_disabled);
+ mLockPatternUtils.requireCredentialEntry(KeyguardUpdateMonitor.getCurrentUser());
+ }
+
+ public void bindCameraPrewarmService() {
+ Intent intent = getCameraIntent();
+ ActivityInfo targetInfo = PreviewInflater.getTargetActivityInfo(mContext, intent,
+ KeyguardUpdateMonitor.getCurrentUser(), true /* onlyDirectBootAware */);
+ if (targetInfo != null && targetInfo.metaData != null) {
+ String clazz = targetInfo.metaData.getString(
+ MediaStore.META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE);
+ if (clazz != null) {
+ Intent serviceIntent = new Intent();
+ serviceIntent.setClassName(targetInfo.packageName, clazz);
+ serviceIntent.setAction(CameraPrewarmService.ACTION_PREWARM);
+ try {
+ if (getContext().bindServiceAsUser(serviceIntent, mPrewarmConnection,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+ new UserHandle(UserHandle.USER_CURRENT))) {
+ mPrewarmBound = true;
+ }
+ } catch (SecurityException e) {
+ Log.w(TAG, "Unable to bind to prewarm service package=" + targetInfo.packageName
+ + " class=" + clazz, e);
+ }
+ }
+ }
+ }
+
+ public void unbindCameraPrewarmService(boolean launched) {
+ if (mPrewarmBound) {
+ if (mPrewarmMessenger != null && launched) {
+ try {
+ mPrewarmMessenger.send(Message.obtain(null /* handler */,
+ CameraPrewarmService.MSG_CAMERA_FIRED));
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error sending camera fired message", e);
+ }
+ }
+ mContext.unbindService(mPrewarmConnection);
+ mPrewarmBound = false;
+ }
+ }
+
+ public void launchCamera(String source) {
+ final Intent intent = getCameraIntent();
+ intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source);
+ boolean wouldLaunchResolverActivity = PreviewInflater.wouldLaunchResolverActivity(
+ mContext, intent, KeyguardUpdateMonitor.getCurrentUser());
+ if (intent == SECURE_CAMERA_INTENT && !wouldLaunchResolverActivity) {
+ AsyncTask.execute(new Runnable() {
+ @Override
+ public void run() {
+ int result = ActivityManager.START_CANCELED;
+
+ // Normally an activity will set it's requested rotation
+ // animation on its window. However when launching an activity
+ // causes the orientation to change this is too late. In these cases
+ // the default animation is used. This doesn't look good for
+ // the camera (as it rotates the camera contents out of sync
+ // with physical reality). So, we ask the WindowManager to
+ // force the crossfade animation if an orientation change
+ // happens to occur during the launch.
+ ActivityOptions o = ActivityOptions.makeBasic();
+ o.setDisallowEnterPictureInPictureWhileLaunching(true);
+ o.setRotationAnimationHint(
+ WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS);
+ try {
+ result = ActivityManager.getService().startActivityAsUser(
+ null, getContext().getBasePackageName(),
+ intent,
+ intent.resolveTypeIfNeeded(getContext().getContentResolver()),
+ null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, o.toBundle(),
+ UserHandle.CURRENT.getIdentifier());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to start camera activity", e);
+ }
+ final boolean launched = isSuccessfulLaunch(result);
+ post(new Runnable() {
+ @Override
+ public void run() {
+ unbindCameraPrewarmService(launched);
+ }
+ });
+ }
+ });
+ } else {
+
+ // We need to delay starting the activity because ResolverActivity finishes itself if
+ // launched behind lockscreen.
+ mActivityStarter.startActivity(intent, false /* dismissShade */,
+ new ActivityStarter.Callback() {
+ @Override
+ public void onActivityStarted(int resultCode) {
+ unbindCameraPrewarmService(isSuccessfulLaunch(resultCode));
+ }
+ });
+ }
+ }
+
+ private static boolean isSuccessfulLaunch(int result) {
+ return result == ActivityManager.START_SUCCESS
+ || result == ActivityManager.START_DELIVERED_TO_TOP
+ || result == ActivityManager.START_TASK_TO_FRONT;
+ }
+
+ public void launchLeftAffordance() {
+ if (mLeftIsVoiceAssist) {
+ launchVoiceAssist();
+ } else {
+ launchPhone();
+ }
+ }
+
+ private void launchVoiceAssist() {
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ mAssistManager.launchVoiceAssistFromKeyguard();
+ }
+ };
+ if (mStatusBar.isKeyguardCurrentlySecure()) {
+ AsyncTask.execute(runnable);
+ } else {
+ boolean dismissShade = !TextUtils.isEmpty(mRightButtonStr)
+ && Dependency.get(TunerService.class).getValue(LOCKSCREEN_RIGHT_UNLOCK, 1) != 0;
+ mStatusBar.executeRunnableDismissingKeyguard(runnable, null /* cancelAction */,
+ dismissShade, false /* afterKeyguardGone */, true /* deferred */);
+ }
+ }
+
+ private boolean canLaunchVoiceAssist() {
+ return mAssistManager.canVoiceAssistBeLaunchedFromKeyguard();
+ }
+
+ private void launchPhone() {
+ final TelecomManager tm = TelecomManager.from(mContext);
+ if (tm.isInCall()) {
+ AsyncTask.execute(new Runnable() {
+ @Override
+ public void run() {
+ tm.showInCallScreen(false /* showDialpad */);
+ }
+ });
+ } else {
+ boolean dismissShade = !TextUtils.isEmpty(mLeftButtonStr)
+ && Dependency.get(TunerService.class).getValue(LOCKSCREEN_LEFT_UNLOCK, 1) != 0;
+ mActivityStarter.startActivity(mLeftButton.getIntent(), dismissShade);
+ }
+ }
+
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ if (changedView == this && visibility == VISIBLE) {
+ mLockIcon.update();
+ updateCameraVisibility();
+ }
+ }
+
+ public KeyguardAffordanceView getLeftView() {
+ return mLeftAffordanceView;
+ }
+
+ public KeyguardAffordanceView getRightView() {
+ return mRightAffordanceView;
+ }
+
+ public View getLeftPreview() {
+ return mLeftPreview;
+ }
+
+ public View getRightPreview() {
+ return mCameraPreview;
+ }
+
+ public LockIcon getLockIcon() {
+ return mLockIcon;
+ }
+
+ public View getIndicationArea() {
+ return mIndicationArea;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ @Override
+ public void onUnlockMethodStateChanged() {
+ mLockIcon.update();
+ updateCameraVisibility();
+ }
+
+ private void inflateCameraPreview() {
+ View previewBefore = mCameraPreview;
+ boolean visibleBefore = false;
+ if (previewBefore != null) {
+ mPreviewContainer.removeView(previewBefore);
+ visibleBefore = previewBefore.getVisibility() == View.VISIBLE;
+ }
+ mCameraPreview = mPreviewInflater.inflatePreview(getCameraIntent());
+ if (mCameraPreview != null) {
+ mPreviewContainer.addView(mCameraPreview);
+ mCameraPreview.setVisibility(visibleBefore ? View.VISIBLE : View.INVISIBLE);
+ }
+ if (mAffordanceHelper != null) {
+ mAffordanceHelper.updatePreviews();
+ }
+ }
+
+ private void updateLeftPreview() {
+ View previewBefore = mLeftPreview;
+ if (previewBefore != null) {
+ mPreviewContainer.removeView(previewBefore);
+ }
+ if (mLeftIsVoiceAssist) {
+ mLeftPreview = mPreviewInflater.inflatePreviewFromService(
+ mAssistManager.getVoiceInteractorComponentName());
+ } else {
+ mLeftPreview = mPreviewInflater.inflatePreview(mLeftButton.getIntent());
+ }
+ if (mLeftPreview != null) {
+ mPreviewContainer.addView(mLeftPreview);
+ mLeftPreview.setVisibility(View.INVISIBLE);
+ }
+ if (mAffordanceHelper != null) {
+ mAffordanceHelper.updatePreviews();
+ }
+ }
+
+ public void startFinishDozeAnimation() {
+ long delay = 0;
+ if (mLeftAffordanceView.getVisibility() == View.VISIBLE) {
+ startFinishDozeAnimationElement(mLeftAffordanceView, delay);
+ delay += DOZE_ANIMATION_STAGGER_DELAY;
+ }
+ startFinishDozeAnimationElement(mLockIcon, delay);
+ delay += DOZE_ANIMATION_STAGGER_DELAY;
+ if (mRightAffordanceView.getVisibility() == View.VISIBLE) {
+ startFinishDozeAnimationElement(mRightAffordanceView, delay);
+ }
+ mIndicationArea.setAlpha(0f);
+ mIndicationArea.animate()
+ .alpha(1f)
+ .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+ .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(Interpolators.LINEAR_OUT_SLOW_IN)
+ .setStartDelay(delay)
+ .setDuration(DOZE_ANIMATION_ELEMENT_DURATION);
+ }
+
+ private final BroadcastReceiver mDevicePolicyReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ updateCameraVisibility();
+ }
+ });
+ }
+ };
+
+ private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
+ new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onUserSwitchComplete(int userId) {
+ updateCameraVisibility();
+ }
+
+ @Override
+ public void onStartedWakingUp() {
+ mLockIcon.setDeviceInteractive(true);
+ }
+
+ @Override
+ public void onFinishedGoingToSleep(int why) {
+ mLockIcon.setDeviceInteractive(false);
+ }
+
+ @Override
+ public void onScreenTurnedOn() {
+ mLockIcon.setScreenOn(true);
+ }
+
+ @Override
+ public void onScreenTurnedOff() {
+ mLockIcon.setScreenOn(false);
+ }
+
+ @Override
+ public void onKeyguardVisibilityChanged(boolean showing) {
+ mLockIcon.update();
+ }
+
+ @Override
+ public void onFingerprintRunningStateChanged(boolean running) {
+ mLockIcon.update();
+ }
+
+ @Override
+ public void onStrongAuthStateChanged(int userId) {
+ mLockIcon.update();
+ }
+
+ @Override
+ public void onUserUnlocked() {
+ inflateCameraPreview();
+ updateCameraVisibility();
+ updateLeftAffordance();
+ }
+ };
+
+ public void setKeyguardIndicationController(
+ KeyguardIndicationController keyguardIndicationController) {
+ mIndicationController = keyguardIndicationController;
+ }
+
+ public void updateLeftAffordance() {
+ updateLeftAffordanceIcon();
+ updateLeftPreview();
+ }
+
+ public void onKeyguardShowingChanged() {
+ updateLeftAffordance();
+ inflateCameraPreview();
+ }
+
+ private void setRightButton(IntentButton button) {
+ mRightButton = button;
+ updateRightAffordanceIcon();
+ updateCameraVisibility();
+ inflateCameraPreview();
+ }
+
+ private void setLeftButton(IntentButton button) {
+ mLeftButton = button;
+ if (!(mLeftButton instanceof DefaultLeftButton)) {
+ mLeftIsVoiceAssist = false;
+ }
+ updateLeftAffordance();
+ }
+
+ public void setDozing(boolean dozing, boolean animate) {
+ mDozing = dozing;
+
+ updateCameraVisibility();
+ updateLeftAffordanceIcon();
+
+ if (dozing) {
+ mLockIcon.setVisibility(INVISIBLE);
+ mOverlayContainer.setVisibility(INVISIBLE);
+ } else {
+ mLockIcon.setVisibility(VISIBLE);
+ mOverlayContainer.setVisibility(VISIBLE);
+ if (animate) {
+ startFinishDozeAnimation();
+ }
+ }
+ }
+
+ private class DefaultLeftButton implements IntentButton {
+
+ private IconState mIconState = new IconState();
+
+ @Override
+ public IconState getIcon() {
+ mLeftIsVoiceAssist = canLaunchVoiceAssist();
+ if (mLeftIsVoiceAssist) {
+ mIconState.isVisible = mUserSetupComplete;
+ if (mLeftAssistIcon == null) {
+ mIconState.drawable = mContext.getDrawable(R.drawable.ic_mic_26dp);
+ } else {
+ mIconState.drawable = mLeftAssistIcon;
+ }
+ mIconState.contentDescription = mContext.getString(
+ R.string.accessibility_voice_assist_button);
+ } else {
+ mIconState.isVisible = mUserSetupComplete && isPhoneVisible();
+ mIconState.drawable = mContext.getDrawable(R.drawable.ic_phone_24dp);
+ mIconState.contentDescription = mContext.getString(
+ R.string.accessibility_phone_button);
+ }
+ return mIconState;
+ }
+
+ @Override
+ public Intent getIntent() {
+ return PHONE_INTENT;
+ }
+ }
+
+ private class DefaultRightButton implements IntentButton {
+
+ private IconState mIconState = new IconState();
+
+ @Override
+ public IconState getIcon() {
+ ResolveInfo resolved = resolveCameraIntent();
+ boolean isCameraDisabled = (mStatusBar != null) && !mStatusBar.isCameraAllowedByAdmin();
+ mIconState.isVisible = !isCameraDisabled && resolved != null
+ && getResources().getBoolean(R.bool.config_keyguardShowCameraAffordance)
+ && mUserSetupComplete;
+ mIconState.drawable = mContext.getDrawable(R.drawable.ic_camera_alt_24dp);
+ mIconState.contentDescription =
+ mContext.getString(R.string.accessibility_camera_button);
+ return mIconState;
+ }
+
+ @Override
+ public Intent getIntent() {
+ KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+ boolean canSkipBouncer = updateMonitor.getUserCanSkipBouncer(
+ KeyguardUpdateMonitor.getCurrentUser());
+ boolean secure = mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser());
+ return (secure && !canSkipBouncer) ? SECURE_CAMERA_INTENT : INSECURE_CAMERA_INTENT;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
new file mode 100644
index 0000000..165ed78
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -0,0 +1,319 @@
+/*
+ * 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.app.ActivityManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardHostView;
+import com.android.keyguard.KeyguardSecurityView;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.R;
+import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.keyguard.DismissCallbackRegistry;
+
+import static com.android.keyguard.KeyguardHostView.OnDismissAction;
+import static com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+
+/**
+ * A class which manages the bouncer on the lockscreen.
+ */
+public class KeyguardBouncer {
+
+ final static private String TAG = "KeyguardBouncer";
+
+ protected final Context mContext;
+ protected final ViewMediatorCallback mCallback;
+ protected final LockPatternUtils mLockPatternUtils;
+ protected final ViewGroup mContainer;
+ private final FalsingManager mFalsingManager;
+ private final DismissCallbackRegistry mDismissCallbackRegistry;
+ private final Handler mHandler;
+ protected KeyguardHostView mKeyguardView;
+ protected ViewGroup mRoot;
+ private boolean mShowingSoon;
+ private int mBouncerPromptReason;
+ private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
+ new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onStrongAuthStateChanged(int userId) {
+ mBouncerPromptReason = mCallback.getBouncerPromptReason();
+ }
+ };
+ private final Runnable mRemoveViewRunnable = this::removeView;
+
+ public KeyguardBouncer(Context context, ViewMediatorCallback callback,
+ LockPatternUtils lockPatternUtils, ViewGroup container,
+ DismissCallbackRegistry dismissCallbackRegistry) {
+ mContext = context;
+ mCallback = callback;
+ mLockPatternUtils = lockPatternUtils;
+ mContainer = container;
+ KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallback);
+ mFalsingManager = FalsingManager.getInstance(mContext);
+ mDismissCallbackRegistry = dismissCallbackRegistry;
+ mHandler = new Handler();
+ }
+
+ public void show(boolean resetSecuritySelection) {
+ final int keyguardUserId = KeyguardUpdateMonitor.getCurrentUser();
+ if (keyguardUserId == UserHandle.USER_SYSTEM && UserManager.isSplitSystemUser()) {
+ // In split system user mode, we never unlock system user.
+ return;
+ }
+ mFalsingManager.onBouncerShown();
+ 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) {
+ return;
+ }
+
+ final int activeUserId = ActivityManager.getCurrentUser();
+ final boolean isSystemUser =
+ UserManager.isSplitSystemUser() && activeUserId == UserHandle.USER_SYSTEM;
+ final boolean allowDismissKeyguard = !isSystemUser && activeUserId == keyguardUserId;
+
+ // If allowed, try to dismiss the Keyguard. If no security auth (password/pin/pattern) is
+ // set, this will dismiss the whole Keyguard. Otherwise, show the bouncer.
+ if (allowDismissKeyguard && mKeyguardView.dismiss(activeUserId)) {
+ return;
+ }
+
+ // This condition may indicate an error on Android, so log it.
+ if (!allowDismissKeyguard) {
+ Slog.w(TAG, "User can't dismiss keyguard: " + activeUserId + " != " + keyguardUserId);
+ }
+
+ mShowingSoon = true;
+
+ // Split up the work over multiple frames.
+ DejankUtils.postAfterTraversal(mShowRunnable);
+ }
+
+ private final Runnable mShowRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mRoot.setVisibility(View.VISIBLE);
+ mKeyguardView.onResume();
+ showPromptReason(mBouncerPromptReason);
+ if (mKeyguardView.getHeight() != 0) {
+ mKeyguardView.startAppearAnimation();
+ } else {
+ mKeyguardView.getViewTreeObserver().addOnPreDrawListener(
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ mKeyguardView.getViewTreeObserver().removeOnPreDrawListener(this);
+ mKeyguardView.startAppearAnimation();
+ return true;
+ }
+ });
+ mKeyguardView.requestLayout();
+ }
+ mShowingSoon = false;
+ mKeyguardView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+ };
+
+ /**
+ * Show a string explaining why the security view needs to be solved.
+ *
+ * @param reason a flag indicating which string should be shown, see
+ * {@link KeyguardSecurityView#PROMPT_REASON_NONE}
+ * and {@link KeyguardSecurityView#PROMPT_REASON_RESTART}
+ */
+ public void showPromptReason(int reason) {
+ mKeyguardView.showPromptReason(reason);
+ }
+
+ public void showMessage(String message, int color) {
+ mKeyguardView.showMessage(message, color);
+ }
+
+ private void cancelShowRunnable() {
+ DejankUtils.removeCallbacks(mShowRunnable);
+ mShowingSoon = false;
+ }
+
+ public void showWithDismissAction(OnDismissAction r, Runnable cancelAction) {
+ ensureView();
+ mKeyguardView.setOnDismissAction(r, cancelAction);
+ show(false /* resetSecuritySelection */);
+ }
+
+ public void hide(boolean destroyView) {
+ if (isShowing()) {
+ mDismissCallbackRegistry.notifyDismissCancelled();
+ }
+ mFalsingManager.onBouncerHidden();
+ cancelShowRunnable();
+ if (mKeyguardView != null) {
+ mKeyguardView.cancelDismissAction();
+ mKeyguardView.cleanUp();
+ }
+ if (mRoot != null) {
+ mRoot.setVisibility(View.INVISIBLE);
+ if (destroyView) {
+
+ // We have a ViewFlipper that unregisters a broadcast when being detached, which may
+ // be slow because of AM lock contention during unlocking. We can delay it a bit.
+ mHandler.postDelayed(mRemoveViewRunnable, 50);
+ }
+ }
+ }
+
+ /**
+ * See {@link StatusBarKeyguardViewManager#startPreHideAnimation}.
+ */
+ public void startPreHideAnimation(Runnable runnable) {
+ if (mKeyguardView != null) {
+ mKeyguardView.startDisappearAnimation(runnable);
+ } else if (runnable != null) {
+ runnable.run();
+ }
+ }
+
+ /**
+ * Reset the state of the view.
+ */
+ public void reset() {
+ cancelShowRunnable();
+ inflateView();
+ mFalsingManager.onBouncerHidden();
+ }
+
+ public void onScreenTurnedOff() {
+ if (mKeyguardView != null && mRoot != null && mRoot.getVisibility() == View.VISIBLE) {
+ mKeyguardView.onPause();
+ }
+ }
+
+ public boolean isShowing() {
+ return mShowingSoon || (mRoot != null && mRoot.getVisibility() == View.VISIBLE);
+ }
+
+ public void prepare() {
+ boolean wasInitialized = mRoot != null;
+ ensureView();
+ if (wasInitialized) {
+ mKeyguardView.showPrimarySecurityScreen();
+ }
+ mBouncerPromptReason = mCallback.getBouncerPromptReason();
+ }
+
+ protected void ensureView() {
+ // Removal of the view might be deferred to reduce unlock latency,
+ // in this case we need to force the removal, otherwise we'll
+ // end up in an unpredictable state.
+ boolean forceRemoval = mHandler.hasCallbacks(mRemoveViewRunnable);
+ if (mRoot == null || forceRemoval) {
+ inflateView();
+ }
+ }
+
+ protected void inflateView() {
+ removeView();
+ mHandler.removeCallbacks(mRemoveViewRunnable);
+ mRoot = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.keyguard_bouncer, null);
+ mKeyguardView = mRoot.findViewById(R.id.keyguard_host_view);
+ mKeyguardView.setLockPatternUtils(mLockPatternUtils);
+ mKeyguardView.setViewMediatorCallback(mCallback);
+ mContainer.addView(mRoot, mContainer.getChildCount());
+ mRoot.setVisibility(View.INVISIBLE);
+
+ final WindowInsets rootInsets = mRoot.getRootWindowInsets();
+ if (rootInsets != null) {
+ mRoot.dispatchApplyWindowInsets(rootInsets);
+ }
+ }
+
+ protected void removeView() {
+ if (mRoot != null && mRoot.getParent() == mContainer) {
+ mContainer.removeView(mRoot);
+ mRoot = null;
+ }
+ }
+
+ public boolean onBackPressed() {
+ return mKeyguardView != null && mKeyguardView.handleBackKey();
+ }
+
+ /**
+ * @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() {
+ ensureView();
+ if (mKeyguardView != null) {
+ SecurityMode mode = mKeyguardView.getSecurityMode();
+ 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;
+ }
+
+ public boolean shouldDismissOnMenuPressed() {
+ return mKeyguardView.shouldEnableMenuKey();
+ }
+
+ public boolean interceptMediaKey(KeyEvent event) {
+ ensureView();
+ return mKeyguardView.interceptMediaKey(event);
+ }
+
+ public void notifyKeyguardAuthenticated(boolean strongAuth) {
+ ensureView();
+ mKeyguardView.finish(strongAuth, KeyguardUpdateMonitor.getCurrentUser());
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
new file mode 100644
index 0000000..f7aa818
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -0,0 +1,282 @@
+/*
+ * 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 static com.android.systemui.statusbar.notification.NotificationUtils.interpolate;
+
+import android.content.res.Resources;
+import android.graphics.Path;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.systemui.R;
+
+/**
+ * Utility class to calculate the clock position and top padding of notifications on Keyguard.
+ */
+public class KeyguardClockPositionAlgorithm {
+
+ private static final float SLOW_DOWN_FACTOR = 0.4f;
+
+ private static final float CLOCK_RUBBERBAND_FACTOR_MIN = 0.08f;
+ private static final float CLOCK_RUBBERBAND_FACTOR_MAX = 0.8f;
+ private static final float CLOCK_SCALE_FADE_START = 0.95f;
+ private static final float CLOCK_SCALE_FADE_END = 0.75f;
+ private static final float CLOCK_SCALE_FADE_END_NO_NOTIFS = 0.5f;
+
+ private static final float CLOCK_ADJ_TOP_PADDING_MULTIPLIER_MIN = 1.4f;
+ private static final float CLOCK_ADJ_TOP_PADDING_MULTIPLIER_MAX = 3.2f;
+
+ private static final long MILLIS_PER_MINUTES = 1000 * 60;
+ private static final float BURN_IN_PREVENTION_PERIOD_Y = 521;
+ private static final float BURN_IN_PREVENTION_PERIOD_X = 83;
+
+ private int mClockNotificationsMarginMin;
+ private int mClockNotificationsMarginMax;
+ private float mClockYFractionMin;
+ private float mClockYFractionMax;
+ private int mMaxKeyguardNotifications;
+ private int mMaxPanelHeight;
+ private float mExpandedHeight;
+ private int mNotificationCount;
+ private int mHeight;
+ private int mKeyguardStatusHeight;
+ private float mEmptyDragAmount;
+ private float mDensity;
+ private int mBurnInPreventionOffsetX;
+ private int mBurnInPreventionOffsetY;
+
+ /**
+ * The number (fractional) of notifications the "more" card counts when calculating how many
+ * notifications are currently visible for the y positioning of the clock.
+ */
+ private float mMoreCardNotificationAmount;
+
+ private static final PathInterpolator sSlowDownInterpolator;
+
+ static {
+ Path path = new Path();
+ path.moveTo(0, 0);
+ path.cubicTo(0.3f, 0.875f, 0.6f, 1f, 1f, 1f);
+ sSlowDownInterpolator = new PathInterpolator(path);
+ }
+
+ private AccelerateInterpolator mAccelerateInterpolator = new AccelerateInterpolator();
+ private int mClockBottom;
+ private float mDarkAmount;
+ private int mDozingStackPadding;
+
+ /**
+ * Refreshes the dimension values.
+ */
+ public void loadDimens(Resources res) {
+ mClockNotificationsMarginMin = res.getDimensionPixelSize(
+ R.dimen.keyguard_clock_notifications_margin_min);
+ mClockNotificationsMarginMax = res.getDimensionPixelSize(
+ R.dimen.keyguard_clock_notifications_margin_max);
+ mClockYFractionMin = res.getFraction(R.fraction.keyguard_clock_y_fraction_min, 1, 1);
+ mClockYFractionMax = res.getFraction(R.fraction.keyguard_clock_y_fraction_max, 1, 1);
+ mMoreCardNotificationAmount =
+ (float) res.getDimensionPixelSize(R.dimen.notification_shelf_height) /
+ res.getDimensionPixelSize(R.dimen.notification_min_height);
+ mDensity = res.getDisplayMetrics().density;
+ mBurnInPreventionOffsetX = res.getDimensionPixelSize(
+ R.dimen.burn_in_prevention_offset_x);
+ mBurnInPreventionOffsetY = res.getDimensionPixelSize(
+ R.dimen.burn_in_prevention_offset_y);
+ mDozingStackPadding = res.getDimensionPixelSize(R.dimen.dozing_stack_padding);
+ }
+
+ public void setup(int maxKeyguardNotifications, int maxPanelHeight, float expandedHeight,
+ int notificationCount, int height, int keyguardStatusHeight, float emptyDragAmount,
+ int clockBottom, float dark) {
+ mMaxKeyguardNotifications = maxKeyguardNotifications;
+ mMaxPanelHeight = maxPanelHeight;
+ mExpandedHeight = expandedHeight;
+ mNotificationCount = notificationCount;
+ mHeight = height;
+ mKeyguardStatusHeight = keyguardStatusHeight;
+ mEmptyDragAmount = emptyDragAmount;
+ mClockBottom = clockBottom;
+ mDarkAmount = dark;
+ }
+
+ public float getMinStackScrollerPadding(int height, int keyguardStatusHeight) {
+ return mClockYFractionMin * height + keyguardStatusHeight / 2
+ + mClockNotificationsMarginMin;
+ }
+
+ public void run(Result result) {
+ int y = getClockY() - mKeyguardStatusHeight / 2;
+ float clockAdjustment = getClockYExpansionAdjustment();
+ float topPaddingAdjMultiplier = getTopPaddingAdjMultiplier();
+ result.stackScrollerPaddingAdjustment = (int) (clockAdjustment*topPaddingAdjMultiplier);
+ int clockNotificationsPadding = getClockNotificationsPadding()
+ + result.stackScrollerPaddingAdjustment;
+ int padding = y + clockNotificationsPadding;
+ result.clockY = y;
+ result.stackScrollerPadding = mKeyguardStatusHeight + padding;
+ result.clockScale = getClockScale(result.stackScrollerPadding,
+ result.clockY,
+ y + getClockNotificationsPadding() + mKeyguardStatusHeight);
+ result.clockAlpha = getClockAlpha(result.clockScale);
+
+ result.stackScrollerPadding = (int) interpolate(
+ result.stackScrollerPadding,
+ mClockBottom + y + mDozingStackPadding,
+ mDarkAmount);
+
+ result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount);
+ }
+
+ private float getClockScale(int notificationPadding, int clockY, int startPadding) {
+ float scaleMultiplier = getNotificationAmountT() == 0 ? 6.0f : 5.0f;
+ float scaleEnd = clockY - mKeyguardStatusHeight * scaleMultiplier;
+ float distanceToScaleEnd = notificationPadding - scaleEnd;
+ float progress = distanceToScaleEnd / (startPadding - scaleEnd);
+ progress = Math.max(0.0f, Math.min(progress, 1.0f));
+ progress = mAccelerateInterpolator.getInterpolation(progress);
+ progress *= Math.pow(1 + mEmptyDragAmount / mDensity / 300, 0.3f);
+ return interpolate(progress, 1, mDarkAmount);
+ }
+
+ private int getClockNotificationsPadding() {
+ float t = getNotificationAmountT();
+ t = Math.min(t, 1.0f);
+ return (int) (t * mClockNotificationsMarginMin + (1 - t) * mClockNotificationsMarginMax);
+ }
+
+ private float getClockYFraction() {
+ float t = getNotificationAmountT();
+ t = Math.min(t, 1.0f);
+ return (1 - t) * mClockYFractionMax + t * mClockYFractionMin;
+ }
+
+ private int getClockY() {
+ // Dark: Align the bottom edge of the clock at one third:
+ // clockBottomEdge = result - mKeyguardStatusHeight / 2 + mClockBottom
+ float clockYDark = (0.33f * mHeight + (float) mKeyguardStatusHeight / 2 - mClockBottom)
+ + burnInPreventionOffsetY();
+ float clockYRegular = getClockYFraction() * mHeight;
+ return (int) interpolate(clockYRegular, clockYDark, mDarkAmount);
+ }
+
+ private float burnInPreventionOffsetY() {
+ return zigzag(System.currentTimeMillis() / MILLIS_PER_MINUTES,
+ mBurnInPreventionOffsetY * 2,
+ BURN_IN_PREVENTION_PERIOD_Y)
+ - mBurnInPreventionOffsetY;
+ }
+
+ private float burnInPreventionOffsetX() {
+ return zigzag(System.currentTimeMillis() / MILLIS_PER_MINUTES,
+ mBurnInPreventionOffsetX * 2,
+ BURN_IN_PREVENTION_PERIOD_X)
+ - mBurnInPreventionOffsetX;
+ }
+
+ /**
+ * Implements a continuous, piecewise linear, periodic zig-zag function
+ *
+ * Can be thought of as a linear approximation of abs(sin(x)))
+ *
+ * @param period period of the function, ie. zigzag(x + period) == zigzag(x)
+ * @param amplitude maximum value of the function
+ * @return a value between 0 and amplitude
+ */
+ private float zigzag(float x, float amplitude, float period) {
+ float xprime = (x % period) / (period / 2);
+ float interpolationAmount = (xprime <= 1) ? xprime : (2 - xprime);
+ return interpolate(0, amplitude, interpolationAmount);
+ }
+
+ private float getClockYExpansionAdjustment() {
+ float rubberbandFactor = getClockYExpansionRubberbandFactor();
+ float value = (rubberbandFactor * (mMaxPanelHeight - mExpandedHeight));
+ float t = value / mMaxPanelHeight;
+ float slowedDownValue = -sSlowDownInterpolator.getInterpolation(t) * SLOW_DOWN_FACTOR
+ * mMaxPanelHeight;
+ if (mNotificationCount == 0) {
+ return (-2*value + slowedDownValue)/3;
+ } else {
+ return slowedDownValue;
+ }
+ }
+
+ private float getClockYExpansionRubberbandFactor() {
+ float t = getNotificationAmountT();
+ t = Math.min(t, 1.0f);
+ t = (float) Math.pow(t, 0.3f);
+ return (1 - t) * CLOCK_RUBBERBAND_FACTOR_MAX + t * CLOCK_RUBBERBAND_FACTOR_MIN;
+ }
+
+ private float getTopPaddingAdjMultiplier() {
+ float t = getNotificationAmountT();
+ t = Math.min(t, 1.0f);
+ return (1 - t) * CLOCK_ADJ_TOP_PADDING_MULTIPLIER_MIN
+ + t * CLOCK_ADJ_TOP_PADDING_MULTIPLIER_MAX;
+ }
+
+ private float getClockAlpha(float scale) {
+ float fadeEnd = getNotificationAmountT() == 0.0f
+ ? CLOCK_SCALE_FADE_END_NO_NOTIFS
+ : CLOCK_SCALE_FADE_END;
+ float alpha = (scale - fadeEnd)
+ / (CLOCK_SCALE_FADE_START - fadeEnd);
+ return Math.max(0, Math.min(1, alpha));
+ }
+
+ /**
+ * @return a value from 0 to 1 depending on how many notification there are
+ */
+ private float getNotificationAmountT() {
+ return mNotificationCount
+ / (mMaxKeyguardNotifications + mMoreCardNotificationAmount);
+ }
+
+ public static class Result {
+
+ /**
+ * The y translation of the clock.
+ */
+ public int clockY;
+
+ /**
+ * The scale of the Clock
+ */
+ public float clockScale;
+
+ /**
+ * The alpha value of the clock.
+ */
+ public float clockAlpha;
+
+ /**
+ * The top padding of the stack scroller, in pixels.
+ */
+ public int stackScrollerPadding;
+
+ /**
+ * The top padding adjustment of the stack scroller, in pixels. This value is used to adjust
+ * the padding, but not the overall panel size.
+ */
+ public int stackScrollerPaddingAdjustment;
+
+ /** The x translation of the clock. */
+ public int clockX;
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
new file mode 100644
index 0000000..c5c3fff
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
@@ -0,0 +1,69 @@
+/*
+ * 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.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A view to show hints on Keyguard ("Swipe up to unlock", "Tap again to open").
+ */
+public class KeyguardIndicationTextView extends TextView {
+
+ public KeyguardIndicationTextView(Context context) {
+ super(context);
+ }
+
+ public KeyguardIndicationTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public KeyguardIndicationTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public KeyguardIndicationTextView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Changes the text with an animation and makes sure a single indication is shown long enough.
+ *
+ * @param text The text to show.
+ */
+ public void switchIndication(CharSequence text) {
+
+ // TODO: Animation, make sure that we will show one indication long enough.
+ if (TextUtils.isEmpty(text)) {
+ setVisibility(View.INVISIBLE);
+ } else {
+ setVisibility(View.VISIBLE);
+ setText(text);
+ }
+ }
+
+ /**
+ * See {@link #switchIndication}.
+ */
+ public void switchIndication(int textResId) {
+ switchIndication(getResources().getText(textResId));
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java b/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java
new file mode 100644
index 0000000..076e5f1
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java
@@ -0,0 +1,69 @@
+/*
+ * 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.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+
+/**
+ * A view group which contains the preview of phone/camera and draws a black bar at the bottom as
+ * the fake navigation bar.
+ */
+public class KeyguardPreviewContainer extends FrameLayout {
+
+ private Drawable mBlackBarDrawable = new Drawable() {
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.save();
+ canvas.clipRect(0, getHeight() - getPaddingBottom(), getWidth(), getHeight());
+ canvas.drawColor(Color.BLACK);
+ canvas.restore();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // noop
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ // noop
+ }
+
+ @Override
+ public int getOpacity() {
+ return android.graphics.PixelFormat.OPAQUE;
+ }
+ };
+
+ public KeyguardPreviewContainer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setBackground(mBlackBarDrawable);
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ setPadding(0, 0, 0, insets.getStableInsetBottom());
+ return super.onApplyWindowInsets(insets);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
new file mode 100644
index 0000000..a6691b1
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -0,0 +1,361 @@
+/*
+ * 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.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.settingslib.Utils;
+import com.android.systemui.BatteryMeterView;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.qs.QSPanel;
+import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager;
+import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
+import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+
+/**
+ * The header group on Keyguard.
+ */
+public class KeyguardStatusBarView extends RelativeLayout
+ implements BatteryStateChangeCallback, OnUserInfoChangedListener, ConfigurationListener {
+
+ private boolean mBatteryCharging;
+ private boolean mKeyguardUserSwitcherShowing;
+ private boolean mBatteryListening;
+
+ private TextView mCarrierLabel;
+ private View mSystemIconsSuperContainer;
+ private MultiUserSwitch mMultiUserSwitch;
+ private ImageView mMultiUserAvatar;
+ private BatteryMeterView mBatteryView;
+
+ private BatteryController mBatteryController;
+ private KeyguardUserSwitcher mKeyguardUserSwitcher;
+ private UserSwitcherController mUserSwitcherController;
+
+ private int mSystemIconsSwitcherHiddenExpandedMargin;
+ private int mSystemIconsBaseMargin;
+ private View mSystemIconsContainer;
+ private TintedIconManager mIconManager;
+
+ public KeyguardStatusBarView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mSystemIconsSuperContainer = findViewById(R.id.system_icons_super_container);
+ mSystemIconsContainer = findViewById(R.id.system_icons_container);
+ mMultiUserSwitch = (MultiUserSwitch) findViewById(R.id.multi_user_switch);
+ mMultiUserAvatar = (ImageView) findViewById(R.id.multi_user_avatar);
+ mCarrierLabel = (TextView) findViewById(R.id.keyguard_carrier_text);
+ mBatteryView = (BatteryMeterView) mSystemIconsContainer.findViewById(R.id.battery);
+
+ loadDimens();
+ updateUserSwitcher();
+ mBatteryController = Dependency.get(BatteryController.class);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ MarginLayoutParams lp = (MarginLayoutParams) mMultiUserAvatar.getLayoutParams();
+ lp.width = lp.height = getResources().getDimensionPixelSize(
+ R.dimen.multi_user_avatar_keyguard_size);
+ mMultiUserAvatar.setLayoutParams(lp);
+
+ lp = (MarginLayoutParams) mMultiUserSwitch.getLayoutParams();
+ lp.width = getResources().getDimensionPixelSize(
+ R.dimen.multi_user_switch_width_keyguard);
+ lp.setMarginEnd(getResources().getDimensionPixelSize(
+ R.dimen.multi_user_switch_keyguard_margin));
+ mMultiUserSwitch.setLayoutParams(lp);
+
+ lp = (MarginLayoutParams) mSystemIconsSuperContainer.getLayoutParams();
+ lp.height = getResources().getDimensionPixelSize(
+ R.dimen.status_bar_header_height);
+ lp.setMarginStart(getResources().getDimensionPixelSize(
+ R.dimen.system_icons_super_container_margin_start));
+ mSystemIconsSuperContainer.setLayoutParams(lp);
+ mSystemIconsSuperContainer.setPaddingRelative(mSystemIconsSuperContainer.getPaddingStart(),
+ mSystemIconsSuperContainer.getPaddingTop(),
+ getResources().getDimensionPixelSize(R.dimen.system_icons_keyguard_padding_end),
+ mSystemIconsSuperContainer.getPaddingBottom());
+
+ lp = (MarginLayoutParams) mSystemIconsContainer.getLayoutParams();
+ lp.height = getResources().getDimensionPixelSize(
+ R.dimen.status_bar_height);
+ mSystemIconsContainer.setLayoutParams(lp);
+
+ // Respect font size setting.
+ mCarrierLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.text_size_small_material));
+ lp = (MarginLayoutParams) mCarrierLabel.getLayoutParams();
+ lp.setMarginStart(
+ getResources().getDimensionPixelSize(R.dimen.keyguard_carrier_text_margin));
+ mCarrierLabel.setLayoutParams(lp);
+
+ lp = (MarginLayoutParams) getLayoutParams();
+ lp.height = getResources().getDimensionPixelSize(
+ R.dimen.status_bar_header_height_keyguard);
+ setLayoutParams(lp);
+ }
+
+ private void loadDimens() {
+ Resources res = getResources();
+ mSystemIconsSwitcherHiddenExpandedMargin = res.getDimensionPixelSize(
+ R.dimen.system_icons_switcher_hidden_expanded_margin);
+ mSystemIconsBaseMargin = res.getDimensionPixelSize(
+ R.dimen.system_icons_super_container_avatarless_margin_end);
+ }
+
+ private void updateVisibilities() {
+ if (mMultiUserSwitch.getParent() != this && !mKeyguardUserSwitcherShowing) {
+ if (mMultiUserSwitch.getParent() != null) {
+ getOverlay().remove(mMultiUserSwitch);
+ }
+ addView(mMultiUserSwitch, 0);
+ } else if (mMultiUserSwitch.getParent() == this && mKeyguardUserSwitcherShowing) {
+ removeView(mMultiUserSwitch);
+ }
+ if (mKeyguardUserSwitcher == null) {
+ // If we have no keyguard switcher, the screen width is under 600dp. In this case,
+ // we don't show the multi-user avatar unless there is more than 1 user on the device.
+ if (mUserSwitcherController != null
+ && mUserSwitcherController.getSwitchableUserCount() > 1) {
+ mMultiUserSwitch.setVisibility(View.VISIBLE);
+ } else {
+ mMultiUserSwitch.setVisibility(View.GONE);
+ }
+ }
+ mBatteryView.setForceShowPercent(mBatteryCharging);
+ }
+
+ private void updateSystemIconsLayoutParams() {
+ RelativeLayout.LayoutParams lp =
+ (LayoutParams) mSystemIconsSuperContainer.getLayoutParams();
+ // If the avatar icon is gone, we need to have some end margin to display the system icons
+ // correctly.
+ int baseMarginEnd = mMultiUserSwitch.getVisibility() == View.GONE
+ ? mSystemIconsBaseMargin
+ : 0;
+ int marginEnd = mKeyguardUserSwitcherShowing ? mSystemIconsSwitcherHiddenExpandedMargin :
+ baseMarginEnd;
+ if (marginEnd != lp.getMarginEnd()) {
+ lp.setMarginEnd(marginEnd);
+ mSystemIconsSuperContainer.setLayoutParams(lp);
+ }
+ }
+
+ public void setListening(boolean listening) {
+ if (listening == mBatteryListening) {
+ return;
+ }
+ mBatteryListening = listening;
+ if (mBatteryListening) {
+ mBatteryController.addCallback(this);
+ } else {
+ mBatteryController.removeCallback(this);
+ }
+ }
+
+ private void updateUserSwitcher() {
+ boolean keyguardSwitcherAvailable = mKeyguardUserSwitcher != null;
+ mMultiUserSwitch.setClickable(keyguardSwitcherAvailable);
+ mMultiUserSwitch.setFocusable(keyguardSwitcherAvailable);
+ mMultiUserSwitch.setKeyguardMode(keyguardSwitcherAvailable);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ UserInfoController userInfoController = Dependency.get(UserInfoController.class);
+ userInfoController.addCallback(this);
+ mUserSwitcherController = Dependency.get(UserSwitcherController.class);
+ mMultiUserSwitch.setUserSwitcherController(mUserSwitcherController);
+ userInfoController.reloadUserInfo();
+ Dependency.get(ConfigurationController.class).addCallback(this);
+ mIconManager = new TintedIconManager(findViewById(R.id.statusIcons));
+ Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager);
+ onOverlayChanged();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ Dependency.get(UserInfoController.class).removeCallback(this);
+ Dependency.get(StatusBarIconController.class).removeIconGroup(mIconManager);
+ Dependency.get(ConfigurationController.class).removeCallback(this);
+ }
+
+ @Override
+ public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
+ mMultiUserAvatar.setImageDrawable(picture);
+ }
+
+ public void setQSPanel(QSPanel qsp) {
+ mMultiUserSwitch.setQsPanel(qsp);
+ }
+
+ @Override
+ public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+ if (mBatteryCharging != charging) {
+ mBatteryCharging = charging;
+ updateVisibilities();
+ }
+ }
+
+ @Override
+ public void onPowerSaveChanged(boolean isPowerSave) {
+ // could not care less
+ }
+
+ public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
+ mKeyguardUserSwitcher = keyguardUserSwitcher;
+ mMultiUserSwitch.setKeyguardUserSwitcher(keyguardUserSwitcher);
+ updateUserSwitcher();
+ }
+
+ public void setKeyguardUserSwitcherShowing(boolean showing, boolean animate) {
+ mKeyguardUserSwitcherShowing = showing;
+ if (animate) {
+ animateNextLayoutChange();
+ }
+ updateVisibilities();
+ updateSystemIconsLayoutParams();
+ }
+
+ private void animateNextLayoutChange() {
+ final int systemIconsCurrentX = mSystemIconsSuperContainer.getLeft();
+ final boolean userSwitcherVisible = mMultiUserSwitch.getParent() == this;
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ boolean userSwitcherHiding = userSwitcherVisible
+ && mMultiUserSwitch.getParent() != KeyguardStatusBarView.this;
+ mSystemIconsSuperContainer.setX(systemIconsCurrentX);
+ mSystemIconsSuperContainer.animate()
+ .translationX(0)
+ .setDuration(400)
+ .setStartDelay(userSwitcherHiding ? 300 : 0)
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .start();
+ if (userSwitcherHiding) {
+ getOverlay().add(mMultiUserSwitch);
+ mMultiUserSwitch.animate()
+ .alpha(0f)
+ .setDuration(300)
+ .setStartDelay(0)
+ .setInterpolator(Interpolators.ALPHA_OUT)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mMultiUserSwitch.setAlpha(1f);
+ getOverlay().remove(mMultiUserSwitch);
+ }
+ })
+ .start();
+
+ } else {
+ mMultiUserSwitch.setAlpha(0f);
+ mMultiUserSwitch.animate()
+ .alpha(1f)
+ .setDuration(300)
+ .setStartDelay(200)
+ .setInterpolator(Interpolators.ALPHA_IN);
+ }
+ return true;
+ }
+ });
+
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ if (visibility != View.VISIBLE) {
+ mSystemIconsSuperContainer.animate().cancel();
+ mSystemIconsSuperContainer.setTranslationX(0);
+ mMultiUserSwitch.animate().cancel();
+ mMultiUserSwitch.setAlpha(1f);
+ } else {
+ updateVisibilities();
+ updateSystemIconsLayoutParams();
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ public void onOverlayChanged() {
+ @ColorInt int textColor = Utils.getColorAttr(mContext, R.attr.wallpaperTextColor);
+ @ColorInt int iconColor = Utils.getDefaultColor(mContext, Color.luminance(textColor) < 0.5 ?
+ R.color.dark_mode_icon_color_single_tone :
+ R.color.light_mode_icon_color_single_tone);
+ float intensity = textColor == Color.WHITE ? 0 : 1;
+ mCarrierLabel.setTextColor(iconColor);
+ mBatteryView.setFillColor(iconColor);
+ mIconManager.setTint(iconColor);
+ Rect tintArea = new Rect(0, 0, 0, 0);
+
+ applyDarkness(R.id.signal_cluster, tintArea, intensity, iconColor);
+ applyDarkness(R.id.battery, tintArea, intensity, iconColor);
+ applyDarkness(R.id.clock, tintArea, intensity, iconColor);
+ // Reload user avatar
+ ((UserInfoControllerImpl) Dependency.get(UserInfoController.class))
+ .onDensityOrFontScaleChanged();
+ }
+
+ private void applyDarkness(int id, Rect tintArea, float intensity, int color) {
+ View v = findViewById(id);
+ if (v instanceof DarkReceiver) {
+ ((DarkReceiver) v).onDarkChanged(tintArea, intensity, color);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/LightBarController.java b/com/android/systemui/statusbar/phone/LightBarController.java
new file mode 100644
index 0000000..533771a
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LightBarController.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2016 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.app.WallpaperColors;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.view.View;
+
+import com.android.internal.colorextraction.ColorExtractor.GradientColors;
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+
+/**
+ * Controls how light status bar flag applies to the icons.
+ */
+public class LightBarController implements BatteryController.BatteryStateChangeCallback, Dumpable {
+
+ private static final float NAV_BAR_INVERSION_SCRIM_ALPHA_THRESHOLD = 0.1f;
+
+ private final DarkIconDispatcher mStatusBarIconController;
+ private final BatteryController mBatteryController;
+ private FingerprintUnlockController mFingerprintUnlockController;
+
+ private LightBarTransitionsController mNavigationBarController;
+ private int mSystemUiVisibility;
+ private int mFullscreenStackVisibility;
+ private int mDockedStackVisibility;
+ private boolean mFullscreenLight;
+ private boolean mDockedLight;
+ private int mLastStatusBarMode;
+ private int mLastNavigationBarMode;
+ private final Color mDarkModeColor;
+
+ /**
+ * Whether the navigation bar should be light factoring in already how much alpha the scrim has
+ */
+ private boolean mNavigationLight;
+
+ /**
+ * Whether the flags indicate that a light status bar is requested. This doesn't factor in the
+ * scrim alpha yet.
+ */
+ private boolean mHasLightNavigationBar;
+ private boolean mScrimAlphaBelowThreshold;
+ private boolean mInvertLightNavBarWithScrim;
+ private float mScrimAlpha;
+
+ private final Rect mLastFullscreenBounds = new Rect();
+ private final Rect mLastDockedBounds = new Rect();
+ private boolean mQsCustomizing;
+
+ public LightBarController(Context ctx) {
+ mDarkModeColor = Color.valueOf(ctx.getColor(R.color.dark_mode_icon_color_single_tone));
+ mStatusBarIconController = Dependency.get(DarkIconDispatcher.class);
+ mBatteryController = Dependency.get(BatteryController.class);
+ mBatteryController.addCallback(this);
+ }
+
+ public void setNavigationBar(LightBarTransitionsController navigationBar) {
+ mNavigationBarController = navigationBar;
+ updateNavigation();
+ }
+
+ public void setFingerprintUnlockController(
+ FingerprintUnlockController fingerprintUnlockController) {
+ mFingerprintUnlockController = fingerprintUnlockController;
+ }
+
+ public void onSystemUiVisibilityChanged(int fullscreenStackVis, int dockedStackVis,
+ int mask, Rect fullscreenStackBounds, Rect dockedStackBounds, boolean sbModeChanged,
+ int statusBarMode) {
+ int oldFullscreen = mFullscreenStackVisibility;
+ int newFullscreen = (oldFullscreen & ~mask) | (fullscreenStackVis & mask);
+ int diffFullscreen = newFullscreen ^ oldFullscreen;
+ int oldDocked = mDockedStackVisibility;
+ int newDocked = (oldDocked & ~mask) | (dockedStackVis & mask);
+ int diffDocked = newDocked ^ oldDocked;
+ if ((diffFullscreen & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0
+ || (diffDocked & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0
+ || sbModeChanged
+ || !mLastFullscreenBounds.equals(fullscreenStackBounds)
+ || !mLastDockedBounds.equals(dockedStackBounds)) {
+
+ mFullscreenLight = isLight(newFullscreen, statusBarMode,
+ View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ mDockedLight = isLight(newDocked, statusBarMode, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ updateStatus(fullscreenStackBounds, dockedStackBounds);
+ }
+
+ mFullscreenStackVisibility = newFullscreen;
+ mDockedStackVisibility = newDocked;
+ mLastStatusBarMode = statusBarMode;
+ mLastFullscreenBounds.set(fullscreenStackBounds);
+ mLastDockedBounds.set(dockedStackBounds);
+ }
+
+ public void onNavigationVisibilityChanged(int vis, int mask, boolean nbModeChanged,
+ int navigationBarMode) {
+ int oldVis = mSystemUiVisibility;
+ int newVis = (oldVis & ~mask) | (vis & mask);
+ int diffVis = newVis ^ oldVis;
+ if ((diffVis & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0
+ || nbModeChanged) {
+ boolean last = mNavigationLight;
+ mHasLightNavigationBar = isLight(vis, navigationBarMode,
+ View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
+ mNavigationLight = mHasLightNavigationBar
+ && (mScrimAlphaBelowThreshold || !mInvertLightNavBarWithScrim)
+ && !mQsCustomizing;
+ if (mNavigationLight != last) {
+ updateNavigation();
+ }
+ }
+ mSystemUiVisibility = newVis;
+ mLastNavigationBarMode = navigationBarMode;
+ }
+
+ private void reevaluate() {
+ onSystemUiVisibilityChanged(mFullscreenStackVisibility,
+ mDockedStackVisibility, 0 /* mask */, mLastFullscreenBounds, mLastDockedBounds,
+ true /* sbModeChange*/, mLastStatusBarMode);
+ onNavigationVisibilityChanged(mSystemUiVisibility, 0 /* mask */, true /* nbModeChanged */,
+ mLastNavigationBarMode);
+ }
+
+ public void setQsCustomizing(boolean customizing) {
+ if (mQsCustomizing == customizing) return;
+ mQsCustomizing = customizing;
+ reevaluate();
+ }
+
+ public void setScrimAlpha(float alpha) {
+ mScrimAlpha = alpha;
+ boolean belowThresholdBefore = mScrimAlphaBelowThreshold;
+ mScrimAlphaBelowThreshold = mScrimAlpha < NAV_BAR_INVERSION_SCRIM_ALPHA_THRESHOLD;
+ if (mHasLightNavigationBar && belowThresholdBefore != mScrimAlphaBelowThreshold) {
+ reevaluate();
+ }
+ }
+
+ public void setScrimColor(GradientColors colors) {
+ boolean invertLightNavBarWithScrimBefore = mInvertLightNavBarWithScrim;
+ mInvertLightNavBarWithScrim = !colors.supportsDarkText();
+ if (mHasLightNavigationBar
+ && invertLightNavBarWithScrimBefore != mInvertLightNavBarWithScrim) {
+ reevaluate();
+ }
+ }
+
+ private boolean isLight(int vis, int barMode, int flag) {
+ boolean isTransparentBar = (barMode == MODE_TRANSPARENT
+ || barMode == MODE_LIGHTS_OUT_TRANSPARENT);
+ boolean allowLight = isTransparentBar && !mBatteryController.isPowerSave();
+ boolean light = (vis & flag) != 0;
+ return allowLight && light;
+ }
+
+ private boolean animateChange() {
+ if (mFingerprintUnlockController == null) {
+ return false;
+ }
+ int unlockMode = mFingerprintUnlockController.getMode();
+ return unlockMode != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
+ && unlockMode != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK;
+ }
+
+ private void updateStatus(Rect fullscreenStackBounds, Rect dockedStackBounds) {
+ boolean hasDockedStack = !dockedStackBounds.isEmpty();
+
+ // If both are light or fullscreen is light and there is no docked stack, all icons get
+ // dark.
+ if ((mFullscreenLight && mDockedLight) || (mFullscreenLight && !hasDockedStack)) {
+ mStatusBarIconController.setIconsDarkArea(null);
+ mStatusBarIconController.getTransitionsController().setIconsDark(true, animateChange());
+
+ }
+
+ // If no one is light or the fullscreen is not light and there is no docked stack,
+ // all icons become white.
+ else if ((!mFullscreenLight && !mDockedLight) || (!mFullscreenLight && !hasDockedStack)) {
+ mStatusBarIconController.getTransitionsController().setIconsDark(
+ false, animateChange());
+ }
+
+ // Not the same for every stack, magic!
+ else {
+ Rect bounds = mFullscreenLight ? fullscreenStackBounds : dockedStackBounds;
+ if (bounds.isEmpty()) {
+ mStatusBarIconController.setIconsDarkArea(null);
+ } else {
+ mStatusBarIconController.setIconsDarkArea(bounds);
+ }
+ mStatusBarIconController.getTransitionsController().setIconsDark(true, animateChange());
+ }
+ }
+
+ private void updateNavigation() {
+ if (mNavigationBarController != null) {
+ mNavigationBarController.setIconsDark(
+ mNavigationLight, animateChange());
+ }
+ }
+
+ @Override
+ public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+
+ }
+
+ @Override
+ public void onPowerSaveChanged(boolean isPowerSave) {
+ reevaluate();
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("LightBarController: ");
+ pw.print(" mSystemUiVisibility=0x"); pw.print(
+ Integer.toHexString(mSystemUiVisibility));
+ pw.print(" mFullscreenStackVisibility=0x"); pw.print(
+ Integer.toHexString(mFullscreenStackVisibility));
+ pw.print(" mDockedStackVisibility=0x"); pw.println(
+ Integer.toHexString(mDockedStackVisibility));
+
+ pw.print(" mFullscreenLight="); pw.print(mFullscreenLight);
+ pw.print(" mDockedLight="); pw.println(mDockedLight);
+
+ pw.print(" mLastFullscreenBounds="); pw.print(mLastFullscreenBounds);
+ pw.print(" mLastDockedBounds="); pw.println(mLastDockedBounds);
+
+ pw.print(" mNavigationLight="); pw.print(mNavigationLight);
+ pw.print(" mHasLightNavigationBar="); pw.println(mHasLightNavigationBar);
+
+ pw.print(" mLastStatusBarMode="); pw.print(mLastStatusBarMode);
+ pw.print(" mLastNavigationBarMode="); pw.println(mLastNavigationBarMode);
+
+ pw.print(" mScrimAlpha="); pw.print(mScrimAlpha);
+ pw.print(" mScrimAlphaBelowThreshold="); pw.println(mScrimAlphaBelowThreshold);
+ pw.println();
+ pw.println(" StatusBarTransitionsController:");
+ mStatusBarIconController.getTransitionsController().dump(fd, pw, args);
+ pw.println();
+ pw.println(" NavigationBarTransitionsController:");
+ mNavigationBarController.dump(fd, pw, args);
+ pw.println();
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/LightBarTransitionsController.java b/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
new file mode 100644
index 0000000..b0ac6ec
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2016 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.ValueAnimator;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.TimeUtils;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.Interpolators;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.CommandQueue.Callbacks;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Class to control all aspects about light bar changes.
+ */
+public class LightBarTransitionsController implements Dumpable, Callbacks {
+
+ public static final long DEFAULT_TINT_ANIMATION_DURATION = 120;
+ private static final String EXTRA_DARK_INTENSITY = "dark_intensity";
+
+ private final Handler mHandler;
+ private final DarkIntensityApplier mApplier;
+ private final KeyguardMonitor mKeyguardMonitor;
+
+ private boolean mTransitionDeferring;
+ private long mTransitionDeferringStartTime;
+ private long mTransitionDeferringDuration;
+ private boolean mTransitionPending;
+ private boolean mTintChangePending;
+ private float mPendingDarkIntensity;
+ private ValueAnimator mTintAnimator;
+ private float mDarkIntensity;
+ private float mNextDarkIntensity;
+ private final Runnable mTransitionDeferringDoneRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mTransitionDeferring = false;
+ }
+ };
+
+ public LightBarTransitionsController(Context context, DarkIntensityApplier applier) {
+ mApplier = applier;
+ mHandler = new Handler();
+ mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
+ SysUiServiceProvider.getComponent(context, CommandQueue.class)
+ .addCallbacks(this);
+ }
+
+ public void destroy(Context context) {
+ SysUiServiceProvider.getComponent(context, CommandQueue.class)
+ .removeCallbacks(this);
+ }
+
+ public void saveState(Bundle outState) {
+ float intensity = mTintAnimator != null && mTintAnimator.isRunning()
+ ? mNextDarkIntensity : mDarkIntensity;
+ outState.putFloat(EXTRA_DARK_INTENSITY, intensity);
+ }
+
+ public void restoreState(Bundle savedInstanceState) {
+ setIconTintInternal(savedInstanceState.getFloat(EXTRA_DARK_INTENSITY, 0));
+ }
+
+ @Override
+ public void appTransitionPending(boolean forced) {
+ if (mKeyguardMonitor.isKeyguardGoingAway() && !forced) {
+ return;
+ }
+ mTransitionPending = true;
+ }
+
+ @Override
+ public void appTransitionCancelled() {
+ if (mTransitionPending && mTintChangePending) {
+ mTintChangePending = false;
+ animateIconTint(mPendingDarkIntensity, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
+ }
+ mTransitionPending = false;
+ }
+
+ @Override
+ public void appTransitionStarting(long startTime, long duration, boolean forced) {
+ if (mKeyguardMonitor.isKeyguardGoingAway() && !forced) {
+ return;
+ }
+ if (mTransitionPending && mTintChangePending) {
+ mTintChangePending = false;
+ animateIconTint(mPendingDarkIntensity,
+ Math.max(0, startTime - SystemClock.uptimeMillis()),
+ duration);
+
+ } else if (mTransitionPending) {
+
+ // If we don't have a pending tint change yet, the change might come in the future until
+ // startTime is reached.
+ mTransitionDeferring = true;
+ mTransitionDeferringStartTime = startTime;
+ mTransitionDeferringDuration = duration;
+ mHandler.removeCallbacks(mTransitionDeferringDoneRunnable);
+ mHandler.postAtTime(mTransitionDeferringDoneRunnable, startTime);
+ }
+ mTransitionPending = false;
+ }
+
+ public void setIconsDark(boolean dark, boolean animate) {
+ if (!animate) {
+ setIconTintInternal(dark ? 1.0f : 0.0f);
+ mNextDarkIntensity = dark ? 1.0f : 0.0f;
+ } else if (mTransitionPending) {
+ deferIconTintChange(dark ? 1.0f : 0.0f);
+ } else if (mTransitionDeferring) {
+ animateIconTint(dark ? 1.0f : 0.0f,
+ Math.max(0, mTransitionDeferringStartTime - SystemClock.uptimeMillis()),
+ mTransitionDeferringDuration);
+ } else {
+ animateIconTint(dark ? 1.0f : 0.0f, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
+ }
+ }
+
+ public float getCurrentDarkIntensity() {
+ return mDarkIntensity;
+ }
+
+ private void deferIconTintChange(float darkIntensity) {
+ if (mTintChangePending && darkIntensity == mPendingDarkIntensity) {
+ return;
+ }
+ mTintChangePending = true;
+ mPendingDarkIntensity = darkIntensity;
+ }
+
+ private void animateIconTint(float targetDarkIntensity, long delay,
+ long duration) {
+ if (mNextDarkIntensity == targetDarkIntensity) {
+ return;
+ }
+ if (mTintAnimator != null) {
+ mTintAnimator.cancel();
+ }
+ mNextDarkIntensity = targetDarkIntensity;
+ mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity);
+ mTintAnimator.addUpdateListener(
+ animation -> setIconTintInternal((Float) animation.getAnimatedValue()));
+ mTintAnimator.setDuration(duration);
+ mTintAnimator.setStartDelay(delay);
+ mTintAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ mTintAnimator.start();
+ }
+
+ private void setIconTintInternal(float darkIntensity) {
+ mDarkIntensity = darkIntensity;
+ mApplier.applyDarkIntensity(darkIntensity);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.print(" mTransitionDeferring="); pw.print(mTransitionDeferring);
+ if (mTransitionDeferring) {
+ pw.println();
+ pw.print(" mTransitionDeferringStartTime=");
+ pw.println(TimeUtils.formatUptime(mTransitionDeferringStartTime));
+
+ pw.print(" mTransitionDeferringDuration=");
+ TimeUtils.formatDuration(mTransitionDeferringDuration, pw);
+ pw.println();
+ }
+ pw.print(" mTransitionPending="); pw.print(mTransitionPending);
+ pw.print(" mTintChangePending="); pw.println(mTintChangePending);
+
+ pw.print(" mPendingDarkIntensity="); pw.print(mPendingDarkIntensity);
+ pw.print(" mDarkIntensity="); pw.print(mDarkIntensity);
+ pw.print(" mNextDarkIntensity="); pw.println(mNextDarkIntensity);
+ }
+
+ /**
+ * Interface to apply a specific dark intensity.
+ */
+ public interface DarkIntensityApplier {
+ void applyDarkIntensity(float darkIntensity);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/LockIcon.java b/com/android/systemui/statusbar/phone/LockIcon.java
new file mode 100644
index 0000000..5c9446c
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LockIcon.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2015 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.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.systemui.statusbar.policy.AccessibilityController;
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
+
+/**
+ * Manages the different states and animations of the unlock icon.
+ */
+public class LockIcon extends KeyguardAffordanceView implements OnUserInfoChangedListener {
+
+ private static final int FP_DRAW_OFF_TIMEOUT = 800;
+
+ private static final int STATE_LOCKED = 0;
+ private static final int STATE_LOCK_OPEN = 1;
+ private static final int STATE_FACE_UNLOCK = 2;
+ private static final int STATE_FINGERPRINT = 3;
+ private static final int STATE_FINGERPRINT_ERROR = 4;
+
+ private int mLastState = 0;
+ private boolean mLastDeviceInteractive;
+ private boolean mTransientFpError;
+ private boolean mDeviceInteractive;
+ private boolean mScreenOn;
+ private boolean mLastScreenOn;
+ private Drawable mUserAvatarIcon;
+ private TrustDrawable mTrustDrawable;
+ private final UnlockMethodCache mUnlockMethodCache;
+ private AccessibilityController mAccessibilityController;
+ private boolean mHasFingerPrintIcon;
+ private int mDensity;
+
+ private final Runnable mDrawOffTimeout = () -> update(true /* forceUpdate */);
+
+ public LockIcon(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mTrustDrawable = new TrustDrawable(context);
+ setBackground(mTrustDrawable);
+ mUnlockMethodCache = UnlockMethodCache.getInstance(context);
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ if (isShown()) {
+ mTrustDrawable.start();
+ } else {
+ mTrustDrawable.stop();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mTrustDrawable.stop();
+ }
+
+ @Override
+ public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
+ mUserAvatarIcon = picture;
+ update();
+ }
+
+ public void setTransientFpError(boolean transientFpError) {
+ mTransientFpError = transientFpError;
+ update();
+ }
+
+ public void setDeviceInteractive(boolean deviceInteractive) {
+ mDeviceInteractive = deviceInteractive;
+ update();
+ }
+
+ public void setScreenOn(boolean screenOn) {
+ mScreenOn = screenOn;
+ update();
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ final int density = newConfig.densityDpi;
+ if (density != mDensity) {
+ mDensity = density;
+ mTrustDrawable.stop();
+ mTrustDrawable = new TrustDrawable(getContext());
+ setBackground(mTrustDrawable);
+ update();
+ }
+ }
+
+ public void update() {
+ update(false /* force */);
+ }
+
+ public void update(boolean force) {
+ boolean visible = isShown()
+ && KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive();
+ if (visible) {
+ mTrustDrawable.start();
+ } else {
+ mTrustDrawable.stop();
+ }
+ int state = getState();
+ boolean anyFingerprintIcon = state == STATE_FINGERPRINT || state == STATE_FINGERPRINT_ERROR;
+ boolean useAdditionalPadding = anyFingerprintIcon;
+ boolean trustHidden = anyFingerprintIcon;
+ if (state != mLastState || mDeviceInteractive != mLastDeviceInteractive
+ || mScreenOn != mLastScreenOn || force) {
+ int iconAnimRes =
+ getAnimationResForTransition(mLastState, state, mLastDeviceInteractive,
+ mDeviceInteractive, mLastScreenOn, mScreenOn);
+ boolean isAnim = iconAnimRes != -1;
+ if (iconAnimRes == R.drawable.lockscreen_fingerprint_draw_off_animation) {
+ anyFingerprintIcon = true;
+ useAdditionalPadding = true;
+ trustHidden = true;
+ } else if (iconAnimRes == R.drawable.trusted_state_to_error_animation) {
+ anyFingerprintIcon = true;
+ useAdditionalPadding = false;
+ trustHidden = true;
+ } else if (iconAnimRes == R.drawable.error_to_trustedstate_animation) {
+ anyFingerprintIcon = true;
+ useAdditionalPadding = false;
+ trustHidden = false;
+ }
+
+ Drawable icon;
+ if (isAnim) {
+ // Load the animation resource.
+ icon = mContext.getDrawable(iconAnimRes);
+ } else {
+ // Load the static icon resource based on the current state.
+ icon = getIconForState(state, mScreenOn, mDeviceInteractive);
+ }
+
+ final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
+ ? (AnimatedVectorDrawable) icon
+ : null;
+ int iconHeight = getResources().getDimensionPixelSize(
+ R.dimen.keyguard_affordance_icon_height);
+ int iconWidth = getResources().getDimensionPixelSize(
+ R.dimen.keyguard_affordance_icon_width);
+ if (!anyFingerprintIcon && (icon.getIntrinsicHeight() != iconHeight
+ || icon.getIntrinsicWidth() != iconWidth)) {
+ icon = new IntrinsicSizeDrawable(icon, iconWidth, iconHeight);
+ }
+ setPaddingRelative(0, 0, 0, useAdditionalPadding
+ ? getResources().getDimensionPixelSize(
+ R.dimen.fingerprint_icon_additional_padding)
+ : 0);
+ setRestingAlpha(
+ anyFingerprintIcon ? 1f : KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT);
+ setImageDrawable(icon, false);
+ mHasFingerPrintIcon = anyFingerprintIcon;
+ if (animation != null && isAnim) {
+ animation.forceAnimationOnUI();
+ animation.start();
+ }
+
+ if (iconAnimRes == R.drawable.lockscreen_fingerprint_draw_off_animation) {
+ removeCallbacks(mDrawOffTimeout);
+ postDelayed(mDrawOffTimeout, FP_DRAW_OFF_TIMEOUT);
+ } else {
+ removeCallbacks(mDrawOffTimeout);
+ }
+
+ mLastState = state;
+ mLastDeviceInteractive = mDeviceInteractive;
+ mLastScreenOn = mScreenOn;
+ }
+
+ // Hide trust circle when fingerprint is running.
+ boolean trustManaged = mUnlockMethodCache.isTrustManaged() && !trustHidden;
+ mTrustDrawable.setTrustManaged(trustManaged);
+ updateClickability();
+ }
+
+ private void updateClickability() {
+ if (mAccessibilityController == null) {
+ return;
+ }
+ boolean clickToUnlock = mAccessibilityController.isAccessibilityEnabled();
+ boolean clickToForceLock = mUnlockMethodCache.isTrustManaged()
+ && !clickToUnlock;
+ boolean longClickToForceLock = mUnlockMethodCache.isTrustManaged()
+ && !clickToForceLock;
+ setClickable(clickToForceLock || clickToUnlock);
+ setLongClickable(longClickToForceLock);
+ setFocusable(mAccessibilityController.isAccessibilityEnabled());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ if (mHasFingerPrintIcon) {
+ AccessibilityNodeInfo.AccessibilityAction unlock
+ = new AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_CLICK,
+ getContext().getString(R.string.accessibility_unlock_without_fingerprint));
+ info.addAction(unlock);
+ info.setHintText(getContext().getString(
+ R.string.accessibility_waiting_for_fingerprint));
+ }
+ }
+
+ public void setAccessibilityController(AccessibilityController accessibilityController) {
+ mAccessibilityController = accessibilityController;
+ }
+
+ private Drawable getIconForState(int state, boolean screenOn, boolean deviceInteractive) {
+ int iconRes;
+ switch (state) {
+ case STATE_LOCKED:
+ iconRes = R.drawable.ic_lock_24dp;
+ break;
+ case STATE_LOCK_OPEN:
+ if (mUnlockMethodCache.isTrustManaged() && mUnlockMethodCache.isTrusted()
+ && mUserAvatarIcon != null) {
+ return mUserAvatarIcon;
+ } else {
+ iconRes = R.drawable.ic_lock_open_24dp;
+ }
+ break;
+ case STATE_FACE_UNLOCK:
+ iconRes = com.android.internal.R.drawable.ic_account_circle;
+ break;
+ case STATE_FINGERPRINT:
+ // If screen is off and device asleep, use the draw on animation so the first frame
+ // gets drawn.
+ iconRes = screenOn && deviceInteractive
+ ? R.drawable.ic_fingerprint
+ : R.drawable.lockscreen_fingerprint_draw_on_animation;
+ break;
+ case STATE_FINGERPRINT_ERROR:
+ iconRes = R.drawable.ic_fingerprint_error;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+
+ return mContext.getDrawable(iconRes);
+ }
+
+ private int getAnimationResForTransition(int oldState, int newState,
+ boolean oldDeviceInteractive, boolean deviceInteractive,
+ boolean oldScreenOn, boolean screenOn) {
+ if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
+ return R.drawable.lockscreen_fingerprint_fp_to_error_state_animation;
+ } else if (oldState == STATE_LOCK_OPEN && newState == STATE_FINGERPRINT_ERROR) {
+ return R.drawable.trusted_state_to_error_animation;
+ } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_LOCK_OPEN) {
+ return R.drawable.error_to_trustedstate_animation;
+ } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
+ return R.drawable.lockscreen_fingerprint_error_state_to_fp_animation;
+ } else if (oldState == STATE_FINGERPRINT && newState == STATE_LOCK_OPEN
+ && !mUnlockMethodCache.isTrusted()) {
+ return R.drawable.lockscreen_fingerprint_draw_off_animation;
+ } else if (newState == STATE_FINGERPRINT && (!oldScreenOn && screenOn && deviceInteractive
+ || screenOn && !oldDeviceInteractive && deviceInteractive)) {
+ return R.drawable.lockscreen_fingerprint_draw_on_animation;
+ } else {
+ return -1;
+ }
+ }
+
+ private int getState() {
+ KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+ boolean fingerprintRunning = updateMonitor.isFingerprintDetectionRunning();
+ boolean unlockingAllowed = updateMonitor.isUnlockingWithFingerprintAllowed();
+ if (mTransientFpError) {
+ return STATE_FINGERPRINT_ERROR;
+ } else if (mUnlockMethodCache.canSkipBouncer()) {
+ return STATE_LOCK_OPEN;
+ } else if (mUnlockMethodCache.isFaceUnlockRunning()) {
+ return STATE_FACE_UNLOCK;
+ } else if (fingerprintRunning && unlockingAllowed) {
+ return STATE_FINGERPRINT;
+ } else {
+ return STATE_LOCKED;
+ }
+ }
+
+ /**
+ * A wrapper around another Drawable that overrides the intrinsic size.
+ */
+ private static class IntrinsicSizeDrawable extends InsetDrawable {
+
+ private final int mIntrinsicWidth;
+ private final int mIntrinsicHeight;
+
+ public IntrinsicSizeDrawable(Drawable drawable, int intrinsicWidth, int intrinsicHeight) {
+ super(drawable, 0);
+ mIntrinsicWidth = intrinsicWidth;
+ mIntrinsicHeight = intrinsicHeight;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mIntrinsicWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mIntrinsicHeight;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java b/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java
new file mode 100644
index 0000000..4d99a46
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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.metrics.LogMaker;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.Dependency;
+import com.android.systemui.EventLogConstants;
+import com.android.systemui.EventLogTags;
+
+/**
+ * Wrapper that emits both new- and old-style gesture logs.
+ * TODO: delete this once the old logs are no longer needed.
+ */
+public class LockscreenGestureLogger {
+ private ArrayMap<Integer, Integer> mLegacyMap;
+ private LogMaker mLogMaker = new LogMaker(MetricsEvent.VIEW_UNKNOWN)
+ .setType(MetricsEvent.TYPE_ACTION);
+ private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+
+ public LockscreenGestureLogger() {
+ mLegacyMap = new ArrayMap<>(EventLogConstants.METRICS_GESTURE_TYPE_MAP.length);
+ for (int i = 0; i < EventLogConstants.METRICS_GESTURE_TYPE_MAP.length ; i++) {
+ mLegacyMap.put(EventLogConstants.METRICS_GESTURE_TYPE_MAP[i], i);
+ }
+ }
+
+ public void write(int gesture, int length, int velocity) {
+ mMetricsLogger.write(mLogMaker.setCategory(gesture)
+ .setType(MetricsEvent.TYPE_ACTION)
+ .addTaggedData(MetricsEvent.FIELD_GESTURE_LENGTH, length)
+ .addTaggedData(MetricsEvent.FIELD_GESTURE_VELOCITY, velocity));
+ // also write old-style logs for backward-0compatibility
+ EventLogTags.writeSysuiLockscreenGesture(safeLookup(gesture), length, velocity);
+ }
+
+ private int safeLookup(int gesture) {
+ Integer value = mLegacyMap.get(gesture);
+ if (value == null) {
+ return MetricsEvent.VIEW_UNKNOWN;
+ }
+ return value;
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
new file mode 100644
index 0000000..87f5ca7
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2016 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.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.IWallpaperManager;
+import android.app.IWallpaperManagerCallback;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.Xfermode;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableWrapper;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.app.WallpaperColors;
+import android.util.Log;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+
+import libcore.io.IoUtils;
+
+import java.util.Objects;
+
+/**
+ * Manages the lockscreen wallpaper.
+ */
+public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implements Runnable {
+
+ private static final String TAG = "LockscreenWallpaper";
+
+ private final StatusBar mBar;
+ private final WallpaperManager mWallpaperManager;
+ private final Handler mH;
+ private final KeyguardUpdateMonitor mUpdateMonitor;
+
+ private boolean mCached;
+ private Bitmap mCache;
+ private int mCurrentUserId;
+ // The user selected in the UI, or null if no user is selected or UI doesn't support selecting
+ // users.
+ private UserHandle mSelectedUser;
+ private AsyncTask<Void, Void, LoaderResult> mLoader;
+
+ public LockscreenWallpaper(Context ctx, StatusBar bar, Handler h) {
+ mBar = bar;
+ mH = h;
+ mWallpaperManager = (WallpaperManager) ctx.getSystemService(Context.WALLPAPER_SERVICE);
+ mCurrentUserId = ActivityManager.getCurrentUser();
+ mUpdateMonitor = KeyguardUpdateMonitor.getInstance(ctx);
+
+ IWallpaperManager service = IWallpaperManager.Stub.asInterface(
+ ServiceManager.getService(Context.WALLPAPER_SERVICE));
+ try {
+ service.setLockWallpaperCallback(this);
+ } catch (RemoteException e) {
+ Log.e(TAG, "System dead?" + e);
+ }
+ }
+
+ public Bitmap getBitmap() {
+ if (mCached) {
+ return mCache;
+ }
+ if (!mWallpaperManager.isWallpaperSupported()) {
+ mCached = true;
+ mCache = null;
+ return null;
+ }
+
+ LoaderResult result = loadBitmap(mCurrentUserId, mSelectedUser);
+ if (result.success) {
+ mCached = true;
+ mUpdateMonitor.setHasLockscreenWallpaper(result.bitmap != null);
+ mCache = result.bitmap;
+ }
+ return mCache;
+ }
+
+ public LoaderResult loadBitmap(int currentUserId, UserHandle selectedUser) {
+ // May be called on any thread - only use thread safe operations.
+
+ // Prefer the selected user (when specified) over the current user for the FLAG_SET_LOCK
+ // wallpaper.
+ final int lockWallpaperUserId =
+ selectedUser != null ? selectedUser.getIdentifier() : currentUserId;
+ ParcelFileDescriptor fd = mWallpaperManager.getWallpaperFile(
+ WallpaperManager.FLAG_LOCK, lockWallpaperUserId);
+
+ if (fd != null) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ return LoaderResult.success(BitmapFactory.decodeFileDescriptor(
+ fd.getFileDescriptor(), null, options));
+ } catch (OutOfMemoryError e) {
+ Log.w(TAG, "Can't decode file", e);
+ return LoaderResult.fail();
+ } finally {
+ IoUtils.closeQuietly(fd);
+ }
+ } else {
+ if (selectedUser != null) {
+ // Show the selected user's static wallpaper.
+ return LoaderResult.success(
+ mWallpaperManager.getBitmapAsUser(selectedUser.getIdentifier()));
+
+ } else {
+ // When there is no selected user, show the system wallpaper
+ return LoaderResult.success(null);
+ }
+ }
+ }
+
+ public void setCurrentUser(int user) {
+ if (user != mCurrentUserId) {
+ if (mSelectedUser == null || user != mSelectedUser.getIdentifier()) {
+ mCached = false;
+ }
+ mCurrentUserId = user;
+ }
+ }
+
+ public void setSelectedUser(UserHandle selectedUser) {
+ if (Objects.equals(selectedUser, mSelectedUser)) {
+ return;
+ }
+ mSelectedUser = selectedUser;
+ postUpdateWallpaper();
+ }
+
+ @Override
+ public void onWallpaperChanged() {
+ // Called on Binder thread.
+ postUpdateWallpaper();
+ }
+
+ @Override
+ public void onWallpaperColorsChanged(WallpaperColors colors, int which, int userId) {
+
+ }
+
+ private void postUpdateWallpaper() {
+ mH.removeCallbacks(this);
+ mH.post(this);
+ }
+
+ @Override
+ public void run() {
+ // Called in response to onWallpaperChanged on the main thread.
+
+ if (mLoader != null) {
+ mLoader.cancel(false /* interrupt */);
+ }
+
+ final int currentUser = mCurrentUserId;
+ final UserHandle selectedUser = mSelectedUser;
+ mLoader = new AsyncTask<Void, Void, LoaderResult>() {
+ @Override
+ protected LoaderResult doInBackground(Void... params) {
+ return loadBitmap(currentUser, selectedUser);
+ }
+
+ @Override
+ protected void onPostExecute(LoaderResult result) {
+ super.onPostExecute(result);
+ if (isCancelled()) {
+ return;
+ }
+ if (result.success) {
+ mCached = true;
+ mCache = result.bitmap;
+ mUpdateMonitor.setHasLockscreenWallpaper(result.bitmap != null);
+ mBar.updateMediaMetaData(
+ true /* metaDataChanged */, true /* allowEnterAnimation */);
+ }
+ mLoader = null;
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private static class LoaderResult {
+ public final boolean success;
+ public final Bitmap bitmap;
+
+ LoaderResult(boolean success, Bitmap bitmap) {
+ this.success = success;
+ this.bitmap = bitmap;
+ }
+
+ static LoaderResult success(Bitmap b) {
+ return new LoaderResult(true, b);
+ }
+
+ static LoaderResult fail() {
+ return new LoaderResult(false, null);
+ }
+ }
+
+ /**
+ * Drawable that aligns left horizontally and center vertically (like ImageWallpaper).
+ */
+ public static class WallpaperDrawable extends DrawableWrapper {
+
+ private final ConstantState mState;
+ private final Rect mTmpRect = new Rect();
+
+ public WallpaperDrawable(Resources r, Bitmap b) {
+ this(r, new ConstantState(b));
+ }
+
+ private WallpaperDrawable(Resources r, ConstantState state) {
+ super(new BitmapDrawable(r, state.mBackground));
+ mState = state;
+ }
+
+ @Override
+ public void setXfermode(@Nullable Xfermode mode) {
+ // DrawableWrapper does not call this for us.
+ getDrawable().setXfermode(mode);
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return -1;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return -1;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ int vwidth = getBounds().width();
+ int vheight = getBounds().height();
+ int dwidth = mState.mBackground.getWidth();
+ int dheight = mState.mBackground.getHeight();
+ float scale;
+ float dx = 0, dy = 0;
+
+ if (dwidth * vheight > vwidth * dheight) {
+ scale = (float) vheight / (float) dheight;
+ } else {
+ scale = (float) vwidth / (float) dwidth;
+ }
+
+ if (scale <= 1f) {
+ scale = 1f;
+ }
+ dy = (vheight - dheight * scale) * 0.5f;
+
+ mTmpRect.set(
+ bounds.left,
+ bounds.top + Math.round(dy),
+ bounds.left + Math.round(dwidth * scale),
+ bounds.top + Math.round(dheight * scale + dy));
+
+ super.onBoundsChange(mTmpRect);
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ return mState;
+ }
+
+ static class ConstantState extends Drawable.ConstantState {
+
+ private final Bitmap mBackground;
+
+ ConstantState(Bitmap background) {
+ mBackground = background;
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return newDrawable(null);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ return new WallpaperDrawable(res, this);
+ }
+
+ @Override
+ public int getChangingConfigurations() {
+ // DrawableWrapper already handles this for us.
+ return 0;
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/ManagedProfileController.java b/com/android/systemui/statusbar/phone/ManagedProfileController.java
new file mode 100644
index 0000000..4969a1c
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ManagedProfileController.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 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 com.android.systemui.statusbar.phone.ManagedProfileController.Callback;
+import com.android.systemui.statusbar.policy.CallbackController;
+
+public interface ManagedProfileController extends CallbackController<Callback> {
+
+ void setWorkModeEnabled(boolean enabled);
+
+ boolean hasActiveProfile();
+
+ boolean isWorkModeEnabled();
+
+ public interface Callback {
+ void onManagedProfileChanged();
+ void onManagedProfileRemoved();
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
new file mode 100644
index 0000000..316bd5b
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 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.app.ActivityManager;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.UserInfo;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class ManagedProfileControllerImpl implements ManagedProfileController {
+
+ private final List<Callback> mCallbacks = new ArrayList<>();
+
+ private final Context mContext;
+ private final UserManager mUserManager;
+ private final LinkedList<UserInfo> mProfiles;
+ private boolean mListening;
+ private int mCurrentUser;
+
+ public ManagedProfileControllerImpl(Context context) {
+ mContext = context;
+ mUserManager = UserManager.get(mContext);
+ mProfiles = new LinkedList<UserInfo>();
+ }
+
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
+ if (mCallbacks.size() == 1) {
+ setListening(true);
+ }
+ callback.onManagedProfileChanged();
+ }
+
+ public void removeCallback(Callback callback) {
+ if (mCallbacks.remove(callback) && mCallbacks.size() == 0) {
+ setListening(false);
+ }
+ }
+
+ public void setWorkModeEnabled(boolean enableWorkMode) {
+ synchronized (mProfiles) {
+ for (UserInfo ui : mProfiles) {
+ if (enableWorkMode) {
+ if (!mUserManager.trySetQuietModeDisabled(ui.id, null)) {
+ StatusBarManager statusBarManager = (StatusBarManager) mContext
+ .getSystemService(android.app.Service.STATUS_BAR_SERVICE);
+ statusBarManager.collapsePanels();
+ }
+ } else {
+ mUserManager.setQuietModeEnabled(ui.id, true);
+ }
+ }
+ }
+ }
+
+ private void reloadManagedProfiles() {
+ synchronized (mProfiles) {
+ boolean hadProfile = mProfiles.size() > 0;
+ int user = ActivityManager.getCurrentUser();
+ mProfiles.clear();
+
+ for (UserInfo ui : mUserManager.getEnabledProfiles(user)) {
+ if (ui.isManagedProfile()) {
+ mProfiles.add(ui);
+ }
+ }
+ if (mProfiles.size() == 0 && hadProfile && (user == mCurrentUser)) {
+ for (Callback callback : mCallbacks) {
+ callback.onManagedProfileRemoved();
+ }
+ }
+ mCurrentUser = user;
+ }
+ }
+
+ public boolean hasActiveProfile() {
+ if (!mListening) reloadManagedProfiles();
+ synchronized (mProfiles) {
+ return mProfiles.size() > 0;
+ }
+ }
+
+ public boolean isWorkModeEnabled() {
+ if (!mListening) reloadManagedProfiles();
+ synchronized (mProfiles) {
+ for (UserInfo ui : mProfiles) {
+ if (ui.isQuietModeEnabled()) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private void setListening(boolean listening) {
+ mListening = listening;
+ if (listening) {
+ reloadManagedProfiles();
+
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
+ mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, null);
+ } else {
+ mContext.unregisterReceiver(mReceiver);
+ }
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ reloadManagedProfiles();
+ for (Callback callback : mCallbacks) {
+ callback.onManagedProfileChanged();
+ }
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/com/android/systemui/statusbar/phone/MultiUserSwitch.java
new file mode 100644
index 0000000..f393dcd
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/MultiUserSwitch.java
@@ -0,0 +1,193 @@
+/*
+ * 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.content.Context;
+import android.content.Intent;
+import android.os.UserManager;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
+import android.widget.FrameLayout;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.Prefs;
+import com.android.systemui.Prefs.Key;
+import com.android.systemui.R;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.DetailAdapter;
+import com.android.systemui.qs.QSPanel;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+
+/**
+ * Container for image of the multi user switcher (tappable).
+ */
+public class MultiUserSwitch extends FrameLayout implements View.OnClickListener {
+
+ protected QSPanel mQsPanel;
+ private KeyguardUserSwitcher mKeyguardUserSwitcher;
+ private boolean mKeyguardMode;
+ private UserSwitcherController.BaseUserAdapter mUserListener;
+
+ final UserManager mUserManager;
+
+ private final int[] mTmpInt2 = new int[2];
+
+ protected UserSwitcherController mUserSwitcherController;
+
+ public MultiUserSwitch(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mUserManager = UserManager.get(getContext());
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ setOnClickListener(this);
+ refreshContentDescription();
+ }
+
+ public void setQsPanel(QSPanel qsPanel) {
+ mQsPanel = qsPanel;
+ setUserSwitcherController(Dependency.get(UserSwitcherController.class));
+ }
+
+ public boolean hasMultipleUsers() {
+ if (mUserListener == null) {
+ return false;
+ }
+ return mUserListener.getUserCount() != 0
+ && Prefs.getBoolean(getContext(), Key.SEEN_MULTI_USER, false);
+ }
+
+ public void setUserSwitcherController(UserSwitcherController userSwitcherController) {
+ mUserSwitcherController = userSwitcherController;
+ registerListener();
+ refreshContentDescription();
+ }
+
+ public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
+ mKeyguardUserSwitcher = keyguardUserSwitcher;
+ }
+
+ public void setKeyguardMode(boolean keyguardShowing) {
+ mKeyguardMode = keyguardShowing;
+ registerListener();
+ }
+
+ private void registerListener() {
+ if (mUserManager.isUserSwitcherEnabled() && mUserListener == null) {
+
+ final UserSwitcherController controller = mUserSwitcherController;
+ if (controller != null) {
+ mUserListener = new UserSwitcherController.BaseUserAdapter(controller) {
+ @Override
+ public void notifyDataSetChanged() {
+ refreshContentDescription();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return null;
+ }
+ };
+ refreshContentDescription();
+ }
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mUserManager.isUserSwitcherEnabled()) {
+ if (mKeyguardMode) {
+ if (mKeyguardUserSwitcher != null) {
+ mKeyguardUserSwitcher.show(true /* animate */);
+ }
+ } else if (mQsPanel != null && mUserSwitcherController != null) {
+ View center = getChildCount() > 0 ? getChildAt(0) : this;
+
+ center.getLocationInWindow(mTmpInt2);
+ mTmpInt2[0] += center.getWidth() / 2;
+ mTmpInt2[1] += center.getHeight() / 2;
+
+ mQsPanel.showDetailAdapter(true,
+ getUserDetailAdapter(),
+ mTmpInt2);
+ }
+ } else {
+ if (mQsPanel != null) {
+ Intent intent = ContactsContract.QuickContact.composeQuickContactsIntent(
+ getContext(), v, ContactsContract.Profile.CONTENT_URI,
+ ContactsContract.QuickContact.MODE_LARGE, null);
+ Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(intent, 0);
+ }
+ }
+ }
+
+ @Override
+ public void setClickable(boolean clickable) {
+ super.setClickable(clickable);
+ refreshContentDescription();
+ }
+
+ private void refreshContentDescription() {
+ String currentUser = null;
+ if (mUserManager.isUserSwitcherEnabled()
+ && mUserSwitcherController != null) {
+ currentUser = mUserSwitcherController.getCurrentUserName(mContext);
+ }
+
+ String text = null;
+
+ if (!TextUtils.isEmpty(currentUser)) {
+ text = mContext.getString(
+ R.string.accessibility_quick_settings_user,
+ currentUser);
+ }
+
+ if (!TextUtils.equals(getContentDescription(), text)) {
+ setContentDescription(text);
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(Button.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(Button.class.getName());
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ protected DetailAdapter getUserDetailAdapter() {
+ return mUserSwitcherController.userDetailAdapter;
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
new file mode 100644
index 0000000..cfe0a4a
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2017 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 static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
+import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.app.StatusBarManager.windowStateToString;
+
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_WINDOW_STATE;
+import static com.android.systemui.statusbar.phone.StatusBar.dumpBarTransitions;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManagerNative;
+import android.app.Fragment;
+import android.app.IActivityManager;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.database.ContentObserver;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.inputmethodservice.InputMethodService;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.IRotationWatcher.Stub;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.keyguard.LatencyTracker;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.stackdivider.Divider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.CommandQueue.Callbacks;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
+import com.android.systemui.statusbar.policy.KeyButtonView;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Fragment containing the NavigationBarFragment. Contains logic for what happens
+ * on clicks and view states of the nav bar.
+ */
+public class NavigationBarFragment extends Fragment implements Callbacks {
+
+ public static final String TAG = "NavigationBar";
+ private static final boolean DEBUG = false;
+ private static final String EXTRA_DISABLE_STATE = "disabled_state";
+
+ /** Allow some time inbetween the long press for back and recents. */
+ private static final int LOCK_TO_APP_GESTURE_TOLERENCE = 200;
+
+ protected NavigationBarView mNavigationBarView = null;
+ protected AssistManager mAssistManager;
+
+ private int mNavigationBarWindowState = WINDOW_STATE_SHOWING;
+
+ private int mNavigationIconHints = 0;
+ private int mNavigationBarMode;
+ private AccessibilityManager mAccessibilityManager;
+ private MagnificationContentObserver mMagnificationObserver;
+ private ContentResolver mContentResolver;
+
+ private int mDisabledFlags1;
+ private StatusBar mStatusBar;
+ private Recents mRecents;
+ private Divider mDivider;
+ private WindowManager mWindowManager;
+ private CommandQueue mCommandQueue;
+ private long mLastLockToAppLongPress;
+
+ private Locale mLocale;
+ private int mLayoutDirection;
+
+ private int mSystemUiVisibility;
+ private LightBarController mLightBarController;
+
+ public boolean mHomeBlockedThisTouch;
+
+ // ----- Fragment Lifecycle Callbacks -----
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mCommandQueue = SysUiServiceProvider.getComponent(getContext(), CommandQueue.class);
+ mCommandQueue.addCallbacks(this);
+ mStatusBar = SysUiServiceProvider.getComponent(getContext(), StatusBar.class);
+ mRecents = SysUiServiceProvider.getComponent(getContext(), Recents.class);
+ mDivider = SysUiServiceProvider.getComponent(getContext(), Divider.class);
+ mWindowManager = getContext().getSystemService(WindowManager.class);
+ mAccessibilityManager = getContext().getSystemService(AccessibilityManager.class);
+ Dependency.get(AccessibilityManagerWrapper.class).addCallback(
+ mAccessibilityListener);
+ mContentResolver = getContext().getContentResolver();
+ mMagnificationObserver = new MagnificationContentObserver(
+ getContext().getMainThreadHandler());
+ mContentResolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED), false,
+ mMagnificationObserver, UserHandle.USER_ALL);
+
+ if (savedInstanceState != null) {
+ mDisabledFlags1 = savedInstanceState.getInt(EXTRA_DISABLE_STATE, 0);
+ }
+ mAssistManager = Dependency.get(AssistManager.class);
+
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .watchRotation(mRotationWatcher, getContext().getDisplay().getDisplayId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mCommandQueue.removeCallbacks(this);
+ Dependency.get(AccessibilityManagerWrapper.class).removeCallback(
+ mAccessibilityListener);
+ mContentResolver.unregisterContentObserver(mMagnificationObserver);
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .removeRotationWatcher(mRotationWatcher);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.navigation_bar, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mNavigationBarView = (NavigationBarView) view;
+
+ mNavigationBarView.setDisabledFlags(mDisabledFlags1);
+ mNavigationBarView.setComponents(mRecents, mDivider);
+ mNavigationBarView.setOnVerticalChangedListener(this::onVerticalChanged);
+ mNavigationBarView.setOnTouchListener(this::onNavigationTouch);
+ if (savedInstanceState != null) {
+ mNavigationBarView.getLightTransitionsController().restoreState(savedInstanceState);
+ }
+
+ prepareNavigationBarView();
+ checkNavBarModes();
+
+ IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+ notifyNavigationBarScreenOn();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mNavigationBarView.getLightTransitionsController().destroy(getContext());
+ getContext().unregisterReceiver(mBroadcastReceiver);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(EXTRA_DISABLE_STATE, mDisabledFlags1);
+ if (mNavigationBarView != null) {
+ mNavigationBarView.getLightTransitionsController().saveState(outState);
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ final Locale locale = getContext().getResources().getConfiguration().locale;
+ final int ld = TextUtils.getLayoutDirectionFromLocale(locale);
+ if (!locale.equals(mLocale) || ld != mLayoutDirection) {
+ if (DEBUG) {
+ Log.v(TAG, String.format(
+ "config changed locale/LD: %s (%d) -> %s (%d)", mLocale, mLayoutDirection,
+ locale, ld));
+ }
+ mLocale = locale;
+ mLayoutDirection = ld;
+ refreshLayout(ld);
+ }
+ repositionNavigationBar();
+ }
+
+ @Override
+ public void dump(String prefix, FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mNavigationBarView != null) {
+ pw.print(" mNavigationBarWindowState=");
+ pw.println(windowStateToString(mNavigationBarWindowState));
+ pw.print(" mNavigationBarMode=");
+ pw.println(BarTransitions.modeToString(mNavigationBarMode));
+ dumpBarTransitions(pw, "mNavigationBarView", mNavigationBarView.getBarTransitions());
+ }
+
+ pw.print(" mNavigationBarView=");
+ if (mNavigationBarView == null) {
+ pw.println("null");
+ } else {
+ mNavigationBarView.dump(fd, pw, args);
+ }
+ }
+
+ // ----- CommandQueue Callbacks -----
+
+ @Override
+ public void setImeWindowStatus(IBinder token, int vis, int backDisposition,
+ boolean showImeSwitcher) {
+ boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0;
+ int hints = mNavigationIconHints;
+ if ((backDisposition == InputMethodService.BACK_DISPOSITION_WILL_DISMISS) || imeShown) {
+ hints |= NAVIGATION_HINT_BACK_ALT;
+ } else {
+ hints &= ~NAVIGATION_HINT_BACK_ALT;
+ }
+ if (showImeSwitcher) {
+ hints |= NAVIGATION_HINT_IME_SHOWN;
+ } else {
+ hints &= ~NAVIGATION_HINT_IME_SHOWN;
+ }
+ if (hints == mNavigationIconHints) return;
+
+ mNavigationIconHints = hints;
+
+ if (mNavigationBarView != null) {
+ mNavigationBarView.setNavigationIconHints(hints);
+ }
+ mStatusBar.checkBarModes();
+ }
+
+ @Override
+ public void topAppWindowChanged(boolean showMenu) {
+ if (mNavigationBarView != null) {
+ mNavigationBarView.setMenuVisibility(showMenu);
+ }
+ }
+
+ @Override
+ public void setWindowState(int window, int state) {
+ if (mNavigationBarView != null
+ && window == StatusBarManager.WINDOW_NAVIGATION_BAR
+ && mNavigationBarWindowState != state) {
+ mNavigationBarWindowState = state;
+ if (DEBUG_WINDOW_STATE) Log.d(TAG, "Navigation bar " + windowStateToString(state));
+ }
+ }
+
+ // Injected from StatusBar at creation.
+ public void setCurrentSysuiVisibility(int systemUiVisibility) {
+ mSystemUiVisibility = systemUiVisibility;
+ mNavigationBarMode = mStatusBar.computeBarMode(0, mSystemUiVisibility,
+ View.NAVIGATION_BAR_TRANSIENT, View.NAVIGATION_BAR_TRANSLUCENT,
+ View.NAVIGATION_BAR_TRANSPARENT);
+ checkNavBarModes();
+ mStatusBar.touchAutoHide();
+ mLightBarController.onNavigationVisibilityChanged(mSystemUiVisibility, 0 /* mask */,
+ true /* nbModeChanged */, mNavigationBarMode);
+ }
+
+ @Override
+ public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
+ int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
+ final int oldVal = mSystemUiVisibility;
+ final int newVal = (oldVal & ~mask) | (vis & mask);
+ final int diff = newVal ^ oldVal;
+ boolean nbModeChanged = false;
+ if (diff != 0) {
+ mSystemUiVisibility = newVal;
+
+ // update navigation bar mode
+ final int nbMode = getView() == null
+ ? -1 : mStatusBar.computeBarMode(oldVal, newVal,
+ View.NAVIGATION_BAR_TRANSIENT, View.NAVIGATION_BAR_TRANSLUCENT,
+ View.NAVIGATION_BAR_TRANSPARENT);
+ nbModeChanged = nbMode != -1;
+ if (nbModeChanged) {
+ if (mNavigationBarMode != nbMode) {
+ mNavigationBarMode = nbMode;
+ checkNavBarModes();
+ }
+ mStatusBar.touchAutoHide();
+ }
+ }
+
+ mLightBarController.onNavigationVisibilityChanged(vis, mask, nbModeChanged,
+ mNavigationBarMode);
+ }
+
+ @Override
+ public void disable(int state1, int state2, boolean animate) {
+ // All navigation bar flags are in state1.
+ int masked = state1 & (StatusBarManager.DISABLE_HOME
+ | StatusBarManager.DISABLE_RECENT
+ | StatusBarManager.DISABLE_BACK
+ | StatusBarManager.DISABLE_SEARCH);
+ if (masked != mDisabledFlags1) {
+ mDisabledFlags1 = masked;
+ if (mNavigationBarView != null) mNavigationBarView.setDisabledFlags(state1);
+ }
+ }
+
+ // ----- Internal stuffz -----
+
+ private void refreshLayout(int layoutDirection) {
+ if (mNavigationBarView != null) {
+ mNavigationBarView.setLayoutDirection(layoutDirection);
+ }
+ }
+
+ private boolean shouldDisableNavbarGestures() {
+ return !mStatusBar.isDeviceProvisioned()
+ || (mDisabledFlags1 & StatusBarManager.DISABLE_SEARCH) != 0;
+ }
+
+ private void repositionNavigationBar() {
+ if (mNavigationBarView == null || !mNavigationBarView.isAttachedToWindow()) return;
+
+ prepareNavigationBarView();
+
+ mWindowManager.updateViewLayout((View) mNavigationBarView.getParent(),
+ ((View) mNavigationBarView.getParent()).getLayoutParams());
+ }
+
+ private void notifyNavigationBarScreenOn() {
+ mNavigationBarView.notifyScreenOn();
+ }
+
+ private void prepareNavigationBarView() {
+ mNavigationBarView.reorient();
+
+ ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();
+ recentsButton.setOnClickListener(this::onRecentsClick);
+ recentsButton.setOnTouchListener(this::onRecentsTouch);
+ recentsButton.setLongClickable(true);
+ recentsButton.setOnLongClickListener(this::onLongPressBackRecents);
+
+ ButtonDispatcher backButton = mNavigationBarView.getBackButton();
+ backButton.setLongClickable(true);
+ backButton.setOnLongClickListener(this::onLongPressBackRecents);
+
+ ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
+ homeButton.setOnTouchListener(this::onHomeTouch);
+ homeButton.setOnLongClickListener(this::onHomeLongClick);
+
+ ButtonDispatcher accessibilityButton = mNavigationBarView.getAccessibilityButton();
+ accessibilityButton.setOnClickListener(this::onAccessibilityClick);
+ accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick);
+ updateAccessibilityServicesState(mAccessibilityManager);
+ }
+
+ private boolean onHomeTouch(View v, MotionEvent event) {
+ if (mHomeBlockedThisTouch && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+ return true;
+ }
+ // If an incoming call is ringing, HOME is totally disabled.
+ // (The user is already on the InCallUI at this point,
+ // and his ONLY options are to answer or reject the call.)
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mHomeBlockedThisTouch = false;
+ TelecomManager telecomManager =
+ getContext().getSystemService(TelecomManager.class);
+ if (telecomManager != null && telecomManager.isRinging()) {
+ if (mStatusBar.isKeyguardShowing()) {
+ Log.i(TAG, "Ignoring HOME; there's a ringing incoming call. " +
+ "No heads up");
+ mHomeBlockedThisTouch = true;
+ return true;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mStatusBar.awakenDreams();
+ break;
+ }
+ return false;
+ }
+
+ private void onVerticalChanged(boolean isVertical) {
+ mStatusBar.setQsScrimEnabled(!isVertical);
+ }
+
+ private boolean onNavigationTouch(View v, MotionEvent event) {
+ mStatusBar.checkUserAutohide(v, event);
+ return false;
+ }
+
+ @VisibleForTesting
+ boolean onHomeLongClick(View v) {
+ if (shouldDisableNavbarGestures()) {
+ return false;
+ }
+ MetricsLogger.action(getContext(), MetricsEvent.ACTION_ASSIST_LONG_PRESS);
+ mAssistManager.startAssist(new Bundle() /* args */);
+ mStatusBar.awakenDreams();
+ if (mNavigationBarView != null) {
+ mNavigationBarView.abortCurrentGesture();
+ }
+ return true;
+ }
+
+ // additional optimization when we have software system buttons - start loading the recent
+ // tasks on touch down
+ private boolean onRecentsTouch(View v, MotionEvent event) {
+ int action = event.getAction() & MotionEvent.ACTION_MASK;
+ if (action == MotionEvent.ACTION_DOWN) {
+ mCommandQueue.preloadRecentApps();
+ } else if (action == MotionEvent.ACTION_CANCEL) {
+ mCommandQueue.cancelPreloadRecentApps();
+ } else if (action == MotionEvent.ACTION_UP) {
+ if (!v.isPressed()) {
+ mCommandQueue.cancelPreloadRecentApps();
+ }
+ }
+ return false;
+ }
+
+ private void onRecentsClick(View v) {
+ if (LatencyTracker.isEnabled(getContext())) {
+ LatencyTracker.getInstance(getContext()).onActionStart(
+ LatencyTracker.ACTION_TOGGLE_RECENTS);
+ }
+ mStatusBar.awakenDreams();
+ mCommandQueue.toggleRecentApps();
+ }
+
+ /**
+ * This handles long-press of both back and recents. They are
+ * handled together to capture them both being long-pressed
+ * at the same time to exit screen pinning (lock task).
+ *
+ * When accessibility mode is on, only a long-press from recents
+ * is required to exit.
+ *
+ * In all other circumstances we try to pass through long-press events
+ * for Back, so that apps can still use it. Which can be from two things.
+ * 1) Not currently in screen pinning (lock task).
+ * 2) Back is long-pressed without recents.
+ */
+ private boolean onLongPressBackRecents(View v) {
+ try {
+ boolean sendBackLongPress = false;
+ IActivityManager activityManager = ActivityManagerNative.getDefault();
+ boolean touchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled();
+ boolean inLockTaskMode = activityManager.isInLockTaskMode();
+ if (inLockTaskMode && !touchExplorationEnabled) {
+ long time = System.currentTimeMillis();
+ // If we recently long-pressed the other button then they were
+ // long-pressed 'together'
+ if ((time - mLastLockToAppLongPress) < LOCK_TO_APP_GESTURE_TOLERENCE) {
+ activityManager.stopSystemLockTaskMode();
+ // When exiting refresh disabled flags.
+ mNavigationBarView.setDisabledFlags(mDisabledFlags1, true);
+ return true;
+ } else if ((v.getId() == R.id.back)
+ && !mNavigationBarView.getRecentsButton().getCurrentView().isPressed()) {
+ // If we aren't pressing recents right now then they presses
+ // won't be together, so send the standard long-press action.
+ sendBackLongPress = true;
+ }
+ mLastLockToAppLongPress = time;
+ } else {
+ // If this is back still need to handle sending the long-press event.
+ if (v.getId() == R.id.back) {
+ sendBackLongPress = true;
+ } else if (touchExplorationEnabled && inLockTaskMode) {
+ // When in accessibility mode a long press that is recents (not back)
+ // should stop lock task.
+ activityManager.stopSystemLockTaskMode();
+ // When exiting refresh disabled flags.
+ mNavigationBarView.setDisabledFlags(mDisabledFlags1, true);
+ return true;
+ } else if (v.getId() == R.id.recent_apps) {
+ return onLongPressRecents();
+ }
+ }
+ if (sendBackLongPress) {
+ KeyButtonView keyButtonView = (KeyButtonView) v;
+ keyButtonView.sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
+ keyButtonView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
+ return true;
+ }
+ } catch (RemoteException e) {
+ Log.d(TAG, "Unable to reach activity manager", e);
+ }
+ return false;
+ }
+
+ private boolean onLongPressRecents() {
+ if (mRecents == null || !ActivityManager.supportsMultiWindow(getContext())
+ || !mDivider.getView().getSnapAlgorithm().isSplitScreenFeasible()
+ || Recents.getConfiguration().isLowRamDevice) {
+ return false;
+ }
+
+ return mStatusBar.toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
+ MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
+ }
+
+ private void onAccessibilityClick(View v) {
+ mAccessibilityManager.notifyAccessibilityButtonClicked();
+ }
+
+ private boolean onAccessibilityLongClick(View v) {
+ Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ v.getContext().startActivityAsUser(intent, UserHandle.CURRENT);
+ return true;
+ }
+
+ private void updateAccessibilityServicesState(AccessibilityManager accessibilityManager) {
+ int requestingServices = 0;
+ try {
+ if (Settings.Secure.getIntForUser(mContentResolver,
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED,
+ UserHandle.USER_CURRENT) == 1) {
+ requestingServices++;
+ }
+ } catch (Settings.SettingNotFoundException e) {
+ }
+
+ // AccessibilityManagerService resolves services for the current user since the local
+ // AccessibilityManager is created from a Context with the INTERACT_ACROSS_USERS permission
+ final List<AccessibilityServiceInfo> services =
+ accessibilityManager.getEnabledAccessibilityServiceList(
+ AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+ for (int i = services.size() - 1; i >= 0; --i) {
+ AccessibilityServiceInfo info = services.get(i);
+ if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) {
+ requestingServices++;
+ }
+ }
+
+ final boolean showAccessibilityButton = requestingServices >= 1;
+ final boolean targetSelection = requestingServices >= 2;
+ mNavigationBarView.setAccessibilityButtonState(showAccessibilityButton, targetSelection);
+ }
+
+ // ----- Methods that StatusBar talks to (should be minimized) -----
+
+ public void setLightBarController(LightBarController lightBarController) {
+ mLightBarController = lightBarController;
+ mLightBarController.setNavigationBar(mNavigationBarView.getLightTransitionsController());
+ }
+
+ public boolean isSemiTransparent() {
+ return mNavigationBarMode == MODE_SEMI_TRANSPARENT;
+ }
+
+ public void disableAnimationsDuringHide(long delay) {
+ mNavigationBarView.setLayoutTransitionsEnabled(false);
+ mNavigationBarView.postDelayed(() -> mNavigationBarView.setLayoutTransitionsEnabled(true),
+ delay + StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE);
+ }
+
+ public BarTransitions getBarTransitions() {
+ return mNavigationBarView.getBarTransitions();
+ }
+
+ public void checkNavBarModes() {
+ mStatusBar.checkBarMode(mNavigationBarMode,
+ mNavigationBarWindowState, mNavigationBarView.getBarTransitions());
+ }
+
+ public void finishBarAnimations() {
+ mNavigationBarView.getBarTransitions().finishAnimations();
+ }
+
+ private final AccessibilityServicesStateChangeListener mAccessibilityListener =
+ this::updateAccessibilityServicesState;
+
+ private class MagnificationContentObserver extends ContentObserver {
+
+ public MagnificationContentObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ NavigationBarFragment.this.updateAccessibilityServicesState(mAccessibilityManager);
+ }
+ }
+
+ private final Stub mRotationWatcher = new Stub() {
+ @Override
+ public void onRotationChanged(int rotation) throws RemoteException {
+ // We need this to be scheduled as early as possible to beat the redrawing of
+ // window in response to the orientation change.
+ Handler h = getView().getHandler();
+ Message msg = Message.obtain(h, () -> {
+ if (mNavigationBarView != null
+ && mNavigationBarView.needsReorient(rotation)) {
+ repositionNavigationBar();
+ }
+ });
+ msg.setAsynchronous(true);
+ h.sendMessageAtFrontOfQueue(msg);
+ }
+ };
+
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_SCREEN_OFF.equals(action)
+ || Intent.ACTION_SCREEN_ON.equals(action)) {
+ notifyNavigationBarScreenOn();
+ }
+ }
+ };
+
+ public static View create(Context context, FragmentListener listener) {
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
+ WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+ | WindowManager.LayoutParams.FLAG_SLIPPERY,
+ PixelFormat.TRANSLUCENT);
+ lp.token = new Binder();
+ lp.setTitle("NavigationBar");
+ lp.windowAnimations = 0;
+
+ View navigationBarView = LayoutInflater.from(context).inflate(
+ R.layout.navigation_bar_window, null);
+
+ if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + navigationBarView);
+ if (navigationBarView == null) return null;
+
+ context.getSystemService(WindowManager.class).addView(navigationBarView, lp);
+ FragmentHostManager fragmentHost = FragmentHostManager.get(navigationBarView);
+ NavigationBarFragment fragment = new NavigationBarFragment();
+ fragmentHost.getFragmentManager().beginTransaction()
+ .replace(R.id.navigation_bar_frame, fragment, TAG)
+ .commit();
+ fragmentHost.addTagListener(TAG, listener);
+ return navigationBarView;
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarFrame.java b/com/android/systemui/statusbar/phone/NavigationBarFrame.java
new file mode 100644
index 0000000..741f783
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarFrame.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 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 static android.view.MotionEvent.ACTION_OUTSIDE;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+
+import com.android.systemui.statusbar.policy.DeadZone;
+
+public class NavigationBarFrame extends FrameLayout {
+
+ private DeadZone mDeadZone = null;
+
+ public NavigationBarFrame(@NonNull Context context) {
+ super(context);
+ }
+
+ public NavigationBarFrame(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NavigationBarFrame(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public void setDeadZone(@NonNull DeadZone deadZone) {
+ mDeadZone = deadZone;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (event.getAction() == ACTION_OUTSIDE) {
+ if (mDeadZone != null) {
+ return mDeadZone.onTouchEvent(event);
+ }
+ }
+ return super.dispatchTouchEvent(event);
+ }
+}
\ No newline at end of file
diff --git a/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java b/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
new file mode 100644
index 0000000..ee9a791
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
@@ -0,0 +1,316 @@
+/*
+ * 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.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.RecentsComponent;
+import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
+import com.android.systemui.stackdivider.Divider;
+import com.android.systemui.tuner.TunerService;
+
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+/**
+ * Class to detect gestures on the navigation bar.
+ */
+public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureListener
+ implements TunerService.Tunable, GestureHelper {
+
+ private static final String KEY_DOCK_WINDOW_GESTURE = "overview_nav_bar_gesture";
+ /**
+ * When dragging from the navigation bar, we drag in recents.
+ */
+ public static final int DRAG_MODE_NONE = -1;
+
+ /**
+ * When dragging from the navigation bar, we drag in recents.
+ */
+ public static final int DRAG_MODE_RECENTS = 0;
+
+ /**
+ * When dragging from the navigation bar, we drag the divider.
+ */
+ public static final int DRAG_MODE_DIVIDER = 1;
+
+ private RecentsComponent mRecentsComponent;
+ private Divider mDivider;
+ private Context mContext;
+ private NavigationBarView mNavigationBarView;
+ private boolean mIsVertical;
+ private boolean mIsRTL;
+
+ private final GestureDetector mTaskSwitcherDetector;
+ private final int mScrollTouchSlop;
+ private final int mMinFlingVelocity;
+ private int mTouchDownX;
+ private int mTouchDownY;
+ private boolean mDownOnRecents;
+ private VelocityTracker mVelocityTracker;
+
+ private boolean mDockWindowEnabled;
+ private boolean mDockWindowTouchSlopExceeded;
+ private int mDragMode;
+
+ public NavigationBarGestureHelper(Context context) {
+ mContext = context;
+ ViewConfiguration configuration = ViewConfiguration.get(context);
+ Resources r = context.getResources();
+ mScrollTouchSlop = r.getDimensionPixelSize(R.dimen.navigation_bar_min_swipe_distance);
+ mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+ mTaskSwitcherDetector = new GestureDetector(context, this);
+ Dependency.get(TunerService.class).addTunable(this, KEY_DOCK_WINDOW_GESTURE);
+ }
+
+ public void destroy() {
+ Dependency.get(TunerService.class).removeTunable(this);
+ }
+
+ public void setComponents(RecentsComponent recentsComponent, Divider divider,
+ NavigationBarView navigationBarView) {
+ mRecentsComponent = recentsComponent;
+ mDivider = divider;
+ mNavigationBarView = navigationBarView;
+ }
+
+ public void setBarState(boolean isVertical, boolean isRTL) {
+ mIsVertical = isVertical;
+ mIsRTL = isRTL;
+ }
+
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ // If we move more than a fixed amount, then start capturing for the
+ // task switcher detector
+ mTaskSwitcherDetector.onTouchEvent(event);
+ int action = event.getAction();
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ mTouchDownX = (int) event.getX();
+ mTouchDownY = (int) event.getY();
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ int xDiff = Math.abs(x - mTouchDownX);
+ int yDiff = Math.abs(y - mTouchDownY);
+ boolean exceededTouchSlop = !mIsVertical
+ ? xDiff > mScrollTouchSlop && xDiff > yDiff
+ : yDiff > mScrollTouchSlop && yDiff > xDiff;
+ if (exceededTouchSlop) {
+ return true;
+ }
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ break;
+ }
+ return mDockWindowEnabled && interceptDockWindowEvent(event);
+ }
+
+ private boolean interceptDockWindowEvent(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ handleDragActionDownEvent(event);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ return handleDragActionMoveEvent(event);
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ handleDragActionUpEvent(event);
+ break;
+ }
+ return false;
+ }
+
+ private boolean handleDockWindowEvent(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ handleDragActionDownEvent(event);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ handleDragActionMoveEvent(event);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ handleDragActionUpEvent(event);
+ break;
+ }
+ return true;
+ }
+
+ private void handleDragActionDownEvent(MotionEvent event) {
+ mVelocityTracker = VelocityTracker.obtain();
+ mVelocityTracker.addMovement(event);
+ mDockWindowTouchSlopExceeded = false;
+ mTouchDownX = (int) event.getX();
+ mTouchDownY = (int) event.getY();
+
+ if (mNavigationBarView != null) {
+ View recentsButton = mNavigationBarView.getRecentsButton().getCurrentView();
+ if (recentsButton != null) {
+ mDownOnRecents = mTouchDownX >= recentsButton.getLeft()
+ && mTouchDownX <= recentsButton.getRight()
+ && mTouchDownY >= recentsButton.getTop()
+ && mTouchDownY <= recentsButton.getBottom();
+ } else {
+ mDownOnRecents = false;
+ }
+ }
+ }
+
+ private boolean handleDragActionMoveEvent(MotionEvent event) {
+ mVelocityTracker.addMovement(event);
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ int xDiff = Math.abs(x - mTouchDownX);
+ int yDiff = Math.abs(y - mTouchDownY);
+ if (mDivider == null || mRecentsComponent == null) {
+ return false;
+ }
+ if (!mDockWindowTouchSlopExceeded) {
+ boolean touchSlopExceeded = !mIsVertical
+ ? yDiff > mScrollTouchSlop && yDiff > xDiff
+ : xDiff > mScrollTouchSlop && xDiff > yDiff;
+ if (mDownOnRecents && touchSlopExceeded
+ && mDivider.getView().getWindowManagerProxy().getDockSide() == DOCKED_INVALID) {
+ Rect initialBounds = null;
+ int dragMode = calculateDragMode();
+ int createMode = ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
+ if (dragMode == DRAG_MODE_DIVIDER) {
+ initialBounds = new Rect();
+ mDivider.getView().calculateBoundsForPosition(mIsVertical
+ ? (int) event.getRawX()
+ : (int) event.getRawY(),
+ mDivider.getView().isHorizontalDivision()
+ ? DOCKED_TOP
+ : DOCKED_LEFT,
+ initialBounds);
+ } else if (dragMode == DRAG_MODE_RECENTS && mTouchDownX
+ < mContext.getResources().getDisplayMetrics().widthPixels / 2) {
+ createMode = ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT;
+ }
+ boolean docked = mRecentsComponent.dockTopTask(dragMode, createMode, initialBounds,
+ MetricsEvent.ACTION_WINDOW_DOCK_SWIPE);
+ if (docked) {
+ mDragMode = dragMode;
+ if (mDragMode == DRAG_MODE_DIVIDER) {
+ mDivider.getView().startDragging(false /* animate */, true /* touching*/);
+ }
+ mDockWindowTouchSlopExceeded = true;
+ return true;
+ }
+ }
+ } else {
+ if (mDragMode == DRAG_MODE_DIVIDER) {
+ int position = !mIsVertical ? (int) event.getRawY() : (int) event.getRawX();
+ SnapTarget snapTarget = mDivider.getView().getSnapAlgorithm()
+ .calculateSnapTarget(position, 0f /* velocity */, false /* hardDismiss */);
+ mDivider.getView().resizeStack(position, snapTarget.position, snapTarget);
+ } else if (mDragMode == DRAG_MODE_RECENTS) {
+ mRecentsComponent.onDraggingInRecents(event.getRawY());
+ }
+ }
+ return false;
+ }
+
+ private void handleDragActionUpEvent(MotionEvent event) {
+ mVelocityTracker.addMovement(event);
+ mVelocityTracker.computeCurrentVelocity(1000);
+ if (mDockWindowTouchSlopExceeded && mDivider != null && mRecentsComponent != null) {
+ if (mDragMode == DRAG_MODE_DIVIDER) {
+ mDivider.getView().stopDragging(mIsVertical
+ ? (int) event.getRawX()
+ : (int) event.getRawY(),
+ mIsVertical
+ ? mVelocityTracker.getXVelocity()
+ : mVelocityTracker.getYVelocity(),
+ true /* avoidDismissStart */, false /* logMetrics */);
+ } else if (mDragMode == DRAG_MODE_RECENTS) {
+ mRecentsComponent.onDraggingInRecentsEnded(mVelocityTracker.getYVelocity());
+ }
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ private int calculateDragMode() {
+ if (mIsVertical && !mDivider.getView().isHorizontalDivision()) {
+ return DRAG_MODE_DIVIDER;
+ }
+ if (!mIsVertical && mDivider.getView().isHorizontalDivision()) {
+ return DRAG_MODE_DIVIDER;
+ }
+ return DRAG_MODE_RECENTS;
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean result = mTaskSwitcherDetector.onTouchEvent(event);
+ if (mDockWindowEnabled) {
+ result |= handleDockWindowEvent(event);
+ }
+ return result;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ float absVelX = Math.abs(velocityX);
+ float absVelY = Math.abs(velocityY);
+ boolean isValidFling = absVelX > mMinFlingVelocity &&
+ mIsVertical ? (absVelY > absVelX) : (absVelX > absVelY);
+ if (isValidFling && mRecentsComponent != null) {
+ boolean showNext;
+ if (!mIsRTL) {
+ showNext = mIsVertical ? (velocityY < 0) : (velocityX < 0);
+ } else {
+ // In RTL, vertical is still the same, but horizontal is flipped
+ showNext = mIsVertical ? (velocityY < 0) : (velocityX > 0);
+ }
+ if (showNext) {
+ mRecentsComponent.showNextAffiliatedTask();
+ } else {
+ mRecentsComponent.showPrevAffiliatedTask();
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ switch (key) {
+ case KEY_DOCK_WINDOW_GESTURE:
+ mDockWindowEnabled = newValue != null && (Integer.parseInt(newValue) != 0);
+ break;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
new file mode 100644
index 0000000..4e79314
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2016 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.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.Display.Mode;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.Space;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider;
+import com.android.systemui.statusbar.phone.ReverseLinearLayout.ReverseFrameLayout;
+import com.android.systemui.statusbar.policy.KeyButtonView;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+public class NavigationBarInflaterView extends FrameLayout
+ implements Tunable, PluginListener<NavBarButtonProvider> {
+
+ private static final String TAG = "NavBarInflater";
+
+ public static final String NAV_BAR_VIEWS = "sysui_nav_bar";
+ public static final String NAV_BAR_LEFT = "sysui_nav_bar_left";
+ public static final String NAV_BAR_RIGHT = "sysui_nav_bar_right";
+
+ public static final String MENU_IME = "menu_ime";
+ public static final String BACK = "back";
+ public static final String HOME = "home";
+ public static final String RECENT = "recent";
+ public static final String NAVSPACE = "space";
+ public static final String CLIPBOARD = "clipboard";
+ public static final String KEY = "key";
+ public static final String LEFT = "left";
+ public static final String RIGHT = "right";
+
+ public static final String GRAVITY_SEPARATOR = ";";
+ public static final String BUTTON_SEPARATOR = ",";
+
+ public static final String SIZE_MOD_START = "[";
+ public static final String SIZE_MOD_END = "]";
+
+ public static final String KEY_CODE_START = "(";
+ public static final String KEY_IMAGE_DELIM = ":";
+ public static final String KEY_CODE_END = ")";
+ private static final String WEIGHT_SUFFIX = "W";
+ private static final String WEIGHT_CENTERED_SUFFIX = "WC";
+
+ private final List<NavBarButtonProvider> mPlugins = new ArrayList<>();
+
+ protected LayoutInflater mLayoutInflater;
+ protected LayoutInflater mLandscapeInflater;
+
+ protected FrameLayout mRot0;
+ protected FrameLayout mRot90;
+ private boolean isRot0Landscape;
+
+ private SparseArray<ButtonDispatcher> mButtonDispatchers;
+ private String mCurrentLayout;
+
+ private View mLastPortrait;
+ private View mLastLandscape;
+
+ private boolean mAlternativeOrder;
+
+ public NavigationBarInflaterView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ createInflaters();
+ Display display = ((WindowManager)
+ context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+ Mode displayMode = display.getMode();
+ isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight();
+ }
+
+ private void createInflaters() {
+ mLayoutInflater = LayoutInflater.from(mContext);
+ Configuration landscape = new Configuration();
+ landscape.setTo(mContext.getResources().getConfiguration());
+ landscape.orientation = Configuration.ORIENTATION_LANDSCAPE;
+ mLandscapeInflater = LayoutInflater.from(mContext.createConfigurationContext(landscape));
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ inflateChildren();
+ clearViews();
+ inflateLayout(getDefaultLayout());
+ }
+
+ private void inflateChildren() {
+ removeAllViews();
+ mRot0 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout, this, false);
+ mRot0.setId(R.id.rot0);
+ addView(mRot0);
+ mRot90 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_rot90, this,
+ false);
+ mRot90.setId(R.id.rot90);
+ addView(mRot90);
+ updateAlternativeOrder();
+ }
+
+ protected String getDefaultLayout() {
+ return mContext.getString(R.string.config_navBarLayout);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ Dependency.get(TunerService.class).addTunable(this, NAV_BAR_VIEWS, NAV_BAR_LEFT,
+ NAV_BAR_RIGHT);
+ Dependency.get(PluginManager.class).addPluginListener(this,
+ NavBarButtonProvider.class, true /* Allow multiple */);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ Dependency.get(TunerService.class).removeTunable(this);
+ Dependency.get(PluginManager.class).removePluginListener(this);
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ if (NAV_BAR_VIEWS.equals(key)) {
+ if (!Objects.equals(mCurrentLayout, newValue)) {
+ clearViews();
+ inflateLayout(newValue);
+ }
+ } else if (NAV_BAR_LEFT.equals(key) || NAV_BAR_RIGHT.equals(key)) {
+ clearViews();
+ inflateLayout(mCurrentLayout);
+ }
+ }
+
+ public void setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDisatchers) {
+ mButtonDispatchers = buttonDisatchers;
+ for (int i = 0; i < buttonDisatchers.size(); i++) {
+ initiallyFill(buttonDisatchers.valueAt(i));
+ }
+ }
+
+ public void setAlternativeOrder(boolean alternativeOrder) {
+ if (alternativeOrder != mAlternativeOrder) {
+ mAlternativeOrder = alternativeOrder;
+ updateAlternativeOrder();
+ }
+ }
+
+ private void updateAlternativeOrder() {
+ updateAlternativeOrder(mRot0.findViewById(R.id.ends_group));
+ updateAlternativeOrder(mRot0.findViewById(R.id.center_group));
+ updateAlternativeOrder(mRot90.findViewById(R.id.ends_group));
+ updateAlternativeOrder(mRot90.findViewById(R.id.center_group));
+ }
+
+ private void updateAlternativeOrder(View v) {
+ if (v instanceof ReverseLinearLayout) {
+ ((ReverseLinearLayout) v).setAlternativeOrder(mAlternativeOrder);
+ }
+ }
+
+ private void initiallyFill(ButtonDispatcher buttonDispatcher) {
+ addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.ends_group));
+ addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.center_group));
+ addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.ends_group));
+ addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.center_group));
+ }
+
+ private void addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent) {
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ // Need to manually search for each id, just in case each group has more than one
+ // of a single id. It probably mostly a waste of time, but shouldn't take long
+ // and will only happen once.
+ if (parent.getChildAt(i).getId() == buttonDispatcher.getId()) {
+ buttonDispatcher.addView(parent.getChildAt(i));
+ } else if (parent.getChildAt(i) instanceof ViewGroup) {
+ addAll(buttonDispatcher, (ViewGroup) parent.getChildAt(i));
+ }
+ }
+ }
+
+ protected void inflateLayout(String newLayout) {
+ mCurrentLayout = newLayout;
+ if (newLayout == null) {
+ newLayout = getDefaultLayout();
+ }
+ String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
+ String[] start = sets[0].split(BUTTON_SEPARATOR);
+ String[] center = sets[1].split(BUTTON_SEPARATOR);
+ String[] end = sets[2].split(BUTTON_SEPARATOR);
+ // Inflate these in start to end order or accessibility traversal will be messed up.
+ inflateButtons(start, mRot0.findViewById(R.id.ends_group), isRot0Landscape, true);
+ inflateButtons(start, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, true);
+
+ inflateButtons(center, mRot0.findViewById(R.id.center_group), isRot0Landscape, false);
+ inflateButtons(center, mRot90.findViewById(R.id.center_group), !isRot0Landscape, false);
+
+ addGravitySpacer(mRot0.findViewById(R.id.ends_group));
+ addGravitySpacer(mRot90.findViewById(R.id.ends_group));
+
+ inflateButtons(end, mRot0.findViewById(R.id.ends_group), isRot0Landscape, false);
+ inflateButtons(end, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, false);
+ }
+
+ private void addGravitySpacer(LinearLayout layout) {
+ layout.addView(new Space(mContext), new LinearLayout.LayoutParams(0, 0, 1));
+ }
+
+ private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape,
+ boolean start) {
+ for (int i = 0; i < buttons.length; i++) {
+ inflateButton(buttons[i], parent, landscape, start);
+ }
+ }
+
+ private ViewGroup.LayoutParams copy(ViewGroup.LayoutParams layoutParams) {
+ if (layoutParams instanceof LinearLayout.LayoutParams) {
+ return new LinearLayout.LayoutParams(layoutParams.width, layoutParams.height,
+ ((LinearLayout.LayoutParams) layoutParams).weight);
+ }
+ return new LayoutParams(layoutParams.width, layoutParams.height);
+ }
+
+ @Nullable
+ protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
+ boolean start) {
+ LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
+ View v = createView(buttonSpec, parent, inflater);
+ if (v == null) return null;
+
+ v = applySize(v, buttonSpec, landscape, start);
+ parent.addView(v);
+ addToDispatchers(v);
+ View lastView = landscape ? mLastLandscape : mLastPortrait;
+ View accessibilityView = v;
+ if (v instanceof ReverseFrameLayout) {
+ accessibilityView = ((ReverseFrameLayout) v).getChildAt(0);
+ }
+ if (lastView != null) {
+ accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
+ }
+ if (landscape) {
+ mLastLandscape = accessibilityView;
+ } else {
+ mLastPortrait = accessibilityView;
+ }
+ return v;
+ }
+
+ private View applySize(View v, String buttonSpec, boolean landscape, boolean start) {
+ String sizeStr = extractSize(buttonSpec);
+ if (sizeStr == null) return v;
+
+ if (sizeStr.contains(WEIGHT_SUFFIX)) {
+ float weight = Float.parseFloat(sizeStr.substring(0, sizeStr.indexOf(WEIGHT_SUFFIX)));
+ FrameLayout frame = new ReverseFrameLayout(mContext);
+ LayoutParams childParams = new LayoutParams(v.getLayoutParams());
+ if (sizeStr.endsWith(WEIGHT_CENTERED_SUFFIX)) {
+ childParams.gravity = Gravity.CENTER;
+ } else {
+ childParams.gravity = landscape ? (start ? Gravity.BOTTOM : Gravity.TOP)
+ : (start ? Gravity.START : Gravity.END);
+ }
+ frame.addView(v, childParams);
+ frame.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, weight));
+ frame.setClipChildren(false);
+ frame.setClipToPadding(false);
+ return frame;
+ }
+ float size = Float.parseFloat(sizeStr);
+ ViewGroup.LayoutParams params = v.getLayoutParams();
+ params.width = (int) (params.width * size);
+ return v;
+ }
+
+ private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {
+ View v = null;
+ String button = extractButton(buttonSpec);
+ if (LEFT.equals(button)) {
+ String s = Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT, NAVSPACE);
+ button = extractButton(s);
+ } else if (RIGHT.equals(button)) {
+ String s = Dependency.get(TunerService.class).getValue(NAV_BAR_RIGHT, MENU_IME);
+ button = extractButton(s);
+ }
+ // Let plugins go first so they can override a standard view if they want.
+ for (NavBarButtonProvider provider : mPlugins) {
+ v = provider.createView(buttonSpec, parent);
+ if (v != null) return v;
+ }
+ if (HOME.equals(button)) {
+ v = inflater.inflate(R.layout.home, parent, false);
+ } else if (BACK.equals(button)) {
+ v = inflater.inflate(R.layout.back, parent, false);
+ } else if (RECENT.equals(button)) {
+ v = inflater.inflate(R.layout.recent_apps, parent, false);
+ } else if (MENU_IME.equals(button)) {
+ v = inflater.inflate(R.layout.menu_ime, parent, false);
+ } else if (NAVSPACE.equals(button)) {
+ v = inflater.inflate(R.layout.nav_key_space, parent, false);
+ } else if (CLIPBOARD.equals(button)) {
+ v = inflater.inflate(R.layout.clipboard, parent, false);
+ } else if (button.startsWith(KEY)) {
+ String uri = extractImage(button);
+ int code = extractKeycode(button);
+ v = inflater.inflate(R.layout.custom_key, parent, false);
+ ((KeyButtonView) v).setCode(code);
+ if (uri != null) {
+ if (uri.contains(":")) {
+ ((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri));
+ } else if (uri.contains("/")) {
+ int index = uri.indexOf('/');
+ String pkg = uri.substring(0, index);
+ int id = Integer.parseInt(uri.substring(index + 1));
+ ((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id));
+ }
+ }
+ }
+ return v;
+ }
+
+ public static String extractImage(String buttonSpec) {
+ if (!buttonSpec.contains(KEY_IMAGE_DELIM)) {
+ return null;
+ }
+ final int start = buttonSpec.indexOf(KEY_IMAGE_DELIM);
+ String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_CODE_END));
+ return subStr;
+ }
+
+ public static int extractKeycode(String buttonSpec) {
+ if (!buttonSpec.contains(KEY_CODE_START)) {
+ return 1;
+ }
+ final int start = buttonSpec.indexOf(KEY_CODE_START);
+ String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_IMAGE_DELIM));
+ return Integer.parseInt(subStr);
+ }
+
+ public static String extractSize(String buttonSpec) {
+ if (!buttonSpec.contains(SIZE_MOD_START)) {
+ return null;
+ }
+ final int sizeStart = buttonSpec.indexOf(SIZE_MOD_START);
+ return buttonSpec.substring(sizeStart + 1, buttonSpec.indexOf(SIZE_MOD_END));
+ }
+
+ public static String extractButton(String buttonSpec) {
+ if (!buttonSpec.contains(SIZE_MOD_START)) {
+ return buttonSpec;
+ }
+ return buttonSpec.substring(0, buttonSpec.indexOf(SIZE_MOD_START));
+ }
+
+ private void addToDispatchers(View v) {
+ if (mButtonDispatchers != null) {
+ final int indexOfKey = mButtonDispatchers.indexOfKey(v.getId());
+ if (indexOfKey >= 0) {
+ mButtonDispatchers.valueAt(indexOfKey).addView(v);
+ } else if (v instanceof ViewGroup) {
+ final ViewGroup viewGroup = (ViewGroup)v;
+ final int N = viewGroup.getChildCount();
+ for (int i = 0; i < N; i++) {
+ addToDispatchers(viewGroup.getChildAt(i));
+ }
+ }
+ }
+ }
+
+
+
+ private void clearViews() {
+ if (mButtonDispatchers != null) {
+ for (int i = 0; i < mButtonDispatchers.size(); i++) {
+ mButtonDispatchers.valueAt(i).clear();
+ }
+ }
+ clearAllChildren(mRot0.findViewById(R.id.nav_buttons));
+ clearAllChildren(mRot90.findViewById(R.id.nav_buttons));
+ }
+
+ private void clearAllChildren(ViewGroup group) {
+ for (int i = 0; i < group.getChildCount(); i++) {
+ ((ViewGroup) group.getChildAt(i)).removeAllViews();
+ }
+ }
+
+ @Override
+ public void onPluginConnected(NavBarButtonProvider plugin, Context context) {
+ mPlugins.add(plugin);
+ clearViews();
+ inflateLayout(mCurrentLayout);
+ }
+
+ @Override
+ public void onPluginDisconnected(NavBarButtonProvider plugin) {
+ mPlugins.remove(plugin);
+ clearViews();
+ inflateLayout(mCurrentLayout);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarTransitions.java b/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
new file mode 100644
index 0000000..f3ca66f
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
@@ -0,0 +1,130 @@
+/*
+ * 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.Context;
+import android.os.ServiceManager;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.systemui.R;
+
+public final class NavigationBarTransitions extends BarTransitions {
+
+ private final NavigationBarView mView;
+ private final IStatusBarService mBarService;
+ private final LightBarTransitionsController mLightTransitionsController;
+
+ private boolean mLightsOut;
+ private boolean mAutoDim;
+
+ public NavigationBarTransitions(NavigationBarView view) {
+ super(view, R.drawable.nav_background);
+ mView = view;
+ mBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ mLightTransitionsController = new LightBarTransitionsController(view.getContext(),
+ this::applyDarkIntensity);
+ }
+
+ public void init() {
+ applyModeBackground(-1, getMode(), false /*animate*/);
+ applyLightsOut(false /*animate*/, true /*force*/);
+ }
+
+ @Override
+ public void setAutoDim(boolean autoDim) {
+ if (mAutoDim == autoDim) return;
+ mAutoDim = autoDim;
+ applyLightsOut(true, false);
+ }
+
+ @Override
+ protected boolean isLightsOut(int mode) {
+ return super.isLightsOut(mode) || mAutoDim;
+ }
+
+ public LightBarTransitionsController getLightTransitionsController() {
+ return mLightTransitionsController;
+ }
+
+ @Override
+ protected void onTransition(int oldMode, int newMode, boolean animate) {
+ super.onTransition(oldMode, newMode, animate);
+ applyLightsOut(animate, false /*force*/);
+ }
+
+ private void applyLightsOut(boolean animate, boolean force) {
+ // apply to lights out
+ applyLightsOut(isLightsOut(getMode()), animate, force);
+ }
+
+ private void applyLightsOut(boolean lightsOut, boolean animate, boolean force) {
+ if (!force && lightsOut == mLightsOut) return;
+
+ mLightsOut = lightsOut;
+
+ final View navButtons = mView.getCurrentView().findViewById(R.id.nav_buttons);
+
+ // ok, everyone, stop it right there
+ navButtons.animate().cancel();
+
+ final float navButtonsAlpha = lightsOut ? 0.5f : 1f;
+
+ if (!animate) {
+ navButtons.setAlpha(navButtonsAlpha);
+ } else {
+ final int duration = lightsOut ? LIGHTS_OUT_DURATION : LIGHTS_IN_DURATION;
+ navButtons.animate()
+ .alpha(navButtonsAlpha)
+ .setDuration(duration)
+ .start();
+ }
+ }
+
+ public void reapplyDarkIntensity() {
+ applyDarkIntensity(mLightTransitionsController.getCurrentDarkIntensity());
+ }
+
+ public void applyDarkIntensity(float darkIntensity) {
+ SparseArray<ButtonDispatcher> buttonDispatchers = mView.getButtonDispatchers();
+ for (int i = buttonDispatchers.size() - 1; i >= 0; i--) {
+ buttonDispatchers.valueAt(i).setDarkIntensity(darkIntensity);
+ }
+ }
+
+ private final View.OnTouchListener mLightsOutListener = new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ // even though setting the systemUI visibility below will turn these views
+ // on, we need them to come up faster so that they can catch this motion
+ // event
+ applyLightsOut(false, false, false);
+
+ try {
+ mBarService.setSystemUiVisibility(0, View.SYSTEM_UI_FLAG_LOW_PROFILE,
+ "LightsOutListener");
+ } catch (android.os.RemoteException ex) {
+ }
+ }
+ return false;
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarView.java b/com/android/systemui/statusbar/phone/NavigationBarView.java
new file mode 100644
index 0000000..9a7039a
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -0,0 +1,838 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import android.animation.LayoutTransition;
+import android.animation.LayoutTransition.TransitionListener;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.DrawableRes;
+import android.app.ActivityManager;
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+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 com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
+import com.android.systemui.DockedStackExistsListener;
+import com.android.systemui.R;
+import com.android.systemui.RecentsComponent;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.plugins.statusbar.phone.NavGesture;
+import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
+import com.android.systemui.stackdivider.Divider;
+import com.android.systemui.statusbar.policy.DeadZone;
+import com.android.systemui.statusbar.policy.KeyButtonDrawable;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+public class NavigationBarView extends FrameLayout implements PluginListener<NavGesture> {
+ final static boolean DEBUG = false;
+ final static String TAG = "StatusBar/NavBarView";
+
+ // slippery nav bar when everything is disabled, e.g. during setup
+ final static boolean SLIPPERY_WHEN_DISABLED = true;
+
+ final static boolean ALTERNATE_CAR_MODE_UI = false;
+
+ final Display mDisplay;
+ View mCurrentView = null;
+ View[] mRotatedViews = new View[4];
+
+ boolean mVertical;
+ private int mCurrentRotation = -1;
+
+ boolean mShowMenu;
+ boolean mShowAccessibilityButton;
+ boolean mLongClickableAccessibilityButton;
+ int mDisabledFlags = 0;
+ int mNavigationIconHints = 0;
+
+ private KeyButtonDrawable mBackIcon, mBackLandIcon, mBackAltIcon, mBackAltLandIcon;
+ private KeyButtonDrawable mBackCarModeIcon, mBackLandCarModeIcon;
+ private KeyButtonDrawable mBackAltCarModeIcon, mBackAltLandCarModeIcon;
+ private KeyButtonDrawable mHomeDefaultIcon, mHomeCarModeIcon;
+ private KeyButtonDrawable mRecentIcon;
+ private KeyButtonDrawable mDockedIcon;
+ private KeyButtonDrawable mImeIcon;
+ private KeyButtonDrawable mMenuIcon;
+ private KeyButtonDrawable mAccessibilityIcon;
+
+ private GestureHelper mGestureHelper;
+ private DeadZone mDeadZone;
+ private final NavigationBarTransitions mBarTransitions;
+
+ // workaround for LayoutTransitions leaving the nav buttons in a weird state (bug 5549288)
+ final static boolean WORKAROUND_INVALID_LAYOUT = true;
+ final static int MSG_CHECK_INVALID_LAYOUT = 8686;
+
+ // performs manual animation in sync with layout transitions
+ private final NavTransitionListener mTransitionListener = new NavTransitionListener();
+
+ private OnVerticalChangedListener mOnVerticalChangedListener;
+ private boolean mLayoutTransitionsEnabled = true;
+ private boolean mWakeAndUnlocking;
+ private boolean mUseCarModeUi = false;
+ private boolean mInCarMode = false;
+ private boolean mDockedStackExists;
+
+ private final SparseArray<ButtonDispatcher> mButtonDispatchers = new SparseArray<>();
+ private Configuration mConfiguration;
+
+ private NavigationBarInflaterView mNavigationInflaterView;
+ private RecentsComponent mRecentsComponent;
+ private Divider mDivider;
+
+ private class NavTransitionListener implements TransitionListener {
+ private boolean mBackTransitioning;
+ private boolean mHomeAppearing;
+ private long mStartDelay;
+ private long mDuration;
+ private TimeInterpolator mInterpolator;
+
+ @Override
+ public void startTransition(LayoutTransition transition, ViewGroup container,
+ View view, int transitionType) {
+ if (view.getId() == R.id.back) {
+ mBackTransitioning = true;
+ } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) {
+ mHomeAppearing = true;
+ mStartDelay = transition.getStartDelay(transitionType);
+ mDuration = transition.getDuration(transitionType);
+ mInterpolator = transition.getInterpolator(transitionType);
+ }
+ }
+
+ @Override
+ public void endTransition(LayoutTransition transition, ViewGroup container,
+ View view, int transitionType) {
+ if (view.getId() == R.id.back) {
+ mBackTransitioning = false;
+ } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) {
+ mHomeAppearing = false;
+ }
+ }
+
+ public void onBackAltCleared() {
+ ButtonDispatcher backButton = getBackButton();
+
+ // When dismissing ime during unlock, force the back button to run the same appearance
+ // animation as home (if we catch this condition early enough).
+ if (!mBackTransitioning && backButton.getVisibility() == VISIBLE
+ && mHomeAppearing && getHomeButton().getAlpha() == 0) {
+ getBackButton().setAlpha(0);
+ ValueAnimator a = ObjectAnimator.ofFloat(backButton, "alpha", 0, 1);
+ a.setStartDelay(mStartDelay);
+ a.setDuration(mDuration);
+ a.setInterpolator(mInterpolator);
+ a.start();
+ }
+ }
+ }
+
+ private final OnClickListener mImeSwitcherClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mContext.getSystemService(InputMethodManager.class)
+ .showInputMethodPicker(true /* showAuxiliarySubtypes */);
+ }
+ };
+
+ private class H extends Handler {
+ public void handleMessage(Message m) {
+ switch (m.what) {
+ case MSG_CHECK_INVALID_LAYOUT:
+ final String how = "" + m.obj;
+ final int w = getWidth();
+ final int h = getHeight();
+ final int vw = getCurrentView().getWidth();
+ final int vh = getCurrentView().getHeight();
+
+ if (h != vh || w != vw) {
+ Log.w(TAG, String.format(
+ "*** Invalid layout in navigation bar (%s this=%dx%d cur=%dx%d)",
+ how, w, h, vw, vh));
+ if (WORKAROUND_INVALID_LAYOUT) {
+ requestLayout();
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ public NavigationBarView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mDisplay = ((WindowManager) context.getSystemService(
+ Context.WINDOW_SERVICE)).getDefaultDisplay();
+
+ mVertical = false;
+ mShowMenu = false;
+
+ mShowAccessibilityButton = false;
+ mLongClickableAccessibilityButton = false;
+
+ mConfiguration = new Configuration();
+ mConfiguration.updateFrom(context.getResources().getConfiguration());
+ updateIcons(context, Configuration.EMPTY, mConfiguration);
+
+ mBarTransitions = new NavigationBarTransitions(this);
+
+ mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back));
+ mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home));
+ mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps));
+ mButtonDispatchers.put(R.id.menu, new ButtonDispatcher(R.id.menu));
+ mButtonDispatchers.put(R.id.ime_switcher, new ButtonDispatcher(R.id.ime_switcher));
+ mButtonDispatchers.put(R.id.accessibility_button,
+ new ButtonDispatcher(R.id.accessibility_button));
+ }
+
+ public BarTransitions getBarTransitions() {
+ return mBarTransitions;
+ }
+
+ public LightBarTransitionsController getLightTransitionsController() {
+ return mBarTransitions.getLightTransitionsController();
+ }
+
+ public void setComponents(RecentsComponent recentsComponent, Divider divider) {
+ mRecentsComponent = recentsComponent;
+ mDivider = divider;
+ if (mGestureHelper instanceof NavigationBarGestureHelper) {
+ ((NavigationBarGestureHelper) mGestureHelper).setComponents(
+ recentsComponent, divider, this);
+ }
+ }
+
+ public void setOnVerticalChangedListener(OnVerticalChangedListener onVerticalChangedListener) {
+ mOnVerticalChangedListener = onVerticalChangedListener;
+ notifyVerticalChangedListener(mVertical);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mGestureHelper.onTouchEvent(event)) {
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ return mGestureHelper.onInterceptTouchEvent(event);
+ }
+
+ public void abortCurrentGesture() {
+ getHomeButton().abortCurrentGesture();
+ }
+
+ private H mHandler = new H();
+
+ public View getCurrentView() {
+ return mCurrentView;
+ }
+
+ public View[] getAllViews() {
+ return mRotatedViews;
+ }
+
+ public ButtonDispatcher getRecentsButton() {
+ return mButtonDispatchers.get(R.id.recent_apps);
+ }
+
+ public ButtonDispatcher getMenuButton() {
+ return mButtonDispatchers.get(R.id.menu);
+ }
+
+ public ButtonDispatcher getBackButton() {
+ return mButtonDispatchers.get(R.id.back);
+ }
+
+ public ButtonDispatcher getHomeButton() {
+ return mButtonDispatchers.get(R.id.home);
+ }
+
+ public ButtonDispatcher getImeSwitchButton() {
+ return mButtonDispatchers.get(R.id.ime_switcher);
+ }
+
+ public ButtonDispatcher getAccessibilityButton() {
+ return mButtonDispatchers.get(R.id.accessibility_button);
+ }
+
+ public SparseArray<ButtonDispatcher> getButtonDispatchers() {
+ return mButtonDispatchers;
+ }
+
+ private void updateCarModeIcons(Context ctx) {
+ mBackCarModeIcon = getDrawable(ctx,
+ R.drawable.ic_sysbar_back_carmode, R.drawable.ic_sysbar_back_carmode);
+ mBackLandCarModeIcon = mBackCarModeIcon;
+ mBackAltCarModeIcon = getDrawable(ctx,
+ R.drawable.ic_sysbar_back_ime_carmode, R.drawable.ic_sysbar_back_ime_carmode);
+ mBackAltLandCarModeIcon = mBackAltCarModeIcon;
+ mHomeCarModeIcon = getDrawable(ctx,
+ R.drawable.ic_sysbar_home_carmode, R.drawable.ic_sysbar_home_carmode);
+ }
+
+ private void updateIcons(Context ctx, Configuration oldConfig, Configuration newConfig) {
+ if (oldConfig.orientation != newConfig.orientation
+ || oldConfig.densityDpi != newConfig.densityDpi) {
+ mDockedIcon = getDrawable(ctx,
+ R.drawable.ic_sysbar_docked, R.drawable.ic_sysbar_docked_dark);
+ }
+ if (oldConfig.densityDpi != newConfig.densityDpi
+ || oldConfig.getLayoutDirection() != newConfig.getLayoutDirection()) {
+ mBackIcon = getDrawable(ctx, R.drawable.ic_sysbar_back, R.drawable.ic_sysbar_back_dark);
+ mBackLandIcon = mBackIcon;
+ mBackAltIcon = getDrawable(ctx,
+ R.drawable.ic_sysbar_back_ime, R.drawable.ic_sysbar_back_ime_dark);
+ mBackAltLandIcon = mBackAltIcon;
+
+ mHomeDefaultIcon = getDrawable(ctx,
+ R.drawable.ic_sysbar_home, R.drawable.ic_sysbar_home_dark);
+ mRecentIcon = getDrawable(ctx,
+ R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_dark);
+ mMenuIcon = getDrawable(ctx, R.drawable.ic_sysbar_menu, R.drawable.ic_sysbar_menu_dark);
+ mAccessibilityIcon = getDrawable(ctx, R.drawable.ic_sysbar_accessibility_button,
+ R.drawable.ic_sysbar_accessibility_button_dark);
+
+ int dualToneDarkTheme = Utils.getThemeAttr(ctx, R.attr.darkIconTheme);
+ int dualToneLightTheme = Utils.getThemeAttr(ctx, R.attr.lightIconTheme);
+ Context darkContext = new ContextThemeWrapper(ctx, dualToneDarkTheme);
+ Context lightContext = new ContextThemeWrapper(ctx, dualToneLightTheme);
+ mImeIcon = getDrawable(darkContext, lightContext,
+ R.drawable.ic_ime_switcher_default, R.drawable.ic_ime_switcher_default);
+
+ if (ALTERNATE_CAR_MODE_UI) {
+ updateCarModeIcons(ctx);
+ }
+ }
+ }
+
+ private KeyButtonDrawable getDrawable(Context ctx, @DrawableRes int lightIcon,
+ @DrawableRes int darkIcon) {
+ return getDrawable(ctx, ctx, lightIcon, darkIcon);
+ }
+
+ private KeyButtonDrawable getDrawable(Context darkContext, Context lightContext,
+ @DrawableRes int lightIcon, @DrawableRes int darkIcon) {
+ return KeyButtonDrawable.create(lightContext.getDrawable(lightIcon),
+ darkContext.getDrawable(darkIcon));
+ }
+
+ @Override
+ public void setLayoutDirection(int layoutDirection) {
+ // Reload all the icons
+ updateIcons(getContext(), Configuration.EMPTY, mConfiguration);
+
+ super.setLayoutDirection(layoutDirection);
+ }
+
+ public void notifyScreenOn() {
+ setDisabledFlags(mDisabledFlags, true);
+ }
+
+ public void setNavigationIconHints(int hints) {
+ setNavigationIconHints(hints, false);
+ }
+
+ private KeyButtonDrawable getBackIconWithAlt(boolean carMode, boolean landscape) {
+ return landscape
+ ? carMode ? mBackAltLandCarModeIcon : mBackAltLandIcon
+ : carMode ? mBackAltCarModeIcon : mBackAltIcon;
+ }
+
+ private KeyButtonDrawable getBackIcon(boolean carMode, boolean landscape) {
+ return landscape
+ ? carMode ? mBackLandCarModeIcon : mBackLandIcon
+ : carMode ? mBackCarModeIcon : mBackIcon;
+ }
+
+ public void setNavigationIconHints(int hints, boolean force) {
+ if (!force && hints == mNavigationIconHints) return;
+ final boolean backAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
+ if ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0 && !backAlt) {
+ mTransitionListener.onBackAltCleared();
+ }
+ if (DEBUG) {
+ android.widget.Toast.makeText(getContext(),
+ "Navigation icon hints = " + hints,
+ 500).show();
+ }
+
+ mNavigationIconHints = hints;
+
+ // We have to replace or restore the back and home button icons when exiting or entering
+ // carmode, respectively. Recents are not available in CarMode in nav bar so change
+ // to recent icon is not required.
+ KeyButtonDrawable backIcon = (backAlt)
+ ? getBackIconWithAlt(mUseCarModeUi, mVertical)
+ : getBackIcon(mUseCarModeUi, mVertical);
+
+ getBackButton().setImageDrawable(backIcon);
+
+ updateRecentsIcon();
+
+ if (mUseCarModeUi) {
+ getHomeButton().setImageDrawable(mHomeCarModeIcon);
+ } else {
+ getHomeButton().setImageDrawable(mHomeDefaultIcon);
+ }
+
+ // The Accessibility button always overrides the appearance of the IME switcher
+ final boolean showImeButton =
+ !mShowAccessibilityButton && ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN)
+ != 0);
+ getImeSwitchButton().setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE);
+ getImeSwitchButton().setImageDrawable(mImeIcon);
+
+ // Update menu button in case the IME state has changed.
+ setMenuVisibility(mShowMenu, true);
+ getMenuButton().setImageDrawable(mMenuIcon);
+
+ setAccessibilityButtonState(mShowAccessibilityButton, mLongClickableAccessibilityButton);
+ getAccessibilityButton().setImageDrawable(mAccessibilityIcon);
+
+ setDisabledFlags(mDisabledFlags, true);
+
+ mBarTransitions.reapplyDarkIntensity();
+ }
+
+ public void setDisabledFlags(int disabledFlags) {
+ setDisabledFlags(disabledFlags, false);
+ }
+
+ public void setDisabledFlags(int disabledFlags, boolean force) {
+ if (!force && mDisabledFlags == disabledFlags) return;
+
+ mDisabledFlags = disabledFlags;
+
+ final boolean disableHome = ((disabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
+
+ // Always disable recents when alternate car mode UI is active.
+ boolean disableRecent = mUseCarModeUi
+ || ((disabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0);
+ final boolean disableBack = ((disabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)
+ && ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) == 0);
+
+ ViewGroup navButtons = (ViewGroup) getCurrentView().findViewById(R.id.nav_buttons);
+ if (navButtons != null) {
+ LayoutTransition lt = navButtons.getLayoutTransition();
+ if (lt != null) {
+ if (!lt.getTransitionListeners().contains(mTransitionListener)) {
+ lt.addTransitionListener(mTransitionListener);
+ }
+ }
+ }
+ 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);
+ getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE);
+ }
+
+ private boolean inLockTask() {
+ try {
+ return ActivityManager.getService().isInLockTaskMode();
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ public void setLayoutTransitionsEnabled(boolean enabled) {
+ mLayoutTransitionsEnabled = enabled;
+ updateLayoutTransitionsEnabled();
+ }
+
+ public void setWakeAndUnlocking(boolean wakeAndUnlocking) {
+ setUseFadingAnimations(wakeAndUnlocking);
+ mWakeAndUnlocking = wakeAndUnlocking;
+ updateLayoutTransitionsEnabled();
+ }
+
+ private void updateLayoutTransitionsEnabled() {
+ boolean enabled = !mWakeAndUnlocking && mLayoutTransitionsEnabled;
+ ViewGroup navButtons = (ViewGroup) getCurrentView().findViewById(R.id.nav_buttons);
+ LayoutTransition lt = navButtons.getLayoutTransition();
+ if (lt != null) {
+ if (enabled) {
+ lt.enableTransitionType(LayoutTransition.APPEARING);
+ lt.enableTransitionType(LayoutTransition.DISAPPEARING);
+ lt.enableTransitionType(LayoutTransition.CHANGE_APPEARING);
+ lt.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+ } else {
+ lt.disableTransitionType(LayoutTransition.APPEARING);
+ lt.disableTransitionType(LayoutTransition.DISAPPEARING);
+ lt.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
+ lt.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+ }
+ }
+ }
+
+ private void setUseFadingAnimations(boolean useFadingAnimations) {
+ WindowManager.LayoutParams lp = (WindowManager.LayoutParams) ((ViewGroup) getParent())
+ .getLayoutParams();
+ if (lp != null) {
+ boolean old = lp.windowAnimations != 0;
+ if (!old && useFadingAnimations) {
+ lp.windowAnimations = R.style.Animation_NavigationBarFadeIn;
+ } else if (old && !useFadingAnimations) {
+ lp.windowAnimations = 0;
+ } else {
+ return;
+ }
+ WindowManager wm = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
+ wm.updateViewLayout((View) getParent(), lp);
+ }
+ }
+
+ public void setMenuVisibility(final boolean show) {
+ setMenuVisibility(show, false);
+ }
+
+ public void setMenuVisibility(final boolean show, final boolean force) {
+ if (!force && mShowMenu == show) return;
+
+ mShowMenu = show;
+
+ // Only show Menu if IME switcher and Accessibility button not shown.
+ final boolean shouldShow = mShowMenu && !mShowAccessibilityButton &&
+ ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) == 0);
+
+ getMenuButton().setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ public void setAccessibilityButtonState(final boolean visible, final boolean longClickable) {
+ mShowAccessibilityButton = visible;
+ mLongClickableAccessibilityButton = longClickable;
+ if (visible) {
+ // Accessibility button overrides Menu and IME switcher buttons.
+ setMenuVisibility(false, true);
+ getImeSwitchButton().setVisibility(View.INVISIBLE);
+ }
+
+ getAccessibilityButton().setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ getAccessibilityButton().setLongClickable(longClickable);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ mNavigationInflaterView = (NavigationBarInflaterView) findViewById(
+ R.id.navigation_inflater);
+ mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
+
+ getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
+
+ DockedStackExistsListener.register(mDockedListener);
+ updateRotatedViews();
+ }
+
+ private void updateRotatedViews() {
+ mRotatedViews[Surface.ROTATION_0] =
+ mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0);
+ mRotatedViews[Surface.ROTATION_270] =
+ mRotatedViews[Surface.ROTATION_90] = findViewById(R.id.rot90);
+
+ updateCurrentView();
+ }
+
+ public boolean needsReorient(int rotation) {
+ return mCurrentRotation != rotation;
+ }
+
+ private void updateCurrentView() {
+ final int rot = mDisplay.getRotation();
+ for (int i=0; i<4; i++) {
+ mRotatedViews[i].setVisibility(View.GONE);
+ }
+ mCurrentView = mRotatedViews[rot];
+ mCurrentView.setVisibility(View.VISIBLE);
+ mNavigationInflaterView.setAlternativeOrder(rot == Surface.ROTATION_90);
+ for (int i = 0; i < mButtonDispatchers.size(); i++) {
+ mButtonDispatchers.valueAt(i).setCurrentView(mCurrentView);
+ }
+ updateLayoutTransitionsEnabled();
+ mCurrentRotation = rot;
+ }
+
+ private void updateRecentsIcon() {
+ getRecentsButton().setImageDrawable(mDockedStackExists ? mDockedIcon : mRecentIcon);
+ mBarTransitions.reapplyDarkIntensity();
+ }
+
+ public boolean isVertical() {
+ return mVertical;
+ }
+
+ public void reorient() {
+ updateCurrentView();
+
+ mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone);
+
+ ((NavigationBarFrame) getRootView()).setDeadZone(mDeadZone);
+ mDeadZone.setDisplayRotation(mCurrentRotation);
+
+ // force the low profile & disabled states into compliance
+ mBarTransitions.init();
+ setDisabledFlags(mDisabledFlags, true /* force */);
+ setMenuVisibility(mShowMenu, true /* force */);
+
+ if (DEBUG) {
+ Log.d(TAG, "reorient(): rot=" + mCurrentRotation);
+ }
+
+ updateTaskSwitchHelper();
+ setNavigationIconHints(mNavigationIconHints, true);
+
+ getHomeButton().setVertical(mVertical);
+ }
+
+ private void updateTaskSwitchHelper() {
+ if (mGestureHelper == null) return;
+ boolean isRtl = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
+ mGestureHelper.setBarState(mVertical, isRtl);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if (DEBUG) Log.d(TAG, String.format(
+ "onSizeChanged: (%dx%d) old: (%dx%d)", w, h, oldw, oldh));
+
+ final boolean newVertical = w > 0 && h > w;
+ if (newVertical != mVertical) {
+ mVertical = newVertical;
+ //Log.v(TAG, String.format("onSizeChanged: h=%d, w=%d, vert=%s", h, w, mVertical?"y":"n"));
+ reorient();
+ notifyVerticalChangedListener(newVertical);
+ }
+
+ postCheckForInvalidLayout("sizeChanged");
+ super.onSizeChanged(w, h, oldw, oldh);
+ }
+
+ private void notifyVerticalChangedListener(boolean newVertical) {
+ if (mOnVerticalChangedListener != null) {
+ mOnVerticalChangedListener.onVerticalChanged(newVertical);
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ boolean uiCarModeChanged = updateCarMode(newConfig);
+ updateTaskSwitchHelper();
+ updateIcons(getContext(), mConfiguration, newConfig);
+ updateRecentsIcon();
+ if (uiCarModeChanged || mConfiguration.densityDpi != newConfig.densityDpi
+ || mConfiguration.getLayoutDirection() != newConfig.getLayoutDirection()) {
+ // If car mode or density changes, we need to reset the icons.
+ setNavigationIconHints(mNavigationIconHints, true);
+ }
+ mConfiguration.updateFrom(newConfig);
+ }
+
+ /**
+ * If the configuration changed, update the carmode and return that it was updated.
+ */
+ private boolean updateCarMode(Configuration newConfig) {
+ boolean uiCarModeChanged = false;
+ if (newConfig != null) {
+ int uiMode = newConfig.uiMode & Configuration.UI_MODE_TYPE_MASK;
+ final boolean isCarMode = (uiMode == Configuration.UI_MODE_TYPE_CAR);
+
+ if (isCarMode != mInCarMode) {
+ mInCarMode = isCarMode;
+ if (ALTERNATE_CAR_MODE_UI) {
+ mUseCarModeUi = isCarMode;
+ uiCarModeChanged = true;
+ } else {
+ // Don't use car mode behavior if ALTERNATE_CAR_MODE_UI not set.
+ mUseCarModeUi = false;
+ }
+ }
+ }
+ return uiCarModeChanged;
+ }
+
+ /*
+ @Override
+ protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
+ if (DEBUG) Log.d(TAG, String.format(
+ "onLayout: %s (%d,%d,%d,%d)",
+ changed?"changed":"notchanged", left, top, right, bottom));
+ super.onLayout(changed, left, top, right, bottom);
+ }
+
+ // uncomment this for extra defensiveness in WORKAROUND_INVALID_LAYOUT situations: if all else
+ // fails, any touch on the display will fix the layout.
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (DEBUG) Log.d(TAG, "onInterceptTouchEvent: " + ev.toString());
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ postCheckForInvalidLayout("touch");
+ }
+ return super.onInterceptTouchEvent(ev);
+ }
+ */
+
+
+ private String getResourceName(int resId) {
+ if (resId != 0) {
+ final android.content.res.Resources res = getContext().getResources();
+ try {
+ return res.getResourceName(resId);
+ } catch (android.content.res.Resources.NotFoundException ex) {
+ return "(unknown)";
+ }
+ } else {
+ return "(null)";
+ }
+ }
+
+ private void postCheckForInvalidLayout(final String how) {
+ mHandler.obtainMessage(MSG_CHECK_INVALID_LAYOUT, 0, 0, how).sendToTarget();
+ }
+
+ private static String visibilityToString(int vis) {
+ switch (vis) {
+ case View.INVISIBLE:
+ return "INVISIBLE";
+ case View.GONE:
+ return "GONE";
+ default:
+ return "VISIBLE";
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ reorient();
+ onPluginDisconnected(null); // Create default gesture helper
+ Dependency.get(PluginManager.class).addPluginListener(this,
+ NavGesture.class, false /* Only one */);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ Dependency.get(PluginManager.class).removePluginListener(this);
+ if (mGestureHelper != null) {
+ mGestureHelper.destroy();
+ }
+ }
+
+ @Override
+ public void onPluginConnected(NavGesture plugin, Context context) {
+ mGestureHelper = plugin.getGestureHelper();
+ updateTaskSwitchHelper();
+ }
+
+ @Override
+ public void onPluginDisconnected(NavGesture plugin) {
+ NavigationBarGestureHelper defaultHelper = new NavigationBarGestureHelper(getContext());
+ defaultHelper.setComponents(mRecentsComponent, mDivider, this);
+ if (mGestureHelper != null) {
+ mGestureHelper.destroy();
+ }
+ mGestureHelper = defaultHelper;
+ updateTaskSwitchHelper();
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("NavigationBarView {");
+ final Rect r = new Rect();
+ final Point size = new Point();
+ mDisplay.getRealSize(size);
+
+ pw.println(String.format(" this: " + StatusBar.viewInfo(this)
+ + " " + visibilityToString(getVisibility())));
+
+ getWindowVisibleDisplayFrame(r);
+ final boolean offscreen = r.right > size.x || r.bottom > size.y;
+ pw.println(" window: "
+ + r.toShortString()
+ + " " + visibilityToString(getWindowVisibility())
+ + (offscreen ? " OFFSCREEN!" : ""));
+
+ pw.println(String.format(" mCurrentView: id=%s (%dx%d) %s",
+ getResourceName(getCurrentView().getId()),
+ getCurrentView().getWidth(), getCurrentView().getHeight(),
+ visibilityToString(getCurrentView().getVisibility())));
+
+ pw.println(String.format(" disabled=0x%08x vertical=%s menu=%s",
+ mDisabledFlags,
+ mVertical ? "true" : "false",
+ mShowMenu ? "true" : "false"));
+
+ dumpButton(pw, "back", getBackButton());
+ dumpButton(pw, "home", getHomeButton());
+ dumpButton(pw, "rcnt", getRecentsButton());
+ dumpButton(pw, "menu", getMenuButton());
+ dumpButton(pw, "a11y", getAccessibilityButton());
+
+ pw.println(" }");
+ }
+
+ private static void dumpButton(PrintWriter pw, String caption, ButtonDispatcher button) {
+ pw.print(" " + caption + ": ");
+ if (button == null) {
+ pw.print("null");
+ } else {
+ pw.print(visibilityToString(button.getVisibility())
+ + " alpha=" + button.getAlpha()
+ );
+ }
+ pw.println();
+ }
+
+ public interface OnVerticalChangedListener {
+ void onVerticalChanged(boolean isVertical);
+ }
+
+ private final Consumer<Boolean> mDockedListener = exists -> mHandler.post(() -> {
+ mDockedStackExists = exists;
+ updateRecentsIcon();
+ });
+}
diff --git a/com/android/systemui/statusbar/phone/NearestTouchFrame.java b/com/android/systemui/statusbar/phone/NearestTouchFrame.java
new file mode 100644
index 0000000..1452e0c
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NearestTouchFrame.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2017 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.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.support.annotation.VisibleForTesting;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ * Redirects touches that aren't handled by any child view to the nearest
+ * clickable child. Only takes effect on <sw600dp.
+ */
+public class NearestTouchFrame extends FrameLayout {
+
+ private final ArrayList<View> mClickableChildren = new ArrayList<>();
+ private final boolean mIsActive;
+ private final int[] mTmpInt = new int[2];
+ private final int[] mOffset = new int[2];
+ private View mTouchingChild;
+
+ public NearestTouchFrame(Context context, AttributeSet attrs) {
+ this(context, attrs, context.getResources().getConfiguration());
+ }
+
+ @VisibleForTesting
+ NearestTouchFrame(Context context, AttributeSet attrs, Configuration c) {
+ super(context, attrs);
+ mIsActive = c.smallestScreenWidthDp < 600;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ mClickableChildren.clear();
+ addClickableChildren(this);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ getLocationInWindow(mOffset);
+ }
+
+ private void addClickableChildren(ViewGroup group) {
+ final int N = group.getChildCount();
+ for (int i = 0; i < N; i++) {
+ View child = group.getChildAt(i);
+ if (child.isClickable()) {
+ mClickableChildren.add(child);
+ } else if (child instanceof ViewGroup) {
+ addClickableChildren((ViewGroup) child);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mIsActive) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ mTouchingChild = findNearestChild(event);
+ }
+ if (mTouchingChild != null) {
+ event.offsetLocation(mTouchingChild.getWidth() / 2 - event.getX(),
+ mTouchingChild.getHeight() / 2 - event.getY());
+ return mTouchingChild.getVisibility() == VISIBLE
+ && mTouchingChild.dispatchTouchEvent(event);
+ }
+ }
+ return super.onTouchEvent(event);
+ }
+
+ private View findNearestChild(MotionEvent event) {
+ return mClickableChildren.stream().map(v -> new Pair<>(distance(v, event), v))
+ .min(Comparator.comparingInt(f -> f.first)).get().second;
+ }
+
+ private int distance(View v, MotionEvent event) {
+ v.getLocationInWindow(mTmpInt);
+ int left = mTmpInt[0] - mOffset[0];
+ int top = mTmpInt[1] - mOffset[1];
+ int right = left + v.getWidth();
+ int bottom = top + v.getHeight();
+
+ int x = Math.min(Math.abs(left - (int) event.getX()),
+ Math.abs((int) event.getX() - right));
+ int y = Math.min(Math.abs(top - (int) event.getY()),
+ Math.abs((int) event.getY() - bottom));
+
+ return Math.max(x, y);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NoisyVelocityTracker.java b/com/android/systemui/statusbar/phone/NoisyVelocityTracker.java
new file mode 100644
index 0000000..214dda2
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NoisyVelocityTracker.java
@@ -0,0 +1,135 @@
+/*
+ * 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.util.Log;
+import android.util.Pools;
+import android.view.MotionEvent;
+
+import java.util.ArrayDeque;
+import java.util.Iterator;
+
+/**
+ * A very simple low-pass velocity filter for motion events for noisy touch screens.
+ */
+public class NoisyVelocityTracker implements VelocityTrackerInterface {
+
+ private static final Pools.SynchronizedPool<NoisyVelocityTracker> sNoisyPool =
+ new Pools.SynchronizedPool<>(2);
+
+ private static final float DECAY = 0.75f;
+ private static final boolean DEBUG = false;
+
+ private final int MAX_EVENTS = 8;
+ private ArrayDeque<MotionEventCopy> mEventBuf = new ArrayDeque<MotionEventCopy>(MAX_EVENTS);
+ private float mVX, mVY = 0;
+
+ private static class MotionEventCopy {
+ public MotionEventCopy(float x2, float y2, long eventTime) {
+ this.x = x2;
+ this.y = y2;
+ this.t = eventTime;
+ }
+ float x, y;
+ long t;
+ }
+
+ public static NoisyVelocityTracker obtain() {
+ NoisyVelocityTracker instance = sNoisyPool.acquire();
+ return (instance != null) ? instance : new NoisyVelocityTracker();
+ }
+
+ private NoisyVelocityTracker() {
+ }
+
+ public void addMovement(MotionEvent event) {
+ if (mEventBuf.size() == MAX_EVENTS) {
+ mEventBuf.remove();
+ }
+ mEventBuf.add(new MotionEventCopy(event.getX(), event.getY(), event.getEventTime()));
+ }
+
+ public void computeCurrentVelocity(int units) {
+ if (NoisyVelocityTracker.DEBUG) {
+ Log.v("FlingTracker", "computing velocities for " + mEventBuf.size() + " events");
+ }
+ mVX = mVY = 0;
+ MotionEventCopy last = null;
+ int i = 0;
+ float totalweight = 0f;
+ float weight = 10f;
+ for (final Iterator<MotionEventCopy> iter = mEventBuf.iterator();
+ iter.hasNext();) {
+ final MotionEventCopy event = iter.next();
+ if (last != null) {
+ final float dt = (float) (event.t - last.t) / units;
+ final float dx = (event.x - last.x);
+ final float dy = (event.y - last.y);
+ if (NoisyVelocityTracker.DEBUG) {
+ Log.v("FlingTracker", String.format(
+ " [%d] (t=%d %.1f,%.1f) dx=%.1f dy=%.1f dt=%f vx=%.1f vy=%.1f",
+ i, event.t, event.x, event.y,
+ dx, dy, dt,
+ (dx/dt),
+ (dy/dt)
+ ));
+ }
+ if (event.t == last.t) {
+ // Really not sure what to do with events that happened at the same time,
+ // so we'll skip subsequent events.
+ continue;
+ }
+ mVX += weight * dx / dt;
+ mVY += weight * dy / dt;
+ totalweight += weight;
+ weight *= DECAY;
+ }
+ last = event;
+ i++;
+ }
+ if (totalweight > 0) {
+ mVX /= totalweight;
+ mVY /= totalweight;
+ } else {
+ // so as not to contaminate the velocities with NaN
+ mVX = mVY = 0;
+ }
+
+ if (NoisyVelocityTracker.DEBUG) {
+ Log.v("FlingTracker", "computed: vx=" + mVX + " vy=" + mVY);
+ }
+ }
+
+ public float getXVelocity() {
+ if (Float.isNaN(mVX) || Float.isInfinite(mVX)) {
+ mVX = 0;
+ }
+ return mVX;
+ }
+
+ public float getYVelocity() {
+ if (Float.isNaN(mVY) || Float.isInfinite(mVX)) {
+ mVY = 0;
+ }
+ return mVY;
+ }
+
+ public void recycle() {
+ mEventBuf.clear();
+ sNoisyPool.release(this);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/com/android/systemui/statusbar/phone/NotificationGroupManager.java
new file mode 100644
index 0000000..b75c7e0
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2015 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.service.notification.StatusBarNotification;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A class to handle notifications and their corresponding groups.
+ */
+public class NotificationGroupManager implements OnHeadsUpChangedListener {
+
+ private static final String TAG = "NotificationGroupManager";
+ private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
+ private OnGroupChangeListener mListener;
+ private int mBarState = -1;
+ private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>();
+ private HeadsUpManager mHeadsUpManager;
+ private boolean mIsUpdatingUnchangedGroup;
+
+ public void setOnGroupChangeListener(OnGroupChangeListener listener) {
+ mListener = listener;
+ }
+
+ public boolean isGroupExpanded(StatusBarNotification sbn) {
+ NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+ if (group == null) {
+ return false;
+ }
+ return group.expanded;
+ }
+
+ public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) {
+ NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+ if (group == null) {
+ return;
+ }
+ setGroupExpanded(group, expanded);
+ }
+
+ private void setGroupExpanded(NotificationGroup group, boolean expanded) {
+ group.expanded = expanded;
+ if (group.summary != null) {
+ mListener.onGroupExpansionChanged(group.summary.row, expanded);
+ }
+ }
+
+ public void onEntryRemoved(NotificationData.Entry removed) {
+ onEntryRemovedInternal(removed, removed.notification);
+ mIsolatedEntries.remove(removed.key);
+ }
+
+ /**
+ * An entry was removed.
+ *
+ * @param removed the removed entry
+ * @param sbn the notification the entry has, which doesn't need to be the same as it's internal
+ * notification
+ */
+ private void onEntryRemovedInternal(NotificationData.Entry removed,
+ final StatusBarNotification sbn) {
+ String groupKey = getGroupKey(sbn);
+ final NotificationGroup group = mGroupMap.get(groupKey);
+ if (group == null) {
+ // When an app posts 2 different notifications as summary of the same group, then a
+ // cancellation of the first notification removes this group.
+ // This situation is not supported and we will not allow such notifications anymore in
+ // the close future. See b/23676310 for reference.
+ return;
+ }
+ if (isGroupChild(sbn)) {
+ group.children.remove(removed.key);
+ } else {
+ group.summary = null;
+ }
+ updateSuppression(group);
+ if (group.children.isEmpty()) {
+ if (group.summary == null) {
+ mGroupMap.remove(groupKey);
+ }
+ }
+ }
+
+ public void onEntryAdded(final NotificationData.Entry added) {
+ if (added.row.isRemoved()) {
+ added.setDebugThrowable(new Throwable());
+ }
+ final StatusBarNotification sbn = added.notification;
+ boolean isGroupChild = isGroupChild(sbn);
+ String groupKey = getGroupKey(sbn);
+ NotificationGroup group = mGroupMap.get(groupKey);
+ if (group == null) {
+ group = new NotificationGroup();
+ mGroupMap.put(groupKey, group);
+ }
+ if (isGroupChild) {
+ NotificationData.Entry existing = group.children.get(added.key);
+ if (existing != null && existing != added) {
+ Throwable existingThrowable = existing.getDebugThrowable();
+ Log.wtf(TAG, "Inconsistent entries found with the same key " + added.key
+ + "existing removed: " + existing.row.isRemoved()
+ + (existingThrowable != null
+ ? Log.getStackTraceString(existingThrowable) + "\n": "")
+ + " added removed" + added.row.isRemoved()
+ , new Throwable());
+ }
+ group.children.put(added.key, added);
+ updateSuppression(group);
+ } else {
+ group.summary = added;
+ group.expanded = added.row.areChildrenExpanded();
+ updateSuppression(group);
+ if (!group.children.isEmpty()) {
+ ArrayList<NotificationData.Entry> childrenCopy
+ = new ArrayList<>(group.children.values());
+ for (NotificationData.Entry child : childrenCopy) {
+ onEntryBecomingChild(child);
+ }
+ mListener.onGroupCreatedFromChildren(group);
+ }
+ }
+ }
+
+ private void onEntryBecomingChild(NotificationData.Entry entry) {
+ if (entry.row.isHeadsUp()) {
+ onHeadsUpStateChanged(entry, true);
+ }
+ }
+
+ private void updateSuppression(NotificationGroup group) {
+ if (group == null) {
+ return;
+ }
+ boolean prevSuppressed = group.suppressed;
+ group.suppressed = group.summary != null && !group.expanded
+ && (group.children.size() == 1
+ || (group.children.size() == 0
+ && group.summary.notification.getNotification().isGroupSummary()
+ && hasIsolatedChildren(group)));
+ if (prevSuppressed != group.suppressed) {
+ if (group.suppressed) {
+ handleSuppressedSummaryHeadsUpped(group.summary);
+ }
+ if (!mIsUpdatingUnchangedGroup) {
+ mListener.onGroupsChanged();
+ }
+ }
+ }
+
+ private boolean hasIsolatedChildren(NotificationGroup group) {
+ return getNumberOfIsolatedChildren(group.summary.notification.getGroupKey()) != 0;
+ }
+
+ private int getNumberOfIsolatedChildren(String groupKey) {
+ int count = 0;
+ for (StatusBarNotification sbn : mIsolatedEntries.values()) {
+ if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private NotificationData.Entry getIsolatedChild(String groupKey) {
+ for (StatusBarNotification sbn : mIsolatedEntries.values()) {
+ if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
+ return mGroupMap.get(sbn.getKey()).summary;
+ }
+ }
+ return null;
+ }
+
+ public void onEntryUpdated(NotificationData.Entry entry,
+ StatusBarNotification oldNotification) {
+ String oldKey = oldNotification.getGroupKey();
+ String newKey = entry.notification.getGroupKey();
+ boolean groupKeysChanged = !oldKey.equals(newKey);
+ boolean wasGroupChild = isGroupChild(oldNotification);
+ boolean isGroupChild = isGroupChild(entry.notification);
+ mIsUpdatingUnchangedGroup = !groupKeysChanged && wasGroupChild == isGroupChild;
+ if (mGroupMap.get(getGroupKey(oldNotification)) != null) {
+ onEntryRemovedInternal(entry, oldNotification);
+ }
+ onEntryAdded(entry);
+ mIsUpdatingUnchangedGroup = false;
+ if (isIsolated(entry.notification)) {
+ mIsolatedEntries.put(entry.key, entry.notification);
+ if (groupKeysChanged) {
+ updateSuppression(mGroupMap.get(oldKey));
+ updateSuppression(mGroupMap.get(newKey));
+ }
+ } else if (!wasGroupChild && isGroupChild) {
+ onEntryBecomingChild(entry);
+ }
+ }
+
+ public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) {
+ return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary();
+ }
+
+ private boolean isOnlyChild(StatusBarNotification sbn) {
+ return !sbn.getNotification().isGroupSummary()
+ && getTotalNumberOfChildren(sbn) == 1;
+ }
+
+ public boolean isOnlyChildInGroup(StatusBarNotification sbn) {
+ if (!isOnlyChild(sbn)) {
+ return false;
+ }
+ ExpandableNotificationRow logicalGroupSummary = getLogicalGroupSummary(sbn);
+ return logicalGroupSummary != null
+ && !logicalGroupSummary.getStatusBarNotification().equals(sbn);
+ }
+
+ private int getTotalNumberOfChildren(StatusBarNotification sbn) {
+ int isolatedChildren = getNumberOfIsolatedChildren(sbn.getGroupKey());
+ NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+ int realChildren = group != null ? group.children.size() : 0;
+ return isolatedChildren + realChildren;
+ }
+
+ private boolean isGroupSuppressed(String groupKey) {
+ NotificationGroup group = mGroupMap.get(groupKey);
+ return group != null && group.suppressed;
+ }
+
+ public void setStatusBarState(int newState) {
+ if (mBarState == newState) {
+ return;
+ }
+ mBarState = newState;
+ if (mBarState == StatusBarState.KEYGUARD) {
+ collapseAllGroups();
+ }
+ }
+
+ public void collapseAllGroups() {
+ // Because notifications can become isolated when the group becomes suppressed it can
+ // lead to concurrent modifications while looping. We need to make a copy.
+ ArrayList<NotificationGroup> groupCopy = new ArrayList<>(mGroupMap.values());
+ int size = groupCopy.size();
+ for (int i = 0; i < size; i++) {
+ NotificationGroup group = groupCopy.get(i);
+ if (group.expanded) {
+ setGroupExpanded(group, false);
+ }
+ updateSuppression(group);
+ }
+ }
+
+ /**
+ * @return whether a given notification is a child in a group which has a summary
+ */
+ public boolean isChildInGroupWithSummary(StatusBarNotification sbn) {
+ if (!isGroupChild(sbn)) {
+ return false;
+ }
+ NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+ if (group == null || group.summary == null || group.suppressed) {
+ return false;
+ }
+ if (group.children.isEmpty()) {
+ // If the suppression of a group changes because the last child was removed, this can
+ // still be called temporarily because the child hasn't been fully removed yet. Let's
+ // make sure we still return false in that case.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @return whether a given notification is a summary in a group which has children
+ */
+ public boolean isSummaryOfGroup(StatusBarNotification sbn) {
+ if (!isGroupSummary(sbn)) {
+ return false;
+ }
+ NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+ if (group == null) {
+ return false;
+ }
+ return !group.children.isEmpty();
+ }
+
+ /**
+ * Get the summary of a specified status bar notification. For isolated notification this return
+ * itself.
+ */
+ public ExpandableNotificationRow getGroupSummary(StatusBarNotification sbn) {
+ return getGroupSummary(getGroupKey(sbn));
+ }
+
+ /**
+ * Similar to {@link #getGroupSummary(StatusBarNotification)} but doesn't get the visual summary
+ * but the logical summary, i.e when a child is isolated, it still returns the summary as if
+ * it wasn't isolated.
+ */
+ public ExpandableNotificationRow getLogicalGroupSummary(
+ StatusBarNotification sbn) {
+ return getGroupSummary(sbn.getGroupKey());
+ }
+
+ @Nullable
+ private ExpandableNotificationRow getGroupSummary(String groupKey) {
+ NotificationGroup group = mGroupMap.get(groupKey);
+ return group == null ? null
+ : group.summary == null ? null
+ : group.summary.row;
+ }
+
+ /** @return group expansion state after toggling. */
+ public boolean toggleGroupExpansion(StatusBarNotification sbn) {
+ NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+ if (group == null) {
+ return false;
+ }
+ setGroupExpanded(group, !group.expanded);
+ return group.expanded;
+ }
+
+ private boolean isIsolated(StatusBarNotification sbn) {
+ return mIsolatedEntries.containsKey(sbn.getKey());
+ }
+
+ private boolean isGroupSummary(StatusBarNotification sbn) {
+ if (isIsolated(sbn)) {
+ return true;
+ }
+ return sbn.getNotification().isGroupSummary();
+ }
+
+ private boolean isGroupChild(StatusBarNotification sbn) {
+ if (isIsolated(sbn)) {
+ return false;
+ }
+ return sbn.isGroup() && !sbn.getNotification().isGroupSummary();
+ }
+
+ private String getGroupKey(StatusBarNotification sbn) {
+ if (isIsolated(sbn)) {
+ return sbn.getKey();
+ }
+ return sbn.getGroupKey();
+ }
+
+ @Override
+ public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+ }
+
+ @Override
+ public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
+ }
+
+ @Override
+ public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
+ }
+
+ @Override
+ public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+ final StatusBarNotification sbn = entry.notification;
+ if (entry.row.isHeadsUp()) {
+ if (shouldIsolate(sbn)) {
+ // We will be isolated now, so lets update the groups
+ onEntryRemovedInternal(entry, entry.notification);
+
+ mIsolatedEntries.put(sbn.getKey(), sbn);
+
+ onEntryAdded(entry);
+ // We also need to update the suppression of the old group, because this call comes
+ // even before the groupManager knows about the notification at all.
+ // When the notification gets added afterwards it is already isolated and therefore
+ // it doesn't lead to an update.
+ updateSuppression(mGroupMap.get(entry.notification.getGroupKey()));
+ mListener.onGroupsChanged();
+ } else {
+ handleSuppressedSummaryHeadsUpped(entry);
+ }
+ } else {
+ if (mIsolatedEntries.containsKey(sbn.getKey())) {
+ // not isolated anymore, we need to update the groups
+ onEntryRemovedInternal(entry, entry.notification);
+ mIsolatedEntries.remove(sbn.getKey());
+ onEntryAdded(entry);
+ mListener.onGroupsChanged();
+ }
+ }
+ }
+
+ private void handleSuppressedSummaryHeadsUpped(NotificationData.Entry entry) {
+ StatusBarNotification sbn = entry.notification;
+ if (!isGroupSuppressed(sbn.getGroupKey())
+ || !sbn.getNotification().isGroupSummary()
+ || !entry.row.isHeadsUp()) {
+ return;
+ }
+ // The parent of a suppressed group got huned, lets hun the child!
+ NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
+ if (notificationGroup != null) {
+ Iterator<NotificationData.Entry> iterator
+ = notificationGroup.children.values().iterator();
+ NotificationData.Entry child = iterator.hasNext() ? iterator.next() : null;
+ if (child == null) {
+ child = getIsolatedChild(sbn.getGroupKey());
+ }
+ if (child != null) {
+ if (child.row.keepInParent() || child.row.isRemoved() || child.row.isDismissed()) {
+ // the notification is actually already removed, no need to do heads-up on it.
+ return;
+ }
+ if (mHeadsUpManager.isHeadsUp(child.key)) {
+ mHeadsUpManager.updateNotification(child, true);
+ } else {
+ mHeadsUpManager.showNotification(child);
+ }
+ }
+ }
+ mHeadsUpManager.releaseImmediately(entry.key);
+ }
+
+ private boolean shouldIsolate(StatusBarNotification sbn) {
+ NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
+ return (sbn.isGroup() && !sbn.getNotification().isGroupSummary())
+ && (sbn.getNotification().fullScreenIntent != null
+ || notificationGroup == null
+ || !notificationGroup.expanded
+ || isGroupNotFullyVisible(notificationGroup));
+ }
+
+ private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) {
+ return notificationGroup.summary == null
+ || notificationGroup.summary.row.getClipTopAmount() > 0
+ || notificationGroup.summary.row.getTranslationY() < 0;
+ }
+
+ public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ mHeadsUpManager = headsUpManager;
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("GroupManager state:");
+ pw.println(" number of groups: " + mGroupMap.size());
+ for (Map.Entry<String, NotificationGroup> entry : mGroupMap.entrySet()) {
+ pw.println("\n key: " + entry.getKey()); pw.println(entry.getValue());
+ }
+ pw.println("\n isolated entries: " + mIsolatedEntries.size());
+ for (Map.Entry<String, StatusBarNotification> entry : mIsolatedEntries.entrySet()) {
+ pw.print(" "); pw.print(entry.getKey());
+ pw.print(", "); pw.println(entry.getValue());
+ }
+ }
+
+ public static class NotificationGroup {
+ public final HashMap<String, NotificationData.Entry> children = new HashMap<>();
+ public NotificationData.Entry summary;
+ public boolean expanded;
+ /**
+ * Is this notification group suppressed, i.e its summary is hidden
+ */
+ public boolean suppressed;
+
+ @Override
+ public String toString() {
+ String result = " summary:\n "
+ + (summary != null ? summary.notification : "null")
+ + (summary != null && summary.getDebugThrowable() != null
+ ? Log.getStackTraceString(summary.getDebugThrowable())
+ : "");
+ result += "\n children size: " + children.size();
+ for (NotificationData.Entry child : children.values()) {
+ result += "\n " + child.notification
+ + (child.getDebugThrowable() != null
+ ? Log.getStackTraceString(child.getDebugThrowable())
+ : "");
+ }
+ return result;
+ }
+ }
+
+ public interface OnGroupChangeListener {
+ /**
+ * The expansion of a group has changed.
+ *
+ * @param changedRow the row for which the expansion has changed, which is also the summary
+ * @param expanded a boolean indicating the new expanded state
+ */
+ void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded);
+
+ /**
+ * A group of children just received a summary notification and should therefore become
+ * children of it.
+ *
+ * @param group the group created
+ */
+ void onGroupCreatedFromChildren(NotificationGroup group);
+
+ /**
+ * The groups have changed. This can happen if the isolation of a child has changes or if a
+ * group became suppressed / unsuppressed
+ */
+ void onGroupsChanged();
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
new file mode 100644
index 0000000..41a69b4
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -0,0 +1,288 @@
+package com.android.systemui.statusbar.phone;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Icon;
+import android.support.annotation.NonNull;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+import java.util.ArrayList;
+import java.util.function.Function;
+
+/**
+ * A controller for the space in the status bar to the left of the system icons. This area is
+ * normally reserved for notifications.
+ */
+public class NotificationIconAreaController implements DarkReceiver {
+ private final NotificationColorUtil mNotificationColorUtil;
+
+ private int mIconSize;
+ private int mIconHPadding;
+ private int mIconTint = Color.WHITE;
+
+ private StatusBar mStatusBar;
+ protected View mNotificationIconArea;
+ private NotificationIconContainer mNotificationIcons;
+ private NotificationIconContainer mShelfIcons;
+ private final Rect mTintArea = new Rect();
+ private NotificationStackScrollLayout mNotificationScrollLayout;
+ private Context mContext;
+
+ public NotificationIconAreaController(Context context, StatusBar statusBar) {
+ mStatusBar = statusBar;
+ mNotificationColorUtil = NotificationColorUtil.getInstance(context);
+ mContext = context;
+
+ initializeNotificationAreaViews(context);
+ }
+
+ protected View inflateIconArea(LayoutInflater inflater) {
+ return inflater.inflate(R.layout.notification_icon_area, null);
+ }
+
+ /**
+ * Initializes the views that will represent the notification area.
+ */
+ protected void initializeNotificationAreaViews(Context context) {
+ reloadDimens(context);
+
+ LayoutInflater layoutInflater = LayoutInflater.from(context);
+ mNotificationIconArea = inflateIconArea(layoutInflater);
+ mNotificationIcons = (NotificationIconContainer) mNotificationIconArea.findViewById(
+ R.id.notificationIcons);
+
+ mNotificationScrollLayout = mStatusBar.getNotificationScrollLayout();
+ }
+
+ public void setupShelf(NotificationShelf shelf) {
+ mShelfIcons = shelf.getShelfIcons();
+ shelf.setCollapsedIcons(mNotificationIcons);
+ }
+
+ public void onDensityOrFontScaleChanged(Context context) {
+ reloadDimens(context);
+ final FrameLayout.LayoutParams params = generateIconLayoutParams();
+ for (int i = 0; i < mNotificationIcons.getChildCount(); i++) {
+ View child = mNotificationIcons.getChildAt(i);
+ child.setLayoutParams(params);
+ }
+ for (int i = 0; i < mShelfIcons.getChildCount(); i++) {
+ View child = mShelfIcons.getChildAt(i);
+ child.setLayoutParams(params);
+ }
+ }
+
+ @NonNull
+ private FrameLayout.LayoutParams generateIconLayoutParams() {
+ return new FrameLayout.LayoutParams(
+ mIconSize + 2 * mIconHPadding, getHeight());
+ }
+
+ private void reloadDimens(Context context) {
+ Resources res = context.getResources();
+ mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
+ mIconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_padding);
+ }
+
+ /**
+ * Returns the view that represents the notification area.
+ */
+ public View getNotificationInnerAreaView() {
+ return mNotificationIconArea;
+ }
+
+ /**
+ * See {@link com.android.systemui.statusbar.policy.DarkIconDispatcher#setIconsDarkArea}.
+ * Sets the color that should be used to tint any icons in the notification area.
+ *
+ * @param tintArea the area in which to tint the icons, specified in screen coordinates
+ * @param darkIntensity
+ */
+ public void onDarkChanged(Rect tintArea, float darkIntensity, int iconTint) {
+ if (tintArea == null) {
+ mTintArea.setEmpty();
+ } else {
+ mTintArea.set(tintArea);
+ }
+ mIconTint = iconTint;
+ applyNotificationIconsTint();
+ }
+
+ protected int getHeight() {
+ return mStatusBar.getStatusBarHeight();
+ }
+
+ protected boolean shouldShowNotificationIcon(NotificationData.Entry entry,
+ NotificationData notificationData, boolean showAmbient) {
+ if (notificationData.isAmbient(entry.key) && !showAmbient) {
+ return false;
+ }
+ if (!StatusBar.isTopLevelChild(entry)) {
+ return false;
+ }
+ if (entry.row.getVisibility() == View.GONE) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Updates the notifications with the given list of notifications to display.
+ */
+ public void updateNotificationIcons(NotificationData notificationData) {
+
+ updateIconsForLayout(notificationData, entry -> entry.icon, mNotificationIcons,
+ false /* showAmbient */);
+ updateIconsForLayout(notificationData, entry -> entry.expandedIcon, mShelfIcons,
+ NotificationShelf.SHOW_AMBIENT_ICONS);
+
+ applyNotificationIconsTint();
+ }
+
+ /**
+ * Updates the notification icons for a host layout. This will ensure that the notification
+ * host layout will have the same icons like the ones in here.
+ *
+ * @param notificationData the notification data to look up which notifications are relevant
+ * @param function A function to look up an icon view based on an entry
+ * @param hostLayout which layout should be updated
+ * @param showAmbient should ambient notification icons be shown
+ */
+ private void updateIconsForLayout(NotificationData notificationData,
+ Function<NotificationData.Entry, StatusBarIconView> function,
+ NotificationIconContainer hostLayout, boolean showAmbient) {
+ ArrayList<StatusBarIconView> toShow = new ArrayList<>(
+ mNotificationScrollLayout.getChildCount());
+
+ // Filter out ambient notifications and notification children.
+ for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) {
+ View view = mNotificationScrollLayout.getChildAt(i);
+ if (view instanceof ExpandableNotificationRow) {
+ NotificationData.Entry ent = ((ExpandableNotificationRow) view).getEntry();
+ if (shouldShowNotificationIcon(ent, notificationData, showAmbient)) {
+ toShow.add(function.apply(ent));
+ }
+ }
+ }
+
+ // In case we are changing the suppression of a group, the replacement shouldn't flicker
+ // and it should just be replaced instead. We therefore look for notifications that were
+ // just replaced by the child or vice-versa to suppress this.
+
+ ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons = new ArrayMap<>();
+ ArrayList<View> toRemove = new ArrayList<>();
+ for (int i = 0; i < hostLayout.getChildCount(); i++) {
+ View child = hostLayout.getChildAt(i);
+ if (!(child instanceof StatusBarIconView)) {
+ continue;
+ }
+ if (!toShow.contains(child)) {
+ boolean iconWasReplaced = false;
+ StatusBarIconView removedIcon = (StatusBarIconView) child;
+ String removedGroupKey = removedIcon.getNotification().getGroupKey();
+ for (int j = 0; j < toShow.size(); j++) {
+ StatusBarIconView candidate = toShow.get(j);
+ if (candidate.getSourceIcon().sameAs((removedIcon.getSourceIcon()))
+ && candidate.getNotification().getGroupKey().equals(removedGroupKey)) {
+ if (!iconWasReplaced) {
+ iconWasReplaced = true;
+ } else {
+ iconWasReplaced = false;
+ break;
+ }
+ }
+ }
+ if (iconWasReplaced) {
+ ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(removedGroupKey);
+ if (statusBarIcons == null) {
+ statusBarIcons = new ArrayList<>();
+ replacingIcons.put(removedGroupKey, statusBarIcons);
+ }
+ statusBarIcons.add(removedIcon.getStatusBarIcon());
+ }
+ toRemove.add(removedIcon);
+ }
+ }
+ // removing all duplicates
+ ArrayList<String> duplicates = new ArrayList<>();
+ for (String key : replacingIcons.keySet()) {
+ ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(key);
+ if (statusBarIcons.size() != 1) {
+ duplicates.add(key);
+ }
+ }
+ replacingIcons.removeAll(duplicates);
+ hostLayout.setReplacingIcons(replacingIcons);
+
+ final int toRemoveCount = toRemove.size();
+ for (int i = 0; i < toRemoveCount; i++) {
+ hostLayout.removeView(toRemove.get(i));
+ }
+
+ final FrameLayout.LayoutParams params = generateIconLayoutParams();
+ for (int i = 0; i < toShow.size(); i++) {
+ View v = toShow.get(i);
+ // The view might still be transiently added if it was just removed and added again
+ hostLayout.removeTransientView(v);
+ if (v.getParent() == null) {
+ hostLayout.addView(v, i, params);
+ }
+ }
+
+ hostLayout.setChangingViewPositions(true);
+ // Re-sort notification icons
+ final int childCount = hostLayout.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View actual = hostLayout.getChildAt(i);
+ StatusBarIconView expected = toShow.get(i);
+ if (actual == expected) {
+ continue;
+ }
+ hostLayout.removeView(expected);
+ hostLayout.addView(expected, i);
+ }
+ hostLayout.setChangingViewPositions(false);
+ hostLayout.setReplacingIcons(null);
+ }
+
+ /**
+ * Applies {@link #mIconTint} to the notification icons.
+ */
+ private void applyNotificationIconsTint() {
+ for (int i = 0; i < mNotificationIcons.getChildCount(); i++) {
+ StatusBarIconView v = (StatusBarIconView) mNotificationIcons.getChildAt(i);
+ boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L));
+ int color = StatusBarIconView.NO_COLOR;
+ boolean colorize = !isPreL || NotificationUtils.isGrayscale(v, mNotificationColorUtil);
+ if (colorize) {
+ color = DarkIconDispatcher.getTint(mTintArea, v, mIconTint);
+ }
+ v.setStaticDrawableColor(color);
+ v.setDecorColor(mIconTint);
+ }
+ }
+
+ public void setDark(boolean dark) {
+ mNotificationIcons.setDark(dark, false, 0);
+ mShelfIcons.setDark(dark, false, 0);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
new file mode 100644
index 0000000..88a5626
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -0,0 +1,675 @@
+/*
+ * Copyright (C) 2016 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 static com.android.systemui.statusbar.notification.NotificationUtils.isHapticFeedbackDisabled;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Icon;
+import android.os.AsyncTask;
+import android.os.UserHandle;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.ArraySet;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.stack.AnimationFilter;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+import com.android.systemui.statusbar.stack.ViewState;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * A container for notification icons. It handles overflowing icons properly and positions them
+ * correctly on the screen.
+ */
+public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
+ /**
+ * A float value indicating how much before the overflow start the icons should transform into
+ * a dot. A value of 0 means that they are exactly at the end and a value of 1 means it starts
+ * 1 icon width early.
+ */
+ public static final float OVERFLOW_EARLY_AMOUNT = 0.2f;
+ private static final int NO_VALUE = Integer.MIN_VALUE;
+ private static final String TAG = "NotificationIconContainer";
+ private static final boolean DEBUG = false;
+ private static final int CANNED_ANIMATION_DURATION = 100;
+ private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
+ private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
+
+ @Override
+ public AnimationFilter getAnimationFilter() {
+ return mAnimationFilter;
+ }
+ }.setDuration(200);
+
+ private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
+ private AnimationFilter mAnimationFilter = new AnimationFilter().animateY().animateAlpha()
+ .animateScale();
+
+ @Override
+ public AnimationFilter getAnimationFilter() {
+ return mAnimationFilter;
+ }
+
+ }.setDuration(CANNED_ANIMATION_DURATION)
+ .setCustomInterpolator(View.TRANSLATION_Y, Interpolators.ICON_OVERSHOT);
+
+ private static final AnimationProperties mTempProperties = new AnimationProperties() {
+ private AnimationFilter mAnimationFilter = new AnimationFilter();
+
+ @Override
+ public AnimationFilter getAnimationFilter() {
+ return mAnimationFilter;
+ }
+ };
+
+ private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
+ private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
+
+ @Override
+ public AnimationFilter getAnimationFilter() {
+ return mAnimationFilter;
+ }
+ }.setDuration(200).setDelay(50);
+
+ private static final AnimationProperties UNDARK_PROPERTIES = new AnimationProperties() {
+ private AnimationFilter mAnimationFilter = new AnimationFilter()
+ .animateX();
+
+ @Override
+ public AnimationFilter getAnimationFilter() {
+ return mAnimationFilter;
+ }
+ }.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
+ public static final int MAX_VISIBLE_ICONS_WHEN_DARK = 5;
+
+ private boolean mShowAllIcons = true;
+ private final HashMap<View, IconState> mIconStates = new HashMap<>();
+ private int mDotPadding;
+ private int mStaticDotRadius;
+ private int mActualLayoutWidth = NO_VALUE;
+ private float mActualPaddingEnd = NO_VALUE;
+ private float mActualPaddingStart = NO_VALUE;
+ private boolean mDark;
+ private boolean mChangingViewPositions;
+ private int mAddAnimationStartIndex = -1;
+ private int mCannedAnimationStartIndex = -1;
+ private int mSpeedBumpIndex = -1;
+ private int mIconSize;
+ private float mOpenedAmount = 0.0f;
+ private float mVisualOverflowAdaption;
+ private boolean mDisallowNextAnimation;
+ private boolean mAnimationsEnabled = true;
+ private boolean mVibrateOnAnimation;
+ private Vibrator mVibrator;
+ private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
+ private int mDarkOffsetX;
+
+ public NotificationIconContainer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initDimens();
+ setWillNotDraw(!DEBUG);
+ mVibrator = mContext.getSystemService(Vibrator.class);
+ }
+
+ private void initDimens() {
+ mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
+ mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Paint paint = new Paint();
+ paint.setColor(Color.RED);
+ paint.setStyle(Paint.Style.STROKE);
+ canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ initDimens();
+ }
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ float centerY = getHeight() / 2.0f;
+ // we layout all our children on the left at the top
+ mIconSize = 0;
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ // We need to layout all children even the GONE ones, such that the heights are
+ // calculated correctly as they are used to calculate how many we can fit on the screen
+ int width = child.getMeasuredWidth();
+ int height = child.getMeasuredHeight();
+ int top = (int) (centerY - height / 2.0f);
+ child.layout(0, top, width, top + height);
+ if (i == 0) {
+ mIconSize = child.getWidth();
+ }
+ }
+ if (mShowAllIcons) {
+ resetViewStates();
+ calculateIconTranslations();
+ applyIconStates();
+ }
+ }
+
+ public void applyIconStates() {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ ViewState childState = mIconStates.get(child);
+ if (childState != null) {
+ childState.applyToView(child);
+ }
+ }
+ mAddAnimationStartIndex = -1;
+ mCannedAnimationStartIndex = -1;
+ mDisallowNextAnimation = false;
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ super.onViewAdded(child);
+ boolean isReplacingIcon = isReplacingIcon(child);
+ if (!mChangingViewPositions) {
+ IconState v = new IconState();
+ if (isReplacingIcon) {
+ v.justAdded = false;
+ v.justReplaced = true;
+ }
+ mIconStates.put(child, v);
+ }
+ int childIndex = indexOfChild(child);
+ if (childIndex < getChildCount() - 1 && !isReplacingIcon
+ && mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) {
+ if (mAddAnimationStartIndex < 0) {
+ mAddAnimationStartIndex = childIndex;
+ } else {
+ mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, childIndex);
+ }
+ }
+ if (mDark && child instanceof StatusBarIconView) {
+ ((StatusBarIconView) child).setDark(mDark, false, 0);
+ }
+ }
+
+ private boolean isReplacingIcon(View child) {
+ if (mReplacingIcons == null) {
+ return false;
+ }
+ if (!(child instanceof StatusBarIconView)) {
+ return false;
+ }
+ StatusBarIconView iconView = (StatusBarIconView) child;
+ Icon sourceIcon = iconView.getSourceIcon();
+ String groupKey = iconView.getNotification().getGroupKey();
+ ArrayList<StatusBarIcon> statusBarIcons = mReplacingIcons.get(groupKey);
+ if (statusBarIcons != null) {
+ StatusBarIcon replacedIcon = statusBarIcons.get(0);
+ if (sourceIcon.sameAs(replacedIcon.icon)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onViewRemoved(View child) {
+ super.onViewRemoved(child);
+ if (child instanceof StatusBarIconView) {
+ boolean isReplacingIcon = isReplacingIcon(child);
+ final StatusBarIconView icon = (StatusBarIconView) child;
+ if (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
+ && child.getVisibility() == VISIBLE && isReplacingIcon) {
+ int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX());
+ if (mAddAnimationStartIndex < 0) {
+ mAddAnimationStartIndex = animationStartIndex;
+ } else {
+ mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, animationStartIndex);
+ }
+ }
+ if (!mChangingViewPositions) {
+ mIconStates.remove(child);
+ if (!isReplacingIcon) {
+ addTransientView(icon, 0);
+ icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */,
+ () -> removeTransientView(icon));
+ }
+ }
+ }
+ }
+
+ /**
+ * Finds the first view with a translation bigger then a given value
+ */
+ private int findFirstViewIndexAfter(float translationX) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View view = getChildAt(i);
+ if (view.getTranslationX() > translationX) {
+ return i;
+ }
+ }
+ return getChildCount();
+ }
+
+ public void resetViewStates() {
+ for (int i = 0; i < getChildCount(); i++) {
+ View view = getChildAt(i);
+ ViewState iconState = mIconStates.get(view);
+ iconState.initFrom(view);
+ iconState.alpha = 1.0f;
+ iconState.hidden = false;
+ }
+ }
+
+ /**
+ * Calulate the horizontal translations for each notification based on how much the icons
+ * are inserted into the notification container.
+ * If this is not a whole number, the fraction means by how much the icon is appearing.
+ */
+ public void calculateIconTranslations() {
+ float translationX = getActualPaddingStart();
+ int firstOverflowIndex = -1;
+ int childCount = getChildCount();
+ int maxVisibleIcons = mDark ? MAX_VISIBLE_ICONS_WHEN_DARK : childCount;
+ float layoutEnd = getLayoutEnd();
+ float overflowStart = layoutEnd - mIconSize * (2 + OVERFLOW_EARLY_AMOUNT);
+ boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount();
+ float visualOverflowStart = 0;
+ for (int i = 0; i < childCount; i++) {
+ View view = getChildAt(i);
+ IconState iconState = mIconStates.get(view);
+ iconState.xTranslation = translationX;
+ boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
+ && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
+ boolean noOverflowAfter = i == childCount - 1;
+ float drawingScale = mDark && view instanceof StatusBarIconView
+ ? ((StatusBarIconView) view).getIconScaleFullyDark()
+ : 1f;
+ if (mOpenedAmount != 0.0f) {
+ noOverflowAfter = noOverflowAfter && !hasAmbient && !forceOverflow;
+ }
+ iconState.visibleState = StatusBarIconView.STATE_ICON;
+ if (firstOverflowIndex == -1 && (forceOverflow
+ || (translationX >= (noOverflowAfter ? layoutEnd - mIconSize : overflowStart)))) {
+ firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;
+ int totalDotLength = mStaticDotRadius * 6 + 2 * mDotPadding;
+ visualOverflowStart = overflowStart + mIconSize * (1 + OVERFLOW_EARLY_AMOUNT)
+ - totalDotLength / 2
+ - mIconSize * 0.5f + mStaticDotRadius;
+ if (forceOverflow) {
+ visualOverflowStart = Math.min(translationX, visualOverflowStart
+ + mStaticDotRadius * 2 + mDotPadding);
+ } else {
+ visualOverflowStart += (translationX - overflowStart) / mIconSize
+ * (mStaticDotRadius * 2 + mDotPadding);
+ }
+ if (mShowAllIcons) {
+ // We want to perfectly position the overflow in the static state, such that
+ // it's perfectly centered instead of measuring it from the end.
+ mVisualOverflowAdaption = 0;
+ if (firstOverflowIndex != -1) {
+ View firstOverflowView = getChildAt(i);
+ IconState overflowState = mIconStates.get(firstOverflowView);
+ float totalAmount = layoutEnd - overflowState.xTranslation;
+ float newPosition = overflowState.xTranslation + totalAmount / 2
+ - totalDotLength / 2
+ - mIconSize * 0.5f + mStaticDotRadius;
+ mVisualOverflowAdaption = newPosition - visualOverflowStart;
+ visualOverflowStart = newPosition;
+ }
+ } else {
+ visualOverflowStart += mVisualOverflowAdaption * (1f - mOpenedAmount);
+ }
+ }
+ translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
+ }
+ if (firstOverflowIndex != -1) {
+ int numDots = 1;
+ translationX = visualOverflowStart;
+ for (int i = firstOverflowIndex; i < childCount; i++) {
+ View view = getChildAt(i);
+ IconState iconState = mIconStates.get(view);
+ int dotWidth = mStaticDotRadius * 2 + mDotPadding;
+ iconState.xTranslation = translationX;
+ if (numDots <= 3) {
+ if (numDots == 1 && iconState.iconAppearAmount < 0.8f) {
+ iconState.visibleState = StatusBarIconView.STATE_ICON;
+ numDots--;
+ } else {
+ iconState.visibleState = StatusBarIconView.STATE_DOT;
+ }
+ translationX += (numDots == 3 ? 3 * dotWidth : dotWidth)
+ * iconState.iconAppearAmount;
+ } else {
+ iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
+ }
+ numDots++;
+ }
+ }
+ boolean center = mDark;
+ if (center && translationX < getLayoutEnd()) {
+ float delta = (getLayoutEnd() - translationX) / 2;
+ if (firstOverflowIndex != -1) {
+ // If we have an overflow, only count those half for centering because the dots
+ // don't have a lot of visual weight.
+ float deltaIgnoringOverflow = (getLayoutEnd() - visualOverflowStart) / 2;
+ delta = (deltaIgnoringOverflow + delta) / 2;
+ }
+ for (int i = 0; i < childCount; i++) {
+ View view = getChildAt(i);
+ IconState iconState = mIconStates.get(view);
+ iconState.xTranslation += delta;
+ }
+ }
+
+ if (isLayoutRtl()) {
+ for (int i = 0; i < childCount; i++) {
+ View view = getChildAt(i);
+ IconState iconState = mIconStates.get(view);
+ iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth();
+ }
+ }
+
+ if (mDark && mDarkOffsetX != 0) {
+ for (int i = 0; i < childCount; i++) {
+ View view = getChildAt(i);
+ IconState iconState = mIconStates.get(view);
+ iconState.xTranslation += mDarkOffsetX;
+ }
+ }
+ }
+
+ private float getLayoutEnd() {
+ return getActualWidth() - getActualPaddingEnd();
+ }
+
+ private float getActualPaddingEnd() {
+ if (mActualPaddingEnd == NO_VALUE) {
+ return getPaddingEnd();
+ }
+ return mActualPaddingEnd;
+ }
+
+ private float getActualPaddingStart() {
+ if (mActualPaddingStart == NO_VALUE) {
+ return getPaddingStart();
+ }
+ return mActualPaddingStart;
+ }
+
+ /**
+ * Sets whether the layout should always show all icons.
+ * If this is true, the icon positions will be updated on layout.
+ * If this if false, the layout is managed from the outside and layouting won't trigger a
+ * repositioning of the icons.
+ */
+ public void setShowAllIcons(boolean showAllIcons) {
+ mShowAllIcons = showAllIcons;
+ }
+
+ public void setActualLayoutWidth(int actualLayoutWidth) {
+ mActualLayoutWidth = actualLayoutWidth;
+ if (DEBUG) {
+ invalidate();
+ }
+ }
+
+ public void setActualPaddingEnd(float paddingEnd) {
+ mActualPaddingEnd = paddingEnd;
+ if (DEBUG) {
+ invalidate();
+ }
+ }
+
+ public void setActualPaddingStart(float paddingStart) {
+ mActualPaddingStart = paddingStart;
+ if (DEBUG) {
+ invalidate();
+ }
+ }
+
+ public int getActualWidth() {
+ if (mActualLayoutWidth == NO_VALUE) {
+ return getWidth();
+ }
+ return mActualLayoutWidth;
+ }
+
+ public void setChangingViewPositions(boolean changingViewPositions) {
+ mChangingViewPositions = changingViewPositions;
+ }
+
+ public void setDark(boolean dark, boolean fade, long delay) {
+ mDark = dark;
+ mDisallowNextAnimation |= !fade;
+ for (int i = 0; i < getChildCount(); i++) {
+ View view = getChildAt(i);
+ if (view instanceof StatusBarIconView) {
+ ((StatusBarIconView) view).setDark(dark, fade, delay);
+ if (!dark && fade) {
+ getIconState((StatusBarIconView) view).justUndarkened = true;
+ }
+ }
+ }
+ }
+
+ public IconState getIconState(StatusBarIconView icon) {
+ return mIconStates.get(icon);
+ }
+
+ public void setSpeedBumpIndex(int speedBumpIndex) {
+ mSpeedBumpIndex = speedBumpIndex;
+ }
+
+ public void setOpenedAmount(float expandAmount) {
+ mOpenedAmount = expandAmount;
+ }
+
+ public float getVisualOverflowAdaption() {
+ return mVisualOverflowAdaption;
+ }
+
+ public void setVisualOverflowAdaption(float visualOverflowAdaption) {
+ mVisualOverflowAdaption = visualOverflowAdaption;
+ }
+
+ public boolean hasOverflow() {
+ float width = (getChildCount() + OVERFLOW_EARLY_AMOUNT) * mIconSize;
+ return width - (getWidth() - getActualPaddingStart() - getActualPaddingEnd()) > 0;
+ }
+
+ public void setVibrateOnAnimation(boolean vibrateOnAnimation) {
+ mVibrateOnAnimation = vibrateOnAnimation;
+ }
+
+ public int getIconSize() {
+ return mIconSize;
+ }
+
+ public void setAnimationsEnabled(boolean enabled) {
+ if (!enabled && mAnimationsEnabled) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ ViewState childState = mIconStates.get(child);
+ if (childState != null) {
+ childState.cancelAnimations(child);
+ childState.applyToView(child);
+ }
+ }
+ }
+ mAnimationsEnabled = enabled;
+ }
+
+ public void setDarkOffsetX(int offsetX) {
+ mDarkOffsetX = offsetX;
+ }
+
+ public void setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
+ mReplacingIcons = replacingIcons;
+ }
+
+ public class IconState extends ViewState {
+ public static final int NO_VALUE = NotificationIconContainer.NO_VALUE;
+ public float iconAppearAmount = 1.0f;
+ public float clampedAppearAmount = 1.0f;
+ public int visibleState;
+ public boolean justAdded = true;
+ private boolean justReplaced;
+ public boolean needsCannedAnimation;
+ public boolean useFullTransitionAmount;
+ public boolean useLinearTransitionAmount;
+ public boolean translateContent;
+ public boolean justUndarkened;
+ public int iconColor = StatusBarIconView.NO_COLOR;
+ public boolean noAnimations;
+ public boolean isLastExpandIcon;
+ public int customTransformHeight = NO_VALUE;
+
+ @Override
+ public void applyToView(View view) {
+ if (view instanceof StatusBarIconView) {
+ StatusBarIconView icon = (StatusBarIconView) view;
+ boolean animate = false;
+ AnimationProperties animationProperties = null;
+ boolean animationsAllowed = (mAnimationsEnabled || justUndarkened)
+ && !mDisallowNextAnimation
+ && !noAnimations;
+ if (animationsAllowed) {
+ if (justAdded || justReplaced) {
+ super.applyToView(icon);
+ if (justAdded && iconAppearAmount != 0.0f) {
+ icon.setAlpha(0.0f);
+ icon.setVisibleState(StatusBarIconView.STATE_HIDDEN,
+ false /* animate */);
+ animationProperties = ADD_ICON_PROPERTIES;
+ animate = true;
+ }
+ } else if (justUndarkened) {
+ animationProperties = UNDARK_PROPERTIES;
+ animate = true;
+ } else if (visibleState != icon.getVisibleState()) {
+ animationProperties = DOT_ANIMATION_PROPERTIES;
+ animate = true;
+ }
+ if (!animate && mAddAnimationStartIndex >= 0
+ && indexOfChild(view) >= mAddAnimationStartIndex
+ && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
+ || visibleState != StatusBarIconView.STATE_HIDDEN)) {
+ animationProperties = DOT_ANIMATION_PROPERTIES;
+ animate = true;
+ }
+ if (needsCannedAnimation) {
+ AnimationFilter animationFilter = mTempProperties.getAnimationFilter();
+ animationFilter.reset();
+ animationFilter.combineFilter(
+ ICON_ANIMATION_PROPERTIES.getAnimationFilter());
+ mTempProperties.resetCustomInterpolators();
+ mTempProperties.combineCustomInterpolators(ICON_ANIMATION_PROPERTIES);
+ if (animationProperties != null) {
+ animationFilter.combineFilter(animationProperties.getAnimationFilter());
+ mTempProperties.combineCustomInterpolators(animationProperties);
+ }
+ animationProperties = mTempProperties;
+ animationProperties.setDuration(CANNED_ANIMATION_DURATION);
+ animate = true;
+ mCannedAnimationStartIndex = indexOfChild(view);
+ }
+ if (!animate && mCannedAnimationStartIndex >= 0
+ && indexOfChild(view) > mCannedAnimationStartIndex
+ && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
+ || visibleState != StatusBarIconView.STATE_HIDDEN)) {
+ AnimationFilter animationFilter = mTempProperties.getAnimationFilter();
+ animationFilter.reset();
+ animationFilter.animateX();
+ mTempProperties.resetCustomInterpolators();
+ animationProperties = mTempProperties;
+ animationProperties.setDuration(CANNED_ANIMATION_DURATION);
+ animate = true;
+ }
+ }
+ icon.setVisibleState(visibleState, animationsAllowed);
+ icon.setIconColor(iconColor, needsCannedAnimation && animationsAllowed);
+ if (animate) {
+ animateTo(icon, animationProperties);
+ } else {
+ super.applyToView(view);
+ }
+ boolean wasInShelf = icon.isInShelf();
+ boolean inShelf = iconAppearAmount == 1.0f;
+ icon.setIsInShelf(inShelf);
+ if (shouldVibrateChange(wasInShelf != inShelf)) {
+ AsyncTask.execute(
+ () -> mVibrator.vibrate(VibrationEffect.get(
+ VibrationEffect.EFFECT_TICK)));
+ }
+ }
+ justAdded = false;
+ justReplaced = false;
+ needsCannedAnimation = false;
+ justUndarkened = false;
+ }
+
+ private boolean shouldVibrateChange(boolean inShelfChanged) {
+ if (!mVibrateOnAnimation) {
+ return false;
+ }
+ if (justAdded) {
+ return false;
+ }
+ if (!mAnimationsEnabled) {
+ return false;
+ }
+ if (!inShelfChanged) {
+ return false;
+ }
+ if (isHapticFeedbackDisabled(mContext)) {
+ return false;
+ }
+ return true;
+ }
+
+ public boolean hasCustomTransformHeight() {
+ return isLastExpandIcon && customTransformHeight != NO_VALUE;
+ }
+
+ @Override
+ public void initFrom(View view) {
+ super.initFrom(view);
+ if (view instanceof StatusBarIconView) {
+ iconColor = ((StatusBarIconView) view).getStaticDrawableColor();
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationListenerWithPlugins.java b/com/android/systemui/statusbar/phone/NotificationListenerWithPlugins.java
new file mode 100644
index 0000000..9ff907b
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationListenerWithPlugins.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017 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.os.RemoteException;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.plugins.NotificationListenerController;
+import com.android.systemui.plugins.NotificationListenerController.NotificationProvider;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+
+import java.util.ArrayList;
+
+/**
+ * A version of NotificationListenerService that passes all info to
+ * any plugins connected. Also allows those plugins the chance to cancel
+ * any incoming callbacks or to trigger new ones.
+ */
+public class NotificationListenerWithPlugins extends NotificationListenerService implements
+ PluginListener<NotificationListenerController> {
+
+ private ArrayList<NotificationListenerController> mPlugins = new ArrayList<>();
+ private boolean mConnected;
+
+ @Override
+ public void registerAsSystemService(Context context, ComponentName componentName,
+ int currentUser) throws RemoteException {
+ super.registerAsSystemService(context, componentName, currentUser);
+ Dependency.get(PluginManager.class).addPluginListener(this,
+ NotificationListenerController.class);
+ }
+
+ @Override
+ public void unregisterAsSystemService() throws RemoteException {
+ super.unregisterAsSystemService();
+ Dependency.get(PluginManager.class).removePluginListener(this);
+ }
+
+ @Override
+ public StatusBarNotification[] getActiveNotifications() {
+ StatusBarNotification[] activeNotifications = super.getActiveNotifications();
+ for (NotificationListenerController plugin : mPlugins) {
+ activeNotifications = plugin.getActiveNotifications(activeNotifications);
+ }
+ return activeNotifications;
+ }
+
+ @Override
+ public RankingMap getCurrentRanking() {
+ RankingMap currentRanking = super.getCurrentRanking();
+ for (NotificationListenerController plugin : mPlugins) {
+ currentRanking = plugin.getCurrentRanking(currentRanking);
+ }
+ return currentRanking;
+ }
+
+ public void onPluginConnected() {
+ mConnected = true;
+ mPlugins.forEach(p -> p.onListenerConnected(getProvider()));
+ }
+
+ /**
+ * Called when listener receives a onNotificationPosted.
+ * Returns true to indicate this callback should be skipped.
+ */
+ public boolean onPluginNotificationPosted(StatusBarNotification sbn,
+ final RankingMap rankingMap) {
+ for (NotificationListenerController plugin : mPlugins) {
+ if (plugin.onNotificationPosted(sbn, rankingMap)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called when listener receives a onNotificationRemoved.
+ * Returns true to indicate this callback should be skipped.
+ */
+ public boolean onPluginNotificationRemoved(StatusBarNotification sbn,
+ final RankingMap rankingMap) {
+ for (NotificationListenerController plugin : mPlugins) {
+ if (plugin.onNotificationRemoved(sbn, rankingMap)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public RankingMap onPluginRankingUpdate(RankingMap rankingMap) {
+ return getCurrentRanking();
+ }
+
+ @Override
+ public void onPluginConnected(NotificationListenerController plugin, Context pluginContext) {
+ mPlugins.add(plugin);
+ if (mConnected) {
+ plugin.onListenerConnected(getProvider());
+ }
+ }
+
+ @Override
+ public void onPluginDisconnected(NotificationListenerController plugin) {
+ mPlugins.remove(plugin);
+ }
+
+ private NotificationProvider getProvider() {
+ return new NotificationProvider() {
+ @Override
+ public StatusBarNotification[] getActiveNotifications() {
+ return NotificationListenerWithPlugins.super.getActiveNotifications();
+ }
+
+ @Override
+ public RankingMap getRankingMap() {
+ return NotificationListenerWithPlugins.super.getCurrentRanking();
+ }
+
+ @Override
+ public void addNotification(StatusBarNotification sbn) {
+ onNotificationPosted(sbn, getRankingMap());
+ }
+
+ @Override
+ public void removeNotification(StatusBarNotification sbn) {
+ onNotificationRemoved(sbn, getRankingMap());
+ }
+
+ @Override
+ public void updateRanking() {
+ onNotificationRankingUpdate(getRankingMap());
+ }
+ };
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationPanelView.java b/com/android/systemui/statusbar/phone/NotificationPanelView.java
new file mode 100644
index 0000000..078e818
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -0,0 +1,2660 @@
+/*
+ * Copyright (C) 2012 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.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.ActivityManager;
+import android.app.Fragment;
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.PowerManager;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.MathUtils;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.keyguard.KeyguardStatusView;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.FlingAnimationUtils;
+import com.android.systemui.statusbar.GestureRecorder;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+import java.util.List;
+
+public class NotificationPanelView extends PanelView implements
+ ExpandableView.OnHeightChangedListener,
+ View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
+ KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener,
+ OnHeadsUpChangedListener, QS.HeightListener {
+
+ 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.
+ private static final int CAP_HEIGHT = 1456;
+ private static final int FONT_HEIGHT = 2163;
+
+ private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f;
+
+ static final String COUNTER_PANEL_OPEN = "panel_open";
+ static final String COUNTER_PANEL_OPEN_QS = "panel_open_qs";
+ private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek";
+
+ private static final Rect mDummyDirtyRect = new Rect(0, 0, 1, 1);
+
+ public static final long DOZE_ANIMATION_DURATION = 700;
+
+ private static final FloatProperty<NotificationPanelView> SET_DARK_AMOUNT_PROPERTY =
+ new FloatProperty<NotificationPanelView>("mDarkAmount") {
+ @Override
+ public void setValue(NotificationPanelView object, float value) {
+ object.setDarkAmount(value);
+ }
+
+ @Override
+ public Float get(NotificationPanelView object) {
+ return object.mDarkAmount;
+ }
+ };
+ private final PowerManager mPowerManager;
+
+ private KeyguardAffordanceHelper mAffordanceHelper;
+ private KeyguardUserSwitcher mKeyguardUserSwitcher;
+ private KeyguardStatusBarView mKeyguardStatusBar;
+ private QS mQs;
+ private FrameLayout mQsFrame;
+ private KeyguardStatusView mKeyguardStatusView;
+ private View mReserveNotificationSpace;
+ private View mQsNavbarScrim;
+ protected NotificationsQuickSettingsContainer mNotificationContainerParent;
+ protected NotificationStackScrollLayout mNotificationStackScroller;
+ private boolean mAnimateNextTopPaddingChange;
+
+ private int mTrackingPointer;
+ private VelocityTracker mQsVelocityTracker;
+ private boolean mQsTracking;
+
+ /**
+ * If set, the ongoing touch gesture might both trigger the expansion in {@link PanelView} and
+ * the expansion for quick settings.
+ */
+ private boolean mConflictingQsExpansionGesture;
+
+ /**
+ * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't
+ * intercepted yet.
+ */
+ private boolean mIntercepting;
+ private boolean mPanelExpanded;
+ private boolean mQsExpanded;
+ private boolean mQsExpandedWhenExpandingStarted;
+ private boolean mQsFullyExpanded;
+ private boolean mKeyguardShowing;
+ private boolean mDozing;
+ private boolean mDozingOnDown;
+ protected int mStatusBarState;
+ private float mInitialHeightOnTouch;
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+ private float mLastTouchX;
+ private float mLastTouchY;
+ protected float mQsExpansionHeight;
+ protected int mQsMinExpansionHeight;
+ protected int mQsMaxExpansionHeight;
+ private int mQsPeekHeight;
+ private boolean mStackScrollerOverscrolling;
+ private boolean mQsExpansionFromOverscroll;
+ private float mLastOverscroll;
+ protected boolean mQsExpansionEnabled = true;
+ private ValueAnimator mQsExpansionAnimator;
+ private FlingAnimationUtils mFlingAnimationUtils;
+ private int mStatusBarMinHeight;
+ private boolean mUnlockIconActive;
+ private int mNotificationsHeaderCollideDistance;
+ private int mUnlockMoveDistance;
+ private float mEmptyDragAmount;
+
+ private Animator mClockAnimator;
+ private int mClockAnimationTargetX = Integer.MIN_VALUE;
+ private int mClockAnimationTargetY = Integer.MIN_VALUE;
+ private int mTopPaddingAdjustment;
+ private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
+ new KeyguardClockPositionAlgorithm();
+ private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
+ new KeyguardClockPositionAlgorithm.Result();
+ private boolean mIsExpanding;
+
+ private boolean mBlockTouches;
+ private int mNotificationScrimWaitDistance;
+ // Used for two finger gesture as well as accessibility shortcut to QS.
+ private boolean mQsExpandImmediate;
+ private boolean mTwoFingerQsExpandPossible;
+
+ /**
+ * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still
+ * need to take this into account in our panel height calculation.
+ */
+ private boolean mQsAnimatorExpand;
+ private boolean mIsLaunchTransitionFinished;
+ private boolean mIsLaunchTransitionRunning;
+ private Runnable mLaunchAnimationEndRunnable;
+ private boolean mOnlyAffordanceInThisMotion;
+ private boolean mKeyguardStatusViewAnimating;
+ private ValueAnimator mQsSizeChangeAnimator;
+
+ private boolean mShowEmptyShadeView;
+
+ private boolean mQsScrimEnabled = true;
+ private boolean mLastAnnouncementWasQuickSettings;
+ private boolean mQsTouchAboveFalsingThreshold;
+ private int mQsFalsingThreshold;
+
+ private float mKeyguardStatusBarAnimateAlpha = 1f;
+ private float mQsClockAlphaOverride = 1f;
+ private int mOldLayoutDirection;
+ private HeadsUpTouchHelper mHeadsUpTouchHelper;
+ private boolean mIsExpansionFromHeadsUp;
+ private boolean mListenForHeadsUp;
+ private int mNavigationBarBottomHeight;
+ private boolean mExpandingFromHeadsUp;
+ private boolean mCollapsedOnDown;
+ private int mPositionMinSideMargin;
+ private int mMaxFadeoutHeight;
+ private int mLastOrientation = -1;
+ private boolean mClosingWithAlphaFadeOut;
+ private boolean mHeadsUpAnimatingAway;
+ private boolean mLaunchingAffordance;
+ private FalsingManager mFalsingManager;
+ private String mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
+
+ private Runnable mHeadsUpExistenceChangedRunnable = new Runnable() {
+ @Override
+ public void run() {
+ setHeadsUpAnimatingAway(false);
+ notifyBarPanelExpansionChanged();
+ }
+ };
+ private NotificationGroupManager mGroupManager;
+ private boolean mShowIconsWhenExpanded;
+ private int mIndicationBottomPadding;
+ private int mAmbientIndicationBottomPadding;
+ private boolean mIsFullWidth;
+ private float mDarkAmount;
+ private float mDarkAmountTarget;
+ private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
+ private boolean mNoVisibleNotifications = true;
+ private ValueAnimator mDarkAnimator;
+ private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ private boolean mUserSetupComplete;
+
+ public NotificationPanelView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setWillNotDraw(!DEBUG);
+ mFalsingManager = FalsingManager.getInstance(context);
+ mPowerManager = context.getSystemService(PowerManager.class);
+ }
+
+ public void setStatusBar(StatusBar bar) {
+ mStatusBar = bar;
+ mKeyguardBottomArea.setStatusBar(mStatusBar);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mKeyguardStatusBar = findViewById(R.id.keyguard_header);
+ mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
+
+ mNotificationContainerParent = (NotificationsQuickSettingsContainer)
+ findViewById(R.id.notification_container_parent);
+ mNotificationStackScroller = (NotificationStackScrollLayout)
+ findViewById(R.id.notification_stack_scroller);
+ mNotificationStackScroller.setOnHeightChangedListener(this);
+ mNotificationStackScroller.setOverscrollTopChangedListener(this);
+ mNotificationStackScroller.setOnEmptySpaceClickListener(this);
+ mKeyguardBottomArea = findViewById(R.id.keyguard_bottom_area);
+ mQsNavbarScrim = findViewById(R.id.qs_navbar_scrim);
+ mLastOrientation = getResources().getConfiguration().orientation;
+
+ initBottomArea();
+
+ mQsFrame = findViewById(R.id.qs_frame);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ FragmentHostManager.get(this).addTagListener(QS.TAG, mFragmentListener);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ FragmentHostManager.get(this).removeTagListener(QS.TAG, mFragmentListener);
+ }
+
+ @Override
+ protected void loadDimens() {
+ super.loadDimens();
+ mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
+ mStatusBarMinHeight = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_height);
+ mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
+ mNotificationsHeaderCollideDistance =
+ getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance);
+ mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance);
+ mClockPositionAlgorithm.loadDimens(getResources());
+ mNotificationScrimWaitDistance =
+ getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance);
+ mQsFalsingThreshold = getResources().getDimensionPixelSize(
+ R.dimen.qs_falsing_threshold);
+ mPositionMinSideMargin = getResources().getDimensionPixelSize(
+ R.dimen.notification_panel_min_side_margin);
+ mMaxFadeoutHeight = getResources().getDimensionPixelSize(
+ R.dimen.max_notification_fadeout_height);
+ mIndicationBottomPadding = getResources().getDimensionPixelSize(
+ R.dimen.keyguard_indication_bottom_padding);
+ }
+
+ public void updateResources() {
+ Resources res = getResources();
+ int qsWidth = res.getDimensionPixelSize(R.dimen.qs_panel_width);
+ int panelGravity = getResources().getInteger(R.integer.notification_panel_layout_gravity);
+ FrameLayout.LayoutParams lp =
+ (FrameLayout.LayoutParams) mQsFrame.getLayoutParams();
+ if (lp.width != qsWidth || lp.gravity != panelGravity) {
+ lp.width = qsWidth;
+ lp.gravity = panelGravity;
+ mQsFrame.setLayoutParams(lp);
+ }
+
+ int panelWidth = res.getDimensionPixelSize(R.dimen.notification_panel_width);
+ lp = (FrameLayout.LayoutParams) mNotificationStackScroller.getLayoutParams();
+ if (lp.width != panelWidth || lp.gravity != panelGravity) {
+ lp.width = panelWidth;
+ lp.gravity = panelGravity;
+ mNotificationStackScroller.setLayoutParams(lp);
+ }
+ }
+
+ public void onOverlayChanged() {
+ // Re-inflate the status view group.
+ int index = indexOfChild(mKeyguardStatusView);
+ removeView(mKeyguardStatusView);
+ mKeyguardStatusView = (KeyguardStatusView) LayoutInflater.from(mContext).inflate(
+ R.layout.keyguard_status_view,
+ this,
+ false);
+ addView(mKeyguardStatusView, index);
+
+ // Update keyguard bottom area
+ index = indexOfChild(mKeyguardBottomArea);
+ removeView(mKeyguardBottomArea);
+ mKeyguardBottomArea = (KeyguardBottomAreaView) LayoutInflater.from(mContext).inflate(
+ R.layout.keyguard_bottom_area,
+ this,
+ false);
+ addView(mKeyguardBottomArea, index);
+ initBottomArea();
+ setDarkAmount(mDarkAmount);
+
+ setKeyguardStatusViewVisibility(mStatusBarState, false, false);
+ setKeyguardBottomAreaVisibility(mStatusBarState, false);
+ }
+
+ private void initBottomArea() {
+ mAffordanceHelper = new KeyguardAffordanceHelper(this, getContext());
+ mKeyguardBottomArea.setAffordanceHelper(mAffordanceHelper);
+ mKeyguardBottomArea.setStatusBar(mStatusBar);
+ mKeyguardBottomArea.setUserSetupComplete(mUserSetupComplete);
+ }
+
+ public void setKeyguardIndicationController(KeyguardIndicationController indicationController) {
+ mKeyguardBottomArea.setKeyguardIndicationController(indicationController);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ setIsFullWidth(mNotificationStackScroller.getWidth() == getWidth());
+
+ // Update Clock Pivot
+ mKeyguardStatusView.setPivotX(getWidth() / 2);
+ mKeyguardStatusView.setPivotY((FONT_HEIGHT - CAP_HEIGHT) / 2048f *
+ mKeyguardStatusView.getClockTextSize());
+
+ // Calculate quick setting heights.
+ int oldMaxHeight = mQsMaxExpansionHeight;
+ if (mQs != null) {
+ mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight();
+ mQsMaxExpansionHeight = mQs.getDesiredHeight();
+ }
+ positionClockAndNotifications();
+ if (mQsExpanded && mQsFullyExpanded) {
+ mQsExpansionHeight = mQsMaxExpansionHeight;
+ requestScrollerTopPaddingUpdate(false /* animate */);
+ requestPanelHeightUpdate();
+
+ // Size has changed, start an animation.
+ if (mQsMaxExpansionHeight != oldMaxHeight) {
+ startQsSizeChangeAnimation(oldMaxHeight, mQsMaxExpansionHeight);
+ }
+ } else if (!mQsExpanded) {
+ setQsExpansion(mQsMinExpansionHeight + mLastOverscroll);
+ }
+ updateExpandedHeight(getExpandedHeight());
+ updateHeader();
+
+ // 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 && mQs != null) {
+ mQs.setHeightOverride(mQs.getDesiredHeight());
+ }
+ updateMaxHeadsUpTranslation();
+ }
+
+ private void setIsFullWidth(boolean isFullWidth) {
+ mIsFullWidth = isFullWidth;
+ mNotificationStackScroller.setIsFullWidth(isFullWidth);
+ }
+
+ 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(Interpolators.FAST_OUT_SLOW_IN);
+ mQsSizeChangeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ requestScrollerTopPaddingUpdate(false /* animate */);
+ requestPanelHeightUpdate();
+ int height = (int) mQsSizeChangeAnimator.getAnimatedValue();
+ mQs.setHeightOverride(height);
+ }
+ });
+ 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.
+ */
+ private void positionClockAndNotifications() {
+ boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
+ int stackScrollerPadding;
+ if (mStatusBarState != StatusBarState.KEYGUARD) {
+ stackScrollerPadding = (mQs != null ? mQs.getHeader().getHeight() : 0) + mQsPeekHeight;
+ mTopPaddingAdjustment = 0;
+ } else {
+ mClockPositionAlgorithm.setup(
+ mStatusBar.getMaxKeyguardNotifications(),
+ getMaxPanelHeight(),
+ getExpandedHeight(),
+ mNotificationStackScroller.getNotGoneChildCount(),
+ getHeight(),
+ mKeyguardStatusView.getHeight(),
+ mEmptyDragAmount,
+ mKeyguardStatusView.getClockBottom(),
+ mDarkAmount);
+ mClockPositionAlgorithm.run(mClockPositionResult);
+ if (animate || mClockAnimator != null) {
+ startClockAnimation(mClockPositionResult.clockX, mClockPositionResult.clockY);
+ } else {
+ mKeyguardStatusView.setX(mClockPositionResult.clockX);
+ mKeyguardStatusView.setY(mClockPositionResult.clockY);
+ }
+ updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale);
+ stackScrollerPadding = mClockPositionResult.stackScrollerPadding;
+ mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
+ }
+ mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding);
+ mNotificationStackScroller.setDarkShelfOffsetX(mClockPositionResult.clockX);
+ requestScrollerTopPaddingUpdate(animate);
+ }
+
+ /**
+ * @param maximum the maximum to return at most
+ * @return the maximum keyguard notifications that can fit on the screen
+ */
+ public int computeMaxKeyguardNotifications(int maximum) {
+ float minPadding = mClockPositionAlgorithm.getMinStackScrollerPadding(getHeight(),
+ mKeyguardStatusView.getHeight());
+ int notificationPadding = Math.max(1, getResources().getDimensionPixelSize(
+ R.dimen.notification_divider_height));
+ NotificationShelf shelf = mNotificationStackScroller.getNotificationShelf();
+ float shelfSize = shelf.getVisibility() == GONE ? 0
+ : shelf.getIntrinsicHeight() + notificationPadding;
+ float availableSpace = mNotificationStackScroller.getHeight() - minPadding - shelfSize
+ - Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding);
+ int count = 0;
+ for (int i = 0; i < mNotificationStackScroller.getChildCount(); i++) {
+ ExpandableView child = (ExpandableView) mNotificationStackScroller.getChildAt(i);
+ if (!(child instanceof ExpandableNotificationRow)) {
+ continue;
+ }
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
+ row.getStatusBarNotification());
+ if (suppressedSummary) {
+ continue;
+ }
+ if (!mStatusBar.shouldShowOnKeyguard(row.getStatusBarNotification())) {
+ continue;
+ }
+ if (row.isRemoved()) {
+ continue;
+ }
+ availableSpace -= child.getMinHeight() + notificationPadding;
+ if (availableSpace >= 0 && count < maximum) {
+ count++;
+ } else if (availableSpace > -shelfSize) {
+ // if we are exactly the last view, then we can show us still!
+ for (int j = i + 1; j < mNotificationStackScroller.getChildCount(); j++) {
+ if (mNotificationStackScroller.getChildAt(j)
+ instanceof ExpandableNotificationRow) {
+ return count;
+ }
+ }
+ count++;
+ return count;
+ } else {
+ return count;
+ }
+ }
+ return count;
+ }
+
+ private void startClockAnimation(int x, int y) {
+ if (mClockAnimationTargetX == x && mClockAnimationTargetY == y) {
+ return;
+ }
+ mClockAnimationTargetX = x;
+ mClockAnimationTargetY = y;
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ if (mClockAnimator != null) {
+ mClockAnimator.removeAllListeners();
+ mClockAnimator.cancel();
+ }
+ AnimatorSet set = new AnimatorSet();
+ set.play(ObjectAnimator.ofFloat(
+ mKeyguardStatusView, View.Y, mClockAnimationTargetY))
+ .with(ObjectAnimator.ofFloat(
+ mKeyguardStatusView, View.X, mClockAnimationTargetX));
+ mClockAnimator = set;
+ mClockAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ mClockAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mClockAnimator = null;
+ mClockAnimationTargetX = Integer.MIN_VALUE;
+ mClockAnimationTargetY = Integer.MIN_VALUE;
+ }
+ });
+ mClockAnimator.start();
+ return true;
+ }
+ });
+ }
+
+ private void updateClock(float alpha, float scale) {
+ if (!mKeyguardStatusViewAnimating) {
+ mKeyguardStatusView.setAlpha(alpha * mQsClockAlphaOverride);
+ }
+ mKeyguardStatusView.setScaleX(scale);
+ mKeyguardStatusView.setScaleY(scale);
+ }
+
+ public void animateToFullShade(long delay) {
+ mAnimateNextTopPaddingChange = true;
+ mNotificationStackScroller.goToFullShade(delay);
+ requestLayout();
+ }
+
+ public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
+ mQsExpansionEnabled = qsExpansionEnabled;
+ if (mQs == null) return;
+ mQs.setHeaderClickable(qsExpansionEnabled);
+ }
+
+ @Override
+ public void resetViews() {
+ mIsLaunchTransitionFinished = false;
+ mBlockTouches = false;
+ mUnlockIconActive = false;
+ if (!mLaunchingAffordance) {
+ mAffordanceHelper.reset(false);
+ mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
+ }
+ closeQs();
+ mStatusBar.closeAndSaveGuts(true /* leavebehind */, true /* force */,
+ true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
+ mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */,
+ true /* cancelAnimators */);
+ mNotificationStackScroller.resetScrollPosition();
+ }
+
+ public void closeQs() {
+ cancelQsAnimation();
+ setQsExpansion(mQsMinExpansionHeight);
+ }
+
+ public void animateCloseQs() {
+ if (mQsExpansionAnimator != null) {
+ if (!mQsAnimatorExpand) {
+ return;
+ }
+ float height = mQsExpansionHeight;
+ mQsExpansionAnimator.cancel();
+ setQsExpansion(height);
+ }
+ flingSettings(0 /* vel */, false);
+ }
+
+ public void openQs() {
+ cancelQsAnimation();
+ if (mQsExpansionEnabled) {
+ setQsExpansion(mQsMaxExpansionHeight);
+ }
+ }
+
+ public void expandWithQs() {
+ if (mQsExpansionEnabled) {
+ mQsExpandImmediate = true;
+ }
+ expand(true /* animate */);
+ }
+
+ @Override
+ public void fling(float vel, boolean expand) {
+ GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
+ if (gr != null) {
+ gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
+ }
+ super.fling(vel, expand);
+ }
+
+ @Override
+ protected void flingToHeight(float vel, boolean expand, float target,
+ float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
+ mHeadsUpTouchHelper.notifyFling(!expand);
+ setClosingWithAlphaFadeout(!expand && getFadeoutAlpha() == 1.0f);
+ super.flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ event.getText().add(getKeyguardOrLockScreenString());
+ mLastAnnouncementWasQuickSettings = false;
+ return true;
+ }
+ return super.dispatchPopulateAccessibilityEventInternal(event);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (mBlockTouches || mQs.isCustomizing()) {
+ return false;
+ }
+ initDownStates(event);
+ if (mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+ mIsExpansionFromHeadsUp = true;
+ MetricsLogger.count(mContext, COUNTER_PANEL_OPEN, 1);
+ MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_PEEK, 1);
+ return true;
+ }
+
+ if (!isFullyCollapsed() && onQsIntercept(event)) {
+ return true;
+ }
+ return super.onInterceptTouchEvent(event);
+ }
+
+ private boolean onQsIntercept(MotionEvent event) {
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mIntercepting = true;
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ initVelocityTracker();
+ trackMovement(event);
+ if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ }
+ if (mQsExpansionAnimator != null) {
+ onQsExpansionStarted();
+ mInitialHeightOnTouch = mQsExpansionHeight;
+ mQsTracking = true;
+ mIntercepting = false;
+ mNotificationStackScroller.removeLongPressCallback();
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ mTrackingPointer = event.getPointerId(newIndex);
+ mInitialTouchX = event.getX(newIndex);
+ mInitialTouchY = event.getY(newIndex);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final float h = y - mInitialTouchY;
+ trackMovement(event);
+ if (mQsTracking) {
+
+ // Already tracking because onOverscrolled was called. We need to update here
+ // so we don't stop for a frame until the next touch event gets handled in
+ // onTouchEvent.
+ setQsExpansion(h + mInitialHeightOnTouch);
+ trackMovement(event);
+ mIntercepting = false;
+ return true;
+ }
+ if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
+ && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
+ mQsTracking = true;
+ onQsExpansionStarted();
+ notifyExpandingFinished();
+ mInitialHeightOnTouch = mQsExpansionHeight;
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ mIntercepting = false;
+ mNotificationStackScroller.removeLongPressCallback();
+ return true;
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ trackMovement(event);
+ if (mQsTracking) {
+ flingQsWithCurrentVelocity(y,
+ event.getActionMasked() == MotionEvent.ACTION_CANCEL);
+ mQsTracking = false;
+ }
+ mIntercepting = false;
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean isInContentBounds(float x, float y) {
+ float stackScrollerX = mNotificationStackScroller.getX();
+ return !mNotificationStackScroller.isBelowLastNotification(x - stackScrollerX, y)
+ && stackScrollerX < x && x < stackScrollerX + mNotificationStackScroller.getWidth();
+ }
+
+ private void initDownStates(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mOnlyAffordanceInThisMotion = false;
+ mQsTouchAboveFalsingThreshold = mQsFullyExpanded;
+ mDozingOnDown = isDozing();
+ mCollapsedOnDown = isFullyCollapsed();
+ mListenForHeadsUp = mCollapsedOnDown && mHeadsUpManager.hasPinnedHeadsUp();
+ }
+ }
+
+ private void flingQsWithCurrentVelocity(float y, boolean isCancelMotionEvent) {
+ float vel = getCurrentQSVelocity();
+ final boolean expandsQs = flingExpandsQs(vel);
+ if (expandsQs) {
+ logQsSwipeDown(y);
+ }
+ flingSettings(vel, expandsQs && !isCancelMotionEvent);
+ }
+
+ private void logQsSwipeDown(float y) {
+ float vel = getCurrentQSVelocity();
+ final int gesture = mStatusBarState == StatusBarState.KEYGUARD
+ ? MetricsEvent.ACTION_LS_QS
+ : MetricsEvent.ACTION_SHADE_QS_PULL;
+ mLockscreenGestureLogger.write(gesture,
+ (int) ((y - mInitialTouchY) / mStatusBar.getDisplayDensity()),
+ (int) (vel / mStatusBar.getDisplayDensity()));
+ }
+
+ private boolean flingExpandsQs(float vel) {
+ if (isFalseTouch()) {
+ return false;
+ }
+ if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+ return getQsExpansionFraction() > 0.5f;
+ } else {
+ return vel > 0;
+ }
+ }
+
+ private boolean isFalseTouch() {
+ if (!needsAntiFalsing()) {
+ return false;
+ }
+ if (mFalsingManager.isClassiferEnabled()) {
+ return mFalsingManager.isFalseTouch();
+ }
+ return !mQsTouchAboveFalsingThreshold;
+ }
+
+ private float getQsExpansionFraction() {
+ return Math.min(1f, (mQsExpansionHeight - mQsMinExpansionHeight)
+ / (getTempQsMaxExpansion() - mQsMinExpansionHeight));
+ }
+
+ @Override
+ protected float getOpeningHeight() {
+ return mNotificationStackScroller.getOpeningHeight();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mBlockTouches || (mQs != null && mQs.isCustomizing())) {
+ return false;
+ }
+ initDownStates(event);
+ if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
+ && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+ mIsExpansionFromHeadsUp = true;
+ MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_PEEK, 1);
+ }
+ boolean handled = false;
+ if ((!mIsExpanding || mHintAnimationRunning)
+ && !mQsExpanded
+ && mStatusBar.getBarState() != StatusBarState.SHADE
+ && !mDozing) {
+ handled |= mAffordanceHelper.onTouchEvent(event);
+ }
+ if (mOnlyAffordanceInThisMotion) {
+ return true;
+ }
+ handled |= mHeadsUpTouchHelper.onTouchEvent(event);
+
+ if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
+ return true;
+ }
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
+ MetricsLogger.count(mContext, COUNTER_PANEL_OPEN, 1);
+ updateVerticalPanelPosition(event.getX());
+ handled = true;
+ }
+ handled |= super.onTouchEvent(event);
+ return mDozing ? handled : true;
+ }
+
+ private boolean handleQsTouch(MotionEvent event) {
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
+ && mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded
+ && mQsExpansionEnabled) {
+
+ // Down in the empty area while fully expanded - go to QS.
+ mQsTracking = true;
+ mConflictingQsExpansionGesture = true;
+ onQsExpansionStarted();
+ mInitialHeightOnTouch = mQsExpansionHeight;
+ mInitialTouchY = event.getX();
+ mInitialTouchX = event.getY();
+ }
+ if (!isFullyCollapsed()) {
+ handleQsDown(event);
+ }
+ if (!mQsExpandImmediate && mQsTracking) {
+ onQsTouch(event);
+ if (!mConflictingQsExpansionGesture) {
+ return true;
+ }
+ }
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mConflictingQsExpansionGesture = false;
+ }
+ if (action == MotionEvent.ACTION_DOWN && isFullyCollapsed()
+ && mQsExpansionEnabled) {
+ mTwoFingerQsExpandPossible = true;
+ }
+ if (mTwoFingerQsExpandPossible && isOpenQsEvent(event)
+ && event.getY(event.getActionIndex()) < mStatusBarMinHeight) {
+ MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_QS, 1);
+ mQsExpandImmediate = true;
+ requestPanelHeightUpdate();
+
+ // Normally, we start listening when the panel is expanded, but here we need to start
+ // earlier so the state is already up to date when dragging down.
+ setListening(true);
+ }
+ return false;
+ }
+
+ private boolean isInQsArea(float x, float y) {
+ return (x >= mQsFrame.getX()
+ && x <= mQsFrame.getX() + mQsFrame.getWidth())
+ && (y <= mNotificationStackScroller.getBottomMostNotificationBottom()
+ || y <= mQs.getView().getY() + mQs.getView().getHeight());
+ }
+
+ private boolean isOpenQsEvent(MotionEvent event) {
+ final int pointerCount = event.getPointerCount();
+ final int action = event.getActionMasked();
+
+ final boolean twoFingerDrag = action == MotionEvent.ACTION_POINTER_DOWN
+ && pointerCount == 2;
+
+ final boolean stylusButtonClickDrag = action == MotionEvent.ACTION_DOWN
+ && (event.isButtonPressed(MotionEvent.BUTTON_STYLUS_PRIMARY)
+ || event.isButtonPressed(MotionEvent.BUTTON_STYLUS_SECONDARY));
+
+ final boolean mouseButtonClickDrag = action == MotionEvent.ACTION_DOWN
+ && (event.isButtonPressed(MotionEvent.BUTTON_SECONDARY)
+ || event.isButtonPressed(MotionEvent.BUTTON_TERTIARY));
+
+ return twoFingerDrag || stylusButtonClickDrag || mouseButtonClickDrag;
+ }
+
+ private void handleQsDown(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN
+ && shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) {
+ mFalsingManager.onQsDown();
+ mQsTracking = true;
+ onQsExpansionStarted();
+ mInitialHeightOnTouch = mQsExpansionHeight;
+ mInitialTouchY = event.getX();
+ mInitialTouchX = event.getY();
+
+ // If we interrupt an expansion gesture here, make sure to update the state correctly.
+ notifyExpandingFinished();
+ }
+ }
+
+ @Override
+ protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
+ boolean expands = super.flingExpands(vel, vectorVel, x, y);
+
+ // If we are already running a QS expansion, make sure that we keep the panel open.
+ if (mQsExpansionAnimator != null) {
+ expands = true;
+ }
+ return expands;
+ }
+
+ @Override
+ protected boolean hasConflictingGestures() {
+ return mStatusBar.getBarState() != StatusBarState.SHADE;
+ }
+
+ @Override
+ protected boolean shouldGestureIgnoreXTouchSlop(float x, float y) {
+ return !mAffordanceHelper.isOnAffordanceIcon(x, y);
+ }
+
+ private void onQsTouch(MotionEvent event) {
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float y = event.getY(pointerIndex);
+ final float x = event.getX(pointerIndex);
+ final float h = y - mInitialTouchY;
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mQsTracking = true;
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ onQsExpansionStarted();
+ mInitialHeightOnTouch = mQsExpansionHeight;
+ initVelocityTracker();
+ trackMovement(event);
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ final float newY = event.getY(newIndex);
+ final float newX = event.getX(newIndex);
+ mTrackingPointer = event.getPointerId(newIndex);
+ mInitialHeightOnTouch = mQsExpansionHeight;
+ mInitialTouchY = newY;
+ mInitialTouchX = newX;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ setQsExpansion(h + mInitialHeightOnTouch);
+ if (h >= getFalsingThreshold()) {
+ mQsTouchAboveFalsingThreshold = true;
+ }
+ trackMovement(event);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mQsTracking = false;
+ mTrackingPointer = -1;
+ trackMovement(event);
+ float fraction = getQsExpansionFraction();
+ if (fraction != 0f || y >= mInitialTouchY) {
+ flingQsWithCurrentVelocity(y,
+ event.getActionMasked() == MotionEvent.ACTION_CANCEL);
+ }
+ if (mQsVelocityTracker != null) {
+ mQsVelocityTracker.recycle();
+ mQsVelocityTracker = null;
+ }
+ break;
+ }
+ }
+
+ private int getFalsingThreshold() {
+ float factor = mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+ return (int) (mQsFalsingThreshold * factor);
+ }
+
+ @Override
+ public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
+ cancelQsAnimation();
+ if (!mQsExpansionEnabled) {
+ amount = 0f;
+ }
+ float rounded = amount >= 1f ? amount : 0f;
+ setOverScrolling(rounded != 0f && isRubberbanded);
+ mQsExpansionFromOverscroll = rounded != 0f;
+ mLastOverscroll = rounded;
+ updateQsState();
+ setQsExpansion(mQsMinExpansionHeight + rounded);
+ }
+
+ @Override
+ public void flingTopOverscroll(float velocity, boolean open) {
+ mLastOverscroll = 0f;
+ mQsExpansionFromOverscroll = false;
+ setQsExpansion(mQsExpansionHeight);
+ flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, open && mQsExpansionEnabled,
+ new Runnable() {
+ @Override
+ public void run() {
+ mStackScrollerOverscrolling = false;
+ setOverScrolling(false);
+ updateQsState();
+ }
+ }, false /* isClick */);
+ }
+
+ private void setOverScrolling(boolean overscrolling) {
+ mStackScrollerOverscrolling = overscrolling;
+ if (mQs == null) return;
+ mQs.setOverscrolling(overscrolling);
+ }
+
+ private void onQsExpansionStarted() {
+ onQsExpansionStarted(0);
+ }
+
+ protected void onQsExpansionStarted(int overscrollAmount) {
+ cancelQsAnimation();
+ cancelHeightAnimator();
+
+ // Reset scroll position and apply that position to the expanded height.
+ float height = mQsExpansionHeight - overscrollAmount;
+ setQsExpansion(height);
+ requestPanelHeightUpdate();
+ mNotificationStackScroller.checkSnoozeLeavebehind();
+ }
+
+ private void setQsExpanded(boolean expanded) {
+ boolean changed = mQsExpanded != expanded;
+ if (changed) {
+ mQsExpanded = expanded;
+ updateQsState();
+ requestPanelHeightUpdate();
+ mFalsingManager.setQsExpanded(expanded);
+ mStatusBar.setQsExpanded(expanded);
+ mNotificationContainerParent.setQsExpanded(expanded);
+ }
+ }
+
+ public void setBarState(int statusBarState, boolean keyguardFadingAway,
+ boolean goingToFullShade) {
+ int oldState = mStatusBarState;
+ boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD;
+ setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade);
+ setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
+
+ mStatusBarState = statusBarState;
+ mKeyguardShowing = keyguardShowing;
+ if (mQs != null) {
+ mQs.setKeyguardShowing(mKeyguardShowing);
+ }
+
+ if (oldState == StatusBarState.KEYGUARD
+ && (goingToFullShade || statusBarState == StatusBarState.SHADE_LOCKED)) {
+ animateKeyguardStatusBarOut();
+ long delay = mStatusBarState == StatusBarState.SHADE_LOCKED
+ ? 0 : mStatusBar.calculateGoingToFullShadeDelay();
+ mQs.animateHeaderSlidingIn(delay);
+ } else if (oldState == StatusBarState.SHADE_LOCKED
+ && statusBarState == StatusBarState.KEYGUARD) {
+ animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ mQs.animateHeaderSlidingOut();
+ } else {
+ mKeyguardStatusBar.setAlpha(1f);
+ mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE);
+ if (keyguardShowing && oldState != mStatusBarState) {
+ mKeyguardBottomArea.onKeyguardShowingChanged();
+ if (mQs != null) {
+ mQs.hideImmediately();
+ }
+ }
+ }
+ if (keyguardShowing) {
+ updateDozingVisibilities(false /* animate */);
+ }
+ resetVerticalPanelPosition();
+ updateQsState();
+ }
+
+ private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mKeyguardStatusViewAnimating = false;
+ mKeyguardStatusView.setVisibility(View.GONE);
+ }
+ };
+
+ private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mKeyguardStatusViewAnimating = false;
+ }
+ };
+
+ private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mKeyguardStatusBar.setVisibility(View.INVISIBLE);
+ mKeyguardStatusBar.setAlpha(1f);
+ mKeyguardStatusBarAnimateAlpha = 1f;
+ }
+ };
+
+ private void animateKeyguardStatusBarOut() {
+ ValueAnimator anim = ValueAnimator.ofFloat(mKeyguardStatusBar.getAlpha(), 0f);
+ anim.addUpdateListener(mStatusBarAnimateAlphaListener);
+ anim.setStartDelay(mStatusBar.isKeyguardFadingAway()
+ ? mStatusBar.getKeyguardFadingAwayDelay()
+ : 0);
+ anim.setDuration(mStatusBar.isKeyguardFadingAway()
+ ? mStatusBar.getKeyguardFadingAwayDuration() / 2
+ : StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimateKeyguardStatusBarInvisibleEndRunnable.run();
+ }
+ });
+ anim.start();
+ }
+
+ private final ValueAnimator.AnimatorUpdateListener mStatusBarAnimateAlphaListener =
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mKeyguardStatusBarAnimateAlpha = (float) animation.getAnimatedValue();
+ updateHeaderKeyguardAlpha();
+ }
+ };
+
+ private void animateKeyguardStatusBarIn(long duration) {
+ mKeyguardStatusBar.setVisibility(View.VISIBLE);
+ mKeyguardStatusBar.setAlpha(0f);
+ ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
+ anim.addUpdateListener(mStatusBarAnimateAlphaListener);
+ anim.setDuration(duration);
+ anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ anim.start();
+ }
+
+ private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mKeyguardBottomArea.setVisibility(View.GONE);
+ }
+ };
+
+ private void setKeyguardBottomAreaVisibility(int statusBarState,
+ boolean goingToFullShade) {
+ mKeyguardBottomArea.animate().cancel();
+ if (goingToFullShade) {
+ mKeyguardBottomArea.animate()
+ .alpha(0f)
+ .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
+ .setDuration(mStatusBar.getKeyguardFadingAwayDuration() / 2)
+ .setInterpolator(Interpolators.ALPHA_OUT)
+ .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable)
+ .start();
+ } else if (statusBarState == StatusBarState.KEYGUARD
+ || statusBarState == StatusBarState.SHADE_LOCKED) {
+ mKeyguardBottomArea.setVisibility(View.VISIBLE);
+ mKeyguardBottomArea.setAlpha(1f);
+ } else {
+ mKeyguardBottomArea.setVisibility(View.GONE);
+ mKeyguardBottomArea.setAlpha(1f);
+ }
+ }
+
+ private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway,
+ boolean goingToFullShade) {
+ if ((!keyguardFadingAway && mStatusBarState == StatusBarState.KEYGUARD
+ && statusBarState != StatusBarState.KEYGUARD) || goingToFullShade) {
+ mKeyguardStatusView.animate().cancel();
+ mKeyguardStatusViewAnimating = true;
+ mKeyguardStatusView.animate()
+ .alpha(0f)
+ .setStartDelay(0)
+ .setDuration(160)
+ .setInterpolator(Interpolators.ALPHA_OUT)
+ .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable);
+ if (keyguardFadingAway) {
+ mKeyguardStatusView.animate()
+ .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
+ .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
+ .start();
+ }
+ } else if (mStatusBarState == StatusBarState.SHADE_LOCKED
+ && statusBarState == StatusBarState.KEYGUARD) {
+ mKeyguardStatusView.animate().cancel();
+ mKeyguardStatusView.setVisibility(View.VISIBLE);
+ mKeyguardStatusViewAnimating = true;
+ mKeyguardStatusView.setAlpha(0f);
+ mKeyguardStatusView.animate()
+ .alpha(1f)
+ .setStartDelay(0)
+ .setDuration(320)
+ .setInterpolator(Interpolators.ALPHA_IN)
+ .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable);
+ } else if (statusBarState == StatusBarState.KEYGUARD) {
+ mKeyguardStatusView.animate().cancel();
+ mKeyguardStatusViewAnimating = false;
+ mKeyguardStatusView.setVisibility(View.VISIBLE);
+ mKeyguardStatusView.setAlpha(1f);
+ } else {
+ mKeyguardStatusView.animate().cancel();
+ mKeyguardStatusViewAnimating = false;
+ mKeyguardStatusView.setVisibility(View.GONE);
+ mKeyguardStatusView.setAlpha(1f);
+ }
+ }
+
+ private void updateQsState() {
+ mNotificationStackScroller.setQsExpanded(mQsExpanded);
+ mNotificationStackScroller.setScrollingEnabled(
+ mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded
+ || mQsExpansionFromOverscroll));
+ updateEmptyShadeView();
+ mQsNavbarScrim.setVisibility(mStatusBarState == StatusBarState.SHADE && mQsExpanded
+ && !mStackScrollerOverscrolling && mQsScrimEnabled
+ ? View.VISIBLE
+ : View.INVISIBLE);
+ if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) {
+ mKeyguardUserSwitcher.hideIfNotSimple(true /* animate */);
+ }
+ if (mQs == null) return;
+ mQs.setExpanded(mQsExpanded);
+ }
+
+ private void setQsExpansion(float height) {
+ height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
+ mQsFullyExpanded = height == mQsMaxExpansionHeight && mQsMaxExpansionHeight != 0;
+ if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
+ setQsExpanded(true);
+ } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
+ setQsExpanded(false);
+ if (mLastAnnouncementWasQuickSettings && !mTracking && !isCollapsing()) {
+ announceForAccessibility(getKeyguardOrLockScreenString());
+ mLastAnnouncementWasQuickSettings = false;
+ }
+ }
+ mQsExpansionHeight = height;
+ updateQsExpansion();
+ requestScrollerTopPaddingUpdate(false /* animate */);
+ if (mKeyguardShowing) {
+ updateHeaderKeyguardAlpha();
+ }
+ if (mStatusBarState == StatusBarState.SHADE_LOCKED
+ || mStatusBarState == StatusBarState.KEYGUARD) {
+ updateKeyguardBottomAreaAlpha();
+ }
+ if (mStatusBarState == StatusBarState.SHADE && mQsExpanded
+ && !mStackScrollerOverscrolling && mQsScrimEnabled) {
+ mQsNavbarScrim.setAlpha(getQsExpansionFraction());
+ }
+
+ // Fade clock when QS is on top of it
+ float newClockAlpha = (height - mKeyguardStatusView.getY()) /
+ mKeyguardStatusView.getHeight();
+ newClockAlpha = 1 - MathUtils.constrain(newClockAlpha, 0, 1);
+ if (newClockAlpha != mQsClockAlphaOverride) {
+ mQsClockAlphaOverride = Interpolators.ALPHA_OUT.getInterpolation(newClockAlpha);
+ updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale);
+ }
+
+ // Upon initialisation when we are not layouted yet we don't want to announce that we are
+ // fully expanded, hence the != 0.0f check.
+ if (height != 0.0f && mQsFullyExpanded && !mLastAnnouncementWasQuickSettings) {
+ announceForAccessibility(getContext().getString(
+ R.string.accessibility_desc_quick_settings));
+ mLastAnnouncementWasQuickSettings = true;
+ }
+ if (mQsFullyExpanded && mFalsingManager.shouldEnforceBouncer()) {
+ mStatusBar.executeRunnableDismissingKeyguard(null, null /* cancelAction */,
+ false /* dismissShade */, true /* afterKeyguardGone */, false /* deferred */);
+ }
+ if (DEBUG) {
+ invalidate();
+ }
+ }
+
+ protected void updateQsExpansion() {
+ if (mQs == null) return;
+ mQs.setQsExpansion(getQsExpansionFraction(), getHeaderTranslation());
+ }
+
+ private String getKeyguardOrLockScreenString() {
+ if (mQs != null && mQs.isCustomizing()) {
+ return getContext().getString(R.string.accessibility_desc_quick_settings_edit);
+ } else if (mStatusBarState == StatusBarState.KEYGUARD) {
+ return getContext().getString(R.string.accessibility_desc_lock_screen);
+ } else {
+ return getContext().getString(R.string.accessibility_desc_notification_shade);
+ }
+ }
+
+ private float calculateQsTopPadding() {
+ 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;
+ 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) {
+
+ // 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(),
+ mQsMaxExpansionHeight);
+ } else {
+ return mQsExpansionHeight;
+ }
+ }
+
+ protected void requestScrollerTopPaddingUpdate(boolean animate) {
+ mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(),
+ mAnimateNextTopPaddingChange || animate,
+ mKeyguardShowing
+ && (mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted));
+ mAnimateNextTopPaddingChange = false;
+ }
+
+ private void trackMovement(MotionEvent event) {
+ if (mQsVelocityTracker != null) mQsVelocityTracker.addMovement(event);
+ mLastTouchX = event.getX();
+ mLastTouchY = event.getY();
+ }
+
+ private void initVelocityTracker() {
+ if (mQsVelocityTracker != null) {
+ mQsVelocityTracker.recycle();
+ }
+ mQsVelocityTracker = VelocityTracker.obtain();
+ }
+
+ private float getCurrentQSVelocity() {
+ if (mQsVelocityTracker == null) {
+ return 0;
+ }
+ mQsVelocityTracker.computeCurrentVelocity(1000);
+ return mQsVelocityTracker.getYVelocity();
+ }
+
+ private void cancelQsAnimation() {
+ if (mQsExpansionAnimator != null) {
+ mQsExpansionAnimator.cancel();
+ }
+ }
+
+ public void flingSettings(float vel, boolean expand) {
+ flingSettings(vel, expand, null, false /* isClick */);
+ }
+
+ protected void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable,
+ boolean isClick) {
+ float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
+ if (target == mQsExpansionHeight) {
+ if (onFinishRunnable != null) {
+ onFinishRunnable.run();
+ }
+ return;
+ }
+
+ // If we move in the opposite direction, reset velocity and use a different duration.
+ boolean oppositeDirection = false;
+ if (vel > 0 && !expand || vel < 0 && expand) {
+ vel = 0;
+ oppositeDirection = true;
+ }
+ ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
+ if (isClick) {
+ animator.setInterpolator(Interpolators.TOUCH_RESPONSE);
+ animator.setDuration(368);
+ } else {
+ mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
+ }
+ if (oppositeDirection) {
+ animator.setDuration(350);
+ }
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setQsExpansion((Float) animation.getAnimatedValue());
+ }
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mNotificationStackScroller.resetCheckSnoozeLeavebehind();
+ mQsExpansionAnimator = null;
+ if (onFinishRunnable != null) {
+ onFinishRunnable.run();
+ }
+ }
+ });
+ animator.start();
+ mQsExpansionAnimator = animator;
+ mQsAnimatorExpand = expand;
+ }
+
+ /**
+ * @return Whether we should intercept a gesture to open Quick Settings.
+ */
+ private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
+ if (!mQsExpansionEnabled || mCollapsedOnDown) {
+ return false;
+ }
+ View header = mKeyguardShowing ? mKeyguardStatusBar : mQs.getHeader();
+ final boolean onHeader = x >= mQsFrame.getX()
+ && x <= mQsFrame.getX() + mQsFrame.getWidth()
+ && y >= header.getTop() && y <= header.getBottom();
+ if (mQsExpanded) {
+ return onHeader || (yDiff < 0 && isInQsArea(x, y));
+ } else {
+ return onHeader;
+ }
+ }
+
+ @Override
+ protected boolean isScrolledToBottom() {
+ if (!isInSettings()) {
+ return mStatusBar.getBarState() == StatusBarState.KEYGUARD
+ || mNotificationStackScroller.isScrolledToBottom();
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ protected int getMaxPanelHeight() {
+ int min = mStatusBarMinHeight;
+ if (mStatusBar.getBarState() != StatusBarState.KEYGUARD
+ && mNotificationStackScroller.getNotGoneChildCount() == 0) {
+ int minHeight = (int) (mQsMinExpansionHeight + getOverExpansionAmount());
+ min = Math.max(min, minHeight);
+ }
+ int maxHeight;
+ if (mQsExpandImmediate || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) {
+ maxHeight = calculatePanelHeightQsExpanded();
+ } else {
+ maxHeight = calculatePanelHeightShade();
+ }
+ maxHeight = Math.max(maxHeight, min);
+ return maxHeight;
+ }
+
+ public boolean isInSettings() {
+ return mQsExpanded;
+ }
+
+ public boolean isExpanding() {
+ return mIsExpanding;
+ }
+
+ @Override
+ protected void onHeightUpdated(float expandedHeight) {
+ if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) {
+ positionClockAndNotifications();
+ }
+ if (mQsExpandImmediate || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
+ && !mQsExpansionFromOverscroll) {
+ 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.getLayoutMinHeight();
+ float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
+ t = (expandedHeight - panelHeightQsCollapsed)
+ / (panelHeightQsExpanded - panelHeightQsCollapsed);
+ }
+ setQsExpansion(mQsMinExpansionHeight
+ + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight));
+ }
+ updateExpandedHeight(expandedHeight);
+ updateHeader();
+ updateUnlockIcon();
+ updateNotificationTranslucency();
+ updatePanelExpanded();
+ mNotificationStackScroller.setShadeExpanded(!isFullyCollapsed());
+ if (DEBUG) {
+ invalidate();
+ }
+ }
+
+ private void updatePanelExpanded() {
+ boolean isExpanded = !isFullyCollapsed();
+ if (mPanelExpanded != isExpanded) {
+ mHeadsUpManager.setIsExpanded(isExpanded);
+ mStatusBar.setPanelExpanded(isExpanded);
+ mPanelExpanded = isExpanded;
+ }
+ }
+
+ /**
+ * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when
+ * collapsing QS / the panel when QS was scrolled
+ */
+ private int getTempQsMaxExpansion() {
+ return mQsMaxExpansionHeight;
+ }
+
+ private int calculatePanelHeightShade() {
+ int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin();
+ int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin
+ - mTopPaddingAdjustment;
+ maxHeight += mNotificationStackScroller.getTopPaddingOverflow();
+ return maxHeight;
+ }
+
+ private int calculatePanelHeightQsExpanded() {
+ float notificationHeight = mNotificationStackScroller.getHeight()
+ - mNotificationStackScroller.getEmptyBottomMargin()
+ - mNotificationStackScroller.getTopPadding();
+
+ // 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
+ && mShowEmptyShadeView) {
+ notificationHeight = mNotificationStackScroller.getEmptyShadeViewHeight();
+ }
+ 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, mStatusBarState == StatusBarState.KEYGUARD
+ ? mClockPositionResult.stackScrollerPadding - mTopPaddingAdjustment
+ : 0)
+ + notificationHeight + mNotificationStackScroller.getTopPaddingOverflow();
+ if (totalHeight > mNotificationStackScroller.getHeight()) {
+ float fullyCollapsedHeight = maxQsHeight
+ + mNotificationStackScroller.getLayoutMinHeight();
+ totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight());
+ }
+ return (int) totalHeight;
+ }
+
+ private void updateNotificationTranslucency() {
+ float alpha = 1f;
+ if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp && !mHeadsUpManager.hasPinnedHeadsUp()) {
+ alpha = getFadeoutAlpha();
+ }
+ mNotificationStackScroller.setAlpha(alpha);
+ }
+
+ private float getFadeoutAlpha() {
+ float alpha = (getNotificationsTopY() + mNotificationStackScroller.getFirstItemMinHeight())
+ / mQsMinExpansionHeight;
+ alpha = Math.max(0, Math.min(alpha, 1));
+ alpha = (float) Math.pow(alpha, 0.75);
+ return alpha;
+ }
+
+ @Override
+ protected float getOverExpansionAmount() {
+ return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */);
+ }
+
+ @Override
+ protected float getOverExpansionPixels() {
+ return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */);
+ }
+
+ private void updateUnlockIcon() {
+ if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
+ || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
+ boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance;
+ KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
+ if (active && !mUnlockIconActive && mTracking) {
+ lockIcon.setImageAlpha(1.0f, true, 150, Interpolators.FAST_OUT_LINEAR_IN, null);
+ lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150,
+ Interpolators.FAST_OUT_LINEAR_IN);
+ } else if (!active && mUnlockIconActive && mTracking) {
+ lockIcon.setImageAlpha(lockIcon.getRestingAlpha(), true /* animate */,
+ 150, Interpolators.FAST_OUT_LINEAR_IN, null);
+ lockIcon.setImageScale(1.0f, true, 150,
+ Interpolators.FAST_OUT_LINEAR_IN);
+ }
+ mUnlockIconActive = active;
+ }
+ }
+
+ /**
+ * Hides the header when notifications are colliding with it.
+ */
+ private void updateHeader() {
+ if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+ updateHeaderKeyguardAlpha();
+ }
+ updateQsExpansion();
+ }
+
+ protected float getHeaderTranslation() {
+ if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+ return 0;
+ }
+ float translation = NotificationUtils.interpolate(-mQsMinExpansionHeight, 0,
+ mNotificationStackScroller.getAppearFraction(mExpandedHeight));
+ return Math.min(0, translation);
+ }
+
+ /**
+ * @return the alpha to be used to fade out the contents on Keyguard (status bar, bottom area)
+ * during swiping up
+ */
+ private float getKeyguardContentsAlpha() {
+ float alpha;
+ if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+
+ // When on Keyguard, we hide the header as soon as the top card of the notification
+ // stack scroller is close enough (collision distance) to the bottom of the header.
+ alpha = getNotificationsTopY()
+ /
+ (mKeyguardStatusBar.getHeight() + mNotificationsHeaderCollideDistance);
+ } else {
+
+ // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
+ // soon as we start translating the stack.
+ alpha = getNotificationsTopY() / mKeyguardStatusBar.getHeight();
+ }
+ alpha = MathUtils.constrain(alpha, 0, 1);
+ alpha = (float) Math.pow(alpha, 0.75);
+ return alpha;
+ }
+
+ private void updateHeaderKeyguardAlpha() {
+ float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2);
+ mKeyguardStatusBar.setAlpha(Math.min(getKeyguardContentsAlpha(), alphaQsExpansion)
+ * mKeyguardStatusBarAnimateAlpha);
+ mKeyguardStatusBar.setVisibility(mKeyguardStatusBar.getAlpha() != 0f
+ && !mDozing ? VISIBLE : INVISIBLE);
+ }
+
+ private void updateKeyguardBottomAreaAlpha() {
+ float alpha = Math.min(getKeyguardContentsAlpha(), 1 - getQsExpansionFraction());
+ mKeyguardBottomArea.setAlpha(alpha);
+ mKeyguardBottomArea.setImportantForAccessibility(alpha == 0f
+ ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ View ambientIndicationContainer = mStatusBar.getAmbientIndicationContainer();
+ if (ambientIndicationContainer != null) {
+ ambientIndicationContainer.setAlpha(alpha);
+ }
+ }
+
+ private float getNotificationsTopY() {
+ if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
+ return getExpandedHeight();
+ }
+ return mNotificationStackScroller.getNotificationsTopY();
+ }
+
+ @Override
+ protected void onExpandingStarted() {
+ super.onExpandingStarted();
+ mNotificationStackScroller.onExpansionStarted();
+ mIsExpanding = true;
+ mQsExpandedWhenExpandingStarted = mQsFullyExpanded;
+ if (mQsExpanded) {
+ onQsExpansionStarted();
+ }
+ // Since there are QS tiles in the header now, we need to make sure we start listening
+ // immediately so they can be up to date.
+ if (mQs == null) return;
+ mQs.setHeaderListening(true);
+ }
+
+ @Override
+ protected void onExpandingFinished() {
+ super.onExpandingFinished();
+ mNotificationStackScroller.onExpansionStopped();
+ mHeadsUpManager.onExpandingFinished();
+ mIsExpanding = false;
+ if (isFullyCollapsed()) {
+ DejankUtils.postAfterTraversal(new Runnable() {
+ @Override
+ public void run() {
+ setListening(false);
+ }
+ });
+
+ // Workaround b/22639032: Make sure we invalidate something because else RenderThread
+ // thinks we are actually drawing a frame put in reality we don't, so RT doesn't go
+ // ahead with rendering and we jank.
+ postOnAnimation(new Runnable() {
+ @Override
+ public void run() {
+ getParent().invalidateChild(NotificationPanelView.this, mDummyDirtyRect);
+ }
+ });
+ } else {
+ setListening(true);
+ }
+ mQsExpandImmediate = false;
+ mTwoFingerQsExpandPossible = false;
+ mIsExpansionFromHeadsUp = false;
+ mNotificationStackScroller.setTrackingHeadsUp(false);
+ mExpandingFromHeadsUp = false;
+ setPanelScrimMinFraction(0.0f);
+ }
+
+ private void setListening(boolean listening) {
+ mKeyguardStatusBar.setListening(listening);
+ if (mQs == null) return;
+ mQs.setListening(listening);
+ }
+
+ @Override
+ public void expand(boolean animate) {
+ super.expand(animate);
+ setListening(true);
+ }
+
+ @Override
+ protected void setOverExpansion(float overExpansion, boolean isPixels) {
+ if (mConflictingQsExpansionGesture || mQsExpandImmediate) {
+ return;
+ }
+ if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
+ mNotificationStackScroller.setOnHeightChangedListener(null);
+ if (isPixels) {
+ mNotificationStackScroller.setOverScrolledPixels(
+ overExpansion, true /* onTop */, false /* animate */);
+ } else {
+ mNotificationStackScroller.setOverScrollAmount(
+ overExpansion, true /* onTop */, false /* animate */);
+ }
+ mNotificationStackScroller.setOnHeightChangedListener(this);
+ }
+ }
+
+ @Override
+ protected void onTrackingStarted() {
+ mFalsingManager.onTrackingStarted();
+ super.onTrackingStarted();
+ if (mQsFullyExpanded) {
+ mQsExpandImmediate = true;
+ }
+ if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
+ || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
+ mAffordanceHelper.animateHideLeftRightIcon();
+ }
+ mNotificationStackScroller.onPanelTrackingStarted();
+ }
+
+ @Override
+ protected void onTrackingStopped(boolean expand) {
+ mFalsingManager.onTrackingStopped();
+ super.onTrackingStopped(expand);
+ if (expand) {
+ mNotificationStackScroller.setOverScrolledPixels(
+ 0.0f, true /* onTop */, true /* animate */);
+ }
+ mNotificationStackScroller.onPanelTrackingStopped();
+ if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
+ || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
+ if (!mHintAnimationRunning) {
+ mAffordanceHelper.reset(true);
+ }
+ }
+ if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
+ || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
+ KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
+ lockIcon.setImageAlpha(0.0f, true, 100, Interpolators.FAST_OUT_LINEAR_IN, null);
+ lockIcon.setImageScale(2.0f, true, 100, Interpolators.FAST_OUT_LINEAR_IN);
+ }
+ }
+
+ @Override
+ public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
+
+ // Block update if we are in quick settings and just the top padding changed
+ // (i.e. view == null).
+ if (view == null && mQsExpanded) {
+ return;
+ }
+ ExpandableView firstChildNotGone = mNotificationStackScroller.getFirstChildNotGone();
+ ExpandableNotificationRow firstRow = firstChildNotGone instanceof ExpandableNotificationRow
+ ? (ExpandableNotificationRow) firstChildNotGone
+ : null;
+ if (firstRow != null
+ && (view == firstRow || (firstRow.getNotificationParent() == firstRow))) {
+ requestScrollerTopPaddingUpdate(false);
+ }
+ requestPanelHeightUpdate();
+ }
+
+ @Override
+ public void onReset(ExpandableView view) {
+ }
+
+ public void onQsHeightChanged() {
+ mQsMaxExpansionHeight = mQs != null ? mQs.getDesiredHeight() : 0;
+ if (mQsExpanded && mQsFullyExpanded) {
+ mQsExpansionHeight = mQsMaxExpansionHeight;
+ requestScrollerTopPaddingUpdate(false /* animate */);
+ requestPanelHeightUpdate();
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mAffordanceHelper.onConfigurationChanged();
+ if (newConfig.orientation != mLastOrientation) {
+ resetVerticalPanelPosition();
+ }
+ mLastOrientation = newConfig.orientation;
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ mNavigationBarBottomHeight = insets.getStableInsetBottom();
+ updateMaxHeadsUpTranslation();
+ return insets;
+ }
+
+ private void updateMaxHeadsUpTranslation() {
+ mNotificationStackScroller.setHeadsUpBoundaries(getHeight(), mNavigationBarBottomHeight);
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ if (layoutDirection != mOldLayoutDirection) {
+ mAffordanceHelper.onRtlPropertiesChanged();
+ mOldLayoutDirection = layoutDirection;
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.expand_indicator) {
+ onQsExpansionStarted();
+ if (mQsExpanded) {
+ flingSettings(0 /* vel */, false /* expand */, null, true /* isClick */);
+ } else if (mQsExpansionEnabled) {
+ mLockscreenGestureLogger.write(MetricsEvent.ACTION_SHADE_QS_TAP, 0, 0);
+ flingSettings(0 /* vel */, true /* expand */, null, true /* isClick */);
+ }
+ }
+ }
+
+ @Override
+ 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) {
+ mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_DIALER, lengthDp, velocityDp);
+
+ mFalsingManager.onLeftAffordanceOn();
+ if (mFalsingManager.shouldEnforceBouncer()) {
+ mStatusBar.executeRunnableDismissingKeyguard(new Runnable() {
+ @Override
+ public void run() {
+ mKeyguardBottomArea.launchLeftAffordance();
+ }
+ }, null, true /* dismissShade */, false /* afterKeyguardGone */,
+ true /* deferred */);
+ }
+ else {
+ mKeyguardBottomArea.launchLeftAffordance();
+ }
+ } else {
+ if (KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE.equals(
+ mLastCameraLaunchSource)) {
+ mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_CAMERA, lengthDp, velocityDp);
+ }
+ mFalsingManager.onCameraOn();
+ if (mFalsingManager.shouldEnforceBouncer()) {
+ mStatusBar.executeRunnableDismissingKeyguard(new Runnable() {
+ @Override
+ public void run() {
+ mKeyguardBottomArea.launchCamera(mLastCameraLaunchSource);
+ }
+ }, null, true /* dismissShade */, false /* afterKeyguardGone */,
+ true /* deferred */);
+ }
+ else {
+ mKeyguardBottomArea.launchCamera(mLastCameraLaunchSource);
+ }
+ }
+ mStatusBar.startLaunchTransitionTimeout();
+ mBlockTouches = true;
+ }
+
+ @Override
+ public void onAnimationToSideEnded() {
+ mIsLaunchTransitionRunning = false;
+ mIsLaunchTransitionFinished = true;
+ if (mLaunchAnimationEndRunnable != null) {
+ mLaunchAnimationEndRunnable.run();
+ mLaunchAnimationEndRunnable = null;
+ }
+ mStatusBar.readyForKeyguardDone();
+ }
+
+ @Override
+ protected void startUnlockHintAnimation() {
+ if (mPowerManager.isPowerSaveMode()) {
+ onUnlockHintStarted();
+ onUnlockHintFinished();
+ return;
+ }
+ super.startUnlockHintAnimation();
+ startHighlightIconAnimation(getCenterIcon());
+ }
+
+ /**
+ * Starts the highlight (making it fully opaque) animation on an icon.
+ */
+ private void startHighlightIconAnimation(final KeyguardAffordanceView icon) {
+ icon.setImageAlpha(1.0f, true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
+ Interpolators.FAST_OUT_SLOW_IN, new Runnable() {
+ @Override
+ public void run() {
+ icon.setImageAlpha(icon.getRestingAlpha(),
+ true /* animate */, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
+ Interpolators.FAST_OUT_SLOW_IN, null);
+ }
+ });
+ }
+
+ @Override
+ public float getMaxTranslationDistance() {
+ return (float) Math.hypot(getWidth(), getHeight());
+ }
+
+ @Override
+ public void onSwipingStarted(boolean rightIcon) {
+ mFalsingManager.onAffordanceSwipingStarted(rightIcon);
+ boolean camera = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? !rightIcon
+ : rightIcon;
+ if (camera) {
+ mKeyguardBottomArea.bindCameraPrewarmService();
+ }
+ requestDisallowInterceptTouchEvent(true);
+ mOnlyAffordanceInThisMotion = true;
+ mQsTracking = false;
+ }
+
+ @Override
+ public void onSwipingAborted() {
+ mFalsingManager.onAffordanceSwipingAborted();
+ mKeyguardBottomArea.unbindCameraPrewarmService(false /* launched */);
+ }
+
+ @Override
+ public void onIconClicked(boolean rightIcon) {
+ if (mHintAnimationRunning) {
+ return;
+ }
+ mHintAnimationRunning = true;
+ mAffordanceHelper.startHintAnimation(rightIcon, new Runnable() {
+ @Override
+ public void run() {
+ mHintAnimationRunning = false;
+ mStatusBar.onHintFinished();
+ }
+ });
+ rightIcon = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? !rightIcon : rightIcon;
+ if (rightIcon) {
+ mStatusBar.onCameraHintStarted();
+ } else {
+ if (mKeyguardBottomArea.isLeftVoiceAssist()) {
+ mStatusBar.onVoiceAssistHintStarted();
+ } else {
+ mStatusBar.onPhoneHintStarted();
+ }
+ }
+ }
+
+ @Override
+ protected void onUnlockHintFinished() {
+ super.onUnlockHintFinished();
+ mNotificationStackScroller.setUnlockHintRunning(false);
+ }
+
+ @Override
+ protected void onUnlockHintStarted() {
+ super.onUnlockHintStarted();
+ mNotificationStackScroller.setUnlockHintRunning(true);
+ }
+
+ @Override
+ public KeyguardAffordanceView getLeftIcon() {
+ return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+ ? mKeyguardBottomArea.getRightView()
+ : mKeyguardBottomArea.getLeftView();
+ }
+
+ @Override
+ public KeyguardAffordanceView getCenterIcon() {
+ return mKeyguardBottomArea.getLockIcon();
+ }
+
+ @Override
+ public KeyguardAffordanceView getRightIcon() {
+ return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+ ? mKeyguardBottomArea.getLeftView()
+ : mKeyguardBottomArea.getRightView();
+ }
+
+ @Override
+ public View getLeftPreview() {
+ return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+ ? mKeyguardBottomArea.getRightPreview()
+ : mKeyguardBottomArea.getLeftPreview();
+ }
+
+ @Override
+ public View getRightPreview() {
+ return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+ ? mKeyguardBottomArea.getLeftPreview()
+ : mKeyguardBottomArea.getRightPreview();
+ }
+
+ @Override
+ public float getAffordanceFalsingFactor() {
+ return mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+ }
+
+ @Override
+ public boolean needsAntiFalsing() {
+ return mStatusBarState == StatusBarState.KEYGUARD;
+ }
+
+ @Override
+ protected float getPeekHeight() {
+ if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
+ return mNotificationStackScroller.getPeekHeight();
+ } else {
+ return mQsMinExpansionHeight;
+ }
+ }
+
+ @Override
+ protected boolean shouldUseDismissingAnimation() {
+ return mStatusBarState != StatusBarState.SHADE
+ && (!mStatusBar.isKeyguardCurrentlySecure() || !isTracking());
+ }
+
+ @Override
+ protected boolean fullyExpandedClearAllVisible() {
+ return mNotificationStackScroller.isDismissViewNotGone()
+ && mNotificationStackScroller.isScrolledToBottom() && !mQsExpandImmediate;
+ }
+
+ @Override
+ protected boolean isClearAllVisible() {
+ return mNotificationStackScroller.isDismissViewVisible();
+ }
+
+ @Override
+ protected int getClearAllHeight() {
+ return mNotificationStackScroller.getDismissViewHeight();
+ }
+
+ @Override
+ protected boolean isTrackingBlocked() {
+ return mConflictingQsExpansionGesture && mQsExpanded;
+ }
+
+ public boolean isQsExpanded() {
+ return mQsExpanded;
+ }
+
+ public boolean isQsDetailShowing() {
+ return mQs.isShowingDetail();
+ }
+
+ public void closeQsDetail() {
+ mQs.closeDetail();
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return true;
+ }
+
+ public boolean isLaunchTransitionFinished() {
+ return mIsLaunchTransitionFinished;
+ }
+
+ public boolean isLaunchTransitionRunning() {
+ return mIsLaunchTransitionRunning;
+ }
+
+ public void setLaunchTransitionEndRunnable(Runnable r) {
+ mLaunchAnimationEndRunnable = r;
+ }
+
+ public void setEmptyDragAmount(float amount) {
+ float factor = 0.8f;
+ if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
+ factor = 0.4f;
+ } else if (!mStatusBar.hasActiveNotifications()) {
+ factor = 0.4f;
+ }
+ mEmptyDragAmount = amount * factor;
+ positionClockAndNotifications();
+ }
+
+ private static float interpolate(float t, float start, float end) {
+ return (1 - t) * start + t * end;
+ }
+
+ public void setDozing(boolean dozing, boolean animate) {
+ if (dozing == mDozing) return;
+ mDozing = dozing;
+ if (mStatusBarState == StatusBarState.KEYGUARD) {
+ updateDozingVisibilities(animate);
+ }
+ }
+
+ private void updateDozingVisibilities(boolean animate) {
+ if (mDozing) {
+ mKeyguardStatusBar.setVisibility(View.INVISIBLE);
+ mKeyguardBottomArea.setDozing(mDozing, animate);
+ } else {
+ mKeyguardStatusBar.setVisibility(View.VISIBLE);
+ mKeyguardBottomArea.setDozing(mDozing, animate);
+ if (animate) {
+ animateKeyguardStatusBarIn(DOZE_ANIMATION_DURATION);
+ }
+ }
+ }
+
+ @Override
+ public boolean isDozing() {
+ return mDozing;
+ }
+
+ public void showEmptyShadeView(boolean emptyShadeViewVisible) {
+ mShowEmptyShadeView = emptyShadeViewVisible;
+ updateEmptyShadeView();
+ }
+
+ private void updateEmptyShadeView() {
+
+ // Hide "No notifications" in QS.
+ mNotificationStackScroller.updateEmptyShadeView(mShowEmptyShadeView && !mQsExpanded);
+ }
+
+ public void setQsScrimEnabled(boolean qsScrimEnabled) {
+ boolean changed = mQsScrimEnabled != qsScrimEnabled;
+ mQsScrimEnabled = qsScrimEnabled;
+ if (changed) {
+ updateQsState();
+ }
+ }
+
+ public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
+ mKeyguardUserSwitcher = keyguardUserSwitcher;
+ }
+
+ public void onScreenTurningOn() {
+ mKeyguardStatusView.refreshTime();
+ }
+
+ @Override
+ public void onEmptySpaceClicked(float x, float y) {
+ onEmptySpaceClick(x);
+ }
+
+ @Override
+ protected boolean onMiddleClicked() {
+ switch (mStatusBar.getBarState()) {
+ case StatusBarState.KEYGUARD:
+ if (!mDozingOnDown) {
+ mLockscreenGestureLogger.write(
+ MetricsEvent.ACTION_LS_HINT,
+ 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
+ startUnlockHintAnimation();
+ }
+ return true;
+ case StatusBarState.SHADE_LOCKED:
+ if (!mQsExpanded) {
+ mStatusBar.goToKeyguard();
+ }
+ return true;
+ case StatusBarState.SHADE:
+
+ // This gets called in the middle of the touch handling, where the state is still
+ // that we are tracking the panel. Collapse the panel after this is done.
+ post(mPostCollapseRunnable);
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ @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);
+ }
+ }
+
+ @Override
+ public void onHeadsUpPinnedModeChanged(final boolean inPinnedMode) {
+ mNotificationStackScroller.setInHeadsUpPinnedMode(inPinnedMode);
+ if (inPinnedMode) {
+ mHeadsUpExistenceChangedRunnable.run();
+ updateNotificationTranslucency();
+ } else {
+ setHeadsUpAnimatingAway(true);
+ mNotificationStackScroller.runAfterAnimationFinished(
+ mHeadsUpExistenceChangedRunnable);
+ }
+ }
+
+ public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
+ mHeadsUpAnimatingAway = headsUpAnimatingAway;
+ mNotificationStackScroller.setHeadsUpAnimatingAway(headsUpAnimatingAway);
+ }
+
+ @Override
+ public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
+ mNotificationStackScroller.generateHeadsUpAnimation(headsUp, true);
+ }
+
+ @Override
+ public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
+ }
+
+ @Override
+ public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+ mNotificationStackScroller.generateHeadsUpAnimation(entry.row, isHeadsUp);
+ }
+
+ @Override
+ public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ super.setHeadsUpManager(headsUpManager);
+ mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager, mNotificationStackScroller,
+ this);
+ }
+
+ public void setTrackingHeadsUp(boolean tracking) {
+ if (tracking) {
+ mNotificationStackScroller.setTrackingHeadsUp(true);
+ mExpandingFromHeadsUp = true;
+ }
+ // otherwise we update the state when the expansion is finished
+ }
+
+ @Override
+ protected void onClosingFinished() {
+ super.onClosingFinished();
+ resetVerticalPanelPosition();
+ setClosingWithAlphaFadeout(false);
+ }
+
+ private void setClosingWithAlphaFadeout(boolean closing) {
+ mClosingWithAlphaFadeOut = closing;
+ mNotificationStackScroller.forceNoOverlappingRendering(closing);
+ }
+
+ /**
+ * Updates the vertical position of the panel so it is positioned closer to the touch
+ * responsible for opening the panel.
+ *
+ * @param x the x-coordinate the touch event
+ */
+ protected void updateVerticalPanelPosition(float x) {
+ if (mNotificationStackScroller.getWidth() * 1.75f > getWidth()) {
+ resetVerticalPanelPosition();
+ return;
+ }
+ float leftMost = mPositionMinSideMargin + mNotificationStackScroller.getWidth() / 2;
+ float rightMost = getWidth() - mPositionMinSideMargin
+ - mNotificationStackScroller.getWidth() / 2;
+ if (Math.abs(x - getWidth() / 2) < mNotificationStackScroller.getWidth() / 4) {
+ x = getWidth() / 2;
+ }
+ x = Math.min(rightMost, Math.max(leftMost, x));
+ setVerticalPanelTranslation(x -
+ (mNotificationStackScroller.getLeft() + mNotificationStackScroller.getWidth() / 2));
+ }
+
+ private void resetVerticalPanelPosition() {
+ setVerticalPanelTranslation(0f);
+ }
+
+ protected void setVerticalPanelTranslation(float translation) {
+ mNotificationStackScroller.setTranslationX(translation);
+ mQsFrame.setTranslationX(translation);
+ }
+
+ protected void updateExpandedHeight(float expandedHeight) {
+ if (mTracking) {
+ mNotificationStackScroller.setExpandingVelocity(getCurrentExpandVelocity());
+ }
+ mNotificationStackScroller.setExpandedHeight(expandedHeight);
+ updateKeyguardBottomAreaAlpha();
+ updateStatusBarIcons();
+ }
+
+ /**
+ * @return whether the notifications are displayed full width and don't have any margins on
+ * the side.
+ */
+ public boolean isFullWidth() {
+ return mIsFullWidth;
+ }
+
+ private void updateStatusBarIcons() {
+ boolean showIconsWhenExpanded = isFullWidth() && getExpandedHeight() < getOpeningHeight();
+ if (showIconsWhenExpanded && mNoVisibleNotifications && isOnKeyguard()) {
+ showIconsWhenExpanded = false;
+ }
+ if (showIconsWhenExpanded != mShowIconsWhenExpanded) {
+ mShowIconsWhenExpanded = showIconsWhenExpanded;
+ mStatusBar.recomputeDisableFlags(false);
+ }
+ }
+
+ private boolean isOnKeyguard() {
+ return mStatusBar.getBarState() == StatusBarState.KEYGUARD;
+ }
+
+ public void setPanelScrimMinFraction(float minFraction) {
+ mBar.panelScrimMinFractionChanged(minFraction);
+ }
+
+ public void clearNotificationEffects() {
+ mStatusBar.clearNotificationEffects();
+ }
+
+ @Override
+ protected boolean isPanelVisibleBecauseOfHeadsUp() {
+ return mHeadsUpManager.hasPinnedHeadsUp() || mHeadsUpAnimatingAway;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return !mDozing;
+ }
+
+ public void launchCamera(boolean animate, int source) {
+ if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) {
+ mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP;
+ } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE) {
+ mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_WIGGLE;
+ } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER) {
+ mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER;
+ } else {
+
+ // Default.
+ mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
+ }
+
+ // If we are launching it when we are occluded already we don't want it to animate,
+ // nor setting these flags, since the occluded state doesn't change anymore, hence it's
+ // never reset.
+ if (!isFullyCollapsed()) {
+ mLaunchingAffordance = true;
+ setLaunchingAffordance(true);
+ } else {
+ animate = false;
+ }
+ mAffordanceHelper.launchAffordance(animate, getLayoutDirection() == LAYOUT_DIRECTION_RTL);
+ }
+
+ public void onAffordanceLaunchEnded() {
+ mLaunchingAffordance = false;
+ setLaunchingAffordance(false);
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ super.setAlpha(alpha);
+ updateFullyVisibleState(false /* forceNotFullyVisible */);
+ }
+
+ /**
+ * Must be called before starting a ViewPropertyAnimator alpha animation because those
+ * do NOT call setAlpha and therefore don't properly update the fullyVisibleState.
+ */
+ public void notifyStartFading() {
+ updateFullyVisibleState(true /* forceNotFullyVisible */);
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ updateFullyVisibleState(false /* forceNotFullyVisible */);
+ }
+
+ private void updateFullyVisibleState(boolean forceNotFullyVisible) {
+ mNotificationStackScroller.setParentNotFullyVisible(forceNotFullyVisible
+ || getAlpha() != 1.0f
+ || getVisibility() != VISIBLE);
+ }
+
+ /**
+ * Set whether we are currently launching an affordance. This is currently only set when
+ * launched via a camera gesture.
+ */
+ private void setLaunchingAffordance(boolean launchingAffordance) {
+ getLeftIcon().setLaunchingAffordance(launchingAffordance);
+ getRightIcon().setLaunchingAffordance(launchingAffordance);
+ getCenterIcon().setLaunchingAffordance(launchingAffordance);
+ }
+
+ /**
+ * Whether the camera application can be launched for the camera launch gesture.
+ *
+ * @param keyguardIsShowing whether keyguard is being shown
+ */
+ public boolean canCameraGestureBeLaunched(boolean keyguardIsShowing) {
+ if (!mStatusBar.isCameraAllowedByAdmin()) {
+ return false;
+ }
+
+ ResolveInfo resolveInfo = mKeyguardBottomArea.resolveCameraIntent();
+ String packageToLaunch = (resolveInfo == null || resolveInfo.activityInfo == null)
+ ? null : resolveInfo.activityInfo.packageName;
+ return packageToLaunch != null &&
+ (keyguardIsShowing || !isForegroundApp(packageToLaunch)) &&
+ !mAffordanceHelper.isSwipingInProgress();
+ }
+
+ /**
+ * Return true if the applications with the package name is running in foreground.
+ *
+ * @param pkgName application package name.
+ */
+ private boolean isForegroundApp(String pkgName) {
+ ActivityManager am = getContext().getSystemService(ActivityManager.class);
+ List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1);
+ return !tasks.isEmpty() && pkgName.equals(tasks.get(0).topActivity.getPackageName());
+ }
+
+ public void setGroupManager(NotificationGroupManager groupManager) {
+ mGroupManager = groupManager;
+ }
+
+ public boolean hideStatusBarIconsWhenExpanded() {
+ return !isFullWidth() || !mShowIconsWhenExpanded;
+ }
+
+ private final FragmentListener mFragmentListener = new FragmentListener() {
+ @Override
+ public void onFragmentViewCreated(String tag, Fragment fragment) {
+ mQs = (QS) fragment;
+ mQs.setPanelView(NotificationPanelView.this);
+ mQs.setExpandClickListener(NotificationPanelView.this);
+ mQs.setHeaderClickable(mQsExpansionEnabled);
+ mQs.setKeyguardShowing(mKeyguardShowing);
+ mQs.setOverscrolling(mStackScrollerOverscrolling);
+
+ // recompute internal state when qspanel height changes
+ mQs.getView().addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ final int height = bottom - top;
+ final int oldHeight = oldBottom - oldTop;
+ if (height != oldHeight) {
+ onQsHeightChanged();
+ }
+ });
+ mNotificationStackScroller.setQsContainer((ViewGroup) mQs.getView());
+ updateQsExpansion();
+ }
+
+ @Override
+ public void onFragmentViewDestroyed(String tag, Fragment fragment) {
+ // Manual handling of fragment lifecycle is only required because this bridges
+ // non-fragment and fragment code. Once we are using a fragment for the notification
+ // panel, mQs will not need to be null cause it will be tied to the same lifecycle.
+ if (fragment == mQs) {
+ mQs = null;
+ }
+ }
+ };
+
+ @Override
+ public void setTouchDisabled(boolean disabled) {
+ super.setTouchDisabled(disabled);
+ if (disabled && mAffordanceHelper.isSwipingInProgress() && !mIsLaunchTransitionRunning) {
+ mAffordanceHelper.reset(false /* animate */);
+ }
+ }
+
+ public void setDark(boolean dark, boolean animate) {
+ float darkAmount = dark ? 1 : 0;
+ if (mDarkAmount == darkAmount) {
+ return;
+ }
+ if (mDarkAnimator != null && mDarkAnimator.isRunning()) {
+ if (animate && mDarkAmountTarget == darkAmount) {
+ return;
+ } else {
+ mDarkAnimator.cancel();
+ }
+ }
+ mDarkAmountTarget = darkAmount;
+ if (animate) {
+ mDarkAnimator = ObjectAnimator.ofFloat(this, SET_DARK_AMOUNT_PROPERTY, darkAmount);
+ mDarkAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ mDarkAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
+ mDarkAnimator.start();
+ } else {
+ setDarkAmount(darkAmount);
+ }
+ }
+
+ private void setDarkAmount(float amount) {
+ mDarkAmount = amount;
+ mKeyguardStatusView.setDark(mDarkAmount);
+ positionClockAndNotifications();
+ }
+
+ public void setNoVisibleNotifications(boolean noNotifications) {
+ mNoVisibleNotifications = noNotifications;
+ if (mQs != null) {
+ mQs.setHasNotifications(!noNotifications);
+ }
+ }
+
+ public void setPulsing(boolean pulsing) {
+ mKeyguardStatusView.setPulsing(pulsing);
+ }
+
+ public void setAmbientIndicationBottomPadding(int ambientIndicationBottomPadding) {
+ if (mAmbientIndicationBottomPadding != ambientIndicationBottomPadding) {
+ mAmbientIndicationBottomPadding = ambientIndicationBottomPadding;
+ mStatusBar.updateKeyguardMaxNotifications();
+ }
+ }
+
+ public void refreshTime() {
+ mKeyguardStatusView.refreshTime();
+ if (mDarkAmount > 0) {
+ positionClockAndNotifications();
+ }
+ }
+
+ public void setStatusAccessibilityImportance(int mode) {
+ mKeyguardStatusView.setImportantForAccessibility(mode);
+ }
+
+ /**
+ * TODO: this should be removed.
+ * It's not correct to pass this view forward because other classes will end up adding
+ * children to it. Theme will be out of sync.
+ * @return bottom area view
+ */
+ public KeyguardBottomAreaView getKeyguardBottomAreaView() {
+ return mKeyguardBottomArea;
+ }
+
+ public void setUserSetupComplete(boolean userSetupComplete) {
+ mUserSetupComplete = userSetupComplete;
+ mKeyguardBottomArea.setUserSetupComplete(userSetupComplete);
+ }
+
+ public LockIcon getLockIcon() {
+ return mKeyguardBottomArea.getLockIcon();
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java b/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
new file mode 100644
index 0000000..76cc0ff
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
@@ -0,0 +1,196 @@
+/*
+ * 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.app.Fragment;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.support.annotation.DimenRes;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewStub;
+import android.view.ViewStub.OnInflateListener;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.statusbar.NotificationData.Entry;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+/**
+ * The container with notification stack scroller and quick settings inside.
+ */
+public class NotificationsQuickSettingsContainer extends FrameLayout
+ implements OnInflateListener, FragmentListener,
+ AboveShelfObserver.HasViewAboveShelfChangedListener {
+
+ private FrameLayout mQsFrame;
+ private View mUserSwitcher;
+ private NotificationStackScrollLayout mStackScroller;
+ private View mKeyguardStatusBar;
+ private boolean mInflated;
+ private boolean mQsExpanded;
+ private boolean mCustomizerAnimating;
+
+ private int mBottomPadding;
+ private int mStackScrollerMargin;
+ private boolean mHasViewsAboveShelf;
+
+ public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mQsFrame = (FrameLayout) findViewById(R.id.qs_frame);
+ mStackScroller = findViewById(R.id.notification_stack_scroller);
+ mStackScrollerMargin = ((LayoutParams) mStackScroller.getLayoutParams()).bottomMargin;
+ mKeyguardStatusBar = findViewById(R.id.keyguard_header);
+ ViewStub userSwitcher = (ViewStub) findViewById(R.id.keyguard_user_switcher);
+ userSwitcher.setOnInflateListener(this);
+ mUserSwitcher = userSwitcher;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ FragmentHostManager.get(this).addTagListener(QS.TAG, this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ FragmentHostManager.get(this).removeTagListener(QS.TAG, this);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ reloadWidth(mQsFrame, R.dimen.qs_panel_width);
+ reloadWidth(mStackScroller, R.dimen.notification_panel_width);
+ }
+
+ /**
+ * Loads the given width resource and sets it on the given View.
+ */
+ private void reloadWidth(View view, @DimenRes int width) {
+ LayoutParams params = (LayoutParams) view.getLayoutParams();
+ params.width = getResources().getDimensionPixelSize(width);
+ view.setLayoutParams(params);
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ mBottomPadding = insets.getStableInsetBottom();
+ setPadding(0, 0, 0, mBottomPadding);
+ return insets;
+ }
+
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ boolean userSwitcherVisible = mInflated && mUserSwitcher.getVisibility() == View.VISIBLE;
+ boolean statusBarVisible = mKeyguardStatusBar.getVisibility() == View.VISIBLE;
+
+ final boolean qsBottom = mHasViewsAboveShelf;
+ View stackQsTop = qsBottom ? mStackScroller : mQsFrame;
+ View stackQsBottom = !qsBottom ? mStackScroller : mQsFrame;
+ // Invert the order of the scroll view and user switcher such that the notifications receive
+ // touches first but the panel gets drawn above.
+ if (child == mQsFrame) {
+ return super.drawChild(canvas, userSwitcherVisible && statusBarVisible ? mUserSwitcher
+ : statusBarVisible ? mKeyguardStatusBar
+ : userSwitcherVisible ? mUserSwitcher
+ : stackQsBottom, drawingTime);
+ } else if (child == mStackScroller) {
+ return super.drawChild(canvas,
+ userSwitcherVisible && statusBarVisible ? mKeyguardStatusBar
+ : statusBarVisible || userSwitcherVisible ? stackQsBottom
+ : stackQsTop,
+ drawingTime);
+ } else if (child == mUserSwitcher) {
+ return super.drawChild(canvas,
+ userSwitcherVisible && statusBarVisible ? stackQsBottom
+ : stackQsTop,
+ drawingTime);
+ } else if (child == mKeyguardStatusBar) {
+ return super.drawChild(canvas,
+ stackQsTop,
+ drawingTime);
+ } else {
+ return super.drawChild(canvas, child, drawingTime);
+ }
+ }
+
+ @Override
+ public void onInflate(ViewStub stub, View inflated) {
+ if (stub == mUserSwitcher) {
+ mUserSwitcher = inflated;
+ mInflated = true;
+ }
+ }
+
+ @Override
+ public void onFragmentViewCreated(String tag, Fragment fragment) {
+ QS container = (QS) fragment;
+ container.setContainer(this);
+ }
+
+ public void setQsExpanded(boolean expanded) {
+ if (mQsExpanded != expanded) {
+ mQsExpanded = expanded;
+ invalidate();
+ }
+ }
+
+ public void setCustomizerAnimating(boolean isAnimating) {
+ if (mCustomizerAnimating != isAnimating) {
+ mCustomizerAnimating = isAnimating;
+ invalidate();
+ }
+ }
+
+ public void setCustomizerShowing(boolean isShowing) {
+ if (isShowing) {
+ // Clear out bottom paddings/margins so the qs customization can be full height.
+ setPadding(0, 0, 0, 0);
+ setBottomMargin(mStackScroller, 0);
+ } else {
+ setPadding(0, 0, 0, mBottomPadding);
+ setBottomMargin(mStackScroller, mStackScrollerMargin);
+ }
+ mStackScroller.setQsCustomizerShowing(isShowing);
+ }
+
+ private void setBottomMargin(View v, int bottomMargin) {
+ LayoutParams params = (LayoutParams) v.getLayoutParams();
+ params.bottomMargin = bottomMargin;
+ v.setLayoutParams(params);
+ }
+
+ @Override
+ public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
+ mHasViewsAboveShelf = hasViewsAboveShelf;
+ invalidate();
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/ObservableScrollView.java b/com/android/systemui/statusbar/phone/ObservableScrollView.java
new file mode 100644
index 0000000..9e5cefd
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ObservableScrollView.java
@@ -0,0 +1,150 @@
+/*
+ * 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.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ScrollView;
+
+/**
+ * A scroll view which can be observed for scroll change events.
+ */
+public class ObservableScrollView extends ScrollView {
+
+ private Listener mListener;
+ private int mLastOverscrollAmount;
+ private boolean mTouchEnabled = true;
+ private boolean mHandlingTouchEvent;
+ private float mLastX;
+ private float mLastY;
+ private boolean mBlockFlinging;
+ private boolean mTouchCancelled;
+
+ public ObservableScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void setTouchEnabled(boolean touchEnabled) {
+ mTouchEnabled = touchEnabled;
+ }
+
+ public boolean isScrolledToBottom() {
+ return getScrollY() == getMaxScrollY();
+ }
+
+ public boolean isHandlingTouchEvent() {
+ return mHandlingTouchEvent;
+ }
+
+ private int getMaxScrollY() {
+ int scrollRange = 0;
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ scrollRange = Math.max(0,
+ child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
+ }
+ return scrollRange;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ mHandlingTouchEvent = true;
+ mLastX = ev.getX();
+ mLastY = ev.getY();
+ boolean result = super.onTouchEvent(ev);
+ mHandlingTouchEvent = false;
+ return result;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ mHandlingTouchEvent = true;
+ mLastX = ev.getX();
+ mLastY = ev.getY();
+ boolean result = super.onInterceptTouchEvent(ev);
+ mHandlingTouchEvent = false;
+ return result;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ if (!mTouchEnabled) {
+ mTouchCancelled = true;
+ return false;
+ }
+ mTouchCancelled = false;
+ } else if (mTouchCancelled) {
+ return false;
+ } else if (!mTouchEnabled) {
+ MotionEvent cancel = MotionEvent.obtain(ev);
+ cancel.setAction(MotionEvent.ACTION_CANCEL);
+ super.dispatchTouchEvent(cancel);
+ cancel.recycle();
+ mTouchCancelled = true;
+ return false;
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ if (mListener != null) {
+ mListener.onScrollChanged();
+ }
+ }
+
+ @Override
+ protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY,
+ int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY,
+ boolean isTouchEvent) {
+ mLastOverscrollAmount = Math.max(0, scrollY + deltaY - getMaxScrollY());
+ return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
+ maxOverScrollX, maxOverScrollY, isTouchEvent);
+ }
+
+ public void setBlockFlinging(boolean blockFlinging) {
+ mBlockFlinging = blockFlinging;
+ }
+
+ @Override
+ public void fling(int velocityY) {
+ if (!mBlockFlinging) {
+ super.fling(velocityY);
+ }
+ }
+
+ @Override
+ protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
+ super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
+ if (mListener != null && mLastOverscrollAmount > 0) {
+ mListener.onOverscrolled(mLastX, mLastY, mLastOverscrollAmount);
+ }
+ }
+
+ public interface Listener {
+ void onScrollChanged();
+ void onOverscrolled(float lastX, float lastY, int amount);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/PanelBar.java b/com/android/systemui/statusbar/phone/PanelBar.java
new file mode 100644
index 0000000..cefe972
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PanelBar.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2012 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.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+public abstract class PanelBar extends FrameLayout {
+ public static final boolean DEBUG = false;
+ public static final String TAG = PanelBar.class.getSimpleName();
+ private static final boolean SPEW = false;
+
+ public static final void LOG(String fmt, Object... args) {
+ if (!DEBUG) return;
+ Log.v(TAG, String.format(fmt, args));
+ }
+
+ public static final int STATE_CLOSED = 0;
+ public static final int STATE_OPENING = 1;
+ public static final int STATE_OPEN = 2;
+
+ PanelView mPanel;
+ private int mState = STATE_CLOSED;
+ private boolean mTracking;
+
+ public void go(int state) {
+ if (DEBUG) LOG("go state: %d -> %d", mState, state);
+ mState = state;
+ }
+
+ public int getState() {
+ return mState;
+ }
+
+ public PanelBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ }
+
+ public void setPanel(PanelView pv) {
+ mPanel = pv;
+ pv.setBar(this);
+ }
+
+ public void setBouncerShowing(boolean showing) {
+ int important = showing ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ : IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+
+ setImportantForAccessibility(important);
+
+ if (mPanel != null) mPanel.setImportantForAccessibility(important);
+ }
+
+ public boolean panelEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Allow subclasses to implement enable/disable semantics
+ if (!panelEnabled()) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ Log.v(TAG, String.format("onTouch: all panels disabled, ignoring touch at (%d,%d)",
+ (int) event.getX(), (int) event.getY()));
+ }
+ return false;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ final PanelView panel = mPanel;
+ if (panel == null) {
+ // panel is not there, so we'll eat the gesture
+ Log.v(TAG, String.format("onTouch: no panel for touch at (%d,%d)",
+ (int) event.getX(), (int) event.getY()));
+ return true;
+ }
+ boolean enabled = panel.isEnabled();
+ if (DEBUG) LOG("PanelBar.onTouch: state=%d ACTION_DOWN: panel %s %s", mState, panel,
+ (enabled ? "" : " (disabled)"));
+ if (!enabled) {
+ // panel is disabled, so we'll eat the gesture
+ Log.v(TAG, String.format(
+ "onTouch: panel (%s) is disabled, ignoring touch at (%d,%d)",
+ panel, (int) event.getX(), (int) event.getY()));
+ return true;
+ }
+ }
+ return mPanel == null || mPanel.onTouchEvent(event);
+ }
+
+ public abstract void panelScrimMinFractionChanged(float minFraction);
+
+ /**
+ * @param frac the fraction from the expansion in [0, 1]
+ * @param expanded whether the panel is currently expanded; this is independent from the
+ * fraction as the panel also might be expanded if the fraction is 0
+ */
+ public void panelExpansionChanged(float frac, boolean expanded) {
+ boolean fullyClosed = true;
+ boolean fullyOpened = false;
+ if (SPEW) LOG("panelExpansionChanged: start state=%d", mState);
+ PanelView pv = mPanel;
+ pv.setVisibility(expanded ? VISIBLE : INVISIBLE);
+ // adjust any other panels that may be partially visible
+ if (expanded) {
+ if (mState == STATE_CLOSED) {
+ go(STATE_OPENING);
+ onPanelPeeked();
+ }
+ fullyClosed = false;
+ final float thisFrac = pv.getExpandedFraction();
+ if (SPEW) LOG("panelExpansionChanged: -> %s: f=%.1f", pv.getName(), thisFrac);
+ fullyOpened = thisFrac >= 1f;
+ }
+ if (fullyOpened && !mTracking) {
+ go(STATE_OPEN);
+ onPanelFullyOpened();
+ } else if (fullyClosed && !mTracking && mState != STATE_CLOSED) {
+ go(STATE_CLOSED);
+ onPanelCollapsed();
+ }
+
+ if (SPEW) LOG("panelExpansionChanged: end state=%d [%s%s ]", mState,
+ fullyOpened?" fullyOpened":"", fullyClosed?" fullyClosed":"");
+ }
+
+ public void collapsePanel(boolean animate, boolean delayed, float speedUpFactor) {
+ boolean waiting = false;
+ PanelView pv = mPanel;
+ if (animate && !pv.isFullyCollapsed()) {
+ pv.collapse(delayed, speedUpFactor);
+ waiting = true;
+ } else {
+ pv.resetViews();
+ pv.setExpandedFraction(0); // just in case
+ pv.cancelPeek();
+ }
+ if (DEBUG) LOG("collapsePanel: animate=%s waiting=%s", animate, waiting);
+ if (!waiting && mState != STATE_CLOSED) {
+ // it's possible that nothing animated, so we replicate the termination
+ // conditions of panelExpansionChanged here
+ go(STATE_CLOSED);
+ onPanelCollapsed();
+ }
+ }
+
+ public void onPanelPeeked() {
+ if (DEBUG) LOG("onPanelPeeked");
+ }
+
+ public boolean isClosed() {
+ return mState == STATE_CLOSED;
+ }
+
+ public void onPanelCollapsed() {
+ if (DEBUG) LOG("onPanelCollapsed");
+ }
+
+ public void onPanelFullyOpened() {
+ if (DEBUG) LOG("onPanelFullyOpened");
+ }
+
+ public void onTrackingStarted() {
+ mTracking = true;
+ }
+
+ public void onTrackingStopped(boolean expand) {
+ mTracking = false;
+ }
+
+ public void onExpandingFinished() {
+ if (DEBUG) LOG("onExpandingFinished");
+ }
+
+ public void onClosingFinished() {
+
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/PanelView.java b/com/android/systemui/statusbar/phone/PanelView.java
new file mode 100644
index 0000000..afe5c91
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PanelView.java
@@ -0,0 +1,1237 @@
+/*
+ * Copyright (C) 2012 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 static com.android.systemui.statusbar.notification.NotificationUtils.isHapticFeedbackDisabled;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.AsyncTask;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewTreeObserver;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.keyguard.LatencyTracker;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.doze.DozeLog;
+import com.android.systemui.statusbar.FlingAnimationUtils;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public abstract class PanelView extends FrameLayout {
+ public static final boolean DEBUG = PanelBar.DEBUG;
+ public static final String TAG = PanelView.class.getSimpleName();
+ private static final int INITIAL_OPENING_PEEK_DURATION = 200;
+ private static final int PEEK_ANIMATION_DURATION = 360;
+ private long mDownTime;
+ private float mMinExpandHeight;
+ private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
+ private boolean mPanelUpdateWhenAnimatorEnds;
+ private boolean mVibrateOnOpening;
+
+ private final void logf(String fmt, Object... args) {
+ Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
+ }
+
+ protected StatusBar mStatusBar;
+ protected HeadsUpManager mHeadsUpManager;
+
+ private float mPeekHeight;
+ private float mHintDistance;
+ private float mInitialOffsetOnTouch;
+ private boolean mCollapsedAndHeadsUpOnDown;
+ private float mExpandedFraction = 0;
+ protected float mExpandedHeight = 0;
+ private boolean mPanelClosedOnDown;
+ private boolean mHasLayoutedSinceDown;
+ private float mUpdateFlingVelocity;
+ private boolean mUpdateFlingOnLayout;
+ private boolean mPeekTouching;
+ private boolean mJustPeeked;
+ private boolean mClosing;
+ protected boolean mTracking;
+ private boolean mTouchSlopExceeded;
+ private int mTrackingPointer;
+ protected int mTouchSlop;
+ protected boolean mHintAnimationRunning;
+ private boolean mOverExpandedBeforeFling;
+ private boolean mTouchAboveFalsingThreshold;
+ private int mUnlockFalsingThreshold;
+ private boolean mTouchStartedInEmptyArea;
+ private boolean mMotionAborted;
+ private boolean mUpwardsWhenTresholdReached;
+ private boolean mAnimatingOnDown;
+
+ private ValueAnimator mHeightAnimator;
+ private ObjectAnimator mPeekAnimator;
+ private VelocityTrackerInterface mVelocityTracker;
+ private FlingAnimationUtils mFlingAnimationUtils;
+ private FlingAnimationUtils mFlingAnimationUtilsClosing;
+ private FlingAnimationUtils mFlingAnimationUtilsDismissing;
+ private FalsingManager mFalsingManager;
+ private final Vibrator mVibrator;
+
+ /**
+ * Whether an instant expand request is currently pending and we are just waiting for layout.
+ */
+ private boolean mInstantExpanding;
+ private boolean mAnimateAfterExpanding;
+
+ PanelBar mBar;
+
+ private String mViewName;
+ private float mInitialTouchY;
+ private float mInitialTouchX;
+ private boolean mTouchDisabled;
+
+ /**
+ * Whether or not the PanelView can be expanded or collapsed with a drag.
+ */
+ private boolean mNotificationsDragEnabled;
+
+ private Interpolator mBounceInterpolator;
+ protected KeyguardBottomAreaView mKeyguardBottomArea;
+
+ /**
+ * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time.
+ */
+ private float mNextCollapseSpeedUpFactor = 1.0f;
+
+ protected boolean mExpanding;
+ private boolean mGestureWaitForTouchSlop;
+ private boolean mIgnoreXTouchSlop;
+ private boolean mExpandLatencyTracking;
+
+ protected void onExpandingFinished() {
+ mBar.onExpandingFinished();
+ }
+
+ protected void onExpandingStarted() {
+ }
+
+ private void notifyExpandingStarted() {
+ if (!mExpanding) {
+ mExpanding = true;
+ onExpandingStarted();
+ }
+ }
+
+ protected final void notifyExpandingFinished() {
+ endClosing();
+ if (mExpanding) {
+ mExpanding = false;
+ onExpandingFinished();
+ }
+ }
+
+ private void runPeekAnimation(long duration, float peekHeight, boolean collapseWhenFinished) {
+ mPeekHeight = peekHeight;
+ if (DEBUG) logf("peek to height=%.1f", mPeekHeight);
+ if (mHeightAnimator != null) {
+ return;
+ }
+ if (mPeekAnimator != null) {
+ mPeekAnimator.cancel();
+ }
+ mPeekAnimator = ObjectAnimator.ofFloat(this, "expandedHeight", mPeekHeight)
+ .setDuration(duration);
+ mPeekAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ mPeekAnimator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPeekAnimator = null;
+ if (!mCancelled && collapseWhenFinished) {
+ postOnAnimation(mPostCollapseRunnable);
+ }
+
+ }
+ });
+ notifyExpandingStarted();
+ mPeekAnimator.start();
+ mJustPeeked = true;
+ }
+
+ public PanelView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f /* maxLengthSeconds */,
+ 0.6f /* speedUpFactor */);
+ mFlingAnimationUtilsClosing = new FlingAnimationUtils(context, 0.5f /* maxLengthSeconds */,
+ 0.6f /* speedUpFactor */);
+ mFlingAnimationUtilsDismissing = new FlingAnimationUtils(context,
+ 0.5f /* maxLengthSeconds */, 0.2f /* speedUpFactor */, 0.6f /* x2 */,
+ 0.84f /* y2 */);
+ mBounceInterpolator = new BounceInterpolator();
+ mFalsingManager = FalsingManager.getInstance(context);
+ mNotificationsDragEnabled =
+ getResources().getBoolean(R.bool.config_enableNotificationShadeDrag);
+ mVibrator = mContext.getSystemService(Vibrator.class);
+ mVibrateOnOpening = mContext.getResources().getBoolean(
+ R.bool.config_vibrateOnIconAnimation);
+ }
+
+ protected void loadDimens() {
+ final Resources res = getContext().getResources();
+ final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mHintDistance = res.getDimension(R.dimen.hint_move_distance);
+ mUnlockFalsingThreshold = res.getDimensionPixelSize(R.dimen.unlock_falsing_threshold);
+ }
+
+ private void trackMovement(MotionEvent event) {
+ // Add movement to velocity tracker using raw screen X and Y coordinates instead
+ // of window coordinates because the window frame may be moving at the same time.
+ float deltaX = event.getRawX() - event.getX();
+ float deltaY = event.getRawY() - event.getY();
+ event.offsetLocation(deltaX, deltaY);
+ if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
+ event.offsetLocation(-deltaX, -deltaY);
+ }
+
+ public void setTouchDisabled(boolean disabled) {
+ mTouchDisabled = disabled;
+ if (mTouchDisabled) {
+ cancelHeightAnimator();
+ if (mTracking) {
+ onTrackingStopped(true /* expanded */);
+ }
+ notifyExpandingFinished();
+ }
+ }
+
+ public void startExpandLatencyTracking() {
+ if (LatencyTracker.isEnabled(mContext)) {
+ LatencyTracker.getInstance(mContext).onActionStart(
+ LatencyTracker.ACTION_EXPAND_PANEL);
+ mExpandLatencyTracking = true;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mInstantExpanding || mTouchDisabled
+ || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+ return false;
+ }
+
+ // If dragging should not expand the notifications shade, then return false.
+ if (!mNotificationsDragEnabled) {
+ if (mTracking) {
+ // Turn off tracking if it's on or the shade can get stuck in the down position.
+ onTrackingStopped(true /* expand */);
+ }
+ return false;
+ }
+
+ // On expanding, single mouse click expands the panel instead of dragging.
+ if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ expand(true);
+ }
+ return true;
+ }
+
+ /*
+ * We capture touch events here and update the expand height here in case according to
+ * the users fingers. This also handles multi-touch.
+ *
+ * If the user just clicks shortly, we show a quick peek of the shade.
+ *
+ * Flinging is also enabled in order to open or close the shade.
+ */
+
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures();
+ mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);
+ }
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+ mJustPeeked = false;
+ mMinExpandHeight = 0.0f;
+ mPanelClosedOnDown = isFullyCollapsed();
+ mHasLayoutedSinceDown = false;
+ mUpdateFlingOnLayout = false;
+ mMotionAborted = false;
+ mPeekTouching = mPanelClosedOnDown;
+ mDownTime = SystemClock.uptimeMillis();
+ mTouchAboveFalsingThreshold = false;
+ mCollapsedAndHeadsUpOnDown = isFullyCollapsed()
+ && mHeadsUpManager.hasPinnedHeadsUp();
+ if (mVelocityTracker == null) {
+ initVelocityTracker();
+ }
+ trackMovement(event);
+ if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning)
+ || mPeekAnimator != null) {
+ mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning)
+ || mPeekAnimator != null;
+ cancelHeightAnimator();
+ cancelPeek();
+ onTrackingStarted();
+ }
+ if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()) {
+ startOpening();
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ final float newY = event.getY(newIndex);
+ final float newX = event.getX(newIndex);
+ mTrackingPointer = event.getPointerId(newIndex);
+ startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+ mMotionAborted = true;
+ endMotionEvent(event, x, y, true /* forceCancel */);
+ return false;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ trackMovement(event);
+ float h = y - mInitialTouchY;
+
+ // If the panel was collapsed when touching, we only need to check for the
+ // y-component of the gesture, as we have no conflicting horizontal gesture.
+ if (Math.abs(h) > mTouchSlop
+ && (Math.abs(h) > Math.abs(x - mInitialTouchX)
+ || mIgnoreXTouchSlop)) {
+ mTouchSlopExceeded = true;
+ if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
+ if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
+ startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+ h = 0;
+ }
+ cancelHeightAnimator();
+ onTrackingStarted();
+ }
+ }
+ float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
+ if (newHeight > mPeekHeight) {
+ if (mPeekAnimator != null) {
+ mPeekAnimator.cancel();
+ }
+ mJustPeeked = false;
+ } else if (mPeekAnimator == null && mJustPeeked) {
+ // The initial peek has finished, but we haven't dragged as far yet, lets
+ // speed it up by starting at the peek height.
+ mInitialOffsetOnTouch = mExpandedHeight;
+ mInitialTouchY = y;
+ mMinExpandHeight = mExpandedHeight;
+ mJustPeeked = false;
+ }
+ newHeight = Math.max(newHeight, mMinExpandHeight);
+ if (-h >= getFalsingThreshold()) {
+ mTouchAboveFalsingThreshold = true;
+ mUpwardsWhenTresholdReached = isDirectionUpwards(x, y);
+ }
+ if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) &&
+ !isTrackingBlocked()) {
+ setExpandedHeightInternal(newHeight);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ trackMovement(event);
+ endMotionEvent(event, x, y, false /* forceCancel */);
+ break;
+ }
+ return !mGestureWaitForTouchSlop || mTracking;
+ }
+
+ private void startOpening() {;
+ runPeekAnimation(INITIAL_OPENING_PEEK_DURATION, getOpeningHeight(),
+ false /* collapseWhenFinished */);
+ notifyBarPanelExpansionChanged();
+ if (mVibrateOnOpening && !isHapticFeedbackDisabled(mContext)) {
+ AsyncTask.execute(() ->
+ mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_TICK, false)));
+ }
+ }
+
+ protected abstract float getOpeningHeight();
+
+ /**
+ * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
+ * horizontal direction
+ */
+ private boolean isDirectionUpwards(float x, float y) {
+ float xDiff = x - mInitialTouchX;
+ float yDiff = y - mInitialTouchY;
+ if (yDiff >= 0) {
+ return false;
+ }
+ return Math.abs(yDiff) >= Math.abs(xDiff);
+ }
+
+ protected void startExpandingFromPeek() {
+ mStatusBar.handlePeekToExpandTransistion();
+ }
+
+ protected void startExpandMotion(float newX, float newY, boolean startTracking,
+ float expandedHeight) {
+ mInitialOffsetOnTouch = expandedHeight;
+ mInitialTouchY = newY;
+ mInitialTouchX = newX;
+ if (startTracking) {
+ mTouchSlopExceeded = true;
+ setExpandedHeight(mInitialOffsetOnTouch);
+ onTrackingStarted();
+ }
+ }
+
+ private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
+ mTrackingPointer = -1;
+ if ((mTracking && mTouchSlopExceeded)
+ || Math.abs(x - mInitialTouchX) > mTouchSlop
+ || Math.abs(y - mInitialTouchY) > mTouchSlop
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || forceCancel) {
+ float vel = 0f;
+ float vectorVel = 0f;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.computeCurrentVelocity(1000);
+ vel = mVelocityTracker.getYVelocity();
+ vectorVel = (float) Math.hypot(
+ mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+ }
+ boolean expand = flingExpands(vel, vectorVel, x, y)
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || forceCancel;
+ DozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
+ mStatusBar.isFalsingThresholdNeeded(),
+ mStatusBar.isWakeUpComingFromTouch());
+ // 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);
+ mLockscreenGestureLogger.write(
+ MetricsEvent.ACTION_LS_UNLOCK,
+ heightDp, velocityDp);
+ }
+ fling(vel, expand, isFalseTouch(x, y));
+ onTrackingStopped(expand);
+ mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
+ if (mUpdateFlingOnLayout) {
+ mUpdateFlingVelocity = vel;
+ }
+ } else if (mPanelClosedOnDown && !mHeadsUpManager.hasPinnedHeadsUp() && !mTracking) {
+ long timePassed = SystemClock.uptimeMillis() - mDownTime;
+ if (timePassed < ViewConfiguration.getLongPressTimeout()) {
+ // Lets show the user that he can actually expand the panel
+ runPeekAnimation(PEEK_ANIMATION_DURATION, getPeekHeight(), true /* collapseWhenFinished */);
+ } else {
+ // We need to collapse the panel since we peeked to the small height.
+ postOnAnimation(mPostCollapseRunnable);
+ }
+ } else {
+ boolean expands = onEmptySpaceClick(mInitialTouchX);
+ onTrackingStopped(expands);
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ mPeekTouching = false;
+ }
+
+ protected float getCurrentExpandVelocity() {
+ if (mVelocityTracker == null) {
+ return 0;
+ }
+ mVelocityTracker.computeCurrentVelocity(1000);
+ return mVelocityTracker.getYVelocity();
+ }
+
+ private int getFalsingThreshold() {
+ float factor = mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+ return (int) (mUnlockFalsingThreshold * factor);
+ }
+
+ protected abstract boolean hasConflictingGestures();
+
+ protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y);
+
+ protected void onTrackingStopped(boolean expand) {
+ mTracking = false;
+ mBar.onTrackingStopped(expand);
+ notifyBarPanelExpansionChanged();
+ }
+
+ protected void onTrackingStarted() {
+ endClosing();
+ mTracking = true;
+ mBar.onTrackingStarted();
+ notifyExpandingStarted();
+ notifyBarPanelExpansionChanged();
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled
+ || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+ return false;
+ }
+
+ /*
+ * If the user drags anywhere inside the panel we intercept it if the movement is
+ * upwards. This allows closing the shade from anywhere inside the panel.
+ *
+ * We only do this if the current content is scrolled to the bottom,
+ * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture
+ * possible.
+ */
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ boolean scrolledToBottom = isScrolledToBottom();
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mStatusBar.userActivity();
+ mAnimatingOnDown = mHeightAnimator != null;
+ mMinExpandHeight = 0.0f;
+ mDownTime = SystemClock.uptimeMillis();
+ if (mAnimatingOnDown && mClosing && !mHintAnimationRunning
+ || mPeekAnimator != null) {
+ cancelHeightAnimator();
+ cancelPeek();
+ mTouchSlopExceeded = true;
+ return true;
+ }
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ mTouchStartedInEmptyArea = !isInContentBounds(x, y);
+ mTouchSlopExceeded = false;
+ mJustPeeked = false;
+ mMotionAborted = false;
+ mPanelClosedOnDown = isFullyCollapsed();
+ mCollapsedAndHeadsUpOnDown = false;
+ mHasLayoutedSinceDown = false;
+ mUpdateFlingOnLayout = false;
+ mTouchAboveFalsingThreshold = false;
+ initVelocityTracker();
+ trackMovement(event);
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ mTrackingPointer = event.getPointerId(newIndex);
+ mInitialTouchX = event.getX(newIndex);
+ mInitialTouchY = event.getY(newIndex);
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+ mMotionAborted = true;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final float h = y - mInitialTouchY;
+ trackMovement(event);
+ if (scrolledToBottom || mTouchStartedInEmptyArea || mAnimatingOnDown) {
+ float hAbs = Math.abs(h);
+ if ((h < -mTouchSlop || (mAnimatingOnDown && hAbs > mTouchSlop))
+ && hAbs > Math.abs(x - mInitialTouchX)) {
+ cancelHeightAnimator();
+ startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
+ return true;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * @return Whether a pair of coordinates are inside the visible view content bounds.
+ */
+ protected abstract boolean isInContentBounds(float x, float y);
+
+ protected void cancelHeightAnimator() {
+ if (mHeightAnimator != null) {
+ if (mHeightAnimator.isRunning()) {
+ mPanelUpdateWhenAnimatorEnds = false;
+ }
+ mHeightAnimator.cancel();
+ }
+ endClosing();
+ }
+
+ private void endClosing() {
+ if (mClosing) {
+ mClosing = false;
+ onClosingFinished();
+ }
+ }
+
+ private void initVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ }
+ mVelocityTracker = VelocityTrackerFactory.obtain(getContext());
+ }
+
+ protected boolean isScrolledToBottom() {
+ return true;
+ }
+
+ protected float getContentHeight() {
+ return mExpandedHeight;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ loadDimens();
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ loadDimens();
+ }
+
+ /**
+ * @param vel the current vertical velocity of the motion
+ * @param vectorVel the length of the vectorial velocity
+ * @return whether a fling should expands the panel; contracts otherwise
+ */
+ protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
+ if (isFalseTouch(x, y)) {
+ return true;
+ }
+ if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+ return getExpandedFraction() > 0.5f;
+ } else {
+ return vel > 0;
+ }
+ }
+
+ /**
+ * @param x the final x-coordinate when the finger was lifted
+ * @param y the final y-coordinate when the finger was lifted
+ * @return whether this motion should be regarded as a false touch
+ */
+ private boolean isFalseTouch(float x, float y) {
+ if (!mStatusBar.isFalsingThresholdNeeded()) {
+ return false;
+ }
+ if (mFalsingManager.isClassiferEnabled()) {
+ return mFalsingManager.isFalseTouch();
+ }
+ if (!mTouchAboveFalsingThreshold) {
+ return true;
+ }
+ if (mUpwardsWhenTresholdReached) {
+ return false;
+ }
+ return !isDirectionUpwards(x, y);
+ }
+
+ protected void fling(float vel, boolean expand) {
+ fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
+ }
+
+ protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
+ fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
+ }
+
+ protected void fling(float vel, boolean expand, float collapseSpeedUpFactor,
+ boolean expandBecauseOfFalsing) {
+ cancelPeek();
+ float target = expand ? getMaxPanelHeight() : 0;
+ if (!expand) {
+ mClosing = true;
+ }
+ flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+ }
+
+ protected void flingToHeight(float vel, boolean expand, float target,
+ float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
+ // Hack to make the expand transition look nice when clear all button is visible - we make
+ // the animation only to the last notification, and then jump to the maximum panel height so
+ // clear all just fades in and the decelerating motion is towards the last notification.
+ final boolean clearAllExpandHack = expand && fullyExpandedClearAllVisible()
+ && mExpandedHeight < getMaxPanelHeight() - getClearAllHeight()
+ && !isClearAllVisible();
+ if (clearAllExpandHack) {
+ target = getMaxPanelHeight() - getClearAllHeight();
+ }
+ if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) {
+ notifyExpandingFinished();
+ return;
+ }
+ mOverExpandedBeforeFling = getOverExpansionAmount() > 0f;
+ ValueAnimator animator = createHeightAnimator(target);
+ if (expand) {
+ if (expandBecauseOfFalsing && vel < 0) {
+ vel = 0;
+ }
+ mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight());
+ if (vel == 0) {
+ animator.setDuration(350);
+ }
+ } else {
+ if (shouldUseDismissingAnimation()) {
+ if (vel == 0) {
+ animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
+ long duration = (long) (200 + mExpandedHeight / getHeight() * 100);
+ animator.setDuration(duration);
+ } else {
+ mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
+ getHeight());
+ }
+ } else {
+ mFlingAnimationUtilsClosing
+ .apply(animator, mExpandedHeight, target, vel, getHeight());
+ }
+
+ // Make it shorter if we run a canned animation
+ if (vel == 0) {
+ animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
+ }
+ }
+ animator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (clearAllExpandHack && !mCancelled) {
+ setExpandedHeightInternal(getMaxPanelHeight());
+ }
+ setAnimator(null);
+ if (!mCancelled) {
+ notifyExpandingFinished();
+ }
+ notifyBarPanelExpansionChanged();
+ }
+ });
+ setAnimator(animator);
+ animator.start();
+ }
+
+ protected abstract boolean shouldUseDismissingAnimation();
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mViewName = getResources().getResourceName(getId());
+ }
+
+ public String getName() {
+ return mViewName;
+ }
+
+ public void setExpandedHeight(float height) {
+ if (DEBUG) logf("setExpandedHeight(%.1f)", height);
+ setExpandedHeightInternal(height + getOverExpansionPixels());
+ }
+
+ @Override
+ protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mStatusBar.onPanelLaidOut();
+ requestPanelHeightUpdate();
+ mHasLayoutedSinceDown = true;
+ if (mUpdateFlingOnLayout) {
+ abortAnimations();
+ fling(mUpdateFlingVelocity, true /* expands */);
+ mUpdateFlingOnLayout = false;
+ }
+ }
+
+ protected void requestPanelHeightUpdate() {
+ float currentMaxPanelHeight = getMaxPanelHeight();
+
+ if (isFullyCollapsed()) {
+ return;
+ }
+
+ if (currentMaxPanelHeight == mExpandedHeight) {
+ return;
+ }
+
+ if (mPeekAnimator != null || mPeekTouching) {
+ return;
+ }
+
+ if (mTracking && !isTrackingBlocked()) {
+ return;
+ }
+
+ if (mHeightAnimator != null) {
+ mPanelUpdateWhenAnimatorEnds = true;
+ return;
+ }
+
+ setExpandedHeight(currentMaxPanelHeight);
+ }
+
+ public void setExpandedHeightInternal(float h) {
+ if (mExpandLatencyTracking && h != 0f) {
+ DejankUtils.postAfterTraversal(() -> LatencyTracker.getInstance(mContext).onActionEnd(
+ LatencyTracker.ACTION_EXPAND_PANEL));
+ mExpandLatencyTracking = false;
+ }
+ float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount();
+ if (mHeightAnimator == null) {
+ float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion);
+ if (getOverExpansionPixels() != overExpansionPixels && mTracking) {
+ setOverExpansion(overExpansionPixels, true /* isPixels */);
+ }
+ mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount();
+ } else {
+ mExpandedHeight = h;
+ if (mOverExpandedBeforeFling) {
+ setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */);
+ }
+ }
+
+ // If we are closing the panel and we are almost there due to a slow decelerating
+ // interpolator, abort the animation.
+ if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
+ mExpandedHeight = 0f;
+ if (mHeightAnimator != null) {
+ mHeightAnimator.end();
+ }
+ }
+ mExpandedFraction = Math.min(1f,
+ fhWithoutOverExpansion == 0 ? 0 : mExpandedHeight / fhWithoutOverExpansion);
+ onHeightUpdated(mExpandedHeight);
+ notifyBarPanelExpansionChanged();
+ }
+
+ /**
+ * @return true if the panel tracking should be temporarily blocked; this is used when a
+ * conflicting gesture (opening QS) is happening
+ */
+ protected abstract boolean isTrackingBlocked();
+
+ protected abstract void setOverExpansion(float overExpansion, boolean isPixels);
+
+ protected abstract void onHeightUpdated(float expandedHeight);
+
+ protected abstract float getOverExpansionAmount();
+
+ protected abstract float getOverExpansionPixels();
+
+ /**
+ * This returns the maximum height of the panel. Children should override this if their
+ * desired height is not the full height.
+ *
+ * @return the default implementation simply returns the maximum height.
+ */
+ protected abstract int getMaxPanelHeight();
+
+ public void setExpandedFraction(float frac) {
+ setExpandedHeight(getMaxPanelHeight() * frac);
+ }
+
+ public float getExpandedHeight() {
+ return mExpandedHeight;
+ }
+
+ public float getExpandedFraction() {
+ return mExpandedFraction;
+ }
+
+ public boolean isFullyExpanded() {
+ return mExpandedHeight >= getMaxPanelHeight();
+ }
+
+ public boolean isFullyCollapsed() {
+ return mExpandedFraction <= 0.0f;
+ }
+
+ public boolean isCollapsing() {
+ return mClosing;
+ }
+
+ public boolean isTracking() {
+ return mTracking;
+ }
+
+ public void setBar(PanelBar panelBar) {
+ mBar = panelBar;
+ }
+
+ public void collapse(boolean delayed, float speedUpFactor) {
+ if (DEBUG) logf("collapse: " + this);
+ if (canPanelBeCollapsed()) {
+ cancelHeightAnimator();
+ notifyExpandingStarted();
+
+ // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
+ mClosing = true;
+ if (delayed) {
+ mNextCollapseSpeedUpFactor = speedUpFactor;
+ postDelayed(mFlingCollapseRunnable, 120);
+ } else {
+ fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
+ }
+ }
+ }
+
+ public boolean canPanelBeCollapsed() {
+ return !isFullyCollapsed() && !mTracking && !mClosing;
+ }
+
+ private final Runnable mFlingCollapseRunnable = new Runnable() {
+ @Override
+ public void run() {
+ fling(0, false /* expand */, mNextCollapseSpeedUpFactor,
+ false /* expandBecauseOfFalsing */);
+ }
+ };
+
+ public void cancelPeek() {
+ boolean cancelled = false;
+ if (mPeekAnimator != null) {
+ cancelled = true;
+ mPeekAnimator.cancel();
+ }
+
+ if (cancelled) {
+ // When peeking, we already tell mBar that we expanded ourselves. Make sure that we also
+ // notify mBar that we might have closed ourselves.
+ notifyBarPanelExpansionChanged();
+ }
+ }
+
+ public void expand(final boolean animate) {
+ if (!isFullyCollapsed() && !isCollapsing()) {
+ return;
+ }
+
+ mInstantExpanding = true;
+ mAnimateAfterExpanding = animate;
+ mUpdateFlingOnLayout = false;
+ abortAnimations();
+ cancelPeek();
+ if (mTracking) {
+ onTrackingStopped(true /* expands */); // The panel is expanded after this call.
+ }
+ if (mExpanding) {
+ notifyExpandingFinished();
+ }
+ notifyBarPanelExpansionChanged();
+
+ // Wait for window manager to pickup the change, so we know the maximum height of the panel
+ // then.
+ getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (!mInstantExpanding) {
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ return;
+ }
+ if (mStatusBar.getStatusBarWindow().getHeight()
+ != mStatusBar.getStatusBarHeight()) {
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ if (mAnimateAfterExpanding) {
+ notifyExpandingStarted();
+ fling(0, true /* expand */);
+ } else {
+ setExpandedFraction(1f);
+ }
+ mInstantExpanding = false;
+ }
+ }
+ });
+
+ // Make sure a layout really happens.
+ requestLayout();
+ }
+
+ public void instantCollapse() {
+ abortAnimations();
+ setExpandedFraction(0f);
+ if (mExpanding) {
+ notifyExpandingFinished();
+ }
+ if (mInstantExpanding) {
+ mInstantExpanding = false;
+ notifyBarPanelExpansionChanged();
+ }
+ }
+
+ private void abortAnimations() {
+ cancelPeek();
+ 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
+ // the expansion.
+ if (mHeightAnimator != null || mTracking) {
+ return;
+ }
+ cancelPeek();
+ notifyExpandingStarted();
+ startUnlockHintAnimationPhase1(new Runnable() {
+ @Override
+ public void run() {
+ notifyExpandingFinished();
+ onUnlockHintFinished();
+ mHintAnimationRunning = false;
+ }
+ });
+ onUnlockHintStarted();
+ mHintAnimationRunning = true;
+ }
+
+ protected void onUnlockHintFinished() {
+ mStatusBar.onHintFinished();
+ }
+
+ protected void onUnlockHintStarted() {
+ mStatusBar.onUnlockHintStarted();
+ }
+
+ /**
+ * Phase 1: Move everything upwards.
+ */
+ private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
+ float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
+ ValueAnimator animator = createHeightAnimator(target);
+ animator.setDuration(250);
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ animator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCancelled) {
+ setAnimator(null);
+ onAnimationFinished.run();
+ } else {
+ startUnlockHintAnimationPhase2(onAnimationFinished);
+ }
+ }
+ });
+ animator.start();
+ setAnimator(animator);
+
+ View[] viewsToAnimate = {
+ mKeyguardBottomArea.getIndicationArea(),
+ mStatusBar.getAmbientIndicationContainer()};
+ for (View v : viewsToAnimate) {
+ if (v == null) {
+ continue;
+ }
+ v.animate()
+ .translationY(-mHintDistance)
+ .setDuration(250)
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .withEndAction(() -> v.animate()
+ .translationY(0)
+ .setDuration(450)
+ .setInterpolator(mBounceInterpolator)
+ .start())
+ .start();
+ }
+ }
+
+ private void setAnimator(ValueAnimator animator) {
+ mHeightAnimator = animator;
+ if (animator == null && mPanelUpdateWhenAnimatorEnds) {
+ mPanelUpdateWhenAnimatorEnds = false;
+ requestPanelHeightUpdate();
+ }
+ }
+
+ /**
+ * Phase 2: Bounce down.
+ */
+ private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
+ ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
+ animator.setDuration(450);
+ animator.setInterpolator(mBounceInterpolator);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setAnimator(null);
+ onAnimationFinished.run();
+ notifyBarPanelExpansionChanged();
+ }
+ });
+ animator.start();
+ setAnimator(animator);
+ }
+
+ private ValueAnimator createHeightAnimator(float targetHeight) {
+ ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setExpandedHeightInternal((Float) animation.getAnimatedValue());
+ }
+ });
+ return animator;
+ }
+
+ protected void notifyBarPanelExpansionChanged() {
+ mBar.panelExpansionChanged(mExpandedFraction, mExpandedFraction > 0f
+ || mPeekAnimator != null || mInstantExpanding || isPanelVisibleBecauseOfHeadsUp()
+ || mTracking || mHeightAnimator != null);
+ }
+
+ protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
+
+ /**
+ * Gets called when the user performs a click anywhere in the empty area of the panel.
+ *
+ * @return whether the panel will be expanded after the action performed by this method
+ */
+ protected boolean onEmptySpaceClick(float x) {
+ if (mHintAnimationRunning) {
+ return true;
+ }
+ return onMiddleClicked();
+ }
+
+ protected final Runnable mPostCollapseRunnable = new Runnable() {
+ @Override
+ public void run() {
+ collapse(false /* delayed */, 1.0f /* speedUpFactor */);
+ }
+ };
+
+ protected abstract boolean onMiddleClicked();
+
+ protected abstract boolean isDozing();
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
+ + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s touchDisabled=%s"
+ + "]",
+ this.getClass().getSimpleName(),
+ getExpandedHeight(),
+ getMaxPanelHeight(),
+ mClosing?"T":"f",
+ mTracking?"T":"f",
+ mJustPeeked?"T":"f",
+ mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
+ mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":""),
+ mTouchDisabled?"T":"f"
+ ));
+ }
+
+ public abstract void resetViews();
+
+ protected abstract float getPeekHeight();
+ /**
+ * @return whether "Clear all" button will be visible when the panel is fully expanded
+ */
+ protected abstract boolean fullyExpandedClearAllVisible();
+
+ protected abstract boolean isClearAllVisible();
+
+ /**
+ * @return the height of the clear all button, in pixels
+ */
+ protected abstract int getClearAllHeight();
+
+ public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ mHeadsUpManager = headsUpManager;
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
new file mode 100644
index 0000000..4ae1393
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -0,0 +1,785 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.StackId;
+import android.app.ActivityManager.StackInfo;
+import android.app.AlarmManager;
+import android.app.AlarmManager.AlarmClockInfo;
+import android.app.AppGlobals;
+import android.app.Notification;
+import android.app.Notification.Action;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.SynchronousUserSwitchObserver;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.graphics.drawable.Icon;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.service.notification.StatusBarNotification;
+import android.telecom.TelecomManager;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.telephony.IccCardConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.systemui.Dependency;
+import com.android.systemui.DockedStackExistsListener;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.UiOffloadThread;
+import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.qs.tiles.RotationLockTile;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.CommandQueue.Callbacks;
+import com.android.systemui.statusbar.policy.BluetoothController;
+import com.android.systemui.statusbar.policy.BluetoothController.Callback;
+import com.android.systemui.statusbar.policy.CastController;
+import com.android.systemui.statusbar.policy.CastController.CastDevice;
+import com.android.systemui.statusbar.policy.DataSaverController;
+import com.android.systemui.statusbar.policy.DataSaverController.Listener;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+import com.android.systemui.statusbar.policy.HotspotController;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.LocationController;
+import com.android.systemui.statusbar.policy.LocationController.LocationChangeCallback;
+import com.android.systemui.statusbar.policy.NextAlarmController;
+import com.android.systemui.statusbar.policy.RotationLockController;
+import com.android.systemui.statusbar.policy.RotationLockController.RotationLockControllerCallback;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.ZenModeController;
+import com.android.systemui.util.NotificationChannels;
+
+import java.util.List;
+
+/**
+ * This class contains all of the policy about which icons are installed in the status
+ * bar at boot time. It goes through the normal API for icons, even though it probably
+ * strictly doesn't need to.
+ */
+public class PhoneStatusBarPolicy implements Callback, Callbacks,
+ RotationLockControllerCallback, Listener, LocationChangeCallback,
+ ZenModeController.Callback, DeviceProvisionedListener, KeyguardMonitor.Callback {
+ private static final String TAG = "PhoneStatusBarPolicy";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ public static final int LOCATION_STATUS_ICON_ID = R.drawable.stat_sys_location;
+ public static final int NUM_TASKS_FOR_INSTANT_APP_INFO = 5;
+
+ private final String mSlotCast;
+ private final String mSlotHotspot;
+ private final String mSlotBluetooth;
+ private final String mSlotTty;
+ private final String mSlotZen;
+ private final String mSlotVolume;
+ private final String mSlotAlarmClock;
+ private final String mSlotManagedProfile;
+ private final String mSlotRotate;
+ private final String mSlotHeadset;
+ private final String mSlotDataSaver;
+ private final String mSlotLocation;
+
+ private final Context mContext;
+ private final Handler mHandler = new Handler();
+ private final CastController mCast;
+ private final HotspotController mHotspot;
+ private final NextAlarmController mNextAlarm;
+ private final AlarmManager mAlarmManager;
+ private final UserInfoController mUserInfoController;
+ private final UserManager mUserManager;
+ private final StatusBarIconController mIconController;
+ private final RotationLockController mRotationLockController;
+ private final DataSaverController mDataSaver;
+ private final ZenModeController mZenController;
+ private final DeviceProvisionedController mProvisionedController;
+ private final KeyguardMonitor mKeyguardMonitor;
+ private final LocationController mLocationController;
+ private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>();
+ private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
+
+ // Assume it's all good unless we hear otherwise. We don't always seem
+ // to get broadcasts that it *is* there.
+ IccCardConstants.State mSimState = IccCardConstants.State.READY;
+
+ private boolean mZenVisible;
+ private boolean mVolumeVisible;
+ private boolean mCurrentUserSetup;
+ private boolean mDockedStackExists;
+
+ private boolean mManagedProfileIconVisible = false;
+ private boolean mManagedProfileInQuietMode = false;
+
+ private BluetoothController mBluetooth;
+
+ public PhoneStatusBarPolicy(Context context, StatusBarIconController iconController) {
+ mContext = context;
+ mIconController = iconController;
+ mCast = Dependency.get(CastController.class);
+ mHotspot = Dependency.get(HotspotController.class);
+ mBluetooth = Dependency.get(BluetoothController.class);
+ mNextAlarm = Dependency.get(NextAlarmController.class);
+ mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ mUserInfoController = Dependency.get(UserInfoController.class);
+ mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mRotationLockController = Dependency.get(RotationLockController.class);
+ mDataSaver = Dependency.get(DataSaverController.class);
+ mZenController = Dependency.get(ZenModeController.class);
+ mProvisionedController = Dependency.get(DeviceProvisionedController.class);
+ mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
+ mLocationController = Dependency.get(LocationController.class);
+
+ mSlotCast = context.getString(com.android.internal.R.string.status_bar_cast);
+ mSlotHotspot = context.getString(com.android.internal.R.string.status_bar_hotspot);
+ mSlotBluetooth = context.getString(com.android.internal.R.string.status_bar_bluetooth);
+ mSlotTty = context.getString(com.android.internal.R.string.status_bar_tty);
+ mSlotZen = context.getString(com.android.internal.R.string.status_bar_zen);
+ mSlotVolume = context.getString(com.android.internal.R.string.status_bar_volume);
+ mSlotAlarmClock = context.getString(com.android.internal.R.string.status_bar_alarm_clock);
+ mSlotManagedProfile = context.getString(
+ com.android.internal.R.string.status_bar_managed_profile);
+ mSlotRotate = context.getString(com.android.internal.R.string.status_bar_rotate);
+ mSlotHeadset = context.getString(com.android.internal.R.string.status_bar_headset);
+ mSlotDataSaver = context.getString(com.android.internal.R.string.status_bar_data_saver);
+ mSlotLocation = context.getString(com.android.internal.R.string.status_bar_location);
+
+ // listen for broadcasts
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
+ filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
+ filter.addAction(AudioManager.ACTION_HEADSET_PLUG);
+ filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
+ filter.addAction(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
+ mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
+
+ // listen for user / profile change.
+ try {
+ ActivityManager.getService().registerUserSwitchObserver(mUserSwitchListener, TAG);
+ } catch (RemoteException e) {
+ // Ignore
+ }
+
+ // TTY status
+ updateTTY();
+
+ // bluetooth status
+ updateBluetooth();
+
+ // Alarm clock
+ mIconController.setIcon(mSlotAlarmClock, R.drawable.stat_sys_alarm, null);
+ mIconController.setIconVisibility(mSlotAlarmClock, false);
+
+ // zen
+ mIconController.setIcon(mSlotZen, R.drawable.stat_sys_zen_important, null);
+ mIconController.setIconVisibility(mSlotZen, false);
+
+ // volume
+ mIconController.setIcon(mSlotVolume, R.drawable.stat_sys_ringer_vibrate, null);
+ mIconController.setIconVisibility(mSlotVolume, false);
+ updateVolumeZen();
+
+ // cast
+ mIconController.setIcon(mSlotCast, R.drawable.stat_sys_cast, null);
+ mIconController.setIconVisibility(mSlotCast, false);
+
+ // hotspot
+ mIconController.setIcon(mSlotHotspot, R.drawable.stat_sys_hotspot,
+ mContext.getString(R.string.accessibility_status_bar_hotspot));
+ mIconController.setIconVisibility(mSlotHotspot, mHotspot.isHotspotEnabled());
+
+ // managed profile
+ mIconController.setIcon(mSlotManagedProfile, R.drawable.stat_sys_managed_profile_status,
+ mContext.getString(R.string.accessibility_managed_profile));
+ mIconController.setIconVisibility(mSlotManagedProfile, mManagedProfileIconVisible);
+
+ // data saver
+ mIconController.setIcon(mSlotDataSaver, R.drawable.stat_sys_data_saver,
+ context.getString(R.string.accessibility_data_saver_on));
+ mIconController.setIconVisibility(mSlotDataSaver, false);
+
+ mRotationLockController.addCallback(this);
+ mBluetooth.addCallback(this);
+ mProvisionedController.addCallback(this);
+ mZenController.addCallback(this);
+ mCast.addCallback(mCastCallback);
+ mHotspot.addCallback(mHotspotCallback);
+ mNextAlarm.addCallback(mNextAlarmCallback);
+ mDataSaver.addCallback(this);
+ mKeyguardMonitor.addCallback(this);
+ mLocationController.addCallback(this);
+
+ SysUiServiceProvider.getComponent(mContext, CommandQueue.class).addCallbacks(this);
+ SystemServicesProxy.getInstance(mContext).registerTaskStackListener(mTaskListener);
+
+ // Clear out all old notifications on startup (only present in the case where sysui dies)
+ NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+ for (StatusBarNotification notification : noMan.getActiveNotifications()) {
+ if (notification.getId() == SystemMessage.NOTE_INSTANT_APPS) {
+ noMan.cancel(notification.getTag(), notification.getId());
+ }
+ }
+ DockedStackExistsListener.register(exists -> {
+ mDockedStackExists = exists;
+ updateForegroundInstantApps();
+ });
+ }
+
+ public void destroy() {
+ mRotationLockController.removeCallback(this);
+ mBluetooth.removeCallback(this);
+ mProvisionedController.removeCallback(this);
+ mZenController.removeCallback(this);
+ mCast.removeCallback(mCastCallback);
+ mHotspot.removeCallback(mHotspotCallback);
+ mNextAlarm.removeCallback(mNextAlarmCallback);
+ mDataSaver.removeCallback(this);
+ mKeyguardMonitor.removeCallback(this);
+ mLocationController.removeCallback(this);
+ SysUiServiceProvider.getComponent(mContext, CommandQueue.class).removeCallbacks(this);
+ mContext.unregisterReceiver(mIntentReceiver);
+
+ NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+ mCurrentNotifs.forEach(v -> noMan.cancelAsUser(v.first, SystemMessage.NOTE_INSTANT_APPS,
+ new UserHandle(v.second)));
+ }
+
+ @Override
+ public void onZenChanged(int zen) {
+ updateVolumeZen();
+ }
+
+ @Override
+ public void onLocationActiveChanged(boolean active) {
+ updateLocation();
+ }
+
+ // Updates the status view based on the current state of location requests.
+ private void updateLocation() {
+ if (mLocationController.isLocationActive()) {
+ mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID,
+ mContext.getString(R.string.accessibility_location_active));
+ } else {
+ mIconController.removeIcon(mSlotLocation);
+ }
+ }
+
+ private void updateAlarm() {
+ final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT);
+ final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0;
+ int zen = mZenController.getZen();
+ final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS;
+ mIconController.setIcon(mSlotAlarmClock, zenNone ? R.drawable.stat_sys_alarm_dim
+ : R.drawable.stat_sys_alarm, null);
+ mIconController.setIconVisibility(mSlotAlarmClock, mCurrentUserSetup && hasAlarm);
+ }
+
+ 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_CARD_IO_ERROR.equals(stateExtra)) {
+ mSimState = IccCardConstants.State.CARD_IO_ERROR;
+ } else if (IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED.equals(stateExtra)) {
+ mSimState = IccCardConstants.State.CARD_RESTRICTED;
+ } else if (IccCardConstants.INTENT_VALUE_ICC_READY.equals(stateExtra)) {
+ mSimState = IccCardConstants.State.READY;
+ } 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;
+ }
+ }
+
+ private final void updateVolumeZen() {
+ AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+ boolean zenVisible = false;
+ int zenIconId = 0;
+ String zenDescription = null;
+
+ boolean volumeVisible = false;
+ int volumeIconId = 0;
+ String volumeDescription = null;
+ int zen = mZenController.getZen();
+
+ if (DndTile.isVisible(mContext) || DndTile.isCombinedIcon(mContext)) {
+ zenVisible = zen != Global.ZEN_MODE_OFF;
+ zenIconId = zen == Global.ZEN_MODE_NO_INTERRUPTIONS
+ ? R.drawable.stat_sys_dnd_total_silence : R.drawable.stat_sys_dnd;
+ zenDescription = mContext.getString(R.string.quick_settings_dnd_label);
+ } else if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) {
+ zenVisible = true;
+ zenIconId = R.drawable.stat_sys_zen_none;
+ zenDescription = mContext.getString(R.string.interruption_level_none);
+ } else if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {
+ zenVisible = true;
+ zenIconId = R.drawable.stat_sys_zen_important;
+ zenDescription = mContext.getString(R.string.interruption_level_priority);
+ }
+
+ if (DndTile.isVisible(mContext) && !DndTile.isCombinedIcon(mContext)
+ && audioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) {
+ volumeVisible = true;
+ volumeIconId = R.drawable.stat_sys_ringer_silent;
+ volumeDescription = mContext.getString(R.string.accessibility_ringer_silent);
+ } else if (zen != Global.ZEN_MODE_NO_INTERRUPTIONS && zen != Global.ZEN_MODE_ALARMS &&
+ audioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) {
+ volumeVisible = true;
+ volumeIconId = R.drawable.stat_sys_ringer_vibrate;
+ volumeDescription = mContext.getString(R.string.accessibility_ringer_vibrate);
+ }
+
+ if (zenVisible) {
+ mIconController.setIcon(mSlotZen, zenIconId, zenDescription);
+ }
+ if (zenVisible != mZenVisible) {
+ mIconController.setIconVisibility(mSlotZen, zenVisible);
+ mZenVisible = zenVisible;
+ }
+
+ if (volumeVisible) {
+ mIconController.setIcon(mSlotVolume, volumeIconId, volumeDescription);
+ }
+ if (volumeVisible != mVolumeVisible) {
+ mIconController.setIconVisibility(mSlotVolume, volumeVisible);
+ mVolumeVisible = volumeVisible;
+ }
+ updateAlarm();
+ }
+
+ @Override
+ public void onBluetoothDevicesChanged() {
+ updateBluetooth();
+ }
+
+ @Override
+ public void onBluetoothStateChange(boolean enabled) {
+ updateBluetooth();
+ }
+
+ private final void updateBluetooth() {
+ int iconId = R.drawable.stat_sys_data_bluetooth;
+ String contentDescription =
+ mContext.getString(R.string.accessibility_quick_settings_bluetooth_on);
+ boolean bluetoothEnabled = false;
+ if (mBluetooth != null) {
+ bluetoothEnabled = mBluetooth.isBluetoothEnabled();
+ if (mBluetooth.isBluetoothConnected()) {
+ iconId = R.drawable.stat_sys_data_bluetooth_connected;
+ contentDescription = mContext.getString(R.string.accessibility_bluetooth_connected);
+ }
+ }
+
+ mIconController.setIcon(mSlotBluetooth, iconId, contentDescription);
+ mIconController.setIconVisibility(mSlotBluetooth, bluetoothEnabled);
+ }
+
+ private final void updateTTY() {
+ TelecomManager telecomManager =
+ (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
+ if (telecomManager == null) {
+ updateTTY(TelecomManager.TTY_MODE_OFF);
+ } else {
+ updateTTY(telecomManager.getCurrentTtyMode());
+ }
+ }
+
+ private final void updateTTY(int currentTtyMode) {
+ boolean enabled = currentTtyMode != TelecomManager.TTY_MODE_OFF;
+
+ if (DEBUG) Log.v(TAG, "updateTTY: enabled: " + enabled);
+
+ if (enabled) {
+ // TTY is on
+ if (DEBUG) Log.v(TAG, "updateTTY: set TTY on");
+ mIconController.setIcon(mSlotTty, R.drawable.stat_sys_tty_mode,
+ mContext.getString(R.string.accessibility_tty_enabled));
+ mIconController.setIconVisibility(mSlotTty, true);
+ } else {
+ // TTY is off
+ if (DEBUG) Log.v(TAG, "updateTTY: set TTY off");
+ mIconController.setIconVisibility(mSlotTty, false);
+ }
+ }
+
+ private void updateCast() {
+ boolean isCasting = false;
+ for (CastDevice device : mCast.getCastDevices()) {
+ if (device.state == CastDevice.STATE_CONNECTING
+ || device.state == CastDevice.STATE_CONNECTED) {
+ isCasting = true;
+ break;
+ }
+ }
+ if (DEBUG) Log.v(TAG, "updateCast: isCasting: " + isCasting);
+ mHandler.removeCallbacks(mRemoveCastIconRunnable);
+ if (isCasting) {
+ mIconController.setIcon(mSlotCast, R.drawable.stat_sys_cast,
+ mContext.getString(R.string.accessibility_casting));
+ mIconController.setIconVisibility(mSlotCast, true);
+ } else {
+ // don't turn off the screen-record icon for a few seconds, just to make sure the user
+ // has seen it
+ if (DEBUG) Log.v(TAG, "updateCast: hiding icon in 3 sec...");
+ mHandler.postDelayed(mRemoveCastIconRunnable, 3000);
+ }
+ }
+
+ private void updateQuietState() {
+ mManagedProfileInQuietMode = false;
+ int currentUserId = ActivityManager.getCurrentUser();
+ for (UserInfo ui : mUserManager.getEnabledProfiles(currentUserId)) {
+ if (ui.isManagedProfile() && ui.isQuietModeEnabled()) {
+ mManagedProfileInQuietMode = true;
+ return;
+ }
+ }
+ }
+
+ private void updateManagedProfile() {
+ // getLastResumedActivityUserId needds to acquire the AM lock, which may be contended in
+ // some cases. Since it doesn't really matter here whether it's updated in this frame
+ // or in the next one, we call this method from our UI offload thread.
+ mUiOffloadThread.submit(() -> {
+ final int userId;
+ try {
+ userId = ActivityManager.getService().getLastResumedActivityUserId();
+ boolean isManagedProfile = mUserManager.isManagedProfile(userId);
+ mHandler.post(() -> {
+ final boolean showIcon;
+ if (isManagedProfile &&
+ (!mKeyguardMonitor.isShowing() || mKeyguardMonitor.isOccluded())) {
+ showIcon = true;
+ mIconController.setIcon(mSlotManagedProfile,
+ R.drawable.stat_sys_managed_profile_status,
+ mContext.getString(R.string.accessibility_managed_profile));
+ } else if (mManagedProfileInQuietMode) {
+ showIcon = true;
+ mIconController.setIcon(mSlotManagedProfile,
+ R.drawable.stat_sys_managed_profile_status_off,
+ mContext.getString(R.string.accessibility_managed_profile));
+ } else {
+ showIcon = false;
+ }
+ if (mManagedProfileIconVisible != showIcon) {
+ mIconController.setIconVisibility(mSlotManagedProfile, showIcon);
+ mManagedProfileIconVisible = showIcon;
+ }
+ });
+ } catch (RemoteException e) {
+ Log.w(TAG, "updateManagedProfile: ", e);
+ }
+ });
+ }
+
+ private void updateForegroundInstantApps() {
+ NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+ ArraySet<Pair<String, Integer>> notifs = new ArraySet<>(mCurrentNotifs);
+ IPackageManager pm = AppGlobals.getPackageManager();
+ mCurrentNotifs.clear();
+ mUiOffloadThread.submit(() -> {
+ try {
+ int focusedId = ActivityManager.getService().getFocusedStackId();
+ if (focusedId == StackId.FULLSCREEN_WORKSPACE_STACK_ID) {
+ checkStack(StackId.FULLSCREEN_WORKSPACE_STACK_ID, notifs, noMan, pm);
+ }
+ if (mDockedStackExists) {
+ checkStack(StackId.DOCKED_STACK_ID, notifs, noMan, pm);
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ // Cancel all the leftover notifications that don't have a foreground process anymore.
+ notifs.forEach(v -> noMan.cancelAsUser(v.first, SystemMessage.NOTE_INSTANT_APPS,
+ new UserHandle(v.second)));
+ });
+ }
+
+ private void checkStack(int stackId, ArraySet<Pair<String, Integer>> notifs,
+ NotificationManager noMan, IPackageManager pm) {
+ try {
+ StackInfo info = ActivityManager.getService().getStackInfo(stackId);
+ if (info == null || info.topActivity == null) return;
+ String pkg = info.topActivity.getPackageName();
+ if (!hasNotif(notifs, pkg, info.userId)) {
+ // TODO: Optimize by not always needing to get application info.
+ // Maybe cache non-ephemeral packages?
+ ApplicationInfo appInfo = pm.getApplicationInfo(pkg,
+ PackageManager.MATCH_UNINSTALLED_PACKAGES, info.userId);
+ if (appInfo.isInstantApp()) {
+ postEphemeralNotif(pkg, info.userId, appInfo, noMan, info.taskIds[info.taskIds.length - 1]);
+ }
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ private void postEphemeralNotif(String pkg, int userId, ApplicationInfo appInfo,
+ NotificationManager noMan, int taskId) {
+ final Bundle extras = new Bundle();
+ extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
+ mContext.getString(R.string.instant_apps));
+ mCurrentNotifs.add(new Pair<>(pkg, userId));
+ String message = mContext.getString(R.string.instant_apps_message);
+ PendingIntent appInfoAction = PendingIntent.getActivity(mContext, 0,
+ new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.fromParts("package", pkg, null)), 0);
+ Action action = new Notification.Action.Builder(null, mContext.getString(R.string.app_info),
+ appInfoAction).build();
+
+ Intent browserIntent = getTaskIntent(taskId, userId);
+ Notification.Builder builder = new Notification.Builder(mContext, NotificationChannels.GENERAL);
+ if (browserIntent != null) {
+ // Make sure that this doesn't resolve back to an instant app
+ browserIntent.setComponent(null)
+ .setPackage(null)
+ .addFlags(Intent.FLAG_IGNORE_EPHEMERAL)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext,
+ 0 /* requestCode */, browserIntent, 0 /* flags */);
+ ComponentName aiaComponent = null;
+ try {
+ aiaComponent = AppGlobals.getPackageManager().getInstantAppInstallerComponent();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ Intent goToWebIntent = new Intent()
+ .setComponent(aiaComponent)
+ .setAction(Intent.ACTION_VIEW)
+ .addCategory(Intent.CATEGORY_BROWSABLE)
+ .addCategory("unique:" + System.currentTimeMillis())
+ .putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName)
+ .putExtra(Intent.EXTRA_VERSION_CODE, appInfo.versionCode)
+ .putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, pendingIntent);
+
+ PendingIntent webPendingIntent = PendingIntent.getActivity(mContext, 0, goToWebIntent, 0);
+ Action webAction = new Notification.Action.Builder(null, mContext.getString(R.string.go_to_web),
+ webPendingIntent).build();
+ builder.addAction(webAction);
+ }
+
+ noMan.notifyAsUser(pkg, SystemMessage.NOTE_INSTANT_APPS, builder
+ .addExtras(extras)
+ .addAction(action)
+ .setContentIntent(appInfoAction)
+ .setColor(mContext.getColor(R.color.instant_apps_color))
+ .setContentTitle(appInfo.loadLabel(mContext.getPackageManager()))
+ .setLargeIcon(Icon.createWithResource(pkg, appInfo.icon))
+ .setSmallIcon(Icon.createWithResource(mContext.getPackageName(),
+ R.drawable.instant_icon))
+ .setContentText(message)
+ .setOngoing(true)
+ .build(),
+ new UserHandle(userId));
+ }
+
+ private Intent getTaskIntent(int taskId, int userId) {
+ List<ActivityManager.RecentTaskInfo> tasks = mContext.getSystemService(ActivityManager.class)
+ .getRecentTasksForUser(NUM_TASKS_FOR_INSTANT_APP_INFO, 0, userId);
+ for (int i = 0; i < tasks.size(); i++) {
+ if (tasks.get(i).id == taskId) {
+ return tasks.get(i).baseIntent;
+ }
+ }
+ return null;
+ }
+
+ private boolean hasNotif(ArraySet<Pair<String, Integer>> notifs, String pkg, int userId) {
+ Pair<String, Integer> key = new Pair<>(pkg, userId);
+ if (notifs.remove(key)) {
+ mCurrentNotifs.add(key);
+ return true;
+ }
+ return false;
+ }
+
+ private final SynchronousUserSwitchObserver mUserSwitchListener =
+ new SynchronousUserSwitchObserver() {
+ @Override
+ public void onUserSwitching(int newUserId) throws RemoteException {
+ mHandler.post(() -> mUserInfoController.reloadUserInfo());
+ }
+
+ @Override
+ public void onUserSwitchComplete(int newUserId) throws RemoteException {
+ mHandler.post(() -> {
+ updateAlarm();
+ updateQuietState();
+ updateManagedProfile();
+ updateForegroundInstantApps();
+ });
+ }
+ };
+
+ private final HotspotController.Callback mHotspotCallback = new HotspotController.Callback() {
+ @Override
+ public void onHotspotChanged(boolean enabled) {
+ mIconController.setIconVisibility(mSlotHotspot, enabled);
+ }
+ };
+
+ private final CastController.Callback mCastCallback = new CastController.Callback() {
+ @Override
+ public void onCastDevicesChanged() {
+ updateCast();
+ }
+ };
+
+ private final NextAlarmController.NextAlarmChangeCallback mNextAlarmCallback =
+ new NextAlarmController.NextAlarmChangeCallback() {
+ @Override
+ public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
+ updateAlarm();
+ }
+ };
+
+ @Override
+ public void appTransitionStarting(long startTime, long duration, boolean forced) {
+ updateManagedProfile();
+ updateForegroundInstantApps();
+ }
+
+ @Override
+ public void onKeyguardShowingChanged() {
+ updateManagedProfile();
+ updateForegroundInstantApps();
+ }
+
+ @Override
+ public void onUserSetupChanged() {
+ boolean userSetup = mProvisionedController.isUserSetup(
+ mProvisionedController.getCurrentUser());
+ if (mCurrentUserSetup == userSetup) return;
+ mCurrentUserSetup = userSetup;
+ updateAlarm();
+ updateQuietState();
+ }
+
+ @Override
+ public void preloadRecentApps() {
+ updateForegroundInstantApps();
+ }
+
+ @Override
+ public void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible) {
+ boolean portrait = RotationLockTile.isCurrentOrientationLockPortrait(
+ mRotationLockController, mContext);
+ if (rotationLocked) {
+ if (portrait) {
+ mIconController.setIcon(mSlotRotate, R.drawable.stat_sys_rotate_portrait,
+ mContext.getString(R.string.accessibility_rotation_lock_on_portrait));
+ } else {
+ mIconController.setIcon(mSlotRotate, R.drawable.stat_sys_rotate_landscape,
+ mContext.getString(R.string.accessibility_rotation_lock_on_landscape));
+ }
+ mIconController.setIconVisibility(mSlotRotate, true);
+ } else {
+ mIconController.setIconVisibility(mSlotRotate, false);
+ }
+ }
+
+ private void updateHeadsetPlug(Intent intent) {
+ boolean connected = intent.getIntExtra("state", 0) != 0;
+ boolean hasMic = intent.getIntExtra("microphone", 0) != 0;
+ if (connected) {
+ String contentDescription = mContext.getString(hasMic
+ ? R.string.accessibility_status_bar_headset
+ : R.string.accessibility_status_bar_headphones);
+ mIconController.setIcon(mSlotHeadset, hasMic ? R.drawable.ic_headset_mic
+ : R.drawable.ic_headset, contentDescription);
+ mIconController.setIconVisibility(mSlotHeadset, true);
+ } else {
+ mIconController.setIconVisibility(mSlotHeadset, false);
+ }
+ }
+
+ @Override
+ public void onDataSaverChanged(boolean isDataSaving) {
+ mIconController.setIconVisibility(mSlotDataSaver, isDataSaving);
+ }
+
+ private final TaskStackListener mTaskListener = new TaskStackListener() {
+ @Override
+ public void onTaskStackChanged() {
+ // Listen for changes to stacks and then check which instant apps are foreground.
+ updateForegroundInstantApps();
+ }
+ };
+
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ 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)) {
+ updateSimState(intent);
+ } else if (action.equals(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED)) {
+ updateTTY(intent.getIntExtra(TelecomManager.EXTRA_CURRENT_TTY_MODE,
+ TelecomManager.TTY_MODE_OFF));
+ } else if (action.equals(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) ||
+ action.equals(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) ||
+ action.equals(Intent.ACTION_MANAGED_PROFILE_REMOVED)) {
+ updateQuietState();
+ updateManagedProfile();
+ } else if (action.equals(AudioManager.ACTION_HEADSET_PLUG)) {
+ updateHeadsetPlug(intent);
+ }
+ }
+ };
+
+ private Runnable mRemoveCastIconRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.v(TAG, "updateCast: hiding icon NOW");
+ mIconController.setIconVisibility(mSlotCast, false);
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java b/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
new file mode 100644
index 0000000..fb1addf
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
@@ -0,0 +1,110 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.res.Resources;
+import android.view.View;
+
+import com.android.systemui.R;
+
+public final class PhoneStatusBarTransitions extends BarTransitions {
+ private static final float ICON_ALPHA_WHEN_NOT_OPAQUE = 1;
+ private static final float ICON_ALPHA_WHEN_LIGHTS_OUT_BATTERY_CLOCK = 0.5f;
+ private static final float ICON_ALPHA_WHEN_LIGHTS_OUT_NON_BATTERY_CLOCK = 0;
+
+ private final PhoneStatusBarView mView;
+ private final float mIconAlphaWhenOpaque;
+
+ private View mLeftSide, mStatusIcons, mSignalCluster, mBattery, mClock;
+ private Animator mCurrentAnimation;
+
+ public PhoneStatusBarTransitions(PhoneStatusBarView view) {
+ super(view, R.drawable.status_background);
+ mView = view;
+ final Resources res = mView.getContext().getResources();
+ mIconAlphaWhenOpaque = res.getFraction(R.dimen.status_bar_icon_drawing_alpha, 1, 1);
+ }
+
+ public void init() {
+ mLeftSide = mView.findViewById(R.id.notification_icon_area);
+ mStatusIcons = mView.findViewById(R.id.statusIcons);
+ mSignalCluster = mView.findViewById(R.id.signal_cluster);
+ mBattery = mView.findViewById(R.id.battery);
+ mClock = mView.findViewById(R.id.clock);
+ applyModeBackground(-1, getMode(), false /*animate*/);
+ applyMode(getMode(), false /*animate*/);
+ }
+
+ public ObjectAnimator animateTransitionTo(View v, float toAlpha) {
+ return ObjectAnimator.ofFloat(v, "alpha", v.getAlpha(), toAlpha);
+ }
+
+ private float getNonBatteryClockAlphaFor(int mode) {
+ 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 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_LIGHTS_OUT_TRANSPARENT);
+ }
+
+ @Override
+ protected void onTransition(int oldMode, int newMode, boolean animate) {
+ super.onTransition(oldMode, newMode, animate);
+ applyMode(newMode, animate);
+ }
+
+ private void applyMode(int mode, boolean animate) {
+ if (mLeftSide == null) return; // pre-init
+ float newAlpha = getNonBatteryClockAlphaFor(mode);
+ float newAlphaBC = getBatteryClockAlpha(mode);
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.cancel();
+ }
+ if (animate) {
+ AnimatorSet anims = new AnimatorSet();
+ anims.playTogether(
+ animateTransitionTo(mLeftSide, newAlpha),
+ animateTransitionTo(mStatusIcons, newAlpha),
+ animateTransitionTo(mSignalCluster, newAlpha),
+ animateTransitionTo(mBattery, newAlphaBC),
+ animateTransitionTo(mClock, newAlphaBC)
+ );
+ if (isLightsOut(mode)) {
+ anims.setDuration(LIGHTS_OUT_DURATION);
+ }
+ anims.start();
+ mCurrentAnimation = anims;
+ } else {
+ mLeftSide.setAlpha(newAlpha);
+ mStatusIcons.setAlpha(newAlpha);
+ mSignalCluster.setAlpha(newAlpha);
+ mBattery.setAlpha(newAlphaBC);
+ mClock.setAlpha(newAlphaBC);
+ }
+ }
+}
\ No newline at end of file
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
new file mode 100644
index 0000000..970d1de
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.EventLog;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.systemui.BatteryMeterView;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.Dependency;
+import com.android.systemui.EventLogTags;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+
+public class PhoneStatusBarView extends PanelBar {
+ private static final String TAG = "PhoneStatusBarView";
+ private static final boolean DEBUG = StatusBar.DEBUG;
+ private static final boolean DEBUG_GESTURES = false;
+
+ StatusBar mBar;
+
+ boolean mIsFullyOpenedPanel = false;
+ private final PhoneStatusBarTransitions mBarTransitions;
+ private ScrimController mScrimController;
+ private float mMinFraction;
+ private float mPanelFraction;
+ private Runnable mHideExpandedRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mPanelFraction == 0.0f) {
+ mBar.makeExpandedInvisible();
+ }
+ }
+ };
+ private DarkReceiver mBattery;
+
+ public PhoneStatusBarView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mBarTransitions = new PhoneStatusBarTransitions(this);
+ }
+
+ public BarTransitions getBarTransitions() {
+ return mBarTransitions;
+ }
+
+ public void setBar(StatusBar bar) {
+ mBar = bar;
+ }
+
+ public void setScrimController(ScrimController scrimController) {
+ mScrimController = scrimController;
+ }
+
+ @Override
+ public void onFinishInflate() {
+ mBarTransitions.init();
+ mBattery = findViewById(R.id.battery);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ // Always have Battery meters in the status bar observe the dark/light modes.
+ Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery);
+ }
+
+ @Override
+ public boolean panelEnabled() {
+ return mBar.panelsEnabled();
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
+ if (super.onRequestSendAccessibilityEventInternal(child, event)) {
+ // The status bar is very small so augment the view that the user is touching
+ // with the content of the status bar a whole. This way an accessibility service
+ // may announce the current item as well as the entire content if appropriate.
+ AccessibilityEvent record = AccessibilityEvent.obtain();
+ onInitializeAccessibilityEvent(record);
+ dispatchPopulateAccessibilityEvent(record);
+ event.appendRecord(record);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onPanelPeeked() {
+ super.onPanelPeeked();
+ mBar.makeExpandedVisible(false);
+ }
+
+ @Override
+ public void onPanelCollapsed() {
+ super.onPanelCollapsed();
+ // Close the status bar in the next frame so we can show the end of the animation.
+ post(mHideExpandedRunnable);
+ mIsFullyOpenedPanel = false;
+ }
+
+ public void removePendingHideExpandedRunnables() {
+ removeCallbacks(mHideExpandedRunnable);
+ }
+
+ @Override
+ public void onPanelFullyOpened() {
+ super.onPanelFullyOpened();
+ if (!mIsFullyOpenedPanel) {
+ mPanel.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+ mIsFullyOpenedPanel = true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean barConsumedEvent = mBar.interceptTouchEvent(event);
+
+ if (DEBUG_GESTURES) {
+ if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
+ EventLog.writeEvent(EventLogTags.SYSUI_PANELBAR_TOUCH,
+ event.getActionMasked(), (int) event.getX(), (int) event.getY(),
+ barConsumedEvent ? 1 : 0);
+ }
+ }
+
+ return barConsumedEvent || super.onTouchEvent(event);
+ }
+
+ @Override
+ public void onTrackingStarted() {
+ super.onTrackingStarted();
+ mBar.onTrackingStarted();
+ mScrimController.onTrackingStarted();
+ removePendingHideExpandedRunnables();
+ }
+
+ @Override
+ public void onClosingFinished() {
+ super.onClosingFinished();
+ mBar.onClosingFinished();
+ }
+
+ @Override
+ public void onTrackingStopped(boolean expand) {
+ super.onTrackingStopped(expand);
+ mBar.onTrackingStopped(expand);
+ }
+
+ @Override
+ public void onExpandingFinished() {
+ super.onExpandingFinished();
+ mScrimController.onExpandingFinished();
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ return mBar.interceptTouchEvent(event) || super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ public void panelScrimMinFractionChanged(float minFraction) {
+ if (mMinFraction != minFraction) {
+ mMinFraction = minFraction;
+ updateScrimFraction();
+ }
+ }
+
+ @Override
+ public void panelExpansionChanged(float frac, boolean expanded) {
+ super.panelExpansionChanged(frac, expanded);
+ mPanelFraction = frac;
+ updateScrimFraction();
+ }
+
+ private void updateScrimFraction() {
+ float scrimFraction = mPanelFraction;
+ if (mMinFraction < 1.0f) {
+ scrimFraction = Math.max((mPanelFraction - mMinFraction) / (1.0f - mMinFraction),
+ 0);
+ }
+ mScrimController.setPanelExpansion(scrimFraction);
+ }
+
+ public void onDensityOrFontScaleChanged() {
+ ViewGroup.LayoutParams layoutParams = getLayoutParams();
+ layoutParams.height = getResources().getDimensionPixelSize(
+ R.dimen.status_bar_height);
+ setLayoutParams(layoutParams);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/PlatformVelocityTracker.java b/com/android/systemui/statusbar/phone/PlatformVelocityTracker.java
new file mode 100644
index 0000000..f589c3d
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PlatformVelocityTracker.java
@@ -0,0 +1,72 @@
+/*
+ * 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.util.Pools;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+/**
+ * An implementation of {@link VelocityTrackerInterface} using the platform-standard
+ * {@link VelocityTracker}.
+ */
+public class PlatformVelocityTracker implements VelocityTrackerInterface {
+
+ private static final Pools.SynchronizedPool<PlatformVelocityTracker> sPool =
+ new Pools.SynchronizedPool<>(2);
+
+ private VelocityTracker mTracker;
+
+ public static PlatformVelocityTracker obtain() {
+ PlatformVelocityTracker tracker = sPool.acquire();
+ if (tracker == null) {
+ tracker = new PlatformVelocityTracker();
+ }
+ tracker.setTracker(VelocityTracker.obtain());
+ return tracker;
+ }
+
+ public void setTracker(VelocityTracker tracker) {
+ mTracker = tracker;
+ }
+
+ @Override
+ public void addMovement(MotionEvent event) {
+ mTracker.addMovement(event);
+ }
+
+ @Override
+ public void computeCurrentVelocity(int units) {
+ mTracker.computeCurrentVelocity(units);
+ }
+
+ @Override
+ public float getXVelocity() {
+ return mTracker.getXVelocity();
+ }
+
+ @Override
+ public float getYVelocity() {
+ return mTracker.getYVelocity();
+ }
+
+ @Override
+ public void recycle() {
+ mTracker.recycle();
+ sPool.release(this);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/ReverseLinearLayout.java b/com/android/systemui/statusbar/phone/ReverseLinearLayout.java
new file mode 100644
index 0000000..bcbc345
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ReverseLinearLayout.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 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.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+
+/**
+ * Automatically reverses the order of children as they are added.
+ * Also reverse the width and height values of layout params
+ */
+public class ReverseLinearLayout extends LinearLayout {
+
+ /** If true, the layout is reversed vs. a regular linear layout */
+ private boolean mIsLayoutReverse;
+
+ /** If true, the layout is opposite to it's natural reversity from the layout direction */
+ private boolean mIsAlternativeOrder;
+
+ public ReverseLinearLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ updateOrder();
+ }
+
+ @Override
+ public void addView(View child) {
+ reverseParams(child.getLayoutParams(), child);
+ if (mIsLayoutReverse) {
+ super.addView(child, 0);
+ } else {
+ super.addView(child);
+ }
+ }
+
+ @Override
+ public void addView(View child, ViewGroup.LayoutParams params) {
+ reverseParams(params, child);
+ if (mIsLayoutReverse) {
+ super.addView(child, 0, params);
+ } else {
+ super.addView(child, params);
+ }
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+ updateOrder();
+ }
+
+ public void setAlternativeOrder(boolean alternative) {
+ mIsAlternativeOrder = alternative;
+ updateOrder();
+ }
+
+ /**
+ * In landscape, the LinearLayout is not auto mirrored since it is vertical. Therefore we
+ * have to do it manually
+ */
+ private void updateOrder() {
+ boolean isLayoutRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ boolean isLayoutReverse = isLayoutRtl ^ mIsAlternativeOrder;
+
+ if (mIsLayoutReverse != isLayoutReverse) {
+ // reversity changed, swap the order of all views.
+ int childCount = getChildCount();
+ ArrayList<View> childList = new ArrayList<>(childCount);
+ for (int i = 0; i < childCount; i++) {
+ childList.add(getChildAt(i));
+ }
+ removeAllViews();
+ for (int i = childCount - 1; i >= 0; i--) {
+ super.addView(childList.get(i));
+ }
+ mIsLayoutReverse = isLayoutReverse;
+ }
+ }
+
+ private static void reverseParams(ViewGroup.LayoutParams params, View child) {
+ if (child instanceof Reversable) {
+ ((Reversable) child).reverse();
+ }
+ if (child.getPaddingLeft() == child.getPaddingRight()
+ && child.getPaddingTop() == child.getPaddingBottom()) {
+ child.setPadding(child.getPaddingTop(), child.getPaddingLeft(),
+ child.getPaddingTop(), child.getPaddingLeft());
+ }
+ if (params == null) {
+ return;
+ }
+ int width = params.width;
+ params.width = params.height;
+ params.height = width;
+ }
+
+ public interface Reversable {
+ void reverse();
+ }
+
+ public static class ReverseFrameLayout extends FrameLayout implements Reversable {
+
+ public ReverseFrameLayout(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void reverse() {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ reverseParams(child.getLayoutParams(), child);
+ }
+ }
+ }
+
+}
diff --git a/com/android/systemui/statusbar/phone/ScrimController.java b/com/android/systemui/statusbar/phone/ScrimController.java
new file mode 100644
index 0000000..702afa3
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ScrimController.java
@@ -0,0 +1,790 @@
+/*
+ * 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.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Trace;
+import android.util.MathUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.colorextraction.ColorExtractor.GradientColors;
+import com.android.internal.colorextraction.ColorExtractor.OnColorsChangedListener;
+import com.android.internal.graphics.ColorUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.statusbar.stack.ViewState;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * Controls both the scrim behind the notifications and in front of the notifications (when a
+ * security method gets shown).
+ */
+public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
+ OnHeadsUpChangedListener, OnColorsChangedListener {
+ public static final long ANIMATION_DURATION = 220;
+ public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR
+ = new PathInterpolator(0f, 0, 0.7f, 1f);
+ public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR_LOCKED
+ = new PathInterpolator(0.3f, 0f, 0.8f, 1f);
+ // Default alpha value for most scrims, if unsure use this constant
+ public static final float GRADIENT_SCRIM_ALPHA = 0.45f;
+ // A scrim varies its opacity based on a busyness factor, for example
+ // how many notifications are currently visible.
+ public static final float GRADIENT_SCRIM_ALPHA_BUSY = 0.70f;
+ protected static final float SCRIM_BEHIND_ALPHA_KEYGUARD = GRADIENT_SCRIM_ALPHA;
+ protected static final float SCRIM_BEHIND_ALPHA_UNLOCKING = 0.2f;
+ private static final float SCRIM_IN_FRONT_ALPHA = GRADIENT_SCRIM_ALPHA_BUSY;
+ private static final float SCRIM_IN_FRONT_ALPHA_LOCKED = GRADIENT_SCRIM_ALPHA_BUSY;
+ private static final int TAG_KEY_ANIM = R.id.scrim;
+ private static final int TAG_KEY_ANIM_TARGET = R.id.scrim_target;
+ private static final int TAG_START_ALPHA = R.id.scrim_alpha_start;
+ private static final int TAG_END_ALPHA = R.id.scrim_alpha_end;
+ private static final float NOT_INITIALIZED = -1;
+
+ private final LightBarController mLightBarController;
+ protected final ScrimView mScrimBehind;
+ protected final ScrimView mScrimInFront;
+ private final UnlockMethodCache mUnlockMethodCache;
+ private final View mHeadsUpScrim;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+ private final SysuiColorExtractor mColorExtractor;
+ private GradientColors mLockColors;
+ private GradientColors mSystemColors;
+ private boolean mNeedsDrawableColorUpdate;
+
+ protected float mScrimBehindAlpha;
+ protected float mScrimBehindAlphaResValue;
+ protected float mScrimBehindAlphaKeyguard = SCRIM_BEHIND_ALPHA_KEYGUARD;
+ protected float mScrimBehindAlphaUnlocking = SCRIM_BEHIND_ALPHA_UNLOCKING;
+
+ protected boolean mKeyguardShowing;
+ private float mFraction;
+
+ private boolean mDarkenWhileDragging;
+ protected boolean mBouncerShowing;
+ protected boolean mBouncerIsKeyguard = false;
+ private boolean mWakeAndUnlocking;
+ protected boolean mAnimateChange;
+ private boolean mUpdatePending;
+ private boolean mTracking;
+ private boolean mAnimateKeyguardFadingOut;
+ protected long mDurationOverride = -1;
+ private long mAnimationDelay;
+ private Runnable mOnAnimationFinished;
+ private boolean mDeferFinishedListener;
+ private final Interpolator mInterpolator = new DecelerateInterpolator();
+ private boolean mDozing;
+ private float mDozeInFrontAlpha;
+ private float mDozeBehindAlpha;
+ private float mCurrentInFrontAlpha = NOT_INITIALIZED;
+ private float mCurrentBehindAlpha = NOT_INITIALIZED;
+ private float mCurrentHeadsUpAlpha = NOT_INITIALIZED;
+ private int mPinnedHeadsUpCount;
+ private float mTopHeadsUpDragAmount;
+ private View mDraggedHeadsUpView;
+ private boolean mForceHideScrims;
+ private boolean mSkipFirstFrame;
+ private boolean mDontAnimateBouncerChanges;
+ private boolean mKeyguardFadingOutInProgress;
+ private boolean mAnimatingDozeUnlock;
+ private ValueAnimator mKeyguardFadeoutAnimation;
+ /** Wake up from AOD transition is starting; need fully opaque front scrim */
+ private boolean mWakingUpFromAodStarting;
+ /** Wake up from AOD transition is in progress; need black tint */
+ private boolean mWakingUpFromAodInProgress;
+ /** Wake up from AOD transition is animating; need to reset when animation finishes */
+ private boolean mWakingUpFromAodAnimationRunning;
+ private boolean mScrimsVisble;
+ private final Consumer<Boolean> mScrimVisibleListener;
+
+ public ScrimController(LightBarController lightBarController, ScrimView scrimBehind,
+ ScrimView scrimInFront, View headsUpScrim,
+ Consumer<Boolean> scrimVisibleListener) {
+ mScrimBehind = scrimBehind;
+ mScrimInFront = scrimInFront;
+ mHeadsUpScrim = headsUpScrim;
+ mScrimVisibleListener = scrimVisibleListener;
+ final Context context = scrimBehind.getContext();
+ mUnlockMethodCache = UnlockMethodCache.getInstance(context);
+ mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(context);
+ mLightBarController = lightBarController;
+ mScrimBehindAlphaResValue = context.getResources().getFloat(R.dimen.scrim_behind_alpha);
+ // Scrim alpha is initially set to the value on the resource but might be changed
+ // to make sure that text on top of it is legible.
+ mScrimBehindAlpha = mScrimBehindAlphaResValue;
+
+ mColorExtractor = Dependency.get(SysuiColorExtractor.class);
+ mColorExtractor.addOnColorsChangedListener(this);
+ mLockColors = mColorExtractor.getColors(WallpaperManager.FLAG_LOCK,
+ ColorExtractor.TYPE_DARK, true /* ignoreVisibility */);
+ mSystemColors = mColorExtractor.getColors(WallpaperManager.FLAG_SYSTEM,
+ ColorExtractor.TYPE_DARK, true /* ignoreVisibility */);
+ mNeedsDrawableColorUpdate = true;
+
+ updateHeadsUpScrim(false);
+ updateScrims();
+ }
+
+ public void setKeyguardShowing(boolean showing) {
+ mKeyguardShowing = showing;
+
+ // Showing/hiding the keyguard means that scrim colors have to be switched
+ mNeedsDrawableColorUpdate = true;
+ scheduleUpdate();
+ }
+
+ protected void setScrimBehindValues(float scrimBehindAlphaKeyguard,
+ float scrimBehindAlphaUnlocking) {
+ mScrimBehindAlphaKeyguard = scrimBehindAlphaKeyguard;
+ mScrimBehindAlphaUnlocking = scrimBehindAlphaUnlocking;
+ scheduleUpdate();
+ }
+
+ public void onTrackingStarted() {
+ mTracking = true;
+ mDarkenWhileDragging = !mUnlockMethodCache.canSkipBouncer();
+ }
+
+ public void onExpandingFinished() {
+ mTracking = false;
+ }
+
+ public void setPanelExpansion(float fraction) {
+ if (mFraction != fraction) {
+ mFraction = fraction;
+ scheduleUpdate();
+ if (mPinnedHeadsUpCount != 0) {
+ updateHeadsUpScrim(false);
+ }
+ if (mKeyguardFadeoutAnimation != null && mTracking) {
+ mKeyguardFadeoutAnimation.cancel();
+ }
+ }
+ }
+
+ public void setBouncerShowing(boolean showing) {
+ mBouncerShowing = showing;
+ mAnimateChange = !mTracking && !mDontAnimateBouncerChanges && !mKeyguardFadingOutInProgress;
+ scheduleUpdate();
+ }
+
+ /** Prepares the wakeUpFromAod animation (while turning on screen); Forces black scrims. */
+ public void prepareWakeUpFromAod() {
+ if (mWakingUpFromAodInProgress) {
+ return;
+ }
+ mWakingUpFromAodInProgress = true;
+ mWakingUpFromAodStarting = true;
+ mAnimateChange = false;
+ scheduleUpdate();
+ onPreDraw();
+ }
+
+ /** Starts the wakeUpFromAod animation (once screen is on); animate to transparent scrims. */
+ public void wakeUpFromAod() {
+ if (mWakeAndUnlocking || mAnimateKeyguardFadingOut) {
+ // Wake and unlocking has a separate transition that must not be interfered with.
+ mWakingUpFromAodStarting = false;
+ mWakingUpFromAodInProgress = false;
+ return;
+ }
+ if (mWakingUpFromAodStarting) {
+ mWakingUpFromAodInProgress = true;
+ mWakingUpFromAodStarting = false;
+ mAnimateChange = true;
+ scheduleUpdate();
+ }
+ }
+
+ public void setWakeAndUnlocking() {
+ mWakeAndUnlocking = true;
+ mAnimatingDozeUnlock = true;
+ mWakingUpFromAodStarting = false;
+ mWakingUpFromAodInProgress = false;
+ scheduleUpdate();
+ }
+
+ public void animateKeyguardFadingOut(long delay, long duration, Runnable onAnimationFinished,
+ boolean skipFirstFrame) {
+ mWakeAndUnlocking = false;
+ mAnimateKeyguardFadingOut = true;
+ mDurationOverride = duration;
+ mAnimationDelay = delay;
+ mAnimateChange = true;
+ mSkipFirstFrame = skipFirstFrame;
+ mOnAnimationFinished = onAnimationFinished;
+
+ if (!mKeyguardUpdateMonitor.needsSlowUnlockTransition()) {
+ scheduleUpdate();
+
+ // No need to wait for the next frame to be drawn for this case - onPreDraw will execute
+ // the changes we just scheduled.
+ onPreDraw();
+ } else {
+
+ // In case the user isn't unlocked, make sure to delay a bit because the system is hosed
+ // with too many things in this case, in order to not skip the initial frames.
+ mScrimInFront.postOnAnimationDelayed(this::scheduleUpdate, 16);
+ }
+ }
+
+ public void abortKeyguardFadingOut() {
+ if (mAnimateKeyguardFadingOut) {
+ endAnimateKeyguardFadingOut(true /* force */);
+ }
+ }
+
+ public void animateKeyguardUnoccluding(long duration) {
+ mAnimateChange = false;
+ setScrimBehindAlpha(0f);
+ mAnimateChange = true;
+ scheduleUpdate();
+ mDurationOverride = duration;
+ }
+
+ public void animateGoingToFullShade(long delay, long duration) {
+ mDurationOverride = duration;
+ mAnimationDelay = delay;
+ mAnimateChange = true;
+ scheduleUpdate();
+ }
+
+ public void setDozing(boolean dozing) {
+ if (mDozing != dozing) {
+ mDozing = dozing;
+ scheduleUpdate();
+ }
+ }
+
+ public void setDozeInFrontAlpha(float alpha) {
+ mDozeInFrontAlpha = alpha;
+ updateScrimColor(mScrimInFront);
+ }
+
+ public void setDozeBehindAlpha(float alpha) {
+ mDozeBehindAlpha = alpha;
+ updateScrimColor(mScrimBehind);
+ }
+
+ public float getDozeBehindAlpha() {
+ return mDozeBehindAlpha;
+ }
+
+ public float getDozeInFrontAlpha() {
+ return mDozeInFrontAlpha;
+ }
+
+ public void setNotificationCount(int notificationCount) {
+ final float maxNotificationDensity = 3;
+ float notificationDensity = Math.min(notificationCount / maxNotificationDensity, 1f);
+ float newAlpha = MathUtils.map(0, 1,
+ GRADIENT_SCRIM_ALPHA, GRADIENT_SCRIM_ALPHA_BUSY,
+ notificationDensity);
+ if (mScrimBehindAlphaKeyguard != newAlpha) {
+ mScrimBehindAlphaKeyguard = newAlpha;
+ mAnimateChange = true;
+ scheduleUpdate();
+ }
+ }
+
+ private float getScrimInFrontAlpha() {
+ return mKeyguardUpdateMonitor.needsSlowUnlockTransition()
+ ? SCRIM_IN_FRONT_ALPHA_LOCKED
+ : SCRIM_IN_FRONT_ALPHA;
+ }
+
+ /**
+ * Sets the given drawable as the background of the scrim that shows up behind the
+ * notifications.
+ */
+ public void setScrimBehindDrawable(Drawable drawable) {
+ mScrimBehind.setDrawable(drawable);
+ }
+
+ protected void scheduleUpdate() {
+ if (mUpdatePending) return;
+
+ // Make sure that a frame gets scheduled.
+ mScrimBehind.invalidate();
+ mScrimBehind.getViewTreeObserver().addOnPreDrawListener(this);
+ mUpdatePending = true;
+ }
+
+ protected void updateScrims() {
+ // Make sure we have the right gradients and their opacities will satisfy GAR.
+ if (mNeedsDrawableColorUpdate) {
+ mNeedsDrawableColorUpdate = false;
+ final GradientColors currentScrimColors;
+ if (mKeyguardShowing) {
+ // Always animate color changes if we're seeing the keyguard
+ mScrimInFront.setColors(mLockColors, true /* animated */);
+ mScrimBehind.setColors(mLockColors, true /* animated */);
+ currentScrimColors = mLockColors;
+ } else {
+ // Only animate scrim color if the scrim view is actually visible
+ boolean animateScrimInFront = mScrimInFront.getViewAlpha() != 0;
+ boolean animateScrimBehind = mScrimBehind.getViewAlpha() != 0;
+ mScrimInFront.setColors(mSystemColors, animateScrimInFront);
+ mScrimBehind.setColors(mSystemColors, animateScrimBehind);
+ currentScrimColors = mSystemColors;
+ }
+
+ // Calculate minimum scrim opacity for white or black text.
+ int textColor = currentScrimColors.supportsDarkText() ? Color.BLACK : Color.WHITE;
+ int mainColor = currentScrimColors.getMainColor();
+ float minOpacity = ColorUtils.calculateMinimumBackgroundAlpha(textColor, mainColor,
+ 4.5f /* minimumContrast */) / 255f;
+ mScrimBehindAlpha = Math.max(mScrimBehindAlphaResValue, minOpacity);
+ mLightBarController.setScrimColor(mScrimInFront.getColors());
+ }
+
+ if (mAnimateKeyguardFadingOut || mForceHideScrims) {
+ setScrimInFrontAlpha(0f);
+ setScrimBehindAlpha(0f);
+ } else if (mWakeAndUnlocking) {
+ // During wake and unlock, we first hide everything behind a black scrim, which then
+ // gets faded out from animateKeyguardFadingOut. This must never be animated.
+ mAnimateChange = false;
+ if (mDozing) {
+ setScrimInFrontAlpha(0f);
+ setScrimBehindAlpha(1f);
+ } else {
+ setScrimInFrontAlpha(1f);
+ setScrimBehindAlpha(0f);
+ }
+ } else if (!mKeyguardShowing && !mBouncerShowing && !mWakingUpFromAodStarting) {
+ updateScrimNormal();
+ setScrimInFrontAlpha(0);
+ } else {
+ updateScrimKeyguard();
+ }
+ mAnimateChange = false;
+ dispatchScrimsVisible();
+ }
+
+ private void dispatchScrimsVisible() {
+ boolean scrimsVisible = mScrimBehind.getViewAlpha() > 0 || mScrimInFront.getViewAlpha() > 0;
+
+ if (mScrimsVisble != scrimsVisible) {
+ mScrimsVisble = scrimsVisible;
+
+ mScrimVisibleListener.accept(scrimsVisible);
+ }
+ }
+
+ private void updateScrimKeyguard() {
+ if (mTracking && mDarkenWhileDragging) {
+ float behindFraction = Math.max(0, Math.min(mFraction, 1));
+ float fraction = 1 - behindFraction;
+ fraction = (float) Math.pow(fraction, 0.8f);
+ behindFraction = (float) Math.pow(behindFraction, 0.8f);
+ setScrimInFrontAlpha(fraction * getScrimInFrontAlpha());
+ setScrimBehindAlpha(behindFraction * mScrimBehindAlphaKeyguard);
+ } else if (mBouncerShowing && !mBouncerIsKeyguard) {
+ setScrimInFrontAlpha(getScrimInFrontAlpha());
+ updateScrimNormal();
+ } else if (mBouncerShowing) {
+ setScrimInFrontAlpha(0f);
+ setScrimBehindAlpha(mScrimBehindAlpha);
+ } else {
+ float fraction = Math.max(0, Math.min(mFraction, 1));
+ if (mWakingUpFromAodStarting) {
+ setScrimInFrontAlpha(1f);
+ } else {
+ setScrimInFrontAlpha(0f);
+ }
+ setScrimBehindAlpha(fraction
+ * (mScrimBehindAlphaKeyguard - mScrimBehindAlphaUnlocking)
+ + mScrimBehindAlphaUnlocking);
+ }
+ }
+
+ private void updateScrimNormal() {
+ float frac = mFraction;
+ // let's start this 20% of the way down the screen
+ frac = frac * 1.2f - 0.2f;
+ if (frac <= 0) {
+ setScrimBehindAlpha(0);
+ } else {
+ // woo, special effects
+ final float k = (float)(1f-0.5f*(1f-Math.cos(3.14159f * Math.pow(1f-frac, 2f))));
+ setScrimBehindAlpha(k * mScrimBehindAlpha);
+ }
+ }
+
+ private void setScrimBehindAlpha(float alpha) {
+ setScrimAlpha(mScrimBehind, alpha);
+ }
+
+ private void setScrimInFrontAlpha(float alpha) {
+ setScrimAlpha(mScrimInFront, alpha);
+ if (alpha == 0f) {
+ mScrimInFront.setClickable(false);
+ } else {
+ // Eat touch events (unless dozing).
+ mScrimInFront.setClickable(!mDozing);
+ }
+ }
+
+ private void setScrimAlpha(View scrim, float alpha) {
+ updateScrim(mAnimateChange, scrim, alpha, getCurrentScrimAlpha(scrim));
+ }
+
+ protected float getDozeAlpha(View scrim) {
+ return scrim == mScrimBehind ? mDozeBehindAlpha : mDozeInFrontAlpha;
+ }
+
+ protected float getCurrentScrimAlpha(View scrim) {
+ return scrim == mScrimBehind ? mCurrentBehindAlpha
+ : scrim == mScrimInFront ? mCurrentInFrontAlpha
+ : mCurrentHeadsUpAlpha;
+ }
+
+ private void setCurrentScrimAlpha(View scrim, float alpha) {
+ if (scrim == mScrimBehind) {
+ mCurrentBehindAlpha = alpha;
+ mLightBarController.setScrimAlpha(mCurrentBehindAlpha);
+ } else if (scrim == mScrimInFront) {
+ mCurrentInFrontAlpha = alpha;
+ } else {
+ alpha = Math.max(0.0f, Math.min(1.0f, alpha));
+ mCurrentHeadsUpAlpha = alpha;
+ }
+ }
+
+ private void updateScrimColor(View scrim) {
+ float alpha1 = getCurrentScrimAlpha(scrim);
+ if (scrim instanceof ScrimView) {
+ ScrimView scrimView = (ScrimView) scrim;
+ float dozeAlpha = getDozeAlpha(scrim);
+ float alpha = 1 - (1 - alpha1) * (1 - dozeAlpha);
+ alpha = Math.max(0, Math.min(1.0f, alpha));
+ scrimView.setViewAlpha(alpha);
+
+ Trace.traceCounter(Trace.TRACE_TAG_APP,
+ scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha",
+ (int) (alpha * 255));
+
+ int dozeTint = Color.TRANSPARENT;
+
+ boolean dozing = mAnimatingDozeUnlock || mDozing;
+ boolean frontScrimDozing = mWakingUpFromAodInProgress;
+ if (dozing || frontScrimDozing && scrim == mScrimInFront) {
+ dozeTint = Color.BLACK;
+ }
+ Trace.traceCounter(Trace.TRACE_TAG_APP,
+ scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint",
+ dozeTint == Color.BLACK ? 1 : 0);
+
+ scrimView.setTint(dozeTint);
+ } else {
+ scrim.setAlpha(alpha1);
+ }
+ dispatchScrimsVisible();
+ }
+
+ private void startScrimAnimation(final View scrim, float target) {
+ float current = getCurrentScrimAlpha(scrim);
+ ValueAnimator anim = ValueAnimator.ofFloat(current, target);
+ anim.addUpdateListener(animation -> {
+ float alpha = (float) animation.getAnimatedValue();
+ setCurrentScrimAlpha(scrim, alpha);
+ updateScrimColor(scrim);
+ dispatchScrimsVisible();
+ });
+ anim.setInterpolator(getInterpolator());
+ anim.setStartDelay(mAnimationDelay);
+ anim.setDuration(mDurationOverride != -1 ? mDurationOverride : ANIMATION_DURATION);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mDeferFinishedListener && mOnAnimationFinished != null) {
+ mOnAnimationFinished.run();
+ mOnAnimationFinished = null;
+ }
+ if (mKeyguardFadingOutInProgress) {
+ mKeyguardFadeoutAnimation = null;
+ mKeyguardFadingOutInProgress = false;
+ mAnimatingDozeUnlock = false;
+ }
+ if (mWakingUpFromAodAnimationRunning && !mDeferFinishedListener) {
+ mWakingUpFromAodAnimationRunning = false;
+ mWakingUpFromAodInProgress = false;
+ }
+ scrim.setTag(TAG_KEY_ANIM, null);
+ scrim.setTag(TAG_KEY_ANIM_TARGET, null);
+ dispatchScrimsVisible();
+ }
+ });
+ anim.start();
+ if (mAnimateKeyguardFadingOut) {
+ mKeyguardFadingOutInProgress = true;
+ mKeyguardFadeoutAnimation = anim;
+ }
+ if (mWakingUpFromAodInProgress) {
+ mWakingUpFromAodAnimationRunning = true;
+ }
+ if (mSkipFirstFrame) {
+ anim.setCurrentPlayTime(16);
+ }
+ scrim.setTag(TAG_KEY_ANIM, anim);
+ scrim.setTag(TAG_KEY_ANIM_TARGET, target);
+ }
+
+ protected Interpolator getInterpolator() {
+ if (mAnimateKeyguardFadingOut && mKeyguardUpdateMonitor.needsSlowUnlockTransition()) {
+ return KEYGUARD_FADE_OUT_INTERPOLATOR_LOCKED;
+ } else if (mAnimateKeyguardFadingOut) {
+ return KEYGUARD_FADE_OUT_INTERPOLATOR;
+ } else {
+ return mInterpolator;
+ }
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ mScrimBehind.getViewTreeObserver().removeOnPreDrawListener(this);
+ mUpdatePending = false;
+ if (mDontAnimateBouncerChanges) {
+ mDontAnimateBouncerChanges = false;
+ }
+ updateScrims();
+ mDurationOverride = -1;
+ mAnimationDelay = 0;
+ mSkipFirstFrame = false;
+
+ // Make sure that we always call the listener even if we didn't start an animation.
+ endAnimateKeyguardFadingOut(false /* force */);
+ return true;
+ }
+
+ private void endAnimateKeyguardFadingOut(boolean force) {
+ mAnimateKeyguardFadingOut = false;
+ if (force || (!isAnimating(mScrimInFront) && !isAnimating(mScrimBehind))) {
+ if (mOnAnimationFinished != null) {
+ mOnAnimationFinished.run();
+ mOnAnimationFinished = null;
+ }
+ mKeyguardFadingOutInProgress = false;
+ if (!mWakeAndUnlocking || force)
+ mAnimatingDozeUnlock = false;
+ }
+ }
+
+ private boolean isAnimating(View scrim) {
+ return scrim.getTag(TAG_KEY_ANIM) != null;
+ }
+
+ public void setDrawBehindAsSrc(boolean asSrc) {
+ mScrimBehind.setDrawAsSrc(asSrc);
+ }
+
+ @Override
+ public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+ }
+
+ @Override
+ public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
+ mPinnedHeadsUpCount++;
+ updateHeadsUpScrim(true);
+ }
+
+ @Override
+ public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
+ mPinnedHeadsUpCount--;
+ if (headsUp == mDraggedHeadsUpView) {
+ mDraggedHeadsUpView = null;
+ mTopHeadsUpDragAmount = 0.0f;
+ }
+ updateHeadsUpScrim(true);
+ }
+
+ @Override
+ public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+ }
+
+ private void updateHeadsUpScrim(boolean animate) {
+ updateScrim(animate, mHeadsUpScrim, calculateHeadsUpAlpha(), mCurrentHeadsUpAlpha);
+ }
+
+ private void updateScrim(boolean animate, View scrim, float alpha, float currentAlpha) {
+ if (mKeyguardFadingOutInProgress && mKeyguardFadeoutAnimation.getCurrentPlayTime() != 0) {
+ return;
+ }
+
+ ValueAnimator previousAnimator = ViewState.getChildTag(scrim,
+ TAG_KEY_ANIM);
+ float animEndValue = -1;
+ if (previousAnimator != null) {
+ if (animate || alpha == currentAlpha) {
+ // We are not done yet! Defer calling the finished listener.
+ if (animate) {
+ mDeferFinishedListener = true;
+ }
+ previousAnimator.cancel();
+ mDeferFinishedListener = false;
+ } else {
+ animEndValue = ViewState.getChildTag(scrim, TAG_END_ALPHA);
+ }
+ }
+ if (alpha != currentAlpha && alpha != animEndValue) {
+ if (animate) {
+ startScrimAnimation(scrim, alpha);
+ scrim.setTag(TAG_START_ALPHA, currentAlpha);
+ scrim.setTag(TAG_END_ALPHA, alpha);
+ } else {
+ if (previousAnimator != null) {
+ float previousStartValue = ViewState.getChildTag(scrim, TAG_START_ALPHA);
+ float previousEndValue = ViewState.getChildTag(scrim, TAG_END_ALPHA);
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ float relativeDiff = alpha - previousEndValue;
+ float newStartValue = previousStartValue + relativeDiff;
+ newStartValue = Math.max(0, Math.min(1.0f, newStartValue));
+ values[0].setFloatValues(newStartValue, alpha);
+ scrim.setTag(TAG_START_ALPHA, newStartValue);
+ scrim.setTag(TAG_END_ALPHA, alpha);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ } else {
+ // update the alpha directly
+ setCurrentScrimAlpha(scrim, alpha);
+ updateScrimColor(scrim);
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the amount the current top heads up view is dragged. The range is from 0 to 1 and 0 means
+ * the heads up is in its resting space and 1 means it's fully dragged out.
+ *
+ * @param draggedHeadsUpView the dragged view
+ * @param topHeadsUpDragAmount how far is it dragged
+ */
+ public void setTopHeadsUpDragAmount(View draggedHeadsUpView, float topHeadsUpDragAmount) {
+ mTopHeadsUpDragAmount = topHeadsUpDragAmount;
+ mDraggedHeadsUpView = draggedHeadsUpView;
+ updateHeadsUpScrim(false);
+ }
+
+ private float calculateHeadsUpAlpha() {
+ float alpha;
+ if (mPinnedHeadsUpCount >= 2) {
+ alpha = 1.0f;
+ } else if (mPinnedHeadsUpCount == 0) {
+ alpha = 0.0f;
+ } else {
+ alpha = 1.0f - mTopHeadsUpDragAmount;
+ }
+ float expandFactor = (1.0f - mFraction);
+ expandFactor = Math.max(expandFactor, 0.0f);
+ return alpha * expandFactor;
+ }
+
+ public void forceHideScrims(boolean hide, boolean animated) {
+ mForceHideScrims = hide;
+ mAnimateChange = animated;
+ scheduleUpdate();
+ }
+
+ public void dontAnimateBouncerChangesUntilNextFrame() {
+ mDontAnimateBouncerChanges = true;
+ }
+
+ public void setExcludedBackgroundArea(Rect area) {
+ mScrimBehind.setExcludedArea(area);
+ }
+
+ public int getBackgroundColor() {
+ int color = mLockColors.getMainColor();
+ return Color.argb((int) (mScrimBehind.getAlpha() * Color.alpha(color)),
+ Color.red(color), Color.green(color), Color.blue(color));
+ }
+
+ public void setScrimBehindChangeRunnable(Runnable changeRunnable) {
+ mScrimBehind.setChangeRunnable(changeRunnable);
+ }
+
+ public void onDensityOrFontScaleChanged() {
+ ViewGroup.LayoutParams layoutParams = mHeadsUpScrim.getLayoutParams();
+ layoutParams.height = mHeadsUpScrim.getResources().getDimensionPixelSize(
+ R.dimen.heads_up_scrim_height);
+ mHeadsUpScrim.setLayoutParams(layoutParams);
+ }
+
+ public void setCurrentUser(int currentUser) {
+ // Don't care in the base class.
+ }
+
+ @Override
+ public void onColorsChanged(ColorExtractor colorExtractor, int which) {
+ if ((which & WallpaperManager.FLAG_LOCK) != 0) {
+ mLockColors = mColorExtractor.getColors(WallpaperManager.FLAG_LOCK,
+ ColorExtractor.TYPE_DARK, true /* ignoreVisibility */);
+ mNeedsDrawableColorUpdate = true;
+ scheduleUpdate();
+ }
+ if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
+ mSystemColors = mColorExtractor.getColors(WallpaperManager.FLAG_SYSTEM,
+ ColorExtractor.TYPE_DARK, mKeyguardShowing);
+ mNeedsDrawableColorUpdate = true;
+ scheduleUpdate();
+ }
+ }
+
+ public void dump(PrintWriter pw) {
+ pw.println(" ScrimController:");
+
+ pw.print(" frontScrim:"); pw.print(" viewAlpha="); pw.print(mScrimInFront.getViewAlpha());
+ pw.print(" alpha="); pw.print(mCurrentInFrontAlpha);
+ pw.print(" dozeAlpha="); pw.print(mDozeInFrontAlpha);
+ pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimInFront.getTint()));
+
+ pw.print(" backScrim:"); pw.print(" viewAlpha="); pw.print(mScrimBehind.getViewAlpha());
+ pw.print(" alpha="); pw.print(mCurrentBehindAlpha);
+ pw.print(" dozeAlpha="); pw.print(mDozeBehindAlpha);
+ pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimBehind.getTint()));
+
+ pw.print(" mBouncerShowing="); pw.println(mBouncerShowing);
+ pw.print(" mTracking="); pw.println(mTracking);
+ pw.print(" mForceHideScrims="); pw.println(mForceHideScrims);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/SettingsButton.java b/com/android/systemui/statusbar/phone/SettingsButton.java
new file mode 100644
index 0000000..6220fcb
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/SettingsButton.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2015 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.Animator.AnimatorListener;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+
+import com.android.keyguard.AlphaOptimizedImageButton;
+import com.android.systemui.Interpolators;
+
+public class SettingsButton extends AlphaOptimizedImageButton {
+
+ private static final long LONG_PRESS_LENGTH = 1000;
+ private static final long ACCEL_LENGTH = 750;
+ private static final long FULL_SPEED_LENGTH = 375;
+ private static final long RUN_DURATION = 350;
+
+ private boolean mUpToSpeed;
+ private ObjectAnimator mAnimator;
+
+ private float mSlop;
+
+ public SettingsButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ }
+
+ public boolean isAnimating() {
+ return mAnimator != null && mAnimator.isRunning();
+ }
+
+ public boolean isTunerClick() {
+ return mUpToSpeed;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ postDelayed(mLongPressCallback, LONG_PRESS_LENGTH);
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mUpToSpeed) {
+ startExitAnimation();
+ } else {
+ cancelLongClick();
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ cancelLongClick();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ float x = event.getX();
+ float y = event.getY();
+ if ((x < -mSlop) || (y < -mSlop) || (x > getWidth() + mSlop)
+ || (y > getHeight() + mSlop)) {
+ cancelLongClick();
+ }
+ break;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ private void cancelLongClick() {
+ cancelAnimation();
+ mUpToSpeed = false;
+ removeCallbacks(mLongPressCallback);
+ }
+
+ private void cancelAnimation() {
+ if (mAnimator != null) {
+ mAnimator.removeAllListeners();
+ mAnimator.cancel();
+ mAnimator = null;
+ }
+ }
+
+ private void startExitAnimation() {
+ animate()
+ .translationX(((View) getParent().getParent()).getWidth() - getX())
+ .alpha(0)
+ .setDuration(RUN_DURATION)
+ .setInterpolator(AnimationUtils.loadInterpolator(mContext,
+ android.R.interpolator.accelerate_cubic))
+ .setListener(new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setAlpha(1f);
+ setTranslationX(0);
+ cancelLongClick();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+ })
+ .start();
+ }
+
+ protected void startAccelSpin() {
+ cancelAnimation();
+ mAnimator = ObjectAnimator.ofFloat(this, View.ROTATION, 0, 360);
+ mAnimator.setInterpolator(AnimationUtils.loadInterpolator(mContext,
+ android.R.interpolator.accelerate_quad));
+ mAnimator.setDuration(ACCEL_LENGTH);
+ mAnimator.addListener(new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ startContinuousSpin();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+ });
+ mAnimator.start();
+ }
+
+ protected void startContinuousSpin() {
+ cancelAnimation();
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ mUpToSpeed = true;
+ mAnimator = ObjectAnimator.ofFloat(this, View.ROTATION, 0, 360);
+ mAnimator.setInterpolator(Interpolators.LINEAR);
+ mAnimator.setDuration(FULL_SPEED_LENGTH);
+ mAnimator.setRepeatCount(Animation.INFINITE);
+ mAnimator.start();
+ }
+
+ private final Runnable mLongPressCallback = new Runnable() {
+ @Override
+ public void run() {
+ startAccelSpin();
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/phone/SignalDrawable.java b/com/android/systemui/statusbar/phone/SignalDrawable.java
new file mode 100644
index 0000000..15ef742
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/SignalDrawable.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2017 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.ArgbEvaluator;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.Path.FillType;
+import android.graphics.Path.Op;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.LayoutDirection;
+
+import com.android.settingslib.R;
+import com.android.settingslib.Utils;
+import com.android.systemui.qs.SlashDrawable;
+
+public class SignalDrawable extends Drawable {
+
+ private static final String TAG = "SignalDrawable";
+
+ private static final int NUM_DOTS = 3;
+
+ private static final float VIEWPORT = 24f;
+ private static final float PAD = 2f / VIEWPORT;
+ private static final float CUT_OUT = 7.9f / VIEWPORT;
+
+ private static final float DOT_SIZE = 3f / VIEWPORT;
+ private static final float DOT_PADDING = 1f / VIEWPORT;
+ private static final float DOT_CUT_WIDTH = (DOT_SIZE * 3) + (DOT_PADDING * 5);
+ private static final float DOT_CUT_HEIGHT = (DOT_SIZE * 1) + (DOT_PADDING * 1);
+
+ private static final float[] FIT = {2.26f, -3.02f, 1.76f};
+
+ // All of these are masks to push all of the drawable state into one int for easy callbacks
+ // and flow through sysui.
+ private static final int LEVEL_MASK = 0xff;
+ private static final int NUM_LEVEL_SHIFT = 8;
+ private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT;
+ private static final int STATE_SHIFT = 16;
+ private static final int STATE_MASK = 0xff << STATE_SHIFT;
+ private static final int STATE_NONE = 0;
+ private static final int STATE_EMPTY = 1;
+ private static final int STATE_CUT = 2;
+ private static final int STATE_CARRIER_CHANGE = 3;
+ private static final int STATE_AIRPLANE = 4;
+
+ private static final long DOT_DELAY = 1000;
+
+ private static float[][] X_PATH = new float[][]{
+ {21.9f / VIEWPORT, 17.0f / VIEWPORT},
+ {-1.1f / VIEWPORT, -1.1f / VIEWPORT},
+ {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
+ {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
+ {-1.1f / VIEWPORT, 1.1f / VIEWPORT},
+ {1.9f / VIEWPORT, 1.9f / VIEWPORT},
+ {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
+ {1.1f / VIEWPORT, 1.1f / VIEWPORT},
+ {1.9f / VIEWPORT, -1.9f / VIEWPORT},
+ {1.9f / VIEWPORT, 1.9f / VIEWPORT},
+ {1.1f / VIEWPORT, -1.1f / VIEWPORT},
+ {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
+ };
+
+ // Rounded corners are achieved by arcing a circle of radius `R` from its tangent points along
+ // the curve (curve ≡ triangle). On the top and left corners of the triangle, the tangents are
+ // as follows:
+ // 1) Along the straight lines (y = 0 and x = width):
+ // Ps = circleOffset + R
+ // 2) Along the diagonal line (y = x):
+ // Pd = √((Ps^2) / 2)
+ // or (remember: sin(π/4) ≈ 0.7071)
+ // Pd = (circleOffset + R - 0.7071, height - R - 0.7071)
+ // Where Pd is the (x,y) coords of the point that intersects the circle at the bottom
+ // left of the triangle
+ private static final float RADIUS_RATIO = 0.75f / 17f;
+ private static final float DIAG_OFFSET_MULTIPLIER = 0.707107f;
+ // How far the circle defining the corners is inset from the edges
+ private final float mAppliedCornerInset;
+
+ private static final float INV_TAN = 1f / (float) Math.tan(Math.PI / 8f);
+ private static final float CUT_WIDTH_DP = 1f / 12f;
+
+ // Where the top and left points of the triangle would be if not for rounding
+ private final PointF mVirtualTop = new PointF();
+ private final PointF mVirtualLeft = new PointF();
+
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final int mDarkModeBackgroundColor;
+ private final int mDarkModeFillColor;
+ private final int mLightModeBackgroundColor;
+ private final int mLightModeFillColor;
+ private final Path mFullPath = new Path();
+ private final Path mForegroundPath = new Path();
+ private final Path mXPath = new Path();
+ // Cut out when STATE_EMPTY
+ private final Path mCutPath = new Path();
+ // Draws the slash when in airplane mode
+ private final SlashArtist mSlash = new SlashArtist();
+ private final Handler mHandler;
+ private float mOldDarkIntensity = -1;
+ private float mNumLevels = 1;
+ private int mIntrinsicSize;
+ private int mLevel;
+ private int mState;
+ private boolean mVisible;
+ private boolean mAnimating;
+ private int mCurrentDot;
+
+ public SignalDrawable(Context context) {
+ mDarkModeBackgroundColor =
+ Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_background);
+ mDarkModeFillColor =
+ Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_fill);
+ mLightModeBackgroundColor =
+ Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_background);
+ mLightModeFillColor =
+ Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill);
+ mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
+
+ mHandler = new Handler();
+ setDarkIntensity(0);
+
+ mAppliedCornerInset = context.getResources()
+ .getDimensionPixelSize(R.dimen.stat_sys_mobile_signal_circle_inset);
+ }
+
+ public void setIntrinsicSize(int size) {
+ mIntrinsicSize = size;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mIntrinsicSize;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mIntrinsicSize;
+ }
+
+ public void setNumLevels(int levels) {
+ if (levels == mNumLevels) return;
+ mNumLevels = levels;
+ invalidateSelf();
+ }
+
+ private void setSignalState(int state) {
+ if (state == mState) return;
+ mState = state;
+ updateAnimation();
+ invalidateSelf();
+ }
+
+ private void updateAnimation() {
+ boolean shouldAnimate = (mState == STATE_CARRIER_CHANGE) && mVisible;
+ if (shouldAnimate == mAnimating) return;
+ mAnimating = shouldAnimate;
+ if (shouldAnimate) {
+ mChangeDot.run();
+ } else {
+ mHandler.removeCallbacks(mChangeDot);
+ }
+ }
+
+ @Override
+ protected boolean onLevelChange(int state) {
+ setNumLevels(getNumLevels(state));
+ setSignalState(getState(state));
+ int level = getLevel(state);
+ if (level != mLevel) {
+ mLevel = level;
+ invalidateSelf();
+ }
+ return true;
+ }
+
+ public void setColors(int background, int foreground) {
+ mPaint.setColor(background);
+ mForegroundPaint.setColor(foreground);
+ }
+
+ public void setDarkIntensity(float darkIntensity) {
+ if (darkIntensity == mOldDarkIntensity) {
+ return;
+ }
+ mPaint.setColor(getBackgroundColor(darkIntensity));
+ mForegroundPaint.setColor(getFillColor(darkIntensity));
+ mOldDarkIntensity = darkIntensity;
+ invalidateSelf();
+ }
+
+ private int getFillColor(float darkIntensity) {
+ return getColorForDarkIntensity(
+ darkIntensity, mLightModeFillColor, mDarkModeFillColor);
+ }
+
+ private int getBackgroundColor(float darkIntensity) {
+ return getColorForDarkIntensity(
+ darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
+ }
+
+ private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
+ return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ final float width = getBounds().width();
+ final float height = getBounds().height();
+
+ boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
+ if (isRtl) {
+ canvas.save();
+ // Mirror the drawable
+ canvas.translate(width, 0);
+ canvas.scale(-1.0f, 1.0f);
+ }
+ mFullPath.reset();
+ mFullPath.setFillType(FillType.WINDING);
+
+ final float padding = Math.round(PAD * width);
+ final float cornerRadius = RADIUS_RATIO * height;
+ // Offset from circle where the hypotenuse meets the circle
+ final float diagOffset = DIAG_OFFSET_MULTIPLIER * cornerRadius;
+
+ // 1 - Bottom right, above corner
+ mFullPath.moveTo(width - padding, height - padding - cornerRadius);
+ // 2 - Line to top right, below corner
+ mFullPath.lineTo(width - padding, padding + cornerRadius + mAppliedCornerInset);
+ // 3 - Arc to top right, on hypotenuse
+ mFullPath.arcTo(
+ width - padding - (2 * cornerRadius),
+ padding + mAppliedCornerInset,
+ width - padding,
+ padding + mAppliedCornerInset + (2 * cornerRadius),
+ 0.f, -135.f, false
+ );
+ // 4 - Line to bottom left, on hypotenuse
+ mFullPath.lineTo(padding + mAppliedCornerInset + cornerRadius - diagOffset,
+ height - padding - cornerRadius - diagOffset);
+ // 5 - Arc to bottom left, on leg
+ mFullPath.arcTo(
+ padding + mAppliedCornerInset,
+ height - padding - (2 * cornerRadius),
+ padding + mAppliedCornerInset + ( 2 * cornerRadius),
+ height - padding,
+ -135.f, -135.f, false
+ );
+ // 6 - Line to bottom rght, before corner
+ mFullPath.lineTo(width - padding - cornerRadius, height - padding);
+ // 7 - Arc to beginning (bottom right, above corner)
+ mFullPath.arcTo(
+ width - padding - (2 * cornerRadius),
+ height - padding - (2 * cornerRadius),
+ width - padding,
+ height - padding,
+ 90.f, -90.f, false
+ );
+
+ if (mState == STATE_CARRIER_CHANGE) {
+ float cutWidth = (DOT_CUT_WIDTH * width);
+ float cutHeight = (DOT_CUT_HEIGHT * width);
+ float dotSize = (DOT_SIZE * height);
+ float dotPadding = (DOT_PADDING * height);
+
+ mFullPath.moveTo(width - padding, height - padding);
+ mFullPath.rLineTo(-cutWidth, 0);
+ mFullPath.rLineTo(0, -cutHeight);
+ mFullPath.rLineTo(cutWidth, 0);
+ mFullPath.rLineTo(0, cutHeight);
+ float dotSpacing = dotPadding * 2 + dotSize;
+ float x = width - padding - dotSize;
+ float y = height - padding - dotSize;
+ mForegroundPath.reset();
+ drawDot(mFullPath, mForegroundPath, x, y, dotSize, 2);
+ drawDot(mFullPath, mForegroundPath, x - dotSpacing, y, dotSize, 1);
+ drawDot(mFullPath, mForegroundPath, x - dotSpacing * 2, y, dotSize, 0);
+ } else if (mState == STATE_CUT) {
+ float cut = (CUT_OUT * width);
+ mFullPath.moveTo(width - padding, height - padding);
+ mFullPath.rLineTo(-cut, 0);
+ mFullPath.rLineTo(0, -cut);
+ mFullPath.rLineTo(cut, 0);
+ mFullPath.rLineTo(0, cut);
+ }
+
+ if (mState == STATE_EMPTY) {
+ // Where the corners would be if this were a real triangle
+ mVirtualTop.set(
+ width - padding,
+ (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius));
+ mVirtualLeft.set(
+ (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius),
+ height - padding);
+
+ final float cutWidth = CUT_WIDTH_DP * height;
+ final float cutDiagInset = cutWidth * INV_TAN;
+
+ // Cut out a smaller triangle from the center of mFullPath
+ mCutPath.reset();
+ mCutPath.setFillType(FillType.WINDING);
+ mCutPath.moveTo(width - padding - cutWidth, height - padding - cutWidth);
+ mCutPath.lineTo(width - padding - cutWidth, mVirtualTop.y + cutDiagInset);
+ mCutPath.lineTo(mVirtualLeft.x + cutDiagInset, height - padding - cutWidth);
+ mCutPath.lineTo(width - padding - cutWidth, height - padding - cutWidth);
+
+ // Draw empty state as only background
+ mForegroundPath.reset();
+ mFullPath.op(mCutPath, Path.Op.DIFFERENCE);
+ } else if (mState == STATE_AIRPLANE) {
+ // Airplane mode is slashed, fully drawn background
+ mForegroundPath.reset();
+ mSlash.draw((int) height, (int) width, canvas, mPaint);
+ } else if (mState != STATE_CARRIER_CHANGE) {
+ mForegroundPath.reset();
+ int sigWidth = Math.round(calcFit(mLevel / (mNumLevels - 1)) * (width - 2 * padding));
+ mForegroundPath.addRect(padding, padding, padding + sigWidth, height - padding,
+ Direction.CW);
+ mForegroundPath.op(mFullPath, Op.INTERSECT);
+ }
+
+ canvas.drawPath(mFullPath, mPaint);
+ canvas.drawPath(mForegroundPath, mForegroundPaint);
+ if (mState == STATE_CUT) {
+ mXPath.reset();
+ mXPath.moveTo(X_PATH[0][0] * width, X_PATH[0][1] * height);
+ for (int i = 1; i < X_PATH.length; i++) {
+ mXPath.rLineTo(X_PATH[i][0] * width, X_PATH[i][1] * height);
+ }
+ canvas.drawPath(mXPath, mForegroundPaint);
+ }
+ if (isRtl) {
+ canvas.restore();
+ }
+ }
+
+ private void drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize,
+ int i) {
+ Path p = (i == mCurrentDot) ? foregroundPath : fullPath;
+ p.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
+ }
+
+ // This is a fit line based on previous values of provided in assets, but if
+ // you look at the a plot of this actual fit, it makes a lot of sense, what it does
+ // is compress the areas that are very visually easy to see changes (the middle sections)
+ // and spread out the sections that are hard to see (each end of the icon).
+ // The current fit is cubic, but pretty easy to change the way the code is written (just add
+ // terms to the end of FIT).
+ private float calcFit(float v) {
+ float ret = 0;
+ float t = v;
+ for (int i = 0; i < FIT.length; i++) {
+ ret += FIT[i] * t;
+ t *= v;
+ }
+ return ret;
+ }
+
+ @Override
+ public int getAlpha() {
+ return mPaint.getAlpha();
+ }
+
+ @Override
+ public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
+ mPaint.setAlpha(alpha);
+ mForegroundPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ mPaint.setColorFilter(colorFilter);
+ mForegroundPaint.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public int getOpacity() {
+ return 255;
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ mVisible = visible;
+ updateAnimation();
+ return super.setVisible(visible, restart);
+ }
+
+ private final Runnable mChangeDot = new Runnable() {
+ @Override
+ public void run() {
+ if (++mCurrentDot == NUM_DOTS) {
+ mCurrentDot = 0;
+ }
+ invalidateSelf();
+ mHandler.postDelayed(mChangeDot, DOT_DELAY);
+ }
+ };
+
+ public static int getLevel(int fullState) {
+ return fullState & LEVEL_MASK;
+ }
+
+ public static int getState(int fullState) {
+ return (fullState & STATE_MASK) >> STATE_SHIFT;
+ }
+
+ public static int getNumLevels(int fullState) {
+ return (fullState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
+ }
+
+ public static int getState(int level, int numLevels, boolean cutOut) {
+ return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
+ | (numLevels << NUM_LEVEL_SHIFT)
+ | level;
+ }
+
+ public static int getCarrierChangeState(int numLevels) {
+ return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
+ }
+
+ public static int getEmptyState(int numLevels) {
+ return (STATE_EMPTY << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
+ }
+
+ public static int getAirplaneModeState(int numLevels) {
+ return (STATE_AIRPLANE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
+ }
+
+ private final class SlashArtist {
+ // These values are derived in un-rotated (vertical) orientation
+ private static final float SLASH_WIDTH = 1.8384776f;
+ private static final float SLASH_HEIGHT = 22f;
+ private static final float CENTER_X = 10.65f;
+ private static final float CENTER_Y = 15.869239f;
+ private static final float SCALE = 24f;
+
+ // Bottom is derived during animation
+ private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
+ private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
+ private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
+ private static final float BOTTOM = (CENTER_Y + (SLASH_HEIGHT / 2)) / SCALE;
+ // Draw the slash washington-monument style; rotate to no-u-turn style
+ private static final float ROTATION = -45f;
+
+ private final Path mPath = new Path();
+ private final RectF mSlashRect = new RectF();
+
+ void draw(int height, int width, @NonNull Canvas canvas, Paint paint) {
+ Matrix m = new Matrix();
+ final float radius = scale(SlashDrawable.CORNER_RADIUS, width);
+ updateRect(
+ scale(LEFT, width),
+ scale(TOP, height),
+ scale(RIGHT, width),
+ scale(BOTTOM, height));
+
+ mPath.reset();
+ // Draw the slash vertically
+ mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW);
+ m.setRotate(ROTATION, width / 2, height / 2);
+ mPath.transform(m);
+ canvas.drawPath(mPath, paint);
+
+ // Rotate back to vertical, and draw the cut-out rect next to this one
+ m.setRotate(-ROTATION, width / 2, height / 2);
+ mPath.transform(m);
+ m.setTranslate(mSlashRect.width(), 0);
+ mPath.transform(m);
+ mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW);
+ m.setRotate(ROTATION, width / 2, height / 2);
+ mPath.transform(m);
+ canvas.clipOutPath(mPath);
+ }
+
+ void updateRect(float left, float top, float right, float bottom) {
+ mSlashRect.left = left;
+ mSlashRect.top = top;
+ mSlashRect.right = right;
+ mSlashRect.bottom = bottom;
+ }
+
+ private float scale(float frac, int width) {
+ return frac * width;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBar.java b/com/android/systemui/statusbar/phone/StatusBar.java
new file mode 100644
index 0000000..efc8d8b
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBar.java
@@ -0,0 +1,7582 @@
+/*
+ * Copyright (C) 2010 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 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.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
+import static com.android.systemui.statusbar.notification.NotificationInflater.InflationCallback;
+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;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManager.StackId;
+import android.app.ActivityOptions;
+import android.app.INotificationManager;
+import android.app.KeyguardManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.app.StatusBarManager;
+import android.app.TaskStackBuilder;
+import android.app.WallpaperColors;
+import android.app.WallpaperManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentCallbacks2;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.om.IOverlayManager;
+import android.content.om.OverlayInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+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.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.metrics.LogMaker;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.service.notification.NotificationListenerService.RankingMap;
+import android.service.notification.StatusBarNotification;
+import android.service.vr.IVrManager;
+import android.service.vr.IVrStateCallbacks;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.view.Display;
+import android.view.HapticFeedbackConstants;
+import android.view.IWindowManager;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.ThreadedRenderer;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewStub;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.DateTimeView;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.internal.util.NotificationMessagingUtil;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardHostView.OnDismissAction;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.ActivityStarterDelegate;
+import com.android.systemui.AutoReinflateContainer;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.DemoMode;
+import com.android.systemui.Dependency;
+import com.android.systemui.EventLogTags;
+import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.Interpolators;
+import com.android.systemui.Prefs;
+import com.android.systemui.R;
+import com.android.systemui.RecentsComponent;
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.SystemUI;
+import com.android.systemui.SystemUIFactory;
+import com.android.systemui.UiOffloadThread;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.classifier.FalsingLog;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.doze.DozeHost;
+import com.android.systemui.doze.DozeLog;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.fragments.ExtensionFragmentListener;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
+import com.android.systemui.qs.QSFragment;
+import com.android.systemui.qs.QSPanel;
+import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.car.CarQSFragment;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.ScreenPinningRequest;
+import com.android.systemui.recents.events.EventBus;
+import com.android.systemui.recents.events.activity.AppTransitionFinishedEvent;
+import com.android.systemui.recents.events.activity.UndockingTaskEvent;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.stackdivider.Divider;
+import com.android.systemui.stackdivider.WindowManagerProxy;
+import com.android.systemui.statusbar.ActivatableNotificationView;
+import com.android.systemui.statusbar.BackDropView;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.DismissView;
+import com.android.systemui.statusbar.DragDownHelper;
+import com.android.systemui.statusbar.EmptyShadeView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.GestureRecorder;
+import com.android.systemui.statusbar.KeyboardShortcuts;
+import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationData.Entry;
+import com.android.systemui.statusbar.NotificationGuts;
+import com.android.systemui.statusbar.NotificationInfo;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.NotificationSnooze;
+import com.android.systemui.statusbar.RemoteInputController;
+import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.statusbar.SignalClusterView;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
+import com.android.systemui.statusbar.notification.InflationException;
+import com.android.systemui.statusbar.notification.RowInflaterTask;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
+import com.android.systemui.statusbar.policy.BrightnessMirrorController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+import com.android.systemui.statusbar.policy.ExtensionController;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.KeyguardMonitorImpl;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.statusbar.policy.PreviewInflater;
+import com.android.systemui.statusbar.policy.RemoteInputView;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout
+ .OnChildLocationsChangedListener;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+import com.android.systemui.util.NotificationChannels;
+import com.android.systemui.util.leak.LeakDetector;
+import com.android.systemui.volume.VolumeComponent;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+public class StatusBar extends SystemUI implements DemoMode,
+ DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener,
+ OnHeadsUpChangedListener, VisualStabilityManager.Callback, CommandQueue.Callbacks,
+ ActivatableNotificationView.OnActivatedListener,
+ ExpandableNotificationRow.ExpansionLogger, NotificationData.Environment,
+ ExpandableNotificationRow.OnExpandClickListener, InflationCallback,
+ ColorExtractor.OnColorsChangedListener, ConfigurationListener {
+ public static final boolean MULTIUSER_DEBUG = false;
+
+ public static final boolean ENABLE_REMOTE_INPUT =
+ SystemProperties.getBoolean("debug.enable_remote_input", true);
+ public static final boolean ENABLE_CHILD_NOTIFICATIONS
+ = SystemProperties.getBoolean("debug.child_notifs", true);
+ public static final boolean FORCE_REMOTE_INPUT_HISTORY =
+ SystemProperties.getBoolean("debug.force_remoteinput_history", false);
+ private static boolean ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT = false;
+
+ 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;
+ protected static final int MSG_PRELOAD_RECENT_APPS = 1022;
+ protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023;
+ protected static final int MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU = 1026;
+ protected static final int MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU = 1027;
+
+ protected static final boolean ENABLE_HEADS_UP = true;
+ protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
+
+ // Must match constant in Settings. Used to highlight preferences when linking to Settings.
+ private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
+
+ private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+
+ // Should match the values in PhoneWindowManager
+ public static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
+ public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
+ static public final String SYSTEM_DIALOG_REASON_SCREENSHOT = "screenshot";
+
+ private static final String BANNER_ACTION_CANCEL =
+ "com.android.systemui.statusbar.banner_action_cancel";
+ private static final String BANNER_ACTION_SETUP =
+ "com.android.systemui.statusbar.banner_action_setup";
+ private static final String NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION
+ = "com.android.systemui.statusbar.work_challenge_unlocked_notification_action";
+ public static final String TAG = "StatusBar";
+ public static final boolean DEBUG = false;
+ public static final boolean SPEW = false;
+ public static final boolean DUMPTRUCK = true; // extra dumpsys info
+ public static final boolean DEBUG_GESTURES = false;
+ public static final boolean DEBUG_MEDIA = false;
+ public static final boolean DEBUG_MEDIA_FAKE_ARTWORK = false;
+ public static final boolean DEBUG_CAMERA_LIFT = false;
+
+ public static final boolean DEBUG_WINDOW_STATE = false;
+
+ // additional instrumentation for testing purposes; intended to be left on during development
+ public static final boolean CHATTY = DEBUG;
+
+ public static final boolean SHOW_LOCKSCREEN_MEDIA_ARTWORK = true;
+
+ public static final String ACTION_FAKE_ARTWORK = "fake_artwork";
+
+ 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 STATUS_OR_NAV_TRANSIENT =
+ View.STATUS_BAR_TRANSIENT | View.NAVIGATION_BAR_TRANSIENT;
+ private static final long AUTOHIDE_TIMEOUT_MS = 3000;
+
+ /** The minimum delay in ms between reports of notification visibility. */
+ private static final int VISIBILITY_REPORT_MIN_DELAY_MS = 500;
+
+ /**
+ * The delay to reset the hint text when the hint animation is finished running.
+ */
+ private static final int HINT_RESET_DELAY_MS = 1200;
+
+ private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+ .build();
+
+ public static final int FADE_KEYGUARD_START_DELAY = 100;
+ public static final int FADE_KEYGUARD_DURATION = 300;
+ public static final int FADE_KEYGUARD_DURATION_PULSING = 96;
+
+ /** If true, the system is in the half-boot-to-decryption-screen state.
+ * Prudently disable QS and notifications. */
+ private static final boolean ONLY_CORE_APPS;
+
+ /** If true, the lockscreen will show a distinct wallpaper */
+ private static final boolean ENABLE_LOCKSCREEN_WALLPAPER = true;
+
+ /* If true, the device supports freeform window management.
+ * This affects the status bar UI. */
+ private static final boolean FREEFORM_WINDOW_MANAGEMENT;
+
+ /**
+ * How long to wait before auto-dismissing a notification that was kept for remote input, and
+ * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
+ * these given that they technically don't exist anymore. We wait a bit in case the app issues
+ * an update.
+ */
+ private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
+
+ /**
+ * Never let the alpha become zero for surfaces that draw with SRC - otherwise the RenderNode
+ * won't draw anything and uninitialized memory will show through
+ * if mScrimSrcModeEnabled. Note that 0.001 is rounded down to 0 in
+ * libhwui.
+ */
+ private static final float SRC_MIN_ALPHA = 0.002f;
+
+ static {
+ boolean onlyCoreApps;
+ boolean freeformWindowManagement;
+ try {
+ IPackageManager packageManager =
+ IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
+ onlyCoreApps = packageManager.isOnlyCoreApps();
+ freeformWindowManagement = packageManager.hasSystemFeature(
+ PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT, 0);
+ } catch (RemoteException e) {
+ onlyCoreApps = false;
+ freeformWindowManagement = false;
+ }
+ ONLY_CORE_APPS = onlyCoreApps;
+ FREEFORM_WINDOW_MANAGEMENT = freeformWindowManagement;
+ }
+
+ /**
+ * The {@link StatusBarState} of the status bar.
+ */
+ protected int mState;
+ protected boolean mBouncerShowing;
+ protected boolean mShowLockscreenNotifications;
+ protected boolean mAllowLockscreenRemoteInput;
+
+ PhoneStatusBarPolicy mIconPolicy;
+
+ VolumeComponent mVolumeComponent;
+ BrightnessMirrorController mBrightnessMirrorController;
+ protected FingerprintUnlockController mFingerprintUnlockController;
+ LightBarController mLightBarController;
+ protected LockscreenWallpaper mLockscreenWallpaper;
+
+ int mNaturalBarHeight = -1;
+
+ Point mCurrentDisplaySize = new Point();
+
+ protected StatusBarWindowView mStatusBarWindow;
+ protected PhoneStatusBarView mStatusBarView;
+ private int mStatusBarWindowState = WINDOW_STATE_SHOWING;
+ protected StatusBarWindowManager mStatusBarWindowManager;
+ protected UnlockMethodCache mUnlockMethodCache;
+ private DozeServiceHost mDozeServiceHost = new DozeServiceHost();
+ private boolean mWakeUpComingFromTouch;
+ private PointF mWakeUpTouchLocation;
+
+ int mPixelFormat;
+ Object mQueueLock = new Object();
+
+ protected StatusBarIconController mIconController;
+
+ // expanded notifications
+ protected NotificationPanelView mNotificationPanel; // the sliding/resizing panel within the notification window
+ View mExpandedContents;
+ TextView mNotificationPanelDebugText;
+
+ /**
+ * {@code true} if notifications not part of a group should by default be rendered in their
+ * expanded state. If {@code false}, then only the first notification will be expanded if
+ * possible.
+ */
+ private boolean mAlwaysExpandNonGroupedNotification;
+
+ // settings
+ private QSPanel mQSPanel;
+
+ // top bar
+ protected KeyguardStatusBarView mKeyguardStatusBar;
+ boolean mLeaveOpenOnKeyguardHide;
+ KeyguardIndicationController mKeyguardIndicationController;
+
+ // Keyguard is going away soon.
+ private boolean mKeyguardGoingAway;
+ // Keyguard is actually fading away now.
+ protected boolean mKeyguardFadingAway;
+ protected long mKeyguardFadingAwayDelay;
+ protected long mKeyguardFadingAwayDuration;
+
+ // RemoteInputView to be activated after unlock
+ private View mPendingRemoteInputView;
+ private View mPendingWorkRemoteInputView;
+
+ private View mReportRejectedTouch;
+
+ int mMaxAllowedKeyguardNotifications;
+
+ boolean mExpandedVisible;
+
+ // the tracker view
+ int mTrackingPosition; // the position of the top of the tracking view.
+
+ // Tracking finger for opening/closing.
+ boolean mTracking;
+
+ int[] mAbsPos = new int[2];
+ ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>();
+
+ // for disabling the status bar
+ int mDisabled1 = 0;
+ int mDisabled2 = 0;
+
+ // tracking calls to View.setSystemUiVisibility()
+ int mSystemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
+ private final Rect mLastFullscreenStackBounds = new Rect();
+ private final Rect mLastDockedStackBounds = new Rect();
+ private final Rect mTmpRect = new Rect();
+
+ // last value sent to window manager
+ private int mLastDispatchedSystemUiVisibility = ~View.SYSTEM_UI_FLAG_VISIBLE;
+
+ DisplayMetrics mDisplayMetrics = new DisplayMetrics();
+
+ // XXX: gesture research
+ private final GestureRecorder mGestureRec = DEBUG_GESTURES
+ ? new GestureRecorder("/sdcard/statusbar_gestures.dat")
+ : null;
+
+ private ScreenPinningRequest mScreenPinningRequest;
+
+ private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+
+ // ensure quick settings is disabled until the current user makes it through the setup wizard
+ private boolean mUserSetup = false;
+ private DeviceProvisionedListener mUserSetupObserver = new DeviceProvisionedListener() {
+ @Override
+ public void onUserSetupChanged() {
+ final boolean userSetup = mDeviceProvisionedController.isUserSetup(
+ mDeviceProvisionedController.getCurrentUser());
+ if (MULTIUSER_DEBUG) Log.d(TAG, String.format("User setup changed: " +
+ "userSetup=%s mUserSetup=%s", userSetup, mUserSetup));
+
+ if (userSetup != mUserSetup) {
+ mUserSetup = userSetup;
+ if (!mUserSetup && mStatusBarView != null)
+ animateCollapseQuickSettings();
+ if (mNotificationPanel != null) {
+ mNotificationPanel.setUserSetupComplete(mUserSetup);
+ }
+ updateQsExpansionEnabled();
+ }
+ }
+ };
+
+ protected H mHandler = createHandler();
+ final private ContentObserver mHeadsUpObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ boolean wasUsing = mUseHeadsUp;
+ mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts
+ && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt(
+ mContext.getContentResolver(), Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
+ Settings.Global.HEADS_UP_OFF);
+ mHeadsUpTicker = mUseHeadsUp && 0 != Settings.Global.getInt(
+ mContext.getContentResolver(), SETTING_HEADS_UP_TICKER, 0);
+ Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled"));
+ if (wasUsing != mUseHeadsUp) {
+ if (!mUseHeadsUp) {
+ Log.d(TAG, "dismissing any existing heads up notification on disable event");
+ mHeadsUpManager.releaseAllImmediately();
+ }
+ }
+ }
+ };
+
+ private int mInteractingWindows;
+ private boolean mAutohideSuspended;
+ private int mStatusBarMode;
+ private int mMaxKeyguardNotifications;
+
+ private ViewMediatorCallback mKeyguardViewMediatorCallback;
+ protected ScrimController mScrimController;
+ protected DozeScrimController mDozeScrimController;
+ private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
+
+ private final Runnable mAutohide = () -> {
+ int requested = mSystemUiVisibility & ~STATUS_OR_NAV_TRANSIENT;
+ if (mSystemUiVisibility != requested) {
+ notifyUiVisibilityChanged(requested);
+ }
+ };
+
+ private boolean mWaitingForKeyguardExit;
+ protected boolean mDozing;
+ private boolean mDozingRequested;
+ protected boolean mScrimSrcModeEnabled;
+
+ public static final Interpolator ALPHA_IN = Interpolators.ALPHA_IN;
+ public static final Interpolator ALPHA_OUT = Interpolators.ALPHA_OUT;
+
+ protected BackDropView mBackdrop;
+ protected ImageView mBackdropFront, mBackdropBack;
+ protected PorterDuffXfermode mSrcXferMode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
+ protected PorterDuffXfermode mSrcOverXferMode =
+ new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER);
+
+ private MediaSessionManager mMediaSessionManager;
+ private MediaController mMediaController;
+ private String mMediaNotificationKey;
+ private MediaMetadata mMediaMetadata;
+ private MediaController.Callback mMediaListener
+ = new MediaController.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ super.onPlaybackStateChanged(state);
+ if (DEBUG_MEDIA) Log.v(TAG, "DEBUG_MEDIA: onPlaybackStateChanged: " + state);
+ if (state != null) {
+ if (!isPlaybackActive(state.getState())) {
+ clearCurrentMediaNotification();
+ updateMediaMetaData(true, true);
+ }
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ super.onMetadataChanged(metadata);
+ if (DEBUG_MEDIA) Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata);
+ mMediaMetadata = metadata;
+ updateMediaMetaData(true, true);
+ }
+ };
+
+ private final OnChildLocationsChangedListener mOnChildLocationsChangedListener =
+ new OnChildLocationsChangedListener() {
+ @Override
+ public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout) {
+ userActivity();
+ }
+ };
+
+ private int mDisabledUnmodified1;
+ private int mDisabledUnmodified2;
+
+ /** Keys of notifications currently visible to the user. */
+ private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications =
+ new ArraySet<>();
+ private long mLastVisibilityReportUptimeMs;
+
+ private Runnable mLaunchTransitionEndRunnable;
+ protected boolean mLaunchTransitionFadingAway;
+ private ExpandableNotificationRow mDraggedDownRow;
+ private boolean mLaunchCameraOnScreenTurningOn;
+ private boolean mLaunchCameraOnFinishedGoingToSleep;
+ private int mLastCameraLaunchSource;
+ private PowerManager.WakeLock mGestureWakeLock;
+ private Vibrator mVibrator;
+ private long[] mCameraLaunchGestureVibePattern;
+
+ private final int[] mTmpInt2 = new int[2];
+
+ // Fingerprint (as computed by getLoggingFingerprint() of the last logged state.
+ private int mLastLoggedStateFingerprint;
+ private boolean mTopHidesStatusBar;
+ private boolean mStatusBarWindowHidden;
+ private boolean mHideIconsForBouncer;
+ private boolean mIsOccluded;
+ private boolean mWereIconsJustHidden;
+ private boolean mBouncerWasShowingWhenHidden;
+
+ public boolean isStartedGoingToSleep() {
+ return mStartedGoingToSleep;
+ }
+
+ /**
+ * If set, the device has started going to sleep but isn't fully non-interactive yet.
+ */
+ protected boolean mStartedGoingToSleep;
+
+ private final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
+ new OnChildLocationsChangedListener() {
+ @Override
+ public void onChildLocationsChanged(
+ NotificationStackScrollLayout stackScrollLayout) {
+ if (mHandler.hasCallbacks(mVisibilityReporter)) {
+ // Visibilities will be reported when the existing
+ // callback is executed.
+ return;
+ }
+ // Calculate when we're allowed to run the visibility
+ // reporter. Note that this timestamp might already have
+ // passed. That's OK, the callback will just be executed
+ // ASAP.
+ long nextReportUptimeMs =
+ mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
+ mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
+ }
+ };
+
+ // Tracks notifications currently visible in mNotificationStackScroller and
+ // emits visibility events via NoMan on changes.
+ protected final Runnable mVisibilityReporter = new Runnable() {
+ private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications =
+ new ArraySet<>();
+ private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications =
+ new ArraySet<>();
+ private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications =
+ new ArraySet<>();
+
+ @Override
+ public void run() {
+ mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis();
+ final String mediaKey = getCurrentMediaNotificationKey();
+
+ // 1. Loop over mNotificationData entries:
+ // A. Keep list of visible notifications.
+ // B. Keep list of previously hidden, now visible notifications.
+ // 2. Compute no-longer visible notifications by removing currently
+ // visible notifications from the set of previously visible
+ // notifications.
+ // 3. Report newly visible and no-longer visible notifications.
+ // 4. Keep currently visible notifications for next report.
+ ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+ int N = activeNotifications.size();
+ for (int i = 0; i < N; i++) {
+ Entry entry = activeNotifications.get(i);
+ String key = entry.notification.getKey();
+ boolean isVisible = mStackScroller.isInVisibleLocation(entry.row);
+ NotificationVisibility visObj = NotificationVisibility.obtain(key, i, isVisible);
+ boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
+ if (isVisible) {
+ // Build new set of visible notifications.
+ mTmpCurrentlyVisibleNotifications.add(visObj);
+ if (!previouslyVisible) {
+ mTmpNewlyVisibleNotifications.add(visObj);
+ }
+ } else {
+ // release object
+ visObj.recycle();
+ }
+ }
+ mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications);
+ mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications);
+
+ logNotificationVisibilityChanges(
+ mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications);
+
+ recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
+ mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications);
+
+ recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
+ mTmpCurrentlyVisibleNotifications.clear();
+ mTmpNewlyVisibleNotifications.clear();
+ mTmpNoLongerVisibleNotifications.clear();
+ }
+ };
+
+ private NotificationMessagingUtil mMessagingUtil;
+ private KeyguardUserSwitcher mKeyguardUserSwitcher;
+ private UserSwitcherController mUserSwitcherController;
+ private NetworkController mNetworkController;
+ private KeyguardMonitorImpl mKeyguardMonitor
+ = (KeyguardMonitorImpl) Dependency.get(KeyguardMonitor.class);
+ private BatteryController mBatteryController;
+ protected boolean mPanelExpanded;
+ private IOverlayManager mOverlayManager;
+ private boolean mKeyguardRequested;
+ private boolean mIsKeyguard;
+ private LogMaker mStatusBarStateLog;
+ private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
+ protected NotificationIconAreaController mNotificationIconAreaController;
+ private boolean mReinflateNotificationsOnUserSwitched;
+ private HashMap<String, Entry> mPendingNotifications = new HashMap<>();
+ private boolean mClearAllEnabled;
+ @Nullable private View mAmbientIndicationContainer;
+ private String mKeyToRemoveOnGutsClosed;
+ private SysuiColorExtractor mColorExtractor;
+ private ForegroundServiceController mForegroundServiceController;
+ private ScreenLifecycle mScreenLifecycle;
+ @VisibleForTesting WakefulnessLifecycle mWakefulnessLifecycle;
+
+ private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
+ final int N = array.size();
+ for (int i = 0 ; i < N; i++) {
+ array.valueAt(i).recycle();
+ }
+ array.clear();
+ }
+
+ private final View.OnClickListener mGoToLockedShadeListener = v -> {
+ if (mState == StatusBarState.KEYGUARD) {
+ wakeUpIfDozing(SystemClock.uptimeMillis(), v);
+ goToLockedShade(null);
+ }
+ };
+ private HashMap<ExpandableNotificationRow, List<ExpandableNotificationRow>> mTmpChildOrderMap
+ = new HashMap<>();
+ private RankingMap mLatestRankingMap;
+ private boolean mNoAnimationOnNextBarModeChange;
+ private FalsingManager mFalsingManager;
+
+ private KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onDreamingStateChanged(boolean dreaming) {
+ if (dreaming) {
+ maybeEscalateHeadsUp();
+ }
+ }
+ };
+
+ private NavigationBarFragment mNavigationBar;
+ private View mNavigationBarView;
+
+ @Override
+ public void start() {
+ mNetworkController = Dependency.get(NetworkController.class);
+ mUserSwitcherController = Dependency.get(UserSwitcherController.class);
+ mScreenLifecycle = Dependency.get(ScreenLifecycle.class);
+ mScreenLifecycle.addObserver(mScreenObserver);
+ mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
+ mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
+ mBatteryController = Dependency.get(BatteryController.class);
+ mAssistManager = Dependency.get(AssistManager.class);
+ mSystemServicesProxy = SystemServicesProxy.getInstance(mContext);
+ mOverlayManager = IOverlayManager.Stub.asInterface(
+ ServiceManager.getService(Context.OVERLAY_SERVICE));
+
+ mColorExtractor = Dependency.get(SysuiColorExtractor.class);
+ mColorExtractor.addOnColorsChangedListener(this);
+
+ mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+
+ mForegroundServiceController = Dependency.get(ForegroundServiceController.class);
+
+ mDisplay = mWindowManager.getDefaultDisplay();
+ updateDisplaySize();
+
+ Resources res = mContext.getResources();
+ mScrimSrcModeEnabled = res.getBoolean(R.bool.config_status_bar_scrim_behind_use_src);
+ mClearAllEnabled = res.getBoolean(R.bool.config_enableNotificationsClearAll);
+ mAlwaysExpandNonGroupedNotification =
+ res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
+
+ DateTimeView.setReceiverHandler(Dependency.get(Dependency.TIME_TICK_HANDLER));
+ putComponent(StatusBar.class, this);
+
+ // start old BaseStatusBar.start().
+ mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
+ mDevicePolicyManager = (DevicePolicyManager) mContext.getSystemService(
+ Context.DEVICE_POLICY_SERVICE);
+
+ mNotificationData = new NotificationData(this);
+ mMessagingUtil = new NotificationMessagingUtil(mContext);
+
+ mAccessibilityManager = (AccessibilityManager)
+ mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+ mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+
+ mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class);
+ mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false,
+ mSettingsObserver);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS), false,
+ mLockscreenSettingsObserver,
+ UserHandle.USER_ALL);
+ if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT),
+ false,
+ mSettingsObserver,
+ UserHandle.USER_ALL);
+ }
+
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS),
+ true,
+ mLockscreenSettingsObserver,
+ UserHandle.USER_ALL);
+
+ mBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+
+ mRecents = getComponent(Recents.class);
+
+ final Configuration currentConfig = res.getConfiguration();
+ mLocale = currentConfig.locale;
+ mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(mLocale);
+
+ mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+ mLockPatternUtils = new LockPatternUtils(mContext);
+
+ // Connect in to the status bar manager service
+ mCommandQueue = getComponent(CommandQueue.class);
+ mCommandQueue.addCallbacks(this);
+
+ int[] switches = new int[9];
+ ArrayList<IBinder> binders = new ArrayList<>();
+ ArrayList<String> iconSlots = new ArrayList<>();
+ ArrayList<StatusBarIcon> icons = new ArrayList<>();
+ Rect fullscreenStackBounds = new Rect();
+ Rect dockedStackBounds = new Rect();
+ try {
+ mBarService.registerStatusBar(mCommandQueue, iconSlots, icons, switches, binders,
+ fullscreenStackBounds, dockedStackBounds);
+ } catch (RemoteException ex) {
+ // If the system process isn't there we're doomed anyway.
+ }
+
+ createAndAddWindows();
+
+ mSettingsObserver.onChange(false); // set up
+ mCommandQueue.disable(switches[0], switches[6], false /* animate */);
+ setSystemUiVisibility(switches[1], switches[7], switches[8], 0xffffffff,
+ fullscreenStackBounds, dockedStackBounds);
+ topAppWindowChanged(switches[2] != 0);
+ // StatusBarManagerService has a back up of IME token and it's restored here.
+ setImeWindowStatus(binders.get(0), switches[3], switches[4], switches[5] != 0);
+
+ // Set up the initial icon state
+ int N = iconSlots.size();
+ for (int i=0; i < N; i++) {
+ mCommandQueue.setIcon(iconSlots.get(i), icons.get(i));
+ }
+
+ // Set up the initial notification state.
+ try {
+ mNotificationListener.registerAsSystemService(mContext,
+ new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()),
+ UserHandle.USER_ALL);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to register notification listener", e);
+ }
+
+
+ if (DEBUG) {
+ Log.d(TAG, String.format(
+ "init: icons=%d disabled=0x%08x lights=0x%08x menu=0x%08x imeButton=0x%08x",
+ icons.size(),
+ switches[0],
+ switches[1],
+ switches[2],
+ switches[3]
+ ));
+ }
+
+ mCurrentUserId = ActivityManager.getCurrentUser();
+ setHeadsUpUser(mCurrentUserId);
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ filter.addAction(Intent.ACTION_USER_ADDED);
+ filter.addAction(Intent.ACTION_USER_PRESENT);
+ mContext.registerReceiver(mBaseBroadcastReceiver, filter);
+
+ IntentFilter internalFilter = new IntentFilter();
+ internalFilter.addAction(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
+ internalFilter.addAction(BANNER_ACTION_CANCEL);
+ internalFilter.addAction(BANNER_ACTION_SETUP);
+ mContext.registerReceiver(mBaseBroadcastReceiver, internalFilter, PERMISSION_SELF, null);
+
+ IntentFilter allUsersFilter = new IntentFilter();
+ allUsersFilter.addAction(
+ DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
+ allUsersFilter.addAction(Intent.ACTION_DEVICE_LOCKED_CHANGED);
+ mContext.registerReceiverAsUser(mAllUsersReceiver, UserHandle.ALL, allUsersFilter,
+ null, null);
+ updateCurrentProfilesCache();
+
+ IVrManager vrManager = IVrManager.Stub.asInterface(ServiceManager.getService(
+ Context.VR_SERVICE));
+ try {
+ vrManager.registerListener(mVrStateCallbacks);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to register VR mode state listener: " + e);
+ }
+
+ mNonBlockablePkgs = new HashSet<>();
+ Collections.addAll(mNonBlockablePkgs, res.getStringArray(
+ com.android.internal.R.array.config_nonBlockableNotificationPackages));
+ // end old BaseStatusBar.start().
+
+ mMediaSessionManager
+ = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+ // TODO: use MediaSessionManager.SessionListener to hook us up to future updates
+ // in session state
+
+ // Lastly, call to the icon policy to install/update all the icons.
+ mIconPolicy = new PhoneStatusBarPolicy(mContext, mIconController);
+ mSettingsObserver.onChange(false); // set up
+
+ mHeadsUpObserver.onChange(true); // set up
+ if (ENABLE_HEADS_UP) {
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), true,
+ mHeadsUpObserver);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
+ mHeadsUpObserver);
+ }
+ mUnlockMethodCache = UnlockMethodCache.getInstance(mContext);
+ mUnlockMethodCache.addListener(this);
+ startKeyguard();
+
+ KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateCallback);
+ putComponent(DozeHost.class, mDozeServiceHost);
+
+ notifyUserAboutHiddenNotifications();
+
+ mScreenPinningRequest = new ScreenPinningRequest(mContext);
+ mFalsingManager = FalsingManager.getInstance(mContext);
+
+ Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(this);
+
+ Dependency.get(ConfigurationController.class).addCallback(this);
+ }
+
+ protected void createIconController() {
+ }
+
+ // ================================================================================
+ // Constructing the view
+ // ================================================================================
+ protected void makeStatusBarView() {
+ final Context context = mContext;
+ updateDisplaySize(); // populates mDisplayMetrics
+ updateResources();
+ updateTheme();
+
+ inflateStatusBarWindow(context);
+ mStatusBarWindow.setService(this);
+ mStatusBarWindow.setOnTouchListener(getStatusBarWindowTouchListener());
+
+ // TODO: Deal with the ugliness that comes from having some of the statusbar broken out
+ // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot.
+ mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById(
+ R.id.notification_panel);
+ mStackScroller = (NotificationStackScrollLayout) mStatusBarWindow.findViewById(
+ R.id.notification_stack_scroller);
+ mNotificationPanel.setStatusBar(this);
+ mNotificationPanel.setGroupManager(mGroupManager);
+ mAboveShelfObserver = new AboveShelfObserver(mStackScroller);
+ mAboveShelfObserver.setListener(mStatusBarWindow.findViewById(
+ R.id.notification_container_parent));
+ mKeyguardStatusBar = (KeyguardStatusBarView) mStatusBarWindow.findViewById(R.id.keyguard_header);
+
+ mNotificationIconAreaController = SystemUIFactory.getInstance()
+ .createNotificationIconAreaController(context, this);
+ inflateShelf();
+ mNotificationIconAreaController.setupShelf(mNotificationShelf);
+ Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mNotificationIconAreaController);
+ FragmentHostManager.get(mStatusBarWindow)
+ .addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {
+ CollapsedStatusBarFragment statusBarFragment =
+ (CollapsedStatusBarFragment) fragment;
+ statusBarFragment.initNotificationIconArea(mNotificationIconAreaController);
+ mStatusBarView = (PhoneStatusBarView) fragment.getView();
+ mStatusBarView.setBar(this);
+ mStatusBarView.setPanel(mNotificationPanel);
+ mStatusBarView.setScrimController(mScrimController);
+ mStatusBarView.setBouncerShowing(mBouncerShowing);
+ setAreThereNotifications();
+ checkBarModes();
+ }).getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.status_bar_container, new CollapsedStatusBarFragment(),
+ CollapsedStatusBarFragment.TAG)
+ .commit();
+ mIconController = Dependency.get(StatusBarIconController.class);
+
+ mHeadsUpManager = new HeadsUpManager(context, mStatusBarWindow, mGroupManager);
+ mHeadsUpManager.setBar(this);
+ mHeadsUpManager.addListener(this);
+ mHeadsUpManager.addListener(mNotificationPanel);
+ mHeadsUpManager.addListener(mGroupManager);
+ mHeadsUpManager.addListener(mVisualStabilityManager);
+ mNotificationPanel.setHeadsUpManager(mHeadsUpManager);
+ mNotificationData.setHeadsUpManager(mHeadsUpManager);
+ mGroupManager.setHeadsUpManager(mHeadsUpManager);
+ mHeadsUpManager.setVisualStabilityManager(mVisualStabilityManager);
+
+ if (MULTIUSER_DEBUG) {
+ mNotificationPanelDebugText = (TextView) mNotificationPanel.findViewById(
+ R.id.header_debug_info);
+ mNotificationPanelDebugText.setVisibility(View.VISIBLE);
+ }
+
+ try {
+ boolean showNav = mWindowManagerService.hasNavigationBar();
+ if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
+ if (showNav) {
+ createNavigationBar();
+ }
+ } catch (RemoteException ex) {
+ // no window manager? good luck with that
+ }
+
+ // figure out which pixel-format to use for the status bar.
+ mPixelFormat = PixelFormat.OPAQUE;
+
+ mStackScroller.setLongPressListener(getNotificationLongClicker());
+ mStackScroller.setStatusBar(this);
+ mStackScroller.setGroupManager(mGroupManager);
+ mStackScroller.setHeadsUpManager(mHeadsUpManager);
+ mGroupManager.setOnGroupChangeListener(mStackScroller);
+ mVisualStabilityManager.setVisibilityLocationProvider(mStackScroller);
+
+ inflateEmptyShadeView();
+ inflateDismissView();
+ mExpandedContents = mStackScroller;
+
+ mBackdrop = (BackDropView) mStatusBarWindow.findViewById(R.id.backdrop);
+ mBackdropFront = (ImageView) mBackdrop.findViewById(R.id.backdrop_front);
+ mBackdropBack = (ImageView) mBackdrop.findViewById(R.id.backdrop_back);
+
+ if (ENABLE_LOCKSCREEN_WALLPAPER) {
+ mLockscreenWallpaper = new LockscreenWallpaper(mContext, this, mHandler);
+ }
+
+ mKeyguardIndicationController =
+ SystemUIFactory.getInstance().createKeyguardIndicationController(mContext,
+ (ViewGroup) mStatusBarWindow.findViewById(R.id.keyguard_indication_area),
+ mNotificationPanel.getLockIcon());
+ mNotificationPanel.setKeyguardIndicationController(mKeyguardIndicationController);
+
+
+ mAmbientIndicationContainer = mStatusBarWindow.findViewById(
+ R.id.ambient_indication_container);
+
+ // set the initial view visibility
+ setAreThereNotifications();
+
+ // TODO: Find better place for this callback.
+ mBatteryController.addCallback(new BatteryStateChangeCallback() {
+ @Override
+ public void onPowerSaveChanged(boolean isPowerSave) {
+ mHandler.post(mCheckBarModes);
+ if (mDozeServiceHost != null) {
+ mDozeServiceHost.firePowerSaveChanged(isPowerSave);
+ }
+ }
+
+ @Override
+ public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+ // noop
+ }
+ });
+
+ mLightBarController = Dependency.get(LightBarController.class);
+ if (mNavigationBar != null) {
+ mNavigationBar.setLightBarController(mLightBarController);
+ }
+
+ ScrimView scrimBehind = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_behind);
+ ScrimView scrimInFront = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_in_front);
+ View headsUpScrim = mStatusBarWindow.findViewById(R.id.heads_up_scrim);
+ mScrimController = SystemUIFactory.getInstance().createScrimController(mLightBarController,
+ scrimBehind, scrimInFront, headsUpScrim, mLockscreenWallpaper,
+ scrimsVisible -> {
+ if (mStatusBarWindowManager != null) {
+ mStatusBarWindowManager.setScrimsVisible(scrimsVisible);
+ }
+ });
+ if (mScrimSrcModeEnabled) {
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ boolean asSrc = mBackdrop.getVisibility() != View.VISIBLE;
+ mScrimController.setDrawBehindAsSrc(asSrc);
+ mStackScroller.setDrawBackgroundAsSrc(asSrc);
+ }
+ };
+ mBackdrop.setOnVisibilityChangedRunnable(runnable);
+ runnable.run();
+ }
+ mHeadsUpManager.addListener(mScrimController);
+ mStackScroller.setScrimController(mScrimController);
+ mDozeScrimController = new DozeScrimController(mScrimController, context);
+
+ // Other icons
+ mVolumeComponent = getComponent(VolumeComponent.class);
+
+ mNotificationPanel.setUserSetupComplete(mUserSetup);
+ if (UserManager.get(mContext).isUserSwitcherEnabled()) {
+ createUserSwitcher();
+ }
+
+ // Set up the quick settings tile panel
+ View container = mStatusBarWindow.findViewById(R.id.qs_frame);
+ if (container != null) {
+ FragmentHostManager fragmentHostManager = FragmentHostManager.get(container);
+ ExtensionFragmentListener.attachExtensonToFragment(container, QS.TAG, R.id.qs_frame,
+ Dependency.get(ExtensionController.class).newExtension(QS.class)
+ .withPlugin(QS.class)
+ .withFeature(
+ PackageManager.FEATURE_AUTOMOTIVE, () -> new CarQSFragment())
+ .withDefault(() -> new QSFragment())
+ .build());
+ final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this,
+ mIconController);
+ mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow,
+ mScrimController);
+ fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> {
+ QS qs = (QS) f;
+ if (qs instanceof QSFragment) {
+ ((QSFragment) qs).setHost(qsh);
+ mQSPanel = ((QSFragment) qs).getQsPanel();
+ mQSPanel.setBrightnessMirror(mBrightnessMirrorController);
+ mKeyguardStatusBar.setQSPanel(mQSPanel);
+ }
+ });
+ }
+
+ mReportRejectedTouch = mStatusBarWindow.findViewById(R.id.report_rejected_touch);
+ if (mReportRejectedTouch != null) {
+ updateReportRejectedTouchVisibility();
+ mReportRejectedTouch.setOnClickListener(v -> {
+ Uri session = mFalsingManager.reportRejectedTouch();
+ if (session == null) { return; }
+
+ StringWriter message = new StringWriter();
+ message.write("Build info: ");
+ message.write(SystemProperties.get("ro.build.description"));
+ message.write("\nSerial number: ");
+ message.write(SystemProperties.get("ro.serialno"));
+ message.write("\n");
+
+ PrintWriter falsingPw = new PrintWriter(message);
+ FalsingLog.dump(falsingPw);
+ falsingPw.flush();
+
+ startActivityDismissingKeyguard(Intent.createChooser(new Intent(Intent.ACTION_SEND)
+ .setType("*/*")
+ .putExtra(Intent.EXTRA_SUBJECT, "Rejected touch report")
+ .putExtra(Intent.EXTRA_STREAM, session)
+ .putExtra(Intent.EXTRA_TEXT, message.toString()),
+ "Share rejected touch report")
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ true /* onlyProvisioned */, true /* dismissShade */);
+ });
+ }
+
+ PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ if (!pm.isScreenOn()) {
+ mBroadcastReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF));
+ }
+ mGestureWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
+ "GestureWakeLock");
+ mVibrator = mContext.getSystemService(Vibrator.class);
+ int[] pattern = mContext.getResources().getIntArray(
+ R.array.config_cameraLaunchGestureVibePattern);
+ mCameraLaunchGestureVibePattern = new long[pattern.length];
+ for (int i = 0; i < pattern.length; i++) {
+ mCameraLaunchGestureVibePattern[i] = pattern[i];
+ }
+
+ // receive broadcasts
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG);
+ context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+
+ IntentFilter demoFilter = new IntentFilter();
+ if (DEBUG_MEDIA_FAKE_ARTWORK) {
+ demoFilter.addAction(ACTION_FAKE_ARTWORK);
+ }
+ demoFilter.addAction(ACTION_DEMO);
+ context.registerReceiverAsUser(mDemoReceiver, UserHandle.ALL, demoFilter,
+ android.Manifest.permission.DUMP, null);
+
+ // listen for USER_SETUP_COMPLETE setting (per-user)
+ mDeviceProvisionedController.addCallback(mUserSetupObserver);
+ mUserSetupObserver.onUserSetupChanged();
+
+ // disable profiling bars, since they overlap and clutter the output on app windows
+ ThreadedRenderer.overrideProperty("disableProfileBars", "true");
+
+ // Private API call to make the shadows look better for Recents
+ ThreadedRenderer.overrideProperty("ambientRatio", String.valueOf(1.5f));
+ }
+
+ protected void createNavigationBar() {
+ mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> {
+ mNavigationBar = (NavigationBarFragment) fragment;
+ if (mLightBarController != null) {
+ mNavigationBar.setLightBarController(mLightBarController);
+ }
+ mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);
+ });
+ }
+
+ /**
+ * Returns the {@link android.view.View.OnTouchListener} that will be invoked when the
+ * background window of the status bar is clicked.
+ */
+ protected View.OnTouchListener getStatusBarWindowTouchListener() {
+ return (v, event) -> {
+ checkUserAutohide(v, event);
+ checkRemoteInputOutside(event);
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (mExpandedVisible) {
+ animateCollapsePanels();
+ }
+ }
+ return mStatusBarWindow.onTouchEvent(event);
+ };
+ }
+
+ private void inflateShelf() {
+ mNotificationShelf =
+ (NotificationShelf) LayoutInflater.from(mContext).inflate(
+ R.layout.status_bar_notification_shelf, mStackScroller, false);
+ mNotificationShelf.setOnActivatedListener(this);
+ mStackScroller.setShelf(mNotificationShelf);
+ mNotificationShelf.setOnClickListener(mGoToLockedShadeListener);
+ mNotificationShelf.setStatusBarState(mState);
+ }
+
+ public void onDensityOrFontScaleChanged() {
+ // start old BaseStatusBar.onDensityOrFontScaleChanged().
+ if (!KeyguardUpdateMonitor.getInstance(mContext).isSwitchingUser()) {
+ updateNotificationsOnDensityOrFontScaleChanged();
+ } else {
+ mReinflateNotificationsOnUserSwitched = true;
+ }
+ // end old BaseStatusBar.onDensityOrFontScaleChanged().
+ mScrimController.onDensityOrFontScaleChanged();
+ // TODO: Remove this.
+ if (mStatusBarView != null) mStatusBarView.onDensityOrFontScaleChanged();
+ if (mBrightnessMirrorController != null) {
+ mBrightnessMirrorController.onDensityOrFontScaleChanged();
+ }
+ mStatusBarKeyguardViewManager.onDensityOrFontScaleChanged();
+ // TODO: Bring these out of StatusBar.
+ ((UserInfoControllerImpl) Dependency.get(UserInfoController.class))
+ .onDensityOrFontScaleChanged();
+ Dependency.get(UserSwitcherController.class).onDensityOrFontScaleChanged();
+ if (mKeyguardUserSwitcher != null) {
+ mKeyguardUserSwitcher.onDensityOrFontScaleChanged();
+ }
+ mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
+
+ reevaluateStyles();
+ }
+
+ private void reinflateViews() {
+ reevaluateStyles();
+
+ // Clock and bottom icons
+ mNotificationPanel.onOverlayChanged();
+ // The status bar on the keyguard is a special layout.
+ if (mKeyguardStatusBar != null) mKeyguardStatusBar.onOverlayChanged();
+ // Recreate Indication controller because internal references changed
+ mKeyguardIndicationController =
+ SystemUIFactory.getInstance().createKeyguardIndicationController(mContext,
+ mStatusBarWindow.findViewById(R.id.keyguard_indication_area),
+ mNotificationPanel.getLockIcon());
+ mNotificationPanel.setKeyguardIndicationController(mKeyguardIndicationController);
+ mKeyguardIndicationController
+ .setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
+ mKeyguardIndicationController.setVisible(mState == StatusBarState.KEYGUARD);
+ mKeyguardIndicationController.setDozing(mDozing);
+ if (mBrightnessMirrorController != null) {
+ mBrightnessMirrorController.onOverlayChanged();
+ }
+ if (mStatusBarKeyguardViewManager != null) {
+ mStatusBarKeyguardViewManager.onOverlayChanged();
+ }
+ if (mAmbientIndicationContainer instanceof AutoReinflateContainer) {
+ ((AutoReinflateContainer) mAmbientIndicationContainer).inflateLayout();
+ }
+ }
+
+ protected void reevaluateStyles() {
+ inflateSignalClusters();
+ inflateDismissView();
+ updateClearAll();
+ inflateEmptyShadeView();
+ updateEmptyShadeView();
+ }
+
+ private void updateNotificationsOnDensityOrFontScaleChanged() {
+ ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+ for (int i = 0; i < activeNotifications.size(); i++) {
+ Entry entry = activeNotifications.get(i);
+ boolean exposedGuts = mNotificationGutsExposed != null
+ && entry.row.getGuts() == mNotificationGutsExposed;
+ entry.row.onDensityOrFontScaleChanged();
+ if (exposedGuts) {
+ mNotificationGutsExposed = entry.row.getGuts();
+ bindGuts(entry.row, mGutsMenuItem);
+ }
+ }
+ }
+
+ private void inflateSignalClusters() {
+ if (mKeyguardStatusBar != null) reinflateSignalCluster(mKeyguardStatusBar);
+ }
+
+ public static SignalClusterView reinflateSignalCluster(View view) {
+ Context context = view.getContext();
+ SignalClusterView signalCluster =
+ (SignalClusterView) view.findViewById(R.id.signal_cluster);
+ if (signalCluster != null) {
+ ViewParent parent = signalCluster.getParent();
+ if (parent instanceof ViewGroup) {
+ ViewGroup viewParent = (ViewGroup) parent;
+ int index = viewParent.indexOfChild(signalCluster);
+ viewParent.removeView(signalCluster);
+ SignalClusterView newCluster = (SignalClusterView) LayoutInflater.from(context)
+ .inflate(R.layout.signal_cluster_view, viewParent, false);
+ ViewGroup.MarginLayoutParams layoutParams =
+ (ViewGroup.MarginLayoutParams) viewParent.getLayoutParams();
+ layoutParams.setMarginsRelative(
+ context.getResources().getDimensionPixelSize(
+ R.dimen.signal_cluster_margin_start),
+ 0, 0, 0);
+ newCluster.setLayoutParams(layoutParams);
+ viewParent.addView(newCluster, index);
+ return newCluster;
+ }
+ return signalCluster;
+ }
+ return null;
+ }
+
+ private void inflateEmptyShadeView() {
+ if (mStackScroller == null) {
+ return;
+ }
+ mEmptyShadeView = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
+ R.layout.status_bar_no_notifications, mStackScroller, false);
+ mStackScroller.setEmptyShadeView(mEmptyShadeView);
+ }
+
+ private void inflateDismissView() {
+ if (!mClearAllEnabled || mStackScroller == null) {
+ return;
+ }
+
+ mDismissView = (DismissView) LayoutInflater.from(mContext).inflate(
+ R.layout.status_bar_notification_dismiss_all, mStackScroller, false);
+ mDismissView.setOnButtonClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES);
+ clearAllNotifications();
+ }
+ });
+ mStackScroller.setDismissView(mDismissView);
+ }
+
+ protected void createUserSwitcher() {
+ mKeyguardUserSwitcher = new KeyguardUserSwitcher(mContext,
+ (ViewStub) mStatusBarWindow.findViewById(R.id.keyguard_user_switcher),
+ mKeyguardStatusBar, mNotificationPanel);
+ }
+
+ protected void inflateStatusBarWindow(Context context) {
+ mStatusBarWindow = (StatusBarWindowView) View.inflate(context,
+ R.layout.super_status_bar, null);
+ }
+
+ public void clearAllNotifications() {
+
+ // animate-swipe all dismissable notifications, then animate the shade closed
+ int numChildren = mStackScroller.getChildCount();
+
+ final ArrayList<View> viewsToHide = new ArrayList<View>(numChildren);
+ final ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>(numChildren);
+ for (int i = 0; i < numChildren; i++) {
+ final View child = mStackScroller.getChildAt(i);
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ boolean parentVisible = false;
+ boolean hasClipBounds = child.getClipBounds(mTmpRect);
+ if (mStackScroller.canChildBeDismissed(child)) {
+ viewsToRemove.add(row);
+ if (child.getVisibility() == View.VISIBLE
+ && (!hasClipBounds || mTmpRect.height() > 0)) {
+ viewsToHide.add(child);
+ parentVisible = true;
+ }
+ } else if (child.getVisibility() == View.VISIBLE
+ && (!hasClipBounds || mTmpRect.height() > 0)) {
+ parentVisible = true;
+ }
+ List<ExpandableNotificationRow> children = row.getNotificationChildren();
+ if (children != null) {
+ for (ExpandableNotificationRow childRow : children) {
+ viewsToRemove.add(childRow);
+ if (parentVisible && row.areChildrenExpanded()
+ && mStackScroller.canChildBeDismissed(childRow)) {
+ hasClipBounds = childRow.getClipBounds(mTmpRect);
+ if (childRow.getVisibility() == View.VISIBLE
+ && (!hasClipBounds || mTmpRect.height() > 0)) {
+ viewsToHide.add(childRow);
+ }
+ }
+ }
+ }
+ }
+ }
+ if (viewsToRemove.isEmpty()) {
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
+ return;
+ }
+
+ addPostCollapseAction(new Runnable() {
+ @Override
+ public void run() {
+ mStackScroller.setDismissAllInProgress(false);
+ for (ExpandableNotificationRow rowToRemove : viewsToRemove) {
+ if (mStackScroller.canChildBeDismissed(rowToRemove)) {
+ removeNotification(rowToRemove.getEntry().key, null);
+ } else {
+ rowToRemove.resetTranslation();
+ }
+ }
+ try {
+ mBarService.onClearAllNotifications(mCurrentUserId);
+ } catch (Exception ex) { }
+ }
+ });
+
+ performDismissAllAnimations(viewsToHide);
+
+ }
+
+ private void performDismissAllAnimations(ArrayList<View> hideAnimatedList) {
+ Runnable animationFinishAction = new Runnable() {
+ @Override
+ public void run() {
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
+ }
+ };
+
+ // let's disable our normal animations
+ mStackScroller.setDismissAllInProgress(true);
+
+ // Decrease the delay for every row we animate to give the sense of
+ // accelerating the swipes
+ int rowDelayDecrement = 10;
+ int currentDelay = 140;
+ int totalDelay = 180;
+ int numItems = hideAnimatedList.size();
+ for (int i = numItems - 1; i >= 0; i--) {
+ View view = hideAnimatedList.get(i);
+ Runnable endRunnable = null;
+ if (i == 0) {
+ endRunnable = animationFinishAction;
+ }
+ mStackScroller.dismissViewAnimated(view, endRunnable, totalDelay, 260);
+ currentDelay = Math.max(50, currentDelay - rowDelayDecrement);
+ totalDelay += currentDelay;
+ }
+ }
+
+ protected void setZenMode(int mode) {
+ // start old BaseStatusBar.setZenMode().
+ if (isDeviceProvisioned()) {
+ mZenMode = mode;
+ updateNotifications();
+ }
+ // end old BaseStatusBar.setZenMode().
+ }
+
+ protected void startKeyguard() {
+ Trace.beginSection("StatusBar#startKeyguard");
+ KeyguardViewMediator keyguardViewMediator = getComponent(KeyguardViewMediator.class);
+ mFingerprintUnlockController = new FingerprintUnlockController(mContext,
+ mDozeScrimController, keyguardViewMediator,
+ mScrimController, this, UnlockMethodCache.getInstance(mContext));
+ mStatusBarKeyguardViewManager = keyguardViewMediator.registerStatusBar(this,
+ getBouncerContainer(), mScrimController,
+ mFingerprintUnlockController);
+ mKeyguardIndicationController
+ .setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
+ mFingerprintUnlockController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
+ mRemoteInputController.addCallback(mStatusBarKeyguardViewManager);
+
+ mRemoteInputController.addCallback(new RemoteInputController.Callback() {
+ @Override
+ public void onRemoteInputSent(Entry entry) {
+ if (FORCE_REMOTE_INPUT_HISTORY && mKeysKeptForRemoteInput.contains(entry.key)) {
+ removeNotification(entry.key, null);
+ } else if (mRemoteInputEntriesToRemoveOnCollapse.contains(entry)) {
+ // We're currently holding onto this notification, but from the apps point of
+ // view it is already canceled, so we'll need to cancel it on the apps behalf
+ // after sending - unless the app posts an update in the mean time, so wait a
+ // bit.
+ mHandler.postDelayed(() -> {
+ if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) {
+ removeNotification(entry.key, null);
+ }
+ }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+ }
+ }
+ });
+
+ mKeyguardViewMediatorCallback = keyguardViewMediator.getViewMediatorCallback();
+ mLightBarController.setFingerprintUnlockController(mFingerprintUnlockController);
+ Trace.endSection();
+ }
+
+ protected View getStatusBarView() {
+ return mStatusBarView;
+ }
+
+ public StatusBarWindowView getStatusBarWindow() {
+ return mStatusBarWindow;
+ }
+
+ protected ViewGroup getBouncerContainer() {
+ return mStatusBarWindow;
+ }
+
+ public int getStatusBarHeight() {
+ if (mNaturalBarHeight < 0) {
+ final Resources res = mContext.getResources();
+ mNaturalBarHeight =
+ res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
+ }
+ return mNaturalBarHeight;
+ }
+
+ protected boolean toggleSplitScreenMode(int metricsDockAction, int metricsUndockAction) {
+ if (mRecents == null) {
+ return false;
+ }
+ int dockSide = WindowManagerProxy.getInstance().getDockSide();
+ if (dockSide == WindowManager.DOCKED_INVALID) {
+ return mRecents.dockTopTask(NavigationBarGestureHelper.DRAG_MODE_NONE,
+ ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, null, metricsDockAction);
+ } else {
+ Divider divider = getComponent(Divider.class);
+ if (divider != null && divider.isMinimized() && !divider.isHomeStackResizable()) {
+ // Undocking from the minimized state is not supported
+ return false;
+ } else {
+ EventBus.getDefault().send(new UndockingTaskEvent());
+ if (metricsUndockAction != -1) {
+ mMetricsLogger.action(metricsUndockAction);
+ }
+ }
+ }
+ return true;
+ }
+
+ void awakenDreams() {
+ SystemServicesProxy.getInstance(mContext).awakenDreamsAsync();
+ }
+
+ public UserHandle getCurrentUserHandle() {
+ return new UserHandle(mCurrentUserId);
+ }
+
+ public void addNotification(StatusBarNotification notification, RankingMap ranking)
+ throws InflationException {
+ String key = notification.getKey();
+ if (DEBUG) Log.d(TAG, "addNotification key=" + key);
+
+ mNotificationData.updateRanking(ranking);
+ Entry shadeEntry = createNotificationViews(notification);
+ boolean isHeadsUped = shouldPeek(shadeEntry);
+ if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) {
+ if (shouldSuppressFullScreenIntent(key)) {
+ if (DEBUG) {
+ Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + key);
+ }
+ } else if (mNotificationData.getImportance(key)
+ < NotificationManager.IMPORTANCE_HIGH) {
+ if (DEBUG) {
+ Log.d(TAG, "No Fullscreen intent: not important enough: "
+ + key);
+ }
+ } else {
+ // Stop screensaver if the notification has a full-screen intent.
+ // (like an incoming phone call)
+ awakenDreams();
+
+ // 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,
+ key);
+ notification.getNotification().fullScreenIntent.send();
+ shadeEntry.notifyFullScreenIntentLaunched();
+ mMetricsLogger.count("note_fullscreen", 1);
+ } catch (PendingIntent.CanceledException e) {
+ }
+ }
+ }
+ abortExistingInflation(key);
+
+ mForegroundServiceController.addNotification(notification,
+ mNotificationData.getImportance(key));
+
+ mPendingNotifications.put(key, shadeEntry);
+ }
+
+ private void abortExistingInflation(String key) {
+ if (mPendingNotifications.containsKey(key)) {
+ Entry entry = mPendingNotifications.get(key);
+ entry.abortTask();
+ mPendingNotifications.remove(key);
+ }
+ Entry addedEntry = mNotificationData.get(key);
+ if (addedEntry != null) {
+ addedEntry.abortTask();
+ }
+ }
+
+ private void addEntry(Entry shadeEntry) {
+ boolean isHeadsUped = shouldPeek(shadeEntry);
+ if (isHeadsUped) {
+ mHeadsUpManager.showNotification(shadeEntry);
+ // Mark as seen immediately
+ setNotificationShown(shadeEntry.notification);
+ }
+ addNotificationViews(shadeEntry);
+ // Recalculate the position of the sliding windows and the titles.
+ setAreThereNotifications();
+ }
+
+ @Override
+ public void handleInflationException(StatusBarNotification notification, Exception e) {
+ handleNotificationError(notification, e.getMessage());
+ }
+
+ @Override
+ public void onAsyncInflationFinished(Entry entry) {
+ mPendingNotifications.remove(entry.key);
+ // If there was an async task started after the removal, we don't want to add it back to
+ // the list, otherwise we might get leaks.
+ boolean isNew = mNotificationData.get(entry.key) == null;
+ if (isNew && !entry.row.isRemoved()) {
+ addEntry(entry);
+ } else if (!isNew && entry.row.hasLowPriorityStateUpdated()) {
+ mVisualStabilityManager.onLowPriorityUpdated(entry);
+ updateNotificationShade();
+ }
+ entry.row.setLowPriorityStateUpdated(false);
+ }
+
+ private boolean shouldSuppressFullScreenIntent(String key) {
+ if (isDeviceInVrMode()) {
+ return true;
+ }
+
+ if (mPowerManager.isInteractive()) {
+ return mNotificationData.shouldSuppressScreenOn(key);
+ } else {
+ return mNotificationData.shouldSuppressScreenOff(key);
+ }
+ }
+
+ protected void updateNotificationRanking(RankingMap ranking) {
+ mNotificationData.updateRanking(ranking);
+ updateNotifications();
+ }
+
+ public void removeNotification(String key, RankingMap ranking) {
+ boolean deferRemoval = false;
+ abortExistingInflation(key);
+ if (mHeadsUpManager.isHeadsUp(key)) {
+ // A cancel() in repsonse to a remote input shouldn't be delayed, as it makes the
+ // sending look longer than it takes.
+ // Also we should not defer the removal if reordering isn't allowed since otherwise
+ // some notifications can't disappear before the panel is closed.
+ boolean ignoreEarliestRemovalTime = mRemoteInputController.isSpinning(key)
+ && !FORCE_REMOTE_INPUT_HISTORY
+ || !mVisualStabilityManager.isReorderingAllowed();
+ deferRemoval = !mHeadsUpManager.removeNotification(key, ignoreEarliestRemovalTime);
+ }
+ if (key.equals(mMediaNotificationKey)) {
+ clearCurrentMediaNotification();
+ updateMediaMetaData(true, true);
+ }
+ if (FORCE_REMOTE_INPUT_HISTORY && mRemoteInputController.isSpinning(key)) {
+ Entry entry = mNotificationData.get(key);
+ StatusBarNotification sbn = entry.notification;
+
+ Notification.Builder b = Notification.Builder
+ .recoverBuilder(mContext, sbn.getNotification().clone());
+ CharSequence[] oldHistory = sbn.getNotification().extras
+ .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
+ CharSequence[] newHistory;
+ if (oldHistory == null) {
+ newHistory = new CharSequence[1];
+ } else {
+ newHistory = new CharSequence[oldHistory.length + 1];
+ for (int i = 0; i < oldHistory.length; i++) {
+ newHistory[i + 1] = oldHistory[i];
+ }
+ }
+ newHistory[0] = String.valueOf(entry.remoteInputText);
+ b.setRemoteInputHistory(newHistory);
+
+ Notification newNotification = b.build();
+
+ // Undo any compatibility view inflation
+ newNotification.contentView = sbn.getNotification().contentView;
+ newNotification.bigContentView = sbn.getNotification().bigContentView;
+ newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
+
+ StatusBarNotification newSbn = new StatusBarNotification(sbn.getPackageName(),
+ sbn.getOpPkg(),
+ sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(),
+ newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime());
+ boolean updated = false;
+ try {
+ updateNotification(newSbn, null);
+ updated = true;
+ } catch (InflationException e) {
+ deferRemoval = false;
+ }
+ if (updated) {
+ mKeysKeptForRemoteInput.add(entry.key);
+ return;
+ }
+ }
+ if (deferRemoval) {
+ mLatestRankingMap = ranking;
+ mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
+ return;
+ }
+ Entry entry = mNotificationData.get(key);
+
+ if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
+ && (entry.row != null && !entry.row.isDismissed())) {
+ mLatestRankingMap = ranking;
+ mRemoteInputEntriesToRemoveOnCollapse.add(entry);
+ return;
+ }
+ if (entry != null && mNotificationGutsExposed != null
+ && mNotificationGutsExposed == entry.row.getGuts() && entry.row.getGuts() != null
+ && !entry.row.getGuts().isLeavebehind()) {
+ Log.w(TAG, "Keeping notification because it's showing guts. " + key);
+ mLatestRankingMap = ranking;
+ mKeyToRemoveOnGutsClosed = key;
+ return;
+ }
+
+ if (entry != null) {
+ mForegroundServiceController.removeNotification(entry.notification);
+ }
+
+ if (entry != null && entry.row != null) {
+ entry.row.setRemoved();
+ mStackScroller.cleanUpViewState(entry.row);
+ }
+ // Let's remove the children if this was a summary
+ handleGroupSummaryRemoved(key, ranking);
+ StatusBarNotification old = removeNotificationViews(key, ranking);
+ if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old);
+
+ if (old != null) {
+ if (CLOSE_PANEL_WHEN_EMPTIED && !hasActiveNotifications()
+ && !mNotificationPanel.isTracking() && !mNotificationPanel.isQsExpanded()) {
+ if (mState == StatusBarState.SHADE) {
+ animateCollapsePanels();
+ } else if (mState == StatusBarState.SHADE_LOCKED && !isCollapsing()) {
+ goToKeyguard();
+ }
+ }
+ }
+ setAreThereNotifications();
+ }
+
+ /**
+ * Ensures that the group children are cancelled immediately when the group summary is cancelled
+ * instead of waiting for the notification manager to send all cancels. Otherwise this could
+ * lead to flickers.
+ *
+ * This also ensures that the animation looks nice and only consists of a single disappear
+ * animation instead of multiple.
+ *
+ * @param key the key of the notification was removed
+ * @param ranking the current ranking
+ */
+ private void handleGroupSummaryRemoved(String key,
+ RankingMap ranking) {
+ Entry entry = mNotificationData.get(key);
+ if (entry != null && entry.row != null
+ && entry.row.isSummaryWithChildren()) {
+ if (entry.notification.getOverrideGroupKey() != null && !entry.row.isDismissed()) {
+ // We don't want to remove children for autobundled notifications as they are not
+ // always cancelled. We only remove them if they were dismissed by the user.
+ return;
+ }
+ List<ExpandableNotificationRow> notificationChildren =
+ entry.row.getNotificationChildren();
+ ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow row = notificationChildren.get(i);
+ if ((row.getStatusBarNotification().getNotification().flags
+ & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
+ // the child is a forground service notification which we can't remove!
+ continue;
+ }
+ toRemove.add(row);
+ row.setKeepInParent(true);
+ // we need to set this state earlier as otherwise we might generate some weird
+ // animations
+ row.setRemoved();
+ }
+ }
+ }
+
+ protected void performRemoveNotification(StatusBarNotification n) {
+ Entry entry = mNotificationData.get(n.getKey());
+ if (mRemoteInputController.isRemoteInputActive(entry)) {
+ mRemoteInputController.removeRemoteInput(entry, null);
+ }
+ // start old BaseStatusBar.performRemoveNotification.
+ final String pkg = n.getPackageName();
+ final String tag = n.getTag();
+ final int id = n.getId();
+ final int userId = n.getUserId();
+ try {
+ mBarService.onNotificationClear(pkg, tag, id, userId);
+ if (FORCE_REMOTE_INPUT_HISTORY
+ && mKeysKeptForRemoteInput.contains(n.getKey())) {
+ mKeysKeptForRemoteInput.remove(n.getKey());
+ }
+ removeNotification(n.getKey(), null);
+
+ } catch (RemoteException ex) {
+ // system process is dead if we're here.
+ }
+ if (mStackScroller.hasPulsingNotifications() && mHeadsUpManager.getAllEntries().isEmpty()) {
+ // We were showing a pulse for a notification, but no notifications are pulsing anymore.
+ // Finish the pulse.
+ mDozeScrimController.pulseOutNow();
+ }
+ // end old BaseStatusBar.performRemoveNotification.
+ }
+
+ private void updateNotificationShade() {
+ if (mStackScroller == null) return;
+
+ // Do not modify the notifications during collapse.
+ if (isCollapsing()) {
+ addPostCollapseAction(new Runnable() {
+ @Override
+ public void run() {
+ updateNotificationShade();
+ }
+ });
+ return;
+ }
+
+ ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+ ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
+ final int N = activeNotifications.size();
+ for (int i=0; i<N; i++) {
+ Entry ent = activeNotifications.get(i);
+ if (ent.row.isDismissed() || ent.row.isRemoved()) {
+ // we don't want to update removed notifications because they could
+ // temporarily become children if they were isolated before.
+ continue;
+ }
+ int userId = ent.notification.getUserId();
+
+ // Display public version of the notification if we need to redact.
+ boolean devicePublic = isLockscreenPublicMode(mCurrentUserId);
+ boolean userPublic = devicePublic || isLockscreenPublicMode(userId);
+ boolean needsRedaction = needsRedaction(ent);
+ boolean sensitive = userPublic && needsRedaction;
+ boolean deviceSensitive = devicePublic
+ && !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
+ ent.row.setSensitive(sensitive, deviceSensitive);
+ ent.row.setNeedsRedaction(needsRedaction);
+ if (mGroupManager.isChildInGroupWithSummary(ent.row.getStatusBarNotification())) {
+ ExpandableNotificationRow summary = mGroupManager.getGroupSummary(
+ ent.row.getStatusBarNotification());
+ List<ExpandableNotificationRow> orderedChildren =
+ mTmpChildOrderMap.get(summary);
+ if (orderedChildren == null) {
+ orderedChildren = new ArrayList<>();
+ mTmpChildOrderMap.put(summary, orderedChildren);
+ }
+ orderedChildren.add(ent.row);
+ } else {
+ toShow.add(ent.row);
+ }
+
+ }
+
+ ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
+ for (int i=0; i< mStackScroller.getChildCount(); i++) {
+ View child = mStackScroller.getChildAt(i);
+ if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
+ toRemove.add((ExpandableNotificationRow) child);
+ }
+ }
+
+ for (ExpandableNotificationRow remove : toRemove) {
+ if (mGroupManager.isChildInGroupWithSummary(remove.getStatusBarNotification())) {
+ // we are only transfering this notification to its parent, don't generate an animation
+ mStackScroller.setChildTransferInProgress(true);
+ }
+ if (remove.isSummaryWithChildren()) {
+ remove.removeAllChildren();
+ }
+ mStackScroller.removeView(remove);
+ mStackScroller.setChildTransferInProgress(false);
+ }
+
+ removeNotificationChildren();
+
+ for (int i=0; i<toShow.size(); i++) {
+ View v = toShow.get(i);
+ if (v.getParent() == null) {
+ mVisualStabilityManager.notifyViewAddition(v);
+ mStackScroller.addView(v);
+ }
+ }
+
+ addNotificationChildrenAndSort();
+
+ // So after all this work notifications still aren't sorted correctly.
+ // Let's do that now by advancing through toShow and mStackScroller in
+ // lock-step, making sure mStackScroller matches what we see in toShow.
+ int j = 0;
+ for (int i = 0; i < mStackScroller.getChildCount(); i++) {
+ View child = mStackScroller.getChildAt(i);
+ if (!(child instanceof ExpandableNotificationRow)) {
+ // We don't care about non-notification views.
+ continue;
+ }
+
+ ExpandableNotificationRow targetChild = toShow.get(j);
+ if (child != targetChild) {
+ // Oops, wrong notification at this position. Put the right one
+ // here and advance both lists.
+ if (mVisualStabilityManager.canReorderNotification(targetChild)) {
+ mStackScroller.changeViewPosition(targetChild, i);
+ } else {
+ mVisualStabilityManager.addReorderingAllowedCallback(this);
+ }
+ }
+ j++;
+
+ }
+
+ mVisualStabilityManager.onReorderingFinished();
+ // clear the map again for the next usage
+ mTmpChildOrderMap.clear();
+
+ updateRowStates();
+ updateSpeedBumpIndex();
+ updateClearAll();
+ updateEmptyShadeView();
+
+ updateQsExpansionEnabled();
+
+ // Let's also update the icons
+ mNotificationIconAreaController.updateNotificationIcons(mNotificationData);
+ }
+
+ /** @return true if the entry needs redaction when on the lockscreen. */
+ private boolean needsRedaction(Entry ent) {
+ int userId = ent.notification.getUserId();
+
+ boolean currentUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
+ boolean notiUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(userId);
+ boolean redactedLockscreen = currentUserWantsRedaction || notiUserWantsRedaction;
+
+ boolean notificationRequestsRedaction =
+ ent.notification.getNotification().visibility == Notification.VISIBILITY_PRIVATE;
+ boolean userForcesRedaction = packageHasVisibilityOverride(ent.notification.getKey());
+
+ return userForcesRedaction || notificationRequestsRedaction && redactedLockscreen;
+ }
+
+ /**
+ * Disable QS if device not provisioned.
+ * 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.
+ */
+ private void updateQsExpansionEnabled() {
+ mNotificationPanel.setQsExpansionEnabled(isDeviceProvisioned()
+ && (mUserSetup || mUserSwitcherController == null
+ || !mUserSwitcherController.isSimpleUserSwitcher())
+ && ((mDisabled2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) == 0)
+ && !mDozing
+ && !ONLY_CORE_APPS);
+ }
+
+ private void addNotificationChildrenAndSort() {
+ // Let's now add all notification children which are missing
+ boolean orderChanged = false;
+ for (int i = 0; i < mStackScroller.getChildCount(); i++) {
+ View view = mStackScroller.getChildAt(i);
+ if (!(view instanceof ExpandableNotificationRow)) {
+ // We don't care about non-notification views.
+ continue;
+ }
+
+ ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
+ List<ExpandableNotificationRow> children = parent.getNotificationChildren();
+ List<ExpandableNotificationRow> orderedChildren = mTmpChildOrderMap.get(parent);
+
+ for (int childIndex = 0; orderedChildren != null && childIndex < orderedChildren.size();
+ childIndex++) {
+ ExpandableNotificationRow childView = orderedChildren.get(childIndex);
+ if (children == null || !children.contains(childView)) {
+ if (childView.getParent() != null) {
+ Log.wtf(TAG, "trying to add a notification child that already has " +
+ "a parent. class:" + childView.getParent().getClass() +
+ "\n child: " + childView);
+ // This shouldn't happen. We can recover by removing it though.
+ ((ViewGroup) childView.getParent()).removeView(childView);
+ }
+ mVisualStabilityManager.notifyViewAddition(childView);
+ parent.addChildNotification(childView, childIndex);
+ mStackScroller.notifyGroupChildAdded(childView);
+ }
+ }
+
+ // Finally after removing and adding has been beformed we can apply the order.
+ orderChanged |= parent.applyChildOrder(orderedChildren, mVisualStabilityManager, this);
+ }
+ if (orderChanged) {
+ mStackScroller.generateChildOrderChangedEvent();
+ }
+ }
+
+ private void removeNotificationChildren() {
+ // First let's remove all children which don't belong in the parents
+ ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
+ for (int i = 0; i < mStackScroller.getChildCount(); i++) {
+ View view = mStackScroller.getChildAt(i);
+ if (!(view instanceof ExpandableNotificationRow)) {
+ // We don't care about non-notification views.
+ continue;
+ }
+
+ ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
+ List<ExpandableNotificationRow> children = parent.getNotificationChildren();
+ List<ExpandableNotificationRow> orderedChildren = mTmpChildOrderMap.get(parent);
+
+ if (children != null) {
+ toRemove.clear();
+ for (ExpandableNotificationRow childRow : children) {
+ if ((orderedChildren == null
+ || !orderedChildren.contains(childRow))
+ && !childRow.keepInParent()) {
+ toRemove.add(childRow);
+ }
+ }
+ for (ExpandableNotificationRow remove : toRemove) {
+ parent.removeChildNotification(remove);
+ if (mNotificationData.get(remove.getStatusBarNotification().getKey()) == null) {
+ // We only want to add an animation if the view is completely removed
+ // otherwise it's just a transfer
+ mStackScroller.notifyGroupChildRemoved(remove,
+ parent.getChildrenContainer());
+ }
+ }
+ }
+ }
+ }
+
+ public void addQsTile(ComponentName tile) {
+ mQSPanel.getHost().addTile(tile);
+ }
+
+ public void remQsTile(ComponentName tile) {
+ mQSPanel.getHost().removeTile(tile);
+ }
+
+ public void clickTile(ComponentName tile) {
+ mQSPanel.clickTile(tile);
+ }
+
+ private boolean packageHasVisibilityOverride(String key) {
+ return mNotificationData.getVisibilityOverride(key) == Notification.VISIBILITY_PRIVATE;
+ }
+
+ private void updateClearAll() {
+ if (!mClearAllEnabled) {
+ return;
+ }
+ boolean showDismissView = mState != StatusBarState.KEYGUARD
+ && hasActiveClearableNotifications();
+ mStackScroller.updateDismissView(showDismissView);
+ }
+
+ /**
+ * Return whether there are any clearable notifications
+ */
+ private boolean hasActiveClearableNotifications() {
+ int childCount = mStackScroller.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = mStackScroller.getChildAt(i);
+ if (!(child instanceof ExpandableNotificationRow)) {
+ continue;
+ }
+ if (((ExpandableNotificationRow) child).canViewBeDismissed()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void updateEmptyShadeView() {
+ boolean showEmptyShadeView =
+ mState != StatusBarState.KEYGUARD &&
+ mNotificationData.getActiveNotifications().size() == 0;
+ mNotificationPanel.showEmptyShadeView(showEmptyShadeView);
+ }
+
+ private void updateSpeedBumpIndex() {
+ int speedBumpIndex = 0;
+ int currentIndex = 0;
+ final int N = mStackScroller.getChildCount();
+ for (int i = 0; i < N; i++) {
+ View view = mStackScroller.getChildAt(i);
+ if (view.getVisibility() == View.GONE || !(view instanceof ExpandableNotificationRow)) {
+ continue;
+ }
+ ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ currentIndex++;
+ if (!mNotificationData.isAmbient(row.getStatusBarNotification().getKey())) {
+ speedBumpIndex = currentIndex;
+ }
+ }
+ boolean noAmbient = speedBumpIndex == N;
+ mStackScroller.updateSpeedBumpIndex(speedBumpIndex, noAmbient);
+ }
+
+ public static boolean isTopLevelChild(Entry entry) {
+ return entry.row.getParent() instanceof NotificationStackScrollLayout;
+ }
+
+ protected void updateNotifications() {
+ mNotificationData.filterAndSort();
+
+ updateNotificationShade();
+ }
+
+ public void requestNotificationUpdate() {
+ updateNotifications();
+ }
+
+ protected void setAreThereNotifications() {
+
+ if (SPEW) {
+ final boolean clearable = hasActiveNotifications() &&
+ hasActiveClearableNotifications();
+ Log.d(TAG, "setAreThereNotifications: N=" +
+ mNotificationData.getActiveNotifications().size() + " any=" +
+ hasActiveNotifications() + " clearable=" + clearable);
+ }
+
+ if (mStatusBarView != null) {
+ final View nlo = mStatusBarView.findViewById(R.id.notification_lights_out);
+ final boolean showDot = hasActiveNotifications() && !areLightsOn();
+ if (showDot != (nlo.getAlpha() == 1.0f)) {
+ if (showDot) {
+ nlo.setAlpha(0f);
+ nlo.setVisibility(View.VISIBLE);
+ }
+ nlo.animate()
+ .alpha(showDot ? 1 : 0)
+ .setDuration(showDot ? 750 : 250)
+ .setInterpolator(new AccelerateInterpolator(2.0f))
+ .setListener(showDot ? null : new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator _a) {
+ nlo.setVisibility(View.GONE);
+ }
+ })
+ .start();
+ }
+ }
+
+ findAndUpdateMediaNotifications();
+ }
+
+ public void findAndUpdateMediaNotifications() {
+ boolean metaDataChanged = false;
+
+ synchronized (mNotificationData) {
+ ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+ final int N = activeNotifications.size();
+
+ // Promote the media notification with a controller in 'playing' state, if any.
+ Entry mediaNotification = null;
+ MediaController controller = null;
+ for (int i = 0; i < N; i++) {
+ final Entry entry = activeNotifications.get(i);
+ if (isMediaNotification(entry)) {
+ final MediaSession.Token token =
+ entry.notification.getNotification().extras
+ .getParcelable(Notification.EXTRA_MEDIA_SESSION);
+ if (token != null) {
+ MediaController aController = new MediaController(mContext, token);
+ if (PlaybackState.STATE_PLAYING ==
+ getMediaControllerPlaybackState(aController)) {
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: found mediastyle controller matching "
+ + entry.notification.getKey());
+ }
+ mediaNotification = entry;
+ controller = aController;
+ break;
+ }
+ }
+ }
+ }
+ if (mediaNotification == null) {
+ // Still nothing? OK, let's just look for live media sessions and see if they match
+ // one of our notifications. This will catch apps that aren't (yet!) using media
+ // notifications.
+
+ if (mMediaSessionManager != null) {
+ final List<MediaController> sessions
+ = mMediaSessionManager.getActiveSessionsForUser(
+ null,
+ UserHandle.USER_ALL);
+
+ for (MediaController aController : sessions) {
+ if (PlaybackState.STATE_PLAYING ==
+ getMediaControllerPlaybackState(aController)) {
+ // now to see if we have one like this
+ final String pkg = aController.getPackageName();
+
+ for (int i = 0; i < N; i++) {
+ final Entry entry = activeNotifications.get(i);
+ if (entry.notification.getPackageName().equals(pkg)) {
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: found controller matching "
+ + entry.notification.getKey());
+ }
+ controller = aController;
+ mediaNotification = entry;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (controller != null && !sameSessions(mMediaController, controller)) {
+ // We have a new media session
+ clearCurrentMediaNotification();
+ mMediaController = controller;
+ mMediaController.registerCallback(mMediaListener);
+ mMediaMetadata = mMediaController.getMetadata();
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: insert listener, receive metadata: "
+ + mMediaMetadata);
+ }
+
+ if (mediaNotification != null) {
+ mMediaNotificationKey = mediaNotification.notification.getKey();
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
+ + mMediaNotificationKey + " controller=" + mMediaController);
+ }
+ }
+ metaDataChanged = true;
+ }
+ }
+
+ if (metaDataChanged) {
+ updateNotifications();
+ }
+ updateMediaMetaData(metaDataChanged, true);
+ }
+
+ private int getMediaControllerPlaybackState(MediaController controller) {
+ if (controller != null) {
+ final PlaybackState playbackState = controller.getPlaybackState();
+ if (playbackState != null) {
+ return playbackState.getState();
+ }
+ }
+ return PlaybackState.STATE_NONE;
+ }
+
+ private boolean isPlaybackActive(int state) {
+ if (state != PlaybackState.STATE_STOPPED
+ && state != PlaybackState.STATE_ERROR
+ && state != PlaybackState.STATE_NONE) {
+ return true;
+ }
+ return false;
+ }
+
+ private void clearCurrentMediaNotification() {
+ mMediaNotificationKey = null;
+ mMediaMetadata = null;
+ if (mMediaController != null) {
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
+ + mMediaController.getPackageName());
+ }
+ mMediaController.unregisterCallback(mMediaListener);
+ }
+ mMediaController = null;
+ }
+
+ private boolean sameSessions(MediaController a, MediaController b) {
+ if (a == b) return true;
+ if (a == null) return false;
+ return a.controlsSameSession(b);
+ }
+
+ /**
+ * Hide the album artwork that is fading out and release its bitmap.
+ */
+ protected Runnable mHideBackdropFront = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: removing fade layer");
+ }
+ mBackdropFront.setVisibility(View.INVISIBLE);
+ mBackdropFront.animate().cancel();
+ mBackdropFront.setImageDrawable(null);
+ }
+ };
+
+ /**
+ * Refresh or remove lockscreen artwork from media metadata or the lockscreen wallpaper.
+ */
+ public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) {
+ Trace.beginSection("StatusBar#updateMediaMetaData");
+ if (!SHOW_LOCKSCREEN_MEDIA_ARTWORK) {
+ Trace.endSection();
+ return;
+ }
+
+ if (mBackdrop == null) {
+ Trace.endSection();
+ return; // called too early
+ }
+
+ if (mLaunchTransitionFadingAway) {
+ mBackdrop.setVisibility(View.INVISIBLE);
+ Trace.endSection();
+ return;
+ }
+
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: updating album art for notification " + mMediaNotificationKey
+ + " metadata=" + mMediaMetadata
+ + " metaDataChanged=" + metaDataChanged
+ + " state=" + mState);
+ }
+
+ Drawable artworkDrawable = null;
+ if (mMediaMetadata != null) {
+ Bitmap artworkBitmap = null;
+ artworkBitmap = mMediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
+ if (artworkBitmap == null) {
+ artworkBitmap = mMediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+ // might still be null
+ }
+ if (artworkBitmap != null) {
+ artworkDrawable = new BitmapDrawable(mBackdropBack.getResources(), artworkBitmap);
+ }
+ }
+ boolean allowWhenShade = false;
+ if (ENABLE_LOCKSCREEN_WALLPAPER && artworkDrawable == null) {
+ Bitmap lockWallpaper = mLockscreenWallpaper.getBitmap();
+ if (lockWallpaper != null) {
+ artworkDrawable = new LockscreenWallpaper.WallpaperDrawable(
+ mBackdropBack.getResources(), lockWallpaper);
+ // We're in the SHADE mode on the SIM screen - yet we still need to show
+ // the lockscreen wallpaper in that mode.
+ allowWhenShade = mStatusBarKeyguardViewManager != null
+ && mStatusBarKeyguardViewManager.isShowing();
+ }
+ }
+
+ boolean hideBecauseOccluded = mStatusBarKeyguardViewManager != null
+ && mStatusBarKeyguardViewManager.isOccluded();
+
+ final boolean hasArtwork = artworkDrawable != null;
+
+ if ((hasArtwork || DEBUG_MEDIA_FAKE_ARTWORK)
+ && (mState != StatusBarState.SHADE || allowWhenShade)
+ && mFingerprintUnlockController.getMode()
+ != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
+ && !hideBecauseOccluded) {
+ // time to show some art!
+ if (mBackdrop.getVisibility() != View.VISIBLE) {
+ mBackdrop.setVisibility(View.VISIBLE);
+ if (allowEnterAnimation) {
+ mBackdrop.setAlpha(SRC_MIN_ALPHA);
+ mBackdrop.animate().alpha(1f);
+ } else {
+ mBackdrop.animate().cancel();
+ mBackdrop.setAlpha(1f);
+ }
+ mStatusBarWindowManager.setBackdropShowing(true);
+ metaDataChanged = true;
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: Fading in album artwork");
+ }
+ }
+ if (metaDataChanged) {
+ if (mBackdropBack.getDrawable() != null) {
+ Drawable drawable =
+ mBackdropBack.getDrawable().getConstantState()
+ .newDrawable(mBackdropFront.getResources()).mutate();
+ mBackdropFront.setImageDrawable(drawable);
+ if (mScrimSrcModeEnabled) {
+ mBackdropFront.getDrawable().mutate().setXfermode(mSrcOverXferMode);
+ }
+ mBackdropFront.setAlpha(1f);
+ mBackdropFront.setVisibility(View.VISIBLE);
+ } else {
+ mBackdropFront.setVisibility(View.INVISIBLE);
+ }
+
+ if (DEBUG_MEDIA_FAKE_ARTWORK) {
+ final int c = 0xFF000000 | (int)(Math.random() * 0xFFFFFF);
+ Log.v(TAG, String.format("DEBUG_MEDIA: setting new color: 0x%08x", c));
+ mBackdropBack.setBackgroundColor(0xFFFFFFFF);
+ mBackdropBack.setImageDrawable(new ColorDrawable(c));
+ } else {
+ mBackdropBack.setImageDrawable(artworkDrawable);
+ }
+ if (mScrimSrcModeEnabled) {
+ mBackdropBack.getDrawable().mutate().setXfermode(mSrcXferMode);
+ }
+
+ if (mBackdropFront.getVisibility() == View.VISIBLE) {
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: Crossfading album artwork from "
+ + mBackdropFront.getDrawable()
+ + " to "
+ + mBackdropBack.getDrawable());
+ }
+ mBackdropFront.animate()
+ .setDuration(250)
+ .alpha(0f).withEndAction(mHideBackdropFront);
+ }
+ }
+ } else {
+ // need to hide the album art, either because we are unlocked or because
+ // the metadata isn't there to support it
+ if (mBackdrop.getVisibility() != View.GONE) {
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: Fading out album artwork");
+ }
+ if (mFingerprintUnlockController.getMode()
+ == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
+ || hideBecauseOccluded) {
+
+ // We are unlocking directly - no animation!
+ mBackdrop.setVisibility(View.GONE);
+ mBackdropBack.setImageDrawable(null);
+ mStatusBarWindowManager.setBackdropShowing(false);
+ } else {
+ mStatusBarWindowManager.setBackdropShowing(false);
+ mBackdrop.animate()
+ .alpha(SRC_MIN_ALPHA)
+ .setInterpolator(Interpolators.ACCELERATE_DECELERATE)
+ .setDuration(300)
+ .setStartDelay(0)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mBackdrop.setVisibility(View.GONE);
+ mBackdropFront.animate().cancel();
+ mBackdropBack.setImageDrawable(null);
+ mHandler.post(mHideBackdropFront);
+ }
+ });
+ if (mKeyguardFadingAway) {
+ mBackdrop.animate()
+ // Make it disappear faster, as the focus should be on the activity
+ // behind.
+ .setDuration(mKeyguardFadingAwayDuration / 2)
+ .setStartDelay(mKeyguardFadingAwayDelay)
+ .setInterpolator(Interpolators.LINEAR)
+ .start();
+ }
+ }
+ }
+ }
+ Trace.endSection();
+ }
+
+ private void updateReportRejectedTouchVisibility() {
+ if (mReportRejectedTouch == null) {
+ return;
+ }
+ mReportRejectedTouch.setVisibility(mState == StatusBarState.KEYGUARD
+ && mFalsingManager.isReportingEnabled() ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ /**
+ * State is one or more of the DISABLE constants from StatusBarManager.
+ */
+ @Override
+ public void disable(int state1, int state2, boolean animate) {
+ animate &= mStatusBarWindowState != WINDOW_STATE_HIDDEN;
+ mDisabledUnmodified1 = state1;
+ mDisabledUnmodified2 = state2;
+ final int old1 = mDisabled1;
+ final int diff1 = state1 ^ old1;
+ mDisabled1 = state1;
+
+ final int old2 = mDisabled2;
+ final int diff2 = state2 ^ old2;
+ mDisabled2 = state2;
+
+ if (DEBUG) {
+ Log.d(TAG, String.format("disable1: 0x%08x -> 0x%08x (diff1: 0x%08x)",
+ old1, state1, diff1));
+ Log.d(TAG, String.format("disable2: 0x%08x -> 0x%08x (diff2: 0x%08x)",
+ old2, state2, diff2));
+ }
+
+ StringBuilder flagdbg = new StringBuilder();
+ flagdbg.append("disable<");
+ flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_EXPAND)) ? 'E' : 'e');
+ flagdbg.append(0 != ((diff1 & StatusBarManager.DISABLE_EXPAND)) ? '!' : ' ');
+ flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_NOTIFICATION_ICONS)) ? 'I' : 'i');
+ flagdbg.append(0 != ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ICONS)) ? '!' : ' ');
+ flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS)) ? 'A' : 'a');
+ flagdbg.append(0 != ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS)) ? '!' : ' ');
+ flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_SYSTEM_INFO)) ? 'S' : 's');
+ flagdbg.append(0 != ((diff1 & StatusBarManager.DISABLE_SYSTEM_INFO)) ? '!' : ' ');
+ flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_BACK)) ? 'B' : 'b');
+ flagdbg.append(0 != ((diff1 & StatusBarManager.DISABLE_BACK)) ? '!' : ' ');
+ flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_HOME)) ? 'H' : 'h');
+ flagdbg.append(0 != ((diff1 & StatusBarManager.DISABLE_HOME)) ? '!' : ' ');
+ flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_RECENT)) ? 'R' : 'r');
+ flagdbg.append(0 != ((diff1 & StatusBarManager.DISABLE_RECENT)) ? '!' : ' ');
+ flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_CLOCK)) ? 'C' : 'c');
+ flagdbg.append(0 != ((diff1 & StatusBarManager.DISABLE_CLOCK)) ? '!' : ' ');
+ flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_SEARCH)) ? 'S' : 's');
+ flagdbg.append(0 != ((diff1 & StatusBarManager.DISABLE_SEARCH)) ? '!' : ' ');
+ flagdbg.append(0 != ((state2 & StatusBarManager.DISABLE2_QUICK_SETTINGS)) ? 'Q' : 'q');
+ flagdbg.append(0 != ((diff2 & StatusBarManager.DISABLE2_QUICK_SETTINGS)) ? '!' : ' ');
+ flagdbg.append('>');
+ Log.d(TAG, flagdbg.toString());
+
+ if ((diff1 & StatusBarManager.DISABLE_EXPAND) != 0) {
+ if ((state1 & StatusBarManager.DISABLE_EXPAND) != 0) {
+ animateCollapsePanels();
+ }
+ }
+
+ if ((diff1 & StatusBarManager.DISABLE_RECENT) != 0) {
+ if ((state1 & StatusBarManager.DISABLE_RECENT) != 0) {
+ // close recents if it's visible
+ mHandler.removeMessages(MSG_HIDE_RECENT_APPS);
+ mHandler.sendEmptyMessage(MSG_HIDE_RECENT_APPS);
+ }
+ }
+
+ if ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) {
+ mDisableNotificationAlerts =
+ (state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0;
+ mHeadsUpObserver.onChange(true);
+ }
+
+ if ((diff2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) != 0) {
+ updateQsExpansionEnabled();
+ }
+ }
+
+ /**
+ * Reapplies the disable flags as last requested by StatusBarManager.
+ *
+ * This needs to be called if state used by {@link #adjustDisableFlags} changes.
+ */
+ public void recomputeDisableFlags(boolean animate) {
+ mCommandQueue.recomputeDisableFlags(animate);
+ }
+
+ protected H createHandler() {
+ return new StatusBar.H();
+ }
+
+ @Override
+ public void startActivity(Intent intent, boolean dismissShade) {
+ startActivityDismissingKeyguard(intent, false, dismissShade);
+ }
+
+ @Override
+ public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade) {
+ startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade);
+ }
+
+ @Override
+ public void startActivity(Intent intent, boolean dismissShade, Callback callback) {
+ startActivityDismissingKeyguard(intent, false, dismissShade,
+ false /* disallowEnterPictureInPictureWhileLaunching */, callback);
+ }
+
+ public void setQsExpanded(boolean expanded) {
+ mStatusBarWindowManager.setQsExpanded(expanded);
+ mNotificationPanel.setStatusAccessibilityImportance(expanded
+ ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+
+ public boolean isGoingToNotificationShade() {
+ return mLeaveOpenOnKeyguardHide;
+ }
+
+ public boolean isWakeUpComingFromTouch() {
+ return mWakeUpComingFromTouch;
+ }
+
+ public boolean isFalsingThresholdNeeded() {
+ return getBarState() == StatusBarState.KEYGUARD;
+ }
+
+ public boolean isDozing() {
+ return mDozing;
+ }
+
+ @Override // NotificationData.Environment
+ public String getCurrentMediaNotificationKey() {
+ return mMediaNotificationKey;
+ }
+
+ public boolean isScrimSrcModeEnabled() {
+ return mScrimSrcModeEnabled;
+ }
+
+ /**
+ * To be called when there's a state change in StatusBarKeyguardViewManager.
+ */
+ public void onKeyguardViewManagerStatesUpdated() {
+ logStateToEventlog();
+ }
+
+ @Override // UnlockMethodCache.OnUnlockMethodChangedListener
+ public void onUnlockMethodStateChanged() {
+ logStateToEventlog();
+ }
+
+ @Override
+ public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+ if (inPinnedMode) {
+ mStatusBarWindowManager.setHeadsUpShowing(true);
+ mStatusBarWindowManager.setForceStatusBarVisible(true);
+ if (mNotificationPanel.isFullyCollapsed()) {
+ // We need to ensure that the touchable region is updated before the window will be
+ // resized, in order to not catch any touches. A layout will ensure that
+ // onComputeInternalInsets will be called and after that we can resize the layout. Let's
+ // make sure that the window stays small for one frame until the touchableRegion is set.
+ mNotificationPanel.requestLayout();
+ mStatusBarWindowManager.setForceWindowCollapsed(true);
+ mNotificationPanel.post(new Runnable() {
+ @Override
+ public void run() {
+ mStatusBarWindowManager.setForceWindowCollapsed(false);
+ }
+ });
+ }
+ } else {
+ if (!mNotificationPanel.isFullyCollapsed() || mNotificationPanel.isTracking()) {
+ // We are currently tracking or is open and the shade doesn't need to be kept
+ // open artificially.
+ mStatusBarWindowManager.setHeadsUpShowing(false);
+ } else {
+ // we need to keep the panel open artificially, let's wait until the animation
+ // is finished.
+ mHeadsUpManager.setHeadsUpGoingAway(true);
+ mStackScroller.runAfterAnimationFinished(new Runnable() {
+ @Override
+ public void run() {
+ if (!mHeadsUpManager.hasPinnedHeadsUp()) {
+ mStatusBarWindowManager.setHeadsUpShowing(false);
+ mHeadsUpManager.setHeadsUpGoingAway(false);
+ }
+ removeRemoteInputEntriesKeptUntilCollapsed();
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
+ dismissVolumeDialog();
+ }
+
+ @Override
+ public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
+ }
+
+ @Override
+ public void onHeadsUpStateChanged(Entry entry, boolean isHeadsUp) {
+ if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) {
+ removeNotification(entry.key, mLatestRankingMap);
+ mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
+ if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) {
+ mLatestRankingMap = null;
+ }
+ } else {
+ updateNotificationRanking(null);
+ if (isHeadsUp) {
+ mDozeServiceHost.fireNotificationHeadsUp();
+ }
+ }
+
+ }
+
+ protected void updateHeadsUp(String key, Entry entry, boolean shouldPeek,
+ boolean alertAgain) {
+ final boolean wasHeadsUp = isHeadsUp(key);
+ if (wasHeadsUp) {
+ if (!shouldPeek) {
+ // We don't want this to be interrupting anymore, lets remove it
+ mHeadsUpManager.removeNotification(key, false /* ignoreEarliestRemovalTime */);
+ } else {
+ mHeadsUpManager.updateNotification(entry, alertAgain);
+ }
+ } else if (shouldPeek && alertAgain) {
+ // This notification was updated to be a heads-up, show it!
+ mHeadsUpManager.showNotification(entry);
+ }
+ }
+
+ protected void setHeadsUpUser(int newUserId) {
+ if (mHeadsUpManager != null) {
+ mHeadsUpManager.setUser(newUserId);
+ }
+ }
+
+ public boolean isHeadsUp(String key) {
+ return mHeadsUpManager.isHeadsUp(key);
+ }
+
+ protected boolean isSnoozedPackage(StatusBarNotification sbn) {
+ return mHeadsUpManager.isSnoozed(sbn.getPackageName());
+ }
+
+ public boolean isKeyguardCurrentlySecure() {
+ return !mUnlockMethodCache.canSkipBouncer();
+ }
+
+ public void setPanelExpanded(boolean isExpanded) {
+ mPanelExpanded = isExpanded;
+ updateHideIconsForBouncer(false /* animate */);
+ mStatusBarWindowManager.setPanelExpanded(isExpanded);
+ mVisualStabilityManager.setPanelExpanded(isExpanded);
+ if (isExpanded && getBarState() != StatusBarState.KEYGUARD) {
+ if (DEBUG) {
+ Log.v(TAG, "clearing notification effects from setPanelExpanded");
+ }
+ clearNotificationEffects();
+ }
+
+ if (!isExpanded) {
+ removeRemoteInputEntriesKeptUntilCollapsed();
+ }
+ }
+
+ private void removeRemoteInputEntriesKeptUntilCollapsed() {
+ for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) {
+ Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i);
+ mRemoteInputController.removeRemoteInput(entry, null);
+ removeNotification(entry.key, mLatestRankingMap);
+ }
+ mRemoteInputEntriesToRemoveOnCollapse.clear();
+ }
+
+ public NotificationStackScrollLayout getNotificationScrollLayout() {
+ return mStackScroller;
+ }
+
+ public boolean isPulsing() {
+ return mDozeScrimController.isPulsing();
+ }
+
+ @Override
+ public void onReorderingAllowed() {
+ updateNotifications();
+ }
+
+ public boolean isLaunchTransitionFadingAway() {
+ return mLaunchTransitionFadingAway;
+ }
+
+ public boolean hideStatusBarIconsWhenExpanded() {
+ return mNotificationPanel.hideStatusBarIconsWhenExpanded();
+ }
+
+ @Override
+ public void onColorsChanged(ColorExtractor extractor, int which) {
+ updateTheme();
+ }
+
+ public boolean isUsingDarkTheme() {
+ OverlayInfo themeInfo = null;
+ try {
+ themeInfo = mOverlayManager.getOverlayInfo("com.android.systemui.theme.dark",
+ mCurrentUserId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ return themeInfo != null && themeInfo.isEnabled();
+ }
+
+ @Nullable
+ public View getAmbientIndicationContainer() {
+ return mAmbientIndicationContainer;
+ }
+
+ public void setOccluded(boolean occluded) {
+ mIsOccluded = occluded;
+ updateHideIconsForBouncer(false /* animate */);
+ }
+
+ public boolean hideStatusBarIconsForBouncer() {
+ return mHideIconsForBouncer || mWereIconsJustHidden;
+ }
+
+ /**
+ * @param animate should the change of the icons be animated.
+ */
+ private void updateHideIconsForBouncer(boolean animate) {
+ boolean shouldHideIconsForBouncer = !mPanelExpanded && mTopHidesStatusBar && mIsOccluded
+ && (mBouncerShowing || mStatusBarWindowHidden);
+ if (mHideIconsForBouncer != shouldHideIconsForBouncer) {
+ mHideIconsForBouncer = shouldHideIconsForBouncer;
+ if (!shouldHideIconsForBouncer && mBouncerWasShowingWhenHidden) {
+ // We're delaying the showing, since most of the time the fullscreen app will
+ // hide the icons again and we don't want them to fade in and out immediately again.
+ mWereIconsJustHidden = true;
+ mHandler.postDelayed(() -> {
+ mWereIconsJustHidden = false;
+ recomputeDisableFlags(true);
+ }, 500);
+ } else {
+ recomputeDisableFlags(animate);
+ }
+ }
+ if (shouldHideIconsForBouncer) {
+ mBouncerWasShowingWhenHidden = mBouncerShowing;
+ }
+ }
+
+ /**
+ * All changes to the status bar and notifications funnel through here and are batched.
+ */
+ protected class H extends Handler {
+ @Override
+ public void handleMessage(Message m) {
+ switch (m.what) {
+ case MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU:
+ toggleKeyboardShortcuts(m.arg1);
+ break;
+ case MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU:
+ dismissKeyboardShortcuts();
+ break;
+ // End old BaseStatusBar.H handling.
+ case MSG_OPEN_NOTIFICATION_PANEL:
+ animateExpandNotificationsPanel();
+ break;
+ case MSG_OPEN_SETTINGS_PANEL:
+ animateExpandSettingsPanel((String) m.obj);
+ break;
+ case MSG_CLOSE_PANELS:
+ animateCollapsePanels();
+ break;
+ case MSG_LAUNCH_TRANSITION_TIMEOUT:
+ onLaunchTransitionTimeout();
+ break;
+ }
+ }
+ }
+
+ public void maybeEscalateHeadsUp() {
+ Collection<HeadsUpManager.HeadsUpEntry> entries = mHeadsUpManager.getAllEntries();
+ for (HeadsUpManager.HeadsUpEntry entry : entries) {
+ final StatusBarNotification sbn = entry.entry.notification;
+ final Notification notification = sbn.getNotification();
+ if (notification.fullScreenIntent != null) {
+ if (DEBUG) {
+ Log.d(TAG, "converting a heads up to fullScreen");
+ }
+ try {
+ EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_ESCALATION,
+ sbn.getKey());
+ notification.fullScreenIntent.send();
+ entry.entry.notifyFullScreenIntentLaunched();
+ } catch (PendingIntent.CanceledException e) {
+ }
+ }
+ }
+ mHeadsUpManager.releaseAllImmediately();
+ }
+
+ /**
+ * Called for system navigation gestures. First action opens the panel, second opens
+ * settings. Down action closes the entire panel.
+ */
+ @Override
+ public void handleSystemKey(int key) {
+ if (SPEW) Log.d(TAG, "handleNavigationKey: " + key);
+ if (!panelsEnabled() || !mKeyguardMonitor.isDeviceInteractive()
+ || mKeyguardMonitor.isShowing() && !mKeyguardMonitor.isOccluded()) {
+ return;
+ }
+
+ // Panels are not available in setup
+ if (!mUserSetup) return;
+
+ if (KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP == key) {
+ mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_UP);
+ mNotificationPanel.collapse(false /* delayed */, 1.0f /* speedUpFactor */);
+ } else if (KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN == key) {
+ mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_DOWN);
+ if (mNotificationPanel.isFullyCollapsed()) {
+ mNotificationPanel.expand(true /* animate */);
+ mMetricsLogger.count(NotificationPanelView.COUNTER_PANEL_OPEN, 1);
+ } else if (!mNotificationPanel.isInSettings() && !mNotificationPanel.isExpanding()){
+ mNotificationPanel.flingSettings(0 /* velocity */, true /* expand */);
+ mMetricsLogger.count(NotificationPanelView.COUNTER_PANEL_OPEN_QS, 1);
+ }
+ }
+
+ }
+
+ boolean panelsEnabled() {
+ return (mDisabled1 & StatusBarManager.DISABLE_EXPAND) == 0 && !ONLY_CORE_APPS;
+ }
+
+ void makeExpandedVisible(boolean force) {
+ if (SPEW) Log.d(TAG, "Make expanded visible: expanded visible=" + mExpandedVisible);
+ if (!force && (mExpandedVisible || !panelsEnabled())) {
+ return;
+ }
+
+ mExpandedVisible = true;
+
+ // Expand the window to encompass the full screen in anticipation of the drag.
+ // This is only possible to do atomically because the status bar is at the top of the screen!
+ mStatusBarWindowManager.setPanelVisible(true);
+
+ visibilityChanged(true);
+ mWaitingForKeyguardExit = false;
+ recomputeDisableFlags(!force /* animate */);
+ setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true);
+ }
+
+ public void animateCollapsePanels() {
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
+ }
+
+ private final Runnable mAnimateCollapsePanels = new Runnable() {
+ @Override
+ public void run() {
+ animateCollapsePanels();
+ }
+ };
+
+ public void postAnimateCollapsePanels() {
+ mHandler.post(mAnimateCollapsePanels);
+ }
+
+ public void postAnimateForceCollapsePanels() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */);
+ }
+ });
+ }
+
+ public void postAnimateOpenPanels() {
+ mHandler.sendEmptyMessage(MSG_OPEN_SETTINGS_PANEL);
+ }
+
+ @Override
+ public void togglePanel() {
+ if (mPanelExpanded) {
+ animateCollapsePanels();
+ } else {
+ animateExpandNotificationsPanel();
+ }
+ }
+
+ @Override
+ public void animateCollapsePanels(int flags) {
+ animateCollapsePanels(flags, false /* force */, false /* delayed */,
+ 1.0f /* speedUpFactor */);
+ }
+
+ public void animateCollapsePanels(int flags, boolean force) {
+ animateCollapsePanels(flags, force, false /* delayed */, 1.0f /* speedUpFactor */);
+ }
+
+ public void animateCollapsePanels(int flags, boolean force, boolean delayed) {
+ animateCollapsePanels(flags, force, delayed, 1.0f /* speedUpFactor */);
+ }
+
+ public void animateCollapsePanels(int flags, boolean force, boolean delayed,
+ float speedUpFactor) {
+ if (!force && mState != StatusBarState.SHADE) {
+ runPostCollapseRunnables();
+ return;
+ }
+ if (SPEW) {
+ Log.d(TAG, "animateCollapse():"
+ + " mExpandedVisible=" + mExpandedVisible
+ + " flags=" + flags);
+ }
+
+ if ((flags & CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL) == 0) {
+ if (!mHandler.hasMessages(MSG_HIDE_RECENT_APPS)) {
+ mHandler.removeMessages(MSG_HIDE_RECENT_APPS);
+ mHandler.sendEmptyMessage(MSG_HIDE_RECENT_APPS);
+ }
+ }
+
+ if (mStatusBarWindow != null && mNotificationPanel.canPanelBeCollapsed()) {
+ // release focus immediately to kick off focus change transition
+ mStatusBarWindowManager.setStatusBarFocusable(false);
+
+ mStatusBarWindow.cancelExpandHelper();
+ mStatusBarView.collapsePanel(true /* animate */, delayed, speedUpFactor);
+ }
+ }
+
+ private void runPostCollapseRunnables() {
+ ArrayList<Runnable> clonedList = new ArrayList<>(mPostCollapseRunnables);
+ mPostCollapseRunnables.clear();
+ int size = clonedList.size();
+ for (int i = 0; i < size; i++) {
+ clonedList.get(i).run();
+ }
+ mStatusBarKeyguardViewManager.readyForKeyguardDone();
+ }
+
+ @Override
+ public void animateExpandNotificationsPanel() {
+ if (SPEW) Log.d(TAG, "animateExpand: mExpandedVisible=" + mExpandedVisible);
+ if (!panelsEnabled()) {
+ return ;
+ }
+
+ mNotificationPanel.expand(true /* animate */);
+
+ if (false) postStartTracing();
+ }
+
+ @Override
+ public void animateExpandSettingsPanel(String subPanel) {
+ if (SPEW) Log.d(TAG, "animateExpand: mExpandedVisible=" + mExpandedVisible);
+ if (!panelsEnabled()) {
+ return;
+ }
+
+ // Settings are not available in setup
+ if (!mUserSetup) return;
+
+
+ if (subPanel != null) {
+ mQSPanel.openDetails(subPanel);
+ }
+ mNotificationPanel.expandWithQs();
+
+ if (false) postStartTracing();
+ }
+
+ public void animateCollapseQuickSettings() {
+ if (mState == StatusBarState.SHADE) {
+ mStatusBarView.collapsePanel(true, false /* delayed */, 1.0f /* speedUpFactor */);
+ }
+ }
+
+ void makeExpandedInvisible() {
+ if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible
+ + " mExpandedVisible=" + mExpandedVisible);
+
+ if (!mExpandedVisible || mStatusBarWindow == null) {
+ return;
+ }
+
+ // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868)
+ mStatusBarView.collapsePanel(/*animate=*/ false, false /* delayed*/,
+ 1.0f /* speedUpFactor */);
+
+ mNotificationPanel.closeQs();
+
+ mExpandedVisible = false;
+ visibilityChanged(false);
+
+ // Shrink the window to the size of the status bar only
+ mStatusBarWindowManager.setPanelVisible(false);
+ mStatusBarWindowManager.setForceStatusBarVisible(false);
+
+ // Close any guts that might be visible
+ closeAndSaveGuts(true /* removeLeavebehind */, true /* force */, true /* removeControls */,
+ -1 /* x */, -1 /* y */, true /* resetMenu */);
+
+ runPostCollapseRunnables();
+ setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
+ showBouncerIfKeyguard();
+ recomputeDisableFlags(mNotificationPanel.hideStatusBarIconsWhenExpanded() /* 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) {
+ if (DEBUG_GESTURES) {
+ if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
+ EventLog.writeEvent(EventLogTags.SYSUI_STATUSBAR_TOUCH,
+ event.getActionMasked(), (int) event.getX(), (int) event.getY(),
+ mDisabled1, mDisabled2);
+ }
+
+ }
+
+ if (SPEW) {
+ Log.d(TAG, "Touch: rawY=" + event.getRawY() + " event=" + event + " mDisabled1="
+ + mDisabled1 + " mDisabled2=" + mDisabled2 + " mTracking=" + mTracking);
+ } else if (CHATTY) {
+ if (event.getAction() != MotionEvent.ACTION_MOVE) {
+ Log.d(TAG, String.format(
+ "panel: %s at (%f, %f) mDisabled1=0x%08x mDisabled2=0x%08x",
+ MotionEvent.actionToString(event.getAction()),
+ event.getRawX(), event.getRawY(), mDisabled1, mDisabled2));
+ }
+ }
+
+ if (DEBUG_GESTURES) {
+ mGestureRec.add(event);
+ }
+
+ if (mStatusBarWindowState == WINDOW_STATE_SHOWING) {
+ final boolean upOrCancel =
+ event.getAction() == MotionEvent.ACTION_UP ||
+ event.getAction() == MotionEvent.ACTION_CANCEL;
+ if (upOrCancel && !mExpandedVisible) {
+ setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
+ } else {
+ setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true);
+ }
+ }
+ return false;
+ }
+
+ public GestureRecorder getGestureRecorder() {
+ return mGestureRec;
+ }
+
+ public FingerprintUnlockController getFingerprintUnlockController() {
+ return mFingerprintUnlockController;
+ }
+
+ @Override // CommandQueue
+ public void setWindowState(int window, int state) {
+ boolean showing = state == WINDOW_STATE_SHOWING;
+ if (mStatusBarWindow != null
+ && window == StatusBarManager.WINDOW_STATUS_BAR
+ && mStatusBarWindowState != state) {
+ mStatusBarWindowState = state;
+ if (DEBUG_WINDOW_STATE) Log.d(TAG, "Status bar " + windowStateToString(state));
+ if (!showing && mState == StatusBarState.SHADE) {
+ mStatusBarView.collapsePanel(false /* animate */, false /* delayed */,
+ 1.0f /* speedUpFactor */);
+ }
+ if (mStatusBarView != null) {
+ mStatusBarWindowHidden = state == WINDOW_STATE_HIDDEN;
+ updateHideIconsForBouncer(false /* animate */);
+ }
+ }
+ }
+
+ @Override // CommandQueue
+ public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
+ int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
+ final int oldVal = mSystemUiVisibility;
+ final int newVal = (oldVal&~mask) | (vis&mask);
+ final int diff = newVal ^ oldVal;
+ if (DEBUG) Log.d(TAG, String.format(
+ "setSystemUiVisibility vis=%s mask=%s oldVal=%s newVal=%s diff=%s",
+ Integer.toHexString(vis), Integer.toHexString(mask),
+ Integer.toHexString(oldVal), Integer.toHexString(newVal),
+ Integer.toHexString(diff)));
+ boolean sbModeChanged = false;
+ if (diff != 0) {
+ mSystemUiVisibility = newVal;
+
+ // update low profile
+ if ((diff & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
+ setAreThereNotifications();
+ }
+
+ // ready to unhide
+ if ((vis & View.STATUS_BAR_UNHIDE) != 0) {
+ mSystemUiVisibility &= ~View.STATUS_BAR_UNHIDE;
+ mNoAnimationOnNextBarModeChange = true;
+ }
+
+ // update status bar mode
+ final int sbMode = computeStatusBarMode(oldVal, newVal);
+
+ sbModeChanged = sbMode != -1;
+ if (sbModeChanged && sbMode != mStatusBarMode) {
+ if (sbMode != mStatusBarMode) {
+ mStatusBarMode = sbMode;
+ checkBarModes();
+ }
+ touchAutoHide();
+ }
+
+ if ((vis & View.NAVIGATION_BAR_UNHIDE) != 0) {
+ mSystemUiVisibility &= ~View.NAVIGATION_BAR_UNHIDE;
+ }
+
+ // send updated sysui visibility to window manager
+ notifyUiVisibilityChanged(mSystemUiVisibility);
+ }
+
+ mLightBarController.onSystemUiVisibilityChanged(fullscreenStackVis, dockedStackVis,
+ mask, fullscreenStackBounds, dockedStackBounds, sbModeChanged, mStatusBarMode);
+ }
+
+ void touchAutoHide() {
+ // update transient bar autohide
+ if (mStatusBarMode == MODE_SEMI_TRANSPARENT || (mNavigationBar != null
+ && mNavigationBar.isSemiTransparent())) {
+ scheduleAutohide();
+ } else {
+ cancelAutohide();
+ }
+ touchAutoDim();
+ }
+
+ protected int computeStatusBarMode(int oldVal, int newVal) {
+ return computeBarMode(oldVal, newVal, View.STATUS_BAR_TRANSIENT,
+ View.STATUS_BAR_TRANSLUCENT, View.STATUS_BAR_TRANSPARENT);
+ }
+
+ protected BarTransitions getStatusBarTransitions() {
+ return mStatusBarView.getBarTransitions();
+ }
+
+ protected int computeBarMode(int oldVis, int newVis,
+ int transientFlag, int translucentFlag, int transparentFlag) {
+ final int oldMode = barMode(oldVis, transientFlag, translucentFlag, transparentFlag);
+ final int newMode = barMode(newVis, transientFlag, translucentFlag, transparentFlag);
+ if (oldMode == newMode) {
+ return -1; // no mode change
+ }
+ return newMode;
+ }
+
+ private int barMode(int vis, int transientFlag, int translucentFlag, int transparentFlag) {
+ int lightsOutTransparent = View.SYSTEM_UI_FLAG_LOW_PROFILE | transparentFlag;
+ return (vis & transientFlag) != 0 ? MODE_SEMI_TRANSPARENT
+ : (vis & translucentFlag) != 0 ? MODE_TRANSLUCENT
+ : (vis & lightsOutTransparent) == lightsOutTransparent ? MODE_LIGHTS_OUT_TRANSPARENT
+ : (vis & transparentFlag) != 0 ? MODE_TRANSPARENT
+ : (vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0 ? MODE_LIGHTS_OUT
+ : MODE_OPAQUE;
+ }
+
+ void checkBarModes() {
+ if (mDemoMode) return;
+ if (mStatusBarView != null) checkBarMode(mStatusBarMode, mStatusBarWindowState,
+ getStatusBarTransitions());
+ if (mNavigationBar != null) mNavigationBar.checkNavBarModes();
+ mNoAnimationOnNextBarModeChange = false;
+ }
+
+ // Called by NavigationBarFragment
+ void setQsScrimEnabled(boolean scrimEnabled) {
+ mNotificationPanel.setQsScrimEnabled(scrimEnabled);
+ }
+
+ void checkBarMode(int mode, int windowState, BarTransitions transitions) {
+ final boolean powerSave = mBatteryController.isPowerSave();
+ final boolean anim = !mNoAnimationOnNextBarModeChange && mDeviceInteractive
+ && windowState != WINDOW_STATE_HIDDEN && !powerSave;
+ if (powerSave && getBarState() == StatusBarState.SHADE) {
+ mode = MODE_WARNING;
+ }
+ transitions.transitionTo(mode, anim);
+ }
+
+ private void finishBarAnimations() {
+ if (mStatusBarView != null) {
+ mStatusBarView.getBarTransitions().finishAnimations();
+ }
+ if (mNavigationBar != null) {
+ mNavigationBar.finishBarAnimations();
+ }
+ }
+
+ private final Runnable mCheckBarModes = new Runnable() {
+ @Override
+ public void run() {
+ checkBarModes();
+ }
+ };
+
+ public void setInteracting(int barWindow, boolean interacting) {
+ final boolean changing = ((mInteractingWindows & barWindow) != 0) != interacting;
+ mInteractingWindows = interacting
+ ? (mInteractingWindows | barWindow)
+ : (mInteractingWindows & ~barWindow);
+ if (mInteractingWindows != 0) {
+ suspendAutohide();
+ } else {
+ resumeSuspendedAutohide();
+ }
+ // manually dismiss the volume panel when interacting with the nav bar
+ if (changing && interacting && barWindow == StatusBarManager.WINDOW_NAVIGATION_BAR) {
+ dismissVolumeDialog();
+ }
+ checkBarModes();
+ touchAutoDim();
+ }
+
+ private void dismissVolumeDialog() {
+ if (mVolumeComponent != null) {
+ mVolumeComponent.dismissNow();
+ }
+ }
+
+ private void resumeSuspendedAutohide() {
+ if (mAutohideSuspended) {
+ scheduleAutohide();
+ mHandler.postDelayed(mCheckBarModes, 500); // longer than home -> launcher
+ }
+ }
+
+ private void suspendAutohide() {
+ mHandler.removeCallbacks(mAutohide);
+ mHandler.removeCallbacks(mCheckBarModes);
+ mAutohideSuspended = (mSystemUiVisibility & STATUS_OR_NAV_TRANSIENT) != 0;
+ }
+
+ private void cancelAutohide() {
+ mAutohideSuspended = false;
+ mHandler.removeCallbacks(mAutohide);
+ }
+
+ private void scheduleAutohide() {
+ cancelAutohide();
+ mHandler.postDelayed(mAutohide, AUTOHIDE_TIMEOUT_MS);
+ }
+
+ public void touchAutoDim() {
+ if (mNavigationBar != null) {
+ mNavigationBar.getBarTransitions().setAutoDim(false);
+ }
+ mHandler.removeCallbacks(mAutoDim);
+ if (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED) {
+ mHandler.postDelayed(mAutoDim, AUTOHIDE_TIMEOUT_MS);
+ }
+ }
+
+ void checkUserAutohide(View v, MotionEvent event) {
+ if ((mSystemUiVisibility & STATUS_OR_NAV_TRANSIENT) != 0 // a transient bar is revealed
+ && event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
+ && event.getX() == 0 && event.getY() == 0 // a touch outside both bars
+ && !mRemoteInputController.isRemoteInputActive()) { // not due to typing in IME
+ userAutohide();
+ }
+ }
+
+ private void checkRemoteInputOutside(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
+ && event.getX() == 0 && event.getY() == 0 // a touch outside both bars
+ && mRemoteInputController.isRemoteInputActive()) {
+ mRemoteInputController.closeRemoteInputs();
+ }
+ }
+
+ private void userAutohide() {
+ cancelAutohide();
+ mHandler.postDelayed(mAutohide, 350); // longer than app gesture -> flag clear
+ }
+
+ private boolean areLightsOn() {
+ return 0 == (mSystemUiVisibility & View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ }
+
+ public void setLightsOn(boolean on) {
+ Log.v(TAG, "setLightsOn(" + on + ")");
+ if (on) {
+ setSystemUiVisibility(0, 0, 0, View.SYSTEM_UI_FLAG_LOW_PROFILE,
+ mLastFullscreenStackBounds, mLastDockedStackBounds);
+ } else {
+ setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE, 0, 0,
+ View.SYSTEM_UI_FLAG_LOW_PROFILE, mLastFullscreenStackBounds,
+ mLastDockedStackBounds);
+ }
+ }
+
+ private void notifyUiVisibilityChanged(int vis) {
+ try {
+ if (mLastDispatchedSystemUiVisibility != vis) {
+ mWindowManagerService.statusBarVisibilityChanged(vis);
+ mLastDispatchedSystemUiVisibility = vis;
+ }
+ } catch (RemoteException ex) {
+ }
+ }
+
+ @Override
+ public void topAppWindowChanged(boolean showMenu) {
+ if (SPEW) {
+ Log.d(TAG, (showMenu?"showing":"hiding") + " the MENU button");
+ }
+
+ // See above re: lights-out policy for legacy apps.
+ if (showMenu) setLightsOn(true);
+ }
+
+ public static String viewInfo(View v) {
+ return "[(" + v.getLeft() + "," + v.getTop() + ")(" + v.getRight() + "," + v.getBottom()
+ + ") " + v.getWidth() + "x" + v.getHeight() + "]";
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ synchronized (mQueueLock) {
+ pw.println("Current Status Bar state:");
+ pw.println(" mExpandedVisible=" + mExpandedVisible
+ + ", mTrackingPosition=" + mTrackingPosition);
+ pw.println(" mTracking=" + mTracking);
+ pw.println(" mDisplayMetrics=" + mDisplayMetrics);
+ pw.println(" mStackScroller: " + viewInfo(mStackScroller));
+ pw.println(" mStackScroller: " + viewInfo(mStackScroller)
+ + " scroll " + mStackScroller.getScrollX()
+ + "," + mStackScroller.getScrollY());
+ }
+ pw.print(" mPendingNotifications=");
+ if (mPendingNotifications.size() == 0) {
+ pw.println("null");
+ } else {
+ for (Entry entry : mPendingNotifications.values()) {
+ pw.println(entry.notification);
+ }
+ }
+
+ pw.print(" mInteractingWindows="); pw.println(mInteractingWindows);
+ pw.print(" mStatusBarWindowState=");
+ pw.println(windowStateToString(mStatusBarWindowState));
+ pw.print(" mStatusBarMode=");
+ pw.println(BarTransitions.modeToString(mStatusBarMode));
+ pw.print(" mDozing="); pw.println(mDozing);
+ pw.print(" mZenMode=");
+ pw.println(Settings.Global.zenModeToString(mZenMode));
+ pw.print(" mUseHeadsUp=");
+ pw.println(mUseHeadsUp);
+ pw.print(" mKeyToRemoveOnGutsClosed=");
+ pw.println(mKeyToRemoveOnGutsClosed);
+ if (mStatusBarView != null) {
+ dumpBarTransitions(pw, "mStatusBarView", mStatusBarView.getBarTransitions());
+ }
+
+ pw.print(" mMediaSessionManager=");
+ pw.println(mMediaSessionManager);
+ pw.print(" mMediaNotificationKey=");
+ pw.println(mMediaNotificationKey);
+ pw.print(" mMediaController=");
+ pw.print(mMediaController);
+ if (mMediaController != null) {
+ pw.print(" state=" + mMediaController.getPlaybackState());
+ }
+ pw.println();
+ pw.print(" mMediaMetadata=");
+ pw.print(mMediaMetadata);
+ if (mMediaMetadata != null) {
+ pw.print(" title=" + mMediaMetadata.getText(MediaMetadata.METADATA_KEY_TITLE));
+ }
+ pw.println();
+
+ pw.println(" Panels: ");
+ if (mNotificationPanel != null) {
+ pw.println(" mNotificationPanel=" +
+ mNotificationPanel + " params=" + mNotificationPanel.getLayoutParams().debug(""));
+ pw.print (" ");
+ mNotificationPanel.dump(fd, pw, args);
+ }
+ pw.println(" mStackScroller: ");
+ if (mStackScroller != null) {
+ pw.print (" ");
+ mStackScroller.dump(fd, pw, args);
+ }
+ pw.println(" Theme:");
+ if (mOverlayManager == null) {
+ pw.println(" overlay manager not initialized!");
+ } else {
+ pw.println(" dark overlay on: " + isUsingDarkTheme());
+ }
+ final boolean lightWpTheme = mContext.getThemeResId() == R.style.Theme_SystemUI_Light;
+ pw.println(" light wallpaper theme: " + lightWpTheme);
+
+ DozeLog.dump(pw);
+
+ if (mFingerprintUnlockController != null) {
+ mFingerprintUnlockController.dump(pw);
+ }
+
+ if (mScrimController != null) {
+ mScrimController.dump(pw);
+ }
+
+ if (DUMPTRUCK) {
+ synchronized (mNotificationData) {
+ mNotificationData.dump(pw, " ");
+ }
+
+ if (false) {
+ pw.println("see the logcat for a dump of the views we have created.");
+ // must happen on ui thread
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mStatusBarView.getLocationOnScreen(mAbsPos);
+ Log.d(TAG, "mStatusBarView: ----- (" + mAbsPos[0] + "," + mAbsPos[1]
+ + ") " + mStatusBarView.getWidth() + "x"
+ + getStatusBarHeight());
+ mStatusBarView.debug();
+ }
+ });
+ }
+ }
+
+ if (DEBUG_GESTURES) {
+ pw.print(" status bar gestures: ");
+ mGestureRec.dump(fd, pw, args);
+ }
+
+ if (mHeadsUpManager != null) {
+ mHeadsUpManager.dump(fd, pw, args);
+ } else {
+ pw.println(" mHeadsUpManager: null");
+ }
+ if (mGroupManager != null) {
+ mGroupManager.dump(fd, pw, args);
+ } else {
+ pw.println(" mGroupManager: null");
+ }
+
+ if (mLightBarController != null) {
+ mLightBarController.dump(fd, pw, args);
+ }
+
+ if (KeyguardUpdateMonitor.getInstance(mContext) != null) {
+ KeyguardUpdateMonitor.getInstance(mContext).dump(fd, pw, args);
+ }
+
+ FalsingManager.getInstance(mContext).dump(pw);
+ FalsingLog.dump(pw);
+
+ pw.println("SharedPreferences:");
+ for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) {
+ pw.print(" "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
+ }
+ }
+
+ static void dumpBarTransitions(PrintWriter pw, String var, BarTransitions transitions) {
+ pw.print(" "); pw.print(var); pw.print(".BarTransitions.mMode=");
+ pw.println(BarTransitions.modeToString(transitions.getMode()));
+ }
+
+ public void createAndAddWindows() {
+ addStatusBarWindow();
+ }
+
+ private void addStatusBarWindow() {
+ makeStatusBarView();
+ mStatusBarWindowManager = Dependency.get(StatusBarWindowManager.class);
+ mRemoteInputController = new RemoteInputController(mHeadsUpManager);
+ mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight());
+ }
+
+ // called by makeStatusbar and also by PhoneStatusBarView
+ void updateDisplaySize() {
+ mDisplay.getMetrics(mDisplayMetrics);
+ mDisplay.getSize(mCurrentDisplaySize);
+ if (DEBUG_GESTURES) {
+ mGestureRec.tag("display",
+ String.format("%dx%d", mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
+ }
+ }
+
+ float getDisplayDensity() {
+ return mDisplayMetrics.density;
+ }
+
+ public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
+ boolean dismissShade) {
+ startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade,
+ false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */);
+ }
+
+ public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
+ final boolean dismissShade, final boolean disallowEnterPictureInPictureWhileLaunching,
+ final Callback callback) {
+ if (onlyProvisioned && !isDeviceProvisioned()) return;
+
+ final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
+ mContext, intent, mCurrentUserId);
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ mAssistManager.hideAssist();
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ int result = ActivityManager.START_CANCELED;
+ ActivityOptions options = new ActivityOptions(getActivityOptions());
+ options.setDisallowEnterPictureInPictureWhileLaunching(
+ disallowEnterPictureInPictureWhileLaunching);
+ if (intent == KeyguardBottomAreaView.INSECURE_CAMERA_INTENT) {
+ // Normally an activity will set it's requested rotation
+ // animation on its window. However when launching an activity
+ // causes the orientation to change this is too late. In these cases
+ // the default animation is used. This doesn't look good for
+ // the camera (as it rotates the camera contents out of sync
+ // with physical reality). So, we ask the WindowManager to
+ // force the crossfade animation if an orientation change
+ // happens to occur during the launch.
+ options.setRotationAnimationHint(
+ WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS);
+ }
+ try {
+ result = ActivityManager.getService().startActivityAsUser(
+ null, mContext.getBasePackageName(),
+ intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null,
+ options.toBundle(), UserHandle.CURRENT.getIdentifier());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to start activity", e);
+ }
+ if (callback != null) {
+ callback.onActivityStarted(result);
+ }
+ }
+ };
+ Runnable cancelRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (callback != null) {
+ callback.onActivityStarted(ActivityManager.START_CANCELED);
+ }
+ }
+ };
+ executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShade,
+ afterKeyguardGone, true /* deferred */);
+ }
+
+ public void readyForKeyguardDone() {
+ mStatusBarKeyguardViewManager.readyForKeyguardDone();
+ }
+
+ public void executeRunnableDismissingKeyguard(final Runnable runnable,
+ final Runnable cancelAction,
+ final boolean dismissShade,
+ final boolean afterKeyguardGone,
+ final boolean deferred) {
+ dismissKeyguardThenExecute(() -> {
+ if (runnable != null) {
+ if (mStatusBarKeyguardViewManager.isShowing()
+ && mStatusBarKeyguardViewManager.isOccluded()) {
+ mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
+ } else {
+ AsyncTask.execute(runnable);
+ }
+ }
+ if (dismissShade) {
+ if (mExpandedVisible) {
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */,
+ true /* delayed*/);
+ } else {
+
+ // Do it after DismissAction has been processed to conserve the needed ordering.
+ mHandler.post(this::runPostCollapseRunnables);
+ }
+ } else if (isInLaunchTransition() && mNotificationPanel.isLaunchTransitionFinished()) {
+
+ // We are not dismissing the shade, but the launch transition is already finished,
+ // so nobody will call readyForKeyguardDone anymore. Post it such that
+ // keyguardDonePending gets called first.
+ mHandler.post(mStatusBarKeyguardViewManager::readyForKeyguardDone);
+ }
+ return deferred;
+ }, cancelAction, afterKeyguardGone);
+ }
+
+ private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) Log.v(TAG, "onReceive: " + intent);
+ String action = intent.getAction();
+ if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
+ KeyboardShortcuts.dismiss();
+ if (mRemoteInputController != null) {
+ mRemoteInputController.closeRemoteInputs();
+ }
+ 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);
+ }
+ }
+ else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+ finishBarAnimations();
+ resetUserExpandedStates();
+ }
+ else if (DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG.equals(action)) {
+ mQSPanel.showDeviceMonitoringDialog();
+ }
+ }
+ };
+
+ private BroadcastReceiver mDemoReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) Log.v(TAG, "onReceive: " + intent);
+ String action = intent.getAction();
+ if (ACTION_DEMO.equals(action)) {
+ Bundle bundle = intent.getExtras();
+ if (bundle != null) {
+ String command = bundle.getString("command", "").trim().toLowerCase();
+ if (command.length() > 0) {
+ try {
+ dispatchDemoCommand(command, bundle);
+ } catch (Throwable t) {
+ Log.w(TAG, "Error running demo command, intent=" + intent, t);
+ }
+ }
+ }
+ } else if (ACTION_FAKE_ARTWORK.equals(action)) {
+ if (DEBUG_MEDIA_FAKE_ARTWORK) {
+ updateMediaMetaData(true, true);
+ }
+ }
+ }
+ };
+
+ public void resetUserExpandedStates() {
+ ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+ final int notificationCount = activeNotifications.size();
+ for (int i = 0; i < notificationCount; i++) {
+ NotificationData.Entry entry = activeNotifications.get(i);
+ if (entry.row != null) {
+ entry.row.resetUserExpansion();
+ }
+ }
+ }
+
+ protected void dismissKeyguardThenExecute(OnDismissAction action, boolean afterKeyguardGone) {
+ dismissKeyguardThenExecute(action, null /* cancelRunnable */, afterKeyguardGone);
+ }
+
+ private void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancelAction,
+ boolean afterKeyguardGone) {
+ if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP
+ && mUnlockMethodCache.canSkipBouncer()
+ && !mLeaveOpenOnKeyguardHide
+ && isPulsing()) {
+ // Reuse the fingerprint wake-and-unlock transition if we dismiss keyguard from a pulse.
+ // TODO: Factor this transition out of FingerprintUnlockController.
+ mFingerprintUnlockController.startWakeAndUnlock(
+ FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING);
+ }
+ if (mStatusBarKeyguardViewManager.isShowing()) {
+ mStatusBarKeyguardViewManager.dismissWithAction(action, cancelAction,
+ afterKeyguardGone);
+ } else {
+ action.onDismiss();
+ }
+ }
+
+ // SystemUIService notifies SystemBars of configuration changes, which then calls down here
+ @Override
+ public void onConfigChanged(Configuration newConfig) {
+ updateResources();
+ updateDisplaySize(); // populates mDisplayMetrics
+
+ if (DEBUG) {
+ Log.v(TAG, "configuration changed: " + mContext.getResources().getConfiguration());
+ }
+
+ updateRowStates();
+ mScreenPinningRequest.onConfigurationChanged();
+ }
+
+ public void userSwitched(int newUserId) {
+ // Begin old BaseStatusBar.userSwitched
+ setHeadsUpUser(newUserId);
+ // End old BaseStatusBar.userSwitched
+ if (MULTIUSER_DEBUG) mNotificationPanelDebugText.setText("USER " + newUserId);
+ animateCollapsePanels();
+ updatePublicMode();
+ mNotificationData.filterAndSort();
+ if (mReinflateNotificationsOnUserSwitched) {
+ updateNotificationsOnDensityOrFontScaleChanged();
+ mReinflateNotificationsOnUserSwitched = false;
+ }
+ updateNotificationShade();
+ clearCurrentMediaNotification();
+ setLockscreenUser(newUserId);
+ }
+
+ protected void setLockscreenUser(int newUserId) {
+ mLockscreenWallpaper.setCurrentUser(newUserId);
+ mScrimController.setCurrentUser(newUserId);
+ updateMediaMetaData(true, false);
+ }
+
+ /**
+ * Reload some of our resources when the configuration changes.
+ *
+ * We don't reload everything when the configuration changes -- we probably
+ * should, but getting that smooth is tough. Someday we'll fix that. In the
+ * meantime, just update the things that we know change.
+ */
+ void updateResources() {
+ // Update the quick setting tiles
+ if (mQSPanel != null) {
+ mQSPanel.updateResources();
+ }
+
+ loadDimens();
+
+ if (mNotificationPanel != null) {
+ mNotificationPanel.updateResources();
+ }
+ if (mBrightnessMirrorController != null) {
+ mBrightnessMirrorController.updateResources();
+ }
+ }
+
+ protected void loadDimens() {
+ final Resources res = mContext.getResources();
+
+ int oldBarHeight = mNaturalBarHeight;
+ mNaturalBarHeight = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_height);
+ if (mStatusBarWindowManager != null && mNaturalBarHeight != oldBarHeight) {
+ mStatusBarWindowManager.setBarHeight(mNaturalBarHeight);
+ }
+ mMaxAllowedKeyguardNotifications = res.getInteger(
+ R.integer.keyguard_max_notification_count);
+
+ if (DEBUG) Log.v(TAG, "defineSlots");
+ }
+
+ // Visibility reporting
+
+ protected void handleVisibleToUserChanged(boolean visibleToUser) {
+ if (visibleToUser) {
+ handleVisibleToUserChangedImpl(visibleToUser);
+ startNotificationLogging();
+ } else {
+ stopNotificationLogging();
+ handleVisibleToUserChangedImpl(visibleToUser);
+ }
+ }
+
+ void handlePeekToExpandTransistion() {
+ try {
+ // consider the transition from peek to expanded to be a panel open,
+ // but not one that clears notification effects.
+ int notificationLoad = mNotificationData.getActiveNotifications().size();
+ mBarService.onPanelRevealed(false, notificationLoad);
+ } catch (RemoteException ex) {
+ // Won't fail unless the world has ended.
+ }
+ }
+
+ /**
+ * The LEDs are turned off when the notification panel is shown, even just a little bit.
+ * See also StatusBar.setPanelExpanded for another place where we attempt to do this.
+ */
+ // Old BaseStatusBar.handleVisibileToUserChanged
+ private void handleVisibleToUserChangedImpl(boolean visibleToUser) {
+ try {
+ if (visibleToUser) {
+ boolean pinnedHeadsUp = mHeadsUpManager.hasPinnedHeadsUp();
+ boolean clearNotificationEffects =
+ !isPanelFullyCollapsed() &&
+ (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED);
+ int notificationLoad = mNotificationData.getActiveNotifications().size();
+ if (pinnedHeadsUp && isPanelFullyCollapsed()) {
+ notificationLoad = 1;
+ }
+ mBarService.onPanelRevealed(clearNotificationEffects, notificationLoad);
+ } else {
+ mBarService.onPanelHidden();
+ }
+ } catch (RemoteException ex) {
+ // Won't fail unless the world has ended.
+ }
+ }
+
+ private void stopNotificationLogging() {
+ // Report all notifications as invisible and turn down the
+ // reporter.
+ if (!mCurrentlyVisibleNotifications.isEmpty()) {
+ logNotificationVisibilityChanges(Collections.<NotificationVisibility>emptyList(),
+ mCurrentlyVisibleNotifications);
+ recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
+ }
+ mHandler.removeCallbacks(mVisibilityReporter);
+ mStackScroller.setChildLocationsChangedListener(null);
+ }
+
+ 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(
+ Collection<NotificationVisibility> newlyVisible,
+ Collection<NotificationVisibility> noLongerVisible) {
+ if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
+ return;
+ }
+ NotificationVisibility[] newlyVisibleAr =
+ newlyVisible.toArray(new NotificationVisibility[newlyVisible.size()]);
+ NotificationVisibility[] noLongerVisibleAr =
+ noLongerVisible.toArray(new NotificationVisibility[noLongerVisible.size()]);
+ try {
+ mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr);
+ } catch (RemoteException e) {
+ // Ignore.
+ }
+
+ final int N = newlyVisible.size();
+ if (N > 0) {
+ String[] newlyVisibleKeyAr = new String[N];
+ for (int i = 0; i < N; i++) {
+ newlyVisibleKeyAr[i] = newlyVisibleAr[i].key;
+ }
+
+ setNotificationsShown(newlyVisibleKeyAr);
+ }
+ }
+
+ // State logging
+
+ private void logStateToEventlog() {
+ boolean isShowing = mStatusBarKeyguardViewManager.isShowing();
+ boolean isOccluded = mStatusBarKeyguardViewManager.isOccluded();
+ boolean isBouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing();
+ boolean isSecure = mUnlockMethodCache.isMethodSecure();
+ boolean canSkipBouncer = mUnlockMethodCache.canSkipBouncer();
+ int stateFingerprint = getLoggingFingerprint(mState,
+ isShowing,
+ isOccluded,
+ isBouncerShowing,
+ isSecure,
+ canSkipBouncer);
+ if (stateFingerprint != mLastLoggedStateFingerprint) {
+ if (mStatusBarStateLog == null) {
+ mStatusBarStateLog = new LogMaker(MetricsEvent.VIEW_UNKNOWN);
+ }
+ mMetricsLogger.write(mStatusBarStateLog
+ .setCategory(isBouncerShowing ? MetricsEvent.BOUNCER : MetricsEvent.LOCKSCREEN)
+ .setType(isShowing ? MetricsEvent.TYPE_OPEN : MetricsEvent.TYPE_CLOSE)
+ .setSubtype(isSecure ? 1 : 0));
+ EventLogTags.writeSysuiStatusBarState(mState,
+ isShowing ? 1 : 0,
+ isOccluded ? 1 : 0,
+ isBouncerShowing ? 1 : 0,
+ isSecure ? 1 : 0,
+ canSkipBouncer ? 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
+ //
+
+ void postStartTracing() {
+ mHandler.postDelayed(mStartTracing, 3000);
+ }
+
+ void vibrate() {
+ android.os.Vibrator vib = (android.os.Vibrator)mContext.getSystemService(
+ Context.VIBRATOR_SERVICE);
+ vib.vibrate(250, VIBRATION_ATTRIBUTES);
+ }
+
+ Runnable mStartTracing = new Runnable() {
+ @Override
+ public void run() {
+ vibrate();
+ SystemClock.sleep(250);
+ Log.d(TAG, "startTracing");
+ android.os.Debug.startMethodTracing("/data/statusbar-traces/trace");
+ mHandler.postDelayed(mStopTracing, 10000);
+ }
+ };
+
+ Runnable mStopTracing = new Runnable() {
+ @Override
+ public void run() {
+ android.os.Debug.stopMethodTracing();
+ Log.d(TAG, "stopTracing");
+ vibrate();
+ }
+ };
+
+ @Override
+ public void postQSRunnableDismissingKeyguard(final Runnable runnable) {
+ mHandler.post(() -> {
+ mLeaveOpenOnKeyguardHide = true;
+ executeRunnableDismissingKeyguard(() -> mHandler.post(runnable), null, false, false,
+ false);
+ });
+ }
+
+ @Override
+ public void postStartActivityDismissingKeyguard(final PendingIntent intent) {
+ mHandler.post(() -> startPendingIntentDismissingKeyguard(intent));
+ }
+
+ @Override
+ public void postStartActivityDismissingKeyguard(final Intent intent, int delay) {
+ mHandler.postDelayed(() ->
+ handleStartActivityDismissingKeyguard(intent, true /*onlyProvisioned*/), delay);
+ }
+
+ private void handleStartActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned) {
+ startActivityDismissingKeyguard(intent, onlyProvisioned, true /* dismissShade */);
+ }
+
+ private static class FastColorDrawable extends Drawable {
+ private final int mColor;
+
+ public FastColorDrawable(int color) {
+ mColor = 0xff000000 | color;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.drawColor(mColor, PorterDuff.Mode.SRC);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+
+ @Override
+ public void setBounds(int left, int top, int right, int bottom) {
+ }
+
+ @Override
+ public void setBounds(Rect bounds) {
+ }
+ }
+
+ public void destroy() {
+ // Begin old BaseStatusBar.destroy().
+ mContext.unregisterReceiver(mBaseBroadcastReceiver);
+ try {
+ mNotificationListener.unregisterAsSystemService();
+ } catch (RemoteException e) {
+ // Ignore.
+ }
+ mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener);
+ // End old BaseStatusBar.destroy().
+ if (mStatusBarWindow != null) {
+ mWindowManager.removeViewImmediate(mStatusBarWindow);
+ mStatusBarWindow = null;
+ }
+ if (mNavigationBarView != null) {
+ mWindowManager.removeViewImmediate(mNavigationBarView);
+ mNavigationBarView = null;
+ }
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ mContext.unregisterReceiver(mDemoReceiver);
+ mAssistManager.destroy();
+
+ if (mQSPanel != null && mQSPanel.getHost() != null) {
+ mQSPanel.getHost().destroy();
+ }
+ Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(null);
+ mDeviceProvisionedController.removeCallback(mUserSetupObserver);
+ Dependency.get(ConfigurationController.class).removeCallback(this);
+ }
+
+ private boolean mDemoModeAllowed;
+ private boolean mDemoMode;
+
+ @Override
+ public void dispatchDemoCommand(String command, Bundle args) {
+ if (!mDemoModeAllowed) {
+ mDemoModeAllowed = Settings.Global.getInt(mContext.getContentResolver(),
+ DEMO_MODE_ALLOWED, 0) != 0;
+ }
+ if (!mDemoModeAllowed) return;
+ if (command.equals(COMMAND_ENTER)) {
+ mDemoMode = true;
+ } else if (command.equals(COMMAND_EXIT)) {
+ mDemoMode = false;
+ checkBarModes();
+ } else if (!mDemoMode) {
+ // automatically enter demo mode on first demo command
+ 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);
+ }
+ if (modeChange || command.equals(COMMAND_BATTERY)) {
+ mBatteryController.dispatchDemoCommand(command, args);
+ }
+ if (modeChange || command.equals(COMMAND_STATUS)) {
+ ((StatusBarIconControllerImpl) mIconController).dispatchDemoCommand(command, args);
+ }
+ if (mNetworkController != null && (modeChange || command.equals(COMMAND_NETWORK))) {
+ mNetworkController.dispatchDemoCommand(command, args);
+ }
+ if (modeChange || command.equals(COMMAND_NOTIFICATIONS)) {
+ View notifications = mStatusBarView == null ? null
+ : mStatusBarView.findViewById(R.id.notification_icon_area);
+ if (notifications != null) {
+ String visible = args.getString("visible");
+ int vis = mDemoMode && "false".equals(visible) ? View.INVISIBLE : View.VISIBLE;
+ notifications.setVisibility(vis);
+ }
+ }
+ if (command.equals(COMMAND_BARS)) {
+ String mode = args.getString("mode");
+ int barMode = "opaque".equals(mode) ? MODE_OPAQUE :
+ "translucent".equals(mode) ? MODE_TRANSLUCENT :
+ "semi-transparent".equals(mode) ? MODE_SEMI_TRANSPARENT :
+ "transparent".equals(mode) ? MODE_TRANSPARENT :
+ "warning".equals(mode) ? MODE_WARNING :
+ -1;
+ if (barMode != -1) {
+ boolean animate = true;
+ if (mStatusBarView != null) {
+ mStatusBarView.getBarTransitions().transitionTo(barMode, animate);
+ }
+ if (mNavigationBar != null) {
+ mNavigationBar.getBarTransitions().transitionTo(barMode, animate);
+ }
+ }
+ }
+ }
+
+ private void dispatchDemoCommandToView(String command, Bundle args, int id) {
+ if (mStatusBarView == null) return;
+ View v = mStatusBarView.findViewById(id);
+ if (v instanceof DemoMode) {
+ ((DemoMode)v).dispatchDemoCommand(command, args);
+ }
+ }
+
+ /**
+ * @return The {@link StatusBarState} the status bar is in.
+ */
+ public int getBarState() {
+ return mState;
+ }
+
+ public boolean isPanelFullyCollapsed() {
+ return mNotificationPanel.isFullyCollapsed();
+ }
+
+ public void showKeyguard() {
+ mKeyguardRequested = true;
+ mLeaveOpenOnKeyguardHide = false;
+ mPendingRemoteInputView = null;
+ updateIsKeyguard();
+ mAssistManager.onLockscreenShown();
+ }
+
+ public boolean hideKeyguard() {
+ mKeyguardRequested = false;
+ return updateIsKeyguard();
+ }
+
+ private boolean updateIsKeyguard() {
+ boolean wakeAndUnlocking = mFingerprintUnlockController.getMode()
+ == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK;
+
+ // For dozing, keyguard needs to be shown whenever the device is non-interactive. Otherwise
+ // there's no surface we can show to the user. Note that the device goes fully interactive
+ // late in the transition, so we also allow the device to start dozing once the screen has
+ // turned off fully.
+ boolean keyguardForDozing = mDozingRequested &&
+ (!mDeviceInteractive || isGoingToSleep() && (isScreenFullyOff() || mIsKeyguard));
+ boolean shouldBeKeyguard = (mKeyguardRequested || keyguardForDozing) && !wakeAndUnlocking;
+ if (keyguardForDozing) {
+ updatePanelExpansionForKeyguard();
+ }
+ if (shouldBeKeyguard) {
+ if (isGoingToSleep()
+ && mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_TURNING_OFF) {
+ // Delay showing the keyguard until screen turned off.
+ } else {
+ showKeyguardImpl();
+ }
+ } else {
+ return hideKeyguardImpl();
+ }
+ return false;
+ }
+
+ public void showKeyguardImpl() {
+ mIsKeyguard = true;
+ if (mLaunchTransitionFadingAway) {
+ mNotificationPanel.animate().cancel();
+ onLaunchTransitionFadingEnded();
+ }
+ mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
+ if (mUserSwitcherController != null && mUserSwitcherController.useFullscreenUserSwitcher()) {
+ setBarState(StatusBarState.FULLSCREEN_USER_SWITCHER);
+ } else {
+ setBarState(StatusBarState.KEYGUARD);
+ }
+ updateKeyguardState(false /* goingToFullShade */, false /* fromShadeLocked */);
+ updatePanelExpansionForKeyguard();
+ if (mDraggedDownRow != null) {
+ mDraggedDownRow.setUserLocked(false);
+ mDraggedDownRow.notifyHeightChanged(false /* needsAnimation */);
+ mDraggedDownRow = null;
+ }
+ }
+
+ private void updatePanelExpansionForKeyguard() {
+ if (mState == StatusBarState.KEYGUARD && mFingerprintUnlockController.getMode()
+ != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK) {
+ instantExpandNotificationsPanel();
+ } else if (mState == StatusBarState.FULLSCREEN_USER_SWITCHER) {
+ instantCollapseNotificationPanel();
+ }
+ }
+
+ private void onLaunchTransitionFadingEnded() {
+ mNotificationPanel.setAlpha(1.0f);
+ mNotificationPanel.onAffordanceLaunchEnded();
+ releaseGestureWakeLock();
+ runLaunchTransitionEndRunnable();
+ mLaunchTransitionFadingAway = false;
+ mScrimController.forceHideScrims(false /* hide */, false /* animated */);
+ updateMediaMetaData(true /* metaDataChanged */, true);
+ }
+
+ public boolean isCollapsing() {
+ return mNotificationPanel.isCollapsing();
+ }
+
+ public void addPostCollapseAction(Runnable r) {
+ mPostCollapseRunnables.add(r);
+ }
+
+ public boolean isInLaunchTransition() {
+ return mNotificationPanel.isLaunchTransitionRunning()
+ || mNotificationPanel.isLaunchTransitionFinished();
+ }
+
+ /**
+ * Fades the content of the keyguard away after the launch transition is done.
+ *
+ * @param beforeFading the runnable to be run when the circle is fully expanded and the fading
+ * starts
+ * @param endRunnable the runnable to be run when the transition is done
+ */
+ public void fadeKeyguardAfterLaunchTransition(final Runnable beforeFading,
+ Runnable endRunnable) {
+ mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
+ mLaunchTransitionEndRunnable = endRunnable;
+ Runnable hideRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mLaunchTransitionFadingAway = true;
+ if (beforeFading != null) {
+ beforeFading.run();
+ }
+ mScrimController.forceHideScrims(true /* hide */, false /* animated */);
+ updateMediaMetaData(false, true);
+ mNotificationPanel.setAlpha(1);
+ mStackScroller.setParentNotFullyVisible(true);
+ mNotificationPanel.animate()
+ .alpha(0)
+ .setStartDelay(FADE_KEYGUARD_START_DELAY)
+ .setDuration(FADE_KEYGUARD_DURATION)
+ .withLayer()
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ onLaunchTransitionFadingEnded();
+ }
+ });
+ mCommandQueue.appTransitionStarting(SystemClock.uptimeMillis(),
+ LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION, true);
+ }
+ };
+ if (mNotificationPanel.isLaunchTransitionRunning()) {
+ mNotificationPanel.setLaunchTransitionEndRunnable(hideRunnable);
+ } else {
+ hideRunnable.run();
+ }
+ }
+
+ /**
+ * Fades the content of the Keyguard while we are dozing and makes it invisible when finished
+ * fading.
+ */
+ public void fadeKeyguardWhilePulsing() {
+ mNotificationPanel.notifyStartFading();
+ mNotificationPanel.animate()
+ .alpha(0f)
+ .setStartDelay(0)
+ .setDuration(FADE_KEYGUARD_DURATION_PULSING)
+ .setInterpolator(ScrimController.KEYGUARD_FADE_OUT_INTERPOLATOR)
+ .start();
+ }
+
+ /**
+ * Plays the animation when an activity that was occluding Keyguard goes away.
+ */
+ public void animateKeyguardUnoccluding() {
+ mScrimController.animateKeyguardUnoccluding(500);
+ mNotificationPanel.setExpandedFraction(0f);
+ animateExpandNotificationsPanel();
+ }
+
+ /**
+ * 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.onAffordanceLaunchEnded();
+ releaseGestureWakeLock();
+ 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 hideKeyguardImpl() {
+ mIsKeyguard = false;
+ Trace.beginSection("StatusBar#hideKeyguard");
+ boolean staying = mLeaveOpenOnKeyguardHide;
+ setBarState(StatusBarState.SHADE);
+ View viewToClick = null;
+ if (mLeaveOpenOnKeyguardHide) {
+ if (!mKeyguardRequested) {
+ mLeaveOpenOnKeyguardHide = false;
+ }
+ long delay = calculateGoingToFullShadeDelay();
+ mNotificationPanel.animateToFullShade(delay);
+ if (mDraggedDownRow != null) {
+ mDraggedDownRow.setUserLocked(false);
+ mDraggedDownRow = null;
+ }
+ if (!mKeyguardRequested) {
+ viewToClick = mPendingRemoteInputView;
+ mPendingRemoteInputView = null;
+ }
+
+ // Disable layout transitions in navbar for this transition because the load is just
+ // too heavy for the CPU and GPU on any device.
+ if (mNavigationBar != null) {
+ mNavigationBar.disableAnimationsDuringHide(delay);
+ }
+ } else if (!mNotificationPanel.isCollapsing()) {
+ instantCollapseNotificationPanel();
+ }
+ updateKeyguardState(staying, false /* fromShadeLocked */);
+
+ if (viewToClick != null && viewToClick.isAttachedToWindow()) {
+ viewToClick.callOnClick();
+ }
+
+ // 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);
+ releaseGestureWakeLock();
+ mNotificationPanel.onAffordanceLaunchEnded();
+ mNotificationPanel.animate().cancel();
+ mNotificationPanel.setAlpha(1f);
+ Trace.endSection();
+ return staying;
+ }
+
+ private void releaseGestureWakeLock() {
+ if (mGestureWakeLock.isHeld()) {
+ mGestureWakeLock.release();
+ }
+ }
+
+ public long calculateGoingToFullShadeDelay() {
+ return mKeyguardFadingAwayDelay + mKeyguardFadingAwayDuration;
+ }
+
+ /**
+ * Notifies the status bar that Keyguard is going away very soon.
+ */
+ public void keyguardGoingAway() {
+
+ // Treat Keyguard exit animation as an app transition to achieve nice transition for status
+ // bar.
+ mKeyguardGoingAway = true;
+ mKeyguardMonitor.notifyKeyguardGoingAway(true);
+ mCommandQueue.appTransitionPending(true);
+ }
+
+ /**
+ * Notifies the status bar the Keyguard is fading away with the specified timings.
+ *
+ * @param startTime the start time of the animations in uptime millis
+ * @param delay the precalculated animation delay in miliseconds
+ * @param fadeoutDuration the duration of the exit animation, in milliseconds
+ */
+ public void setKeyguardFadingAway(long startTime, long delay, long fadeoutDuration) {
+ mKeyguardFadingAway = true;
+ mKeyguardFadingAwayDelay = delay;
+ mKeyguardFadingAwayDuration = fadeoutDuration;
+ mWaitingForKeyguardExit = false;
+ mCommandQueue.appTransitionStarting(startTime + fadeoutDuration
+ - LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION,
+ LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION, true);
+ recomputeDisableFlags(fadeoutDuration > 0 /* animate */);
+ mCommandQueue.appTransitionStarting(
+ startTime - LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION,
+ LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION, true);
+ mKeyguardMonitor.notifyKeyguardFadingAway(delay, fadeoutDuration);
+ }
+
+ public boolean isKeyguardFadingAway() {
+ return mKeyguardFadingAway;
+ }
+
+ /**
+ * Notifies that the Keyguard fading away animation is done.
+ */
+ public void finishKeyguardFadingAway() {
+ mKeyguardFadingAway = false;
+ mKeyguardGoingAway = false;
+ mKeyguardMonitor.notifyKeyguardDoneFading();
+ }
+
+ public void stopWaitingForKeyguardExit() {
+ mWaitingForKeyguardExit = false;
+ }
+
+ private void updatePublicMode() {
+ final boolean showingKeyguard = mStatusBarKeyguardViewManager.isShowing();
+ final boolean devicePublic = showingKeyguard
+ && mStatusBarKeyguardViewManager.isSecure(mCurrentUserId);
+
+ // Look for public mode users. Users are considered public in either case of:
+ // - device keyguard is shown in secure mode;
+ // - profile is locked with a work challenge.
+ for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) {
+ final int userId = mCurrentProfiles.valueAt(i).id;
+ boolean isProfilePublic = devicePublic;
+ if (!devicePublic && userId != mCurrentUserId) {
+ // We can't rely on KeyguardManager#isDeviceLocked() for unified profile challenge
+ // due to a race condition where this code could be called before
+ // TrustManagerService updates its internal records, resulting in an incorrect
+ // state being cached in mLockscreenPublicMode. (b/35951989)
+ if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)
+ && mStatusBarKeyguardViewManager.isSecure(userId)) {
+ isProfilePublic = mKeyguardManager.isDeviceLocked(userId);
+ }
+ }
+ setLockscreenPublicMode(isProfilePublic, userId);
+ }
+ }
+
+ protected void updateKeyguardState(boolean goingToFullShade, boolean fromShadeLocked) {
+ Trace.beginSection("StatusBar#updateKeyguardState");
+ if (mState == StatusBarState.KEYGUARD) {
+ mKeyguardIndicationController.setVisible(true);
+ mNotificationPanel.resetViews();
+ if (mKeyguardUserSwitcher != null) {
+ mKeyguardUserSwitcher.setKeyguard(true, fromShadeLocked);
+ }
+ if (mStatusBarView != null) mStatusBarView.removePendingHideExpandedRunnables();
+ if (mAmbientIndicationContainer != null) {
+ mAmbientIndicationContainer.setVisibility(View.VISIBLE);
+ }
+ } else {
+ mKeyguardIndicationController.setVisible(false);
+ if (mKeyguardUserSwitcher != null) {
+ mKeyguardUserSwitcher.setKeyguard(false,
+ goingToFullShade ||
+ mState == StatusBarState.SHADE_LOCKED ||
+ fromShadeLocked);
+ }
+ if (mAmbientIndicationContainer != null) {
+ mAmbientIndicationContainer.setVisibility(View.INVISIBLE);
+ }
+ }
+ if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+ mScrimController.setKeyguardShowing(true);
+ } else {
+ mScrimController.setKeyguardShowing(false);
+ }
+ mNotificationPanel.setBarState(mState, mKeyguardFadingAway, goingToFullShade);
+ updateTheme();
+ updateDozingState();
+ updatePublicMode();
+ updateStackScrollerState(goingToFullShade, fromShadeLocked);
+ updateNotifications();
+ checkBarModes();
+ updateMediaMetaData(false, mState != StatusBarState.KEYGUARD);
+ mKeyguardMonitor.notifyKeyguardState(mStatusBarKeyguardViewManager.isShowing(),
+ mUnlockMethodCache.isMethodSecure(),
+ mStatusBarKeyguardViewManager.isOccluded());
+ Trace.endSection();
+ }
+
+ /**
+ * Switches theme from light to dark and vice-versa.
+ */
+ protected void updateTheme() {
+ final boolean inflated = mStackScroller != null;
+
+ // The system wallpaper defines if QS should be light or dark.
+ WallpaperColors systemColors = mColorExtractor
+ .getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
+ final boolean useDarkTheme = systemColors != null
+ && (systemColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0;
+ if (isUsingDarkTheme() != useDarkTheme) {
+ try {
+ mOverlayManager.setEnabled("com.android.systemui.theme.dark",
+ useDarkTheme, mCurrentUserId);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Can't change theme", e);
+ }
+ }
+
+ // Lock wallpaper defines the color of the majority of the views, hence we'll use it
+ // to set our default theme.
+ final boolean lockDarkText = mColorExtractor.getColors(WallpaperManager.FLAG_LOCK, true
+ /* ignoreVisibility */).supportsDarkText();
+ final int themeResId = lockDarkText ? R.style.Theme_SystemUI_Light : R.style.Theme_SystemUI;
+ if (mContext.getThemeResId() != themeResId) {
+ mContext.setTheme(themeResId);
+ if (inflated) {
+ reinflateViews();
+ }
+ }
+
+ if (inflated) {
+ int which;
+ if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+ which = WallpaperManager.FLAG_LOCK;
+ } else {
+ which = WallpaperManager.FLAG_SYSTEM;
+ }
+ final boolean useDarkText = mColorExtractor.getColors(which,
+ true /* ignoreVisibility */).supportsDarkText();
+ mStackScroller.updateDecorViews(useDarkText);
+
+ // Make sure we have the correct navbar/statusbar colors.
+ mStatusBarWindowManager.setKeyguardDark(useDarkText);
+ }
+ }
+
+ private void updateDozingState() {
+ Trace.traceCounter(Trace.TRACE_TAG_APP, "dozing", mDozing ? 1 : 0);
+ Trace.beginSection("StatusBar#updateDozingState");
+ boolean animate = !mDozing && mDozeServiceHost.shouldAnimateWakeup();
+ mNotificationPanel.setDozing(mDozing, animate);
+ mStackScroller.setDark(mDozing, animate, mWakeUpTouchLocation);
+ mScrimController.setDozing(mDozing);
+ mKeyguardIndicationController.setDozing(mDozing);
+ mNotificationPanel.setDark(mDozing, animate);
+ updateQsExpansionEnabled();
+ mDozeScrimController.setDozing(mDozing, animate);
+ updateRowStates();
+ Trace.endSection();
+ }
+
+ public void updateStackScrollerState(boolean goingToFullShade, boolean fromShadeLocked) {
+ if (mStackScroller == null) return;
+ boolean onKeyguard = mState == StatusBarState.KEYGUARD;
+ boolean publicMode = isAnyProfilePublicMode();
+ mStackScroller.setHideSensitive(publicMode, goingToFullShade);
+ mStackScroller.setDimmed(onKeyguard, fromShadeLocked /* animate */);
+ mStackScroller.setExpandingEnabled(!onKeyguard);
+ ActivatableNotificationView activatedChild = mStackScroller.getActivatedChild();
+ mStackScroller.setActivatedChild(null);
+ if (activatedChild != null) {
+ activatedChild.makeInactive(false /* animate */);
+ }
+ }
+
+ public void userActivity() {
+ if (mState == StatusBarState.KEYGUARD) {
+ mKeyguardViewMediatorCallback.userActivity();
+ }
+ }
+
+ public boolean interceptMediaKey(KeyEvent event) {
+ return mState == StatusBarState.KEYGUARD
+ && mStatusBarKeyguardViewManager.interceptMediaKey(event);
+ }
+
+ protected boolean shouldUnlockOnMenuPressed() {
+ return mDeviceInteractive && mState != StatusBarState.SHADE
+ && mStatusBarKeyguardViewManager.shouldDismissOnMenuPressed();
+ }
+
+ public boolean onMenuPressed() {
+ if (shouldUnlockOnMenuPressed()) {
+ animateCollapsePanels(
+ CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL /* flags */, true /* force */);
+ return true;
+ }
+ return false;
+ }
+
+ public void endAffordanceLaunch() {
+ releaseGestureWakeLock();
+ mNotificationPanel.onAffordanceLaunchEnded();
+ }
+
+ public boolean onBackPressed() {
+ if (mStatusBarKeyguardViewManager.onBackPressed()) {
+ return true;
+ }
+ if (mNotificationPanel.isQsExpanded()) {
+ if (mNotificationPanel.isQsDetailShowing()) {
+ mNotificationPanel.closeQsDetail();
+ } else {
+ mNotificationPanel.animateCloseQs();
+ }
+ return true;
+ }
+ if (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED) {
+ animateCollapsePanels();
+ return true;
+ }
+ if (mKeyguardUserSwitcher != null && mKeyguardUserSwitcher.hideIfNotSimple(true)) {
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onSpacePressed() {
+ if (mDeviceInteractive && mState != StatusBarState.SHADE) {
+ animateCollapsePanels(
+ CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL /* flags */, true /* force */);
+ return true;
+ }
+ return false;
+ }
+
+ private void showBouncerIfKeyguard() {
+ if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+ showBouncer();
+ }
+ }
+
+ protected void showBouncer() {
+ mWaitingForKeyguardExit = mStatusBarKeyguardViewManager.isShowing();
+ mStatusBarKeyguardViewManager.dismiss();
+ }
+
+ private void instantExpandNotificationsPanel() {
+ // Make our window larger and the panel expanded.
+ makeExpandedVisible(true);
+ mNotificationPanel.expand(false /* animate */);
+ recomputeDisableFlags(false /* animate */);
+ }
+
+ private void instantCollapseNotificationPanel() {
+ mNotificationPanel.instantCollapse();
+ }
+
+ @Override
+ public void onActivated(ActivatableNotificationView view) {
+ onActivated((View)view);
+ mStackScroller.setActivatedChild(view);
+ }
+
+ public void onActivated(View view) {
+ mLockscreenGestureLogger.write(
+ MetricsEvent.ACTION_LS_NOTE,
+ 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
+ mKeyguardIndicationController.showTransientIndication(R.string.notification_tap_again);
+ ActivatableNotificationView previousView = mStackScroller.getActivatedChild();
+ if (previousView != null) {
+ previousView.makeInactive(true /* animate */);
+ }
+ }
+
+ /**
+ * @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
+ || (state == StatusBarState.SHADE && isGoingToNotificationShade()))) {
+ clearNotificationEffects();
+ }
+ if (state == StatusBarState.KEYGUARD) {
+ removeRemoteInputEntriesKeptUntilCollapsed();
+ maybeEscalateHeadsUp();
+ }
+ mState = state;
+ mGroupManager.setStatusBarState(state);
+ mHeadsUpManager.setStatusBarState(state);
+ mFalsingManager.setStatusBarState(state);
+ mStatusBarWindowManager.setStatusBarState(state);
+ mStackScroller.setStatusBarState(state);
+ updateReportRejectedTouchVisibility();
+ updateDozing();
+ updateTheme();
+ touchAutoDim();
+ mNotificationShelf.setStatusBarState(state);
+ }
+
+ @Override
+ public void onActivationReset(ActivatableNotificationView view) {
+ if (view == mStackScroller.getActivatedChild()) {
+ mStackScroller.setActivatedChild(null);
+ onActivationReset((View)view);
+ }
+ }
+
+ public void onActivationReset(View view) {
+ mKeyguardIndicationController.hideTransientIndication();
+ }
+
+ public void onTrackingStarted() {
+ runPostCollapseRunnables();
+ }
+
+ public void onClosingFinished() {
+ runPostCollapseRunnables();
+ if (!isPanelFullyCollapsed()) {
+ // if we set it not to be focusable when collapsing, we have to undo it when we aborted
+ // the closing
+ mStatusBarWindowManager.setStatusBarFocusable(true);
+ }
+ }
+
+ public void onUnlockHintStarted() {
+ mFalsingManager.onUnlockHintStarted();
+ mKeyguardIndicationController.showTransientIndication(R.string.keyguard_unlock);
+ }
+
+ public void onHintFinished() {
+ // Delay the reset a bit so the user can read the text.
+ mKeyguardIndicationController.hideTransientIndicationDelayed(HINT_RESET_DELAY_MS);
+ }
+
+ public void onCameraHintStarted() {
+ mFalsingManager.onCameraHintStarted();
+ mKeyguardIndicationController.showTransientIndication(R.string.camera_hint);
+ }
+
+ public void onVoiceAssistHintStarted() {
+ mFalsingManager.onLeftAffordanceHintStarted();
+ mKeyguardIndicationController.showTransientIndication(R.string.voice_hint);
+ }
+
+ public void onPhoneHintStarted() {
+ mFalsingManager.onLeftAffordanceHintStarted();
+ mKeyguardIndicationController.showTransientIndication(R.string.phone_hint);
+ }
+
+ public void onTrackingStopped(boolean expand) {
+ if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+ if (!expand && !mUnlockMethodCache.canSkipBouncer()) {
+ showBouncerIfKeyguard();
+ }
+ }
+ }
+
+ protected int getMaxKeyguardNotifications(boolean recompute) {
+ if (recompute) {
+ mMaxKeyguardNotifications = Math.max(1,
+ mNotificationPanel.computeMaxKeyguardNotifications(
+ mMaxAllowedKeyguardNotifications));
+ return mMaxKeyguardNotifications;
+ }
+ return mMaxKeyguardNotifications;
+ }
+
+ public int getMaxKeyguardNotifications() {
+ return getMaxKeyguardNotifications(false /* recompute */);
+ }
+
+ // TODO: Figure out way to remove these.
+ public NavigationBarView getNavigationBarView() {
+ return (mNavigationBar != null ? (NavigationBarView) mNavigationBar.getView() : null);
+ }
+
+ public View getNavigationBarWindow() {
+ return mNavigationBarView;
+ }
+
+ /**
+ * TODO: Remove this method. Views should not be passed forward. Will cause theme issues.
+ * @return bottom area view
+ */
+ public KeyguardBottomAreaView getKeyguardBottomAreaView() {
+ return mNotificationPanel.getKeyguardBottomAreaView();
+ }
+
+ // ---------------------- DragDownHelper.OnDragDownListener ------------------------------------
+
+
+ /* Only ever called as a consequence of a lockscreen expansion gesture. */
+ @Override
+ public boolean onDraggedDown(View startingChild, int dragLengthY) {
+ if (mState == StatusBarState.KEYGUARD
+ && hasActiveNotifications() && (!isDozing() || isPulsing())) {
+ mLockscreenGestureLogger.write(
+ MetricsEvent.ACTION_LS_SHADE,
+ (int) (dragLengthY / mDisplayMetrics.density),
+ 0 /* velocityDp - N/A */);
+
+ // We have notifications, go to locked shade.
+ goToLockedShade(startingChild);
+ if (startingChild instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) startingChild;
+ row.onExpandedByGesture(true /* drag down is always an open */);
+ }
+ return true;
+ } else {
+ // abort gesture.
+ return false;
+ }
+ }
+
+ @Override
+ public void onDragDownReset() {
+ mStackScroller.setDimmed(true /* dimmed */, true /* animated */);
+ mStackScroller.resetScrollPosition();
+ mStackScroller.resetCheckSnoozeLeavebehind();
+ }
+
+ @Override
+ public void onCrossedThreshold(boolean above) {
+ mStackScroller.setDimmed(!above /* dimmed */, true /* animate */);
+ }
+
+ @Override
+ public void onTouchSlopExceeded() {
+ mStackScroller.removeLongPressCallback();
+ mStackScroller.checkSnoozeLeavebehind();
+ }
+
+ @Override
+ public void setEmptyDragAmount(float amount) {
+ mNotificationPanel.setEmptyDragAmount(amount);
+ }
+
+ @Override
+ public boolean isFalsingCheckNeeded() {
+ return mState == StatusBarState.KEYGUARD;
+ }
+
+ /**
+ * If secure with redaction: Show bouncer, go to unlocked shade.
+ *
+ * <p>If secure without redaction or no security: Go to {@link StatusBarState#SHADE_LOCKED}.</p>
+ *
+ * @param expandView The view to expand after going to the shade.
+ */
+ public void goToLockedShade(View expandView) {
+ int userId = mCurrentUserId;
+ ExpandableNotificationRow row = null;
+ if (expandView instanceof ExpandableNotificationRow) {
+ row = (ExpandableNotificationRow) expandView;
+ row.setUserExpanded(true /* userExpanded */, true /* allowChildExpansion */);
+ // Indicate that the group expansion is changing at this time -- this way the group
+ // and children backgrounds / divider animations will look correct.
+ row.setGroupExpansionChanging(true);
+ if (row.getStatusBarNotification() != null) {
+ userId = row.getStatusBarNotification().getUserId();
+ }
+ }
+ boolean fullShadeNeedsBouncer = !userAllowsPrivateNotificationsInPublic(mCurrentUserId)
+ || !mShowLockscreenNotifications || mFalsingManager.shouldEnforceBouncer();
+ if (isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) {
+ mLeaveOpenOnKeyguardHide = true;
+ showBouncerIfKeyguard();
+ mDraggedDownRow = row;
+ mPendingRemoteInputView = null;
+ } else {
+ mNotificationPanel.animateToFullShade(0 /* delay */);
+ setBarState(StatusBarState.SHADE_LOCKED);
+ updateKeyguardState(false /* goingToFullShade */, false /* fromShadeLocked */);
+ }
+ }
+
+ public void onLockedNotificationImportanceChange(OnDismissAction dismissAction) {
+ mLeaveOpenOnKeyguardHide = true;
+ dismissKeyguardThenExecute(dismissAction, true /* afterKeyguardGone */);
+ }
+
+ protected void onLockedRemoteInput(ExpandableNotificationRow row, View clicked) {
+ mLeaveOpenOnKeyguardHide = true;
+ showBouncer();
+ mPendingRemoteInputView = clicked;
+ }
+
+ protected void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row,
+ View clickedView) {
+ if (isKeyguardShowing()) {
+ onLockedRemoteInput(row, clickedView);
+ } else {
+ row.setUserExpanded(true);
+ row.getPrivateLayout().setOnExpandedVisibleListener(clickedView::performClick);
+ }
+ }
+
+ protected boolean startWorkChallengeIfNecessary(int userId, IntentSender intendSender,
+ String notificationKey) {
+ // Clear pending remote view, as we do not want to trigger pending remote input view when
+ // it's called by other code
+ mPendingWorkRemoteInputView = null;
+ // Begin old BaseStatusBar.startWorkChallengeIfNecessary.
+ final Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null,
+ null, userId);
+ if (newIntent == null) {
+ return false;
+ }
+ final Intent callBackIntent = new Intent(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
+ callBackIntent.putExtra(Intent.EXTRA_INTENT, intendSender);
+ callBackIntent.putExtra(Intent.EXTRA_INDEX, notificationKey);
+ callBackIntent.setPackage(mContext.getPackageName());
+
+ PendingIntent callBackPendingIntent = PendingIntent.getBroadcast(
+ mContext,
+ 0,
+ callBackIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT |
+ PendingIntent.FLAG_ONE_SHOT |
+ PendingIntent.FLAG_IMMUTABLE);
+ newIntent.putExtra(
+ Intent.EXTRA_INTENT,
+ callBackPendingIntent.getIntentSender());
+ try {
+ ActivityManager.getService().startConfirmDeviceCredentialIntent(newIntent,
+ null /*options*/);
+ } catch (RemoteException ex) {
+ // ignore
+ }
+ return true;
+ // End old BaseStatusBar.startWorkChallengeIfNecessary.
+ }
+
+ protected void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row,
+ View clicked) {
+ // Collapse notification and show work challenge
+ animateCollapsePanels();
+ startWorkChallengeIfNecessary(userId, null, null);
+ // Add pending remote input view after starting work challenge, as starting work challenge
+ // will clear all previous pending review view
+ mPendingWorkRemoteInputView = clicked;
+ }
+
+ private boolean isAnyProfilePublicMode() {
+ for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) {
+ if (isLockscreenPublicMode(mCurrentProfiles.valueAt(i).id)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected void onWorkChallengeChanged() {
+ updatePublicMode();
+ updateNotifications();
+ if (mPendingWorkRemoteInputView != null && !isAnyProfilePublicMode()) {
+ // Expand notification panel and the notification row, then click on remote input view
+ final Runnable clickPendingViewRunnable = new Runnable() {
+ @Override
+ public void run() {
+ final View pendingWorkRemoteInputView = mPendingWorkRemoteInputView;
+ if (pendingWorkRemoteInputView == null) {
+ return;
+ }
+
+ // Climb up the hierarchy until we get to the container for this row.
+ ViewParent p = pendingWorkRemoteInputView.getParent();
+ while (!(p instanceof ExpandableNotificationRow)) {
+ if (p == null) {
+ return;
+ }
+ p = p.getParent();
+ }
+
+ final ExpandableNotificationRow row = (ExpandableNotificationRow) p;
+ ViewParent viewParent = row.getParent();
+ if (viewParent instanceof NotificationStackScrollLayout) {
+ final NotificationStackScrollLayout scrollLayout =
+ (NotificationStackScrollLayout) viewParent;
+ row.makeActionsVisibile();
+ row.post(new Runnable() {
+ @Override
+ public void run() {
+ final Runnable finishScrollingCallback = new Runnable() {
+ @Override
+ public void run() {
+ mPendingWorkRemoteInputView.callOnClick();
+ mPendingWorkRemoteInputView = null;
+ scrollLayout.setFinishScrollingCallback(null);
+ }
+ };
+ if (scrollLayout.scrollTo(row)) {
+ // It scrolls! So call it when it's finished.
+ scrollLayout.setFinishScrollingCallback(
+ finishScrollingCallback);
+ } else {
+ // It does not scroll, so call it now!
+ finishScrollingCallback.run();
+ }
+ }
+ });
+ }
+ }
+ };
+ mNotificationPanel.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (mNotificationPanel.mStatusBar.getStatusBarWindow()
+ .getHeight() != mNotificationPanel.mStatusBar
+ .getStatusBarHeight()) {
+ mNotificationPanel.getViewTreeObserver()
+ .removeOnGlobalLayoutListener(this);
+ mNotificationPanel.post(clickPendingViewRunnable);
+ }
+ }
+ });
+ instantExpandNotificationsPanel();
+ }
+ }
+
+ @Override
+ public void onExpandClicked(Entry clickedEntry, boolean nowExpanded) {
+ mHeadsUpManager.setExpanded(clickedEntry, nowExpanded);
+ if (mState == StatusBarState.KEYGUARD && nowExpanded) {
+ goToLockedShade(clickedEntry.row);
+ }
+ }
+
+ /**
+ * Goes back to the keyguard after hanging around in {@link StatusBarState#SHADE_LOCKED}.
+ */
+ public void goToKeyguard() {
+ if (mState == StatusBarState.SHADE_LOCKED) {
+ mStackScroller.onGoToKeyguard();
+ setBarState(StatusBarState.KEYGUARD);
+ updateKeyguardState(false /* goingToFullShade */, true /* fromShadeLocked*/);
+ }
+ }
+
+ public long getKeyguardFadingAwayDelay() {
+ return mKeyguardFadingAwayDelay;
+ }
+
+ public long getKeyguardFadingAwayDuration() {
+ return mKeyguardFadingAwayDuration;
+ }
+
+ public void setBouncerShowing(boolean bouncerShowing) {
+ mBouncerShowing = bouncerShowing;
+ if (mStatusBarView != null) mStatusBarView.setBouncerShowing(bouncerShowing);
+ updateHideIconsForBouncer(true /* animate */);
+ recomputeDisableFlags(true /* animate */);
+ }
+
+ public void cancelCurrentTouch() {
+ if (mNotificationPanel.isTracking()) {
+ mStatusBarWindow.cancelCurrentTouch();
+ if (mState == StatusBarState.SHADE) {
+ animateCollapsePanels();
+ }
+ }
+ }
+
+ WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
+ @Override
+ public void onFinishedGoingToSleep() {
+ mNotificationPanel.onAffordanceLaunchEnded();
+ releaseGestureWakeLock();
+ mLaunchCameraOnScreenTurningOn = false;
+ mDeviceInteractive = false;
+ mWakeUpComingFromTouch = false;
+ mWakeUpTouchLocation = null;
+ mStackScroller.setAnimationsEnabled(false);
+ mVisualStabilityManager.setScreenOn(false);
+ updateVisibleToUser();
+
+ // We need to disable touch events because these might
+ // collapse the panel after we expanded it, and thus we would end up with a blank
+ // Keyguard.
+ mNotificationPanel.setTouchDisabled(true);
+ mStatusBarWindow.cancelCurrentTouch();
+ if (mLaunchCameraOnFinishedGoingToSleep) {
+ mLaunchCameraOnFinishedGoingToSleep = false;
+
+ // This gets executed before we will show Keyguard, so post it in order that the state
+ // is correct.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onCameraLaunchGestureDetected(mLastCameraLaunchSource);
+ }
+ });
+ }
+ updateIsKeyguard();
+ }
+
+ @Override
+ public void onStartedGoingToSleep() {
+ notifyHeadsUpGoingToSleep();
+ dismissVolumeDialog();
+ }
+
+ @Override
+ public void onStartedWakingUp() {
+ mDeviceInteractive = true;
+ mStackScroller.setAnimationsEnabled(true);
+ mVisualStabilityManager.setScreenOn(true);
+ mNotificationPanel.setTouchDisabled(false);
+
+ maybePrepareWakeUpFromAod();
+
+ mDozeServiceHost.stopDozing();
+ updateVisibleToUser();
+ updateIsKeyguard();
+ }
+ };
+
+ ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() {
+ @Override
+ public void onScreenTurningOn() {
+ mFalsingManager.onScreenTurningOn();
+ mNotificationPanel.onScreenTurningOn();
+
+ maybePrepareWakeUpFromAod();
+
+ if (mLaunchCameraOnScreenTurningOn) {
+ mNotificationPanel.launchCamera(false, mLastCameraLaunchSource);
+ mLaunchCameraOnScreenTurningOn = false;
+ }
+ }
+
+ @Override
+ public void onScreenTurnedOn() {
+ mScrimController.wakeUpFromAod();
+ mDozeScrimController.onScreenTurnedOn();
+ }
+
+ @Override
+ public void onScreenTurnedOff() {
+ mFalsingManager.onScreenOff();
+ // If we pulse in from AOD, we turn the screen off first. However, updatingIsKeyguard
+ // in that case destroys the HeadsUpManager state, so don't do it in that case.
+ if (!isPulsing()) {
+ updateIsKeyguard();
+ }
+ }
+ };
+
+ public int getWakefulnessState() {
+ return mWakefulnessLifecycle.getWakefulness();
+ }
+
+ private void maybePrepareWakeUpFromAod() {
+ int wakefulness = mWakefulnessLifecycle.getWakefulness();
+ if (mDozing && wakefulness == WAKEFULNESS_WAKING && !isPulsing()) {
+ mScrimController.prepareWakeUpFromAod();
+ }
+ }
+
+ private void vibrateForCameraGesture() {
+ // Make sure to pass -1 for repeat so VibratorService doesn't stop us when going to sleep.
+ mVibrator.vibrate(mCameraLaunchGestureVibePattern, -1 /* repeat */);
+ }
+
+ /**
+ * @return true if the screen is currently fully off, i.e. has finished turning off and has
+ * since not started turning on.
+ */
+ public boolean isScreenFullyOff() {
+ return mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_OFF;
+ }
+
+ @Override
+ public void showScreenPinningRequest(int taskId) {
+ 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(taskId, true);
+ }
+
+ public void showScreenPinningRequest(int taskId, boolean allowCancel) {
+ mScreenPinningRequest.showPrompt(taskId, allowCancel);
+ }
+
+ public boolean hasActiveNotifications() {
+ return !mNotificationData.getActiveNotifications().isEmpty();
+ }
+
+ public void wakeUpIfDozing(long time, View where) {
+ if (mDozing) {
+ PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ pm.wakeUp(time, "com.android.systemui:NODOZE");
+ mWakeUpComingFromTouch = true;
+ where.getLocationInWindow(mTmpInt2);
+ mWakeUpTouchLocation = new PointF(mTmpInt2[0] + where.getWidth() / 2,
+ mTmpInt2[1] + where.getHeight() / 2);
+ mStatusBarKeyguardViewManager.notifyDeviceWakeUpRequested();
+ mFalsingManager.onScreenOnFromTouch();
+ }
+ }
+
+ @Override
+ public void appTransitionCancelled() {
+ EventBus.getDefault().send(new AppTransitionFinishedEvent());
+ }
+
+ @Override
+ public void appTransitionFinished() {
+ EventBus.getDefault().send(new AppTransitionFinishedEvent());
+ }
+
+ @Override
+ public void onCameraLaunchGestureDetected(int source) {
+ mLastCameraLaunchSource = source;
+ if (isGoingToSleep()) {
+ if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Finish going to sleep before launching camera");
+ mLaunchCameraOnFinishedGoingToSleep = true;
+ return;
+ }
+ if (!mNotificationPanel.canCameraGestureBeLaunched(
+ mStatusBarKeyguardViewManager.isShowing() && mExpandedVisible)) {
+ if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Can't launch camera right now, mExpandedVisible: " +
+ mExpandedVisible);
+ return;
+ }
+ if (!mDeviceInteractive) {
+ PowerManager pm = mContext.getSystemService(PowerManager.class);
+ pm.wakeUp(SystemClock.uptimeMillis(), "com.android.systemui:CAMERA_GESTURE");
+ mStatusBarKeyguardViewManager.notifyDeviceWakeUpRequested();
+ }
+ vibrateForCameraGesture();
+ if (!mStatusBarKeyguardViewManager.isShowing()) {
+ startActivityDismissingKeyguard(KeyguardBottomAreaView.INSECURE_CAMERA_INTENT,
+ false /* onlyProvisioned */, true /* dismissShade */,
+ true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */);
+ } else {
+ if (!mDeviceInteractive) {
+ // Avoid flickering of the scrim when we instant launch the camera and the bouncer
+ // comes on.
+ mScrimController.dontAnimateBouncerChangesUntilNextFrame();
+ mGestureWakeLock.acquire(LAUNCH_TRANSITION_TIMEOUT_MS + 1000L);
+ }
+ if (isScreenTurningOnOrOn()) {
+ if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Launching camera");
+ mNotificationPanel.launchCamera(mDeviceInteractive /* animate */, source);
+ } else {
+ // We need to defer the camera launch until the screen comes on, since otherwise
+ // we will dismiss us too early since we are waiting on an activity to be drawn and
+ // incorrectly get notified because of the screen on event (which resumes and pauses
+ // some activities)
+ if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Deferring until screen turns on");
+ mLaunchCameraOnScreenTurningOn = true;
+ }
+ }
+ }
+
+ boolean isCameraAllowedByAdmin() {
+ if (mDevicePolicyManager.getCameraDisabled(null, mCurrentUserId)) {
+ return false;
+ } else if (isKeyguardShowing() && isKeyguardSecure()) {
+ // Check if the admin has disabled the camera specifically for the keyguard
+ return (mDevicePolicyManager.getKeyguardDisabledFeatures(null, mCurrentUserId)
+ & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) == 0;
+ }
+
+ return true;
+ }
+
+ private boolean isGoingToSleep() {
+ return mWakefulnessLifecycle.getWakefulness()
+ == WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
+ }
+
+ private boolean isScreenTurningOnOrOn() {
+ return mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_TURNING_ON
+ || mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_ON;
+ }
+
+ public void notifyFpAuthModeChanged() {
+ updateDozing();
+ }
+
+ private void updateDozing() {
+ Trace.beginSection("StatusBar#updateDozing");
+ // When in wake-and-unlock while pulsing, keep dozing state until fully unlocked.
+ mDozing = mDozingRequested && mState == StatusBarState.KEYGUARD
+ || mFingerprintUnlockController.getMode()
+ == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
+ // When in wake-and-unlock we may not have received a change to mState
+ // but we still should not be dozing, manually set to false.
+ if (mFingerprintUnlockController.getMode() ==
+ FingerprintUnlockController.MODE_WAKE_AND_UNLOCK) {
+ mDozing = false;
+ }
+ mStatusBarWindowManager.setDozing(mDozing);
+ mStatusBarKeyguardViewManager.setDozing(mDozing);
+ if (mAmbientIndicationContainer instanceof DozeReceiver) {
+ ((DozeReceiver) mAmbientIndicationContainer).setDozing(mDozing);
+ }
+ updateDozingState();
+ Trace.endSection();
+ }
+
+ public boolean isKeyguardShowing() {
+ if (mStatusBarKeyguardViewManager == null) {
+ Slog.i(TAG, "isKeyguardShowing() called before startKeyguard(), returning true");
+ return true;
+ }
+ return mStatusBarKeyguardViewManager.isShowing();
+ }
+
+ private final class DozeServiceHost implements DozeHost {
+ private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+ private boolean mAnimateWakeup;
+ private boolean mIgnoreTouchWhilePulsing;
+
+ @Override
+ public String toString() {
+ return "PSB.DozeServiceHost[mCallbacks=" + mCallbacks.size() + "]";
+ }
+
+ public void firePowerSaveChanged(boolean active) {
+ for (Callback callback : mCallbacks) {
+ callback.onPowerSaveChanged(active);
+ }
+ }
+
+ public void fireNotificationHeadsUp() {
+ for (Callback callback : mCallbacks) {
+ callback.onNotificationHeadsUp();
+ }
+ }
+
+ @Override
+ public void addCallback(@NonNull Callback callback) {
+ mCallbacks.add(callback);
+ }
+
+ @Override
+ public void removeCallback(@NonNull Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ @Override
+ public void startDozing() {
+ if (!mDozingRequested) {
+ mDozingRequested = true;
+ DozeLog.traceDozing(mContext, mDozing);
+ updateDozing();
+ updateIsKeyguard();
+ }
+ }
+
+ @Override
+ public void pulseWhileDozing(@NonNull PulseCallback callback, int reason) {
+ if (reason == DozeLog.PULSE_REASON_SENSOR_LONG_PRESS) {
+ mPowerManager.wakeUp(SystemClock.uptimeMillis(), "com.android.systemui:NODOZE");
+ startAssist(new Bundle());
+ return;
+ }
+
+ mDozeScrimController.pulse(new PulseCallback() {
+
+ @Override
+ public void onPulseStarted() {
+ callback.onPulseStarted();
+ Collection<HeadsUpManager.HeadsUpEntry> pulsingEntries =
+ mHeadsUpManager.getAllEntries();
+ if (!pulsingEntries.isEmpty()) {
+ // Only pulse the stack scroller if there's actually something to show.
+ // Otherwise just show the always-on screen.
+ setPulsing(pulsingEntries);
+ }
+ }
+
+ @Override
+ public void onPulseFinished() {
+ callback.onPulseFinished();
+ setPulsing(null);
+ }
+
+ private void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) {
+ mStackScroller.setPulsing(pulsing);
+ mNotificationPanel.setPulsing(pulsing != null);
+ mVisualStabilityManager.setPulsing(pulsing != null);
+ mIgnoreTouchWhilePulsing = false;
+ }
+ }, reason);
+ }
+
+ @Override
+ public void stopDozing() {
+ if (mDozingRequested) {
+ mDozingRequested = false;
+ DozeLog.traceDozing(mContext, mDozing);
+ updateDozing();
+ }
+ }
+
+ @Override
+ public void onIgnoreTouchWhilePulsing(boolean ignore) {
+ if (ignore != mIgnoreTouchWhilePulsing) {
+ DozeLog.tracePulseTouchDisabledByProx(mContext, ignore);
+ }
+ mIgnoreTouchWhilePulsing = ignore;
+ if (isDozing() && ignore) {
+ mStatusBarWindow.cancelCurrentTouch();
+ }
+ }
+
+ @Override
+ public void dozeTimeTick() {
+ mNotificationPanel.refreshTime();
+ }
+
+ @Override
+ public boolean isPowerSaveActive() {
+ return mBatteryController.isPowerSave();
+ }
+
+ @Override
+ public boolean isPulsingBlocked() {
+ return mFingerprintUnlockController.getMode()
+ == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK;
+ }
+
+ @Override
+ public boolean isProvisioned() {
+ return mDeviceProvisionedController.isDeviceProvisioned()
+ && mDeviceProvisionedController.isCurrentUserSetup();
+ }
+
+ @Override
+ public boolean isBlockingDoze() {
+ if (mFingerprintUnlockController.hasPendingAuthentication()) {
+ Log.i(TAG, "Blocking AOD because fingerprint has authenticated");
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void startPendingIntentDismissingKeyguard(PendingIntent intent) {
+ StatusBar.this.startPendingIntentDismissingKeyguard(intent);
+ }
+
+ @Override
+ public void abortPulsing() {
+ mDozeScrimController.abortPulsing();
+ }
+
+ @Override
+ public void extendPulse() {
+ mDozeScrimController.extendPulse();
+ }
+
+ @Override
+ public void setAnimateWakeup(boolean animateWakeup) {
+ if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_AWAKE
+ || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_WAKING) {
+ // Too late to change the wakeup animation.
+ return;
+ }
+ mAnimateWakeup = animateWakeup;
+ }
+
+ @Override
+ public void onDoubleTap(float screenX, float screenY) {
+ if (screenX > 0 && screenY > 0 && mAmbientIndicationContainer != null
+ && mAmbientIndicationContainer.getVisibility() == View.VISIBLE) {
+ mAmbientIndicationContainer.getLocationOnScreen(mTmpInt2);
+ float viewX = screenX - mTmpInt2[0];
+ float viewY = screenY - mTmpInt2[1];
+ if (0 <= viewX && viewX <= mAmbientIndicationContainer.getWidth()
+ && 0 <= viewY && viewY <= mAmbientIndicationContainer.getHeight()) {
+ dispatchDoubleTap(viewX, viewY);
+ }
+ }
+ }
+
+ @Override
+ public void setDozeScreenBrightness(int value) {
+ mStatusBarWindowManager.setDozeScreenBrightness(value);
+ }
+
+ @Override
+ public void setAodDimmingScrim(float scrimOpacity) {
+ mDozeScrimController.setAodDimmingScrim(scrimOpacity);
+ }
+
+ public void dispatchDoubleTap(float viewX, float viewY) {
+ dispatchTap(mAmbientIndicationContainer, viewX, viewY);
+ dispatchTap(mAmbientIndicationContainer, viewX, viewY);
+ }
+
+ private void dispatchTap(View view, float x, float y) {
+ long now = SystemClock.elapsedRealtime();
+ dispatchTouchEvent(view, x, y, now, MotionEvent.ACTION_DOWN);
+ dispatchTouchEvent(view, x, y, now, MotionEvent.ACTION_UP);
+ }
+
+ private void dispatchTouchEvent(View view, float x, float y, long now, int action) {
+ MotionEvent ev = MotionEvent.obtain(now, now, action, x, y, 0 /* meta */);
+ view.dispatchTouchEvent(ev);
+ ev.recycle();
+ }
+
+ private boolean shouldAnimateWakeup() {
+ return mAnimateWakeup;
+ }
+ }
+
+ public boolean shouldIgnoreTouch() {
+ return isDozing() && mDozeServiceHost.mIgnoreTouchWhilePulsing;
+ }
+
+ // Begin Extra BaseStatusBar methods.
+
+ protected CommandQueue mCommandQueue;
+ protected IStatusBarService mBarService;
+
+ // all notifications
+ protected NotificationData mNotificationData;
+ protected NotificationStackScrollLayout mStackScroller;
+
+ protected NotificationGroupManager mGroupManager = new NotificationGroupManager();
+
+ protected RemoteInputController mRemoteInputController;
+
+ // for heads up notifications
+ protected HeadsUpManager mHeadsUpManager;
+
+ private AboveShelfObserver mAboveShelfObserver;
+
+ // handling reordering
+ protected VisualStabilityManager mVisualStabilityManager = new VisualStabilityManager();
+
+ protected int mCurrentUserId = 0;
+ final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>();
+
+ protected int mLayoutDirection = -1; // invalid
+ protected AccessibilityManager mAccessibilityManager;
+
+ protected boolean mDeviceInteractive;
+
+ protected boolean mVisible;
+ protected ArraySet<Entry> mHeadsUpEntriesToRemoveOnSwitch = new ArraySet<>();
+ protected ArraySet<Entry> mRemoteInputEntriesToRemoveOnCollapse = new ArraySet<>();
+
+ /**
+ * Notifications with keys in this set are not actually around anymore. We kept them around
+ * when they were canceled in response to a remote input interaction. This allows us to show
+ * what you replied and allows you to continue typing into it.
+ */
+ protected ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>();
+
+ // mScreenOnFromKeyguard && mVisible.
+ private boolean mVisibleToUser;
+
+ private Locale mLocale;
+
+ protected boolean mUseHeadsUp = false;
+ protected boolean mHeadsUpTicker = false;
+ protected boolean mDisableNotificationAlerts = false;
+
+ protected DevicePolicyManager mDevicePolicyManager;
+ protected PowerManager mPowerManager;
+ protected StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+
+ // public mode, private notifications, etc
+ private final SparseBooleanArray mLockscreenPublicMode = new SparseBooleanArray();
+ private final SparseBooleanArray mUsersAllowingPrivateNotifications = new SparseBooleanArray();
+ private final SparseBooleanArray mUsersAllowingNotifications = new SparseBooleanArray();
+
+ private UserManager mUserManager;
+
+ protected KeyguardManager mKeyguardManager;
+ private LockPatternUtils mLockPatternUtils;
+ private DeviceProvisionedController mDeviceProvisionedController
+ = Dependency.get(DeviceProvisionedController.class);
+ protected SystemServicesProxy mSystemServicesProxy;
+
+ // UI-specific methods
+
+ protected WindowManager mWindowManager;
+ protected IWindowManager mWindowManagerService;
+
+ protected Display mDisplay;
+
+ protected RecentsComponent mRecents;
+
+ protected int mZenMode;
+
+ // which notification is currently being longpress-examined by the user
+ private NotificationGuts mNotificationGutsExposed;
+ private MenuItem mGutsMenuItem;
+
+ private KeyboardShortcuts mKeyboardShortcuts;
+
+ protected NotificationShelf mNotificationShelf;
+ protected DismissView mDismissView;
+ protected EmptyShadeView mEmptyShadeView;
+
+ private NotificationClicker mNotificationClicker = new NotificationClicker();
+
+ protected AssistManager mAssistManager;
+
+ protected boolean mVrMode;
+
+ private Set<String> mNonBlockablePkgs;
+
+ public boolean isDeviceInteractive() {
+ return mDeviceInteractive;
+ }
+
+ @Override // NotificationData.Environment
+ public boolean isDeviceProvisioned() {
+ return mDeviceProvisionedController.isDeviceProvisioned();
+ }
+
+ private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
+ @Override
+ public void onVrStateChanged(boolean enabled) {
+ mVrMode = enabled;
+ }
+ };
+
+ public boolean isDeviceInVrMode() {
+ return mVrMode;
+ }
+
+ private final DeviceProvisionedListener mDeviceProvisionedListener =
+ new DeviceProvisionedListener() {
+ @Override
+ public void onDeviceProvisionedChanged() {
+ updateNotifications();
+ }
+ };
+
+ protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final int mode = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF);
+ setZenMode(mode);
+
+ updateLockscreenNotificationSetting();
+ }
+ };
+
+ private final ContentObserver mLockscreenSettingsObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ // We don't know which user changed LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS or
+ // LOCK_SCREEN_SHOW_NOTIFICATIONS, so we just dump our cache ...
+ mUsersAllowingPrivateNotifications.clear();
+ mUsersAllowingNotifications.clear();
+ // ... and refresh all the notifications
+ updateLockscreenNotificationSetting();
+ updateNotifications();
+ }
+ };
+
+ private RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
+
+ @Override
+ public boolean onClickHandler(
+ final View view, final PendingIntent pendingIntent, final Intent fillInIntent) {
+ wakeUpIfDozing(SystemClock.uptimeMillis(), view);
+
+
+ if (handleRemoteInput(view, pendingIntent, fillInIntent)) {
+ return true;
+ }
+
+ 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
+ // point, so make sure new activity switches are now allowed.
+ try {
+ ActivityManager.getService().resumeAppSwitches();
+ } catch (RemoteException e) {
+ }
+ final boolean isActivity = pendingIntent.isActivity();
+ if (isActivity) {
+ final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing();
+ final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
+ mContext, pendingIntent.getIntent(), mCurrentUserId);
+ dismissKeyguardThenExecute(new OnDismissAction() {
+ @Override
+ public boolean onDismiss() {
+ try {
+ ActivityManager.getService().resumeAppSwitches();
+ } catch (RemoteException e) {
+ }
+
+ boolean handled = superOnClickHandler(view, pendingIntent, fillInIntent);
+
+ // close the shade if it was open
+ if (handled && !mNotificationPanel.isFullyCollapsed()) {
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
+ true /* force */);
+ visibilityChanged(false);
+ mAssistManager.hideAssist();
+
+ // Wait for activity start.
+ return true;
+ } else {
+ return false;
+ }
+
+ }
+ }, afterKeyguardGone);
+ return true;
+ } else {
+ return superOnClickHandler(view, pendingIntent, fillInIntent);
+ }
+ }
+
+ 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);
+ }
+ 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,
+ StackId.FULLSCREEN_WORKSPACE_STACK_ID);
+ }
+
+ private boolean handleRemoteInput(View view, PendingIntent pendingIntent, Intent fillInIntent) {
+ Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
+ RemoteInput[] inputs = null;
+ if (tag instanceof RemoteInput[]) {
+ inputs = (RemoteInput[]) tag;
+ }
+
+ if (inputs == null) {
+ return false;
+ }
+
+ RemoteInput input = null;
+
+ for (RemoteInput i : inputs) {
+ if (i.getAllowFreeFormInput()) {
+ input = i;
+ }
+ }
+
+ if (input == null) {
+ return false;
+ }
+
+ ViewParent p = view.getParent();
+ RemoteInputView riv = null;
+ while (p != null) {
+ if (p instanceof View) {
+ View pv = (View) p;
+ if (pv.isRootNamespace()) {
+ riv = findRemoteInputView(pv);
+ break;
+ }
+ }
+ p = p.getParent();
+ }
+ ExpandableNotificationRow row = null;
+ while (p != null) {
+ if (p instanceof ExpandableNotificationRow) {
+ row = (ExpandableNotificationRow) p;
+ break;
+ }
+ p = p.getParent();
+ }
+
+ if (row == null) {
+ return false;
+ }
+
+ row.setUserExpanded(true);
+
+ if (!mAllowLockscreenRemoteInput) {
+ final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
+ if (isLockscreenPublicMode(userId)) {
+ onLockedRemoteInput(row, view);
+ return true;
+ }
+ if (mUserManager.getUserInfo(userId).isManagedProfile()
+ && mKeyguardManager.isDeviceLocked(userId)) {
+ onLockedWorkRemoteInput(userId, row, view);
+ return true;
+ }
+ }
+
+ if (riv == null) {
+ riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
+ if (riv == null) {
+ return false;
+ }
+ if (!row.getPrivateLayout().getExpandedChild().isShown()) {
+ onMakeExpandedVisibleForRemoteInput(row, view);
+ return true;
+ }
+ }
+
+ int width = view.getWidth();
+ if (view instanceof TextView) {
+ // Center the reveal on the text which might be off-center from the TextView
+ TextView tv = (TextView) view;
+ if (tv.getLayout() != null) {
+ int innerWidth = (int) tv.getLayout().getLineWidth(0);
+ innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
+ width = Math.min(width, innerWidth);
+ }
+ }
+ int cx = view.getLeft() + width / 2;
+ int cy = view.getTop() + view.getHeight() / 2;
+ int w = riv.getWidth();
+ int h = riv.getHeight();
+ int r = Math.max(
+ Math.max(cx + cy, cx + (h - cy)),
+ Math.max((w - cx) + cy, (w - cx) + (h - cy)));
+
+ riv.setRevealParameters(cx, cy, r);
+ riv.setPendingIntent(pendingIntent);
+ riv.setRemoteInput(inputs, input);
+ riv.focusAnimated();
+
+ return true;
+ }
+
+ private RemoteInputView findRemoteInputView(View v) {
+ if (v == null) {
+ return null;
+ }
+ return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
+ }
+ };
+
+ private final BroadcastReceiver mBaseBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+ mCurrentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ updateCurrentProfilesCache();
+ if (true) Log.v(TAG, "userId " + mCurrentUserId + " is in the house");
+
+ updateLockscreenNotificationSetting();
+
+ userSwitched(mCurrentUserId);
+ } else if (Intent.ACTION_USER_ADDED.equals(action)) {
+ updateCurrentProfilesCache();
+ } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
+ List<ActivityManager.RecentTaskInfo> recentTask = null;
+ try {
+ recentTask = ActivityManager.getService().getRecentTasks(1,
+ ActivityManager.RECENT_WITH_EXCLUDED
+ | ActivityManager.RECENT_INCLUDE_PROFILES,
+ mCurrentUserId).getList();
+ } catch (RemoteException e) {
+ // Abandon hope activity manager not running.
+ }
+ if (recentTask != null && recentTask.size() > 0) {
+ UserInfo user = mUserManager.getUserInfo(recentTask.get(0).userId);
+ if (user != null && user.isManagedProfile()) {
+ Toast toast = Toast.makeText(mContext,
+ R.string.managed_profile_foreground_toast,
+ Toast.LENGTH_SHORT);
+ TextView text = (TextView) toast.getView().findViewById(
+ android.R.id.message);
+ text.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ R.drawable.stat_sys_managed_profile_status, 0, 0, 0);
+ int paddingPx = mContext.getResources().getDimensionPixelSize(
+ R.dimen.managed_profile_toast_padding);
+ text.setCompoundDrawablePadding(paddingPx);
+ toast.show();
+ }
+ }
+ } else if (BANNER_ACTION_CANCEL.equals(action) || BANNER_ACTION_SETUP.equals(action)) {
+ NotificationManager noMan = (NotificationManager)
+ mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ noMan.cancel(SystemMessage.NOTE_HIDDEN_NOTIFICATIONS);
+
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0);
+ if (BANNER_ACTION_SETUP.equals(action)) {
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
+ true /* force */);
+ mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_REDACTION)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ );
+ }
+ } else if (NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION.equals(action)) {
+ final IntentSender intentSender = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+ final String notificationKey = intent.getStringExtra(Intent.EXTRA_INDEX);
+ if (intentSender != null) {
+ try {
+ mContext.startIntentSender(intentSender, null, 0, 0, 0);
+ } catch (IntentSender.SendIntentException e) {
+ /* ignore */
+ }
+ }
+ if (notificationKey != null) {
+ try {
+ mBarService.onNotificationClick(notificationKey);
+ } catch (RemoteException e) {
+ /* ignore */
+ }
+ }
+ }
+ }
+ };
+
+ private final BroadcastReceiver mAllUsersReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+
+ if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(action) &&
+ isCurrentProfile(getSendingUserId())) {
+ mUsersAllowingPrivateNotifications.clear();
+ updateLockscreenNotificationSetting();
+ updateNotifications();
+ } else if (Intent.ACTION_DEVICE_LOCKED_CHANGED.equals(action)) {
+ if (userId != mCurrentUserId && isCurrentProfile(userId)) {
+ onWorkChallengeChanged();
+ }
+ }
+ }
+ };
+
+ private final NotificationListenerWithPlugins mNotificationListener =
+ new NotificationListenerWithPlugins() {
+ @Override
+ public void onListenerConnected() {
+ if (DEBUG) Log.d(TAG, "onListenerConnected");
+ onPluginConnected();
+ final StatusBarNotification[] notifications = getActiveNotifications();
+ if (notifications == null) {
+ Log.w(TAG, "onListenerConnected unable to get active notifications.");
+ return;
+ }
+ final RankingMap currentRanking = getCurrentRanking();
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (StatusBarNotification sbn : notifications) {
+ try {
+ addNotification(sbn, currentRanking);
+ } catch (InflationException e) {
+ handleInflationException(sbn, e);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onNotificationPosted(final StatusBarNotification sbn,
+ final RankingMap rankingMap) {
+ if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn);
+ if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ processForRemoteInput(sbn.getNotification());
+ String key = sbn.getKey();
+ mKeysKeptForRemoteInput.remove(key);
+ boolean isUpdate = mNotificationData.get(key) != null;
+ // In case we don't allow child notifications, we ignore children of
+ // notifications that have a summary, since we're not going to show them
+ // anyway. This is true also when the summary is canceled,
+ // because children are automatically canceled by NoMan in that case.
+ if (!ENABLE_CHILD_NOTIFICATIONS
+ && mGroupManager.isChildInGroupWithSummary(sbn)) {
+ if (DEBUG) {
+ Log.d(TAG, "Ignoring group child due to existing summary: " + sbn);
+ }
+
+ // Remove existing notification to avoid stale data.
+ if (isUpdate) {
+ removeNotification(key, rankingMap);
+ } else {
+ mNotificationData.updateRanking(rankingMap);
+ }
+ return;
+ }
+ try {
+ if (isUpdate) {
+ updateNotification(sbn, rankingMap);
+ } else {
+ addNotification(sbn, rankingMap);
+ }
+ } catch (InflationException e) {
+ handleInflationException(sbn, e);
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification sbn,
+ final RankingMap rankingMap) {
+ if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn);
+ if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) {
+ final String key = sbn.getKey();
+ mHandler.post(() -> removeNotification(key, rankingMap));
+ }
+ }
+
+ @Override
+ public void onNotificationRankingUpdate(final RankingMap rankingMap) {
+ if (DEBUG) Log.d(TAG, "onRankingUpdate");
+ if (rankingMap != null) {
+ RankingMap r = onPluginRankingUpdate(rankingMap);
+ mHandler.post(() -> updateNotificationRanking(r));
+ }
+ }
+
+ };
+
+ private void updateCurrentProfilesCache() {
+ synchronized (mCurrentProfiles) {
+ mCurrentProfiles.clear();
+ if (mUserManager != null) {
+ for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) {
+ mCurrentProfiles.put(user.id, user);
+ }
+ }
+ }
+ }
+
+ protected void notifyUserAboutHiddenNotifications() {
+ if (0 != Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 1)) {
+ Log.d(TAG, "user hasn't seen notification about hidden notifications");
+ if (!mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())) {
+ Log.d(TAG, "insecure lockscreen, skipping notification");
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0);
+ return;
+ }
+ Log.d(TAG, "disabling lockecreen notifications and alerting the user");
+ // disable lockscreen notifications until user acts on the banner.
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0);
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0);
+
+ final String packageName = mContext.getPackageName();
+ PendingIntent cancelIntent = PendingIntent.getBroadcast(mContext, 0,
+ new Intent(BANNER_ACTION_CANCEL).setPackage(packageName),
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ PendingIntent setupIntent = PendingIntent.getBroadcast(mContext, 0,
+ new Intent(BANNER_ACTION_SETUP).setPackage(packageName),
+ PendingIntent.FLAG_CANCEL_CURRENT);
+
+ final int colorRes = com.android.internal.R.color.system_notification_accent_color;
+ Notification.Builder note =
+ new Notification.Builder(mContext, NotificationChannels.GENERAL)
+ .setSmallIcon(R.drawable.ic_android)
+ .setContentTitle(mContext.getString(
+ R.string.hidden_notifications_title))
+ .setContentText(mContext.getString(R.string.hidden_notifications_text))
+ .setOngoing(true)
+ .setColor(mContext.getColor(colorRes))
+ .setContentIntent(setupIntent)
+ .addAction(R.drawable.ic_close,
+ mContext.getString(R.string.hidden_notifications_cancel),
+ cancelIntent)
+ .addAction(R.drawable.ic_settings,
+ mContext.getString(R.string.hidden_notifications_setup),
+ setupIntent);
+ overrideNotificationAppName(mContext, note);
+
+ NotificationManager noMan =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ noMan.notify(SystemMessage.NOTE_HIDDEN_NOTIFICATIONS, note.build());
+ }
+ }
+
+ @Override // NotificationData.Environment
+ public boolean isNotificationForCurrentProfiles(StatusBarNotification n) {
+ final int thisUserId = mCurrentUserId;
+ final int notificationUserId = n.getUserId();
+ if (DEBUG && MULTIUSER_DEBUG) {
+ Log.v(TAG, String.format("%s: current userid: %d, notification userid: %d",
+ n, thisUserId, notificationUserId));
+ }
+ return isCurrentProfile(notificationUserId);
+ }
+
+ protected void setNotificationShown(StatusBarNotification n) {
+ setNotificationsShown(new String[]{n.getKey()});
+ }
+
+ protected void setNotificationsShown(String[] keys) {
+ try {
+ mNotificationListener.setNotificationsShown(keys);
+ } catch (RuntimeException e) {
+ Log.d(TAG, "failed setNotificationsShown: ", e);
+ }
+ }
+
+ protected boolean isCurrentProfile(int userId) {
+ synchronized (mCurrentProfiles) {
+ return userId == UserHandle.USER_ALL || mCurrentProfiles.get(userId) != null;
+ }
+ }
+
+ @Override
+ public NotificationGroupManager getGroupManager() {
+ return mGroupManager;
+ }
+
+ public boolean isMediaNotification(NotificationData.Entry entry) {
+ // TODO: confirm that there's a valid media key
+ return entry.getExpandedContentView() != null &&
+ entry.getExpandedContentView()
+ .findViewById(com.android.internal.R.id.media_actions) != null;
+ }
+
+ // The button in the guts that links to the system notification settings for that app
+ private void startAppNotificationSettingsActivity(String packageName, final int appUid,
+ final NotificationChannel channel) {
+ final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
+ intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
+ intent.putExtra(Settings.EXTRA_APP_UID, appUid);
+ if (channel != null) {
+ intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId());
+ }
+ startNotificationGutsIntent(intent, appUid);
+ }
+
+ private void startNotificationGutsIntent(final Intent intent, final int appUid) {
+ dismissKeyguardThenExecute(new OnDismissAction() {
+ @Override
+ public boolean onDismiss() {
+ AsyncTask.execute(new Runnable() {
+ @Override
+ public void run() {
+ TaskStackBuilder.create(mContext)
+ .addNextIntentWithParentStack(intent)
+ .startActivities(getActivityOptions(),
+ new UserHandle(UserHandle.getUserId(appUid)));
+ }
+ });
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */);
+ return true;
+ }
+ }, false /* afterKeyguardGone */);
+ }
+
+ public void setNotificationSnoozed(StatusBarNotification sbn, SnoozeOption snoozeOption) {
+ if (snoozeOption.getSnoozeCriterion() != null) {
+ mNotificationListener.snoozeNotification(sbn.getKey(),
+ snoozeOption.getSnoozeCriterion().getId());
+ } else {
+ mNotificationListener.snoozeNotification(sbn.getKey(),
+ snoozeOption.getMinutesToSnoozeFor() * 60 * 1000);
+ }
+ }
+
+ private void bindGuts(final ExpandableNotificationRow row, MenuItem item) {
+ row.inflateGuts();
+ row.setGutsView(item);
+ final StatusBarNotification sbn = row.getStatusBarNotification();
+ row.setTag(sbn.getPackageName());
+ final NotificationGuts guts = row.getGuts();
+ guts.setClosedListener((NotificationGuts g) -> {
+ if (!g.willBeRemoved() && !row.isRemoved()) {
+ mStackScroller.onHeightChanged(row, !isPanelFullyCollapsed() /* needsAnimation */);
+ }
+ if (mNotificationGutsExposed == g) {
+ mNotificationGutsExposed = null;
+ mGutsMenuItem = null;
+ }
+ String key = sbn.getKey();
+ if (key.equals(mKeyToRemoveOnGutsClosed)) {
+ mKeyToRemoveOnGutsClosed = null;
+ removeNotification(key, mLatestRankingMap);
+ }
+ });
+
+ View gutsView = item.getGutsView();
+ if (gutsView instanceof NotificationSnooze) {
+ NotificationSnooze snoozeGuts = (NotificationSnooze) gutsView;
+ snoozeGuts.setSnoozeListener(mStackScroller.getSwipeActionHelper());
+ snoozeGuts.setStatusBarNotification(sbn);
+ snoozeGuts.setSnoozeOptions(row.getEntry().snoozeCriteria);
+ guts.setHeightChangedListener((NotificationGuts g) -> {
+ mStackScroller.onHeightChanged(row, row.isShown() /* needsAnimation */);
+ });
+ }
+
+ if (gutsView instanceof NotificationInfo) {
+ final UserHandle userHandle = sbn.getUser();
+ PackageManager pmUser = getPackageManagerForUser(mContext,
+ userHandle.getIdentifier());
+ final INotificationManager iNotificationManager = INotificationManager.Stub.asInterface(
+ ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+ final String pkg = sbn.getPackageName();
+ NotificationInfo info = (NotificationInfo) gutsView;
+ // Settings link is only valid for notifications that specify a user, unless this is the
+ // system user.
+ NotificationInfo.OnSettingsClickListener onSettingsClick = null;
+ if (!userHandle.equals(UserHandle.ALL) || mCurrentUserId == UserHandle.USER_SYSTEM) {
+ onSettingsClick = (View v, NotificationChannel channel, int appUid) -> {
+ mMetricsLogger.action(MetricsEvent.ACTION_NOTE_INFO);
+ guts.resetFalsingCheck();
+ startAppNotificationSettingsActivity(pkg, appUid, channel);
+ };
+ }
+ final NotificationInfo.OnAppSettingsClickListener onAppSettingsClick = (View v,
+ Intent intent) -> {
+ mMetricsLogger.action(MetricsEvent.ACTION_APP_NOTE_SETTINGS);
+ guts.resetFalsingCheck();
+ startNotificationGutsIntent(intent, sbn.getUid());
+ };
+ final View.OnClickListener onDoneClick = (View v) -> {
+ saveAndCloseNotificationMenu(info, row, guts, v);
+ };
+ final NotificationInfo.CheckSaveListener checkSaveListener =
+ (Runnable saveImportance) -> {
+ // If the user has security enabled, show challenge if the setting is changed.
+ if (isLockscreenPublicMode(userHandle.getIdentifier())
+ && (mState == StatusBarState.KEYGUARD
+ || mState == StatusBarState.SHADE_LOCKED)) {
+ onLockedNotificationImportanceChange(() -> {
+ saveImportance.run();
+ return true;
+ });
+ } else {
+ saveImportance.run();
+ }
+ };
+
+ ArraySet<NotificationChannel> channels = new ArraySet<NotificationChannel>();
+ channels.add(row.getEntry().channel);
+ if (row.isSummaryWithChildren()) {
+ // If this is a summary, then add in the children notification channels for the
+ // same user and pkg.
+ final List<ExpandableNotificationRow> childrenRows = row.getNotificationChildren();
+ final int numChildren = childrenRows.size();
+ for (int i = 0; i < numChildren; i++) {
+ final ExpandableNotificationRow childRow = childrenRows.get(i);
+ final NotificationChannel childChannel = childRow.getEntry().channel;
+ final StatusBarNotification childSbn = childRow.getStatusBarNotification();
+ if (childSbn.getUser().equals(userHandle) &&
+ childSbn.getPackageName().equals(pkg)) {
+ channels.add(childChannel);
+ }
+ }
+ }
+ try {
+ info.bindNotification(pmUser, iNotificationManager, pkg, new ArrayList(channels),
+ row.getEntry().channel.getImportance(), sbn, onSettingsClick,
+ onAppSettingsClick, onDoneClick, checkSaveListener,
+ mNonBlockablePkgs);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ }
+ }
+
+ private void saveAndCloseNotificationMenu(NotificationInfo info,
+ ExpandableNotificationRow row, NotificationGuts guts, View done) {
+ guts.resetFalsingCheck();
+ int[] rowLocation = new int[2];
+ int[] doneLocation = new int[2];
+ row.getLocationOnScreen(rowLocation);
+ done.getLocationOnScreen(doneLocation);
+
+ final int centerX = done.getWidth() / 2;
+ final int centerY = done.getHeight() / 2;
+ final int x = doneLocation[0] - rowLocation[0] + centerX;
+ final int y = doneLocation[1] - rowLocation[1] + centerY;
+ closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
+ true /* removeControls */, x, y, true /* resetMenu */);
+ }
+
+ protected SwipeHelper.LongPressListener getNotificationLongClicker() {
+ return new SwipeHelper.LongPressListener() {
+ @Override
+ public boolean onLongPress(View v, final int x, final int y,
+ MenuItem item) {
+ if (!(v instanceof ExpandableNotificationRow)) {
+ return false;
+ }
+ if (v.getWindowToken() == null) {
+ Log.e(TAG, "Trying to show notification guts, but not attached to window");
+ return false;
+ }
+
+ final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ if (row.isDark()) {
+ return false;
+ }
+ v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ if (row.areGutsExposed()) {
+ closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
+ true /* removeControls */, -1 /* x */, -1 /* y */,
+ true /* resetMenu */);
+ return false;
+ }
+ bindGuts(row, item);
+ NotificationGuts guts = row.getGuts();
+
+ // Assume we are a status_bar_notification_row
+ if (guts == null) {
+ // This view has no guts. Examples are the more card or the dismiss all view
+ return false;
+ }
+
+ mMetricsLogger.action(MetricsEvent.ACTION_NOTE_CONTROLS);
+
+ // ensure that it's laid but not visible until actually laid out
+ guts.setVisibility(View.INVISIBLE);
+ // Post to ensure the the guts are properly laid out.
+ guts.post(new Runnable() {
+ @Override
+ public void run() {
+ if (row.getWindowToken() == null) {
+ Log.e(TAG, "Trying to show notification guts, but not attached to "
+ + "window");
+ return;
+ }
+ closeAndSaveGuts(true /* removeLeavebehind */, true /* force */,
+ true /* removeControls */, -1 /* x */, -1 /* y */,
+ false /* resetMenu */);
+ guts.setVisibility(View.VISIBLE);
+ final double horz = Math.max(guts.getWidth() - x, x);
+ final double vert = Math.max(guts.getHeight() - y, y);
+ final float r = (float) Math.hypot(horz, vert);
+ final Animator a
+ = ViewAnimationUtils.createCircularReveal(guts, x, y, 0, r);
+ a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ a.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ // Move the notification view back over the menu
+ row.resetTranslation();
+ }
+ });
+ a.start();
+ final boolean needsFalsingProtection =
+ (mState == StatusBarState.KEYGUARD &&
+ !mAccessibilityManager.isTouchExplorationEnabled());
+ guts.setExposed(true /* exposed */, needsFalsingProtection);
+ row.closeRemoteInput();
+ mStackScroller.onHeightChanged(row, true /* needsAnimation */);
+ mNotificationGutsExposed = guts;
+ mGutsMenuItem = item;
+ }
+ });
+ return true;
+ }
+ };
+ }
+
+ /**
+ * Returns the exposed NotificationGuts or null if none are exposed.
+ */
+ public NotificationGuts getExposedGuts() {
+ return mNotificationGutsExposed;
+ }
+
+ /**
+ * Closes guts or notification menus that might be visible and saves any changes.
+ *
+ * @param removeLeavebehinds true if leavebehinds (e.g. snooze) should be closed.
+ * @param force true if guts should be closed regardless of state (used for snooze only).
+ * @param removeControls true if controls (e.g. info) should be closed.
+ * @param x if closed based on touch location, this is the x touch location.
+ * @param y if closed based on touch location, this is the y touch location.
+ * @param resetMenu if any notification menus that might be revealed should be closed.
+ */
+ public void closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls,
+ int x, int y, boolean resetMenu) {
+ if (mNotificationGutsExposed != null) {
+ mNotificationGutsExposed.closeControls(removeLeavebehinds, removeControls, x, y, force);
+ }
+ if (resetMenu) {
+ mStackScroller.resetExposedMenuView(false /* animate */, true /* force */);
+ }
+ }
+
+ @Override
+ public void toggleSplitScreen() {
+ toggleSplitScreenMode(-1 /* metricsDockAction */, -1 /* metricsUndockAction */);
+ }
+
+ @Override
+ public void preloadRecentApps() {
+ int msg = MSG_PRELOAD_RECENT_APPS;
+ mHandler.removeMessages(msg);
+ mHandler.sendEmptyMessage(msg);
+ }
+
+ @Override
+ public void cancelPreloadRecentApps() {
+ int msg = MSG_CANCEL_PRELOAD_RECENT_APPS;
+ mHandler.removeMessages(msg);
+ mHandler.sendEmptyMessage(msg);
+ }
+
+ @Override
+ public void dismissKeyboardShortcutsMenu() {
+ int msg = MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU;
+ mHandler.removeMessages(msg);
+ mHandler.sendEmptyMessage(msg);
+ }
+
+ @Override
+ public void toggleKeyboardShortcutsMenu(int deviceId) {
+ int msg = MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU;
+ mHandler.removeMessages(msg);
+ mHandler.obtainMessage(msg, deviceId, 0).sendToTarget();
+ }
+
+ @Override
+ public void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) {
+ mTopHidesStatusBar = topAppHidesStatusBar;
+ if (!topAppHidesStatusBar && mWereIconsJustHidden) {
+ // Immediately update the icon hidden state, since that should only apply if we're
+ // staying fullscreen.
+ mWereIconsJustHidden = false;
+ recomputeDisableFlags(true);
+ }
+ updateHideIconsForBouncer(true /* animate */);
+ }
+
+ protected void sendCloseSystemWindows(String reason) {
+ try {
+ ActivityManager.getService().closeSystemDialogs(reason);
+ } catch (RemoteException e) {
+ }
+ }
+
+ protected void toggleKeyboardShortcuts(int deviceId) {
+ KeyboardShortcuts.toggle(mContext, deviceId);
+ }
+
+ protected void dismissKeyboardShortcuts() {
+ KeyboardShortcuts.dismiss();
+ }
+
+ /**
+ * Save the current "public" (locked and secure) state of the lockscreen.
+ */
+ public void setLockscreenPublicMode(boolean publicMode, int userId) {
+ mLockscreenPublicMode.put(userId, publicMode);
+ }
+
+ public boolean isLockscreenPublicMode(int userId) {
+ if (userId == UserHandle.USER_ALL) {
+ return mLockscreenPublicMode.get(mCurrentUserId, false);
+ }
+ return mLockscreenPublicMode.get(userId, false);
+ }
+
+ /**
+ * Has the given user chosen to allow notifications to be shown even when the lockscreen is in
+ * "public" (secure & locked) mode?
+ */
+ public boolean userAllowsNotificationsInPublic(int userHandle) {
+ if (userHandle == UserHandle.USER_ALL) {
+ return true;
+ }
+
+ if (mUsersAllowingNotifications.indexOfKey(userHandle) < 0) {
+ final boolean allowed = 0 != Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, userHandle);
+ mUsersAllowingNotifications.append(userHandle, allowed);
+ return allowed;
+ }
+
+ return mUsersAllowingNotifications.get(userHandle);
+ }
+
+ /**
+ * Has the given user chosen to allow their private (full) notifications to be shown even
+ * when the lockscreen is in "public" (secure & locked) mode?
+ */
+ public boolean userAllowsPrivateNotificationsInPublic(int userHandle) {
+ if (userHandle == UserHandle.USER_ALL) {
+ return true;
+ }
+
+ if (mUsersAllowingPrivateNotifications.indexOfKey(userHandle) < 0) {
+ final boolean allowedByUser = 0 != Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle);
+ final boolean allowedByDpm = adminAllowsUnredactedNotifications(userHandle);
+ final boolean allowed = allowedByUser && allowedByDpm;
+ mUsersAllowingPrivateNotifications.append(userHandle, allowed);
+ return allowed;
+ }
+
+ return mUsersAllowingPrivateNotifications.get(userHandle);
+ }
+
+ private boolean adminAllowsUnredactedNotifications(int userHandle) {
+ if (userHandle == UserHandle.USER_ALL) {
+ return true;
+ }
+ final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */,
+ userHandle);
+ return (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS) == 0;
+ }
+
+ /**
+ * Returns true if we're on a secure lockscreen and the user wants to hide notification data.
+ * If so, notifications should be hidden.
+ */
+ @Override // NotificationData.Environment
+ public boolean shouldHideNotifications(int userId) {
+ return isLockscreenPublicMode(userId) && !userAllowsNotificationsInPublic(userId)
+ || (userId != mCurrentUserId && shouldHideNotifications(mCurrentUserId));
+ }
+
+ /**
+ * Returns true if we're on a secure lockscreen and the user wants to hide notifications via
+ * package-specific override.
+ */
+ @Override // NotificationDate.Environment
+ public boolean shouldHideNotifications(String key) {
+ return isLockscreenPublicMode(mCurrentUserId)
+ && mNotificationData.getVisibilityOverride(key) == Notification.VISIBILITY_SECRET;
+ }
+
+ /**
+ * Returns true if we're on a secure lockscreen.
+ */
+ @Override // NotificationData.Environment
+ public boolean isSecurelyLocked(int userId) {
+ return isLockscreenPublicMode(userId);
+ }
+
+ public void onNotificationClear(StatusBarNotification notification) {
+ try {
+ mBarService.onNotificationClear(
+ notification.getPackageName(),
+ notification.getTag(),
+ notification.getId(),
+ notification.getUserId());
+ } catch (android.os.RemoteException ex) {
+ // oh well
+ }
+ }
+
+ /**
+ * Called when the notification panel layouts
+ */
+ public void onPanelLaidOut() {
+ updateKeyguardMaxNotifications();
+ }
+
+ public void updateKeyguardMaxNotifications() {
+ if (mState == StatusBarState.KEYGUARD) {
+ // Since the number of notifications is determined based on the height of the view, we
+ // need to update them.
+ int maxBefore = getMaxKeyguardNotifications(false /* recompute */);
+ int maxNotifications = getMaxKeyguardNotifications(true /* recompute */);
+ if (maxBefore != maxNotifications) {
+ updateRowStates();
+ }
+ }
+ }
+
+ protected void inflateViews(Entry entry, ViewGroup parent) {
+ PackageManager pmUser = getPackageManagerForUser(mContext,
+ entry.notification.getUser().getIdentifier());
+
+ final StatusBarNotification sbn = entry.notification;
+ if (entry.row != null) {
+ entry.reset();
+ updateNotification(entry, pmUser, sbn, entry.row);
+ } else {
+ new RowInflaterTask().inflate(mContext, parent, entry,
+ row -> {
+ bindRow(entry, pmUser, sbn, row);
+ updateNotification(entry, pmUser, sbn, row);
+ });
+ }
+
+ }
+
+ private void bindRow(Entry entry, PackageManager pmUser,
+ StatusBarNotification sbn, ExpandableNotificationRow row) {
+ row.setExpansionLogger(this, entry.notification.getKey());
+ row.setGroupManager(mGroupManager);
+ row.setHeadsUpManager(mHeadsUpManager);
+ row.setAboveShelfChangedListener(mAboveShelfObserver);
+ row.setRemoteInputController(mRemoteInputController);
+ row.setOnExpandClickListener(this);
+ row.setRemoteViewClickHandler(mOnClickHandler);
+ row.setInflationCallback(this);
+ row.setSecureStateProvider(this::isKeyguardCurrentlySecure);
+
+ // Get the app name.
+ // Note that Notification.Builder#bindHeaderAppName has similar logic
+ // but since this field is used in the guts, it must be accurate.
+ // Therefore we will only show the application label, or, failing that, the
+ // package name. No substitutions.
+ final String pkg = sbn.getPackageName();
+ String appname = pkg;
+ try {
+ final ApplicationInfo info = pmUser.getApplicationInfo(pkg,
+ PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DISABLED_COMPONENTS);
+ if (info != null) {
+ appname = String.valueOf(pmUser.getApplicationLabel(info));
+ }
+ } catch (NameNotFoundException e) {
+ // Do nothing
+ }
+ row.setAppName(appname);
+ row.setOnDismissRunnable(() ->
+ performRemoveNotification(row.getStatusBarNotification()));
+ row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ if (ENABLE_REMOTE_INPUT) {
+ row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+ }
+ }
+
+ private void updateNotification(Entry entry, PackageManager pmUser,
+ StatusBarNotification sbn, ExpandableNotificationRow row) {
+ row.setNeedsRedaction(needsRedaction(entry));
+ boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey());
+ boolean isUpdate = mNotificationData.get(entry.key) != null;
+ boolean wasLowPriority = row.isLowPriority();
+ row.setIsLowPriority(isLowPriority);
+ row.setLowPriorityStateUpdated(isUpdate && (wasLowPriority != isLowPriority));
+ // bind the click event to the content area
+ mNotificationClicker.register(row, sbn);
+
+ // Extract target SDK version.
+ try {
+ ApplicationInfo info = pmUser.getApplicationInfo(sbn.getPackageName(), 0);
+ entry.targetSdk = info.targetSdkVersion;
+ } catch (NameNotFoundException ex) {
+ Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex);
+ }
+ row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD
+ && entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
+ entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
+ entry.autoRedacted = entry.notification.getNotification().publicVersion == null;
+
+ entry.row = row;
+ entry.row.setOnActivatedListener(this);
+
+ boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(sbn,
+ mNotificationData.getImportance(sbn.getKey()));
+ boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight && mPanelExpanded;
+ row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
+ row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
+ row.updateNotification(entry);
+ }
+
+ /**
+ * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this
+ * via first-class API.
+ *
+ * TODO: Remove once enough apps specify remote inputs on their own.
+ */
+ private void processForRemoteInput(Notification n) {
+ if (!ENABLE_REMOTE_INPUT) return;
+
+ if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") &&
+ (n.actions == null || n.actions.length == 0)) {
+ Notification.Action viableAction = null;
+ Notification.WearableExtender we = new Notification.WearableExtender(n);
+
+ List<Notification.Action> actions = we.getActions();
+ final int numActions = actions.size();
+
+ for (int i = 0; i < numActions; i++) {
+ Notification.Action action = actions.get(i);
+ if (action == null) {
+ continue;
+ }
+ RemoteInput[] remoteInputs = action.getRemoteInputs();
+ if (remoteInputs == null) {
+ continue;
+ }
+ for (RemoteInput ri : remoteInputs) {
+ if (ri.getAllowFreeFormInput()) {
+ viableAction = action;
+ break;
+ }
+ }
+ if (viableAction != null) {
+ break;
+ }
+ }
+
+ if (viableAction != null) {
+ Notification.Builder rebuilder = Notification.Builder.recoverBuilder(mContext, n);
+ rebuilder.setActions(viableAction);
+ rebuilder.build(); // will rewrite n
+ }
+ }
+ }
+
+ public void startPendingIntentDismissingKeyguard(final PendingIntent intent) {
+ if (!isDeviceProvisioned()) return;
+
+ final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing();
+ final boolean afterKeyguardGone = intent.isActivity()
+ && PreviewInflater.wouldLaunchResolverActivity(mContext, intent.getIntent(),
+ mCurrentUserId);
+ dismissKeyguardThenExecute(new OnDismissAction() {
+ @Override
+ public boolean onDismiss() {
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ // 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
+ // point, so make sure new activity switches are now allowed.
+ ActivityManager.getService().resumeAppSwitches();
+ } catch (RemoteException e) {
+ }
+ try {
+ intent.send(null, 0, null, null, null, null, getActivityOptions());
+ } catch (PendingIntent.CanceledException e) {
+ // the stack trace isn't very helpful here.
+ // Just log the exception message.
+ Log.w(TAG, "Sending intent failed: " + e);
+
+ // TODO: Dismiss Keyguard.
+ }
+ if (intent.isActivity()) {
+ mAssistManager.hideAssist();
+ }
+ }
+ }.start();
+
+ if (!mNotificationPanel.isFullyCollapsed()) {
+ // close the shade if it was open
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
+ true /* force */, true /* delayed */);
+ visibilityChanged(false);
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }, afterKeyguardGone);
+ }
+
+
+ private final class NotificationClicker implements View.OnClickListener {
+
+ @Override
+ public void onClick(final View v) {
+ if (!(v instanceof ExpandableNotificationRow)) {
+ Log.e(TAG, "NotificationClicker called on a view that is not a notification row.");
+ return;
+ }
+
+ wakeUpIfDozing(SystemClock.uptimeMillis(), v);
+
+ final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ final StatusBarNotification sbn = row.getStatusBarNotification();
+ if (sbn == null) {
+ Log.e(TAG, "NotificationClicker called on an unclickable notification,");
+ return;
+ }
+
+ // Check if the notification is displaying the menu, if so slide notification back
+ if (row.getProvider() != null && row.getProvider().isMenuVisible()) {
+ row.animateTranslateNotification(0);
+ return;
+ }
+
+ Notification notification = sbn.getNotification();
+ final PendingIntent intent = notification.contentIntent != null
+ ? notification.contentIntent
+ : notification.fullScreenIntent;
+ final String notificationKey = sbn.getKey();
+
+ // Mark notification for one frame.
+ row.setJustClicked(true);
+ DejankUtils.postAfterTraversal(new Runnable() {
+ @Override
+ public void run() {
+ row.setJustClicked(false);
+ }
+ });
+
+ final boolean afterKeyguardGone = intent.isActivity()
+ && PreviewInflater.wouldLaunchResolverActivity(mContext, intent.getIntent(),
+ mCurrentUserId);
+ dismissKeyguardThenExecute(new OnDismissAction() {
+ @Override
+ public boolean onDismiss() {
+ if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUp(notificationKey)) {
+ // Release the HUN notification to the shade.
+
+ if (isPanelFullyCollapsed()) {
+ HeadsUpManager.setIsClickedNotification(row, true);
+ }
+ //
+ // In most cases, when FLAG_AUTO_CANCEL is set, the notification will
+ // become canceled shortly by NoMan, but we can't assume that.
+ mHeadsUpManager.releaseImmediately(notificationKey);
+ }
+ StatusBarNotification parentToCancel = null;
+ if (shouldAutoCancel(sbn) && mGroupManager.isOnlyChildInGroup(sbn)) {
+ StatusBarNotification summarySbn = mGroupManager.getLogicalGroupSummary(sbn)
+ .getStatusBarNotification();
+ if (shouldAutoCancel(summarySbn)) {
+ parentToCancel = summarySbn;
+ }
+ }
+ final StatusBarNotification parentToCancelFinal = parentToCancel;
+ final Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // 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
+ // point, so make sure new activity switches are now allowed.
+ ActivityManager.getService().resumeAppSwitches();
+ } catch (RemoteException e) {
+ }
+ if (intent != null) {
+ // If we are launching a work activity and require to launch
+ // separate work challenge, we defer the activity action and cancel
+ // notification until work challenge is unlocked.
+ if (intent.isActivity()) {
+ final int userId = intent.getCreatorUserHandle()
+ .getIdentifier();
+ if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)
+ && mKeyguardManager.isDeviceLocked(userId)) {
+ // TODO(b/28935539): should allow certain activities to
+ // bypass work challenge
+ if (startWorkChallengeIfNecessary(userId,
+ intent.getIntentSender(), notificationKey)) {
+ // Show work challenge, do not run PendingIntent and
+ // remove notification
+ return;
+ }
+ }
+ }
+ try {
+ intent.send(null, 0, null, null, null, null,
+ getActivityOptions());
+ } catch (PendingIntent.CanceledException e) {
+ // the stack trace isn't very helpful here.
+ // Just log the exception message.
+ Log.w(TAG, "Sending contentIntent failed: " + e);
+
+ // TODO: Dismiss Keyguard.
+ }
+ if (intent.isActivity()) {
+ mAssistManager.hideAssist();
+ }
+ }
+
+ try {
+ mBarService.onNotificationClick(notificationKey);
+ } catch (RemoteException ex) {
+ // system process is dead if we're here.
+ }
+ if (parentToCancelFinal != null) {
+ // We have to post it to the UI thread for synchronization
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Runnable removeRunnable = new Runnable() {
+ @Override
+ public void run() {
+ performRemoveNotification(parentToCancelFinal);
+ }
+ };
+ if (isCollapsing()) {
+ // To avoid lags we're only performing the remove
+ // after the shade was collapsed
+ addPostCollapseAction(removeRunnable);
+ } else {
+ removeRunnable.run();
+ }
+ }
+ });
+ }
+ }
+ };
+
+ if (mStatusBarKeyguardViewManager.isShowing()
+ && mStatusBarKeyguardViewManager.isOccluded()) {
+ mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
+ } else {
+ new Thread(runnable).start();
+ }
+
+ if (!mNotificationPanel.isFullyCollapsed()) {
+ // close the shade if it was open
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
+ true /* force */, true /* delayed */);
+ visibilityChanged(false);
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }, afterKeyguardGone);
+ }
+
+ private boolean shouldAutoCancel(StatusBarNotification sbn) {
+ int flags = sbn.getNotification().flags;
+ if ((flags & Notification.FLAG_AUTO_CANCEL) != Notification.FLAG_AUTO_CANCEL) {
+ return false;
+ }
+ if ((flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public void register(ExpandableNotificationRow row, StatusBarNotification sbn) {
+ Notification notification = sbn.getNotification();
+ if (notification.contentIntent != null || notification.fullScreenIntent != null) {
+ row.setOnClickListener(this);
+ } else {
+ row.setOnClickListener(null);
+ }
+ }
+ }
+
+ protected Bundle getActivityOptions() {
+ // Anything launched from the notification shade should always go into the
+ // fullscreen stack.
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchStackId(StackId.FULLSCREEN_WORKSPACE_STACK_ID);
+ return options.toBundle();
+ }
+
+ protected void visibilityChanged(boolean visible) {
+ if (mVisible != visible) {
+ mVisible = visible;
+ if (!visible) {
+ closeAndSaveGuts(true /* removeLeavebehind */, true /* force */,
+ true /* removeControls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
+ }
+ }
+ updateVisibleToUser();
+ }
+
+ protected void updateVisibleToUser() {
+ boolean oldVisibleToUser = mVisibleToUser;
+ mVisibleToUser = mVisible && mDeviceInteractive;
+
+ if (oldVisibleToUser != mVisibleToUser) {
+ handleVisibleToUserChanged(mVisibleToUser);
+ }
+ }
+
+ /**
+ * Clear Buzz/Beep/Blink.
+ */
+ public void clearNotificationEffects() {
+ try {
+ mBarService.clearNotificationEffects();
+ } catch (RemoteException e) {
+ // Won't fail unless the world has ended.
+ }
+ }
+
+ /**
+ * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService
+ * about the failure.
+ *
+ * WARNING: this will call back into us. Don't hold any locks.
+ */
+ void handleNotificationError(StatusBarNotification n, String message) {
+ removeNotification(n.getKey(), null);
+ try {
+ mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(),
+ n.getInitialPid(), message, n.getUserId());
+ } catch (RemoteException ex) {
+ // The end is nigh.
+ }
+ }
+
+ protected StatusBarNotification removeNotificationViews(String key, RankingMap ranking) {
+ NotificationData.Entry entry = mNotificationData.remove(key, ranking);
+ if (entry == null) {
+ Log.w(TAG, "removeNotification for unknown key: " + key);
+ return null;
+ }
+ updateNotifications();
+ Dependency.get(LeakDetector.class).trackGarbage(entry);
+ return entry.notification;
+ }
+
+ protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn)
+ throws InflationException {
+ if (DEBUG) {
+ Log.d(TAG, "createNotificationViews(notification=" + sbn);
+ }
+ NotificationData.Entry entry = new NotificationData.Entry(sbn);
+ Dependency.get(LeakDetector.class).trackInstance(entry);
+ entry.createIcons(mContext, sbn);
+ // Construct the expanded view.
+ inflateViews(entry, mStackScroller);
+ return entry;
+ }
+
+ protected void addNotificationViews(Entry entry) {
+ if (entry == null) {
+ return;
+ }
+ // Add the expanded view and icon.
+ mNotificationData.add(entry);
+ updateNotifications();
+ }
+
+ /**
+ * Updates expanded, dimmed and locked states of notification rows.
+ */
+ protected void updateRowStates() {
+ final int N = mStackScroller.getChildCount();
+
+ int visibleNotifications = 0;
+ boolean onKeyguard = mState == StatusBarState.KEYGUARD;
+ int maxNotifications = -1;
+ if (onKeyguard) {
+ maxNotifications = getMaxKeyguardNotifications(true /* recompute */);
+ }
+ mStackScroller.setMaxDisplayedNotifications(maxNotifications);
+ Stack<ExpandableNotificationRow> stack = new Stack<>();
+ for (int i = N - 1; i >= 0; i--) {
+ View child = mStackScroller.getChildAt(i);
+ if (!(child instanceof ExpandableNotificationRow)) {
+ continue;
+ }
+ stack.push((ExpandableNotificationRow) child);
+ }
+ while(!stack.isEmpty()) {
+ ExpandableNotificationRow row = stack.pop();
+ NotificationData.Entry entry = row.getEntry();
+ boolean isChildNotification =
+ mGroupManager.isChildInGroupWithSummary(entry.notification);
+
+ row.setOnKeyguard(onKeyguard);
+
+ if (!onKeyguard) {
+ // If mAlwaysExpandNonGroupedNotification is false, then only expand the
+ // very first notification and if it's not a child of grouped notifications.
+ row.setSystemExpanded(mAlwaysExpandNonGroupedNotification
+ || (visibleNotifications == 0 && !isChildNotification
+ && !row.isLowPriority()));
+ }
+
+ entry.row.setShowAmbient(isDozing());
+ int userId = entry.notification.getUserId();
+ boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
+ entry.notification) && !entry.row.isRemoved();
+ boolean showOnKeyguard = shouldShowOnKeyguard(entry.notification);
+ if (suppressedSummary
+ || (isLockscreenPublicMode(userId) && !mShowLockscreenNotifications)
+ || (onKeyguard && !showOnKeyguard)) {
+ entry.row.setVisibility(View.GONE);
+ } else {
+ boolean wasGone = entry.row.getVisibility() == View.GONE;
+ if (wasGone) {
+ entry.row.setVisibility(View.VISIBLE);
+ }
+ if (!isChildNotification && !entry.row.isRemoved()) {
+ if (wasGone) {
+ // notify the scroller of a child addition
+ mStackScroller.generateAddAnimation(entry.row,
+ !showOnKeyguard /* fromMoreCard */);
+ }
+ visibleNotifications++;
+ }
+ }
+ if (row.isSummaryWithChildren()) {
+ List<ExpandableNotificationRow> notificationChildren =
+ row.getNotificationChildren();
+ int size = notificationChildren.size();
+ for (int i = size - 1; i >= 0; i--) {
+ stack.push(notificationChildren.get(i));
+ }
+ }
+ }
+ mNotificationPanel.setNoVisibleNotifications(visibleNotifications == 0);
+
+ // The following views will be moved to the end of mStackScroller. This counter represents
+ // the offset from the last child. Initialized to 1 for the very last position. It is post-
+ // incremented in the following "changeViewPosition" calls so that its value is correct for
+ // subsequent calls.
+ int offsetFromEnd = 1;
+ if (mDismissView != null) {
+ mStackScroller.changeViewPosition(mDismissView,
+ mStackScroller.getChildCount() - offsetFromEnd++);
+ }
+
+ mStackScroller.changeViewPosition(mEmptyShadeView,
+ mStackScroller.getChildCount() - offsetFromEnd++);
+
+ // No post-increment for this call because it is the last one. Make sure to add one if
+ // another "changeViewPosition" call is ever added.
+ mStackScroller.changeViewPosition(mNotificationShelf,
+ mStackScroller.getChildCount() - offsetFromEnd);
+
+ // Scrim opacity varies based on notification count
+ mScrimController.setNotificationCount(mStackScroller.getNotGoneChildCount());
+ }
+
+ public boolean shouldShowOnKeyguard(StatusBarNotification sbn) {
+ return mShowLockscreenNotifications && !mNotificationData.isAmbient(sbn.getKey());
+ }
+
+ // extended in StatusBar
+ protected void setShowLockscreenNotifications(boolean show) {
+ mShowLockscreenNotifications = show;
+ }
+
+ protected void setLockScreenAllowRemoteInput(boolean allowLockscreenRemoteInput) {
+ mAllowLockscreenRemoteInput = allowLockscreenRemoteInput;
+ }
+
+ private void updateLockscreenNotificationSetting() {
+ final boolean show = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+ 1,
+ mCurrentUserId) != 0;
+ final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(
+ null /* admin */, mCurrentUserId);
+ final boolean allowedByDpm = (dpmFlags
+ & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0;
+
+ setShowLockscreenNotifications(show && allowedByDpm);
+
+ if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
+ final boolean remoteInput = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT,
+ 0,
+ mCurrentUserId) != 0;
+ final boolean remoteInputDpm =
+ (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_REMOTE_INPUT) == 0;
+
+ setLockScreenAllowRemoteInput(remoteInput && remoteInputDpm);
+ } else {
+ setLockScreenAllowRemoteInput(false);
+ }
+ }
+
+ public void updateNotification(StatusBarNotification notification, RankingMap ranking)
+ throws InflationException {
+ if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
+
+ final String key = notification.getKey();
+ abortExistingInflation(key);
+ Entry entry = mNotificationData.get(key);
+ if (entry == null) {
+ return;
+ }
+ mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
+ mRemoteInputEntriesToRemoveOnCollapse.remove(entry);
+ if (key.equals(mKeyToRemoveOnGutsClosed)) {
+ mKeyToRemoveOnGutsClosed = null;
+ Log.w(TAG, "Notification that was kept for guts was updated. " + key);
+ }
+
+ Notification n = notification.getNotification();
+ mNotificationData.updateRanking(ranking);
+
+ final StatusBarNotification oldNotification = entry.notification;
+ entry.notification = notification;
+ mGroupManager.onEntryUpdated(entry, oldNotification);
+
+ entry.updateIcons(mContext, notification);
+ inflateViews(entry, mStackScroller);
+
+ mForegroundServiceController.updateNotification(notification,
+ mNotificationData.getImportance(key));
+
+ boolean shouldPeek = shouldPeek(entry, notification);
+ boolean alertAgain = alertAgain(entry, n);
+
+ updateHeadsUp(key, entry, shouldPeek, alertAgain);
+ updateNotifications();
+
+ if (!notification.isClearable()) {
+ // The user may have performed a dismiss action on the notification, since it's
+ // not clearable we should snap it back.
+ mStackScroller.snapViewIfNeeded(entry.row);
+ }
+
+ if (DEBUG) {
+ // Is this for you?
+ boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
+ Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
+ }
+
+ setAreThereNotifications();
+ }
+
+ protected void notifyHeadsUpGoingToSleep() {
+ maybeEscalateHeadsUp();
+ }
+
+ private boolean alertAgain(Entry oldEntry, Notification newNotification) {
+ return oldEntry == null || !oldEntry.hasInterrupted()
+ || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0;
+ }
+
+ protected boolean shouldPeek(Entry entry) {
+ return shouldPeek(entry, entry.notification);
+ }
+
+ protected boolean shouldPeek(Entry entry, StatusBarNotification sbn) {
+ if (!mUseHeadsUp || isDeviceInVrMode()) {
+ if (DEBUG) Log.d(TAG, "No peeking: no huns or vr mode");
+ return false;
+ }
+
+ if (mNotificationData.shouldFilterOut(sbn)) {
+ if (DEBUG) Log.d(TAG, "No peeking: filtered notification: " + sbn.getKey());
+ return false;
+ }
+
+ boolean inUse = mPowerManager.isScreenOn() && !mSystemServicesProxy.isDreaming();
+
+ if (!inUse && !isDozing()) {
+ if (DEBUG) {
+ Log.d(TAG, "No peeking: not in use: " + sbn.getKey());
+ }
+ return false;
+ }
+
+ if (!isDozing() && mNotificationData.shouldSuppressScreenOn(sbn.getKey())) {
+ if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
+ return false;
+ }
+
+ if (isDozing() && mNotificationData.shouldSuppressScreenOff(sbn.getKey())) {
+ if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
+ return false;
+ }
+
+ if (entry.hasJustLaunchedFullScreenIntent()) {
+ if (DEBUG) Log.d(TAG, "No peeking: recent fullscreen: " + sbn.getKey());
+ return false;
+ }
+
+ if (isSnoozedPackage(sbn)) {
+ if (DEBUG) Log.d(TAG, "No peeking: snoozed package: " + sbn.getKey());
+ return false;
+ }
+
+ // Allow peeking for DEFAULT notifications only if we're on Ambient Display.
+ int importanceLevel = isDozing() ? NotificationManager.IMPORTANCE_DEFAULT
+ : NotificationManager.IMPORTANCE_HIGH;
+ if (mNotificationData.getImportance(sbn.getKey()) < importanceLevel) {
+ if (DEBUG) Log.d(TAG, "No peeking: unimportant notification: " + sbn.getKey());
+ return false;
+ }
+
+ if (sbn.getNotification().fullScreenIntent != null) {
+ if (mAccessibilityManager.isTouchExplorationEnabled()) {
+ if (DEBUG) Log.d(TAG, "No peeking: accessible fullscreen: " + sbn.getKey());
+ return false;
+ } else if (mDozing) {
+ // We never want heads up when we are dozing.
+ return false;
+ } else {
+ // we only allow head-up on the lockscreen if it doesn't have a fullscreen intent
+ return !mStatusBarKeyguardViewManager.isShowing()
+ || mStatusBarKeyguardViewManager.isOccluded();
+ }
+ }
+
+ // Don't peek notifications that are suppressed due to group alert behavior
+ if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
+ if (DEBUG) Log.d(TAG, "No peeking: suppressed due to group alert behavior");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @return Whether the security bouncer from Keyguard is showing.
+ */
+ public boolean isBouncerShowing() {
+ return mBouncerShowing;
+ }
+
+ /**
+ * @return a PackageManger for userId or if userId is < 0 (USER_ALL etc) then
+ * return PackageManager for mContext
+ */
+ public static PackageManager getPackageManagerForUser(Context context, int userId) {
+ Context contextForUser = context;
+ // UserHandle defines special userId as negative values, e.g. USER_ALL
+ if (userId >= 0) {
+ try {
+ // Create a context for the correct user so if a package isn't installed
+ // for user 0 we can still load information about the package.
+ contextForUser =
+ context.createPackageContextAsUser(context.getPackageName(),
+ Context.CONTEXT_RESTRICTED,
+ new UserHandle(userId));
+ } catch (NameNotFoundException e) {
+ // Shouldn't fail to find the package name for system ui.
+ }
+ }
+ return contextForUser.getPackageManager();
+ }
+
+ @Override
+ public void logNotificationExpansion(String key, boolean userAction, boolean expanded) {
+ mUiOffloadThread.submit(() -> {
+ try {
+ mBarService.onNotificationExpansionChanged(key, userAction, expanded);
+ } catch (RemoteException e) {
+ // 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();
+ }
+
+ @Override
+ public void showAssistDisclosure() {
+ if (mAssistManager != null) {
+ mAssistManager.showDisclosure();
+ }
+ }
+
+ public NotificationPanelView getPanel() {
+ return mNotificationPanel;
+ }
+
+ @Override
+ public void startAssist(Bundle args) {
+ if (mAssistManager != null) {
+ mAssistManager.startAssist(args);
+ }
+ }
+ // End Extra BaseStatusBarMethods.
+
+ private final Runnable mAutoDim = () -> {
+ if (mNavigationBar != null) {
+ mNavigationBar.getBarTransitions().setAutoDim(true);
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarIconController.java b/com/android/systemui/statusbar/phone/StatusBarIconController.java
new file mode 100644
index 0000000..c240765
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2017 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.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+
+public interface StatusBarIconController {
+
+ public void addIconGroup(IconManager iconManager);
+ public void removeIconGroup(IconManager iconManager);
+ public void setExternalIcon(String slot);
+ public void setIcon(String slot, int resourceId, CharSequence contentDescription);
+ public void setIcon(String slot, StatusBarIcon icon);
+ public void setIconVisibility(String slotTty, boolean b);
+ public void removeIcon(String slot);
+
+ public static final String ICON_BLACKLIST = "icon_blacklist";
+
+ public static ArraySet<String> getIconBlacklist(String blackListStr) {
+ ArraySet<String> ret = new ArraySet<>();
+ if (blackListStr == null) {
+ blackListStr = "rotate,headset";
+ }
+ String[] blacklist = blackListStr.split(",");
+ for (String slot : blacklist) {
+ if (!TextUtils.isEmpty(slot)) {
+ ret.add(slot);
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Version of ViewGroup that observers state from the DarkIconDispatcher.
+ */
+ public static class DarkIconManager extends IconManager {
+ private final DarkIconDispatcher mDarkIconDispatcher;
+ private int mIconHPadding;
+
+ public DarkIconManager(LinearLayout linearLayout) {
+ super(linearLayout);
+ mIconHPadding = mContext.getResources().getDimensionPixelSize(
+ R.dimen.status_bar_icon_padding);
+ mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
+ }
+
+ @Override
+ protected void onIconAdded(int index, String slot, boolean blocked,
+ StatusBarIcon icon) {
+ StatusBarIconView v = addIcon(index, slot, blocked, icon);
+ mDarkIconDispatcher.addDarkReceiver(v);
+ }
+
+ @Override
+ protected LayoutParams onCreateLayoutParams() {
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
+ lp.setMargins(mIconHPadding, 0, mIconHPadding, 0);
+ return lp;
+ }
+
+ @Override
+ protected void destroy() {
+ for (int i = 0; i < mGroup.getChildCount(); i++) {
+ mDarkIconDispatcher.removeDarkReceiver((ImageView) mGroup.getChildAt(i));
+ }
+ mGroup.removeAllViews();
+ }
+
+ @Override
+ protected void onRemoveIcon(int viewIndex) {
+ mDarkIconDispatcher.removeDarkReceiver((ImageView) mGroup.getChildAt(viewIndex));
+ super.onRemoveIcon(viewIndex);
+ }
+
+ @Override
+ public void onSetIcon(int viewIndex, StatusBarIcon icon) {
+ super.onSetIcon(viewIndex, icon);
+ mDarkIconDispatcher.applyDark((ImageView) mGroup.getChildAt(viewIndex));
+ }
+ }
+
+ public static class TintedIconManager extends IconManager {
+ private int mColor;
+
+ public TintedIconManager(ViewGroup group) {
+ super(group);
+ }
+
+ @Override
+ protected void onIconAdded(int index, String slot, boolean blocked, StatusBarIcon icon) {
+ StatusBarIconView v = addIcon(index, slot, blocked, icon);
+ v.setStaticDrawableColor(mColor);
+ }
+
+ public void setTint(int color) {
+ mColor = color;
+ for (int i = 0; i < mGroup.getChildCount(); i++) {
+ View child = mGroup.getChildAt(i);
+ if (child instanceof StatusBarIconView) {
+ StatusBarIconView icon = (StatusBarIconView) child;
+ icon.setStaticDrawableColor(mColor);
+ }
+ }
+ }
+ }
+
+ /**
+ * Turns info from StatusBarIconController into ImageViews in a ViewGroup.
+ */
+ public static class IconManager {
+ protected final ViewGroup mGroup;
+ protected final Context mContext;
+ protected final int mIconSize;
+
+ public IconManager(ViewGroup group) {
+ mGroup = group;
+ mContext = group.getContext();
+ mIconSize = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_icon_size);
+ }
+
+ protected void onIconAdded(int index, String slot, boolean blocked,
+ StatusBarIcon icon) {
+ addIcon(index, slot, blocked, icon);
+ }
+
+ protected StatusBarIconView addIcon(int index, String slot, boolean blocked,
+ StatusBarIcon icon) {
+ StatusBarIconView view = onCreateStatusBarIconView(slot, blocked);
+ view.set(icon);
+ mGroup.addView(view, index, onCreateLayoutParams());
+ return view;
+ }
+
+ @VisibleForTesting
+ protected StatusBarIconView onCreateStatusBarIconView(String slot, boolean blocked) {
+ return new StatusBarIconView(mContext, slot, null, blocked);
+ }
+
+ protected LinearLayout.LayoutParams onCreateLayoutParams() {
+ return new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
+ }
+
+ protected void destroy() {
+ mGroup.removeAllViews();
+ }
+
+ protected void onIconExternal(int viewIndex, int height) {
+ ImageView imageView = (ImageView) mGroup.getChildAt(viewIndex);
+ imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ imageView.setAdjustViewBounds(true);
+ setHeightAndCenter(imageView, height);
+ }
+
+ protected void onDensityOrFontScaleChanged() {
+ for (int i = 0; i < mGroup.getChildCount(); i++) {
+ View child = mGroup.getChildAt(i);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
+ child.setLayoutParams(lp);
+ }
+ }
+
+ private void setHeightAndCenter(ImageView imageView, int height) {
+ ViewGroup.LayoutParams params = imageView.getLayoutParams();
+ params.height = height;
+ if (params instanceof LinearLayout.LayoutParams) {
+ ((LinearLayout.LayoutParams) params).gravity = Gravity.CENTER_VERTICAL;
+ }
+ imageView.setLayoutParams(params);
+ }
+
+ protected void onRemoveIcon(int viewIndex) {
+ mGroup.removeViewAt(viewIndex);
+ }
+
+ public void onSetIcon(int viewIndex, StatusBarIcon icon) {
+ StatusBarIconView view = (StatusBarIconView) mGroup.getChildAt(viewIndex);
+ view.set(icon);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
new file mode 100644
index 0000000..68f8e06
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2017 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.Context;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.IconLogger;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Receives the callbacks from CommandQueue related to icons and tracks the state of
+ * all the icons. Dispatches this state to any IconManagers that are currently
+ * registered with it.
+ */
+public class StatusBarIconControllerImpl extends StatusBarIconList implements Tunable,
+ ConfigurationListener, Dumpable, CommandQueue.Callbacks, StatusBarIconController {
+
+ private final DarkIconDispatcher mDarkIconDispatcher;
+
+ private Context mContext;
+ private DemoStatusIcons mDemoStatusIcons;
+
+ private final ArrayList<IconManager> mIconGroups = new ArrayList<>();
+
+ private final ArraySet<String> mIconBlacklist = new ArraySet<>();
+ private final IconLogger mIconLogger = Dependency.get(IconLogger.class);
+
+ public StatusBarIconControllerImpl(Context context) {
+ super(context.getResources().getStringArray(
+ com.android.internal.R.array.config_statusBarIcons));
+ Dependency.get(ConfigurationController.class).addCallback(this);
+ mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
+ mContext = context;
+
+ loadDimens();
+
+ SysUiServiceProvider.getComponent(context, CommandQueue.class)
+ .addCallbacks(this);
+ Dependency.get(TunerService.class).addTunable(this, ICON_BLACKLIST);
+ }
+
+ @Override
+ public void addIconGroup(IconManager group) {
+ mIconGroups.add(group);
+ for (int i = 0; i < mIcons.size(); i++) {
+ StatusBarIcon icon = mIcons.get(i);
+ if (icon != null) {
+ String slot = mSlots.get(i);
+ boolean blocked = mIconBlacklist.contains(slot);
+ group.onIconAdded(getViewIndex(getSlotIndex(slot)), slot, blocked, icon);
+ }
+ }
+ }
+
+ @Override
+ public void removeIconGroup(IconManager group) {
+ group.destroy();
+ mIconGroups.remove(group);
+ }
+
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ if (!ICON_BLACKLIST.equals(key)) {
+ return;
+ }
+ mIconBlacklist.clear();
+ mIconBlacklist.addAll(StatusBarIconController.getIconBlacklist(newValue));
+ ArrayList<StatusBarIcon> current = new ArrayList<>(mIcons);
+ ArrayList<String> currentSlots = new ArrayList<>(mSlots);
+ // Remove all the icons.
+ for (int i = current.size() - 1; i >= 0; i--) {
+ removeIcon(currentSlots.get(i));
+ }
+ // Add them all back
+ for (int i = 0; i < current.size(); i++) {
+ setIcon(currentSlots.get(i), current.get(i));
+ }
+ }
+
+ private void loadDimens() {
+ }
+
+ private void addSystemIcon(int index, StatusBarIcon icon) {
+ String slot = getSlot(index);
+ int viewIndex = getViewIndex(index);
+ boolean blocked = mIconBlacklist.contains(slot);
+
+ mIconLogger.onIconVisibility(getSlot(index), icon.visible);
+ mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, blocked, icon));
+ }
+
+ @Override
+ public void setIcon(String slot, int resourceId, CharSequence contentDescription) {
+ int index = getSlotIndex(slot);
+ StatusBarIcon icon = getIcon(index);
+ if (icon == null) {
+ icon = new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(),
+ Icon.createWithResource(mContext, resourceId), 0, 0, contentDescription);
+ setIcon(slot, icon);
+ } else {
+ icon.icon = Icon.createWithResource(mContext, resourceId);
+ icon.contentDescription = contentDescription;
+ handleSet(index, icon);
+ }
+ }
+
+ @Override
+ public void setExternalIcon(String slot) {
+ int viewIndex = getViewIndex(getSlotIndex(slot));
+ int height = mContext.getResources().getDimensionPixelSize(
+ R.dimen.status_bar_icon_drawing_size);
+ mIconGroups.forEach(l -> l.onIconExternal(viewIndex, height));
+ }
+
+ @Override
+ public void setIcon(String slot, StatusBarIcon icon) {
+ setIcon(getSlotIndex(slot), icon);
+ }
+
+ @Override
+ public void removeIcon(String slot) {
+ int index = getSlotIndex(slot);
+ removeIcon(index);
+ }
+
+ public void setIconVisibility(String slot, boolean visibility) {
+ int index = getSlotIndex(slot);
+ StatusBarIcon icon = getIcon(index);
+ if (icon == null || icon.visible == visibility) {
+ return;
+ }
+ icon.visible = visibility;
+ handleSet(index, icon);
+ }
+
+ @Override
+ public void removeIcon(int index) {
+ if (getIcon(index) == null) {
+ return;
+ }
+ mIconLogger.onIconHidden(getSlot(index));
+ super.removeIcon(index);
+ int viewIndex = getViewIndex(index);
+ mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex));
+ }
+
+ @Override
+ public void setIcon(int index, StatusBarIcon icon) {
+ if (icon == null) {
+ removeIcon(index);
+ return;
+ }
+ boolean isNew = getIcon(index) == null;
+ super.setIcon(index, icon);
+ if (isNew) {
+ addSystemIcon(index, icon);
+ } else {
+ handleSet(index, icon);
+ }
+ }
+
+ private void handleSet(int index, StatusBarIcon icon) {
+ int viewIndex = getViewIndex(index);
+ mIconLogger.onIconVisibility(getSlot(index), icon.visible);
+ mIconGroups.forEach(l -> l.onSetIcon(viewIndex, icon));
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ // TODO: Dump info about all icon groups?
+ ViewGroup statusIcons = mIconGroups.get(0).mGroup;
+ int N = statusIcons.getChildCount();
+ pw.println(" icon views: " + N);
+ for (int i = 0; i < N; i++) {
+ StatusBarIconView ic = (StatusBarIconView) statusIcons.getChildAt(i);
+ pw.println(" [" + i + "] icon=" + ic);
+ }
+ super.dump(pw);
+ }
+
+ public void dispatchDemoCommand(String command, Bundle args) {
+ if (mDemoStatusIcons == null) {
+ // TODO: Rework how we handle demo mode.
+ int iconSize = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_icon_size);
+ mDemoStatusIcons = new DemoStatusIcons((LinearLayout) mIconGroups.get(0).mGroup,
+ iconSize);
+ }
+ mDemoStatusIcons.dispatchDemoCommand(command, args);
+ }
+
+ @Override
+ public void onDensityOrFontScaleChanged() {
+ loadDimens();
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarIconList.java b/com/android/systemui/statusbar/phone/StatusBarIconList.java
new file mode 100644
index 0000000..f600908
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarIconList.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2015 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 com.android.internal.statusbar.StatusBarIcon;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class StatusBarIconList {
+ protected ArrayList<String> mSlots = new ArrayList<>();
+ protected ArrayList<StatusBarIcon> mIcons = new ArrayList<>();
+
+ public StatusBarIconList(String[] slots) {
+ final int N = slots.length;
+ for (int i=0; i < N; i++) {
+ mSlots.add(slots[i]);
+ mIcons.add(null);
+ }
+ }
+
+ public int getSlotIndex(String slot) {
+ final int N = mSlots.size();
+ for (int i=0; i<N; i++) {
+ if (slot.equals(mSlots.get(i))) {
+ return i;
+ }
+ }
+ // Auto insert new items at the beginning.
+ mSlots.add(0, slot);
+ mIcons.add(0, null);
+ return 0;
+ }
+
+ public int size() {
+ return mSlots.size();
+ }
+
+ public void setIcon(int index, StatusBarIcon icon) {
+ mIcons.set(index, icon);
+ }
+
+ public void removeIcon(int index) {
+ mIcons.set(index, null);
+ }
+
+ public String getSlot(int index) {
+ return mSlots.get(index);
+ }
+
+ public StatusBarIcon getIcon(int index) {
+ return mIcons.get(index);
+ }
+
+ public int getViewIndex(int index) {
+ int count = 0;
+ for (int i = 0; i < index; i++) {
+ if (mIcons.get(i) != null) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public void dump(PrintWriter pw) {
+ final int N = mSlots.size();
+ pw.println(" icon slots: " + N);
+ for (int i=0; i<N; i++) {
+ pw.printf(" %2d: (%s) %s\n", i, mSlots.get(i), mIcons.get(i));
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
new file mode 100644
index 0000000..bbce751
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -0,0 +1,723 @@
+/*
+ * 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 static com.android.keyguard.KeyguardHostView.OnDismissAction;
+import static com.android.systemui.statusbar.phone.FingerprintUnlockController.MODE_WAKE_AND_UNLOCK;
+import static com.android.systemui.statusbar.phone.FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
+
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewRootImpl;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.LatencyTracker;
+import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.Dependency;
+import com.android.systemui.SystemUIFactory;
+import com.android.systemui.keyguard.DismissCallbackRegistry;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.RemoteInputController;
+
+import java.util.ArrayList;
+
+/**
+ * Manages creating, showing, hiding and resetting the keyguard within the status bar. Calls back
+ * via {@link ViewMediatorCallback} to poke the wake lock and report that the keyguard is done,
+ * which is in turn, reported to this class by the current
+ * {@link com.android.keyguard.KeyguardViewBase}.
+ */
+public class StatusBarKeyguardViewManager implements RemoteInputController.Callback {
+
+ // When hiding the Keyguard with timing supplied from WindowManager, better be early than late.
+ private static final long HIDE_TIMING_CORRECTION_MS = - 16 * 3;
+
+ // Delay for showing the navigation bar when the bouncer appears. This should be kept in sync
+ // with the appear animations of the PIN/pattern/password views.
+ private static final long NAV_BAR_SHOW_DELAY_BOUNCER = 320;
+
+ private static final long WAKE_AND_UNLOCK_SCRIM_FADEOUT_DURATION_MS = 200;
+
+ // Duration of the Keyguard dismissal animation in case the user is currently locked. This is to
+ // make everything a bit slower to bridge a gap until the user is unlocked and home screen has
+ // dranw its first frame.
+ private static final long KEYGUARD_DISMISS_DURATION_LOCKED = 2000;
+
+ private static String TAG = "StatusBarKeyguardViewManager";
+
+ protected final Context mContext;
+ private final StatusBarWindowManager mStatusBarWindowManager;
+ private final boolean mDisplayBlanksAfterDoze;
+
+ protected LockPatternUtils mLockPatternUtils;
+ protected ViewMediatorCallback mViewMediatorCallback;
+ protected StatusBar mStatusBar;
+ private ScrimController mScrimController;
+ private FingerprintUnlockController mFingerprintUnlockController;
+
+ private ViewGroup mContainer;
+
+ private boolean mScreenTurnedOn;
+ protected KeyguardBouncer mBouncer;
+ protected boolean mShowing;
+ protected boolean mOccluded;
+ protected boolean mRemoteInputActive;
+ private boolean mDozing;
+
+ protected boolean mFirstUpdate = true;
+ protected boolean mLastShowing;
+ protected boolean mLastOccluded;
+ private boolean mLastBouncerShowing;
+ private boolean mLastBouncerDismissible;
+ protected boolean mLastRemoteInputActive;
+ private boolean mLastDozing;
+ private boolean mLastDeferScrimFadeOut;
+ private int mLastFpMode;
+
+ private OnDismissAction mAfterKeyguardGoneAction;
+ private final ArrayList<Runnable> mAfterKeyguardGoneRunnables = new ArrayList<>();
+ private boolean mDeferScrimFadeOut;
+
+ // Dismiss action to be launched when we stop dozing or the keyguard is gone.
+ private DismissWithActionRequest mPendingWakeupAction;
+
+ private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
+ new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onEmergencyCallAction() {
+
+ // Since we won't get a setOccluded call we have to reset the view manually such that
+ // the bouncer goes away.
+ if (mOccluded) {
+ reset(true /* hideBouncerWhenShowing */);
+ }
+ }
+ };
+
+ public StatusBarKeyguardViewManager(Context context, ViewMediatorCallback callback,
+ LockPatternUtils lockPatternUtils) {
+ mContext = context;
+ mViewMediatorCallback = callback;
+ mLockPatternUtils = lockPatternUtils;
+ mStatusBarWindowManager = Dependency.get(StatusBarWindowManager.class);
+ KeyguardUpdateMonitor.getInstance(context).registerCallback(mUpdateMonitorCallback);
+ mDisplayBlanksAfterDoze = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_displayBlanksAfterDoze);
+ }
+
+ public void registerStatusBar(StatusBar statusBar,
+ ViewGroup container,
+ ScrimController scrimController,
+ FingerprintUnlockController fingerprintUnlockController,
+ DismissCallbackRegistry dismissCallbackRegistry) {
+ mStatusBar = statusBar;
+ mContainer = container;
+ mScrimController = scrimController;
+ mFingerprintUnlockController = fingerprintUnlockController;
+ mBouncer = SystemUIFactory.getInstance().createKeyguardBouncer(mContext,
+ mViewMediatorCallback, mLockPatternUtils, container, dismissCallbackRegistry);
+ }
+
+ /**
+ * Show the keyguard. Will handle creating and attaching to the view manager
+ * lazily.
+ */
+ public void show(Bundle options) {
+ mShowing = true;
+ mStatusBarWindowManager.setKeyguardShowing(true);
+ mScrimController.abortKeyguardFadingOut();
+ reset(true /* hideBouncerWhenShowing */);
+ }
+
+ /**
+ * Shows the notification keyguard or the bouncer depending on
+ * {@link KeyguardBouncer#needsFullscreenBouncer()}.
+ */
+ protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
+ if (mBouncer.needsFullscreenBouncer() && !mDozing) {
+ // The keyguard might be showing (already). So we need to hide it.
+ mStatusBar.hideKeyguard();
+ mBouncer.show(true /* resetSecuritySelection */);
+ } else {
+ mStatusBar.showKeyguard();
+ if (hideBouncerWhenShowing) {
+ hideBouncer(false /* destroyView */);
+ mBouncer.prepare();
+ }
+ }
+ updateStates();
+ }
+
+ private void hideBouncer(boolean destroyView) {
+ mBouncer.hide(destroyView);
+ cancelPendingWakeupAction();
+ }
+
+ private void showBouncer() {
+ if (mShowing) {
+ mBouncer.show(false /* resetSecuritySelection */);
+ }
+ updateStates();
+ }
+
+ public void dismissWithAction(OnDismissAction r, Runnable cancelAction,
+ boolean afterKeyguardGone) {
+ if (mShowing) {
+ cancelPendingWakeupAction();
+ // If we're dozing, this needs to be delayed until after we wake up - unless we're
+ // wake-and-unlocking, because there dozing will last until the end of the transition.
+ if (mDozing && !isWakeAndUnlocking()) {
+ mPendingWakeupAction = new DismissWithActionRequest(
+ r, cancelAction, afterKeyguardGone);
+ return;
+ }
+
+ if (!afterKeyguardGone) {
+ mBouncer.showWithDismissAction(r, cancelAction);
+ } else {
+ mAfterKeyguardGoneAction = r;
+ mBouncer.show(false /* resetSecuritySelection */);
+ }
+ }
+ updateStates();
+ }
+
+ private boolean isWakeAndUnlocking() {
+ int mode = mFingerprintUnlockController.getMode();
+ return mode == MODE_WAKE_AND_UNLOCK || mode == MODE_WAKE_AND_UNLOCK_PULSING;
+ }
+
+ /**
+ * Adds a {@param runnable} to be executed after Keyguard is gone.
+ */
+ public void addAfterKeyguardGoneRunnable(Runnable runnable) {
+ mAfterKeyguardGoneRunnables.add(runnable);
+ }
+
+ /**
+ * Reset the state of the view.
+ */
+ public void reset(boolean hideBouncerWhenShowing) {
+ if (mShowing) {
+ if (mOccluded && !mDozing) {
+ mStatusBar.hideKeyguard();
+ mStatusBar.stopWaitingForKeyguardExit();
+ if (hideBouncerWhenShowing || mBouncer.needsFullscreenBouncer()) {
+ hideBouncer(false /* destroyView */);
+ }
+ } else {
+ showBouncerOrKeyguard(hideBouncerWhenShowing);
+ }
+ KeyguardUpdateMonitor.getInstance(mContext).sendKeyguardReset();
+ updateStates();
+ }
+ }
+
+ public void onStartedGoingToSleep() {
+ // TODO: remove
+ }
+
+ public void onFinishedGoingToSleep() {
+ mBouncer.onScreenTurnedOff();
+ }
+
+ public void onStartedWakingUp() {
+ // TODO: remove
+ }
+
+ public void onScreenTurningOn() {
+ // TODO: remove
+ }
+
+ public void onScreenTurnedOn() {
+ Trace.beginSection("StatusBarKeyguardViewManager#onScreenTurnedOn");
+ mScreenTurnedOn = true;
+ if (mDeferScrimFadeOut) {
+ mDeferScrimFadeOut = false;
+ animateScrimControllerKeyguardFadingOut(0, WAKE_AND_UNLOCK_SCRIM_FADEOUT_DURATION_MS,
+ true /* skipFirstFrame */);
+ updateStates();
+ }
+ Trace.endSection();
+ }
+
+ @Override
+ public void onRemoteInputActive(boolean active) {
+ mRemoteInputActive = active;
+ updateStates();
+ }
+
+ public void setDozing(boolean dozing) {
+ if (mDozing != dozing) {
+ mDozing = dozing;
+ if (dozing || mBouncer.needsFullscreenBouncer() || mOccluded) {
+ reset(dozing /* hideBouncerWhenShowing */);
+ }
+ updateStates();
+
+ if (!dozing) {
+ launchPendingWakeupAction();
+ }
+ }
+ }
+
+ public void onScreenTurnedOff() {
+ mScreenTurnedOn = false;
+ }
+
+ public void notifyDeviceWakeUpRequested() {
+ // TODO: remove
+ }
+
+ public void setNeedsInput(boolean needsInput) {
+ mStatusBarWindowManager.setKeyguardNeedsInput(needsInput);
+ }
+
+ public boolean isUnlockWithWallpaper() {
+ return mStatusBarWindowManager.isShowingWallpaper();
+ }
+
+ public void setOccluded(boolean occluded, boolean animate) {
+ mStatusBar.setOccluded(occluded);
+ if (occluded && !mOccluded && mShowing) {
+ if (mStatusBar.isInLaunchTransition()) {
+ mOccluded = true;
+ mStatusBar.fadeKeyguardAfterLaunchTransition(null /* beforeFading */,
+ new Runnable() {
+ @Override
+ public void run() {
+ mStatusBarWindowManager.setKeyguardOccluded(mOccluded);
+ reset(true /* hideBouncerWhenShowing */);
+ }
+ });
+ return;
+ }
+ }
+ boolean isOccluding = !mOccluded && occluded;
+ mOccluded = occluded;
+ if (mShowing) {
+ mStatusBar.updateMediaMetaData(false, animate && !occluded);
+ }
+ mStatusBarWindowManager.setKeyguardOccluded(occluded);
+
+ // setDozing(false) will call reset once we stop dozing.
+ if (!mDozing) {
+ // If Keyguard is reshown, don't hide the bouncer as it might just have been requested
+ // by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
+ reset(isOccluding /* hideBouncerWhenShowing*/);
+ }
+ if (animate && !occluded && mShowing) {
+ mStatusBar.animateKeyguardUnoccluding();
+ }
+ }
+
+ public boolean isOccluded() {
+ return mOccluded;
+ }
+
+ /**
+ * Starts the animation before we dismiss Keyguard, i.e. an disappearing animation on the
+ * security view of the bouncer.
+ *
+ * @param finishRunnable the runnable to be run after the animation finished, or {@code null} if
+ * no action should be run
+ */
+ public void startPreHideAnimation(Runnable finishRunnable) {
+ if (mBouncer.isShowing()) {
+ mBouncer.startPreHideAnimation(finishRunnable);
+ } else if (finishRunnable != null) {
+ finishRunnable.run();
+ }
+ }
+
+ /**
+ * Hides the keyguard view
+ */
+ public void hide(long startTime, long fadeoutDuration) {
+ mShowing = false;
+ launchPendingWakeupAction();
+
+ if (KeyguardUpdateMonitor.getInstance(mContext).needsSlowUnlockTransition()) {
+ fadeoutDuration = KEYGUARD_DISMISS_DURATION_LOCKED;
+ }
+ long uptimeMillis = SystemClock.uptimeMillis();
+ long delay = Math.max(0, startTime + HIDE_TIMING_CORRECTION_MS - uptimeMillis);
+
+ if (mStatusBar.isInLaunchTransition() ) {
+ mStatusBar.fadeKeyguardAfterLaunchTransition(new Runnable() {
+ @Override
+ public void run() {
+ mStatusBarWindowManager.setKeyguardShowing(false);
+ mStatusBarWindowManager.setKeyguardFadingAway(true);
+ hideBouncer(true /* destroyView */);
+ updateStates();
+ mScrimController.animateKeyguardFadingOut(
+ StatusBar.FADE_KEYGUARD_START_DELAY,
+ StatusBar.FADE_KEYGUARD_DURATION, null,
+ false /* skipFirstFrame */);
+ }
+ }, new Runnable() {
+ @Override
+ public void run() {
+ mStatusBar.hideKeyguard();
+ mStatusBarWindowManager.setKeyguardFadingAway(false);
+ mViewMediatorCallback.keyguardGone();
+ executeAfterKeyguardGoneAction();
+ }
+ });
+ } else {
+ executeAfterKeyguardGoneAction();
+ boolean wakeUnlockPulsing =
+ mFingerprintUnlockController.getMode() == MODE_WAKE_AND_UNLOCK_PULSING;
+ if (wakeUnlockPulsing) {
+ delay = 0;
+ fadeoutDuration = 240;
+ }
+ mStatusBar.setKeyguardFadingAway(startTime, delay, fadeoutDuration);
+ mFingerprintUnlockController.startKeyguardFadingAway();
+ hideBouncer(true /* destroyView */);
+ if (wakeUnlockPulsing) {
+ mStatusBarWindowManager.setKeyguardFadingAway(true);
+ mStatusBar.fadeKeyguardWhilePulsing();
+ animateScrimControllerKeyguardFadingOut(delay, fadeoutDuration,
+ mStatusBar::hideKeyguard, false /* skipFirstFrame */);
+ } else {
+ mFingerprintUnlockController.startKeyguardFadingAway();
+ mStatusBar.setKeyguardFadingAway(startTime, delay, fadeoutDuration);
+ boolean staying = mStatusBar.hideKeyguard();
+ if (!staying) {
+ mStatusBarWindowManager.setKeyguardFadingAway(true);
+ if (mFingerprintUnlockController.getMode() == MODE_WAKE_AND_UNLOCK) {
+ boolean turnedOnSinceAuth =
+ mFingerprintUnlockController.hasScreenTurnedOnSinceAuthenticating();
+ if (!mScreenTurnedOn || mDisplayBlanksAfterDoze && !turnedOnSinceAuth) {
+ // Not ready to animate yet; either because the screen is not on yet,
+ // or it is on but will turn off before waking out of doze.
+ mDeferScrimFadeOut = true;
+ } else {
+
+ // Screen is already on, don't defer with fading out.
+ animateScrimControllerKeyguardFadingOut(0,
+ WAKE_AND_UNLOCK_SCRIM_FADEOUT_DURATION_MS,
+ true /* skipFirstFrame */);
+ }
+ } else {
+ animateScrimControllerKeyguardFadingOut(delay, fadeoutDuration,
+ false /* skipFirstFrame */);
+ }
+ } else {
+ mScrimController.animateGoingToFullShade(delay, fadeoutDuration);
+ mStatusBar.finishKeyguardFadingAway();
+ mFingerprintUnlockController.finishKeyguardFadingAway();
+ }
+ }
+ updateStates();
+ mStatusBarWindowManager.setKeyguardShowing(false);
+ mViewMediatorCallback.keyguardGone();
+ }
+ }
+
+ public void onDensityOrFontScaleChanged() {
+ hideBouncer(true /* destroyView */);
+ }
+
+ public void onOverlayChanged() {
+ hideBouncer(true /* destroyView */);
+ mBouncer.prepare();
+ }
+
+ private void animateScrimControllerKeyguardFadingOut(long delay, long duration,
+ boolean skipFirstFrame) {
+ animateScrimControllerKeyguardFadingOut(delay, duration, null /* endRunnable */,
+ skipFirstFrame);
+ }
+
+ private void animateScrimControllerKeyguardFadingOut(long delay, long duration,
+ final Runnable endRunnable, boolean skipFirstFrame) {
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "Fading out", 0);
+ mScrimController.animateKeyguardFadingOut(delay, duration, new Runnable() {
+ @Override
+ public void run() {
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
+ mContainer.postDelayed(() -> mStatusBarWindowManager.setKeyguardFadingAway(false),
+ 100);
+ mStatusBar.finishKeyguardFadingAway();
+ mFingerprintUnlockController.finishKeyguardFadingAway();
+ WindowManagerGlobal.getInstance().trimMemory(
+ ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, "Fading out", 0);
+ }
+ }, skipFirstFrame);
+ if (mFingerprintUnlockController.getMode() == MODE_WAKE_AND_UNLOCK
+ && LatencyTracker.isEnabled(mContext)) {
+ DejankUtils.postAfterTraversal(() ->
+ LatencyTracker.getInstance(mContext).onActionEnd(
+ LatencyTracker.ACTION_FINGERPRINT_WAKE_AND_UNLOCK));
+ }
+ }
+
+ private void executeAfterKeyguardGoneAction() {
+ if (mAfterKeyguardGoneAction != null) {
+ mAfterKeyguardGoneAction.onDismiss();
+ mAfterKeyguardGoneAction = null;
+ }
+ for (int i = 0; i < mAfterKeyguardGoneRunnables.size(); i++) {
+ mAfterKeyguardGoneRunnables.get(i).run();
+ }
+ mAfterKeyguardGoneRunnables.clear();
+ }
+
+ /**
+ * Dismisses the keyguard by going to the next screen or making it gone.
+ */
+ public void dismissAndCollapse() {
+ mStatusBar.executeRunnableDismissingKeyguard(null, null, true, false, true);
+ }
+
+ public void dismiss() {
+ showBouncer();
+ }
+
+ /**
+ * WARNING: This method might cause Binder calls.
+ */
+ public boolean isSecure() {
+ return mBouncer.isSecure();
+ }
+
+ /**
+ * @return Whether the keyguard is showing
+ */
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ /**
+ * Notifies this manager that the back button has been pressed.
+ *
+ * @return whether the back press has been handled
+ */
+ public boolean onBackPressed() {
+ if (mBouncer.isShowing()) {
+ mStatusBar.endAffordanceLaunch();
+ reset(true /* hideBouncerWhenShowing */);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean isBouncerShowing() {
+ return mBouncer.isShowing();
+ }
+
+ private long getNavBarShowDelay() {
+ if (mStatusBar.isKeyguardFadingAway()) {
+ return mStatusBar.getKeyguardFadingAwayDelay();
+ } else if (mBouncer.isShowing()) {
+ return NAV_BAR_SHOW_DELAY_BOUNCER;
+ } else {
+ // No longer dozing, or remote input is active. No delay.
+ return 0;
+ }
+ }
+
+ private Runnable mMakeNavigationBarVisibleRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mStatusBar.getNavigationBarView().getRootView().setVisibility(View.VISIBLE);
+ }
+ };
+
+ protected void updateStates() {
+ int vis = mContainer.getSystemUiVisibility();
+ boolean showing = mShowing;
+ boolean occluded = mOccluded;
+ boolean bouncerShowing = mBouncer.isShowing();
+ boolean bouncerDismissible = !mBouncer.isFullscreenBouncer();
+ boolean remoteInputActive = mRemoteInputActive;
+
+ if ((bouncerDismissible || !showing || remoteInputActive) !=
+ (mLastBouncerDismissible || !mLastShowing || mLastRemoteInputActive)
+ || mFirstUpdate) {
+ if (bouncerDismissible || !showing || remoteInputActive) {
+ mContainer.setSystemUiVisibility(vis & ~View.STATUS_BAR_DISABLE_BACK);
+ } else {
+ mContainer.setSystemUiVisibility(vis | View.STATUS_BAR_DISABLE_BACK);
+ }
+ }
+
+ boolean navBarVisible = isNavBarVisible();
+ boolean lastNavBarVisible = getLastNavBarVisible();
+ if (navBarVisible != lastNavBarVisible || mFirstUpdate) {
+ if (mStatusBar.getNavigationBarView() != null) {
+ if (navBarVisible) {
+ long delay = getNavBarShowDelay();
+ if (delay == 0) {
+ mMakeNavigationBarVisibleRunnable.run();
+ } else {
+ mContainer.postOnAnimationDelayed(mMakeNavigationBarVisibleRunnable,
+ delay);
+ }
+ } else {
+ mContainer.removeCallbacks(mMakeNavigationBarVisibleRunnable);
+ mStatusBar.getNavigationBarView().getRootView().setVisibility(View.GONE);
+ }
+ }
+ }
+
+ if (bouncerShowing != mLastBouncerShowing || mFirstUpdate) {
+ mStatusBarWindowManager.setBouncerShowing(bouncerShowing);
+ mStatusBar.setBouncerShowing(bouncerShowing);
+ mScrimController.setBouncerShowing(bouncerShowing);
+ }
+
+ KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+ if ((showing && !occluded) != (mLastShowing && !mLastOccluded) || mFirstUpdate) {
+ updateMonitor.onKeyguardVisibilityChanged(showing && !occluded);
+ }
+ if (bouncerShowing != mLastBouncerShowing || mFirstUpdate) {
+ updateMonitor.sendKeyguardBouncerChanged(bouncerShowing);
+ }
+
+ mFirstUpdate = false;
+ mLastShowing = showing;
+ mLastOccluded = occluded;
+ mLastBouncerShowing = bouncerShowing;
+ mLastBouncerDismissible = bouncerDismissible;
+ mLastRemoteInputActive = remoteInputActive;
+ mLastDozing = mDozing;
+ mLastDeferScrimFadeOut = mDeferScrimFadeOut;
+ mLastFpMode = mFingerprintUnlockController.getMode();
+ mStatusBar.onKeyguardViewManagerStatesUpdated();
+ }
+
+ /**
+ * @return Whether the navigation bar should be made visible based on the current state.
+ */
+ protected boolean isNavBarVisible() {
+ int fpMode = mFingerprintUnlockController.getMode();
+ boolean keyguardShowing = mShowing && !mOccluded;
+ boolean hideWhileDozing = mDozing && fpMode != MODE_WAKE_AND_UNLOCK_PULSING;
+ return (!keyguardShowing && !hideWhileDozing || mBouncer.isShowing()
+ || mRemoteInputActive) && !mDeferScrimFadeOut;
+ }
+
+ /**
+ * @return Whether the navigation bar was made visible based on the last known state.
+ */
+ protected boolean getLastNavBarVisible() {
+ boolean keyguardShowing = mLastShowing && !mLastOccluded;
+ boolean hideWhileDozing = mLastDozing && mLastFpMode != MODE_WAKE_AND_UNLOCK_PULSING;
+ return (!keyguardShowing && !hideWhileDozing || mLastBouncerShowing
+ || mLastRemoteInputActive) && !mLastDeferScrimFadeOut;
+ }
+
+ public boolean shouldDismissOnMenuPressed() {
+ return mBouncer.shouldDismissOnMenuPressed();
+ }
+
+ public boolean interceptMediaKey(KeyEvent event) {
+ return mBouncer.interceptMediaKey(event);
+ }
+
+ public void readyForKeyguardDone() {
+ mViewMediatorCallback.readyForKeyguardDone();
+ }
+
+ public boolean shouldDisableWindowAnimationsForUnlock() {
+ return mStatusBar.isInLaunchTransition();
+ }
+
+ public boolean isGoingToNotificationShade() {
+ return mStatusBar.isGoingToNotificationShade();
+ }
+
+ public boolean isSecure(int userId) {
+ return mBouncer.isSecure() || mLockPatternUtils.isSecure(userId);
+ }
+
+ public void keyguardGoingAway() {
+ mStatusBar.keyguardGoingAway();
+ }
+
+ public void animateCollapsePanels(float speedUpFactor) {
+ mStatusBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */,
+ false /* delayed */, speedUpFactor);
+ }
+
+ /**
+ * Notifies that the user has authenticated by other means than using the bouncer, for example,
+ * fingerprint.
+ */
+ public void notifyKeyguardAuthenticated(boolean strongAuth) {
+ mBouncer.notifyKeyguardAuthenticated(strongAuth);
+ }
+
+ public void showBouncerMessage(String message, int color) {
+ mBouncer.showMessage(message, color);
+ }
+
+ public ViewRootImpl getViewRootImpl() {
+ return mStatusBar.getStatusBarView().getViewRootImpl();
+ }
+
+ public void launchPendingWakeupAction() {
+ DismissWithActionRequest request = mPendingWakeupAction;
+ mPendingWakeupAction = null;
+ if (request != null) {
+ if (mShowing) {
+ dismissWithAction(request.dismissAction, request.cancelAction,
+ request.afterKeyguardGone);
+ } else if (request.dismissAction != null) {
+ request.dismissAction.onDismiss();
+ }
+ }
+ }
+
+ public void cancelPendingWakeupAction() {
+ DismissWithActionRequest request = mPendingWakeupAction;
+ mPendingWakeupAction = null;
+ if (request != null && request.cancelAction != null) {
+ request.cancelAction.run();
+ }
+ }
+
+ private static class DismissWithActionRequest {
+ final OnDismissAction dismissAction;
+ final Runnable cancelAction;
+ final boolean afterKeyguardGone;
+
+ DismissWithActionRequest(OnDismissAction dismissAction, Runnable cancelAction,
+ boolean afterKeyguardGone) {
+ this.dismissAction = dismissAction;
+ this.cancelAction = cancelAction;
+ this.afterKeyguardGone = afterKeyguardGone;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
new file mode 100644
index 0000000..ed96b41
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
@@ -0,0 +1,484 @@
+/*
+ * 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.app.ActivityManager;
+import android.app.IActivityManager;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.keyguard.R;
+import com.android.systemui.Dumpable;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.statusbar.RemoteInputController;
+import com.android.systemui.statusbar.StatusBarState;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+
+/**
+ * Encapsulates all logic for the status bar window state management.
+ */
+public class StatusBarWindowManager implements RemoteInputController.Callback, Dumpable {
+
+ private static final String TAG = "StatusBarWindowManager";
+
+ private final Context mContext;
+ private final WindowManager mWindowManager;
+ private final IActivityManager mActivityManager;
+ private View mStatusBarView;
+ private WindowManager.LayoutParams mLp;
+ private WindowManager.LayoutParams mLpChanged;
+ private boolean mHasTopUi;
+ private boolean mHasTopUiChanged;
+ private int mBarHeight;
+ private final boolean mKeyguardScreenRotation;
+ private float mScreenBrightnessDoze;
+ private final State mCurrentState = new State();
+ private OtherwisedCollapsedListener mListener;
+
+ public StatusBarWindowManager(Context context) {
+ mContext = context;
+ mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ mActivityManager = ActivityManager.getService();
+ mKeyguardScreenRotation = shouldEnableKeyguardScreenRotation();
+ mScreenBrightnessDoze = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_screenBrightnessDoze) / 255f;
+ }
+
+ private boolean shouldEnableKeyguardScreenRotation() {
+ Resources res = mContext.getResources();
+ return SystemProperties.getBoolean("lockscreen.rot_override", false)
+ || res.getBoolean(R.bool.config_enableLockScreenRotation);
+ }
+
+ /**
+ * Adds the status bar view to the window manager.
+ *
+ * @param statusBarView The view to add.
+ * @param barHeight The height of the status bar in collapsed state.
+ */
+ public void add(View statusBarView, int barHeight) {
+
+ // Now that the status bar window encompasses the sliding panel and its
+ // translucent backdrop, the entire thing is made TRANSLUCENT and is
+ // hardware-accelerated.
+ mLp = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ barHeight,
+ WindowManager.LayoutParams.TYPE_STATUS_BAR,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+ | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+ | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ PixelFormat.TRANSLUCENT);
+ mLp.token = new Binder();
+ mLp.gravity = Gravity.TOP;
+ mLp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+ mLp.setTitle("StatusBar");
+ mLp.packageName = mContext.getPackageName();
+ mStatusBarView = statusBarView;
+ mBarHeight = barHeight;
+ mWindowManager.addView(mStatusBarView, mLp);
+ mLpChanged = new WindowManager.LayoutParams();
+ mLpChanged.copyFrom(mLp);
+ }
+
+ public void setDozeScreenBrightness(int value) {
+ mScreenBrightnessDoze = value / 255f;
+ }
+
+ public void setKeyguardDark(boolean dark) {
+ int vis = mStatusBarView.getSystemUiVisibility();
+ if (dark) {
+ vis = vis | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+ vis = vis | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+ } else {
+ vis = vis & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+ vis = vis & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+ }
+ mStatusBarView.setSystemUiVisibility(vis);
+ }
+
+ private void applyKeyguardFlags(State state) {
+ if (state.keyguardShowing) {
+ mLpChanged.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
+ } else {
+ mLpChanged.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
+ }
+
+ if (state.keyguardShowing && !state.backdropShowing && !state.dozing) {
+ mLpChanged.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+ } else {
+ mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+ }
+ }
+
+ private void adjustScreenOrientation(State state) {
+ if (state.isKeyguardShowingAndNotOccluded() || state.dozing) {
+ if (mKeyguardScreenRotation) {
+ mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER;
+ } else {
+ mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+ }
+ } else {
+ mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ }
+
+ private void applyFocusableFlag(State state) {
+ boolean panelFocusable = state.statusBarFocusable && state.panelExpanded;
+ if (state.bouncerShowing && (state.keyguardOccluded || state.keyguardNeedsInput)
+ || StatusBar.ENABLE_REMOTE_INPUT && state.remoteInputActive) {
+ mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ } else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) {
+ mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ mLpChanged.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ } else {
+ mLpChanged.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ }
+
+ mLpChanged.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+ }
+
+ private void applyHeight(State state) {
+ boolean expanded = isExpanded(state);
+ if (state.forcePluginOpen) {
+ mListener.setWouldOtherwiseCollapse(expanded);
+ expanded = true;
+ }
+ if (expanded) {
+ mLpChanged.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ } else {
+ mLpChanged.height = mBarHeight;
+ }
+ }
+
+ private boolean isExpanded(State state) {
+ return !state.forceCollapsed && (state.isKeyguardShowingAndNotOccluded()
+ || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing
+ || state.headsUpShowing || state.scrimsVisible);
+ }
+
+ private void applyFitsSystemWindows(State state) {
+ boolean fitsSystemWindows = !state.isKeyguardShowingAndNotOccluded();
+ if (mStatusBarView.getFitsSystemWindows() != fitsSystemWindows) {
+ mStatusBarView.setFitsSystemWindows(fitsSystemWindows);
+ mStatusBarView.requestApplyInsets();
+ }
+ }
+
+ private void applyUserActivityTimeout(State state) {
+ if (state.isKeyguardShowingAndNotOccluded()
+ && state.statusBarState == StatusBarState.KEYGUARD
+ && !state.qsExpanded) {
+ mLpChanged.userActivityTimeout = KeyguardViewMediator.AWAKE_INTERVAL_DEFAULT_MS;
+ } else {
+ mLpChanged.userActivityTimeout = -1;
+ }
+ }
+
+ private void applyInputFeatures(State state) {
+ if (state.isKeyguardShowingAndNotOccluded()
+ && state.statusBarState == StatusBarState.KEYGUARD
+ && !state.qsExpanded && !state.forceUserActivity) {
+ mLpChanged.inputFeatures |=
+ WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
+ } else {
+ mLpChanged.inputFeatures &=
+ ~WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
+ }
+ }
+
+ private void apply(State state) {
+ applyKeyguardFlags(state);
+ applyForceStatusBarVisibleFlag(state);
+ applyFocusableFlag(state);
+ adjustScreenOrientation(state);
+ applyHeight(state);
+ applyUserActivityTimeout(state);
+ applyInputFeatures(state);
+ applyFitsSystemWindows(state);
+ applyModalFlag(state);
+ applyBrightness(state);
+ applyHasTopUi(state);
+ applySleepToken(state);
+ if (mLp.copyFrom(mLpChanged) != 0) {
+ mWindowManager.updateViewLayout(mStatusBarView, mLp);
+ }
+ if (mHasTopUi != mHasTopUiChanged) {
+ try {
+ mActivityManager.setHasTopUi(mHasTopUiChanged);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to call setHasTopUi", e);
+ }
+ mHasTopUi = mHasTopUiChanged;
+ }
+ }
+
+ private void applyForceStatusBarVisibleFlag(State state) {
+ if (state.forceStatusBarVisible) {
+ mLpChanged.privateFlags |= WindowManager
+ .LayoutParams.PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT;
+ } else {
+ mLpChanged.privateFlags &= ~WindowManager
+ .LayoutParams.PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT;
+ }
+ }
+
+ private void applyModalFlag(State state) {
+ if (state.headsUpShowing) {
+ mLpChanged.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+ } else {
+ mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+ }
+ }
+
+ private void applyBrightness(State state) {
+ if (state.forceDozeBrightness) {
+ mLpChanged.screenBrightness = mScreenBrightnessDoze;
+ } else {
+ mLpChanged.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
+ }
+ }
+
+ private void applyHasTopUi(State state) {
+ mHasTopUiChanged = isExpanded(state);
+ }
+
+ private void applySleepToken(State state) {
+ if (state.dozing) {
+ mLpChanged.privateFlags |= LayoutParams.PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN;
+ } else {
+ mLpChanged.privateFlags &= ~LayoutParams.PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN;
+ }
+ }
+
+ public void setKeyguardShowing(boolean showing) {
+ mCurrentState.keyguardShowing = showing;
+ apply(mCurrentState);
+ }
+
+ public void setKeyguardOccluded(boolean occluded) {
+ mCurrentState.keyguardOccluded = occluded;
+ apply(mCurrentState);
+ }
+
+ public void setKeyguardNeedsInput(boolean needsInput) {
+ mCurrentState.keyguardNeedsInput = needsInput;
+ apply(mCurrentState);
+ }
+
+ public void setPanelVisible(boolean visible) {
+ mCurrentState.panelVisible = visible;
+ mCurrentState.statusBarFocusable = visible;
+ apply(mCurrentState);
+ }
+
+ public void setStatusBarFocusable(boolean focusable) {
+ mCurrentState.statusBarFocusable = focusable;
+ apply(mCurrentState);
+ }
+
+ public void setBouncerShowing(boolean showing) {
+ mCurrentState.bouncerShowing = showing;
+ apply(mCurrentState);
+ }
+
+ public void setBackdropShowing(boolean showing) {
+ mCurrentState.backdropShowing = showing;
+ apply(mCurrentState);
+ }
+
+ public void setKeyguardFadingAway(boolean keyguardFadingAway) {
+ mCurrentState.keyguardFadingAway = keyguardFadingAway;
+ apply(mCurrentState);
+ }
+
+ public void setQsExpanded(boolean expanded) {
+ mCurrentState.qsExpanded = expanded;
+ apply(mCurrentState);
+ }
+
+ public void setForceUserActivity(boolean forceUserActivity) {
+ mCurrentState.forceUserActivity = forceUserActivity;
+ apply(mCurrentState);
+ }
+
+ public void setScrimsVisible(boolean scrimsVisible) {
+ mCurrentState.scrimsVisible = scrimsVisible;
+ apply(mCurrentState);
+ }
+
+ public void setHeadsUpShowing(boolean showing) {
+ mCurrentState.headsUpShowing = showing;
+ apply(mCurrentState);
+ }
+
+ /**
+ * @param state The {@link StatusBarState} of the status bar.
+ */
+ public void setStatusBarState(int state) {
+ mCurrentState.statusBarState = state;
+ apply(mCurrentState);
+ }
+
+ public void setForceStatusBarVisible(boolean forceStatusBarVisible) {
+ mCurrentState.forceStatusBarVisible = forceStatusBarVisible;
+ apply(mCurrentState);
+ }
+
+ /**
+ * Force the window to be collapsed, even if it should theoretically be expanded.
+ * Used for when a heads-up comes in but we still need to wait for the touchable regions to
+ * be computed.
+ */
+ public void setForceWindowCollapsed(boolean force) {
+ mCurrentState.forceCollapsed = force;
+ apply(mCurrentState);
+ }
+
+ public void setPanelExpanded(boolean isExpanded) {
+ mCurrentState.panelExpanded = isExpanded;
+ apply(mCurrentState);
+ }
+
+ @Override
+ public void onRemoteInputActive(boolean remoteInputActive) {
+ mCurrentState.remoteInputActive = remoteInputActive;
+ apply(mCurrentState);
+ }
+
+ /**
+ * Set whether the screen brightness is forced to the value we use for doze mode by the status
+ * bar window.
+ */
+ public void setForceDozeBrightness(boolean forceDozeBrightness) {
+ mCurrentState.forceDozeBrightness = forceDozeBrightness;
+ apply(mCurrentState);
+ }
+
+ public void setDozing(boolean dozing) {
+ mCurrentState.dozing = dozing;
+ apply(mCurrentState);
+ }
+
+ public void setBarHeight(int barHeight) {
+ mBarHeight = barHeight;
+ apply(mCurrentState);
+ }
+
+ public void setForcePluginOpen(boolean forcePluginOpen) {
+ mCurrentState.forcePluginOpen = forcePluginOpen;
+ apply(mCurrentState);
+ }
+
+ public void setStateListener(OtherwisedCollapsedListener listener) {
+ mListener = listener;
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("StatusBarWindowManager state:");
+ pw.println(mCurrentState);
+ }
+
+ public boolean isShowingWallpaper() {
+ return !mCurrentState.backdropShowing;
+ }
+
+ private static class State {
+ boolean keyguardShowing;
+ boolean keyguardOccluded;
+ boolean keyguardNeedsInput;
+ boolean panelVisible;
+ boolean panelExpanded;
+ boolean statusBarFocusable;
+ boolean bouncerShowing;
+ boolean keyguardFadingAway;
+ boolean qsExpanded;
+ boolean headsUpShowing;
+ boolean forceStatusBarVisible;
+ boolean forceCollapsed;
+ boolean forceDozeBrightness;
+ boolean forceUserActivity;
+ boolean backdropShowing;
+
+ /**
+ * The {@link StatusBar} state from the status bar.
+ */
+ int statusBarState;
+
+ boolean remoteInputActive;
+ boolean forcePluginOpen;
+ boolean dozing;
+ boolean scrimsVisible;
+
+ private boolean isKeyguardShowingAndNotOccluded() {
+ return keyguardShowing && !keyguardOccluded;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ String newLine = "\n";
+ result.append("Window State {");
+ result.append(newLine);
+
+ Field[] fields = this.getClass().getDeclaredFields();
+
+ // Print field names paired with their values
+ for (Field field : fields) {
+ result.append(" ");
+ try {
+ result.append(field.getName());
+ result.append(": ");
+ //requires access to private field:
+ result.append(field.get(this));
+ } catch (IllegalAccessException ex) {
+ }
+ result.append(newLine);
+ }
+ result.append("}");
+
+ return result.toString();
+ }
+ }
+
+ /**
+ * Custom listener to pipe data back to plugins about whether or not the status bar would be
+ * collapsed if not for the plugin.
+ * TODO: Find cleaner way to do this.
+ */
+ public interface OtherwisedCollapsedListener {
+ void setWouldOtherwiseCollapse(boolean otherwiseCollapse);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/com/android/systemui/statusbar/phone/StatusBarWindowView.java
new file mode 100644
index 0000000..d7f11f7
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -0,0 +1,749 @@
+/*
+ * Copyright (C) 2012 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.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.LayoutRes;
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.session.MediaSessionLegacyHelper;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.InputDevice;
+import android.view.InputQueue;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.widget.FrameLayout;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.view.FloatingActionMode;
+import com.android.internal.widget.FloatingToolbar;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.DragDownHelper;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+
+public class StatusBarWindowView extends FrameLayout {
+ public static final String TAG = "StatusBarWindowView";
+ public static final boolean DEBUG = StatusBar.DEBUG;
+
+ private DragDownHelper mDragDownHelper;
+ private DoubleTapHelper mDoubleTapHelper;
+ private NotificationStackScrollLayout mStackScrollLayout;
+ private NotificationPanelView mNotificationPanel;
+ private View mBrightnessMirror;
+
+ private int mRightInset = 0;
+ private int mLeftInset = 0;
+
+ private StatusBar mService;
+ private final Paint mTransparentSrcPaint = new Paint();
+ private FalsingManager mFalsingManager;
+
+ // Implements the floating action mode for TextView's Cut/Copy/Past menu. Normally provided by
+ // DecorView, but since this is a special window we have to roll our own.
+ private View mFloatingActionModeOriginatingView;
+ private ActionMode mFloatingActionMode;
+ private FloatingToolbar mFloatingToolbar;
+ private ViewTreeObserver.OnPreDrawListener mFloatingToolbarPreDrawListener;
+ private boolean mTouchCancelled;
+ private boolean mTouchActive;
+
+ public StatusBarWindowView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setMotionEventSplittingEnabled(false);
+ mTransparentSrcPaint.setColor(0);
+ mTransparentSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ mFalsingManager = FalsingManager.getInstance(context);
+ mDoubleTapHelper = new DoubleTapHelper(this, active -> {}, () -> {
+ mService.wakeUpIfDozing(SystemClock.uptimeMillis(), this);
+ return true;
+ }, null, null);
+ }
+
+ @Override
+ protected boolean fitSystemWindows(Rect insets) {
+ if (getFitsSystemWindows()) {
+ boolean paddingChanged = insets.top != getPaddingTop()
+ || insets.bottom != getPaddingBottom();
+
+ // Super-special right inset handling, because scrims and backdrop need to ignore it.
+ if (insets.right != mRightInset || insets.left != mLeftInset) {
+ mRightInset = insets.right;
+ mLeftInset = insets.left;
+ applyMargins();
+ }
+ // Drop top inset, and pass through bottom inset.
+ if (paddingChanged) {
+ setPadding(0, 0, 0, 0);
+ }
+ insets.left = 0;
+ insets.top = 0;
+ insets.right = 0;
+ } else {
+ if (mRightInset != 0 || mLeftInset != 0) {
+ mRightInset = 0;
+ mLeftInset = 0;
+ applyMargins();
+ }
+ boolean changed = getPaddingLeft() != 0
+ || getPaddingRight() != 0
+ || getPaddingTop() != 0
+ || getPaddingBottom() != 0;
+ if (changed) {
+ setPadding(0, 0, 0, 0);
+ }
+ insets.top = 0;
+ }
+ return false;
+ }
+
+ private void applyMargins() {
+ final int N = getChildCount();
+ for (int i = 0; i < N; i++) {
+ View child = getChildAt(i);
+ if (child.getLayoutParams() instanceof LayoutParams) {
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.ignoreRightInset
+ && (lp.rightMargin != mRightInset || lp.leftMargin != mLeftInset)) {
+ lp.rightMargin = mRightInset;
+ lp.leftMargin = mLeftInset;
+ child.requestLayout();
+ }
+ }
+ }
+ }
+
+ @Override
+ public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected FrameLayout.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mStackScrollLayout = (NotificationStackScrollLayout) findViewById(
+ R.id.notification_stack_scroller);
+ mNotificationPanel = (NotificationPanelView) findViewById(R.id.notification_panel);
+ mBrightnessMirror = findViewById(R.id.brightness_mirror);
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ super.onViewAdded(child);
+ if (child.getId() == R.id.brightness_mirror) {
+ mBrightnessMirror = child;
+ }
+ }
+
+ public void setService(StatusBar service) {
+ mService = service;
+ setDragDownHelper(new DragDownHelper(getContext(), this, mStackScrollLayout, mService));
+ }
+
+ @VisibleForTesting
+ void setDragDownHelper(DragDownHelper dragDownHelper) {
+ mDragDownHelper = dragDownHelper;
+ }
+
+ @Override
+ protected void onAttachedToWindow () {
+ super.onAttachedToWindow();
+
+ // We need to ensure that our window doesn't suffer from overdraw which would normally
+ // occur if our window is translucent. Since we are drawing the whole window anyway with
+ // the scrim, we don't need the window to be cleared in the beginning.
+ if (mService.isScrimSrcModeEnabled()) {
+ IBinder windowToken = getWindowToken();
+ WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
+ lp.token = windowToken;
+ setLayoutParams(lp);
+ WindowManagerGlobal.getInstance().changeCanvasOpacity(windowToken, true);
+ setWillNotDraw(false);
+ } else {
+ setWillNotDraw(!DEBUG);
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (mService.interceptMediaKey(event)) {
+ return true;
+ }
+ if (super.dispatchKeyEvent(event)) {
+ return true;
+ }
+ boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_BACK:
+ if (!down) {
+ mService.onBackPressed();
+ }
+ return true;
+ case KeyEvent.KEYCODE_MENU:
+ if (!down) {
+ return mService.onMenuPressed();
+ }
+ case KeyEvent.KEYCODE_SPACE:
+ if (!down) {
+ return mService.onSpacePressed();
+ }
+ break;
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ if (mService.isDozing()) {
+ MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(
+ event, AudioManager.USE_DEFAULT_STREAM_TYPE, true);
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ public void setTouchActive(boolean touchActive) {
+ mTouchActive = touchActive;
+ mStackScrollLayout.setTouchActive(touchActive);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ boolean isDown = ev.getActionMasked() == MotionEvent.ACTION_DOWN;
+ boolean isCancel = ev.getActionMasked() == MotionEvent.ACTION_CANCEL;
+ if (!isCancel && mService.shouldIgnoreTouch()) {
+ return false;
+ }
+ if (isDown && mNotificationPanel.isFullyCollapsed()) {
+ mNotificationPanel.startExpandLatencyTracking();
+ }
+ if (isDown) {
+ setTouchActive(true);
+ mTouchCancelled = false;
+ } else if (ev.getActionMasked() == MotionEvent.ACTION_UP
+ || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+ setTouchActive(false);
+ }
+ if (mTouchCancelled) {
+ return false;
+ }
+ mFalsingManager.onTouchEvent(ev, getWidth(), getHeight());
+ if (mBrightnessMirror != null && mBrightnessMirror.getVisibility() == VISIBLE) {
+ // Disallow new pointers while the brightness mirror is visible. This is so that you
+ // can't touch anything other than the brightness slider while the mirror is showing
+ // and the rest of the panel is transparent.
+ if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
+ return false;
+ }
+ }
+ if (isDown) {
+ mStackScrollLayout.closeControlsIfOutsideTouch(ev);
+ }
+ if (mService.isDozing()) {
+ mService.mDozeScrimController.extendPulse();
+ }
+
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (mService.isDozing() && !mStackScrollLayout.hasPulsingNotifications()) {
+ // Capture all touch events in always-on.
+ return true;
+ }
+ boolean intercept = false;
+ if (mNotificationPanel.isFullyExpanded()
+ && mStackScrollLayout.getVisibility() == View.VISIBLE
+ && mService.getBarState() == StatusBarState.KEYGUARD
+ && !mService.isBouncerShowing()
+ && !mService.isDozing()) {
+ intercept = mDragDownHelper.onInterceptTouchEvent(ev);
+ }
+ if (!intercept) {
+ super.onInterceptTouchEvent(ev);
+ }
+ if (intercept) {
+ MotionEvent cancellation = MotionEvent.obtain(ev);
+ cancellation.setAction(MotionEvent.ACTION_CANCEL);
+ mStackScrollLayout.onInterceptTouchEvent(cancellation);
+ mNotificationPanel.onInterceptTouchEvent(cancellation);
+ cancellation.recycle();
+ }
+ return intercept;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ boolean handled = false;
+ if (mService.isDozing()) {
+ mDoubleTapHelper.onTouchEvent(ev);
+ handled = true;
+ }
+ if ((mService.getBarState() == StatusBarState.KEYGUARD && !handled)
+ || mDragDownHelper.isDraggingDown()) {
+ // we still want to finish our drag down gesture when locking the screen
+ handled = mDragDownHelper.onTouchEvent(ev);
+ }
+ if (!handled) {
+ handled = super.onTouchEvent(ev);
+ }
+ final int action = ev.getAction();
+ if (!handled && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) {
+ mService.setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
+ }
+ return handled;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mService.isScrimSrcModeEnabled()) {
+ // We need to ensure that our window is always drawn fully even when we have paddings,
+ // since we simulate it to be opaque.
+ int paddedBottom = getHeight() - getPaddingBottom();
+ int paddedRight = getWidth() - getPaddingRight();
+ if (getPaddingTop() != 0) {
+ canvas.drawRect(0, 0, getWidth(), getPaddingTop(), mTransparentSrcPaint);
+ }
+ if (getPaddingBottom() != 0) {
+ canvas.drawRect(0, paddedBottom, getWidth(), getHeight(), mTransparentSrcPaint);
+ }
+ if (getPaddingLeft() != 0) {
+ canvas.drawRect(0, getPaddingTop(), getPaddingLeft(), paddedBottom,
+ mTransparentSrcPaint);
+ }
+ if (getPaddingRight() != 0) {
+ canvas.drawRect(paddedRight, getPaddingTop(), getWidth(), paddedBottom,
+ mTransparentSrcPaint);
+ }
+ }
+ if (DEBUG) {
+ Paint pt = new Paint();
+ pt.setColor(0x80FFFF00);
+ pt.setStrokeWidth(12.0f);
+ pt.setStyle(Paint.Style.STROKE);
+ canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), pt);
+ }
+ }
+
+ public void cancelExpandHelper() {
+ if (mStackScrollLayout != null) {
+ mStackScrollLayout.cancelExpandHelper();
+ }
+ }
+
+ public void cancelCurrentTouch() {
+ if (mTouchActive) {
+ final long now = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ dispatchTouchEvent(event);
+ event.recycle();
+ mTouchCancelled = true;
+ }
+ }
+
+ public class LayoutParams extends FrameLayout.LayoutParams {
+
+ public boolean ignoreRightInset;
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout);
+ ignoreRightInset = a.getBoolean(
+ R.styleable.StatusBarWindowView_Layout_ignoreRightInset, false);
+ a.recycle();
+ }
+ }
+
+ @Override
+ public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback,
+ int type) {
+ if (type == ActionMode.TYPE_FLOATING) {
+ return startActionMode(originalView, callback, type);
+ }
+ return super.startActionModeForChild(originalView, callback, type);
+ }
+
+ private ActionMode createFloatingActionMode(
+ View originatingView, ActionMode.Callback2 callback) {
+ if (mFloatingActionMode != null) {
+ mFloatingActionMode.finish();
+ }
+ cleanupFloatingActionModeViews();
+ mFloatingToolbar = new FloatingToolbar(mFakeWindow);
+ final FloatingActionMode mode =
+ new FloatingActionMode(mContext, callback, originatingView, mFloatingToolbar);
+ mFloatingActionModeOriginatingView = originatingView;
+ mFloatingToolbarPreDrawListener =
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ mode.updateViewLocationInWindow();
+ return true;
+ }
+ };
+ return mode;
+ }
+
+ private void setHandledFloatingActionMode(ActionMode mode) {
+ mFloatingActionMode = mode;
+ mFloatingActionMode.invalidate(); // Will show the floating toolbar if necessary.
+ mFloatingActionModeOriginatingView.getViewTreeObserver()
+ .addOnPreDrawListener(mFloatingToolbarPreDrawListener);
+ }
+
+ private void cleanupFloatingActionModeViews() {
+ if (mFloatingToolbar != null) {
+ mFloatingToolbar.dismiss();
+ mFloatingToolbar = null;
+ }
+ if (mFloatingActionModeOriginatingView != null) {
+ if (mFloatingToolbarPreDrawListener != null) {
+ mFloatingActionModeOriginatingView.getViewTreeObserver()
+ .removeOnPreDrawListener(mFloatingToolbarPreDrawListener);
+ mFloatingToolbarPreDrawListener = null;
+ }
+ mFloatingActionModeOriginatingView = null;
+ }
+ }
+
+ private ActionMode startActionMode(
+ View originatingView, ActionMode.Callback callback, int type) {
+ ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback);
+ ActionMode mode = createFloatingActionMode(originatingView, wrappedCallback);
+ if (mode != null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) {
+ setHandledFloatingActionMode(mode);
+ } else {
+ mode = null;
+ }
+ return mode;
+ }
+
+ private class ActionModeCallback2Wrapper extends ActionMode.Callback2 {
+ private final ActionMode.Callback mWrapped;
+
+ public ActionModeCallback2Wrapper(ActionMode.Callback wrapped) {
+ mWrapped = wrapped;
+ }
+
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return mWrapped.onCreateActionMode(mode, menu);
+ }
+
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ requestFitSystemWindows();
+ return mWrapped.onPrepareActionMode(mode, menu);
+ }
+
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return mWrapped.onActionItemClicked(mode, item);
+ }
+
+ public void onDestroyActionMode(ActionMode mode) {
+ mWrapped.onDestroyActionMode(mode);
+ if (mode == mFloatingActionMode) {
+ cleanupFloatingActionModeViews();
+ mFloatingActionMode = null;
+ }
+ requestFitSystemWindows();
+ }
+
+ @Override
+ public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
+ if (mWrapped instanceof ActionMode.Callback2) {
+ ((ActionMode.Callback2) mWrapped).onGetContentRect(mode, view, outRect);
+ } else {
+ super.onGetContentRect(mode, view, outRect);
+ }
+ }
+ }
+
+ /**
+ * Minimal window to satisfy FloatingToolbar.
+ */
+ private Window mFakeWindow = new Window(mContext) {
+ @Override
+ public void takeSurface(SurfaceHolder.Callback2 callback) {
+ }
+
+ @Override
+ public void takeInputQueue(InputQueue.Callback callback) {
+ }
+
+ @Override
+ public boolean isFloating() {
+ return false;
+ }
+
+ @Override
+ public void alwaysReadCloseOnTouchAttr() {
+ }
+
+ @Override
+ public void setContentView(@LayoutRes int layoutResID) {
+ }
+
+ @Override
+ public void setContentView(View view) {
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ }
+
+ @Override
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ }
+
+ @Override
+ public void clearContentView() {
+ }
+
+ @Override
+ public View getCurrentFocus() {
+ return null;
+ }
+
+ @Override
+ public LayoutInflater getLayoutInflater() {
+ return null;
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ }
+
+ @Override
+ public void setTitleColor(@ColorInt int textColor) {
+ }
+
+ @Override
+ public void openPanel(int featureId, KeyEvent event) {
+ }
+
+ @Override
+ public void closePanel(int featureId) {
+ }
+
+ @Override
+ public void togglePanel(int featureId, KeyEvent event) {
+ }
+
+ @Override
+ public void invalidatePanelMenu(int featureId) {
+ }
+
+ @Override
+ public boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, int flags) {
+ return false;
+ }
+
+ @Override
+ public boolean performPanelIdentifierAction(int featureId, int id, int flags) {
+ return false;
+ }
+
+ @Override
+ public void closeAllPanels() {
+ }
+
+ @Override
+ public boolean performContextMenuIdentifierAction(int id, int flags) {
+ return false;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable drawable) {
+ }
+
+ @Override
+ public void setFeatureDrawableResource(int featureId, @DrawableRes int resId) {
+ }
+
+ @Override
+ public void setFeatureDrawableUri(int featureId, Uri uri) {
+ }
+
+ @Override
+ public void setFeatureDrawable(int featureId, Drawable drawable) {
+ }
+
+ @Override
+ public void setFeatureDrawableAlpha(int featureId, int alpha) {
+ }
+
+ @Override
+ public void setFeatureInt(int featureId, int value) {
+ }
+
+ @Override
+ public void takeKeyEvents(boolean get) {
+ }
+
+ @Override
+ public boolean superDispatchKeyEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean superDispatchKeyShortcutEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean superDispatchTouchEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean superDispatchTrackballEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean superDispatchGenericMotionEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public View getDecorView() {
+ return StatusBarWindowView.this;
+ }
+
+ @Override
+ public View peekDecorView() {
+ return null;
+ }
+
+ @Override
+ public Bundle saveHierarchyState() {
+ return null;
+ }
+
+ @Override
+ public void restoreHierarchyState(Bundle savedInstanceState) {
+ }
+
+ @Override
+ protected void onActive() {
+ }
+
+ @Override
+ public void setChildDrawable(int featureId, Drawable drawable) {
+ }
+
+ @Override
+ public void setChildInt(int featureId, int value) {
+ }
+
+ @Override
+ public boolean isShortcutKey(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public void setVolumeControlStream(int streamType) {
+ }
+
+ @Override
+ public int getVolumeControlStream() {
+ return 0;
+ }
+
+ @Override
+ public int getStatusBarColor() {
+ return 0;
+ }
+
+ @Override
+ public void setStatusBarColor(@ColorInt int color) {
+ }
+
+ @Override
+ public int getNavigationBarColor() {
+ return 0;
+ }
+
+ @Override
+ public void setNavigationBarColor(@ColorInt int color) {
+ }
+
+ @Override
+ public void setDecorCaptionShade(int decorCaptionShade) {
+ }
+
+ @Override
+ public void setResizingCaptionDrawable(Drawable drawable) {
+ }
+
+ @Override
+ public void onMultiWindowModeChanged() {
+ }
+
+ @Override
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ }
+
+ @Override
+ public void reportActivityRelaunched() {
+ }
+ };
+
+}
+
diff --git a/com/android/systemui/statusbar/phone/SystemUIDialog.java b/com/android/systemui/statusbar/phone/SystemUIDialog.java
new file mode 100644
index 0000000..378dad7
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -0,0 +1,118 @@
+/*
+ * 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.app.AlertDialog;
+import android.app.Dialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.UserHandle;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+
+/**
+ * Base class for dialogs that should appear over panels and keyguard.
+ */
+public class SystemUIDialog extends AlertDialog {
+
+ private final Context mContext;
+
+ public SystemUIDialog(Context context) {
+ this(context, R.style.Theme_SystemUI_Dialog);
+ }
+
+ public SystemUIDialog(Context context, int theme) {
+ super(context, theme);
+ mContext = context;
+
+ applyFlags(this);
+ WindowManager.LayoutParams attrs = getWindow().getAttributes();
+ attrs.setTitle(getClass().getSimpleName());
+ getWindow().setAttributes(attrs);
+
+ registerDismissListener(this);
+ }
+
+ public void setShowForAllUsers(boolean show) {
+ setShowForAllUsers(this, show);
+ }
+
+ public void setMessage(int resId) {
+ setMessage(mContext.getString(resId));
+ }
+
+ public void setPositiveButton(int resId, OnClickListener onClick) {
+ setButton(BUTTON_POSITIVE, mContext.getString(resId), onClick);
+ }
+
+ public void setNegativeButton(int resId, OnClickListener onClick) {
+ setButton(BUTTON_NEGATIVE, mContext.getString(resId), onClick);
+ }
+
+ public static void setShowForAllUsers(Dialog dialog, boolean show) {
+ if (show) {
+ dialog.getWindow().getAttributes().privateFlags |=
+ WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+ } else {
+ dialog.getWindow().getAttributes().privateFlags &=
+ ~WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+ }
+ }
+
+ public static void setWindowOnTop(Dialog dialog) {
+ if (Dependency.get(KeyguardMonitor.class).isShowing()) {
+ dialog.getWindow().setType(LayoutParams.TYPE_STATUS_BAR_PANEL);
+ } else {
+ dialog.getWindow().setType(LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+ }
+ }
+
+ public static AlertDialog applyFlags(AlertDialog dialog) {
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL);
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+ | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ return dialog;
+ }
+
+ public static void registerDismissListener(Dialog dialog) {
+ boolean[] registered = new boolean[1];
+ Context context = dialog.getContext();
+ final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+ };
+ context.registerReceiverAsUser(mReceiver, UserHandle.CURRENT,
+ new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null, null);
+ registered[0] = true;
+ dialog.setOnDismissListener(d -> {
+ if (registered[0]) {
+ context.unregisterReceiver(mReceiver);
+ registered[0] = false;
+ }
+ });
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/TrustDrawable.java b/com/android/systemui/statusbar/phone/TrustDrawable.java
new file mode 100644
index 0000000..028da86
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/TrustDrawable.java
@@ -0,0 +1,290 @@
+/*
+ * 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.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.animation.Interpolator;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+public class TrustDrawable extends Drawable {
+
+ private static final long ENTERING_FROM_UNSET_START_DELAY = 200;
+ private static final long VISIBLE_DURATION = 1000;
+ private static final long EXIT_DURATION = 500;
+ private static final long ENTER_DURATION = 500;
+
+ private static final int ALPHA_VISIBLE_MIN = 0x26;
+ private static final int ALPHA_VISIBLE_MAX = 0x4c;
+
+ private static final int STATE_UNSET = -1;
+ private static final int STATE_GONE = 0;
+ private static final int STATE_ENTERING = 1;
+ private static final int STATE_VISIBLE = 2;
+ private static final int STATE_EXITING = 3;
+
+ private int mAlpha;
+ private boolean mAnimating;
+
+ private int mCurAlpha;
+ private float mCurInnerRadius;
+ private Animator mCurAnimator;
+ private int mState = STATE_UNSET;
+ private Paint mPaint;
+ private boolean mTrustManaged;
+
+ private final float mInnerRadiusVisibleMin;
+ private final float mInnerRadiusVisibleMax;
+ private final float mInnerRadiusExit;
+ private final float mInnerRadiusEnter;
+ private final float mThickness;
+
+ private final Animator mVisibleAnimator;
+
+ public TrustDrawable(Context context) {
+ Resources r = context.getResources();
+ mInnerRadiusVisibleMin = r.getDimension(R.dimen.trust_circle_inner_radius_visible_min);
+ mInnerRadiusVisibleMax = r.getDimension(R.dimen.trust_circle_inner_radius_visible_max);
+ mInnerRadiusExit = r.getDimension(R.dimen.trust_circle_inner_radius_exit);
+ mInnerRadiusEnter = r.getDimension(R.dimen.trust_circle_inner_radius_enter);
+ mThickness = r.getDimension(R.dimen.trust_circle_thickness);
+
+ mCurInnerRadius = mInnerRadiusEnter;
+
+ mVisibleAnimator = makeVisibleAnimator();
+
+ mPaint = new Paint();
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setColor(Utils.getColorAttr(context, R.attr.wallpaperTextColor));
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mThickness);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ int newAlpha = (mCurAlpha * mAlpha) / 256;
+ if (newAlpha == 0) {
+ return;
+ }
+ final Rect r = getBounds();
+ mPaint.setAlpha(newAlpha);
+ canvas.drawCircle(r.exactCenterX(), r.exactCenterY(), mCurInnerRadius, mPaint);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ }
+
+ @Override
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ public void start() {
+ if (!mAnimating) {
+ mAnimating = true;
+ updateState(true);
+ invalidateSelf();
+ }
+ }
+
+ public void stop() {
+ if (mAnimating) {
+ mAnimating = false;
+ if (mCurAnimator != null) {
+ mCurAnimator.cancel();
+ mCurAnimator = null;
+ }
+ mState = STATE_UNSET;
+ mCurAlpha = 0;
+ mCurInnerRadius = mInnerRadiusEnter;
+ invalidateSelf();
+ }
+ }
+
+ public void setTrustManaged(boolean trustManaged) {
+ if (trustManaged == mTrustManaged && mState != STATE_UNSET) return;
+ mTrustManaged = trustManaged;
+ updateState(true);
+ }
+
+ private void updateState(boolean allowTransientState) {
+ if (!mAnimating) {
+ return;
+ }
+
+ int nextState = mState;
+ if (mState == STATE_UNSET) {
+ nextState = mTrustManaged ? STATE_ENTERING : STATE_GONE;
+ } else if (mState == STATE_GONE) {
+ if (mTrustManaged) nextState = STATE_ENTERING;
+ } else if (mState == STATE_ENTERING) {
+ if (!mTrustManaged) nextState = STATE_EXITING;
+ } else if (mState == STATE_VISIBLE) {
+ if (!mTrustManaged) nextState = STATE_EXITING;
+ } else if (mState == STATE_EXITING) {
+ if (mTrustManaged) nextState = STATE_ENTERING;
+ }
+ if (!allowTransientState) {
+ if (nextState == STATE_ENTERING) nextState = STATE_VISIBLE;
+ if (nextState == STATE_EXITING) nextState = STATE_GONE;
+ }
+
+ if (nextState != mState) {
+ if (mCurAnimator != null) {
+ mCurAnimator.cancel();
+ mCurAnimator = null;
+ }
+
+ if (nextState == STATE_GONE) {
+ mCurAlpha = 0;
+ mCurInnerRadius = mInnerRadiusEnter;
+ } else if (nextState == STATE_ENTERING) {
+ mCurAnimator = makeEnterAnimator(mCurInnerRadius, mCurAlpha);
+ if (mState == STATE_UNSET) {
+ mCurAnimator.setStartDelay(ENTERING_FROM_UNSET_START_DELAY);
+ }
+ } else if (nextState == STATE_VISIBLE) {
+ mCurAlpha = ALPHA_VISIBLE_MAX;
+ mCurInnerRadius = mInnerRadiusVisibleMax;
+ mCurAnimator = mVisibleAnimator;
+ } else if (nextState == STATE_EXITING) {
+ mCurAnimator = makeExitAnimator(mCurInnerRadius, mCurAlpha);
+ }
+
+ mState = nextState;
+ if (mCurAnimator != null) {
+ mCurAnimator.start();
+ }
+ invalidateSelf();
+ }
+ }
+
+ private Animator makeVisibleAnimator() {
+ return makeAnimators(mInnerRadiusVisibleMax, mInnerRadiusVisibleMin,
+ ALPHA_VISIBLE_MAX, ALPHA_VISIBLE_MIN, VISIBLE_DURATION,
+ Interpolators.ACCELERATE_DECELERATE,
+ true /* repeating */, false /* stateUpdateListener */);
+ }
+
+ private Animator makeEnterAnimator(float radius, int alpha) {
+ return makeAnimators(radius, mInnerRadiusVisibleMax,
+ alpha, ALPHA_VISIBLE_MAX, ENTER_DURATION, Interpolators.LINEAR_OUT_SLOW_IN,
+ false /* repeating */, true /* stateUpdateListener */);
+ }
+
+ private Animator makeExitAnimator(float radius, int alpha) {
+ return makeAnimators(radius, mInnerRadiusExit,
+ alpha, 0, EXIT_DURATION, Interpolators.FAST_OUT_SLOW_IN,
+ false /* repeating */, true /* stateUpdateListener */);
+ }
+
+ private Animator makeAnimators(float startRadius, float endRadius,
+ int startAlpha, int endAlpha, long duration, Interpolator interpolator,
+ boolean repeating, boolean stateUpdateListener) {
+ ValueAnimator alphaAnimator = configureAnimator(
+ ValueAnimator.ofInt(startAlpha, endAlpha),
+ duration, mAlphaUpdateListener, interpolator, repeating);
+ ValueAnimator sizeAnimator = configureAnimator(
+ ValueAnimator.ofFloat(startRadius, endRadius),
+ duration, mRadiusUpdateListener, interpolator, repeating);
+
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(alphaAnimator, sizeAnimator);
+ if (stateUpdateListener) {
+ set.addListener(new StateUpdateAnimatorListener());
+ }
+ return set;
+ }
+
+ private ValueAnimator configureAnimator(ValueAnimator animator, long duration,
+ ValueAnimator.AnimatorUpdateListener updateListener, Interpolator interpolator,
+ boolean repeating) {
+ animator.setDuration(duration);
+ animator.addUpdateListener(updateListener);
+ animator.setInterpolator(interpolator);
+ if (repeating) {
+ animator.setRepeatCount(ValueAnimator.INFINITE);
+ animator.setRepeatMode(ValueAnimator.REVERSE);
+ }
+ return animator;
+ }
+
+ private final ValueAnimator.AnimatorUpdateListener mAlphaUpdateListener =
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mCurAlpha = (int) animation.getAnimatedValue();
+ invalidateSelf();
+ }
+ };
+
+ private final ValueAnimator.AnimatorUpdateListener mRadiusUpdateListener =
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mCurInnerRadius = (float) animation.getAnimatedValue();
+ invalidateSelf();
+ }
+ };
+
+ private class StateUpdateAnimatorListener extends AnimatorListenerAdapter {
+ boolean mCancelled;
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mCancelled = false;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCancelled) {
+ updateState(false);
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/UnlockMethodCache.java b/com/android/systemui/statusbar/phone/UnlockMethodCache.java
new file mode 100644
index 0000000..f9c2130
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/UnlockMethodCache.java
@@ -0,0 +1,180 @@
+/*
+ * 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.content.Context;
+import android.os.Trace;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+
+import java.util.ArrayList;
+
+/**
+ * Caches whether the current unlock method is insecure, taking trust into account. This information
+ * might be a little bit out of date and should not be used for actual security decisions; it should
+ * be only used for visual indications.
+ */
+public class UnlockMethodCache {
+
+ private static UnlockMethodCache sInstance;
+
+ private final LockPatternUtils mLockPatternUtils;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ private final ArrayList<OnUnlockMethodChangedListener> mListeners = new ArrayList<>();
+ /** 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 mCanSkipBouncer;
+ private boolean mTrustManaged;
+ private boolean mFaceUnlockRunning;
+ private boolean mTrusted;
+
+ private UnlockMethodCache(Context ctx) {
+ mLockPatternUtils = new LockPatternUtils(ctx);
+ mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(ctx);
+ KeyguardUpdateMonitor.getInstance(ctx).registerCallback(mCallback);
+ update(true /* updateAlways */);
+ }
+
+ public static UnlockMethodCache getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new UnlockMethodCache(context);
+ }
+ return sInstance;
+ }
+
+ /**
+ * @return whether the user configured a secure unlock method like PIN, password, etc.
+ */
+ public boolean isMethodSecure() {
+ return mSecure;
+ }
+
+ public boolean isTrusted() {
+ return mTrusted;
+ }
+
+ /**
+ * @return whether the lockscreen is currently insecure, and the bouncer won't be shown
+ */
+ public boolean canSkipBouncer() {
+ return mCanSkipBouncer;
+ }
+
+ public void addListener(OnUnlockMethodChangedListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeListener(OnUnlockMethodChangedListener listener) {
+ mListeners.remove(listener);
+ }
+
+ private void update(boolean updateAlways) {
+ Trace.beginSection("UnlockMethodCache#update");
+ int user = KeyguardUpdateMonitor.getCurrentUser();
+ boolean secure = mLockPatternUtils.isSecure(user);
+ boolean canSkipBouncer = !secure || mKeyguardUpdateMonitor.getUserCanSkipBouncer(user);
+ boolean trustManaged = mKeyguardUpdateMonitor.getUserTrustIsManaged(user);
+ boolean trusted = mKeyguardUpdateMonitor.getUserHasTrust(user);
+ boolean faceUnlockRunning = mKeyguardUpdateMonitor.isFaceUnlockRunning(user)
+ && trustManaged;
+ boolean changed = secure != mSecure || canSkipBouncer != mCanSkipBouncer ||
+ trustManaged != mTrustManaged || faceUnlockRunning != mFaceUnlockRunning;
+ if (changed || updateAlways) {
+ mSecure = secure;
+ mCanSkipBouncer = canSkipBouncer;
+ mTrusted = trusted;
+ mTrustManaged = trustManaged;
+ mFaceUnlockRunning = faceUnlockRunning;
+ notifyListeners();
+ }
+ Trace.endSection();
+ }
+
+ private void notifyListeners() {
+ for (OnUnlockMethodChangedListener listener : mListeners) {
+ listener.onUnlockMethodStateChanged();
+ }
+ }
+
+ private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onUserSwitchComplete(int userId) {
+ update(false /* updateAlways */);
+ }
+
+ @Override
+ public void onTrustChanged(int userId) {
+ update(false /* updateAlways */);
+ }
+
+ @Override
+ public void onTrustManagedChanged(int userId) {
+ update(false /* updateAlways */);
+ }
+
+ @Override
+ public void onStartedWakingUp() {
+ update(false /* updateAlways */);
+ }
+
+ @Override
+ public void onFingerprintAuthenticated(int userId) {
+ Trace.beginSection("KeyguardUpdateMonitorCallback#onFingerprintAuthenticated");
+ if (!mKeyguardUpdateMonitor.isUnlockingWithFingerprintAllowed()) {
+ Trace.endSection();
+ return;
+ }
+ update(false /* updateAlways */);
+ Trace.endSection();
+ }
+
+ @Override
+ public void onFaceUnlockStateChanged(boolean running, int userId) {
+ update(false /* updateAlways */);
+ }
+
+ @Override
+ public void onStrongAuthStateChanged(int userId) {
+ update(false /* updateAlways */);
+ }
+
+ @Override
+ public void onScreenTurnedOff() {
+ update(false /* updateAlways */);
+ }
+
+ @Override
+ public void onKeyguardVisibilityChanged(boolean showing) {
+ update(false /* updateAlways */);
+ }
+ };
+
+ public boolean isTrustManaged() {
+ return mTrustManaged;
+ }
+
+ public boolean isFaceUnlockRunning() {
+ return mFaceUnlockRunning;
+ }
+
+ public static interface OnUnlockMethodChangedListener {
+ void onUnlockMethodStateChanged();
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/UserAvatarView.java b/com/android/systemui/statusbar/phone/UserAvatarView.java
new file mode 100644
index 0000000..dc1b35d
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/UserAvatarView.java
@@ -0,0 +1,142 @@
+/*
+ * 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.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.settingslib.drawable.UserIconDrawable;
+import com.android.systemui.R;
+
+/**
+ * A view that displays a user image cropped to a circle with an optional frame.
+ */
+public class UserAvatarView extends View {
+
+ private final UserIconDrawable mDrawable = new UserIconDrawable();
+
+ public UserAvatarView(Context context, AttributeSet attrs,
+ int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.UserAvatarView, defStyleAttr, defStyleRes);
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case R.styleable.UserAvatarView_avatarPadding:
+ setAvatarPadding(a.getDimension(attr, 0));
+ break;
+ case R.styleable.UserAvatarView_frameWidth:
+ setFrameWidth(a.getDimension(attr, 0));
+ break;
+ case R.styleable.UserAvatarView_framePadding:
+ setFramePadding(a.getDimension(attr, 0));
+ break;
+ case R.styleable.UserAvatarView_frameColor:
+ setFrameColor(a.getColorStateList(attr));
+ break;
+ case R.styleable.UserAvatarView_badgeDiameter:
+ setBadgeDiameter(a.getDimension(attr, 0));
+ break;
+ case R.styleable.UserAvatarView_badgeMargin:
+ setBadgeMargin(a.getDimension(attr, 0));
+ break;
+ }
+ }
+ a.recycle();
+ setBackground(mDrawable);
+ }
+
+ public UserAvatarView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public UserAvatarView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public UserAvatarView(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * @deprecated use {@link #setAvatar(Bitmap)} instead.
+ */
+ @Deprecated
+ public void setBitmap(Bitmap bitmap) {
+ setAvatar(bitmap);
+ }
+
+ public void setFrameColor(ColorStateList color) {
+ mDrawable.setFrameColor(color);
+ }
+
+ public void setFrameWidth(float frameWidth) {
+ mDrawable.setFrameWidth(frameWidth);
+ }
+
+ public void setFramePadding(float framePadding) {
+ mDrawable.setFramePadding(framePadding);
+ }
+
+ public void setAvatarPadding(float avatarPadding) {
+ mDrawable.setPadding(avatarPadding);
+ }
+
+ public void setBadgeDiameter(float diameter) {
+ mDrawable.setBadgeRadius(diameter * 0.5f);
+ }
+
+ public void setBadgeMargin(float margin) {
+ mDrawable.setBadgeMargin(margin);
+ }
+
+ public void setAvatar(Bitmap avatar) {
+ mDrawable.setIcon(avatar);
+ mDrawable.setBadge(null);
+ }
+
+ public void setAvatarWithBadge(Bitmap avatar, int userId) {
+ mDrawable.setIcon(avatar);
+ mDrawable.setBadgeIfManagedUser(getContext(), userId);
+ }
+
+ public void setDrawable(Drawable d) {
+ if (d instanceof UserIconDrawable) {
+ throw new RuntimeException("Recursively adding UserIconDrawable");
+ }
+ mDrawable.setIconDrawable(d);
+ mDrawable.setBadge(null);
+ }
+
+ public void setDrawableWithBadge(Drawable d, int userId) {
+ if (d instanceof UserIconDrawable) {
+ throw new RuntimeException("Recursively adding UserIconDrawable");
+ }
+ mDrawable.setIconDrawable(d);
+ mDrawable.setBadgeIfManagedUser(getContext(), userId);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java b/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java
new file mode 100644
index 0000000..e153b85
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java
@@ -0,0 +1,42 @@
+/*
+ * 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.content.Context;
+
+import com.android.systemui.R;
+
+/**
+ * A class to generate {@link VelocityTrackerInterface}, depending on the configuration.
+ */
+public class VelocityTrackerFactory {
+
+ public static final String PLATFORM_IMPL = "platform";
+ public static final String NOISY_IMPL = "noisy";
+
+ public static VelocityTrackerInterface obtain(Context ctx) {
+ String tracker = ctx.getResources().getString(R.string.velocity_tracker_impl);
+ switch (tracker) {
+ case NOISY_IMPL:
+ return NoisyVelocityTracker.obtain();
+ case PLATFORM_IMPL:
+ return PlatformVelocityTracker.obtain();
+ default:
+ throw new IllegalStateException("Invalid tracker: " + tracker);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/VelocityTrackerInterface.java b/com/android/systemui/statusbar/phone/VelocityTrackerInterface.java
new file mode 100644
index 0000000..a54b054
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/VelocityTrackerInterface.java
@@ -0,0 +1,31 @@
+/*
+ * 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.view.MotionEvent;
+
+/**
+ * An interface for a velocity tracker to delegate. To be implemented by different velocity tracking
+ * algorithms.
+ */
+public interface VelocityTrackerInterface {
+ public void addMovement(MotionEvent event);
+ public void computeCurrentVelocity(int units);
+ public float getXVelocity();
+ public float getYVelocity();
+ public void recycle();
+}
diff --git a/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java b/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java
new file mode 100644
index 0000000..c0a6837
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java
@@ -0,0 +1,173 @@
+/*
+ * 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.policy;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager.ActionListener;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.settingslib.wifi.AccessPoint;
+import com.android.settingslib.wifi.WifiTracker;
+import com.android.settingslib.wifi.WifiTracker.WifiListener;
+import com.android.systemui.R;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccessPointControllerImpl
+ implements NetworkController.AccessPointController, WifiListener {
+ 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_full_0,
+ R.drawable.ic_qs_wifi_full_1,
+ R.drawable.ic_qs_wifi_full_2,
+ R.drawable.ic_qs_wifi_full_3,
+ R.drawable.ic_qs_wifi_full_4,
+ };
+
+ private final Context mContext;
+ private final ArrayList<AccessPointCallback> mCallbacks = new ArrayList<AccessPointCallback>();
+ private final WifiTracker mWifiTracker;
+ private final UserManager mUserManager;
+
+ private int mCurrentUser;
+
+ public AccessPointControllerImpl(Context context, Looper bgLooper) {
+ mContext = context;
+ mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mWifiTracker = new WifiTracker(context, this, bgLooper, false, true);
+ mCurrentUser = ActivityManager.getCurrentUser();
+ }
+
+ public boolean canConfigWifi() {
+ return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI,
+ new UserHandle(mCurrentUser));
+ }
+
+ 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);
+ if (mCallbacks.size() == 1) {
+ mWifiTracker.startTracking();
+ }
+ }
+
+ @Override
+ public void removeAccessPointCallback(AccessPointCallback callback) {
+ if (callback == null) return;
+ if (DEBUG) Log.d(TAG, "removeCallback " + callback);
+ mCallbacks.remove(callback);
+ if (mCallbacks.isEmpty()) {
+ mWifiTracker.stopTracking();
+ }
+ }
+
+ @Override
+ public void scanForAccessPoints() {
+ if (DEBUG) Log.d(TAG, "force update APs!");
+ mWifiTracker.forceUpdate();
+ fireAcccessPointsCallback(mWifiTracker.getAccessPoints());
+ }
+
+ @Override
+ public int getIcon(AccessPoint ap) {
+ int level = ap.getLevel();
+ return ICONS[level >= 0 ? level : 0];
+ }
+
+ public boolean connect(AccessPoint ap) {
+ if (ap == null) return false;
+ if (DEBUG) Log.d(TAG, "connect networkId=" + ap.getConfig().networkId);
+ if (ap.isSaved()) {
+ mWifiTracker.getManager().connect(ap.getConfig().networkId, mConnectListener);
+ } else {
+ // Unknown network, need to add it.
+ if (ap.getSecurity() != AccessPoint.SECURITY_NONE) {
+ Intent intent = new Intent(Settings.ACTION_WIFI_SETTINGS);
+ intent.putExtra(EXTRA_START_CONNECT_SSID, ap.getSsidStr());
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ fireSettingsIntentCallback(intent);
+ return true;
+ } else {
+ ap.generateOpenNetworkConfig();
+ mWifiTracker.getManager().connect(ap.getConfig(), mConnectListener);
+ }
+ }
+ return false;
+ }
+
+ private void fireSettingsIntentCallback(Intent intent) {
+ for (AccessPointCallback callback : mCallbacks) {
+ callback.onSettingsActivityTriggered(intent);
+ }
+ }
+
+ private void fireAcccessPointsCallback(List<AccessPoint> aps) {
+ for (AccessPointCallback callback : mCallbacks) {
+ callback.onAccessPointsChanged(aps);
+ }
+ }
+
+ public void dump(PrintWriter pw) {
+ mWifiTracker.dump(pw);
+ }
+
+ @Override
+ public void onWifiStateChanged(int state) {
+ }
+
+ @Override
+ public void onConnectedChanged() {
+ fireAcccessPointsCallback(mWifiTracker.getAccessPoints());
+ }
+
+ @Override
+ public void onAccessPointsChanged() {
+ fireAcccessPointsCallback(mWifiTracker.getAccessPoints());
+ }
+
+ 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);
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java b/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java
new file mode 100644
index 0000000..8f86e2d
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java
@@ -0,0 +1,43 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.android.systemui.statusbar.policy;
+
+import com.android.systemui.R;
+
+/**
+ * Content descriptions for accessibility support.
+ */
+public class AccessibilityContentDescriptions {
+
+ private AccessibilityContentDescriptions() {}
+ static final int[] PHONE_SIGNAL_STRENGTH = {
+ R.string.accessibility_no_phone,
+ R.string.accessibility_phone_one_bar,
+ R.string.accessibility_phone_two_bars,
+ R.string.accessibility_phone_three_bars,
+ R.string.accessibility_phone_signal_full
+ };
+
+ static final int[] DATA_CONNECTION_STRENGTH = {
+ R.string.accessibility_no_data,
+ R.string.accessibility_data_one_bar,
+ R.string.accessibility_data_two_bars,
+ R.string.accessibility_data_three_bars,
+ R.string.accessibility_data_signal_full
+ };
+
+ static final int[] WIFI_CONNECTION_STRENGTH = {
+ R.string.accessibility_no_wifi,
+ R.string.accessibility_wifi_one_bar,
+ R.string.accessibility_wifi_two_bars,
+ R.string.accessibility_wifi_three_bars,
+ R.string.accessibility_wifi_signal_full
+ };
+
+ static final int WIFI_NO_CONNECTION = R.string.accessibility_no_wifi;
+
+ static final int[] ETHERNET_CONNECTION_VALUES = {
+ R.string.accessibility_ethernet_disconnected,
+ R.string.accessibility_ethernet_connected,
+ };
+}
diff --git a/com/android/systemui/statusbar/policy/AccessibilityController.java b/com/android/systemui/statusbar/policy/AccessibilityController.java
new file mode 100644
index 0000000..cc431dd
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/AccessibilityController.java
@@ -0,0 +1,89 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.view.accessibility.AccessibilityManager;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class AccessibilityController implements
+ AccessibilityManager.AccessibilityStateChangeListener,
+ AccessibilityManager.TouchExplorationStateChangeListener {
+
+ private final ArrayList<AccessibilityStateChangedCallback> mChangeCallbacks = new ArrayList<>();
+
+ private boolean mAccessibilityEnabled;
+ private boolean mTouchExplorationEnabled;
+
+ public AccessibilityController(Context context) {
+ AccessibilityManager am =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ am.addTouchExplorationStateChangeListener(this);
+ am.addAccessibilityStateChangeListener(this);
+ mAccessibilityEnabled = am.isEnabled();
+ mTouchExplorationEnabled = am.isTouchExplorationEnabled();
+ }
+
+ public boolean isAccessibilityEnabled() {
+ return mAccessibilityEnabled;
+ }
+
+ public boolean isTouchExplorationEnabled() {
+ return mTouchExplorationEnabled;
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("AccessibilityController state:");
+ pw.print(" mAccessibilityEnabled="); pw.println(mAccessibilityEnabled);
+ pw.print(" mTouchExplorationEnabled="); pw.println(mTouchExplorationEnabled);
+ }
+
+ public void addStateChangedCallback(AccessibilityStateChangedCallback cb) {
+ mChangeCallbacks.add(cb);
+ cb.onStateChanged(mAccessibilityEnabled, mTouchExplorationEnabled);
+ }
+
+ public void removeStateChangedCallback(AccessibilityStateChangedCallback cb) {
+ mChangeCallbacks.remove(cb);
+ }
+
+ private void fireChanged() {
+ final int N = mChangeCallbacks.size();
+ for (int i = 0; i < N; i++) {
+ mChangeCallbacks.get(i).onStateChanged(mAccessibilityEnabled, mTouchExplorationEnabled);
+ }
+ }
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ mAccessibilityEnabled = enabled;
+ fireChanged();
+ }
+
+ @Override
+ public void onTouchExplorationStateChanged(boolean enabled) {
+ mTouchExplorationEnabled = enabled;
+ fireChanged();
+ }
+
+ public interface AccessibilityStateChangedCallback {
+ void onStateChanged(boolean accessibilityEnabled, boolean touchExplorationEnabled);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java b/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
new file mode 100644
index 0000000..6a573f5
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 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 android.content.Context;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
+
+/**
+ * For mocking because AccessibilityManager is final for some reason...
+ */
+public class AccessibilityManagerWrapper implements
+ CallbackController<AccessibilityServicesStateChangeListener> {
+
+ private final AccessibilityManager mAccessibilityManager;
+
+ public AccessibilityManagerWrapper(Context context) {
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ }
+
+ @Override
+ public void addCallback(AccessibilityServicesStateChangeListener listener) {
+ mAccessibilityManager.addAccessibilityServicesStateChangeListener(listener, null);
+ }
+
+ @Override
+ public void removeCallback(AccessibilityServicesStateChangeListener listener) {
+ mAccessibilityManager.removeAccessibilityServicesStateChangeListener(listener);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/BatteryController.java b/com/android/systemui/statusbar/policy/BatteryController.java
new file mode 100644
index 0000000..641fe69
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BatteryController.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010 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.DemoMode;
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public interface BatteryController extends DemoMode, Dumpable,
+ CallbackController<BatteryStateChangeCallback> {
+ /**
+ * Prints the current state of the {@link BatteryController} to the given {@link PrintWriter}.
+ */
+ void dump(FileDescriptor fd, PrintWriter pw, String[] args);
+
+ /**
+ * Sets if the current device is in power save mode.
+ */
+ void setPowerSaveMode(boolean powerSave);
+
+ /**
+ * Returns {@code true} if the device is currently in power save mode.
+ */
+ boolean isPowerSave();
+
+ /**
+ * A listener that will be notified whenever a change in battery level or power save mode
+ * has occurred.
+ */
+ interface BatteryStateChangeCallback {
+ default void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {}
+ default void onPowerSaveChanged(boolean isPowerSave) {}
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
new file mode 100644
index 0000000..e8d5af6
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2016 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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.util.Log;
+import com.android.systemui.DemoMode;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Default implementation of a {@link BatteryController}. This controller monitors for battery
+ * level change events that are broadcasted by the system.
+ */
+public class BatteryControllerImpl extends BroadcastReceiver implements BatteryController {
+ private static final String TAG = "BatteryController";
+
+ public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST";
+
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ArrayList<BatteryController.BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>();
+ private final PowerManager mPowerManager;
+ private final Handler mHandler;
+ private final Context mContext;
+
+ protected int mLevel;
+ protected boolean mPluggedIn;
+ protected boolean mCharging;
+ protected boolean mCharged;
+ protected boolean mPowerSave;
+ private boolean mTestmode = false;
+ private boolean mHasReceivedBattery = false;
+
+ public BatteryControllerImpl(Context context) {
+ mContext = context;
+ mHandler = new Handler();
+ mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+
+ registerReceiver();
+ updatePowerSave();
+ }
+
+ private void registerReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
+ filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING);
+ filter.addAction(ACTION_LEVEL_TEST);
+ mContext.registerReceiver(this, filter);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("BatteryController state:");
+ pw.print(" mLevel="); pw.println(mLevel);
+ pw.print(" mPluggedIn="); pw.println(mPluggedIn);
+ pw.print(" mCharging="); pw.println(mCharging);
+ pw.print(" mCharged="); pw.println(mCharged);
+ pw.print(" mPowerSave="); pw.println(mPowerSave);
+ }
+
+ @Override
+ public void setPowerSaveMode(boolean powerSave) {
+ mPowerManager.setPowerSaveMode(powerSave);
+ }
+
+ @Override
+ public void addCallback(BatteryController.BatteryStateChangeCallback cb) {
+ synchronized (mChangeCallbacks) {
+ mChangeCallbacks.add(cb);
+ }
+ if (!mHasReceivedBattery) return;
+ cb.onBatteryLevelChanged(mLevel, mPluggedIn, mCharging);
+ cb.onPowerSaveChanged(mPowerSave);
+ }
+
+ @Override
+ public void removeCallback(BatteryController.BatteryStateChangeCallback cb) {
+ synchronized (mChangeCallbacks) {
+ mChangeCallbacks.remove(cb);
+ }
+ }
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
+ if (mTestmode && !intent.getBooleanExtra("testmode", false)) return;
+ mHasReceivedBattery = true;
+ mLevel = (int)(100f
+ * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
+ / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100));
+ mPluggedIn = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
+
+ final int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
+ BatteryManager.BATTERY_STATUS_UNKNOWN);
+ mCharged = status == BatteryManager.BATTERY_STATUS_FULL;
+ mCharging = mCharged || status == BatteryManager.BATTERY_STATUS_CHARGING;
+
+ fireBatteryLevelChanged();
+ } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) {
+ updatePowerSave();
+ } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING)) {
+ setPowerSave(intent.getBooleanExtra(PowerManager.EXTRA_POWER_SAVE_MODE, false));
+ } else if (action.equals(ACTION_LEVEL_TEST)) {
+ mTestmode = true;
+ mHandler.post(new Runnable() {
+ int curLevel = 0;
+ int incr = 1;
+ int saveLevel = mLevel;
+ boolean savePlugged = mPluggedIn;
+ Intent dummy = new Intent(Intent.ACTION_BATTERY_CHANGED);
+ @Override
+ public void run() {
+ if (curLevel < 0) {
+ mTestmode = false;
+ dummy.putExtra("level", saveLevel);
+ dummy.putExtra("plugged", savePlugged);
+ dummy.putExtra("testmode", false);
+ } else {
+ dummy.putExtra("level", curLevel);
+ dummy.putExtra("plugged", incr > 0 ? BatteryManager.BATTERY_PLUGGED_AC
+ : 0);
+ dummy.putExtra("testmode", true);
+ }
+ context.sendBroadcast(dummy);
+
+ if (!mTestmode) return;
+
+ curLevel += incr;
+ if (curLevel == 100) {
+ incr *= -1;
+ }
+ mHandler.postDelayed(this, 200);
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean isPowerSave() {
+ return mPowerSave;
+ }
+
+ private void updatePowerSave() {
+ setPowerSave(mPowerManager.isPowerSaveMode());
+ }
+
+ private void setPowerSave(boolean powerSave) {
+ if (powerSave == mPowerSave) return;
+ mPowerSave = powerSave;
+ if (DEBUG) Log.d(TAG, "Power save is " + (mPowerSave ? "on" : "off"));
+ firePowerSaveChanged();
+ }
+
+ protected void fireBatteryLevelChanged() {
+ synchronized (mChangeCallbacks) {
+ final int N = mChangeCallbacks.size();
+ for (int i = 0; i < N; i++) {
+ mChangeCallbacks.get(i).onBatteryLevelChanged(mLevel, mPluggedIn, mCharging);
+ }
+ }
+ }
+
+ private void firePowerSaveChanged() {
+ synchronized (mChangeCallbacks) {
+ final int N = mChangeCallbacks.size();
+ for (int i = 0; i < N; i++) {
+ mChangeCallbacks.get(i).onPowerSaveChanged(mPowerSave);
+ }
+ }
+ }
+
+ private boolean mDemoMode;
+
+ @Override
+ public void dispatchDemoCommand(String command, Bundle args) {
+ if (!mDemoMode && command.equals(COMMAND_ENTER)) {
+ mDemoMode = true;
+ mContext.unregisterReceiver(this);
+ } else if (mDemoMode && command.equals(COMMAND_EXIT)) {
+ mDemoMode = false;
+ registerReceiver();
+ updatePowerSave();
+ } else if (mDemoMode && command.equals(COMMAND_BATTERY)) {
+ String level = args.getString("level");
+ String plugged = args.getString("plugged");
+ String powerSave = args.getString("powersave");
+ if (level != null) {
+ mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100);
+ }
+ if (plugged != null) {
+ mPluggedIn = Boolean.parseBoolean(plugged);
+ }
+ if (powerSave != null) {
+ mPowerSave = powerSave.equals("true");
+ firePowerSaveChanged();
+ }
+ fireBatteryLevelChanged();
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/BluetoothController.java b/com/android/systemui/statusbar/policy/BluetoothController.java
new file mode 100644
index 0000000..b693ebb
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BluetoothController.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.statusbar.policy;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.BluetoothController.Callback;
+
+import java.util.Collection;
+
+public interface BluetoothController extends CallbackController<Callback>, Dumpable {
+ boolean isBluetoothSupported();
+ boolean isBluetoothEnabled();
+
+ int getBluetoothState();
+
+ boolean isBluetoothConnected();
+ boolean isBluetoothConnecting();
+ String getLastDeviceName();
+ void setBluetoothEnabled(boolean enabled);
+ Collection<CachedBluetoothDevice> getDevices();
+ void connect(CachedBluetoothDevice device);
+ void disconnect(CachedBluetoothDevice device);
+ boolean canConfigBluetooth();
+
+ int getMaxConnectionState(CachedBluetoothDevice device);
+ int getBondState(CachedBluetoothDevice device);
+ CachedBluetoothDevice getLastDevice();
+
+ public interface Callback {
+ void onBluetoothStateChange(boolean enabled);
+ void onBluetoothDevicesChanged();
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
new file mode 100644
index 0000000..3b15c2b
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import android.app.ActivityManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.systemui.Dependency;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.WeakHashMap;
+
+public class BluetoothControllerImpl implements BluetoothController, BluetoothCallback,
+ CachedBluetoothDevice.Callback {
+ private static final String TAG = "BluetoothController";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final LocalBluetoothManager mLocalBluetoothManager;
+ private final UserManager mUserManager;
+ private final int mCurrentUser;
+ private final WeakHashMap<CachedBluetoothDevice, ActuallyCachedState> mCachedState =
+ new WeakHashMap<>();
+ private final Handler mBgHandler;
+
+ private boolean mEnabled;
+ private int mConnectionState = BluetoothAdapter.STATE_DISCONNECTED;
+ private CachedBluetoothDevice mLastDevice;
+
+ private final H mHandler = new H(Looper.getMainLooper());
+ private int mState;
+
+ public BluetoothControllerImpl(Context context, Looper bgLooper) {
+ mLocalBluetoothManager = Dependency.get(LocalBluetoothManager.class);
+ mBgHandler = new Handler(bgLooper);
+ if (mLocalBluetoothManager != null) {
+ mLocalBluetoothManager.getEventManager().setReceiverHandler(mBgHandler);
+ mLocalBluetoothManager.getEventManager().registerCallback(this);
+ onBluetoothStateChanged(
+ mLocalBluetoothManager.getBluetoothAdapter().getBluetoothState());
+ }
+ mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ mCurrentUser = ActivityManager.getCurrentUser();
+ }
+
+ @Override
+ public boolean canConfigBluetooth() {
+ return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_BLUETOOTH,
+ UserHandle.of(mCurrentUser))
+ && !mUserManager.hasUserRestriction(UserManager.DISALLOW_BLUETOOTH,
+ UserHandle.of(mCurrentUser));
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("BluetoothController state:");
+ pw.print(" mLocalBluetoothManager="); pw.println(mLocalBluetoothManager);
+ if (mLocalBluetoothManager == null) {
+ return;
+ }
+ pw.print(" mEnabled="); pw.println(mEnabled);
+ pw.print(" mConnectionState="); pw.println(stateToString(mConnectionState));
+ pw.print(" mLastDevice="); pw.println(mLastDevice);
+ pw.print(" mCallbacks.size="); pw.println(mHandler.mCallbacks.size());
+ pw.println(" Bluetooth Devices:");
+ for (CachedBluetoothDevice device :
+ mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy()) {
+ pw.println(" " + getDeviceString(device));
+ }
+ }
+
+ private static String stateToString(int state) {
+ switch (state) {
+ case BluetoothAdapter.STATE_CONNECTED:
+ return "CONNECTED";
+ case BluetoothAdapter.STATE_CONNECTING:
+ return "CONNECTING";
+ case BluetoothAdapter.STATE_DISCONNECTED:
+ return "DISCONNECTED";
+ case BluetoothAdapter.STATE_DISCONNECTING:
+ return "DISCONNECTING";
+ }
+ return "UNKNOWN(" + state + ")";
+ }
+
+ private String getDeviceString(CachedBluetoothDevice device) {
+ return device.getName() + " " + device.getBondState() + " " + device.isConnected();
+ }
+
+ @Override
+ public int getBondState(CachedBluetoothDevice device) {
+ return getCachedState(device).mBondState;
+ }
+
+ @Override
+ public CachedBluetoothDevice getLastDevice() {
+ return mLastDevice;
+ }
+
+ @Override
+ public int getMaxConnectionState(CachedBluetoothDevice device) {
+ return getCachedState(device).mMaxConnectionState;
+ }
+
+ @Override
+ public void addCallback(Callback cb) {
+ mHandler.obtainMessage(H.MSG_ADD_CALLBACK, cb).sendToTarget();
+ mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
+ }
+
+ @Override
+ public void removeCallback(Callback cb) {
+ mHandler.obtainMessage(H.MSG_REMOVE_CALLBACK, cb).sendToTarget();
+ }
+
+ @Override
+ public boolean isBluetoothEnabled() {
+ return mEnabled;
+ }
+
+ @Override
+ public int getBluetoothState() {
+ return mState;
+ }
+
+ @Override
+ public boolean isBluetoothConnected() {
+ return mConnectionState == BluetoothAdapter.STATE_CONNECTED;
+ }
+
+ @Override
+ public boolean isBluetoothConnecting() {
+ return mConnectionState == BluetoothAdapter.STATE_CONNECTING;
+ }
+
+ @Override
+ public void setBluetoothEnabled(boolean enabled) {
+ if (mLocalBluetoothManager != null) {
+ mLocalBluetoothManager.getBluetoothAdapter().setBluetoothEnabled(enabled);
+ }
+ }
+
+ @Override
+ public boolean isBluetoothSupported() {
+ return mLocalBluetoothManager != null;
+ }
+
+ @Override
+ public void connect(final CachedBluetoothDevice device) {
+ if (mLocalBluetoothManager == null || device == null) return;
+ device.connect(true);
+ }
+
+ @Override
+ public void disconnect(CachedBluetoothDevice device) {
+ if (mLocalBluetoothManager == null || device == null) return;
+ device.disconnect();
+ }
+
+ @Override
+ public String getLastDeviceName() {
+ return mLastDevice != null ? mLastDevice.getName() : null;
+ }
+
+ @Override
+ public Collection<CachedBluetoothDevice> getDevices() {
+ return mLocalBluetoothManager != null
+ ? mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy()
+ : null;
+ }
+
+ private void updateConnected() {
+ // Make sure our connection state is up to date.
+ int state = mLocalBluetoothManager.getBluetoothAdapter().getConnectionState();
+ if (mLastDevice != null && !mLastDevice.isConnected()) {
+ // Clear out last device if no longer connected.
+ mLastDevice = null;
+ }
+ // If any of the devices are in a higher state than the adapter, move the adapter into
+ // that state.
+ for (CachedBluetoothDevice device : getDevices()) {
+ int maxDeviceState = device.getMaxConnectionState();
+ if (maxDeviceState > state) {
+ state = maxDeviceState;
+ }
+ if (mLastDevice == null && device.isConnected()) {
+ // Set as last connected device only if we don't have one.
+ mLastDevice = device;
+ }
+ }
+
+ if (mLastDevice == null && state == BluetoothAdapter.STATE_CONNECTED) {
+ // If somehow we think we are connected, but have no connected devices, we aren't
+ // connected.
+ state = BluetoothAdapter.STATE_DISCONNECTED;
+ }
+ if (state != mConnectionState) {
+ mConnectionState = state;
+ mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
+ }
+ }
+
+ @Override
+ public void onBluetoothStateChanged(int bluetoothState) {
+ mEnabled = bluetoothState == BluetoothAdapter.STATE_ON
+ || bluetoothState == BluetoothAdapter.STATE_TURNING_ON;
+ mState = bluetoothState;
+ mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
+ }
+
+ @Override
+ public void onScanningStateChanged(boolean started) {
+ // Don't care.
+ }
+
+ @Override
+ public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
+ cachedDevice.registerCallback(this);
+ updateConnected();
+ mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+ }
+
+ @Override
+ public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
+ mCachedState.remove(cachedDevice);
+ updateConnected();
+ mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+ }
+
+ @Override
+ public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
+ mCachedState.remove(cachedDevice);
+ updateConnected();
+ mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+ }
+
+ @Override
+ public void onDeviceAttributesChanged() {
+ updateConnected();
+ mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+ }
+
+ @Override
+ public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
+ mCachedState.remove(cachedDevice);
+ mLastDevice = cachedDevice;
+ updateConnected();
+ mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
+ }
+
+ private ActuallyCachedState getCachedState(CachedBluetoothDevice device) {
+ ActuallyCachedState state = mCachedState.get(device);
+ if (state == null) {
+ state = new ActuallyCachedState(device, mHandler);
+ mBgHandler.post(state);
+ mCachedState.put(device, state);
+ return state;
+ }
+ return state;
+ }
+
+ private static class ActuallyCachedState implements Runnable {
+
+ private final WeakReference<CachedBluetoothDevice> mDevice;
+ private final Handler mUiHandler;
+ private int mBondState = BluetoothDevice.BOND_NONE;
+ private int mMaxConnectionState = BluetoothProfile.STATE_DISCONNECTED;
+
+ private ActuallyCachedState(CachedBluetoothDevice device, Handler uiHandler) {
+ mDevice = new WeakReference<>(device);
+ mUiHandler = uiHandler;
+ }
+
+ @Override
+ public void run() {
+ CachedBluetoothDevice device = mDevice.get();
+ if (device != null) {
+ mBondState = device.getBondState();
+ mMaxConnectionState = device.getMaxConnectionState();
+ mUiHandler.removeMessages(H.MSG_PAIRED_DEVICES_CHANGED);
+ mUiHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+ }
+ }
+ }
+
+ private final class H extends Handler {
+ private final ArrayList<BluetoothController.Callback> mCallbacks = new ArrayList<>();
+
+ private static final int MSG_PAIRED_DEVICES_CHANGED = 1;
+ private static final int MSG_STATE_CHANGED = 2;
+ private static final int MSG_ADD_CALLBACK = 3;
+ private static final int MSG_REMOVE_CALLBACK = 4;
+
+ public H(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_PAIRED_DEVICES_CHANGED:
+ firePairedDevicesChanged();
+ break;
+ case MSG_STATE_CHANGED:
+ fireStateChange();
+ break;
+ case MSG_ADD_CALLBACK:
+ mCallbacks.add((BluetoothController.Callback) msg.obj);
+ break;
+ case MSG_REMOVE_CALLBACK:
+ mCallbacks.remove((BluetoothController.Callback) msg.obj);
+ break;
+ }
+ }
+
+ private void firePairedDevicesChanged() {
+ for (BluetoothController.Callback cb : mCallbacks) {
+ cb.onBluetoothDevicesChanged();
+ }
+ }
+
+ private void fireStateChange() {
+ for (BluetoothController.Callback cb : mCallbacks) {
+ fireStateChange(cb);
+ }
+ }
+
+ private void fireStateChange(BluetoothController.Callback cb) {
+ cb.onBluetoothStateChange(mEnabled);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/BrightnessMirrorController.java b/com/android/systemui/statusbar/policy/BrightnessMirrorController.java
new file mode 100644
index 0000000..42ce4c5
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BrightnessMirrorController.java
@@ -0,0 +1,156 @@
+/*
+ * 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.policy;
+
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.widget.FrameLayout;
+
+import com.android.internal.util.Preconditions;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.ScrimController;
+import com.android.systemui.statusbar.phone.StatusBarWindowView;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+/**
+ * Controls showing and hiding of the brightness mirror.
+ */
+public class BrightnessMirrorController
+ implements CallbackController<BrightnessMirrorController.BrightnessMirrorListener> {
+
+ private final NotificationStackScrollLayout mStackScroller;
+ public long TRANSITION_DURATION_OUT = 150;
+ public long TRANSITION_DURATION_IN = 200;
+
+ private final StatusBarWindowView mStatusBarWindow;
+ private final ScrimController mScrimController;
+ private final View mNotificationPanel;
+ private final ArraySet<BrightnessMirrorListener> mBrightnessMirrorListeners = new ArraySet<>();
+ private final int[] mInt2Cache = new int[2];
+ private View mBrightnessMirror;
+
+ public BrightnessMirrorController(StatusBarWindowView statusBarWindow,
+ ScrimController scrimController) {
+ mStatusBarWindow = statusBarWindow;
+ mBrightnessMirror = statusBarWindow.findViewById(R.id.brightness_mirror);
+ mNotificationPanel = statusBarWindow.findViewById(R.id.notification_panel);
+ mStackScroller = (NotificationStackScrollLayout) statusBarWindow.findViewById(
+ R.id.notification_stack_scroller);
+ mScrimController = scrimController;
+ }
+
+ public void showMirror() {
+ mBrightnessMirror.setVisibility(View.VISIBLE);
+ mStackScroller.setFadingOut(true);
+ mScrimController.forceHideScrims(true /* hide */, true /* animated */);
+ outAnimation(mNotificationPanel.animate())
+ .withLayer();
+ }
+
+ public void hideMirror() {
+ mScrimController.forceHideScrims(false /* hide */, true /* animated */);
+ inAnimation(mNotificationPanel.animate())
+ .withLayer()
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mBrightnessMirror.setVisibility(View.INVISIBLE);
+ mStackScroller.setFadingOut(false);
+ }
+ });
+ }
+
+ private ViewPropertyAnimator outAnimation(ViewPropertyAnimator a) {
+ return a.alpha(0.0f)
+ .setDuration(TRANSITION_DURATION_OUT)
+ .setInterpolator(Interpolators.ALPHA_OUT)
+ .withEndAction(null);
+ }
+ private ViewPropertyAnimator inAnimation(ViewPropertyAnimator a) {
+ return a.alpha(1.0f)
+ .setDuration(TRANSITION_DURATION_IN)
+ .setInterpolator(Interpolators.ALPHA_IN);
+ }
+
+ public void setLocation(View original) {
+ original.getLocationInWindow(mInt2Cache);
+
+ // Original is slightly larger than the mirror, so make sure to use the center for the
+ // positioning.
+ int originalX = mInt2Cache[0] + original.getWidth() / 2;
+ int originalY = mInt2Cache[1] + original.getHeight() / 2;
+ mBrightnessMirror.setTranslationX(0);
+ mBrightnessMirror.setTranslationY(0);
+ mBrightnessMirror.getLocationInWindow(mInt2Cache);
+ int mirrorX = mInt2Cache[0] + mBrightnessMirror.getWidth() / 2;
+ int mirrorY = mInt2Cache[1] + mBrightnessMirror.getHeight() / 2;
+ mBrightnessMirror.setTranslationX(originalX - mirrorX);
+ mBrightnessMirror.setTranslationY(originalY - mirrorY);
+ }
+
+ public View getMirror() {
+ return mBrightnessMirror;
+ }
+
+ public void updateResources() {
+ FrameLayout.LayoutParams lp =
+ (FrameLayout.LayoutParams) mBrightnessMirror.getLayoutParams();
+ lp.width = mBrightnessMirror.getResources().getDimensionPixelSize(
+ R.dimen.qs_panel_width);
+ lp.gravity = mBrightnessMirror.getResources().getInteger(
+ R.integer.notification_panel_layout_gravity);
+ mBrightnessMirror.setLayoutParams(lp);
+ }
+
+ public void onOverlayChanged() {
+ reinflate();
+ }
+
+ public void onDensityOrFontScaleChanged() {
+ reinflate();
+ }
+
+ private void reinflate() {
+ int index = mStatusBarWindow.indexOfChild(mBrightnessMirror);
+ mStatusBarWindow.removeView(mBrightnessMirror);
+ mBrightnessMirror = LayoutInflater.from(mBrightnessMirror.getContext()).inflate(
+ R.layout.brightness_mirror, mStatusBarWindow, false);
+ mStatusBarWindow.addView(mBrightnessMirror, index);
+
+ for (int i = 0; i < mBrightnessMirrorListeners.size(); i++) {
+ mBrightnessMirrorListeners.valueAt(i).onBrightnessMirrorReinflated(mBrightnessMirror);
+ }
+ }
+
+ @Override
+ public void addCallback(BrightnessMirrorListener listener) {
+ Preconditions.checkNotNull(listener);
+ mBrightnessMirrorListeners.add(listener);
+ }
+
+ @Override
+ public void removeCallback(BrightnessMirrorListener listener) {
+ mBrightnessMirrorListeners.remove(listener);
+ }
+
+ public interface BrightnessMirrorListener {
+ void onBrightnessMirrorReinflated(View brightnessMirror);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/CallbackController.java b/com/android/systemui/statusbar/policy/CallbackController.java
new file mode 100644
index 0000000..9042ca6
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/CallbackController.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2016 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;
+
+public interface CallbackController<T> {
+ void addCallback(T listener);
+ void removeCallback(T listener);
+}
diff --git a/com/android/systemui/statusbar/policy/CallbackHandler.java b/com/android/systemui/statusbar/policy/CallbackHandler.java
new file mode 100644
index 0000000..a456786
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/CallbackHandler.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2015 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 android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.SubscriptionInfo;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener;
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Implements network listeners and forwards the calls along onto other listeners but on
+ * the current or specified Looper.
+ */
+public class CallbackHandler extends Handler implements EmergencyListener, SignalCallback {
+ private static final int MSG_EMERGENCE_CHANGED = 0;
+ private static final int MSG_SUBS_CHANGED = 1;
+ private static final int MSG_NO_SIM_VISIBLE_CHANGED = 2;
+ private static final int MSG_ETHERNET_CHANGED = 3;
+ private static final int MSG_AIRPLANE_MODE_CHANGED = 4;
+ private static final int MSG_MOBILE_DATA_ENABLED_CHANGED = 5;
+ private static final int MSG_ADD_REMOVE_EMERGENCY = 6;
+ private static final int MSG_ADD_REMOVE_SIGNAL = 7;
+
+ // All the callbacks.
+ private final ArrayList<EmergencyListener> mEmergencyListeners = new ArrayList<>();
+ private final ArrayList<SignalCallback> mSignalCallbacks = new ArrayList<>();
+
+ public CallbackHandler() {
+ super(Looper.getMainLooper());
+ }
+
+ @VisibleForTesting
+ CallbackHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_EMERGENCE_CHANGED:
+ for (EmergencyListener listener : mEmergencyListeners) {
+ listener.setEmergencyCallsOnly(msg.arg1 != 0);
+ }
+ break;
+ case MSG_SUBS_CHANGED:
+ for (SignalCallback signalCluster : mSignalCallbacks) {
+ signalCluster.setSubs((List<SubscriptionInfo>) msg.obj);
+ }
+ break;
+ case MSG_NO_SIM_VISIBLE_CHANGED:
+ for (SignalCallback signalCluster : mSignalCallbacks) {
+ signalCluster.setNoSims(msg.arg1 != 0);
+ }
+ break;
+ case MSG_ETHERNET_CHANGED:
+ for (SignalCallback signalCluster : mSignalCallbacks) {
+ signalCluster.setEthernetIndicators((IconState) msg.obj);
+ }
+ break;
+ case MSG_AIRPLANE_MODE_CHANGED:
+ for (SignalCallback signalCluster : mSignalCallbacks) {
+ signalCluster.setIsAirplaneMode((IconState) msg.obj);
+ }
+ break;
+ case MSG_MOBILE_DATA_ENABLED_CHANGED:
+ for (SignalCallback signalCluster : mSignalCallbacks) {
+ signalCluster.setMobileDataEnabled(msg.arg1 != 0);
+ }
+ break;
+ case MSG_ADD_REMOVE_EMERGENCY:
+ if (msg.arg1 != 0) {
+ mEmergencyListeners.add((EmergencyListener) msg.obj);
+ } else {
+ mEmergencyListeners.remove((EmergencyListener) msg.obj);
+ }
+ break;
+ case MSG_ADD_REMOVE_SIGNAL:
+ if (msg.arg1 != 0) {
+ mSignalCallbacks.add((SignalCallback) msg.obj);
+ } else {
+ mSignalCallbacks.remove((SignalCallback) msg.obj);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void setWifiIndicators(final boolean enabled, final IconState statusIcon,
+ final IconState qsIcon, final boolean activityIn, final boolean activityOut,
+ final String description, boolean isTransient) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ for (SignalCallback callback : mSignalCallbacks) {
+ callback.setWifiIndicators(enabled, statusIcon, qsIcon, activityIn, activityOut,
+ description, isTransient);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void setMobileDataIndicators(final IconState statusIcon, final IconState qsIcon,
+ final int statusType, final int qsType,final boolean activityIn,
+ final boolean activityOut, final String typeContentDescription,
+ final String description, final boolean isWide, final int subId, boolean roaming) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ for (SignalCallback signalCluster : mSignalCallbacks) {
+ signalCluster.setMobileDataIndicators(statusIcon, qsIcon, statusType, qsType,
+ activityIn, activityOut, typeContentDescription, description, isWide,
+ subId, roaming);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void setSubs(List<SubscriptionInfo> subs) {
+ obtainMessage(MSG_SUBS_CHANGED, subs).sendToTarget();
+ }
+
+ @Override
+ public void setNoSims(boolean show) {
+ obtainMessage(MSG_NO_SIM_VISIBLE_CHANGED, show ? 1 : 0, 0).sendToTarget();
+ }
+
+ @Override
+ public void setMobileDataEnabled(boolean enabled) {
+ obtainMessage(MSG_MOBILE_DATA_ENABLED_CHANGED, enabled ? 1 : 0, 0).sendToTarget();
+ }
+
+ @Override
+ public void setEmergencyCallsOnly(boolean emergencyOnly) {
+ obtainMessage(MSG_EMERGENCE_CHANGED, emergencyOnly ? 1 : 0, 0).sendToTarget();
+ }
+
+ @Override
+ public void setEthernetIndicators(IconState icon) {
+ obtainMessage(MSG_ETHERNET_CHANGED, icon).sendToTarget();;
+ }
+
+ @Override
+ public void setIsAirplaneMode(IconState icon) {
+ obtainMessage(MSG_AIRPLANE_MODE_CHANGED, icon).sendToTarget();;
+ }
+
+ public void setListening(EmergencyListener listener, boolean listening) {
+ obtainMessage(MSG_ADD_REMOVE_EMERGENCY, listening ? 1 : 0, 0, listener).sendToTarget();
+ }
+
+ public void setListening(SignalCallback listener, boolean listening) {
+ obtainMessage(MSG_ADD_REMOVE_SIGNAL, listening ? 1 : 0, 0, listener).sendToTarget();
+ }
+
+}
diff --git a/com/android/systemui/statusbar/policy/CastController.java b/com/android/systemui/statusbar/policy/CastController.java
new file mode 100644
index 0000000..97be6ed
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/CastController.java
@@ -0,0 +1,46 @@
+/*
+ * 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.policy;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.CastController.Callback;
+
+import java.util.Set;
+
+public interface CastController extends CallbackController<Callback>, Dumpable {
+ void setDiscovering(boolean request);
+ void setCurrentUserId(int currentUserId);
+ Set<CastDevice> getCastDevices();
+ void startCasting(CastDevice device);
+ void stopCasting(CastDevice device);
+
+ public interface Callback {
+ void onCastDevicesChanged();
+ }
+
+ public static final class CastDevice {
+ public static final int STATE_DISCONNECTED = 0;
+ public static final int STATE_CONNECTING = 1;
+ public static final int STATE_CONNECTED = 2;
+
+ public String id;
+ public String name;
+ public String description;
+ public int state = STATE_DISCONNECTED;
+ public Object tag;
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/CastControllerImpl.java b/com/android/systemui/statusbar/policy/CastControllerImpl.java
new file mode 100644
index 0000000..2bf62bb
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/CastControllerImpl.java
@@ -0,0 +1,313 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.MediaRouter;
+import android.media.MediaRouter.RouteInfo;
+import android.media.projection.MediaProjectionInfo;
+import android.media.projection.MediaProjectionManager;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.systemui.R;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
+
+/** Platform implementation of the cast controller. **/
+public class CastControllerImpl implements CastController {
+ private static final String TAG = "CastController";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final Context mContext;
+ private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+ private final MediaRouter mMediaRouter;
+ private final ArrayMap<String, RouteInfo> mRoutes = new ArrayMap<>();
+ private final Object mDiscoveringLock = new Object();
+ private final MediaProjectionManager mProjectionManager;
+ private final Object mProjectionLock = new Object();
+
+ private boolean mDiscovering;
+ private boolean mCallbackRegistered;
+ private MediaProjectionInfo mProjection;
+
+ public CastControllerImpl(Context context) {
+ mContext = context;
+ mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ mProjectionManager = (MediaProjectionManager)
+ context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
+ mProjection = mProjectionManager.getActiveProjectionInfo();
+ mProjectionManager.addCallback(mProjectionCallback, new Handler());
+ if (DEBUG) Log.d(TAG, "new CastController()");
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("CastController state:");
+ pw.print(" mDiscovering="); pw.println(mDiscovering);
+ pw.print(" mCallbackRegistered="); pw.println(mCallbackRegistered);
+ pw.print(" mCallbacks.size="); pw.println(mCallbacks.size());
+ pw.print(" mRoutes.size="); pw.println(mRoutes.size());
+ for (int i = 0; i < mRoutes.size(); i++) {
+ final RouteInfo route = mRoutes.valueAt(i);
+ pw.print(" "); pw.println(routeToString(route));
+ }
+ pw.print(" mProjection="); pw.println(mProjection);
+ }
+
+ @Override
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
+ fireOnCastDevicesChanged(callback);
+ synchronized (mDiscoveringLock) {
+ handleDiscoveryChangeLocked();
+ }
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ mCallbacks.remove(callback);
+ synchronized (mDiscoveringLock) {
+ handleDiscoveryChangeLocked();
+ }
+ }
+
+ @Override
+ public void setDiscovering(boolean request) {
+ synchronized (mDiscoveringLock) {
+ if (mDiscovering == request) return;
+ mDiscovering = request;
+ if (DEBUG) Log.d(TAG, "setDiscovering: " + request);
+ handleDiscoveryChangeLocked();
+ }
+ }
+
+ private void handleDiscoveryChangeLocked() {
+ if (mCallbackRegistered) {
+ mMediaRouter.removeCallback(mMediaCallback);
+ mCallbackRegistered = false;
+ }
+ if (mDiscovering) {
+ mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaCallback,
+ MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ mCallbackRegistered = true;
+ } else if (mCallbacks.size() != 0) {
+ mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaCallback,
+ MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
+ mCallbackRegistered = true;
+ }
+ }
+
+ @Override
+ public void setCurrentUserId(int currentUserId) {
+ mMediaRouter.rebindAsUser(currentUserId);
+ }
+
+ @Override
+ public Set<CastDevice> getCastDevices() {
+ final ArraySet<CastDevice> devices = new ArraySet<CastDevice>();
+ synchronized (mProjectionLock) {
+ if (mProjection != null) {
+ final CastDevice device = new CastDevice();
+ device.id = mProjection.getPackageName();
+ device.name = getAppName(mProjection.getPackageName());
+ device.description = mContext.getString(R.string.quick_settings_casting);
+ device.state = CastDevice.STATE_CONNECTED;
+ device.tag = mProjection;
+ devices.add(device);
+ return devices;
+ }
+ }
+ synchronized(mRoutes) {
+ for (RouteInfo route : mRoutes.values()) {
+ final CastDevice device = new CastDevice();
+ device.id = route.getTag().toString();
+ final CharSequence name = route.getName(mContext);
+ device.name = name != null ? name.toString() : null;
+ final CharSequence description = route.getDescription();
+ device.description = description != null ? description.toString() : null;
+ device.state = route.isConnecting() ? CastDevice.STATE_CONNECTING
+ : route.isSelected() ? CastDevice.STATE_CONNECTED
+ : CastDevice.STATE_DISCONNECTED;
+ device.tag = route;
+ devices.add(device);
+ }
+ }
+ return devices;
+ }
+
+ @Override
+ public void startCasting(CastDevice device) {
+ if (device == null || device.tag == null) return;
+ final RouteInfo route = (RouteInfo) device.tag;
+ if (DEBUG) Log.d(TAG, "startCasting: " + routeToString(route));
+ mMediaRouter.selectRoute(ROUTE_TYPE_REMOTE_DISPLAY, route);
+ }
+
+ @Override
+ public void stopCasting(CastDevice device) {
+ final boolean isProjection = device.tag instanceof MediaProjectionInfo;
+ if (DEBUG) Log.d(TAG, "stopCasting isProjection=" + isProjection);
+ if (isProjection) {
+ final MediaProjectionInfo projection = (MediaProjectionInfo) device.tag;
+ if (Objects.equals(mProjectionManager.getActiveProjectionInfo(), projection)) {
+ mProjectionManager.stopActiveProjection();
+ } else {
+ Log.w(TAG, "Projection is no longer active: " + projection);
+ }
+ } else {
+ mMediaRouter.getFallbackRoute().select();
+ }
+ }
+
+ private void setProjection(MediaProjectionInfo projection, boolean started) {
+ boolean changed = false;
+ final MediaProjectionInfo oldProjection = mProjection;
+ synchronized (mProjectionLock) {
+ final boolean isCurrent = Objects.equals(projection, mProjection);
+ if (started && !isCurrent) {
+ mProjection = projection;
+ changed = true;
+ } else if (!started && isCurrent) {
+ mProjection = null;
+ changed = true;
+ }
+ }
+ if (changed) {
+ if (DEBUG) Log.d(TAG, "setProjection: " + oldProjection + " -> " + mProjection);
+ fireOnCastDevicesChanged();
+ }
+ }
+
+ private String getAppName(String packageName) {
+ final PackageManager pm = mContext.getPackageManager();
+ try {
+ final ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
+ if (appInfo != null) {
+ final CharSequence label = appInfo.loadLabel(pm);
+ if (!TextUtils.isEmpty(label)) {
+ return label.toString();
+ }
+ }
+ Log.w(TAG, "No label found for package: " + packageName);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Error getting appName for package: " + packageName, e);
+ }
+ return packageName;
+ }
+
+ private void updateRemoteDisplays() {
+ synchronized(mRoutes) {
+ mRoutes.clear();
+ final int n = mMediaRouter.getRouteCount();
+ for (int i = 0; i < n; i++) {
+ final RouteInfo route = mMediaRouter.getRouteAt(i);
+ if (!route.isEnabled()) continue;
+ if (!route.matchesTypes(ROUTE_TYPE_REMOTE_DISPLAY)) continue;
+ ensureTagExists(route);
+ mRoutes.put(route.getTag().toString(), route);
+ }
+ final RouteInfo selected = mMediaRouter.getSelectedRoute(ROUTE_TYPE_REMOTE_DISPLAY);
+ if (selected != null && !selected.isDefault()) {
+ ensureTagExists(selected);
+ mRoutes.put(selected.getTag().toString(), selected);
+ }
+ }
+ fireOnCastDevicesChanged();
+ }
+
+ private void ensureTagExists(RouteInfo route) {
+ if (route.getTag() == null) {
+ route.setTag(UUID.randomUUID().toString());
+ }
+ }
+
+ private void fireOnCastDevicesChanged() {
+ for (Callback callback : mCallbacks) {
+ fireOnCastDevicesChanged(callback);
+ }
+ }
+
+ private void fireOnCastDevicesChanged(Callback callback) {
+ callback.onCastDevicesChanged();
+ }
+
+ private static String routeToString(RouteInfo route) {
+ if (route == null) return null;
+ final StringBuilder sb = new StringBuilder().append(route.getName()).append('/')
+ .append(route.getDescription()).append('@').append(route.getDeviceAddress())
+ .append(",status=").append(route.getStatus());
+ if (route.isDefault()) sb.append(",default");
+ if (route.isEnabled()) sb.append(",enabled");
+ if (route.isConnecting()) sb.append(",connecting");
+ if (route.isSelected()) sb.append(",selected");
+ return sb.append(",id=").append(route.getTag()).toString();
+ }
+
+ private final MediaRouter.SimpleCallback mMediaCallback = new MediaRouter.SimpleCallback() {
+ @Override
+ public void onRouteAdded(MediaRouter router, RouteInfo route) {
+ if (DEBUG) Log.d(TAG, "onRouteAdded: " + routeToString(route));
+ updateRemoteDisplays();
+ }
+ @Override
+ public void onRouteChanged(MediaRouter router, RouteInfo route) {
+ if (DEBUG) Log.d(TAG, "onRouteChanged: " + routeToString(route));
+ updateRemoteDisplays();
+ }
+ @Override
+ public void onRouteRemoved(MediaRouter router, RouteInfo route) {
+ if (DEBUG) Log.d(TAG, "onRouteRemoved: " + routeToString(route));
+ updateRemoteDisplays();
+ }
+ @Override
+ public void onRouteSelected(MediaRouter router, int type, RouteInfo route) {
+ if (DEBUG) Log.d(TAG, "onRouteSelected(" + type + "): " + routeToString(route));
+ updateRemoteDisplays();
+ }
+ @Override
+ public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
+ if (DEBUG) Log.d(TAG, "onRouteUnselected(" + type + "): " + routeToString(route));
+ updateRemoteDisplays();
+ }
+ };
+
+ private final MediaProjectionManager.Callback mProjectionCallback
+ = new MediaProjectionManager.Callback() {
+ @Override
+ public void onStart(MediaProjectionInfo info) {
+ setProjection(info, true);
+ }
+
+ @Override
+ public void onStop(MediaProjectionInfo info) {
+ setProjection(info, false);
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/policy/Clock.java b/com/android/systemui/statusbar/policy/Clock.java
new file mode 100644
index 0000000..4c92d01
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/Clock.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2006 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 libcore.icu.LocaleData;
+
+import android.app.ActivityManager;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.format.DateFormat;
+import android.text.style.CharacterStyle;
+import android.text.style.RelativeSizeSpan;
+import android.util.AttributeSet;
+import android.view.Display;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.systemui.DemoMode;
+import com.android.systemui.Dependency;
+import com.android.systemui.FontSizeUtils;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Digital clock for the status bar.
+ */
+public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.Callbacks,
+ DarkReceiver, ConfigurationListener {
+
+ public static final String CLOCK_SECONDS = "clock_seconds";
+
+ private boolean mClockVisibleByPolicy = true;
+ private boolean mClockVisibleByUser = true;
+
+ private boolean mAttached;
+ private Calendar mCalendar;
+ private String mClockFormatString;
+ private SimpleDateFormat mClockFormat;
+ private SimpleDateFormat mContentDescriptionFormat;
+ private Locale mLocale;
+
+ private static final int AM_PM_STYLE_NORMAL = 0;
+ private static final int AM_PM_STYLE_SMALL = 1;
+ private static final int AM_PM_STYLE_GONE = 2;
+
+ private final int mAmPmStyle;
+ private final boolean mShowDark;
+ private boolean mShowSeconds;
+ private Handler mSecondsHandler;
+
+ public Clock(Context context) {
+ this(context, null);
+ }
+
+ public Clock(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public Clock(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ TypedArray a = context.getTheme().obtainStyledAttributes(
+ attrs,
+ R.styleable.Clock,
+ 0, 0);
+ try {
+ mAmPmStyle = a.getInt(R.styleable.Clock_amPmStyle, AM_PM_STYLE_GONE);
+ mShowDark = a.getBoolean(R.styleable.Clock_showDark, true);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (!mAttached) {
+ mAttached = true;
+ IntentFilter filter = new IntentFilter();
+
+ filter.addAction(Intent.ACTION_TIME_TICK);
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+
+ getContext().registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter,
+ null, Dependency.get(Dependency.TIME_TICK_HANDLER));
+ Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS,
+ StatusBarIconController.ICON_BLACKLIST);
+ SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
+ if (mShowDark) {
+ Dependency.get(DarkIconDispatcher.class).addDarkReceiver(this);
+ }
+ }
+
+ // NOTE: It's safe to do these after registering the receiver since the receiver always runs
+ // in the main thread, therefore the receiver can't run before this method returns.
+
+ // The time zone may have changed while the receiver wasn't registered, so update the Time
+ mCalendar = Calendar.getInstance(TimeZone.getDefault());
+
+ // Make sure we update to the current time
+ updateClock();
+ updateShowSeconds();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mAttached) {
+ getContext().unregisterReceiver(mIntentReceiver);
+ mAttached = false;
+ Dependency.get(TunerService.class).removeTunable(this);
+ SysUiServiceProvider.getComponent(getContext(), CommandQueue.class)
+ .removeCallbacks(this);
+ if (mShowDark) {
+ Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(this);
+ }
+ }
+ }
+
+ private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_TIMEZONE_CHANGED)) {
+ String tz = intent.getStringExtra("time-zone");
+ getHandler().post(() -> {
+ mCalendar = Calendar.getInstance(TimeZone.getTimeZone(tz));
+ if (mClockFormat != null) {
+ mClockFormat.setTimeZone(mCalendar.getTimeZone());
+ }
+ });
+ } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+ final Locale newLocale = getResources().getConfiguration().locale;
+ getHandler().post(() -> {
+ if (!newLocale.equals(mLocale)) {
+ mLocale = newLocale;
+ mClockFormatString = ""; // force refresh
+ }
+ });
+ }
+ getHandler().post(() -> updateClock());
+ }
+ };
+
+ public void setClockVisibleByUser(boolean visible) {
+ mClockVisibleByUser = visible;
+ updateClockVisibility();
+ }
+
+ public void setClockVisibilityByPolicy(boolean visible) {
+ mClockVisibleByPolicy = visible;
+ updateClockVisibility();
+ }
+
+ private void updateClockVisibility() {
+ boolean visible = mClockVisibleByPolicy && mClockVisibleByUser;
+ Dependency.get(IconLogger.class).onIconVisibility("clock", visible);
+ int visibility = visible ? View.VISIBLE : View.GONE;
+ setVisibility(visibility);
+ }
+
+ final void updateClock() {
+ if (mDemoMode) return;
+ mCalendar.setTimeInMillis(System.currentTimeMillis());
+ setText(getSmallTime());
+ setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime()));
+ }
+
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ if (CLOCK_SECONDS.equals(key)) {
+ mShowSeconds = newValue != null && Integer.parseInt(newValue) != 0;
+ updateShowSeconds();
+ } else {
+ setClockVisibleByUser(!StatusBarIconController.getIconBlacklist(newValue)
+ .contains("clock"));
+ updateClockVisibility();
+ }
+ }
+
+ @Override
+ public void disable(int state1, int state2, boolean animate) {
+ boolean clockVisibleByPolicy = (state1 & StatusBarManager.DISABLE_CLOCK) == 0;
+ if (clockVisibleByPolicy != mClockVisibleByPolicy) {
+ setClockVisibilityByPolicy(clockVisibleByPolicy);
+ }
+ }
+
+ @Override
+ public void onDarkChanged(Rect area, float darkIntensity, int tint) {
+ setTextColor(DarkIconDispatcher.getTint(area, this, tint));
+ }
+
+ @Override
+ public void onDensityOrFontScaleChanged() {
+ FontSizeUtils.updateFontSize(this, R.dimen.status_bar_clock_size);
+ setPaddingRelative(
+ mContext.getResources().getDimensionPixelSize(
+ R.dimen.status_bar_clock_starting_padding),
+ 0,
+ mContext.getResources().getDimensionPixelSize(
+ R.dimen.status_bar_clock_end_padding),
+ 0);
+ }
+
+ private void updateShowSeconds() {
+ if (mShowSeconds) {
+ // Wait until we have a display to start trying to show seconds.
+ if (mSecondsHandler == null && getDisplay() != null) {
+ mSecondsHandler = new Handler();
+ if (getDisplay().getState() == Display.STATE_ON) {
+ mSecondsHandler.postAtTime(mSecondTick,
+ SystemClock.uptimeMillis() / 1000 * 1000 + 1000);
+ }
+ IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ mContext.registerReceiver(mScreenReceiver, filter);
+ }
+ } else {
+ if (mSecondsHandler != null) {
+ mContext.unregisterReceiver(mScreenReceiver);
+ mSecondsHandler.removeCallbacks(mSecondTick);
+ mSecondsHandler = null;
+ updateClock();
+ }
+ }
+ }
+
+ private final CharSequence getSmallTime() {
+ Context context = getContext();
+ boolean is24 = DateFormat.is24HourFormat(context, ActivityManager.getCurrentUser());
+ LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
+
+ final char MAGIC1 = '\uEF00';
+ final char MAGIC2 = '\uEF01';
+
+ SimpleDateFormat sdf;
+ String format = mShowSeconds
+ ? is24 ? d.timeFormat_Hms : d.timeFormat_hms
+ : is24 ? d.timeFormat_Hm : d.timeFormat_hm;
+ if (!format.equals(mClockFormatString)) {
+ mContentDescriptionFormat = new SimpleDateFormat(format);
+ /*
+ * Search for an unquoted "a" in the format string, so we can
+ * add dummy characters around it to let us find it again after
+ * formatting and change its size.
+ */
+ if (mAmPmStyle != AM_PM_STYLE_NORMAL) {
+ int a = -1;
+ boolean quoted = false;
+ for (int i = 0; i < format.length(); i++) {
+ char c = format.charAt(i);
+
+ if (c == '\'') {
+ quoted = !quoted;
+ }
+ if (!quoted && c == 'a') {
+ a = i;
+ break;
+ }
+ }
+
+ if (a >= 0) {
+ // Move a back so any whitespace before AM/PM is also in the alternate size.
+ final int b = a;
+ while (a > 0 && Character.isWhitespace(format.charAt(a-1))) {
+ a--;
+ }
+ format = format.substring(0, a) + MAGIC1 + format.substring(a, b)
+ + "a" + MAGIC2 + format.substring(b + 1);
+ }
+ }
+ mClockFormat = sdf = new SimpleDateFormat(format);
+ mClockFormatString = format;
+ } else {
+ sdf = mClockFormat;
+ }
+ String result = sdf.format(mCalendar.getTime());
+
+ if (mAmPmStyle != AM_PM_STYLE_NORMAL) {
+ int magic1 = result.indexOf(MAGIC1);
+ int magic2 = result.indexOf(MAGIC2);
+ if (magic1 >= 0 && magic2 > magic1) {
+ SpannableStringBuilder formatted = new SpannableStringBuilder(result);
+ if (mAmPmStyle == AM_PM_STYLE_GONE) {
+ formatted.delete(magic1, magic2+1);
+ } else {
+ if (mAmPmStyle == AM_PM_STYLE_SMALL) {
+ CharacterStyle style = new RelativeSizeSpan(0.7f);
+ formatted.setSpan(style, magic1, magic2,
+ Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
+ }
+ formatted.delete(magic2, magic2 + 1);
+ formatted.delete(magic1, magic1 + 1);
+ }
+ return formatted;
+ }
+ }
+
+ return result;
+
+ }
+
+ private boolean mDemoMode;
+
+ @Override
+ public void dispatchDemoCommand(String command, Bundle args) {
+ if (!mDemoMode && command.equals(COMMAND_ENTER)) {
+ mDemoMode = true;
+ } else if (mDemoMode && command.equals(COMMAND_EXIT)) {
+ mDemoMode = false;
+ updateClock();
+ } else if (mDemoMode && command.equals(COMMAND_CLOCK)) {
+ String millis = args.getString("millis");
+ String hhmm = args.getString("hhmm");
+ if (millis != null) {
+ mCalendar.setTimeInMillis(Long.parseLong(millis));
+ } else if (hhmm != null && hhmm.length() == 4) {
+ int hh = Integer.parseInt(hhmm.substring(0, 2));
+ int mm = Integer.parseInt(hhmm.substring(2));
+ boolean is24 = DateFormat.is24HourFormat(
+ getContext(), ActivityManager.getCurrentUser());
+ if (is24) {
+ mCalendar.set(Calendar.HOUR_OF_DAY, hh);
+ } else {
+ mCalendar.set(Calendar.HOUR, hh);
+ }
+ mCalendar.set(Calendar.MINUTE, mm);
+ }
+ setText(getSmallTime());
+ setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime()));
+ }
+ }
+
+ private final BroadcastReceiver mScreenReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+ if (mSecondsHandler != null) {
+ mSecondsHandler.removeCallbacks(mSecondTick);
+ }
+ } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
+ if (mSecondsHandler != null) {
+ mSecondsHandler.postAtTime(mSecondTick,
+ SystemClock.uptimeMillis() / 1000 * 1000 + 1000);
+ }
+ }
+ }
+ };
+
+ private final Runnable mSecondTick = new Runnable() {
+ @Override
+ public void run() {
+ if (mCalendar != null) {
+ updateClock();
+ }
+ mSecondsHandler.postAtTime(this, SystemClock.uptimeMillis() / 1000 * 1000 + 1000);
+ }
+ };
+}
+
diff --git a/com/android/systemui/statusbar/policy/ConfigurationController.java b/com/android/systemui/statusbar/policy/ConfigurationController.java
new file mode 100644
index 0000000..3dca371
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ConfigurationController.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 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 android.content.res.Configuration;
+
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+
+/**
+ * Common listener for configuration or subsets of configuration changes (like density or
+ * font scaling), providing easy static dependence on these events.
+ */
+public interface ConfigurationController extends CallbackController<ConfigurationListener> {
+
+ interface ConfigurationListener {
+ default void onConfigChanged(Configuration newConfig) {}
+ default void onDensityOrFontScaleChanged() {}
+ default void onOverlayChanged() {}
+ default void onLocaleListChanged() {}
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/DarkIconDispatcher.java b/com/android/systemui/statusbar/policy/DarkIconDispatcher.java
new file mode 100644
index 0000000..58944c6
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DarkIconDispatcher.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017 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 android.graphics.Color;
+import android.graphics.Rect;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.systemui.statusbar.phone.LightBarTransitionsController;
+
+public interface DarkIconDispatcher {
+
+ void setIconsDarkArea(Rect r);
+ LightBarTransitionsController getTransitionsController();
+
+ void addDarkReceiver(DarkReceiver receiver);
+ void addDarkReceiver(ImageView imageView);
+
+ // Must have been previously been added through one of the addDarkReceive methods above.
+ void removeDarkReceiver(DarkReceiver object);
+ void removeDarkReceiver(ImageView object);
+
+ // Used to reapply darkness on an object, must have previously been added through
+ // addDarkReceiver.
+ void applyDark(ImageView object);
+
+ int DEFAULT_ICON_TINT = Color.WHITE;
+ Rect sTmpRect = new Rect();
+ int[] sTmpInt2 = new int[2];
+
+ /**
+ * @return the tint to apply to {@param view} depending on the desired tint {@param color} and
+ * the screen {@param tintArea} in which to apply that tint
+ */
+ static int getTint(Rect tintArea, View view, int color) {
+ if (isInArea(tintArea, view)) {
+ return color;
+ } else {
+ return DEFAULT_ICON_TINT;
+ }
+ }
+
+ /**
+ * @return the dark intensity to apply to {@param view} depending on the desired dark
+ * {@param intensity} and the screen {@param tintArea} in which to apply that intensity
+ */
+ static float getDarkIntensity(Rect tintArea, View view, float intensity) {
+ if (isInArea(tintArea, view)) {
+ return intensity;
+ } else {
+ return 0f;
+ }
+ }
+
+ /**
+ * @return true if more than half of the {@param view} area are in {@param area}, false
+ * otherwise
+ */
+ static boolean isInArea(Rect area, View view) {
+ if (area.isEmpty()) {
+ return true;
+ }
+ sTmpRect.set(area);
+ view.getLocationOnScreen(sTmpInt2);
+ int left = sTmpInt2[0];
+
+ int intersectStart = Math.max(left, area.left);
+ int intersectEnd = Math.min(left + view.getWidth(), area.right);
+ int intersectAmount = Math.max(0, intersectEnd - intersectStart);
+
+ boolean coversFullStatusBar = area.top <= 0;
+ boolean majorityOfWidth = 2 * intersectAmount > view.getWidth();
+ return majorityOfWidth && coversFullStatusBar;
+ }
+
+ interface DarkReceiver {
+ void onDarkChanged(Rect area, float darkIntensity, int tint);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/DataSaverController.java b/com/android/systemui/statusbar/policy/DataSaverController.java
new file mode 100644
index 0000000..0df7859
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DataSaverController.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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.DataSaverController.Listener;
+
+public interface DataSaverController extends CallbackController<Listener> {
+
+ boolean isDataSaverEnabled();
+ void setDataSaverEnabled(boolean enabled);
+
+ public interface Listener {
+ void onDataSaverChanged(boolean isDataSaving);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java b/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
new file mode 100644
index 0000000..2951943
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2016 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 android.content.Context;
+import android.net.INetworkPolicyListener;
+import android.net.NetworkPolicyManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+
+import com.android.systemui.statusbar.policy.DataSaverController.Listener;
+
+import java.util.ArrayList;
+
+public class DataSaverControllerImpl implements DataSaverController {
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final ArrayList<Listener> mListeners = new ArrayList<>();
+ private final NetworkPolicyManager mPolicyManager;
+
+ public DataSaverControllerImpl(Context context) {
+ mPolicyManager = NetworkPolicyManager.from(context);
+ }
+
+ private void handleRestrictBackgroundChanged(boolean isDataSaving) {
+ synchronized (mListeners) {
+ for (int i = 0; i < mListeners.size(); i++) {
+ mListeners.get(i).onDataSaverChanged(isDataSaving);
+ }
+ }
+ }
+
+ public void addCallback(Listener listener) {
+ synchronized (mListeners) {
+ mListeners.add(listener);
+ if (mListeners.size() == 1) {
+ mPolicyManager.registerListener(mPolicyListener);
+ }
+ }
+ listener.onDataSaverChanged(isDataSaverEnabled());
+ }
+
+ public void removeCallback(Listener listener) {
+ synchronized (mListeners) {
+ mListeners.remove(listener);
+ if (mListeners.size() == 0) {
+ mPolicyManager.unregisterListener(mPolicyListener);
+ }
+ }
+ }
+
+ public boolean isDataSaverEnabled() {
+ return mPolicyManager.getRestrictBackground();
+ }
+
+ public void setDataSaverEnabled(boolean enabled) {
+ mPolicyManager.setRestrictBackground(enabled);
+ try {
+ mPolicyListener.onRestrictBackgroundChanged(enabled);
+ } catch (RemoteException e) {
+ }
+ }
+
+ private final INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
+ @Override
+ public void onUidRulesChanged(int uid, int uidRules) throws RemoteException {
+ }
+
+ @Override
+ public void onMeteredIfacesChanged(String[] strings) throws RemoteException {
+ }
+
+ @Override
+ public void onRestrictBackgroundChanged(final boolean isDataSaving) throws RemoteException {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ handleRestrictBackgroundChanged(isDataSaving);
+ }
+ });
+ }
+
+ @Override
+ public void onUidPoliciesChanged(int uid, int uidPolicies) throws RemoteException {
+ }
+ };
+
+}
diff --git a/com/android/systemui/statusbar/policy/DateView.java b/com/android/systemui/statusbar/policy/DateView.java
new file mode 100644
index 0000000..74a30fa
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DateView.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.icu.text.DateFormat;
+import android.icu.text.DisplayContext;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+
+import java.util.Date;
+import java.util.Locale;
+
+public class DateView extends TextView {
+ private static final String TAG = "DateView";
+
+ private final Date mCurrentTime = new Date();
+
+ private DateFormat mDateFormat;
+ private String mLastText;
+ private String mDatePattern;
+
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_TIME_TICK.equals(action)
+ || Intent.ACTION_TIME_CHANGED.equals(action)
+ || Intent.ACTION_TIMEZONE_CHANGED.equals(action)
+ || Intent.ACTION_LOCALE_CHANGED.equals(action)) {
+ if (Intent.ACTION_LOCALE_CHANGED.equals(action)
+ || Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
+ // need to get a fresh date format
+ getHandler().post(() -> mDateFormat = null);
+ }
+ getHandler().post(() -> updateClock());
+ }
+ }
+ };
+
+ public DateView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.getTheme().obtainStyledAttributes(
+ attrs,
+ R.styleable.DateView,
+ 0, 0);
+
+ try {
+ mDatePattern = a.getString(R.styleable.DateView_datePattern);
+ } finally {
+ a.recycle();
+ }
+ if (mDatePattern == null) {
+ mDatePattern = getContext().getString(R.string.system_ui_date_pattern);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_TIME_TICK);
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ filter.addAction(Intent.ACTION_LOCALE_CHANGED);
+ getContext().registerReceiver(mIntentReceiver, filter, null,
+ Dependency.get(Dependency.TIME_TICK_HANDLER));
+
+ updateClock();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ mDateFormat = null; // reload the locale next time
+ getContext().unregisterReceiver(mIntentReceiver);
+ }
+
+ protected void updateClock() {
+ if (mDateFormat == null) {
+ final Locale l = Locale.getDefault();
+ DateFormat format = DateFormat.getInstanceForSkeleton(mDatePattern, l);
+ format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
+ mDateFormat = format;
+ }
+
+ mCurrentTime.setTime(System.currentTimeMillis());
+
+ final String text = mDateFormat.format(mCurrentTime);
+ if (!text.equals(mLastText)) {
+ setText(text);
+ mLastText = text;
+ }
+ }
+
+ public void setDatePattern(String pattern) {
+ if (TextUtils.equals(pattern, mDatePattern)) {
+ return;
+ }
+ mDatePattern = pattern;
+ mDateFormat = null;
+ if (isAttachedToWindow()) {
+ updateClock();
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/DeadZone.java b/com/android/systemui/statusbar/policy/DeadZone.java
new file mode 100644
index 0000000..06040e2
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DeadZone.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2012 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 android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Slog;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.phone.StatusBar;
+
+/**
+ * The "dead zone" consumes unintentional taps along the top edge of the navigation bar.
+ * When users are typing quickly on an IME they may attempt to hit the space bar, overshoot, and
+ * accidentally hit the home button. The DeadZone expands temporarily after each tap in the UI
+ * outside the navigation bar (since this is when accidental taps are more likely), then contracts
+ * back over time (since a later tap might be intended for the top of the bar).
+ */
+public class DeadZone extends View {
+ public static final String TAG = "DeadZone";
+
+ public static final boolean DEBUG = false;
+ public static final int HORIZONTAL = 0; // Consume taps along the top edge.
+ public static final int VERTICAL = 1; // Consume taps along the left edge.
+
+ private static final boolean CHATTY = true; // print to logcat when we eat a click
+ private final StatusBar mStatusBar;
+
+ private boolean mShouldFlash;
+ private float mFlashFrac = 0f;
+
+ private int mSizeMax;
+ private int mSizeMin;
+ // Upon activity elsewhere in the UI, the dead zone will hold steady for
+ // mHold ms, then move back over the course of mDecay ms
+ private int mHold, mDecay;
+ private boolean mVertical;
+ private long mLastPokeTime;
+ private int mDisplayRotation;
+
+ private final Runnable mDebugFlash = new Runnable() {
+ @Override
+ public void run() {
+ ObjectAnimator.ofFloat(DeadZone.this, "flash", 1f, 0f).setDuration(150).start();
+ }
+ };
+
+ public DeadZone(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DeadZone(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DeadZone,
+ defStyle, 0);
+
+ mHold = a.getInteger(R.styleable.DeadZone_holdTime, 0);
+ mDecay = a.getInteger(R.styleable.DeadZone_decayTime, 0);
+
+ mSizeMin = a.getDimensionPixelSize(R.styleable.DeadZone_minSize, 0);
+ mSizeMax = a.getDimensionPixelSize(R.styleable.DeadZone_maxSize, 0);
+
+ int index = a.getInt(R.styleable.DeadZone_orientation, -1);
+ mVertical = (index == VERTICAL);
+
+ if (DEBUG)
+ Slog.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
+ + (mVertical ? " vertical" : " horizontal"));
+
+ setFlashOnTouchCapture(context.getResources().getBoolean(R.bool.config_dead_zone_flash));
+ mStatusBar = SysUiServiceProvider.getComponent(context, StatusBar.class);
+ }
+
+ static float lerp(float a, float b, float f) {
+ return (b - a) * f + a;
+ }
+
+ private float getSize(long now) {
+ if (mSizeMax == 0)
+ return 0;
+ long dt = (now - mLastPokeTime);
+ if (dt > mHold + mDecay)
+ return mSizeMin;
+ if (dt < mHold)
+ return mSizeMax;
+ return (int) lerp(mSizeMax, mSizeMin, (float) (dt - mHold) / mDecay);
+ }
+
+ public void setFlashOnTouchCapture(boolean dbg) {
+ mShouldFlash = dbg;
+ mFlashFrac = 0f;
+ postInvalidate();
+ }
+
+ // I made you a touch event...
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (DEBUG) {
+ Slog.v(TAG, this + " onTouch: " + MotionEvent.actionToString(event.getAction()));
+ }
+
+ // Don't consume events for high precision pointing devices. For this purpose a stylus is
+ // considered low precision (like a finger), so its events may be consumed.
+ if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
+ return false;
+ }
+
+ final int action = event.getAction();
+ if (action == MotionEvent.ACTION_OUTSIDE) {
+ poke(event);
+ return true;
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ if (DEBUG) {
+ Slog.v(TAG, this + " ACTION_DOWN: " + event.getX() + "," + event.getY());
+ }
+ if (mStatusBar != null) mStatusBar.touchAutoDim();
+ int size = (int) getSize(event.getEventTime());
+ // In the vertical orientation consume taps along the left edge.
+ // In horizontal orientation consume taps along the top edge.
+ final boolean consumeEvent;
+ if (mVertical) {
+ if (mDisplayRotation == Surface.ROTATION_270) {
+ consumeEvent = event.getX() > getWidth() - size;
+ } else {
+ consumeEvent = event.getX() < size;
+ }
+ } else {
+ consumeEvent = event.getY() < size;
+ }
+ if (consumeEvent) {
+ if (CHATTY) {
+ Slog.v(TAG, "consuming errant click: (" + event.getX() + "," + event.getY() + ")");
+ }
+ if (mShouldFlash) {
+ post(mDebugFlash);
+ postInvalidate();
+ }
+ return true; // ...but I eated it
+ }
+ }
+ return false;
+ }
+
+ private void poke(MotionEvent event) {
+ mLastPokeTime = event.getEventTime();
+ if (DEBUG)
+ Slog.v(TAG, "poked! size=" + getSize(mLastPokeTime));
+ if (mShouldFlash) postInvalidate();
+ }
+
+ public void setFlash(float f) {
+ mFlashFrac = f;
+ postInvalidate();
+ }
+
+ public float getFlash() {
+ return mFlashFrac;
+ }
+
+ @Override
+ public void onDraw(Canvas can) {
+ if (!mShouldFlash || mFlashFrac <= 0f) {
+ return;
+ }
+
+ final int size = (int) getSize(SystemClock.uptimeMillis());
+ if (mVertical) {
+ if (mDisplayRotation == Surface.ROTATION_270) {
+ can.clipRect(can.getWidth() - size, 0, can.getWidth(), can.getHeight());
+ } else {
+ can.clipRect(0, 0, size, can.getHeight());
+ }
+ } else {
+ can.clipRect(0, 0, can.getWidth(), size);
+ }
+
+ final float frac = DEBUG ? (mFlashFrac - 0.5f) + 0.5f : mFlashFrac;
+ can.drawARGB((int) (frac * 0xFF), 0xDD, 0xEE, 0xAA);
+
+ if (DEBUG && size > mSizeMin)
+ // crazy aggressive redrawing here, for debugging only
+ postInvalidateDelayed(100);
+ }
+
+ public void setDisplayRotation(int rotation) {
+ mDisplayRotation = rotation;
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/DeviceProvisionedController.java b/com/android/systemui/statusbar/policy/DeviceProvisionedController.java
new file mode 100644
index 0000000..cae76b4
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DeviceProvisionedController.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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 android.content.Context;
+
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+
+public interface DeviceProvisionedController extends CallbackController<DeviceProvisionedListener> {
+
+ boolean isDeviceProvisioned();
+ boolean isUserSetup(int currentUser);
+ int getCurrentUser();
+
+ default boolean isCurrentUserSetup() {
+ return isUserSetup(getCurrentUser());
+ }
+
+ interface DeviceProvisionedListener {
+ default void onDeviceProvisionedChanged() { }
+ default void onUserSwitched() {
+ onUserSetupChanged();
+ }
+ default void onUserSetupChanged() { }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java b/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java
new file mode 100644
index 0000000..f2283a5
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 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 android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings.Global;
+import android.provider.Settings.Secure;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.settings.CurrentUserTracker;
+
+import java.util.ArrayList;
+
+public class DeviceProvisionedControllerImpl extends CurrentUserTracker implements
+ DeviceProvisionedController {
+
+ private final ArrayList<DeviceProvisionedListener> mListeners = new ArrayList<>();
+ private final ContentResolver mContentResolver;
+ private final Context mContext;
+ private final Uri mDeviceProvisionedUri;
+ private final Uri mUserSetupUri;
+
+ public DeviceProvisionedControllerImpl(Context context) {
+ super(context);
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mDeviceProvisionedUri = Global.getUriFor(Global.DEVICE_PROVISIONED);
+ mUserSetupUri = Secure.getUriFor(Secure.USER_SETUP_COMPLETE);
+ }
+
+ @Override
+ public boolean isDeviceProvisioned() {
+ return Global.getInt(mContentResolver, Global.DEVICE_PROVISIONED, 0) != 0;
+ }
+
+ @Override
+ public boolean isUserSetup(int currentUser) {
+ return Secure.getIntForUser(mContentResolver, Secure.USER_SETUP_COMPLETE, 0, currentUser)
+ != 0;
+ }
+
+ @Override
+ public int getCurrentUser() {
+ return ActivityManager.getCurrentUser();
+ }
+
+ @Override
+ public void addCallback(DeviceProvisionedListener listener) {
+ mListeners.add(listener);
+ if (mListeners.size() == 1) {
+ startListening(getCurrentUser());
+ }
+ listener.onUserSetupChanged();
+ listener.onDeviceProvisionedChanged();
+ }
+
+ @Override
+ public void removeCallback(DeviceProvisionedListener listener) {
+ mListeners.remove(listener);
+ if (mListeners.size() == 0) {
+ stopListening();
+ }
+ }
+
+ private void startListening(int user) {
+ mContentResolver.registerContentObserver(mDeviceProvisionedUri, true,
+ mSettingsObserver, 0);
+ mContentResolver.registerContentObserver(mUserSetupUri, true,
+ mSettingsObserver, user);
+ startTracking();
+ }
+
+ private void stopListening() {
+ stopTracking();
+ mContentResolver.unregisterContentObserver(mSettingsObserver);
+ }
+
+ @Override
+ public void onUserSwitched(int newUserId) {
+ mContentResolver.unregisterContentObserver(mSettingsObserver);
+ mContentResolver.registerContentObserver(mDeviceProvisionedUri, true,
+ mSettingsObserver, 0);
+ mContentResolver.registerContentObserver(mUserSetupUri, true,
+ mSettingsObserver, newUserId);
+ notifyUserChanged();
+ }
+
+ private void notifyUserChanged() {
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onUserSwitched();
+ }
+ }
+
+ private void notifySetupChanged() {
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onUserSetupChanged();
+ }
+ }
+
+ private void notifyProvisionedChanged() {
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onDeviceProvisionedChanged();
+ }
+ }
+
+ protected final ContentObserver mSettingsObserver = new ContentObserver(Dependency.get(
+ Dependency.MAIN_HANDLER)) {
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri, int userId) {
+ if (mUserSetupUri.equals(uri)) {
+ notifySetupChanged();
+ } else {
+ notifyProvisionedChanged();
+ }
+ }
+ };
+}
diff --git a/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java b/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
new file mode 100644
index 0000000..c726189
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 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 android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.provider.Settings;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.TextView;
+
+import com.android.internal.telephony.IccCardConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+
+import java.util.List;
+
+public class EmergencyCryptkeeperText extends TextView {
+
+ private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onPhoneStateChanged(int phoneState) {
+ update();
+ }
+
+ @Override
+ public void onRefreshCarrierInfo() {
+ update();
+ }
+ };
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
+ update();
+ }
+ }
+ };
+
+ public EmergencyCryptkeeperText(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ setVisibility(GONE);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+ mKeyguardUpdateMonitor.registerCallback(mCallback);
+ getContext().registerReceiver(mReceiver,
+ new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+ update();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mKeyguardUpdateMonitor != null) {
+ mKeyguardUpdateMonitor.removeCallback(mCallback);
+ }
+ getContext().unregisterReceiver(mReceiver);
+ }
+
+ public void update() {
+ boolean hasMobile = ConnectivityManager.from(mContext)
+ .isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
+ boolean airplaneMode = (Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) == 1);
+
+ if (!hasMobile || airplaneMode) {
+ setText(null);
+ setVisibility(GONE);
+ return;
+ }
+
+ boolean allSimsMissing = true;
+ CharSequence displayText = null;
+
+ List<SubscriptionInfo> subs = mKeyguardUpdateMonitor.getSubscriptionInfo(false);
+ final int N = subs.size();
+ for (int i = 0; i < N; i++) {
+ int subId = subs.get(i).getSubscriptionId();
+ IccCardConstants.State simState = mKeyguardUpdateMonitor.getSimState(subId);
+ CharSequence carrierName = subs.get(i).getCarrierName();
+ if (simState.iccCardExist() && !TextUtils.isEmpty(carrierName)) {
+ allSimsMissing = false;
+ displayText = carrierName;
+ }
+ }
+ if (allSimsMissing) {
+ if (N != 0) {
+ // Shows "Emergency calls only" on devices that are voice-capable.
+ // This depends on mPlmn containing the text "Emergency calls only" when the radio
+ // has some connectivity. Otherwise it should show "No service"
+ // Grab the first subscription, because they all should contain the emergency text,
+ // described above.
+ displayText = subs.get(0).getCarrierName();
+ } else {
+ // We don't have a SubscriptionInfo to get the emergency calls only from.
+ // Grab it from the old sticky broadcast if possible instead. We can use it
+ // here because no subscriptions are active, so we don't have
+ // to worry about MSIM clashing.
+ displayText = getContext().getText(
+ com.android.internal.R.string.emergency_calls_only);
+ Intent i = getContext().registerReceiver(null,
+ new IntentFilter(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION));
+ if (i != null) {
+ displayText = i.getStringExtra(TelephonyIntents.EXTRA_PLMN);
+ }
+ }
+ }
+
+ setText(displayText);
+ setVisibility(TextUtils.isEmpty(displayText) ? GONE : VISIBLE);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/EncryptionHelper.java b/com/android/systemui/statusbar/policy/EncryptionHelper.java
new file mode 100644
index 0000000..639e50c
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/EncryptionHelper.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 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 android.os.SystemProperties;
+
+/**
+ * Helper for determining whether the phone is decrypted yet.
+ */
+public class EncryptionHelper {
+
+ public static final boolean IS_DATA_ENCRYPTED = isDataEncrypted();
+
+ private static boolean isDataEncrypted() {
+ String voldState = SystemProperties.get("vold.decrypt");
+ return "1".equals(voldState) || "trigger_restart_min_framework".equals(voldState);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/EthernetIcons.java b/com/android/systemui/statusbar/policy/EthernetIcons.java
new file mode 100644
index 0000000..b391bd9
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/EthernetIcons.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2015 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.R;
+
+class EthernetIcons {
+ static final int[][] ETHERNET_ICONS = {
+ { R.drawable.stat_sys_ethernet },
+ { R.drawable.stat_sys_ethernet_fully },
+ };
+}
diff --git a/com/android/systemui/statusbar/policy/EthernetSignalController.java b/com/android/systemui/statusbar/policy/EthernetSignalController.java
new file mode 100644
index 0000000..159bd41
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/EthernetSignalController.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 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 android.content.Context;
+import android.net.NetworkCapabilities;
+
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.util.BitSet;
+
+
+public class EthernetSignalController extends
+ SignalController<SignalController.State, SignalController.IconGroup> {
+
+ public EthernetSignalController(Context context,
+ CallbackHandler callbackHandler, NetworkControllerImpl networkController) {
+ super("EthernetSignalController", context, NetworkCapabilities.TRANSPORT_ETHERNET,
+ callbackHandler, networkController);
+ mCurrentState.iconGroup = mLastState.iconGroup = new IconGroup(
+ "Ethernet Icons",
+ EthernetIcons.ETHERNET_ICONS,
+ null,
+ AccessibilityContentDescriptions.ETHERNET_CONNECTION_VALUES,
+ 0, 0, 0, 0,
+ AccessibilityContentDescriptions.ETHERNET_CONNECTION_VALUES[0]);
+ }
+
+ @Override
+ public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) {
+ mCurrentState.connected = connectedTransports.get(mTransportType);
+ super.updateConnectivity(connectedTransports, validatedTransports);
+ }
+
+ @Override
+ public void notifyListeners(SignalCallback callback) {
+ boolean ethernetVisible = mCurrentState.connected;
+ String contentDescription = getStringIfExists(getContentDescription());
+
+ // TODO: wire up data transfer using WifiSignalPoller.
+ callback.setEthernetIndicators(new IconState(ethernetVisible, getCurrentIconId(),
+ contentDescription));
+ }
+
+ @Override
+ public SignalController.State cleanState() {
+ return new SignalController.State();
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/ExtensionController.java b/com/android/systemui/statusbar/policy/ExtensionController.java
new file mode 100644
index 0000000..cade5dc
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ExtensionController.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 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 android.content.Context;
+
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Utility class used to select between a plugin, tuner settings, and a default implementation
+ * of an interface.
+ */
+public interface ExtensionController {
+
+ <T> ExtensionBuilder<T> newExtension(Class<T> cls);
+
+ interface Extension<T> {
+ T get();
+ Context getContext();
+ void destroy();
+ void addCallback(Consumer<T> callback);
+ /**
+ * Triggers the extension to cycle through each of the sources again because something
+ * (like configuration) may have changed.
+ */
+ T reload();
+
+ /**
+ * Null out the cached item for the purpose of memory saving, should only be done
+ * when any other references are already gotten.
+ * @param isDestroyed
+ */
+ void clearItem(boolean isDestroyed);
+ }
+
+ interface ExtensionBuilder<T> {
+ ExtensionBuilder<T> withTunerFactory(TunerFactory<T> factory);
+ <P extends T> ExtensionBuilder<T> withPlugin(Class<P> cls);
+ <P extends T> ExtensionBuilder<T> withPlugin(Class<P> cls, String action);
+ <P> ExtensionBuilder<T> withPlugin(Class<P> cls, String action,
+ PluginConverter<T, P> converter);
+ ExtensionBuilder<T> withDefault(Supplier<T> def);
+ ExtensionBuilder<T> withCallback(Consumer<T> callback);
+ ExtensionBuilder<T> withUiMode(int mode, Supplier<T> def);
+ ExtensionBuilder<T> withFeature(String feature, Supplier<T> def);
+ Extension build();
+ }
+
+ public interface PluginConverter<T, P> {
+ T getInterfaceFromPlugin(P plugin);
+ }
+
+ public interface TunerFactory<T> {
+ String[] keys();
+ T create(Map<String, String> settings);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java b/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java
new file mode 100644
index 0000000..6d75cfc
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2017 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 android.content.Context;
+import android.content.res.Configuration;
+import android.os.Handler;
+import android.util.ArrayMap;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+import com.android.systemui.util.leak.LeakDetector;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class ExtensionControllerImpl implements ExtensionController {
+
+ public static final int SORT_ORDER_PLUGIN = 0;
+ public static final int SORT_ORDER_TUNER = 1;
+ public static final int SORT_ORDER_FEATURE = 2;
+ public static final int SORT_ORDER_UI_MODE = 3;
+ public static final int SORT_ORDER_DEFAULT = 4;
+
+ private final Context mDefaultContext;
+
+ public ExtensionControllerImpl(Context context) {
+ mDefaultContext = context;
+ }
+
+ @Override
+ public <T> ExtensionBuilder<T> newExtension(Class<T> cls) {
+ return new ExtensionBuilder<>();
+ }
+
+ private interface Producer<T> {
+ T get();
+
+ void destroy();
+ }
+
+ private class ExtensionBuilder<T> implements ExtensionController.ExtensionBuilder<T> {
+
+ private ExtensionImpl<T> mExtension = new ExtensionImpl<>();
+
+ @Override
+ public ExtensionController.ExtensionBuilder<T> withTunerFactory(TunerFactory<T> factory) {
+ mExtension.addTunerFactory(factory, factory.keys());
+ return this;
+ }
+
+ @Override
+ public <P extends T> ExtensionController.ExtensionBuilder<T> withPlugin(Class<P> cls) {
+ return withPlugin(cls, PluginManager.getAction(cls));
+ }
+
+ @Override
+ public <P extends T> ExtensionController.ExtensionBuilder<T> withPlugin(Class<P> cls,
+ String action) {
+ return withPlugin(cls, action, null);
+ }
+
+ @Override
+ public <P> ExtensionController.ExtensionBuilder<T> withPlugin(Class<P> cls,
+ String action, PluginConverter<T, P> converter) {
+ mExtension.addPlugin(action, cls, converter);
+ return this;
+ }
+
+ @Override
+ public ExtensionController.ExtensionBuilder<T> withDefault(Supplier<T> def) {
+ mExtension.addDefault(def);
+ return this;
+ }
+
+ @Override
+ public ExtensionController.ExtensionBuilder<T> withUiMode(int uiMode,
+ Supplier<T> supplier) {
+ mExtension.addUiMode(uiMode, supplier);
+ return this;
+ }
+
+ @Override
+ public ExtensionController.ExtensionBuilder<T> withFeature(String feature,
+ Supplier<T> supplier) {
+ mExtension.addFeature(feature, supplier);
+ return this;
+ }
+
+ @Override
+ public ExtensionController.ExtensionBuilder<T> withCallback(
+ Consumer<T> callback) {
+ mExtension.mCallbacks.add(callback);
+ return this;
+ }
+
+ @Override
+ public ExtensionController.Extension build() {
+ // Sort items in ascending order
+ Collections.sort(mExtension.mProducers, Comparator.comparingInt(Item::sortOrder));
+ mExtension.notifyChanged();
+ return mExtension;
+ }
+ }
+
+ private class ExtensionImpl<T> implements ExtensionController.Extension<T> {
+ private final ArrayList<Item<T>> mProducers = new ArrayList<>();
+ private final ArrayList<Consumer<T>> mCallbacks = new ArrayList<>();
+ private T mItem;
+ private Context mPluginContext;
+
+ public void addCallback(Consumer<T> callback) {
+ mCallbacks.add(callback);
+ }
+
+ @Override
+ public T get() {
+ return mItem;
+ }
+
+ @Override
+ public Context getContext() {
+ return mPluginContext != null ? mPluginContext : mDefaultContext;
+ }
+
+ @Override
+ public void destroy() {
+ for (int i = 0; i < mProducers.size(); i++) {
+ mProducers.get(i).destroy();
+ }
+ }
+
+ @Override
+ public T reload() {
+ notifyChanged();
+ return get();
+ }
+
+ @Override
+ public void clearItem(boolean isDestroyed) {
+ if (isDestroyed && mItem != null) {
+ Dependency.get(LeakDetector.class).trackGarbage(mItem);
+ }
+ mItem = null;
+ }
+
+ private void notifyChanged() {
+ if (mItem != null) {
+ Dependency.get(LeakDetector.class).trackGarbage(mItem);
+ }
+ mItem = null;
+ for (int i = 0; i < mProducers.size(); i++) {
+ final T item = mProducers.get(i).get();
+ if (item != null) {
+ mItem = item;
+ break;
+ }
+ }
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).accept(mItem);
+ }
+ }
+
+ public void addDefault(Supplier<T> def) {
+ mProducers.add(new Default(def));
+ }
+
+ public <P> void addPlugin(String action, Class<P> cls, PluginConverter<T, P> converter) {
+ mProducers.add(new PluginItem(action, cls, converter));
+ }
+
+ public void addTunerFactory(TunerFactory<T> factory, String[] keys) {
+ mProducers.add(new TunerItem(factory, keys));
+ }
+
+ public void addUiMode(int uiMode, Supplier<T> mode) {
+ mProducers.add(new UiModeItem(uiMode, mode));
+ }
+
+ public void addFeature(String feature, Supplier<T> mode) {
+ mProducers.add(new FeatureItem<>(feature, mode));
+ }
+
+ private class PluginItem<P extends Plugin> implements Item<T>, PluginListener<P> {
+ private final PluginConverter<T, P> mConverter;
+ private T mItem;
+
+ public PluginItem(String action, Class<P> cls, PluginConverter<T, P> converter) {
+ mConverter = converter;
+ Dependency.get(PluginManager.class).addPluginListener(action, this, cls);
+ }
+
+ @Override
+ public void onPluginConnected(P plugin, Context pluginContext) {
+ mPluginContext = pluginContext;
+ if (mConverter != null) {
+ mItem = mConverter.getInterfaceFromPlugin(plugin);
+ } else {
+ mItem = (T) plugin;
+ }
+ notifyChanged();
+ }
+
+ @Override
+ public void onPluginDisconnected(P plugin) {
+ mPluginContext = null;
+ mItem = null;
+ notifyChanged();
+ }
+
+ @Override
+ public T get() {
+ return mItem;
+ }
+
+ @Override
+ public void destroy() {
+ Dependency.get(PluginManager.class).removePluginListener(this);
+ }
+
+ @Override
+ public int sortOrder() {
+ return SORT_ORDER_PLUGIN;
+ }
+ }
+
+ private class TunerItem<T> implements Item<T>, Tunable {
+ private final TunerFactory<T> mFactory;
+ private final ArrayMap<String, String> mSettings = new ArrayMap<>();
+ private T mItem;
+
+ public TunerItem(TunerFactory<T> factory, String... setting) {
+ mFactory = factory;
+ Dependency.get(TunerService.class).addTunable(this, setting);
+ }
+
+ @Override
+ public T get() {
+ return mItem;
+ }
+
+ @Override
+ public void destroy() {
+ Dependency.get(TunerService.class).removeTunable(this);
+ }
+
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ mSettings.put(key, newValue);
+ mItem = mFactory.create(mSettings);
+ notifyChanged();
+ }
+
+ @Override
+ public int sortOrder() {
+ return SORT_ORDER_TUNER;
+ }
+ }
+
+ private class UiModeItem<T> implements Item<T>, ConfigurationListener {
+
+ private final int mDesiredUiMode;
+ private final Supplier<T> mSupplier;
+ private int mUiMode;
+ private Handler mHandler = new Handler();
+
+ public UiModeItem(int uiMode, Supplier<T> supplier) {
+ mDesiredUiMode = uiMode;
+ mSupplier = supplier;
+ mUiMode = mDefaultContext.getResources().getConfiguration().uiMode
+ & Configuration.UI_MODE_TYPE_MASK;
+ Dependency.get(ConfigurationController.class).addCallback(this);
+ }
+
+ @Override
+ public void onConfigChanged(Configuration newConfig) {
+ int newMode = newConfig.uiMode & Configuration.UI_MODE_TYPE_MASK;
+ if (newMode != mUiMode) {
+ mUiMode = newMode;
+ // Post to make sure we don't have concurrent modifications.
+ mHandler.post(ExtensionImpl.this::notifyChanged);
+ }
+ }
+
+ @Override
+ public T get() {
+ return (mUiMode == mDesiredUiMode) ? mSupplier.get() : null;
+ }
+
+ @Override
+ public void destroy() {
+ Dependency.get(ConfigurationController.class).removeCallback(this);
+ }
+
+ @Override
+ public int sortOrder() {
+ return SORT_ORDER_UI_MODE;
+ }
+ }
+
+ private class FeatureItem<T> implements Item<T> {
+ private final String mFeature;
+ private final Supplier<T> mSupplier;
+
+ public FeatureItem(String feature, Supplier<T> supplier) {
+ mSupplier = supplier;
+ mFeature = feature;
+ }
+
+ @Override
+ public T get() {
+ return mDefaultContext.getPackageManager().hasSystemFeature(mFeature)
+ ? mSupplier.get() : null;
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+
+ @Override
+ public int sortOrder() {
+ return SORT_ORDER_FEATURE;
+ }
+ }
+
+ private class Default<T> implements Item<T> {
+ private final Supplier<T> mSupplier;
+
+ public Default(Supplier<T> supplier) {
+ mSupplier = supplier;
+ }
+
+ @Override
+ public T get() {
+ return mSupplier.get();
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+
+ @Override
+ public int sortOrder() {
+ return SORT_ORDER_DEFAULT;
+ }
+ }
+ }
+
+ private interface Item<T> extends Producer<T> {
+ int sortOrder();
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/FlashlightController.java b/com/android/systemui/statusbar/policy/FlashlightController.java
new file mode 100644
index 0000000..e576f36
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/FlashlightController.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 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.Dumpable;
+import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener;
+
+public interface FlashlightController extends CallbackController<FlashlightListener>, Dumpable {
+
+ boolean hasFlashlight();
+ void setFlashlight(boolean newState);
+ boolean isAvailable();
+ boolean isEnabled();
+
+ public interface FlashlightListener {
+
+ /**
+ * Called when the flashlight was turned off or on.
+ * @param enabled true if the flashlight is currently turned on.
+ */
+ void onFlashlightChanged(boolean enabled);
+
+
+ /**
+ * Called when there is an error that turns the flashlight off.
+ */
+ void onFlashlightError();
+
+ /**
+ * Called when there is a change in availability of the flashlight functionality
+ * @param available true if the flashlight is currently available.
+ */
+ void onFlashlightAvailabilityChanged(boolean available);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
new file mode 100644
index 0000000..f0cfa2c
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
@@ -0,0 +1,255 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Manages the flashlight.
+ */
+public class FlashlightControllerImpl implements FlashlightController {
+
+ private static final String TAG = "FlashlightController";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final int DISPATCH_ERROR = 0;
+ private static final int DISPATCH_CHANGED = 1;
+ private static final int DISPATCH_AVAILABILITY_CHANGED = 2;
+
+ private final CameraManager mCameraManager;
+ private final Context mContext;
+ /** Call {@link #ensureHandler()} before using */
+ private Handler mHandler;
+
+ /** Lock on mListeners when accessing */
+ private final ArrayList<WeakReference<FlashlightListener>> mListeners = new ArrayList<>(1);
+
+ /** Lock on {@code this} when accessing */
+ private boolean mFlashlightEnabled;
+
+ private String mCameraId;
+ private boolean mTorchAvailable;
+
+ public FlashlightControllerImpl(Context context) {
+ mContext = context;
+ mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+
+ tryInitCamera();
+ }
+
+ private void tryInitCamera() {
+ try {
+ mCameraId = getCameraId();
+ } catch (Throwable e) {
+ Log.e(TAG, "Couldn't initialize.", e);
+ return;
+ }
+
+ if (mCameraId != null) {
+ ensureHandler();
+ mCameraManager.registerTorchCallback(mTorchCallback, mHandler);
+ }
+ }
+
+ public void setFlashlight(boolean enabled) {
+ boolean pendingError = false;
+ synchronized (this) {
+ if (mCameraId == null) return;
+ if (mFlashlightEnabled != enabled) {
+ mFlashlightEnabled = enabled;
+ try {
+ mCameraManager.setTorchMode(mCameraId, enabled);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "Couldn't set torch mode", e);
+ mFlashlightEnabled = false;
+ pendingError = true;
+ }
+ }
+ }
+ dispatchModeChanged(mFlashlightEnabled);
+ if (pendingError) {
+ dispatchError();
+ }
+ }
+
+ public boolean hasFlashlight() {
+ return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
+ }
+
+ public synchronized boolean isEnabled() {
+ return mFlashlightEnabled;
+ }
+
+ public synchronized boolean isAvailable() {
+ return mTorchAvailable;
+ }
+
+ public void addCallback(FlashlightListener l) {
+ synchronized (mListeners) {
+ if (mCameraId == null) {
+ tryInitCamera();
+ }
+ cleanUpListenersLocked(l);
+ mListeners.add(new WeakReference<>(l));
+ l.onFlashlightAvailabilityChanged(mTorchAvailable);
+ l.onFlashlightChanged(mFlashlightEnabled);
+ }
+ }
+
+ public void removeCallback(FlashlightListener l) {
+ synchronized (mListeners) {
+ cleanUpListenersLocked(l);
+ }
+ }
+
+ private synchronized void ensureHandler() {
+ if (mHandler == null) {
+ HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+ mHandler = new Handler(thread.getLooper());
+ }
+ }
+
+ private String getCameraId() throws CameraAccessException {
+ String[] ids = mCameraManager.getCameraIdList();
+ for (String id : ids) {
+ CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);
+ Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+ Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);
+ if (flashAvailable != null && flashAvailable
+ && lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
+ return id;
+ }
+ }
+ return null;
+ }
+
+ private void dispatchModeChanged(boolean enabled) {
+ dispatchListeners(DISPATCH_CHANGED, enabled);
+ }
+
+ private void dispatchError() {
+ dispatchListeners(DISPATCH_CHANGED, false /* argument (ignored) */);
+ }
+
+ private void dispatchAvailabilityChanged(boolean available) {
+ dispatchListeners(DISPATCH_AVAILABILITY_CHANGED, available);
+ }
+
+ private void dispatchListeners(int message, boolean argument) {
+ synchronized (mListeners) {
+ final int N = mListeners.size();
+ boolean cleanup = false;
+ for (int i = 0; i < N; i++) {
+ FlashlightListener l = mListeners.get(i).get();
+ if (l != null) {
+ if (message == DISPATCH_ERROR) {
+ l.onFlashlightError();
+ } else if (message == DISPATCH_CHANGED) {
+ l.onFlashlightChanged(argument);
+ } else if (message == DISPATCH_AVAILABILITY_CHANGED) {
+ l.onFlashlightAvailabilityChanged(argument);
+ }
+ } else {
+ cleanup = true;
+ }
+ }
+ if (cleanup) {
+ cleanUpListenersLocked(null);
+ }
+ }
+ }
+
+ private void cleanUpListenersLocked(FlashlightListener listener) {
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ FlashlightListener found = mListeners.get(i).get();
+ if (found == null || found == listener) {
+ mListeners.remove(i);
+ }
+ }
+ }
+
+ private final CameraManager.TorchCallback mTorchCallback =
+ new CameraManager.TorchCallback() {
+
+ @Override
+ public void onTorchModeUnavailable(String cameraId) {
+ if (TextUtils.equals(cameraId, mCameraId)) {
+ setCameraAvailable(false);
+ }
+ }
+
+ @Override
+ public void onTorchModeChanged(String cameraId, boolean enabled) {
+ if (TextUtils.equals(cameraId, mCameraId)) {
+ setCameraAvailable(true);
+ setTorchMode(enabled);
+ }
+ }
+
+ private void setCameraAvailable(boolean available) {
+ boolean changed;
+ synchronized (FlashlightControllerImpl.this) {
+ changed = mTorchAvailable != available;
+ mTorchAvailable = available;
+ }
+ if (changed) {
+ if (DEBUG) Log.d(TAG, "dispatchAvailabilityChanged(" + available + ")");
+ dispatchAvailabilityChanged(available);
+ }
+ }
+
+ private void setTorchMode(boolean enabled) {
+ boolean changed;
+ synchronized (FlashlightControllerImpl.this) {
+ changed = mFlashlightEnabled != enabled;
+ mFlashlightEnabled = enabled;
+ }
+ if (changed) {
+ if (DEBUG) Log.d(TAG, "dispatchModeChanged(" + enabled + ")");
+ dispatchModeChanged(enabled);
+ }
+ }
+ };
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("FlashlightController state:");
+
+ pw.print(" mCameraId=");
+ pw.println(mCameraId);
+ pw.print(" mFlashlightEnabled=");
+ pw.println(mFlashlightEnabled);
+ pw.print(" mTorchAvailable=");
+ pw.println(mTorchAvailable);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/HeadsUpManager.java b/com/android/systemui/statusbar/policy/HeadsUpManager.java
new file mode 100644
index 0000000..53dfb24
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -0,0 +1,766 @@
+/*
+ * Copyright (C) 2015 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 android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.support.v4.util.ArraySet;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pools;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Stack;
+
+/**
+ * A manager which handles heads up notifications which is a special mode where
+ * they simply peek from the top of the screen.
+ */
+public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener,
+ VisualStabilityManager.Callback {
+ private static final String TAG = "HeadsUpManager";
+ private static final boolean DEBUG = false;
+ private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
+ private static final int TAG_CLICKED_NOTIFICATION = R.id.is_clicked_heads_up_tag;
+
+ private final int mHeadsUpNotificationDecay;
+ private final int mMinimumDisplayTime;
+
+ private final int mTouchAcceptanceDelay;
+ private final ArrayMap<String, Long> mSnoozedPackages;
+ private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
+ private final int mDefaultSnoozeLengthMs;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
+
+ private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
+
+ @Override
+ public HeadsUpEntry acquire() {
+ if (!mPoolObjects.isEmpty()) {
+ return mPoolObjects.pop();
+ }
+ return new HeadsUpEntry();
+ }
+
+ @Override
+ public boolean release(HeadsUpEntry instance) {
+ instance.reset();
+ mPoolObjects.push(instance);
+ return true;
+ }
+ };
+
+ private final View mStatusBarWindowView;
+ private final int mStatusBarHeight;
+ private final Context mContext;
+ private final NotificationGroupManager mGroupManager;
+ private StatusBar mBar;
+ private int mSnoozeLengthMs;
+ private ContentObserver mSettingsObserver;
+ private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>();
+ private HashSet<String> mSwipedOutKeys = new HashSet<>();
+ private int mUser;
+ private Clock mClock;
+ private boolean mReleaseOnExpandFinish;
+ private boolean mTrackingHeadsUp;
+ private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
+ private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
+ = new ArraySet<>();
+ private boolean mIsExpanded;
+ private boolean mHasPinnedNotification;
+ private int[] mTmpTwoArray = new int[2];
+ private boolean mHeadsUpGoingAway;
+ private boolean mWaitingOnCollapseWhenGoingAway;
+ private boolean mIsObserving;
+ private boolean mRemoteInputActive;
+ private float mExpandedHeight;
+ private VisualStabilityManager mVisualStabilityManager;
+ private int mStatusBarState;
+
+ public HeadsUpManager(final Context context, View statusBarWindowView,
+ NotificationGroupManager groupManager) {
+ mContext = context;
+ Resources resources = mContext.getResources();
+ mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay);
+ mSnoozedPackages = new ArrayMap<>();
+ mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
+ mSnoozeLengthMs = mDefaultSnoozeLengthMs;
+ mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
+ mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
+ mClock = new Clock();
+
+ mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
+ SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
+ mSettingsObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final int packageSnoozeLengthMs = Settings.Global.getInt(
+ context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
+ if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
+ mSnoozeLengthMs = packageSnoozeLengthMs;
+ if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
+ }
+ }
+ };
+ context.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
+ mSettingsObserver);
+ mStatusBarWindowView = statusBarWindowView;
+ mGroupManager = groupManager;
+ mStatusBarHeight = resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_height);
+ }
+
+ private void updateTouchableRegionListener() {
+ boolean shouldObserve = mHasPinnedNotification || mHeadsUpGoingAway
+ || mWaitingOnCollapseWhenGoingAway;
+ if (shouldObserve == mIsObserving) {
+ return;
+ }
+ if (shouldObserve) {
+ mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ mStatusBarWindowView.requestLayout();
+ } else {
+ mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+ }
+ mIsObserving = shouldObserve;
+ }
+
+ public void setBar(StatusBar bar) {
+ mBar = bar;
+ }
+
+ public void addListener(OnHeadsUpChangedListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeListener(OnHeadsUpChangedListener listener) {
+ mListeners.remove(listener);
+ }
+
+ public StatusBar getBar() {
+ return mBar;
+ }
+
+ /**
+ * Called when posting a new notification to the heads up.
+ */
+ public void showNotification(NotificationData.Entry headsUp) {
+ if (DEBUG) Log.v(TAG, "showNotification");
+ addHeadsUpEntry(headsUp);
+ updateNotification(headsUp, true);
+ headsUp.setInterruption();
+ }
+
+ /**
+ * Called when updating or posting a notification to the heads up.
+ */
+ public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
+ if (DEBUG) Log.v(TAG, "updateNotification");
+
+ headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+
+ if (alert) {
+ HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
+ if (headsUpEntry == null) {
+ // the entry was released before this update (i.e by a listener) This can happen
+ // with the groupmanager
+ return;
+ }
+ headsUpEntry.updateEntry();
+ setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp));
+ }
+ }
+
+ private void addHeadsUpEntry(NotificationData.Entry entry) {
+ HeadsUpEntry headsUpEntry = mEntryPool.acquire();
+
+ // This will also add the entry to the sortedList
+ headsUpEntry.setEntry(entry);
+ mHeadsUpEntries.put(entry.key, headsUpEntry);
+ entry.row.setHeadsUp(true);
+ setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(entry));
+ for (OnHeadsUpChangedListener listener : mListeners) {
+ listener.onHeadsUpStateChanged(entry, true);
+ }
+ entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ }
+
+ private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
+ return mStatusBarState != StatusBarState.KEYGUARD
+ && !mIsExpanded || hasFullScreenIntent(entry);
+ }
+
+ private boolean hasFullScreenIntent(NotificationData.Entry entry) {
+ return entry.notification.getNotification().fullScreenIntent != null;
+ }
+
+ private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) {
+ ExpandableNotificationRow row = headsUpEntry.entry.row;
+ if (row.isPinned() != isPinned) {
+ row.setPinned(isPinned);
+ updatePinnedMode();
+ for (OnHeadsUpChangedListener listener : mListeners) {
+ if (isPinned) {
+ listener.onHeadsUpPinned(row);
+ } else {
+ listener.onHeadsUpUnPinned(row);
+ }
+ }
+ }
+ }
+
+ private void removeHeadsUpEntry(NotificationData.Entry entry) {
+ HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
+ entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ entry.row.setHeadsUp(false);
+ setEntryPinned(remove, false /* isPinned */);
+ for (OnHeadsUpChangedListener listener : mListeners) {
+ listener.onHeadsUpStateChanged(entry, false);
+ }
+ mEntryPool.release(remove);
+ }
+
+ public void removeAllHeadsUpEntries() {
+ for (String key : mHeadsUpEntries.keySet()) {
+ removeHeadsUpEntry(mHeadsUpEntries.get(key).entry);
+ }
+ }
+
+ private void updatePinnedMode() {
+ boolean hasPinnedNotification = hasPinnedNotificationInternal();
+ if (hasPinnedNotification == mHasPinnedNotification) {
+ return;
+ }
+ mHasPinnedNotification = hasPinnedNotification;
+ if (mHasPinnedNotification) {
+ MetricsLogger.count(mContext, "note_peek", 1);
+ }
+ updateTouchableRegionListener();
+ for (OnHeadsUpChangedListener listener : mListeners) {
+ listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
+ }
+ }
+
+ /**
+ * React to the removal of the notification in the heads up.
+ *
+ * @return true if the notification was removed and false if it still needs to be kept around
+ * for a bit since it wasn't shown long enough
+ */
+ public boolean removeNotification(String key, boolean ignoreEarliestRemovalTime) {
+ if (DEBUG) Log.v(TAG, "remove");
+ if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
+ releaseImmediately(key);
+ return true;
+ } else {
+ getHeadsUpEntry(key).removeAsSoonAsPossible();
+ return false;
+ }
+ }
+
+ private boolean wasShownLongEnough(String key) {
+ HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
+ HeadsUpEntry topEntry = getTopEntry();
+ if (mSwipedOutKeys.contains(key)) {
+ // We always instantly dismiss views being manually swiped out.
+ mSwipedOutKeys.remove(key);
+ return true;
+ }
+ if (headsUpEntry != topEntry) {
+ return true;
+ }
+ return headsUpEntry.wasShownLongEnough();
+ }
+
+ public boolean isHeadsUp(String key) {
+ return mHeadsUpEntries.containsKey(key);
+ }
+
+ /**
+ * Push any current Heads Up notification down into the shade.
+ */
+ public void releaseAllImmediately() {
+ if (DEBUG) Log.v(TAG, "releaseAllImmediately");
+ ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet());
+ for (String key : keys) {
+ releaseImmediately(key);
+ }
+ }
+
+ public void releaseImmediately(String key) {
+ HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
+ if (headsUpEntry == null) {
+ return;
+ }
+ NotificationData.Entry shadeEntry = headsUpEntry.entry;
+ removeHeadsUpEntry(shadeEntry);
+ }
+
+ 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;
+ }
+
+ public void snooze() {
+ for (String key : mHeadsUpEntries.keySet()) {
+ HeadsUpEntry entry = mHeadsUpEntries.get(key);
+ String packageName = entry.entry.notification.getPackageName();
+ mSnoozedPackages.put(snoozeKey(packageName, mUser),
+ SystemClock.elapsedRealtime() + mSnoozeLengthMs);
+ }
+ mReleaseOnExpandFinish = true;
+ }
+
+ private static String snoozeKey(String packageName, int user) {
+ return user + "," + packageName;
+ }
+
+ private HeadsUpEntry getHeadsUpEntry(String key) {
+ return mHeadsUpEntries.get(key);
+ }
+
+ public NotificationData.Entry getEntry(String key) {
+ return mHeadsUpEntries.get(key).entry;
+ }
+
+ public Collection<HeadsUpEntry> getAllEntries() {
+ return mHeadsUpEntries.values();
+ }
+
+ public HeadsUpEntry getTopEntry() {
+ if (mHeadsUpEntries.isEmpty()) {
+ return null;
+ }
+ HeadsUpEntry topEntry = null;
+ for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
+ if (topEntry == null || entry.compareTo(topEntry) == -1) {
+ topEntry = entry;
+ }
+ }
+ return topEntry;
+ }
+
+ /**
+ * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
+ * that a user might have consciously clicked on it.
+ *
+ * @param key the key of the touched notification
+ * @return whether the touch is invalid and should be discarded
+ */
+ public boolean shouldSwallowClick(String key) {
+ HeadsUpEntry entry = mHeadsUpEntries.get(key);
+ if (entry != null && mClock.currentTimeMillis() < entry.postTime) {
+ return true;
+ }
+ return false;
+ }
+
+ public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
+ if (mIsExpanded || mBar.isBouncerShowing()) {
+ // The touchable region is always the full area when expanded
+ return;
+ }
+ if (mHasPinnedNotification) {
+ ExpandableNotificationRow topEntry = getTopEntry().entry.row;
+ if (topEntry.isChildInGroup()) {
+ final ExpandableNotificationRow groupSummary
+ = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification());
+ if (groupSummary != null) {
+ topEntry = groupSummary;
+ }
+ }
+ topEntry.getLocationOnScreen(mTmpTwoArray);
+ int minX = mTmpTwoArray[0];
+ int maxX = mTmpTwoArray[0] + topEntry.getWidth();
+ int maxY = topEntry.getIntrinsicHeight();
+
+ info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ info.touchableRegion.set(minX, 0, maxX, maxY);
+ } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
+ info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
+ }
+ }
+
+ public void setUser(int user) {
+ mUser = user;
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("HeadsUpManager state:");
+ pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
+ pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
+ pw.print(" now="); pw.println(SystemClock.elapsedRealtime());
+ pw.print(" mUser="); pw.println(mUser);
+ for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
+ pw.print(" HeadsUpEntry="); pw.println(entry.entry);
+ }
+ int N = mSnoozedPackages.size();
+ pw.println(" snoozed packages: " + N);
+ for (int i = 0; i < N; i++) {
+ pw.print(" "); pw.print(mSnoozedPackages.valueAt(i));
+ pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
+ }
+ }
+
+ public boolean hasPinnedHeadsUp() {
+ return mHasPinnedNotification;
+ }
+
+ private boolean hasPinnedNotificationInternal() {
+ for (String key : mHeadsUpEntries.keySet()) {
+ HeadsUpEntry entry = mHeadsUpEntries.get(key);
+ if (entry.entry.row.isPinned()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Notifies that a notification was swiped out and will be removed.
+ *
+ * @param key the notification key
+ */
+ public void addSwipedOutNotification(String key) {
+ mSwipedOutKeys.add(key);
+ }
+
+ public void unpinAll() {
+ for (String key : mHeadsUpEntries.keySet()) {
+ HeadsUpEntry entry = mHeadsUpEntries.get(key);
+ setEntryPinned(entry, false /* isPinned */);
+ // maybe it got un sticky
+ entry.updateEntry(false /* updatePostTime */);
+ }
+ }
+
+ public void onExpandingFinished() {
+ if (mReleaseOnExpandFinish) {
+ releaseAllImmediately();
+ mReleaseOnExpandFinish = false;
+ } else {
+ for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
+ if (isHeadsUp(entry.key)) {
+ // Maybe the heads-up was removed already
+ removeHeadsUpEntry(entry);
+ }
+ }
+ }
+ mEntriesToRemoveAfterExpand.clear();
+ }
+
+ public void setTrackingHeadsUp(boolean trackingHeadsUp) {
+ mTrackingHeadsUp = trackingHeadsUp;
+ }
+
+ public boolean isTrackingHeadsUp() {
+ return mTrackingHeadsUp;
+ }
+
+ public void setIsExpanded(boolean isExpanded) {
+ if (isExpanded != mIsExpanded) {
+ mIsExpanded = isExpanded;
+ if (isExpanded) {
+ // make sure our state is sane
+ mWaitingOnCollapseWhenGoingAway = false;
+ mHeadsUpGoingAway = false;
+ updateTouchableRegionListener();
+ }
+ }
+ }
+
+ /**
+ * @return the height of the top heads up notification when pinned. This is different from the
+ * intrinsic height, which also includes whether the notification is system expanded and
+ * is mainly used when dragging down from a heads up notification.
+ */
+ public int getTopHeadsUpPinnedHeight() {
+ HeadsUpEntry topEntry = getTopEntry();
+ if (topEntry == null || topEntry.entry == null) {
+ return 0;
+ }
+ ExpandableNotificationRow row = topEntry.entry.row;
+ if (row.isChildInGroup()) {
+ final ExpandableNotificationRow groupSummary
+ = mGroupManager.getGroupSummary(row.getStatusBarNotification());
+ if (groupSummary != null) {
+ row = groupSummary;
+ }
+ }
+ return row.getPinnedHeadsUpHeight();
+ }
+
+ /**
+ * Compare two entries and decide how they should be ranked.
+ *
+ * @return -1 if the first argument should be ranked higher than the second, 1 if the second
+ * one should be ranked higher and 0 if they are equal.
+ */
+ public int compare(NotificationData.Entry a, NotificationData.Entry b) {
+ HeadsUpEntry aEntry = getHeadsUpEntry(a.key);
+ HeadsUpEntry bEntry = getHeadsUpEntry(b.key);
+ if (aEntry == null || bEntry == null) {
+ return aEntry == null ? 1 : -1;
+ }
+ return aEntry.compareTo(bEntry);
+ }
+
+ /**
+ * Set that we are exiting the headsUp pinned mode, but some notifications might still be
+ * animating out. This is used to keep the touchable regions in a sane state.
+ */
+ public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
+ if (headsUpGoingAway != mHeadsUpGoingAway) {
+ mHeadsUpGoingAway = headsUpGoingAway;
+ if (!headsUpGoingAway) {
+ waitForStatusBarLayout();
+ }
+ updateTouchableRegionListener();
+ }
+ }
+
+ /**
+ * We need to wait on the whole panel to collapse, before we can remove the touchable region
+ * listener.
+ */
+ private void waitForStatusBarLayout() {
+ mWaitingOnCollapseWhenGoingAway = true;
+ mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
+ mStatusBarWindowView.removeOnLayoutChangeListener(this);
+ mWaitingOnCollapseWhenGoingAway = false;
+ updateTouchableRegionListener();
+ }
+ }
+ });
+ }
+
+ public static void setIsClickedNotification(View child, boolean clicked) {
+ child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
+ }
+
+ public static boolean isClickedHeadsUpNotification(View child) {
+ Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION);
+ return clicked != null && clicked;
+ }
+
+ public void setRemoteInputActive(NotificationData.Entry entry, boolean remoteInputActive) {
+ HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
+ if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
+ headsUpEntry.remoteInputActive = remoteInputActive;
+ if (remoteInputActive) {
+ headsUpEntry.removeAutoRemovalCallbacks();
+ } else {
+ headsUpEntry.updateEntry(false /* updatePostTime */);
+ }
+ }
+ }
+
+ /**
+ * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
+ * until it's collapsed again.
+ */
+ public void setExpanded(NotificationData.Entry entry, boolean expanded) {
+ HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
+ if (headsUpEntry != null && headsUpEntry.expanded != expanded && entry.row.isPinned()) {
+ headsUpEntry.expanded = expanded;
+ if (expanded) {
+ headsUpEntry.removeAutoRemovalCallbacks();
+ } else {
+ headsUpEntry.updateEntry(false /* updatePostTime */);
+ }
+ }
+ }
+
+ @Override
+ public void onReorderingAllowed() {
+ mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
+ for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
+ if (isHeadsUp(entry.key)) {
+ // Maybe the heads-up was removed already
+ removeHeadsUpEntry(entry);
+ }
+ }
+ mEntriesToRemoveWhenReorderingAllowed.clear();
+ mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true);
+ }
+
+ public void setVisualStabilityManager(VisualStabilityManager visualStabilityManager) {
+ mVisualStabilityManager = visualStabilityManager;
+ }
+
+ public void setStatusBarState(int statusBarState) {
+ mStatusBarState = statusBarState;
+ }
+
+ /**
+ * This represents a notification and how long it is in a heads up mode. It also manages its
+ * lifecycle automatically when created.
+ */
+ public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
+ public NotificationData.Entry entry;
+ public long postTime;
+ public long earliestRemovaltime;
+ private Runnable mRemoveHeadsUpRunnable;
+ public boolean remoteInputActive;
+ public boolean expanded;
+
+ public void setEntry(final NotificationData.Entry entry) {
+ this.entry = entry;
+
+ // The actual post time will be just after the heads-up really slided in
+ postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay;
+ mRemoveHeadsUpRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!mVisualStabilityManager.isReorderingAllowed()) {
+ mEntriesToRemoveWhenReorderingAllowed.add(entry);
+ mVisualStabilityManager.addReorderingAllowedCallback(HeadsUpManager.this);
+ } else if (!mTrackingHeadsUp) {
+ removeHeadsUpEntry(entry);
+ } else {
+ mEntriesToRemoveAfterExpand.add(entry);
+ }
+ }
+ };
+ updateEntry();
+ }
+
+ public void updateEntry() {
+ updateEntry(true);
+ }
+
+ public void updateEntry(boolean updatePostTime) {
+ long currentTime = mClock.currentTimeMillis();
+ earliestRemovaltime = currentTime + mMinimumDisplayTime;
+ if (updatePostTime) {
+ postTime = Math.max(postTime, currentTime);
+ }
+ removeAutoRemovalCallbacks();
+ if (mEntriesToRemoveAfterExpand.contains(entry)) {
+ mEntriesToRemoveAfterExpand.remove(entry);
+ }
+ if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
+ mEntriesToRemoveWhenReorderingAllowed.remove(entry);
+ }
+ if (!isSticky()) {
+ long finishTime = postTime + mHeadsUpNotificationDecay;
+ long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
+ mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
+ }
+ }
+
+ private boolean isSticky() {
+ return (entry.row.isPinned() && expanded)
+ || remoteInputActive || hasFullScreenIntent(entry);
+ }
+
+ @Override
+ public int compareTo(HeadsUpEntry o) {
+ boolean isPinned = entry.row.isPinned();
+ boolean otherPinned = o.entry.row.isPinned();
+ if (isPinned && !otherPinned) {
+ return -1;
+ } else if (!isPinned && otherPinned) {
+ return 1;
+ }
+ boolean selfFullscreen = hasFullScreenIntent(entry);
+ boolean otherFullscreen = hasFullScreenIntent(o.entry);
+ if (selfFullscreen && !otherFullscreen) {
+ return -1;
+ } else if (!selfFullscreen && otherFullscreen) {
+ return 1;
+ }
+
+ if (remoteInputActive && !o.remoteInputActive) {
+ return -1;
+ } else if (!remoteInputActive && o.remoteInputActive) {
+ return 1;
+ }
+
+ return postTime < o.postTime ? 1
+ : postTime == o.postTime ? entry.key.compareTo(o.entry.key)
+ : -1;
+ }
+
+ public void removeAutoRemovalCallbacks() {
+ mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
+ }
+
+ public boolean wasShownLongEnough() {
+ return earliestRemovaltime < mClock.currentTimeMillis();
+ }
+
+ public void removeAsSoonAsPossible() {
+ removeAutoRemovalCallbacks();
+ mHandler.postDelayed(mRemoveHeadsUpRunnable,
+ earliestRemovaltime - mClock.currentTimeMillis());
+ }
+
+ public void reset() {
+ removeAutoRemovalCallbacks();
+ entry = null;
+ mRemoveHeadsUpRunnable = null;
+ expanded = false;
+ remoteInputActive = false;
+ }
+ }
+
+ public static class Clock {
+ public long currentTimeMillis() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+
+}
diff --git a/com/android/systemui/statusbar/policy/HotspotController.java b/com/android/systemui/statusbar/policy/HotspotController.java
new file mode 100644
index 0000000..6457209
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/HotspotController.java
@@ -0,0 +1,32 @@
+/*
+ * 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.policy;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.HotspotController.Callback;
+
+public interface HotspotController extends CallbackController<Callback>, Dumpable {
+ boolean isHotspotEnabled();
+ boolean isHotspotTransient();
+
+ void setHotspotEnabled(boolean enabled);
+ boolean isHotspotSupported();
+
+ public interface Callback {
+ void onHotspotChanged(boolean enabled);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
new file mode 100644
index 0000000..1ebb986
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
@@ -0,0 +1,177 @@
+/*
+ * 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.policy;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.UserManager;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class HotspotControllerImpl implements HotspotController {
+
+ private static final String TAG = "HotspotController";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+ private final Receiver mReceiver = new Receiver();
+ private final ConnectivityManager mConnectivityManager;
+ private final Context mContext;
+
+ private int mHotspotState;
+ private boolean mWaitingForCallback;
+
+ public HotspotControllerImpl(Context context) {
+ mContext = context;
+ mConnectivityManager = (ConnectivityManager) context.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ }
+
+ @Override
+ public boolean isHotspotSupported() {
+ return mConnectivityManager.isTetheringSupported()
+ && mConnectivityManager.getTetherableWifiRegexs().length != 0
+ && UserManager.get(mContext).isUserAdmin(ActivityManager.getCurrentUser());
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("HotspotController state:");
+ pw.print(" mHotspotEnabled="); pw.println(stateToString(mHotspotState));
+ }
+
+ private static String stateToString(int hotspotState) {
+ switch (hotspotState) {
+ case WifiManager.WIFI_AP_STATE_DISABLED:
+ return "DISABLED";
+ case WifiManager.WIFI_AP_STATE_DISABLING:
+ return "DISABLING";
+ case WifiManager.WIFI_AP_STATE_ENABLED:
+ return "ENABLED";
+ case WifiManager.WIFI_AP_STATE_ENABLING:
+ return "ENABLING";
+ case WifiManager.WIFI_AP_STATE_FAILED:
+ return "FAILED";
+ }
+ return null;
+ }
+
+ @Override
+ public void addCallback(Callback callback) {
+ synchronized (mCallbacks) {
+ if (callback == null || mCallbacks.contains(callback)) return;
+ if (DEBUG) Log.d(TAG, "addCallback " + callback);
+ mCallbacks.add(callback);
+ mReceiver.setListening(!mCallbacks.isEmpty());
+ }
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ if (callback == null) return;
+ if (DEBUG) Log.d(TAG, "removeCallback " + callback);
+ synchronized (mCallbacks) {
+ mCallbacks.remove(callback);
+ mReceiver.setListening(!mCallbacks.isEmpty());
+ }
+ }
+
+ @Override
+ public boolean isHotspotEnabled() {
+ return mHotspotState == WifiManager.WIFI_AP_STATE_ENABLED;
+ }
+
+ @Override
+ public boolean isHotspotTransient() {
+ return mWaitingForCallback || (mHotspotState == WifiManager.WIFI_AP_STATE_ENABLING);
+ }
+
+ @Override
+ public void setHotspotEnabled(boolean enabled) {
+ if (enabled) {
+ OnStartTetheringCallback callback = new OnStartTetheringCallback();
+ mWaitingForCallback = true;
+ if (DEBUG) Log.d(TAG, "Starting tethering");
+ mConnectivityManager.startTethering(
+ ConnectivityManager.TETHERING_WIFI, false, callback);
+ fireCallback(isHotspotEnabled());
+ } else {
+ mConnectivityManager.stopTethering(ConnectivityManager.TETHERING_WIFI);
+ }
+ }
+
+ private void fireCallback(boolean isEnabled) {
+ synchronized (mCallbacks) {
+ for (Callback callback : mCallbacks) {
+ callback.onHotspotChanged(isEnabled);
+ }
+ }
+ }
+
+ private final class OnStartTetheringCallback extends
+ ConnectivityManager.OnStartTetheringCallback {
+ @Override
+ public void onTetheringStarted() {
+ if (DEBUG) Log.d(TAG, "onTetheringStarted");
+ mWaitingForCallback = false;
+ // Don't fire a callback here, instead wait for the next update from wifi.
+ }
+
+ @Override
+ public void onTetheringFailed() {
+ if (DEBUG) Log.d(TAG, "onTetheringFailed");
+ mWaitingForCallback = false;
+ fireCallback(isHotspotEnabled());
+ // TODO: Show error.
+ }
+ }
+
+ private final class Receiver extends BroadcastReceiver {
+ private boolean mRegistered;
+
+ public void setListening(boolean listening) {
+ if (listening && !mRegistered) {
+ if (DEBUG) Log.d(TAG, "Registering receiver");
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+ mContext.registerReceiver(this, filter);
+ mRegistered = true;
+ } else if (!listening && mRegistered) {
+ if (DEBUG) Log.d(TAG, "Unregistering receiver");
+ mContext.unregisterReceiver(this);
+ mRegistered = false;
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int state = intent.getIntExtra(
+ WifiManager.EXTRA_WIFI_AP_STATE, WifiManager.WIFI_AP_STATE_FAILED);
+ if (DEBUG) Log.d(TAG, "onReceive " + state);
+ mHotspotState = state;
+ fireCallback(mHotspotState == WifiManager.WIFI_AP_STATE_ENABLED);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/IconLogger.java b/com/android/systemui/statusbar/policy/IconLogger.java
new file mode 100644
index 0000000..710e1df
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/IconLogger.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 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;
+
+public interface IconLogger {
+
+ void onIconShown(String tag);
+ void onIconHidden(String tag);
+
+ default void onIconVisibility(String tag, boolean visible) {
+ if (visible) {
+ onIconShown(tag);
+ } else {
+ onIconHidden(tag);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/IconLoggerImpl.java b/com/android/systemui/statusbar/policy/IconLoggerImpl.java
new file mode 100644
index 0000000..0c201c3
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/IconLoggerImpl.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 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 static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_NUM_STATUS_ICONS;
+import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_STATUS_ICONS;
+import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.STATUS_BAR_ICONS_CHANGED;
+import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION;
+
+import android.content.Context;
+import android.metrics.LogMaker;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
+
+import com.android.internal.logging.MetricsLogger;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class IconLoggerImpl implements IconLogger {
+
+ // Minimum ms between log statements.
+ // NonFinalForTesting
+ @VisibleForTesting
+ protected static long MIN_LOG_INTERVAL = 1000;
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final MetricsLogger mLogger;
+ private final ArraySet<String> mIcons = new ArraySet<>();
+ private final List<String> mIconIndex;
+ private long mLastLog = System.currentTimeMillis();
+
+ public IconLoggerImpl(Context context, Looper bgLooper, MetricsLogger logger) {
+ mContext = context;
+ mHandler = new Handler(bgLooper);
+ mLogger = logger;
+ String[] icons = mContext.getResources().getStringArray(
+ com.android.internal.R.array.config_statusBarIcons);
+ mIconIndex = Arrays.asList(icons);
+ doLog();
+ }
+
+ @Override
+ public void onIconShown(String tag) {
+ synchronized (mIcons) {
+ if (mIcons.contains(tag)) return;
+ mIcons.add(tag);
+ }
+ if (!mHandler.hasCallbacks(mLog)) {
+ mHandler.postDelayed(mLog, MIN_LOG_INTERVAL);
+ }
+ }
+
+ @Override
+ public void onIconHidden(String tag) {
+ synchronized (mIcons) {
+ if (!mIcons.contains(tag)) return;
+ mIcons.remove(tag);
+ }
+ if (!mHandler.hasCallbacks(mLog)) {
+ mHandler.postDelayed(mLog, MIN_LOG_INTERVAL);
+ }
+ }
+
+ private void doLog() {
+ long time = System.currentTimeMillis();
+ long timeSinceLastLog = time - mLastLog;
+ mLastLog = time;
+
+ ArraySet<String> icons;
+ synchronized (mIcons) {
+ icons = new ArraySet<>(mIcons);
+ }
+ mLogger.write(new LogMaker(STATUS_BAR_ICONS_CHANGED)
+ .setType(TYPE_ACTION)
+ .setLatency(timeSinceLastLog)
+ .addTaggedData(FIELD_NUM_STATUS_ICONS, icons.size())
+ .addTaggedData(FIELD_STATUS_ICONS, getBitField(icons)));
+ }
+
+ private int getBitField(ArraySet<String> icons) {
+ int iconsVisible = 0;
+ for (String icon : icons) {
+ int index = mIconIndex.indexOf(icon);
+ if (index >= 0) {
+ iconsVisible |= (1 << index);
+ }
+ }
+ return iconsVisible;
+ }
+
+ private final Runnable mLog = this::doLog;
+}
diff --git a/com/android/systemui/statusbar/policy/KeyButtonDrawable.java b/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
new file mode 100644
index 0000000..21a96e2
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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 android.annotation.Nullable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.view.Gravity;
+
+/**
+ * Drawable for {@link KeyButtonView}s which contains an asset for both normal mode and light
+ * navigation bar mode.
+ */
+public class KeyButtonDrawable extends LayerDrawable {
+
+ private final boolean mHasDarkDrawable;
+
+ public static KeyButtonDrawable create(Drawable lightDrawable,
+ @Nullable Drawable darkDrawable) {
+ if (darkDrawable != null) {
+ return new KeyButtonDrawable(
+ new Drawable[] { lightDrawable.mutate(), darkDrawable.mutate() });
+ } else {
+ return new KeyButtonDrawable(new Drawable[] { lightDrawable.mutate() });
+ }
+ }
+
+ private KeyButtonDrawable(Drawable[] drawables) {
+ super(drawables);
+ for (int i = 0; i < drawables.length; i++) {
+ setLayerGravity(i, Gravity.CENTER);
+ }
+ mutate();
+ mHasDarkDrawable = drawables.length > 1;
+ setDarkIntensity(0f);
+ }
+
+ public void setDarkIntensity(float intensity) {
+ if (!mHasDarkDrawable) {
+ return;
+ }
+ getDrawable(0).setAlpha((int) ((1 - intensity) * 255f));
+ getDrawable(1).setAlpha((int) (intensity * 255f));
+ invalidateSelf();
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/KeyButtonRipple.java b/com/android/systemui/statusbar/policy/KeyButtonRipple.java
new file mode 100644
index 0000000..cc7943b
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyButtonRipple.java
@@ -0,0 +1,384 @@
+/*
+ * 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.policy;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.view.DisplayListCanvas;
+import android.view.RenderNodeAnimator;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+public class KeyButtonRipple extends Drawable {
+
+ private static final float GLOW_MAX_SCALE_FACTOR = 1.35f;
+ private static final float GLOW_MAX_ALPHA = 0.2f;
+ private static final float GLOW_MAX_ALPHA_DARK = 0.1f;
+ private static final int ANIMATION_DURATION_SCALE = 350;
+ private static final int ANIMATION_DURATION_FADE = 450;
+
+ private Paint mRipplePaint;
+ private CanvasProperty<Float> mLeftProp;
+ private CanvasProperty<Float> mTopProp;
+ private CanvasProperty<Float> mRightProp;
+ private CanvasProperty<Float> mBottomProp;
+ private CanvasProperty<Float> mRxProp;
+ private CanvasProperty<Float> mRyProp;
+ private CanvasProperty<Paint> mPaintProp;
+ private float mGlowAlpha = 0f;
+ private float mGlowScale = 1f;
+ private boolean mPressed;
+ private boolean mDrawingHardwareGlow;
+ private int mMaxWidth;
+ private boolean mLastDark;
+ private boolean mDark;
+
+ private final Interpolator mInterpolator = new LogInterpolator();
+ private boolean mSupportHardware;
+ private final View mTargetView;
+
+ private final HashSet<Animator> mRunningAnimations = new HashSet<>();
+ private final ArrayList<Animator> mTmpArray = new ArrayList<>();
+
+ public KeyButtonRipple(Context ctx, View targetView) {
+ mMaxWidth = ctx.getResources().getDimensionPixelSize(R.dimen.key_button_ripple_max_width);
+ mTargetView = targetView;
+ }
+
+ public void setDarkIntensity(float darkIntensity) {
+ mDark = darkIntensity >= 0.5f;
+ }
+
+ private Paint getRipplePaint() {
+ if (mRipplePaint == null) {
+ mRipplePaint = new Paint();
+ mRipplePaint.setAntiAlias(true);
+ mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff);
+ }
+ return mRipplePaint;
+ }
+
+ private void drawSoftware(Canvas canvas) {
+ if (mGlowAlpha > 0f) {
+ final Paint p = getRipplePaint();
+ p.setAlpha((int)(mGlowAlpha * 255f));
+
+ final float w = getBounds().width();
+ final float h = getBounds().height();
+ final boolean horizontal = w > h;
+ final float diameter = getRippleSize() * mGlowScale;
+ final float radius = diameter * .5f;
+ final float cx = w * .5f;
+ final float cy = h * .5f;
+ final float rx = horizontal ? radius : cx;
+ final float ry = horizontal ? cy : radius;
+ final float corner = horizontal ? cy : cx;
+
+ canvas.drawRoundRect(cx - rx, cy - ry,
+ cx + rx, cy + ry,
+ corner, corner, p);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ mSupportHardware = canvas.isHardwareAccelerated();
+ if (mSupportHardware) {
+ drawHardware((DisplayListCanvas) canvas);
+ } else {
+ drawSoftware(canvas);
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // Not supported.
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ // Not supported.
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ private boolean isHorizontal() {
+ return getBounds().width() > getBounds().height();
+ }
+
+ private void drawHardware(DisplayListCanvas c) {
+ if (mDrawingHardwareGlow) {
+ c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp,
+ mPaintProp);
+ }
+ }
+
+ public float getGlowAlpha() {
+ return mGlowAlpha;
+ }
+
+ public void setGlowAlpha(float x) {
+ mGlowAlpha = x;
+ invalidateSelf();
+ }
+
+ public float getGlowScale() {
+ return mGlowScale;
+ }
+
+ public void setGlowScale(float x) {
+ mGlowScale = x;
+ invalidateSelf();
+ }
+
+ private float getMaxGlowAlpha() {
+ return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA;
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ boolean pressed = false;
+ for (int i = 0; i < state.length; i++) {
+ if (state[i] == android.R.attr.state_pressed) {
+ pressed = true;
+ break;
+ }
+ }
+ if (pressed != mPressed) {
+ setPressed(pressed);
+ mPressed = pressed;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void jumpToCurrentState() {
+ cancelAnimations();
+ }
+
+ @Override
+ public boolean isStateful() {
+ return true;
+ }
+
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return true;
+ }
+
+ public void setPressed(boolean pressed) {
+ if (mDark != mLastDark && pressed) {
+ mRipplePaint = null;
+ mLastDark = mDark;
+ }
+ if (mSupportHardware) {
+ setPressedHardware(pressed);
+ } else {
+ setPressedSoftware(pressed);
+ }
+ }
+
+ private void cancelAnimations() {
+ mTmpArray.addAll(mRunningAnimations);
+ int size = mTmpArray.size();
+ for (int i = 0; i < size; i++) {
+ Animator a = mTmpArray.get(i);
+ a.cancel();
+ }
+ mTmpArray.clear();
+ mRunningAnimations.clear();
+ }
+
+ private void setPressedSoftware(boolean pressed) {
+ if (pressed) {
+ enterSoftware();
+ } else {
+ exitSoftware();
+ }
+ }
+
+ private void enterSoftware() {
+ cancelAnimations();
+ mGlowAlpha = getMaxGlowAlpha();
+ ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
+ 0f, GLOW_MAX_SCALE_FACTOR);
+ scaleAnimator.setInterpolator(mInterpolator);
+ scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
+ scaleAnimator.addListener(mAnimatorListener);
+ scaleAnimator.start();
+ mRunningAnimations.add(scaleAnimator);
+ }
+
+ private void exitSoftware() {
+ ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
+ alphaAnimator.setInterpolator(Interpolators.ALPHA_OUT);
+ alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
+ alphaAnimator.addListener(mAnimatorListener);
+ alphaAnimator.start();
+ mRunningAnimations.add(alphaAnimator);
+ }
+
+ private void setPressedHardware(boolean pressed) {
+ if (pressed) {
+ enterHardware();
+ } else {
+ exitHardware();
+ }
+ }
+
+ /**
+ * Sets the left/top property for the round rect to {@code prop} depending on whether we are
+ * horizontal or vertical mode.
+ */
+ private void setExtendStart(CanvasProperty<Float> prop) {
+ if (isHorizontal()) {
+ mLeftProp = prop;
+ } else {
+ mTopProp = prop;
+ }
+ }
+
+ private CanvasProperty<Float> getExtendStart() {
+ return isHorizontal() ? mLeftProp : mTopProp;
+ }
+
+ /**
+ * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
+ * horizontal or vertical mode.
+ */
+ private void setExtendEnd(CanvasProperty<Float> prop) {
+ if (isHorizontal()) {
+ mRightProp = prop;
+ } else {
+ mBottomProp = prop;
+ }
+ }
+
+ private CanvasProperty<Float> getExtendEnd() {
+ return isHorizontal() ? mRightProp : mBottomProp;
+ }
+
+ private int getExtendSize() {
+ return isHorizontal() ? getBounds().width() : getBounds().height();
+ }
+
+ private int getRippleSize() {
+ int size = isHorizontal() ? getBounds().width() : getBounds().height();
+ return Math.min(size, mMaxWidth);
+ }
+
+ private void enterHardware() {
+ cancelAnimations();
+ mDrawingHardwareGlow = true;
+ setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
+ final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
+ getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
+ startAnim.setDuration(ANIMATION_DURATION_SCALE);
+ startAnim.setInterpolator(mInterpolator);
+ startAnim.addListener(mAnimatorListener);
+ startAnim.setTarget(mTargetView);
+
+ setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
+ final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
+ getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
+ endAnim.setDuration(ANIMATION_DURATION_SCALE);
+ endAnim.setInterpolator(mInterpolator);
+ endAnim.addListener(mAnimatorListener);
+ endAnim.setTarget(mTargetView);
+
+ if (isHorizontal()) {
+ mTopProp = CanvasProperty.createFloat(0f);
+ mBottomProp = CanvasProperty.createFloat(getBounds().height());
+ mRxProp = CanvasProperty.createFloat(getBounds().height()/2);
+ mRyProp = CanvasProperty.createFloat(getBounds().height()/2);
+ } else {
+ mLeftProp = CanvasProperty.createFloat(0f);
+ mRightProp = CanvasProperty.createFloat(getBounds().width());
+ mRxProp = CanvasProperty.createFloat(getBounds().width()/2);
+ mRyProp = CanvasProperty.createFloat(getBounds().width()/2);
+ }
+
+ mGlowScale = GLOW_MAX_SCALE_FACTOR;
+ mGlowAlpha = getMaxGlowAlpha();
+ mRipplePaint = getRipplePaint();
+ mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
+ mPaintProp = CanvasProperty.createPaint(mRipplePaint);
+
+ startAnim.start();
+ endAnim.start();
+ mRunningAnimations.add(startAnim);
+ mRunningAnimations.add(endAnim);
+
+ invalidateSelf();
+ }
+
+ private void exitHardware() {
+ mPaintProp = CanvasProperty.createPaint(getRipplePaint());
+ final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
+ RenderNodeAnimator.PAINT_ALPHA, 0);
+ opacityAnim.setDuration(ANIMATION_DURATION_FADE);
+ opacityAnim.setInterpolator(Interpolators.ALPHA_OUT);
+ opacityAnim.addListener(mAnimatorListener);
+ opacityAnim.setTarget(mTargetView);
+
+ opacityAnim.start();
+ mRunningAnimations.add(opacityAnim);
+
+ invalidateSelf();
+ }
+
+ private final AnimatorListenerAdapter mAnimatorListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mRunningAnimations.remove(animation);
+ if (mRunningAnimations.isEmpty() && !mPressed) {
+ mDrawingHardwareGlow = false;
+ invalidateSelf();
+ }
+ }
+ };
+
+ /**
+ * Interpolator with a smooth log deceleration
+ */
+ private static final class LogInterpolator implements Interpolator {
+ @Override
+ public float getInterpolation(float input) {
+ return 1 - (float) Math.pow(400, -input * 1.4);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/KeyButtonView.java b/com/android/systemui/statusbar/policy/KeyButtonView.java
new file mode 100644
index 0000000..0501771
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyButtonView.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.hardware.input.InputManager;
+import android.media.AudioManager;
+import android.metrics.LogMaker;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+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 com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface;
+
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
+
+public class KeyButtonView extends ImageView implements ButtonInterface {
+
+ private final boolean mPlaySounds;
+ private int mContentDescriptionRes;
+ private long mDownTime;
+ private int mCode;
+ private int mTouchSlop;
+ private boolean mSupportsLongpress = true;
+ private AudioManager mAudioManager;
+ private boolean mGestureAborted;
+ private boolean mLongClicked;
+ private OnClickListener mOnClickListener;
+ private final KeyButtonRipple mRipple;
+ private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+
+ private final Runnable mCheckLongPress = new Runnable() {
+ public void run() {
+ if (isPressed()) {
+ // Log.d("KeyButtonView", "longpressed: " + this);
+ if (isLongClickable()) {
+ // Just an old-fashioned ImageView
+ performLongClick();
+ mLongClicked = true;
+ } else if (mSupportsLongpress) {
+ sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
+ mLongClicked = true;
+ }
+ }
+ }
+ };
+
+ public KeyButtonView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public KeyButtonView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView,
+ defStyle, 0);
+
+ mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0);
+
+ mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true);
+ mPlaySounds = a.getBoolean(R.styleable.KeyButtonView_playSound, true);
+
+ TypedValue value = new TypedValue();
+ if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) {
+ mContentDescriptionRes = value.resourceId;
+ }
+
+ a.recycle();
+
+ setClickable(true);
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+ mRipple = new KeyButtonRipple(context, this);
+ setBackground(mRipple);
+ }
+
+ @Override
+ public boolean isClickable() {
+ return mCode != 0 || super.isClickable();
+ }
+
+ public void setCode(int code) {
+ mCode = code;
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener onClickListener) {
+ super.setOnClickListener(onClickListener);
+ mOnClickListener = onClickListener;
+ }
+
+ public void loadAsync(Icon icon) {
+ new AsyncTask<Icon, Void, Drawable>() {
+ @Override
+ protected Drawable doInBackground(Icon... params) {
+ return params[0].loadDrawable(mContext);
+ }
+
+ @Override
+ protected void onPostExecute(Drawable drawable) {
+ setImageDrawable(drawable);
+ }
+ }.execute(icon);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ if (mContentDescriptionRes != 0) {
+ setContentDescription(mContext.getString(mContentDescriptionRes));
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ if (mCode != 0) {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null));
+ if (mSupportsLongpress || isLongClickable()) {
+ info.addAction(
+ new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null));
+ }
+ }
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ if (visibility != View.VISIBLE) {
+ jumpDrawablesToCurrentState();
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (action == ACTION_CLICK && mCode != 0) {
+ sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis());
+ sendEvent(KeyEvent.ACTION_UP, 0);
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+ playSoundEffect(SoundEffectConstants.CLICK);
+ return true;
+ } else if (action == ACTION_LONG_CLICK && mCode != 0) {
+ sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
+ sendEvent(KeyEvent.ACTION_UP, 0);
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
+ return true;
+ }
+ return super.performAccessibilityActionInternal(action, arguments);
+ }
+
+ public boolean onTouchEvent(MotionEvent ev) {
+ final int action = ev.getAction();
+ int x, y;
+ if (action == MotionEvent.ACTION_DOWN) {
+ mGestureAborted = false;
+ }
+ if (mGestureAborted) {
+ return false;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mDownTime = SystemClock.uptimeMillis();
+ mLongClicked = false;
+ setPressed(true);
+ if (mCode != 0) {
+ sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
+ } else {
+ // Provide the same haptic feedback that the system offers for virtual keys.
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+ }
+ playSoundEffect(SoundEffectConstants.CLICK);
+ removeCallbacks(mCheckLongPress);
+ postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
+ break;
+ case MotionEvent.ACTION_MOVE:
+ x = (int)ev.getX();
+ y = (int)ev.getY();
+ setPressed(x >= -mTouchSlop
+ && x < getWidth() + mTouchSlop
+ && y >= -mTouchSlop
+ && y < getHeight() + mTouchSlop);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ setPressed(false);
+ if (mCode != 0) {
+ sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
+ }
+ removeCallbacks(mCheckLongPress);
+ break;
+ case MotionEvent.ACTION_UP:
+ final boolean doIt = isPressed() && !mLongClicked;
+ setPressed(false);
+ // Always send a release ourselves because it doesn't seem to be sent elsewhere
+ // and it feels weird to sometimes get a release haptic and other times not.
+ if ((SystemClock.uptimeMillis() - mDownTime) > 150 && !mLongClicked) {
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
+ }
+ if (mCode != 0) {
+ if (doIt) {
+ sendEvent(KeyEvent.ACTION_UP, 0);
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+ } else {
+ sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
+ }
+ } else {
+ // no key code, just a regular ImageView
+ if (doIt && mOnClickListener != null) {
+ mOnClickListener.onClick(this);
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+ }
+ }
+ removeCallbacks(mCheckLongPress);
+ break;
+ }
+
+ return true;
+ }
+
+ public void playSoundEffect(int soundConstant) {
+ if (!mPlaySounds) return;
+ mAudioManager.playSoundEffect(soundConstant, ActivityManager.getCurrentUser());
+ }
+
+ public void sendEvent(int action, int flags) {
+ sendEvent(action, flags, SystemClock.uptimeMillis());
+ }
+
+ void sendEvent(int action, int flags, long when) {
+ mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_NAV_BUTTON_EVENT)
+ .setType(MetricsEvent.TYPE_ACTION)
+ .setSubtype(mCode)
+ .addTaggedData(MetricsEvent.FIELD_NAV_ACTION, action)
+ .addTaggedData(MetricsEvent.FIELD_FLAGS, flags));
+ final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
+ final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
+ 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
+ InputDevice.SOURCE_KEYBOARD);
+ InputManager.getInstance().injectInputEvent(ev,
+ InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
+ }
+
+ @Override
+ public void abortCurrentGesture() {
+ setPressed(false);
+ mGestureAborted = true;
+ }
+
+ @Override
+ public void setDarkIntensity(float darkIntensity) {
+ Drawable drawable = getDrawable();
+ if (drawable != null) {
+ ((KeyButtonDrawable) getDrawable()).setDarkIntensity(darkIntensity);
+
+ // Since we reuse the same drawable for multiple views, we need to invalidate the view
+ // manually.
+ invalidate();
+ }
+ mRipple.setDarkIntensity(darkIntensity);
+ }
+
+ @Override
+ public void setVertical(boolean vertical) {
+ //no op
+ }
+}
+
+
diff --git a/com/android/systemui/statusbar/policy/KeyguardMonitor.java b/com/android/systemui/statusbar/policy/KeyguardMonitor.java
new file mode 100644
index 0000000..ccfbb26
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyguardMonitor.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 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.KeyguardMonitor.Callback;
+
+public interface KeyguardMonitor extends CallbackController<Callback> {
+
+ boolean isSecure();
+ boolean canSkipBouncer();
+ boolean isShowing();
+ boolean isOccluded();
+ boolean isKeyguardFadingAway();
+ boolean isKeyguardGoingAway();
+ long getKeyguardFadingAwayDuration();
+ long getKeyguardFadingAwayDelay();
+
+ interface Callback {
+ void onKeyguardShowingChanged();
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java b/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java
new file mode 100644
index 0000000..5ead02f
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java
@@ -0,0 +1,163 @@
+/*
+ * 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.policy;
+
+import android.app.ActivityManager;
+import android.content.Context;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.settings.CurrentUserTracker;
+
+import java.util.ArrayList;
+
+public class KeyguardMonitorImpl extends KeyguardUpdateMonitorCallback
+ implements KeyguardMonitor {
+
+ private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+
+ private final Context mContext;
+ private final CurrentUserTracker mUserTracker;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+ private int mCurrentUser;
+ private boolean mShowing;
+ private boolean mSecure;
+ private boolean mOccluded;
+ private boolean mCanSkipBouncer;
+
+ private boolean mListening;
+ private boolean mKeyguardFadingAway;
+ private long mKeyguardFadingAwayDelay;
+ private long mKeyguardFadingAwayDuration;
+ private boolean mKeyguardGoingAway;
+
+ public KeyguardMonitorImpl(Context context) {
+ mContext = context;
+ mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+ mUserTracker = new CurrentUserTracker(mContext) {
+ @Override
+ public void onUserSwitched(int newUserId) {
+ mCurrentUser = newUserId;
+ updateCanSkipBouncerState();
+ }
+ };
+ }
+
+ @Override
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
+ if (mCallbacks.size() != 0 && !mListening) {
+ mListening = true;
+ mCurrentUser = ActivityManager.getCurrentUser();
+ updateCanSkipBouncerState();
+ mKeyguardUpdateMonitor.registerCallback(this);
+ mUserTracker.startTracking();
+ }
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ if (mCallbacks.remove(callback) && mCallbacks.size() == 0 && mListening) {
+ mListening = false;
+ mKeyguardUpdateMonitor.removeCallback(this);
+ mUserTracker.stopTracking();
+ }
+ }
+
+ @Override
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ @Override
+ public boolean isSecure() {
+ return mSecure;
+ }
+
+ @Override
+ public boolean isOccluded() {
+ return mOccluded;
+ }
+
+ @Override
+ public boolean canSkipBouncer() {
+ return mCanSkipBouncer;
+ }
+
+ public void notifyKeyguardState(boolean showing, boolean secure, boolean occluded) {
+ if (mShowing == showing && mSecure == secure && mOccluded == occluded) return;
+ mShowing = showing;
+ mSecure = secure;
+ mOccluded = occluded;
+ notifyKeyguardChanged();
+ }
+
+ @Override
+ public void onTrustChanged(int userId) {
+ updateCanSkipBouncerState();
+ notifyKeyguardChanged();
+ }
+
+ public boolean isDeviceInteractive() {
+ return mKeyguardUpdateMonitor.isDeviceInteractive();
+ }
+
+ private void updateCanSkipBouncerState() {
+ mCanSkipBouncer = mKeyguardUpdateMonitor.getUserCanSkipBouncer(mCurrentUser);
+ }
+
+ private void notifyKeyguardChanged() {
+ // Copy the list to allow removal during callback.
+ new ArrayList<Callback>(mCallbacks).forEach(Callback::onKeyguardShowingChanged);
+ }
+
+ public void notifyKeyguardFadingAway(long delay, long fadeoutDuration) {
+ mKeyguardFadingAway = true;
+ mKeyguardFadingAwayDelay = delay;
+ mKeyguardFadingAwayDuration = fadeoutDuration;
+ }
+
+ public void notifyKeyguardDoneFading() {
+ mKeyguardFadingAway = false;
+ mKeyguardGoingAway = false;
+ }
+
+ @Override
+ public boolean isKeyguardFadingAway() {
+ return mKeyguardFadingAway;
+ }
+
+ @Override
+ public boolean isKeyguardGoingAway() {
+ return mKeyguardGoingAway;
+ }
+
+ @Override
+ public long getKeyguardFadingAwayDelay() {
+ return mKeyguardFadingAwayDelay;
+ }
+
+ @Override
+ public long getKeyguardFadingAwayDuration() {
+ return mKeyguardFadingAwayDuration;
+ }
+
+ public void notifyKeyguardGoingAway(boolean keyguardGoingAway) {
+ mKeyguardGoingAway = keyguardGoingAway;
+ }
+}
\ No newline at end of file
diff --git a/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java b/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java
new file mode 100644
index 0000000..4b283fe
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java
@@ -0,0 +1,326 @@
+/*
+ * 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.policy;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+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.widget.FrameLayout;
+
+import com.android.settingslib.animation.AppearAnimationUtils;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.qs.tiles.UserDetailItemView;
+import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+/**
+ * Manages the user switcher on the Keyguard.
+ */
+public class KeyguardUserSwitcher {
+
+ private static final String TAG = "KeyguardUserSwitcher";
+ private static final boolean ALWAYS_ON = false;
+
+ private final Container mUserSwitcherContainer;
+ private final KeyguardStatusBarView mStatusBarView;
+ private final Adapter mAdapter;
+ private final AppearAnimationUtils mAppearAnimationUtils;
+ private final KeyguardUserSwitcherScrim mBackground;
+
+ private ViewGroup mUserSwitcher;
+ private ObjectAnimator mBgAnimator;
+ private UserSwitcherController mUserSwitcherController;
+ private boolean mAnimating;
+
+ public KeyguardUserSwitcher(Context context, ViewStub userSwitcher,
+ KeyguardStatusBarView statusBarView, NotificationPanelView panelView) {
+ boolean keyguardUserSwitcherEnabled =
+ context.getResources().getBoolean(R.bool.config_keyguardUserSwitcher) || ALWAYS_ON;
+ UserSwitcherController userSwitcherController = Dependency.get(UserSwitcherController.class);
+ if (userSwitcherController != null && keyguardUserSwitcherEnabled) {
+ mUserSwitcherContainer = (Container) userSwitcher.inflate();
+ mBackground = new KeyguardUserSwitcherScrim(context);
+ reinflateViews();
+ mStatusBarView = statusBarView;
+ mStatusBarView.setKeyguardUserSwitcher(this);
+ panelView.setKeyguardUserSwitcher(this);
+ mAdapter = new Adapter(context, userSwitcherController, this);
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+ mUserSwitcherController = userSwitcherController;
+ mAppearAnimationUtils = new AppearAnimationUtils(context, 400, -0.5f, 0.5f,
+ Interpolators.FAST_OUT_SLOW_IN);
+ mUserSwitcherContainer.setKeyguardUserSwitcher(this);
+ } else {
+ mUserSwitcherContainer = null;
+ mStatusBarView = null;
+ mAdapter = null;
+ mAppearAnimationUtils = null;
+ mBackground = null;
+ }
+ }
+
+ private void reinflateViews() {
+ if (mUserSwitcher != null) {
+ mUserSwitcher.setBackground(null);
+ mUserSwitcher.removeOnLayoutChangeListener(mBackground);
+ }
+ mUserSwitcherContainer.removeAllViews();
+
+ LayoutInflater.from(mUserSwitcherContainer.getContext())
+ .inflate(R.layout.keyguard_user_switcher_inner, mUserSwitcherContainer);
+
+ mUserSwitcher = (ViewGroup) mUserSwitcherContainer.findViewById(
+ R.id.keyguard_user_switcher_inner);
+ mUserSwitcher.addOnLayoutChangeListener(mBackground);
+ mUserSwitcher.setBackground(mBackground);
+ }
+
+ public void setKeyguard(boolean keyguard, boolean animate) {
+ if (mUserSwitcher != null) {
+ if (keyguard && shouldExpandByDefault()) {
+ show(animate);
+ } else {
+ hide(animate);
+ }
+ }
+ }
+
+ /**
+ * @return true if the user switcher should be expanded by default on the lock screen.
+ * @see android.os.UserManager#isUserSwitcherEnabled()
+ */
+ private boolean shouldExpandByDefault() {
+ return (mUserSwitcherController != null) && mUserSwitcherController.isSimpleUserSwitcher();
+ }
+
+ public void show(boolean animate) {
+ if (mUserSwitcher != null && mUserSwitcherContainer.getVisibility() != View.VISIBLE) {
+ cancelAnimations();
+ mAdapter.refresh();
+ mUserSwitcherContainer.setVisibility(View.VISIBLE);
+ mStatusBarView.setKeyguardUserSwitcherShowing(true, animate);
+ if (animate) {
+ startAppearAnimation();
+ }
+ }
+ }
+
+ private boolean hide(boolean animate) {
+ if (mUserSwitcher != null && mUserSwitcherContainer.getVisibility() == View.VISIBLE) {
+ cancelAnimations();
+ if (animate) {
+ startDisappearAnimation();
+ } else {
+ mUserSwitcherContainer.setVisibility(View.GONE);
+ }
+ mStatusBarView.setKeyguardUserSwitcherShowing(false, animate);
+ return true;
+ }
+ return false;
+ }
+
+ private void cancelAnimations() {
+ int count = mUserSwitcher.getChildCount();
+ for (int i = 0; i < count; i++) {
+ mUserSwitcher.getChildAt(i).animate().cancel();
+ }
+ if (mBgAnimator != null) {
+ mBgAnimator.cancel();
+ }
+ mUserSwitcher.animate().cancel();
+ mAnimating = false;
+ }
+
+ private void startAppearAnimation() {
+ int count = mUserSwitcher.getChildCount();
+ View[] objects = new View[count];
+ for (int i = 0; i < count; i++) {
+ objects[i] = mUserSwitcher.getChildAt(i);
+ }
+ mUserSwitcher.setClipChildren(false);
+ mUserSwitcher.setClipToPadding(false);
+ 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(Interpolators.ALPHA_IN);
+ mBgAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mBgAnimator = null;
+ mAnimating = false;
+ }
+ });
+ mBgAnimator.start();
+ }
+
+ private void startDisappearAnimation() {
+ mAnimating = true;
+ mUserSwitcher.animate()
+ .alpha(0f)
+ .setDuration(300)
+ .setInterpolator(Interpolators.ALPHA_OUT)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mUserSwitcherContainer.setVisibility(View.GONE);
+ mUserSwitcher.setAlpha(1f);
+ mAnimating = false;
+ }
+ });
+ }
+
+ private void refresh() {
+ final int childCount = mUserSwitcher.getChildCount();
+ final int adapterCount = mAdapter.getCount();
+ final int N = Math.max(childCount, adapterCount);
+ for (int i = 0; i < N; i++) {
+ if (i < adapterCount) {
+ View oldView = null;
+ if (i < childCount) {
+ oldView = mUserSwitcher.getChildAt(i);
+ }
+ View newView = mAdapter.getView(i, oldView, mUserSwitcher);
+ if (oldView == null) {
+ // We ran out of existing views. Add it at the end.
+ mUserSwitcher.addView(newView);
+ } else if (oldView != newView) {
+ // We couldn't rebind the view. Replace it.
+ mUserSwitcher.removeViewAt(i);
+ mUserSwitcher.addView(newView, i);
+ }
+ } else {
+ int lastIndex = mUserSwitcher.getChildCount() - 1;
+ mUserSwitcher.removeViewAt(lastIndex);
+ }
+ }
+ }
+
+ public boolean hideIfNotSimple(boolean animate) {
+ if (mUserSwitcherContainer != null && !mUserSwitcherController.isSimpleUserSwitcher()) {
+ return hide(animate);
+ }
+ return false;
+ }
+
+ boolean isAnimating() {
+ return mAnimating;
+ }
+
+ public final DataSetObserver mDataSetObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ refresh();
+ }
+ };
+
+ public void onDensityOrFontScaleChanged() {
+ if (mUserSwitcherContainer != null) {
+ reinflateViews();
+ refresh();
+ }
+ }
+
+ public static class Adapter extends UserSwitcherController.BaseUserAdapter implements
+ View.OnClickListener {
+
+ private Context mContext;
+ private KeyguardUserSwitcher mKeyguardUserSwitcher;
+
+ public Adapter(Context context, UserSwitcherController controller,
+ KeyguardUserSwitcher kgu) {
+ super(controller);
+ mContext = context;
+ mKeyguardUserSwitcher = kgu;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ UserSwitcherController.UserRecord item = getItem(position);
+ if (!(convertView instanceof UserDetailItemView)
+ || !(convertView.getTag() instanceof UserSwitcherController.UserRecord)) {
+ convertView = LayoutInflater.from(mContext).inflate(
+ R.layout.keyguard_user_switcher_item, parent, false);
+ convertView.setOnClickListener(this);
+ }
+ UserDetailItemView v = (UserDetailItemView) convertView;
+
+ String name = getName(mContext, item);
+ if (item.picture == null) {
+ v.bind(name, getDrawable(mContext, item).mutate(), item.resolveId());
+ } else {
+ v.bind(name, item.picture, item.info.id);
+ }
+ // Disable the icon if switching is disabled
+ v.setAvatarEnabled(item.isSwitchToEnabled);
+ convertView.setActivated(item.isCurrent);
+ convertView.setTag(item);
+ return convertView;
+ }
+
+ @Override
+ public void onClick(View v) {
+ 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 if (user.isSwitchToEnabled) {
+ 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/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java b/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java
new file mode 100644
index 0000000..49f5bcd
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java
@@ -0,0 +1,111 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.util.LayoutDirection;
+import android.view.View;
+
+import com.android.systemui.R;
+
+/**
+ * Gradient background for the user switcher on Keyguard.
+ */
+public class KeyguardUserSwitcherScrim extends Drawable
+ implements View.OnLayoutChangeListener {
+
+ private static final float OUTER_EXTENT = 2.5f;
+ private static final float INNER_EXTENT = 0.75f;
+
+ private int mDarkColor;
+ private int mTop;
+ private int mAlpha = 255;
+ private Paint mRadialGradientPaint = new Paint();
+ private int mLayoutWidth;
+
+ public KeyguardUserSwitcherScrim(Context context) {
+ mDarkColor = context.getColor(
+ R.color.keyguard_user_switcher_background_gradient_color);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ boolean isLtr = getLayoutDirection() == LayoutDirection.LTR;
+ Rect bounds = getBounds();
+ float width = bounds.width() * OUTER_EXTENT;
+ float height = (mTop + bounds.height()) * OUTER_EXTENT;
+ canvas.translate(0, -mTop);
+ canvas.scale(1, height / width);
+ canvas.drawRect(isLtr ? bounds.right - width : 0, 0,
+ isLtr ? bounds.right : bounds.left + width, width, mRadialGradientPaint);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ updatePaint();
+ invalidateSelf();
+ }
+
+ @Override
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
+ mLayoutWidth = right - left;
+ mTop = top;
+ updatePaint();
+ }
+ }
+
+ private void updatePaint() {
+ if (mLayoutWidth == 0) {
+ return;
+ }
+ float radius = mLayoutWidth * OUTER_EXTENT;
+ boolean isLtr = getLayoutDirection() == LayoutDirection.LTR;
+ mRadialGradientPaint.setShader(
+ new RadialGradient(isLtr ? mLayoutWidth : 0, 0, radius,
+ new int[] { Color.argb(
+ (int) (Color.alpha(mDarkColor) * mAlpha / 255f), 0, 0, 0),
+ Color.TRANSPARENT },
+ new float[] { Math.max(0f, mLayoutWidth * INNER_EXTENT / radius), 1f },
+ Shader.TileMode.CLAMP));
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/Listenable.java b/com/android/systemui/statusbar/policy/Listenable.java
new file mode 100644
index 0000000..4fa59fd
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/Listenable.java
@@ -0,0 +1,22 @@
+/*
+ * 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.policy;
+
+/** Common interface for components with an active listening state. **/
+public interface Listenable {
+ void setListening(boolean listening);
+}
diff --git a/com/android/systemui/statusbar/policy/LocationController.java b/com/android/systemui/statusbar/policy/LocationController.java
new file mode 100644
index 0000000..8e8a285
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/LocationController.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.policy;
+
+import com.android.systemui.statusbar.policy.LocationController.LocationChangeCallback;
+
+public interface LocationController extends CallbackController<LocationChangeCallback> {
+ boolean isLocationActive();
+ boolean isLocationEnabled();
+ boolean setLocationEnabled(boolean enabled);
+
+ /**
+ * A callback for change in location settings (the user has enabled/disabled location).
+ */
+ public interface LocationChangeCallback {
+ /**
+ * Called whenever location's state changes.
+ * @param active
+ */
+ default void onLocationActiveChanged(boolean active) {}
+
+ /**
+ * Called whenever location settings change.
+ *
+ * @param locationEnabled A value of true indicates that at least one type of location
+ * is enabled in settings.
+ */
+ default void onLocationSettingsChanged(boolean locationEnabled) {}
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/com/android/systemui/statusbar/policy/LocationControllerImpl.java
new file mode 100644
index 0000000..874f0d9
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/LocationControllerImpl.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.LocationManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.systemui.R;
+import com.android.systemui.util.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A controller to manage changes of location related states and update the views accordingly.
+ */
+public class LocationControllerImpl extends BroadcastReceiver implements LocationController {
+
+ private static final int[] mHighPowerRequestAppOpArray
+ = new int[] {AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION};
+
+ private Context mContext;
+
+ private AppOpsManager mAppOpsManager;
+ private StatusBarManager mStatusBarManager;
+
+ private boolean mAreActiveLocationRequests;
+
+ private ArrayList<LocationChangeCallback> mSettingsChangeCallbacks =
+ new ArrayList<LocationChangeCallback>();
+ private final H mHandler = new H();
+
+ public LocationControllerImpl(Context context, Looper bgLooper) {
+ mContext = context;
+
+ // Register to listen for changes in location settings.
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION);
+ filter.addAction(LocationManager.MODE_CHANGED_ACTION);
+ context.registerReceiverAsUser(this, UserHandle.ALL, filter, null, new Handler(bgLooper));
+
+ mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ mStatusBarManager
+ = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
+
+ // Examine the current location state and initialize the status view.
+ updateActiveLocationRequests();
+ }
+
+ /**
+ * Add a callback to listen for changes in location settings.
+ */
+ public void addCallback(LocationChangeCallback cb) {
+ mSettingsChangeCallbacks.add(cb);
+ mHandler.sendEmptyMessage(H.MSG_LOCATION_SETTINGS_CHANGED);
+ }
+
+ public void removeCallback(LocationChangeCallback cb) {
+ mSettingsChangeCallbacks.remove(cb);
+ }
+
+ /**
+ * Enable or disable location in settings.
+ *
+ * <p>This will attempt to enable/disable every type of location setting
+ * (e.g. high and balanced power).
+ *
+ * <p>If enabling, a user consent dialog will pop up prompting the user to accept.
+ * If the user doesn't accept, network location won't be enabled.
+ *
+ * @return true if attempt to change setting was successful.
+ */
+ public boolean setLocationEnabled(boolean enabled) {
+ int currentUserId = ActivityManager.getCurrentUser();
+ if (isUserLocationRestricted(currentUserId)) {
+ return false;
+ }
+ final ContentResolver cr = mContext.getContentResolver();
+ // When enabling location, a user consent dialog will pop up, and the
+ // setting won't be fully enabled until the user accepts the agreement.
+ int mode = enabled
+ ? Settings.Secure.LOCATION_MODE_PREVIOUS : Settings.Secure.LOCATION_MODE_OFF;
+ // QuickSettings always runs as the owner, so specifically set the settings
+ // for the current foreground user.
+ return Settings.Secure
+ .putIntForUser(cr, Settings.Secure.LOCATION_MODE, mode, currentUserId);
+ }
+
+ /**
+ * Returns true if location isn't disabled in settings.
+ */
+ public boolean isLocationEnabled() {
+ ContentResolver resolver = mContext.getContentResolver();
+ // QuickSettings always runs as the owner, so specifically retrieve the settings
+ // for the current foreground user.
+ int mode = Settings.Secure.getIntForUser(resolver, Settings.Secure.LOCATION_MODE,
+ Settings.Secure.LOCATION_MODE_OFF, ActivityManager.getCurrentUser());
+ return mode != Settings.Secure.LOCATION_MODE_OFF;
+ }
+
+ @Override
+ public boolean isLocationActive() {
+ return mAreActiveLocationRequests;
+ }
+
+ /**
+ * Returns true if the current user is restricted from using location.
+ */
+ private boolean isUserLocationRestricted(int userId) {
+ final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ return um.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION,
+ UserHandle.of(userId));
+ }
+
+ /**
+ * Returns true if there currently exist active high power location requests.
+ */
+ @VisibleForTesting
+ protected boolean areActiveHighPowerLocationRequests() {
+ List<AppOpsManager.PackageOps> packages
+ = mAppOpsManager.getPackagesForOps(mHighPowerRequestAppOpArray);
+ // AppOpsManager can return null when there is no requested data.
+ if (packages != null) {
+ final int numPackages = packages.size();
+ for (int packageInd = 0; packageInd < numPackages; packageInd++) {
+ AppOpsManager.PackageOps packageOp = packages.get(packageInd);
+ List<AppOpsManager.OpEntry> opEntries = packageOp.getOps();
+ if (opEntries != null) {
+ final int numOps = opEntries.size();
+ for (int opInd = 0; opInd < numOps; opInd++) {
+ AppOpsManager.OpEntry opEntry = opEntries.get(opInd);
+ // AppOpsManager should only return OP_MONITOR_HIGH_POWER_LOCATION because
+ // of the mHighPowerRequestAppOpArray filter, but checking defensively.
+ if (opEntry.getOp() == AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION) {
+ if (opEntry.isRunning()) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ // Reads the active location requests and updates the status view if necessary.
+ private void updateActiveLocationRequests() {
+ boolean hadActiveLocationRequests = mAreActiveLocationRequests;
+ mAreActiveLocationRequests = areActiveHighPowerLocationRequests();
+ if (mAreActiveLocationRequests != hadActiveLocationRequests) {
+ mHandler.sendEmptyMessage(H.MSG_LOCATION_ACTIVE_CHANGED);
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION.equals(action)) {
+ updateActiveLocationRequests();
+ } else if (LocationManager.MODE_CHANGED_ACTION.equals(action)) {
+ mHandler.sendEmptyMessage(H.MSG_LOCATION_SETTINGS_CHANGED);
+ }
+ }
+
+ private final class H extends Handler {
+ private static final int MSG_LOCATION_SETTINGS_CHANGED = 1;
+ private static final int MSG_LOCATION_ACTIVE_CHANGED = 2;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_LOCATION_SETTINGS_CHANGED:
+ locationSettingsChanged();
+ break;
+ case MSG_LOCATION_ACTIVE_CHANGED:
+ locationActiveChanged();
+ break;
+ }
+ }
+
+ private void locationActiveChanged() {
+ Utils.safeForeach(mSettingsChangeCallbacks,
+ cb -> cb.onLocationActiveChanged(mAreActiveLocationRequests));
+ }
+
+ private void locationSettingsChanged() {
+ boolean isEnabled = isLocationEnabled();
+ Utils.safeForeach(mSettingsChangeCallbacks,
+ cb -> cb.onLocationSettingsChanged(isEnabled));
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/MobileSignalController.java b/com/android/systemui/statusbar/policy/MobileSignalController.java
new file mode 100644
index 0000000..652f8bb
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/MobileSignalController.java
@@ -0,0 +1,647 @@
+/*
+ * Copyright (C) 2015 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 android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.net.NetworkCapabilities;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings.Global;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.cdma.EriInfo;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.SignalDrawable;
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.policy.NetworkControllerImpl.Config;
+import com.android.systemui.statusbar.policy.NetworkControllerImpl.SubscriptionDefaults;
+
+import java.io.PrintWriter;
+import java.util.BitSet;
+import java.util.Objects;
+
+
+public class MobileSignalController extends SignalController<
+ MobileSignalController.MobileState, MobileSignalController.MobileIconGroup> {
+ private final TelephonyManager mPhone;
+ private final SubscriptionDefaults mDefaults;
+ private final String mNetworkNameDefault;
+ private final String mNetworkNameSeparator;
+ private final ContentObserver mObserver;
+ @VisibleForTesting
+ final PhoneStateListener mPhoneStateListener;
+ // Save entire info for logging, we only use the id.
+ 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 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, CallbackHandler callbackHandler,
+ NetworkControllerImpl networkController, SubscriptionInfo info,
+ SubscriptionDefaults defaults, Looper receiverLooper) {
+ super("MobileSignalController(" + info.getSubscriptionId() + ")", context,
+ NetworkCapabilities.TRANSPORT_CELLULAR, callbackHandler,
+ networkController);
+ mNetworkToIconLookup = new SparseArray<>();
+ mConfig = config;
+ mPhone = phone;
+ mDefaults = defaults;
+ mSubscriptionInfo = info;
+ mPhoneStateListener = new MobilePhoneStateListener(info.getSubscriptionId(),
+ receiverLooper);
+ mNetworkNameSeparator = getStringIfExists(R.string.status_bar_network_name_separator);
+ mNetworkNameDefault = getStringIfExists(
+ com.android.internal.R.string.lockscreen_carrier_default);
+
+ mapIconSets();
+
+ String networkName = info.getCarrierName() != null ? info.getCarrierName().toString()
+ : mNetworkNameDefault;
+ mLastState.networkName = mCurrentState.networkName = networkName;
+ mLastState.networkNameData = mCurrentState.networkNameData = networkName;
+ mLastState.enabled = mCurrentState.enabled = hasMobileData;
+ mLastState.iconGroup = mCurrentState.iconGroup = mDefaultIcons;
+ // Get initial data sim state.
+ updateDataSim();
+ mObserver = new ContentObserver(new Handler(receiverLooper)) {
+ @Override
+ public void onChange(boolean selfChange) {
+ updateTelephony();
+ }
+ };
+ }
+
+ public void setConfiguration(Config config) {
+ mConfig = config;
+ mapIconSets();
+ updateTelephony();
+ }
+
+ public int getDataContentDescription() {
+ return getIcons().mDataContentDescription;
+ }
+
+ public void setAirplaneMode(boolean airplaneMode) {
+ mCurrentState.airplaneMode = airplaneMode;
+ notifyListenersIfNecessary();
+ }
+
+ public void setUserSetupComplete(boolean userSetup) {
+ mCurrentState.userSetup = userSetup;
+ notifyListenersIfNecessary();
+ }
+
+ @Override
+ public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) {
+ boolean isValidated = validatedTransports.get(mTransportType);
+ mCurrentState.isDefault = connectedTransports.get(mTransportType);
+ // Only show this as not having connectivity if we are default.
+ mCurrentState.inetCondition = (isValidated || !mCurrentState.isDefault) ? 1 : 0;
+ notifyListenersIfNecessary();
+ }
+
+ public void setCarrierNetworkChangeMode(boolean carrierNetworkChangeMode) {
+ mCurrentState.carrierNetworkChangeMode = carrierNetworkChangeMode;
+ updateTelephony();
+ }
+
+ /**
+ * 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
+ | PhoneStateListener.LISTEN_CARRIER_NETWORK_CHANGE);
+ mContext.getContentResolver().registerContentObserver(Global.getUriFor(Global.MOBILE_DATA),
+ true, mObserver);
+ mContext.getContentResolver().registerContentObserver(Global.getUriFor(
+ Global.MOBILE_DATA + mSubscriptionInfo.getSubscriptionId()),
+ true, mObserver);
+ }
+
+ /**
+ * Stop listening for phone state changes.
+ */
+ public void unregisterListener() {
+ mPhone.listen(mPhoneStateListener, 0);
+ mContext.getContentResolver().unregisterContentObserver(mObserver);
+ }
+
+ /**
+ * Produce a mapping of data network types to icon groups for simple and quick use in
+ * updateTelephony.
+ */
+ private void mapIconSets() {
+ mNetworkToIconLookup.clear();
+
+ 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);
+ mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_TD_SCDMA, TelephonyIcons.THREE_G);
+
+ 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);
+
+ mDefaultIcons = TelephonyIcons.G;
+ } else {
+ 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;
+ }
+
+ 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);
+
+ if (mConfig.show4gForLte) {
+ mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.FOUR_G);
+ if (mConfig.hideLtePlus) {
+ mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA,
+ TelephonyIcons.FOUR_G);
+ } else {
+ mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA,
+ TelephonyIcons.FOUR_G_PLUS);
+ }
+ } else {
+ mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.LTE);
+ if (mConfig.hideLtePlus) {
+ mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA,
+ TelephonyIcons.LTE);
+ } else {
+ mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA,
+ TelephonyIcons.LTE_PLUS);
+ }
+ }
+ mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_IWLAN, TelephonyIcons.WFC);
+ }
+
+ private int getNumLevels() {
+ if (mConfig.inflateSignalStrengths) {
+ return SignalStrength.NUM_SIGNAL_STRENGTH_BINS + 1;
+ }
+ return SignalStrength.NUM_SIGNAL_STRENGTH_BINS;
+ }
+
+ @Override
+ public int getCurrentIconId() {
+ if (mCurrentState.iconGroup == TelephonyIcons.CARRIER_NETWORK_CHANGE) {
+ return SignalDrawable.getCarrierChangeState(getNumLevels());
+ } else if (mCurrentState.connected) {
+ int level = mCurrentState.level;
+ if (mConfig.inflateSignalStrengths) {
+ level++;
+ }
+ return SignalDrawable.getState(level, getNumLevels(),
+ mCurrentState.inetCondition == 0);
+ } else if (mCurrentState.enabled) {
+ return SignalDrawable.getEmptyState(getNumLevels());
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int getQsCurrentIconId() {
+ if (mCurrentState.airplaneMode) {
+ return SignalDrawable.getAirplaneModeState(getNumLevels());
+ }
+
+ return getCurrentIconId();
+ }
+
+ @Override
+ public void notifyListeners(SignalCallback callback) {
+ MobileIconGroup icons = getIcons();
+
+ String contentDescription = getStringIfExists(getContentDescription());
+ String dataContentDescription = getStringIfExists(icons.mDataContentDescription);
+ final boolean dataDisabled = mCurrentState.iconGroup == TelephonyIcons.DATA_DISABLED
+ && mCurrentState.userSetup;
+
+ // Show icon in QS when we are connected or data is disabled.
+ boolean showDataIcon = mCurrentState.dataConnected || dataDisabled;
+ IconState statusIcon = new IconState(mCurrentState.enabled && !mCurrentState.airplaneMode,
+ getCurrentIconId(), contentDescription);
+
+ int qsTypeIcon = 0;
+ IconState qsIcon = null;
+ String description = null;
+ // Only send data sim callbacks to QS.
+ if (mCurrentState.dataSim) {
+ qsTypeIcon = showDataIcon ? icons.mQsDataType : 0;
+ qsIcon = new IconState(mCurrentState.enabled
+ && !mCurrentState.isEmergency, getQsCurrentIconId(), contentDescription);
+ description = mCurrentState.isEmergency ? null : mCurrentState.networkName;
+ }
+ boolean activityIn = mCurrentState.dataConnected
+ && !mCurrentState.carrierNetworkChangeMode
+ && mCurrentState.activityIn;
+ boolean activityOut = mCurrentState.dataConnected
+ && !mCurrentState.carrierNetworkChangeMode
+ && mCurrentState.activityOut;
+ showDataIcon &= mCurrentState.isDefault || dataDisabled;
+ int typeIcon = showDataIcon ? icons.mDataType : 0;
+ callback.setMobileDataIndicators(statusIcon, qsIcon, typeIcon, qsTypeIcon,
+ activityIn, activityOut, dataContentDescription, description, icons.mIsWide,
+ mSubscriptionInfo.getSubscriptionId(), mCurrentState.roaming);
+ }
+
+ @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 {
+ return false;
+ }
+ }
+
+ private boolean isCdma() {
+ return (mSignalStrength != null) && !mSignalStrength.isGsm();
+ }
+
+ public boolean isEmergencyOnly() {
+ return (mServiceState != null && mServiceState.isEmergencyOnly());
+ }
+
+ private boolean isRoaming() {
+ // During a carrier change, roaming indications need to be supressed.
+ if (isCarrierNetworkChangeActive()) {
+ return false;
+ }
+ if (isCdma() && mServiceState != null) {
+ 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();
+ }
+ }
+
+ private boolean isCarrierNetworkChangeActive() {
+ return mCurrentState.carrierNetworkChangeMode;
+ }
+
+ 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.getStringExtra(TelephonyIntents.EXTRA_DATA_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();
+ notifyListenersIfNecessary();
+ }
+ }
+
+ private void updateDataSim() {
+ int defaultDataSub = mDefaults.getDefaultDataSubId();
+ if (SubscriptionManager.isValidSubscriptionId(defaultDataSub)) {
+ mCurrentState.dataSim = defaultDataSub == mSubscriptionInfo.getSubscriptionId();
+ } else {
+ // 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;
+ }
+ }
+
+ /**
+ * Updates the network's name based on incoming spn and plmn.
+ */
+ void updateNetworkName(boolean showSpn, String spn, String dataSpn,
+ boolean showPlmn, String plmn) {
+ if (CHATTY) {
+ Log.d("CarrierLabel", "updateNetworkName showSpn=" + showSpn
+ + " spn=" + spn + " dataSpn=" + dataSpn
+ + " showPlmn=" + showPlmn + " plmn=" + plmn);
+ }
+ StringBuilder str = new StringBuilder();
+ StringBuilder strData = new StringBuilder();
+ if (showPlmn && plmn != null) {
+ str.append(plmn);
+ strData.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;
+ }
+ if (showSpn && dataSpn != null) {
+ if (strData.length() != 0) {
+ strData.append(mNetworkNameSeparator);
+ }
+ strData.append(dataSpn);
+ }
+ if (strData.length() != 0) {
+ mCurrentState.networkNameData = strData.toString();
+ } else {
+ mCurrentState.networkNameData = mNetworkNameDefault;
+ }
+ }
+
+ /**
+ * 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(mTag, "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;
+
+ mCurrentState.roaming = isRoaming();
+ if (isCarrierNetworkChangeActive()) {
+ mCurrentState.iconGroup = TelephonyIcons.CARRIER_NETWORK_CHANGE;
+ } else if (isDataDisabled()) {
+ mCurrentState.iconGroup = TelephonyIcons.DATA_DISABLED;
+ }
+ 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
+ && !TextUtils.isEmpty(mServiceState.getOperatorAlphaShort())) {
+ mCurrentState.networkName = mServiceState.getOperatorAlphaShort();
+ }
+
+ notifyListenersIfNecessary();
+ }
+
+ private boolean isDataDisabled() {
+ return !mPhone.getDataEnabled(mSubscriptionInfo.getSubscriptionId());
+ }
+
+ @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();
+ }
+
+ @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, Looper looper) {
+ super(subId, looper);
+ }
+
+ @Override
+ public void onSignalStrengthsChanged(SignalStrength signalStrength) {
+ if (DEBUG) {
+ Log.d(mTag, "onSignalStrengthsChanged signalStrength=" + signalStrength +
+ ((signalStrength == null) ? "" : (" level=" + signalStrength.getLevel())));
+ }
+ mSignalStrength = signalStrength;
+ updateTelephony();
+ }
+
+ @Override
+ public void onServiceStateChanged(ServiceState state) {
+ if (DEBUG) {
+ Log.d(mTag, "onServiceStateChanged voiceState=" + state.getVoiceRegState()
+ + " dataState=" + state.getDataRegState());
+ }
+ mServiceState = state;
+ if (state != null) {
+ mDataNetType = state.getDataNetworkType();
+ if (mDataNetType == TelephonyManager.NETWORK_TYPE_LTE && mServiceState != null &&
+ mServiceState.isUsingCarrierAggregation()) {
+ mDataNetType = TelephonyManager.NETWORK_TYPE_LTE_CA;
+ }
+ }
+ updateTelephony();
+ }
+
+ @Override
+ public void onDataConnectionStateChanged(int state, int networkType) {
+ if (DEBUG) {
+ Log.d(mTag, "onDataConnectionStateChanged: state=" + state
+ + " type=" + networkType);
+ }
+ mDataState = state;
+ mDataNetType = networkType;
+ if (mDataNetType == TelephonyManager.NETWORK_TYPE_LTE && mServiceState != null &&
+ mServiceState.isUsingCarrierAggregation()) {
+ mDataNetType = TelephonyManager.NETWORK_TYPE_LTE_CA;
+ }
+ updateTelephony();
+ }
+
+ @Override
+ public void onDataActivity(int direction) {
+ if (DEBUG) {
+ Log.d(mTag, "onDataActivity: direction=" + direction);
+ }
+ setActivity(direction);
+ }
+
+ @Override
+ public void onCarrierNetworkChange(boolean active) {
+ if (DEBUG) {
+ Log.d(mTag, "onCarrierNetworkChange: active=" + active);
+ }
+ mCurrentState.carrierNetworkChangeMode = active;
+
+ updateTelephony();
+ }
+ };
+
+ 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;
+ }
+ }
+
+ static class MobileState extends SignalController.State {
+ String networkName;
+ String networkNameData;
+ boolean dataSim;
+ boolean dataConnected;
+ boolean isEmergency;
+ boolean airplaneMode;
+ boolean carrierNetworkChangeMode;
+ boolean isDefault;
+ boolean userSetup;
+ boolean roaming;
+
+ @Override
+ public void copyFrom(State s) {
+ super.copyFrom(s);
+ MobileState state = (MobileState) s;
+ dataSim = state.dataSim;
+ networkName = state.networkName;
+ networkNameData = state.networkNameData;
+ dataConnected = state.dataConnected;
+ isDefault = state.isDefault;
+ isEmergency = state.isEmergency;
+ airplaneMode = state.airplaneMode;
+ carrierNetworkChangeMode = state.carrierNetworkChangeMode;
+ userSetup = state.userSetup;
+ roaming = state.roaming;
+ }
+
+ @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("networkNameData=").append(networkNameData).append(',');
+ builder.append("dataConnected=").append(dataConnected).append(',');
+ builder.append("roaming=").append(roaming).append(',');
+ builder.append("isDefault=").append(isDefault).append(',');
+ builder.append("isEmergency=").append(isEmergency).append(',');
+ builder.append("airplaneMode=").append(airplaneMode).append(',');
+ builder.append("carrierNetworkChangeMode=").append(carrierNetworkChangeMode)
+ .append(',');
+ builder.append("userSetup=").append(userSetup);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return super.equals(o)
+ && Objects.equals(((MobileState) o).networkName, networkName)
+ && Objects.equals(((MobileState) o).networkNameData, networkNameData)
+ && ((MobileState) o).dataSim == dataSim
+ && ((MobileState) o).dataConnected == dataConnected
+ && ((MobileState) o).isEmergency == isEmergency
+ && ((MobileState) o).airplaneMode == airplaneMode
+ && ((MobileState) o).carrierNetworkChangeMode == carrierNetworkChangeMode
+ && ((MobileState) o).userSetup == userSetup
+ && ((MobileState) o).isDefault == isDefault
+ && ((MobileState) o).roaming == roaming;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/NetworkController.java b/com/android/systemui/statusbar/policy/NetworkController.java
new file mode 100644
index 0000000..2771011
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/NetworkController.java
@@ -0,0 +1,102 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telephony.SubscriptionInfo;
+
+import com.android.settingslib.net.DataUsageController;
+import com.android.settingslib.wifi.AccessPoint;
+import com.android.systemui.DemoMode;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.util.List;
+
+public interface NetworkController extends CallbackController<SignalCallback>, DemoMode {
+
+ boolean hasMobileDataFeature();
+ void addCallback(SignalCallback cb);
+ void removeCallback(SignalCallback cb);
+ void setWifiEnabled(boolean enabled);
+ AccessPointController getAccessPointController();
+ DataUsageController getMobileDataController();
+ DataSaverController getDataSaverController();
+
+ boolean hasVoiceCallingFeature();
+
+ void addEmergencyListener(EmergencyListener listener);
+ void removeEmergencyListener(EmergencyListener listener);
+ boolean hasEmergencyCryptKeeperText();
+ boolean isRadioOn();
+
+ public interface SignalCallback {
+ default void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon,
+ boolean activityIn, boolean activityOut, String description, boolean isTransient) {}
+
+ default void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType,
+ int qsType, boolean activityIn, boolean activityOut, String typeContentDescription,
+ String description, boolean isWide, int subId, boolean roaming) {}
+ default void setSubs(List<SubscriptionInfo> subs) {}
+ default void setNoSims(boolean show) {}
+
+ default void setEthernetIndicators(IconState icon) {}
+
+ default void setIsAirplaneMode(IconState icon) {}
+
+ default void setMobileDataEnabled(boolean enabled) {}
+ }
+
+ public interface EmergencyListener {
+ void setEmergencyCallsOnly(boolean emergencyOnly);
+ }
+
+ public static class IconState {
+ public final boolean visible;
+ public final int icon;
+ public final String contentDescription;
+
+ public IconState(boolean visible, int icon, String contentDescription) {
+ this.visible = visible;
+ this.icon = icon;
+ this.contentDescription = contentDescription;
+ }
+
+ public IconState(boolean visible, int icon, int contentDescription,
+ Context context) {
+ this(visible, icon, context.getString(contentDescription));
+ }
+ }
+
+ /**
+ * 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();
+ int getIcon(AccessPoint ap);
+ boolean connect(AccessPoint ap);
+ boolean canConfigWifi();
+
+ public interface AccessPointCallback {
+ void onAccessPointsChanged(List<AccessPoint> accessPoints);
+ void onSettingsActivityTriggered(Intent settingsIntent);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
new file mode 100644
index 0000000..c217bda
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -0,0 +1,978 @@
+/*
+ * Copyright (C) 2010 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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+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.util.Log;
+import android.util.MathUtils;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.settingslib.net.DataUsageController;
+import com.android.systemui.ConfigurationChangedReceiver;
+import com.android.systemui.DemoMode;
+import com.android.systemui.Dumpable;
+import com.android.systemui.R;
+import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+
+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 static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+
+/** Platform implementation of the network controller. **/
+public class NetworkControllerImpl extends BroadcastReceiver
+ implements NetworkController, DemoMode, DataUsageController.NetworkNameProvider,
+ ConfigurationChangedReceiver, Dumpable {
+ // debug
+ 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);
+
+ private static final int EMERGENCY_NO_CONTROLLERS = 0;
+ private static final int EMERGENCY_FIRST_CONTROLLER = 100;
+ private static final int EMERGENCY_VOICE_CONTROLLER = 200;
+ private static final int EMERGENCY_NO_SUB = 300;
+ private static final int EMERGENCY_ASSUMED_VOICE_CONTROLLER = 400;
+
+ 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 final SubscriptionDefaults mSubDefaults;
+ private final DataSaverController mDataSaverController;
+ private final CurrentUserTracker mUserTracker;
+ private Config mConfig;
+
+ // Subcontrollers.
+ @VisibleForTesting
+ final WifiSignalController mWifiSignalController;
+
+ @VisibleForTesting
+ final EthernetSignalController mEthernetSignalController;
+
+ @VisibleForTesting
+ final SparseArray<MobileSignalController> mMobileSignalControllers = new SparseArray<>();
+ // 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 DataUsageController mDataUsageController;
+
+ private boolean mInetCondition; // Used for Logging and demo.
+
+ // 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 mHasNoSims;
+ private Locale mLocale = null;
+ // This list holds our ordering.
+ private List<SubscriptionInfo> mCurrentSubscriptions = new ArrayList<>();
+
+ @VisibleForTesting
+ boolean mListening;
+
+ // The current user ID.
+ private int mCurrentUserId;
+
+ private OnSubscriptionsChangedListener mSubscriptionListener;
+
+ // Handler that all broadcasts are received on.
+ private final Handler mReceiverHandler;
+ // Handler that all callbacks are made on.
+ private final CallbackHandler mCallbackHandler;
+
+ private int mEmergencySource;
+ private boolean mIsEmergency;
+
+ @VisibleForTesting
+ ServiceState mLastServiceState;
+ private boolean mUserSetup;
+
+ /**
+ * Construct this controller object and register for updates.
+ */
+ public NetworkControllerImpl(Context context, Looper bgLooper,
+ DeviceProvisionedController deviceProvisionedController) {
+ 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), bgLooper,
+ new CallbackHandler(),
+ new AccessPointControllerImpl(context, bgLooper),
+ new DataUsageController(context),
+ new SubscriptionDefaults(),
+ deviceProvisionedController);
+ mReceiverHandler.post(mRegisterListeners);
+ }
+
+ @VisibleForTesting
+ NetworkControllerImpl(Context context, ConnectivityManager connectivityManager,
+ TelephonyManager telephonyManager, WifiManager wifiManager,
+ SubscriptionManager subManager, Config config, Looper bgLooper,
+ CallbackHandler callbackHandler,
+ AccessPointControllerImpl accessPointController,
+ DataUsageController dataUsageController,
+ SubscriptionDefaults defaultsHandler,
+ DeviceProvisionedController deviceProvisionedController) {
+ mContext = context;
+ mConfig = config;
+ mReceiverHandler = new Handler(bgLooper);
+ mCallbackHandler = callbackHandler;
+ mDataSaverController = new DataSaverControllerImpl(context);
+
+ mSubscriptionManager = subManager;
+ mSubDefaults = defaultsHandler;
+ mConnectivityManager = connectivityManager;
+ mHasMobileDataFeature =
+ mConnectivityManager.isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
+
+ // telephony
+ mPhone = telephonyManager;
+
+ // wifi
+ mWifiManager = wifiManager;
+
+ mLocale = mContext.getResources().getConfiguration().locale;
+ mAccessPoints = accessPointController;
+ mDataUsageController = dataUsageController;
+ mDataUsageController.setNetworkController(this);
+ // TODO: Find a way to move this into DataUsageController.
+ mDataUsageController.setCallback(new DataUsageController.Callback() {
+ @Override
+ public void onMobileDataEnabled(boolean enabled) {
+ mCallbackHandler.setMobileDataEnabled(enabled);
+ }
+ });
+ mWifiSignalController = new WifiSignalController(mContext, mHasMobileDataFeature,
+ mCallbackHandler, this);
+
+ mEthernetSignalController = new EthernetSignalController(mContext, mCallbackHandler, this);
+
+ // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it
+ updateAirplaneMode(true /* force callback */);
+ mUserTracker = new CurrentUserTracker(mContext) {
+ @Override
+ public void onUserSwitched(int newUserId) {
+ NetworkControllerImpl.this.onUserSwitched(newUserId);
+ }
+ };
+ mUserTracker.startTracking();
+ deviceProvisionedController.addCallback(new DeviceProvisionedListener() {
+ @Override
+ public void onUserSetupChanged() {
+ setUserSetupComplete(deviceProvisionedController.isUserSetup(
+ deviceProvisionedController.getCurrentUser()));
+ }
+ });
+ }
+
+ public DataSaverController getDataSaverController() {
+ return mDataSaverController;
+ }
+
+ private void registerListeners() {
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+ mobileSignalController.registerListener();
+ }
+ if (mSubscriptionListener == null) {
+ mSubscriptionListener = new SubListener();
+ }
+ mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionListener);
+
+ // broadcasts
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(WifiManager.RSSI_CHANGED_ACTION);
+ 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.ACTION_SERVICE_STATE_CHANGED);
+ filter.addAction(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION);
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ filter.addAction(ConnectivityManager.INET_CONDITION_ACTION);
+ filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ mContext.registerReceiver(this, filter, null, mReceiverHandler);
+ mListening = true;
+
+ updateMobileControllers();
+ }
+
+ private void unregisterListeners() {
+ mListening = false;
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+ mobileSignalController.unregisterListener();
+ }
+ mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener);
+ mContext.unregisterReceiver(this);
+ }
+
+ public int getConnectedWifiLevel() {
+ return mWifiSignalController.getState().level;
+ }
+
+ @Override
+ public AccessPointController getAccessPointController() {
+ return mAccessPoints;
+ }
+
+ @Override
+ public DataUsageController getMobileDataController() {
+ return mDataUsageController;
+ }
+
+ public void addEmergencyListener(EmergencyListener listener) {
+ mCallbackHandler.setListening(listener, true);
+ mCallbackHandler.setEmergencyCallsOnly(isEmergencyOnly());
+ }
+
+ public void removeEmergencyListener(EmergencyListener listener) {
+ mCallbackHandler.setListening(listener, false);
+ }
+
+ public boolean hasMobileDataFeature() {
+ return mHasMobileDataFeature;
+ }
+
+ public boolean hasVoiceCallingFeature() {
+ return mPhone.getPhoneType() != TelephonyManager.PHONE_TYPE_NONE;
+ }
+
+ private MobileSignalController getDataController() {
+ int dataSubId = mSubDefaults.getDefaultDataSubId();
+ if (!SubscriptionManager.isValidSubscriptionId(dataSubId)) {
+ if (DEBUG) Log.e(TAG, "No data sim selected");
+ return mDefaultSignalController;
+ }
+ if (mMobileSignalControllers.indexOfKey(dataSubId) >= 0) {
+ return mMobileSignalControllers.get(dataSubId);
+ }
+ if (DEBUG) Log.e(TAG, "Cannot find controller for data sub: " + dataSubId);
+ return mDefaultSignalController;
+ }
+
+ public String getMobileDataNetworkName() {
+ MobileSignalController controller = getDataController();
+ return controller != null ? controller.getState().networkNameData : "";
+ }
+
+ public boolean isEmergencyOnly() {
+ if (mMobileSignalControllers.size() == 0) {
+ // When there are no active subscriptions, determine emengency state from last
+ // broadcast.
+ mEmergencySource = EMERGENCY_NO_CONTROLLERS;
+ return mLastServiceState != null && mLastServiceState.isEmergencyOnly();
+ }
+ int voiceSubId = mSubDefaults.getDefaultVoiceSubId();
+ if (!SubscriptionManager.isValidSubscriptionId(voiceSubId)) {
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+ if (!mobileSignalController.getState().isEmergency) {
+ mEmergencySource = EMERGENCY_FIRST_CONTROLLER
+ + mobileSignalController.mSubscriptionInfo.getSubscriptionId();
+ if (DEBUG) Log.d(TAG, "Found emergency " + mobileSignalController.mTag);
+ return false;
+ }
+ }
+ }
+ if (mMobileSignalControllers.indexOfKey(voiceSubId) >= 0) {
+ mEmergencySource = EMERGENCY_VOICE_CONTROLLER + voiceSubId;
+ if (DEBUG) Log.d(TAG, "Getting emergency from " + voiceSubId);
+ return mMobileSignalControllers.get(voiceSubId).getState().isEmergency;
+ }
+ // If we have the wrong subId but there is only one sim anyway, assume it should be the
+ // default.
+ if (mMobileSignalControllers.size() == 1) {
+ mEmergencySource = EMERGENCY_ASSUMED_VOICE_CONTROLLER
+ + mMobileSignalControllers.keyAt(0);
+ if (DEBUG) Log.d(TAG, "Getting assumed emergency from "
+ + mMobileSignalControllers.keyAt(0));
+ return mMobileSignalControllers.valueAt(0).getState().isEmergency;
+ }
+ if (DEBUG) Log.e(TAG, "Cannot find controller for voice sub: " + voiceSubId);
+ mEmergencySource = EMERGENCY_NO_SUB + voiceSubId;
+ // Something is wrong, better assume we can't make calls...
+ return true;
+ }
+
+ /**
+ * Emergency status may have changed (triggered by MobileSignalController),
+ * so we should recheck and send out the state to listeners.
+ */
+ void recalculateEmergency() {
+ mIsEmergency = isEmergencyOnly();
+ mCallbackHandler.setEmergencyCallsOnly(mIsEmergency);
+ }
+
+ public void addCallback(SignalCallback cb) {
+ cb.setSubs(mCurrentSubscriptions);
+ cb.setIsAirplaneMode(new IconState(mAirplaneMode,
+ TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode, mContext));
+ cb.setNoSims(mHasNoSims);
+ mWifiSignalController.notifyListeners(cb);
+ mEthernetSignalController.notifyListeners(cb);
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+ mobileSignalController.notifyListeners(cb);
+ }
+ mCallbackHandler.setListening(cb, true);
+ }
+
+ @Override
+ public void removeCallback(SignalCallback cb) {
+ mCallbackHandler.setListening(cb, false);
+ }
+
+ @Override
+ public void setWifiEnabled(final boolean enabled) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... args) {
+ // Disable tethering if enabling Wifi
+ final int wifiApState = mWifiManager.getWifiApState();
+ if (enabled && ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) ||
+ (wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) {
+ mWifiManager.setWifiApEnabled(null, false);
+ }
+
+ mWifiManager.setWifiEnabled(enabled);
+ return null;
+ }
+ }.execute();
+ }
+
+ private void onUserSwitched(int newUserId) {
+ mCurrentUserId = newUserId;
+ mAccessPoints.onUserSwitched(newUserId);
+ updateConnectivity();
+ }
+
+ @Override
+ 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) ||
+ action.equals(ConnectivityManager.INET_CONDITION_ACTION)) {
+ updateConnectivity();
+ } else if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
+ refreshLocale();
+ updateAirplaneMode(false);
+ } 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 (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+ controller.handleBroadcast(intent);
+ }
+ } else if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) {
+ // Might have different subscriptions now.
+ updateMobileControllers();
+ } else if (action.equals(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED)) {
+ mLastServiceState = ServiceState.newFromBundle(intent.getExtras());
+ if (mMobileSignalControllers.size() == 0) {
+ // If none of the subscriptions are active, we might need to recalculate
+ // emergency state.
+ recalculateEmergency();
+ }
+ } else {
+ int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+ if (SubscriptionManager.isValidSubscriptionId(subId)) {
+ if (mMobileSignalControllers.indexOfKey(subId) >= 0) {
+ mMobileSignalControllers.get(subId).handleBroadcast(intent);
+ } else {
+ // Can't find this subscription... We must be out of date.
+ updateMobileControllers();
+ }
+ } else {
+ // No sub id, must be for the wifi.
+ mWifiSignalController.handleBroadcast(intent);
+ }
+ }
+ }
+
+ public void onConfigurationChanged(Configuration newConfig) {
+ mConfig = Config.readConfig(mContext);
+ mReceiverHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ handleConfigurationChanged();
+ }
+ });
+ }
+
+ @VisibleForTesting
+ void handleConfigurationChanged() {
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+ controller.setConfiguration(mConfig);
+ }
+ refreshLocale();
+ }
+
+ private void updateMobileControllers() {
+ if (!mListening) {
+ return;
+ }
+ doUpdateMobileControllers();
+ }
+
+ @VisibleForTesting
+ void doUpdateMobileControllers() {
+ 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();
+ recalculateEmergency();
+ }
+
+ @VisibleForTesting
+ protected void updateNoSims() {
+ boolean hasNoSims = mHasMobileDataFeature && mMobileSignalControllers.size() == 0;
+ if (hasNoSims != mHasNoSims) {
+ mHasNoSims = hasNoSims;
+ mCallbackHandler.setNoSims(mHasNoSims);
+ }
+ }
+
+ @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();
+ }
+ });
+ mCurrentSubscriptions = subscriptions;
+
+ SparseArray<MobileSignalController> cachedControllers =
+ new SparseArray<MobileSignalController>();
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ cachedControllers.put(mMobileSignalControllers.keyAt(i),
+ mMobileSignalControllers.valueAt(i));
+ }
+ 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.indexOfKey(subId) >= 0) {
+ mMobileSignalControllers.put(subId, cachedControllers.get(subId));
+ cachedControllers.remove(subId);
+ } else {
+ MobileSignalController controller = new MobileSignalController(mContext, mConfig,
+ mHasMobileDataFeature, mPhone, mCallbackHandler,
+ this, subscriptions.get(i), mSubDefaults, mReceiverHandler.getLooper());
+ controller.setUserSetupComplete(mUserSetup);
+ mMobileSignalControllers.put(subId, controller);
+ if (subscriptions.get(i).getSimSlotIndex() == 0) {
+ mDefaultSignalController = controller;
+ }
+ if (mListening) {
+ controller.registerListener();
+ }
+ }
+ }
+ if (mListening) {
+ for (int i = 0; i < cachedControllers.size(); i++) {
+ int key = cachedControllers.keyAt(i);
+ if (cachedControllers.get(key) == mDefaultSignalController) {
+ mDefaultSignalController = null;
+ }
+ cachedControllers.get(key).unregisterListener();
+ }
+ }
+ mCallbackHandler.setSubs(subscriptions);
+ notifyAllListeners();
+
+ // There may be new MobileSignalControllers around, make sure they get the current
+ // inet condition and airplane mode.
+ pushConnectivityToSignals();
+ updateAirplaneMode(true /* force */);
+ }
+
+ private void setUserSetupComplete(final boolean userSetup) {
+ mReceiverHandler.post(() -> handleSetUserSetupComplete(userSetup));
+ }
+
+ private void handleSetUserSetupComplete(boolean userSetup) {
+ mUserSetup = userSetup;
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+ controller.setUserSetupComplete(mUserSetup);
+ }
+ }
+
+ @VisibleForTesting
+ boolean hasCorrectMobileControllers(List<SubscriptionInfo> allSubscriptions) {
+ if (allSubscriptions.size() != mMobileSignalControllers.size()) {
+ return false;
+ }
+ for (SubscriptionInfo info : allSubscriptions) {
+ if (mMobileSignalControllers.indexOfKey(info.getSubscriptionId()) < 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ 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 (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+ mobileSignalController.setAirplaneMode(mAirplaneMode);
+ }
+ notifyListeners();
+ }
+ }
+
+ private void refreshLocale() {
+ Locale current = mContext.getResources().getConfiguration().locale;
+ if (!current.equals(mLocale)) {
+ mLocale = current;
+ notifyAllListeners();
+ }
+ }
+
+ /**
+ * Forces update of all callbacks on both SignalClusters and
+ * NetworkSignalChangedCallbacks.
+ */
+ private void notifyAllListeners() {
+ notifyListeners();
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+ mobileSignalController.notifyListeners();
+ }
+ mWifiSignalController.notifyListeners();
+ mEthernetSignalController.notifyListeners();
+ }
+
+ /**
+ * 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() {
+ mCallbackHandler.setIsAirplaneMode(new IconState(mAirplaneMode,
+ TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode, mContext));
+ mCallbackHandler.setNoSims(mHasNoSims);
+ }
+
+ /**
+ * 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);
+ }
+ }
+ }
+
+ if (CHATTY) {
+ Log.d(TAG, "updateConnectivity: mConnectedTransports=" + mConnectedTransports);
+ Log.d(TAG, "updateConnectivity: mValidatedTransports=" + mValidatedTransports);
+ }
+
+ mInetCondition = !mValidatedTransports.isEmpty();
+
+ pushConnectivityToSignals();
+ }
+
+ /**
+ * 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 (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+ mobileSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports);
+ }
+ mWifiSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports);
+ mEthernetSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports);
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("NetworkController state:");
+
+ pw.println(" - telephony ------");
+ pw.print(" hasVoiceCallingFeature()=");
+ pw.println(hasVoiceCallingFeature());
+
+ 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);
+ pw.print(" mLastServiceState=");
+ pw.println(mLastServiceState);
+ pw.print(" mIsEmergency=");
+ pw.println(mIsEmergency);
+ pw.print(" mEmergencySource=");
+ pw.println(emergencyToString(mEmergencySource));
+
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+ mobileSignalController.dump(pw);
+ }
+ mWifiSignalController.dump(pw);
+
+ mEthernetSignalController.dump(pw);
+
+ mAccessPoints.dump(pw);
+ }
+
+ private static final String emergencyToString(int emergencySource) {
+ if (emergencySource > EMERGENCY_NO_SUB) {
+ return "ASSUMED_VOICE_CONTROLLER(" + (emergencySource - EMERGENCY_VOICE_CONTROLLER)
+ + ")";
+ } else if (emergencySource > EMERGENCY_NO_SUB) {
+ return "NO_SUB(" + (emergencySource - EMERGENCY_NO_SUB) + ")";
+ } else if (emergencySource > EMERGENCY_VOICE_CONTROLLER) {
+ return "VOICE_CONTROLLER(" + (emergencySource - EMERGENCY_VOICE_CONTROLLER) + ")";
+ } else if (emergencySource > EMERGENCY_FIRST_CONTROLLER) {
+ return "FIRST_CONTROLLER(" + (emergencySource - EMERGENCY_FIRST_CONTROLLER) + ")";
+ } else if (emergencySource == EMERGENCY_NO_CONTROLLERS) {
+ return "NO_CONTROLLERS";
+ }
+ return "UNKNOWN_SOURCE";
+ }
+
+ private boolean mDemoMode;
+ private boolean 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;
+ mDemoWifiState = mWifiSignalController.getState();
+ mDemoWifiState.ssid = "DemoMode";
+ } 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 (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+ controller.resetLastState();
+ }
+ mWifiSignalController.resetLastState();
+ mReceiverHandler.post(mRegisterListeners);
+ notifyAllListeners();
+ } else if (mDemoMode && command.equals(COMMAND_NETWORK)) {
+ String airplane = args.getString("airplane");
+ if (airplane != null) {
+ boolean show = airplane.equals("show");
+ mCallbackHandler.setIsAirplaneMode(new IconState(show,
+ TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode,
+ mContext));
+ }
+ String fully = args.getString("fully");
+ if (fully != null) {
+ mDemoInetCondition = Boolean.parseBoolean(fully);
+ BitSet connected = new BitSet();
+
+ if (mDemoInetCondition) {
+ connected.set(mWifiSignalController.mTransportType);
+ }
+ mWifiSignalController.updateConnectivity(connected, connected);
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+ if (mDemoInetCondition) {
+ connected.set(controller.mTransportType);
+ }
+ controller.updateConnectivity(connected, connected);
+ }
+ }
+ 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;
+ }
+ String activity = args.getString("activity");
+ if (activity != null) {
+ switch (activity) {
+ case "inout":
+ mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_INOUT);
+ break;
+ case "in":
+ mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_IN);
+ break;
+ case "out":
+ mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_OUT);
+ break;
+ default:
+ mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_NONE);
+ break;
+ }
+ } else {
+ mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_NONE);
+ }
+ mDemoWifiState.enabled = show;
+ mWifiSignalController.notifyListeners();
+ }
+ String sims = args.getString("sims");
+ if (sims != null) {
+ int num = MathUtils.constrain(Integer.parseInt(sims), 1, 8);
+ List<SubscriptionInfo> subs = new ArrayList<>();
+ if (num != mMobileSignalControllers.size()) {
+ mMobileSignalControllers.clear();
+ int start = mSubscriptionManager.getActiveSubscriptionInfoCountMax();
+ for (int i = start /* get out of normal index range */; i < start + num; i++) {
+ subs.add(addSignalController(i, i));
+ }
+ mCallbackHandler.setSubs(subs);
+ }
+ }
+ String nosim = args.getString("nosim");
+ if (nosim != null) {
+ mHasNoSims = nosim.equals("show");
+ mCallbackHandler.setNoSims(mHasNoSims);
+ }
+ 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);
+ slot = MathUtils.constrain(slot, 0, 8);
+ // Ensure we have enough sim slots
+ List<SubscriptionInfo> subs = new ArrayList<>();
+ while (mMobileSignalControllers.size() <= slot) {
+ int nextSlot = mMobileSignalControllers.size();
+ subs.add(addSignalController(nextSlot, nextSlot));
+ }
+ if (!subs.isEmpty()) {
+ mCallbackHandler.setSubs(subs);
+ }
+ // Hack to index linearly for easy use.
+ MobileSignalController controller = mMobileSignalControllers.valueAt(slot);
+ controller.getState().dataSim = datatype != null;
+ controller.getState().isDefault = datatype != null;
+ controller.getState().dataConnected = 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("4g+") ? TelephonyIcons.FOUR_G_PLUS :
+ datatype.equals("e") ? TelephonyIcons.E :
+ datatype.equals("g") ? TelephonyIcons.G :
+ datatype.equals("h") ? TelephonyIcons.H :
+ datatype.equals("lte") ? TelephonyIcons.LTE :
+ datatype.equals("lte+") ? TelephonyIcons.LTE_PLUS :
+ datatype.equals("dis") ? TelephonyIcons.DATA_DISABLED :
+ TelephonyIcons.UNKNOWN;
+ }
+ if (args.containsKey("roam")) {
+ controller.getState().roaming = "show".equals(args.getString("roam"));
+ }
+ String level = args.getString("level");
+ if (level != null) {
+ controller.getState().level = level.equals("null") ? -1
+ : Math.min(Integer.parseInt(level),
+ SignalStrength.NUM_SIGNAL_STRENGTH_BINS);
+ controller.getState().connected = controller.getState().level >= 0;
+ }
+ String activity = args.getString("activity");
+ if (activity != null) {
+ controller.getState().dataConnected = true;
+ switch (activity) {
+ case "inout":
+ controller.setActivity(TelephonyManager.DATA_ACTIVITY_INOUT);
+ break;
+ case "in":
+ controller.setActivity(TelephonyManager.DATA_ACTIVITY_IN);
+ break;
+ case "out":
+ controller.setActivity(TelephonyManager.DATA_ACTIVITY_OUT);
+ break;
+ default:
+ controller.setActivity(TelephonyManager.DATA_ACTIVITY_NONE);
+ break;
+ }
+ } else {
+ controller.setActivity(TelephonyManager.DATA_ACTIVITY_NONE);
+ }
+ controller.getState().enabled = show;
+ controller.notifyListeners();
+ }
+ String carrierNetworkChange = args.getString("carriernetworkchange");
+ if (carrierNetworkChange != null) {
+ boolean show = carrierNetworkChange.equals("show");
+ for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+ MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+ controller.setCarrierNetworkChangeMode(show);
+ }
+ }
+ }
+ }
+
+ private SubscriptionInfo addSignalController(int id, int simSlotIndex) {
+ SubscriptionInfo info = new SubscriptionInfo(id, "", simSlotIndex, "", "", 0, 0, "", 0,
+ null, 0, 0, "");
+ MobileSignalController controller = new MobileSignalController(mContext,
+ mConfig, mHasMobileDataFeature, mPhone, mCallbackHandler, this, info,
+ mSubDefaults, mReceiverHandler.getLooper());
+ mMobileSignalControllers.put(id, controller);
+ controller.getState().userSetup = true;
+ return info;
+ }
+
+ public boolean hasEmergencyCryptKeeperText() {
+ return EncryptionHelper.IS_DATA_ENCRYPTED;
+ }
+
+ public boolean isRadioOn() {
+ return !mAirplaneMode;
+ }
+
+ private class SubListener extends OnSubscriptionsChangedListener {
+ @Override
+ public void onSubscriptionsChanged() {
+ updateMobileControllers();
+ }
+ }
+
+ /**
+ * Used to register listeners from the BG Looper, this way the PhoneStateListeners that
+ * get created will also run on the BG Looper.
+ */
+ private final Runnable mRegisterListeners = new Runnable() {
+ @Override
+ public void run() {
+ registerListeners();
+ }
+ };
+
+ public static class SubscriptionDefaults {
+ public int getDefaultVoiceSubId() {
+ return SubscriptionManager.getDefaultVoiceSubscriptionId();
+ }
+
+ public int getDefaultDataSubId() {
+ return SubscriptionManager.getDefaultDataSubscriptionId();
+ }
+ }
+
+ @VisibleForTesting
+ static class Config {
+ boolean showAtLeast3G = false;
+ boolean alwaysShowCdmaRssi = false;
+ boolean show4gForLte = false;
+ boolean hideLtePlus = false;
+ boolean hspaDataDistinguishable;
+ boolean inflateSignalStrengths = false;
+
+ 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);
+ config.hideLtePlus = res.getBoolean(R.bool.config_hideLtePlus);
+ config.inflateSignalStrengths = res.getBoolean(R.bool.config_inflateSignalStrength);
+ return config;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/NextAlarmController.java b/com/android/systemui/statusbar/policy/NextAlarmController.java
new file mode 100644
index 0000000..366a752
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/NextAlarmController.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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 android.app.AlarmManager;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback;
+
+public interface NextAlarmController extends CallbackController<NextAlarmChangeCallback>, Dumpable {
+
+ public interface NextAlarmChangeCallback {
+ void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
new file mode 100644
index 0000000..dfdeae1
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
@@ -0,0 +1,82 @@
+/*
+ * 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.policy;
+
+import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.UserHandle;
+
+import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class NextAlarmControllerImpl extends BroadcastReceiver
+ implements NextAlarmController {
+
+ private final ArrayList<NextAlarmChangeCallback> mChangeCallbacks = new ArrayList<>();
+
+ private AlarmManager mAlarmManager;
+ private AlarmManager.AlarmClockInfo mNextAlarm;
+
+ public NextAlarmControllerImpl(Context context) {
+ mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
+ context.registerReceiverAsUser(this, UserHandle.ALL, filter, null, null);
+ updateNextAlarm();
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("NextAlarmController state:");
+ pw.print(" mNextAlarm="); pw.println(mNextAlarm);
+ }
+
+ public void addCallback(NextAlarmChangeCallback cb) {
+ mChangeCallbacks.add(cb);
+ cb.onNextAlarmChanged(mNextAlarm);
+ }
+
+ public void removeCallback(NextAlarmChangeCallback cb) {
+ mChangeCallbacks.remove(cb);
+ }
+
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(Intent.ACTION_USER_SWITCHED)
+ || action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) {
+ updateNextAlarm();
+ }
+ }
+
+ private void updateNextAlarm() {
+ mNextAlarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT);
+ fireNextAlarmChanged();
+ }
+
+ private void fireNextAlarmChanged() {
+ int n = mChangeCallbacks.size();
+ for (int i = 0; i < n; i++) {
+ mChangeCallbacks.get(i).onNextAlarmChanged(mNextAlarm);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java b/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
new file mode 100644
index 0000000..5444f06
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 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.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+
+/**
+ * A listener to heads up changes
+ */
+public interface OnHeadsUpChangedListener {
+ /**
+ * The state whether there exist pinned heads-ups or not changed.
+ *
+ * @param inPinnedMode whether there are any pinned heads-ups
+ */
+ default void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {}
+
+ /**
+ * A notification was just pinned to the top.
+ */
+ default void onHeadsUpPinned(ExpandableNotificationRow headsUp) {}
+
+ /**
+ * A notification was just unpinned from the top.
+ */
+ default void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {}
+
+ /**
+ * A notification just became a heads up or turned back to its normal state.
+ *
+ * @param entry the entry of the changed notification
+ * @param isHeadsUp whether the notification is now a headsUp notification
+ */
+ default void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {}
+}
diff --git a/com/android/systemui/statusbar/policy/PreviewInflater.java b/com/android/systemui/statusbar/policy/PreviewInflater.java
new file mode 100644
index 0000000..687b83a
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/PreviewInflater.java
@@ -0,0 +1,196 @@
+/*
+ * 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.policy;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.statusbar.phone.KeyguardPreviewContainer;
+
+import java.util.List;
+
+/**
+ * Utility class to inflate previews for phone and camera affordance.
+ */
+public class PreviewInflater {
+
+ private static final String TAG = "PreviewInflater";
+
+ private static final String META_DATA_KEYGUARD_LAYOUT = "com.android.keyguard.layout";
+
+ private Context mContext;
+ private LockPatternUtils mLockPatternUtils;
+
+ public PreviewInflater(Context context, LockPatternUtils lockPatternUtils) {
+ mContext = context;
+ mLockPatternUtils = lockPatternUtils;
+ }
+
+ public View inflatePreview(Intent intent) {
+ WidgetInfo info = getWidgetInfo(intent);
+ return inflatePreview(info);
+ }
+
+ public View inflatePreviewFromService(ComponentName componentName) {
+ WidgetInfo info = getWidgetInfoFromService(componentName);
+ return inflatePreview(info);
+ }
+
+ private KeyguardPreviewContainer inflatePreview(WidgetInfo info) {
+ if (info == null) {
+ return null;
+ }
+ View v = inflateWidgetView(info);
+ if (v == null) {
+ return null;
+ }
+ KeyguardPreviewContainer container = new KeyguardPreviewContainer(mContext, null);
+ container.addView(v);
+ return container;
+ }
+
+ private View inflateWidgetView(WidgetInfo widgetInfo) {
+ View widgetView = null;
+ try {
+ Context appContext = mContext.createPackageContext(
+ widgetInfo.contextPackage, Context.CONTEXT_RESTRICTED);
+ LayoutInflater appInflater = (LayoutInflater)
+ appContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ appInflater = appInflater.cloneInContext(appContext);
+ widgetView = appInflater.inflate(widgetInfo.layoutId, null, false);
+ } catch (PackageManager.NameNotFoundException|RuntimeException e) {
+ Log.w(TAG, "Error creating widget view", e);
+ }
+ return widgetView;
+ }
+
+ private WidgetInfo getWidgetInfoFromService(ComponentName componentName) {
+ PackageManager packageManager = mContext.getPackageManager();
+ // Look for the preview specified in the service meta-data
+ try {
+ Bundle metaData = packageManager.getServiceInfo(
+ componentName, PackageManager.GET_META_DATA).metaData;
+ return getWidgetInfoFromMetaData(componentName.getPackageName(), metaData);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Failed to load preview; " + componentName.flattenToShortString()
+ + " not found", e);
+ }
+ return null;
+ }
+
+ private WidgetInfo getWidgetInfoFromMetaData(String contextPackage,
+ Bundle metaData) {
+ if (metaData == null) {
+ return null;
+ }
+ int layoutId = metaData.getInt(META_DATA_KEYGUARD_LAYOUT);
+ if (layoutId == 0) {
+ return null;
+ }
+ WidgetInfo info = new WidgetInfo();
+ info.contextPackage = contextPackage;
+ info.layoutId = layoutId;
+ return info;
+ }
+
+ private WidgetInfo getWidgetInfo(Intent intent) {
+ PackageManager packageManager = mContext.getPackageManager();
+ int flags = PackageManager.MATCH_DEFAULT_ONLY
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+ final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
+ intent, flags, KeyguardUpdateMonitor.getCurrentUser());
+ if (appList.size() == 0) {
+ return null;
+ }
+ ResolveInfo resolved = packageManager.resolveActivityAsUser(intent,
+ flags | PackageManager.GET_META_DATA,
+ KeyguardUpdateMonitor.getCurrentUser());
+ if (wouldLaunchResolverActivity(resolved, appList)) {
+ return null;
+ }
+ if (resolved == null || resolved.activityInfo == null) {
+ return null;
+ }
+ return getWidgetInfoFromMetaData(resolved.activityInfo.packageName,
+ resolved.activityInfo.metaData);
+ }
+
+ public static boolean wouldLaunchResolverActivity(Context ctx, Intent intent,
+ int currentUserId) {
+ return getTargetActivityInfo(ctx, intent, currentUserId, false /* onlyDirectBootAware */)
+ == null;
+ }
+
+ /**
+ * @param onlyDirectBootAware a boolean indicating whether the matched activity packages must
+ * be direct boot aware when in direct boot mode if false, all
+ * packages are considered a match even if they are not aware.
+ * @return the target activity info of the intent it resolves to a specific package or
+ * {@code null} if it resolved to the resolver activity
+ */
+ public static ActivityInfo getTargetActivityInfo(Context ctx, Intent intent,
+ int currentUserId, boolean onlyDirectBootAware) {
+ PackageManager packageManager = ctx.getPackageManager();
+ int flags = PackageManager.MATCH_DEFAULT_ONLY;
+ if (!onlyDirectBootAware) {
+ flags |= PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+ }
+ final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
+ intent, flags, currentUserId);
+ if (appList.size() == 0) {
+ return null;
+ }
+ ResolveInfo resolved = packageManager.resolveActivityAsUser(intent,
+ flags | PackageManager.GET_META_DATA, currentUserId);
+ if (resolved == null || wouldLaunchResolverActivity(resolved, appList)) {
+ return null;
+ } else {
+ return resolved.activityInfo;
+ }
+ }
+
+ private static boolean wouldLaunchResolverActivity(
+ ResolveInfo resolved, List<ResolveInfo> appList) {
+ // If the list contains the above resolved activity, then it can't be
+ // ResolverActivity itself.
+ for (int i = 0; i < appList.size(); i++) {
+ ResolveInfo tmp = appList.get(i);
+ if (tmp.activityInfo.name.equals(resolved.activityInfo.name)
+ && tmp.activityInfo.packageName.equals(resolved.activityInfo.packageName)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static class WidgetInfo {
+ String contextPackage;
+ int layoutId;
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/RemoteInputView.java b/com/android/systemui/statusbar/policy/RemoteInputView.java
new file mode 100644
index 0000000..37b0de4
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2015 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 android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutManager;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.RemoteInputController;
+import com.android.systemui.statusbar.notification.NotificationViewWrapper;
+import com.android.systemui.statusbar.stack.ScrollContainer;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * Host for the remote input.
+ */
+public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
+
+ private static final String TAG = "RemoteInput";
+
+ // A marker object that let's us easily find views of this class.
+ public static final Object VIEW_TAG = new Object();
+
+ public final Object mToken = new Object();
+
+ private RemoteEditText mEditText;
+ private ImageButton mSendButton;
+ private ProgressBar mProgressBar;
+ private PendingIntent mPendingIntent;
+ private RemoteInput[] mRemoteInputs;
+ private RemoteInput mRemoteInput;
+ private RemoteInputController mController;
+
+ private NotificationData.Entry mEntry;
+
+ private ScrollContainer mScrollContainer;
+ private View mScrollContainerChild;
+ private boolean mRemoved;
+
+ private int mRevealCx;
+ private int mRevealCy;
+ private int mRevealR;
+
+ private boolean mResetting;
+ private NotificationViewWrapper mWrapper;
+
+ public RemoteInputView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mProgressBar = findViewById(R.id.remote_input_progress);
+
+ mSendButton = findViewById(R.id.remote_input_send);
+ mSendButton.setOnClickListener(this);
+
+ mEditText = (RemoteEditText) getChildAt(0);
+ mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ final boolean isSoftImeEvent = event == null
+ && (actionId == EditorInfo.IME_ACTION_DONE
+ || actionId == EditorInfo.IME_ACTION_NEXT
+ || actionId == EditorInfo.IME_ACTION_SEND);
+ final boolean isKeyboardEnterKey = event != null
+ && KeyEvent.isConfirmKey(event.getKeyCode())
+ && event.getAction() == KeyEvent.ACTION_DOWN;
+
+ if (isSoftImeEvent || isKeyboardEnterKey) {
+ if (mEditText.length() > 0) {
+ sendRemoteInput();
+ }
+ // Consume action to prevent IME from closing.
+ return true;
+ }
+ return false;
+ }
+ });
+ mEditText.addTextChangedListener(this);
+ mEditText.setInnerFocusable(false);
+ mEditText.mRemoteInputView = this;
+ }
+
+ private void sendRemoteInput() {
+ Bundle results = new Bundle();
+ results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
+ Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
+ results);
+
+ mEditText.setEnabled(false);
+ mSendButton.setVisibility(INVISIBLE);
+ mProgressBar.setVisibility(VISIBLE);
+ mEntry.remoteInputText = mEditText.getText();
+ mController.addSpinning(mEntry.key, mToken);
+ mController.removeRemoteInput(mEntry, mToken);
+ mEditText.mShowImeOnInputConnection = false;
+ mController.remoteInputSent(mEntry);
+
+ // Tell ShortcutManager that this package has been "activated". ShortcutManager
+ // will reset the throttling for this package.
+ // Strictly speaking, the intent receiver may be different from the notification publisher,
+ // but that's an edge case, and also because we can't always know which package will receive
+ // an intent, so we just reset for the publisher.
+ getContext().getSystemService(ShortcutManager.class).onApplicationActive(
+ mEntry.notification.getPackageName(),
+ mEntry.notification.getUser().getIdentifier());
+
+ MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND,
+ mEntry.notification.getPackageName());
+ try {
+ mPendingIntent.send(mContext, 0, fillInIntent);
+ } catch (PendingIntent.CanceledException e) {
+ Log.i(TAG, "Unable to send remote input result", e);
+ MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_FAIL,
+ mEntry.notification.getPackageName());
+ }
+ }
+
+ public static RemoteInputView inflate(Context context, ViewGroup root,
+ NotificationData.Entry entry,
+ RemoteInputController controller) {
+ RemoteInputView v = (RemoteInputView)
+ LayoutInflater.from(context).inflate(R.layout.remote_input, root, false);
+ v.mController = controller;
+ v.mEntry = entry;
+ v.setTag(VIEW_TAG);
+
+ return v;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mSendButton) {
+ sendRemoteInput();
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ // We never want for a touch to escape to an outer view or one we covered.
+ return true;
+ }
+
+ private void onDefocus(boolean animate) {
+ mController.removeRemoteInput(mEntry, mToken);
+ mEntry.remoteInputText = mEditText.getText();
+
+ // During removal, we get reattached and lose focus. Not hiding in that
+ // case to prevent flicker.
+ if (!mRemoved) {
+ if (animate && mRevealR > 0) {
+ Animator reveal = ViewAnimationUtils.createCircularReveal(
+ this, mRevealCx, mRevealCy, mRevealR, 0);
+ reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+ reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
+ reveal.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setVisibility(INVISIBLE);
+ if (mWrapper != null) {
+ mWrapper.setRemoteInputVisible(false);
+ }
+ }
+ });
+ reveal.start();
+ } else {
+ setVisibility(INVISIBLE);
+ if (mWrapper != null) {
+ mWrapper.setRemoteInputVisible(false);
+ }
+ }
+ }
+ MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE,
+ mEntry.notification.getPackageName());
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mEntry.row.isChangingPosition()) {
+ if (getVisibility() == VISIBLE && mEditText.isFocusable()) {
+ mEditText.requestFocus();
+ }
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mEntry.row.isChangingPosition() || isTemporarilyDetached()) {
+ return;
+ }
+ mController.removeRemoteInput(mEntry, mToken);
+ mController.removeSpinning(mEntry.key, mToken);
+ }
+
+ public void setPendingIntent(PendingIntent pendingIntent) {
+ mPendingIntent = pendingIntent;
+ }
+
+ public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
+ mRemoteInputs = remoteInputs;
+ mRemoteInput = remoteInput;
+ mEditText.setHint(mRemoteInput.getLabel());
+ }
+
+ public void focusAnimated() {
+ if (getVisibility() != VISIBLE) {
+ Animator animator = ViewAnimationUtils.createCircularReveal(
+ this, mRevealCx, mRevealCy, 0, mRevealR);
+ animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ animator.start();
+ }
+ focus();
+ }
+
+ public void focus() {
+ MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN,
+ mEntry.notification.getPackageName());
+
+ setVisibility(VISIBLE);
+ if (mWrapper != null) {
+ mWrapper.setRemoteInputVisible(true);
+ }
+ mController.addRemoteInput(mEntry, mToken);
+ mEditText.setInnerFocusable(true);
+ mEditText.mShowImeOnInputConnection = true;
+ mEditText.setText(mEntry.remoteInputText);
+ mEditText.setSelection(mEditText.getText().length());
+ mEditText.requestFocus();
+ updateSendButton();
+ }
+
+ public void onNotificationUpdateOrReset() {
+ boolean sending = mProgressBar.getVisibility() == VISIBLE;
+
+ if (sending) {
+ // Update came in after we sent the reply, time to reset.
+ reset();
+ }
+
+ if (isActive() && mWrapper != null) {
+ mWrapper.setRemoteInputVisible(true);
+ }
+ }
+
+ private void reset() {
+ mResetting = true;
+
+ mEditText.getText().clear();
+ mEditText.setEnabled(true);
+ mSendButton.setVisibility(VISIBLE);
+ mProgressBar.setVisibility(INVISIBLE);
+ mController.removeSpinning(mEntry.key, mToken);
+ updateSendButton();
+ onDefocus(false /* animate */);
+
+ mResetting = false;
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+ if (mResetting && child == mEditText) {
+ // Suppress text events if it happens during resetting. Ideally this would be
+ // suppressed by the text view not being shown, but that doesn't work here because it
+ // needs to stay visible for the animation.
+ return false;
+ }
+ return super.onRequestSendAccessibilityEvent(child, event);
+ }
+
+ private void updateSendButton() {
+ mSendButton.setEnabled(mEditText.getText().length() != 0);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ updateSendButton();
+ }
+
+ public void close() {
+ mEditText.defocusIfNeeded(false /* animated */);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ findScrollContainer();
+ if (mScrollContainer != null) {
+ mScrollContainer.requestDisallowLongPress();
+ mScrollContainer.requestDisallowDismiss();
+ }
+ }
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ public boolean requestScrollTo() {
+ findScrollContainer();
+ mScrollContainer.lockScrollTo(mScrollContainerChild);
+ return true;
+ }
+
+ private void findScrollContainer() {
+ if (mScrollContainer == null) {
+ mScrollContainerChild = null;
+ ViewParent p = this;
+ while (p != null) {
+ if (mScrollContainerChild == null && p instanceof ExpandableView) {
+ mScrollContainerChild = (View) p;
+ }
+ if (p.getParent() instanceof ScrollContainer) {
+ mScrollContainer = (ScrollContainer) p.getParent();
+ if (mScrollContainerChild == null) {
+ mScrollContainerChild = (View) p;
+ }
+ break;
+ }
+ p = p.getParent();
+ }
+ }
+ }
+
+ public boolean isActive() {
+ return mEditText.isFocused() && mEditText.isEnabled();
+ }
+
+ public void stealFocusFrom(RemoteInputView other) {
+ other.close();
+ setPendingIntent(other.mPendingIntent);
+ setRemoteInput(other.mRemoteInputs, other.mRemoteInput);
+ setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR);
+ focus();
+ }
+
+ /**
+ * Tries to find an action in {@param actions} that matches the current pending intent
+ * of this view and updates its state to that of the found action
+ *
+ * @return true if a matching action was found, false otherwise
+ */
+ public boolean updatePendingIntentFromActions(Notification.Action[] actions) {
+ if (mPendingIntent == null || actions == null) {
+ return false;
+ }
+ Intent current = mPendingIntent.getIntent();
+ if (current == null) {
+ return false;
+ }
+
+ for (Notification.Action a : actions) {
+ RemoteInput[] inputs = a.getRemoteInputs();
+ if (a.actionIntent == null || inputs == null) {
+ continue;
+ }
+ Intent candidate = a.actionIntent.getIntent();
+ if (!current.filterEquals(candidate)) {
+ continue;
+ }
+
+ RemoteInput input = null;
+ for (RemoteInput i : inputs) {
+ if (i.getAllowFreeFormInput()) {
+ input = i;
+ }
+ }
+ if (input == null) {
+ continue;
+ }
+ setPendingIntent(a.actionIntent);
+ setRemoteInput(inputs, input);
+ return true;
+ }
+ return false;
+ }
+
+ public PendingIntent getPendingIntent() {
+ return mPendingIntent;
+ }
+
+ public void setRemoved() {
+ mRemoved = true;
+ }
+
+ public void setRevealParameters(int cx, int cy, int r) {
+ mRevealCx = cx;
+ mRevealCy = cy;
+ mRevealR = r;
+ }
+
+ @Override
+ public void dispatchStartTemporaryDetach() {
+ super.dispatchStartTemporaryDetach();
+ // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and
+ // won't lose IME focus.
+ detachViewFromParent(mEditText);
+ }
+
+ @Override
+ public void dispatchFinishTemporaryDetach() {
+ if (isAttachedToWindow()) {
+ attachViewToParent(mEditText, 0, mEditText.getLayoutParams());
+ } else {
+ removeDetachedView(mEditText, false /* animate */);
+ }
+ super.dispatchFinishTemporaryDetach();
+ }
+
+ public void setWrapper(NotificationViewWrapper wrapper) {
+ mWrapper = wrapper;
+ }
+
+ /**
+ * An EditText that changes appearance based on whether it's focusable and becomes
+ * un-focusable whenever the user navigates away from it or it becomes invisible.
+ */
+ public static class RemoteEditText extends EditText {
+
+ private final Drawable mBackground;
+ private RemoteInputView mRemoteInputView;
+ boolean mShowImeOnInputConnection;
+
+ public RemoteEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mBackground = getBackground();
+ }
+
+ private void defocusIfNeeded(boolean animate) {
+ if (mRemoteInputView != null && mRemoteInputView.mEntry.row.isChangingPosition()
+ || isTemporarilyDetached()) {
+ if (isTemporarilyDetached()) {
+ // We might get reattached but then the other one of HUN / expanded might steal
+ // our focus, so we'll need to save our text here.
+ if (mRemoteInputView != null) {
+ mRemoteInputView.mEntry.remoteInputText = getText();
+ }
+ }
+ return;
+ }
+ if (isFocusable() && isEnabled()) {
+ setInnerFocusable(false);
+ if (mRemoteInputView != null) {
+ mRemoteInputView.onDefocus(animate);
+ }
+ mShowImeOnInputConnection = false;
+ }
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+
+ if (!isShown()) {
+ defocusIfNeeded(false /* animate */);
+ }
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ if (!focused) {
+ defocusIfNeeded(true /* animate */);
+ }
+ }
+
+ @Override
+ public void getFocusedRect(Rect r) {
+ super.getFocusedRect(r);
+ r.top = mScrollY;
+ r.bottom = mScrollY + (mBottom - mTop);
+ }
+
+ @Override
+ public boolean requestRectangleOnScreen(Rect rectangle) {
+ return mRemoteInputView.requestScrollTo();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ // Eat the DOWN event here to prevent any default behavior.
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ defocusIfNeeded(true /* animate */);
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onCheckIsTextEditor() {
+ // Stop being editable while we're being removed. During removal, we get reattached,
+ // and editable views get their spellchecking state re-evaluated which is too costly
+ // during the removal animation.
+ boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved;
+ return !flyingOut && super.onCheckIsTextEditor();
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
+
+ if (mShowImeOnInputConnection && inputConnection != null) {
+ final InputMethodManager imm = InputMethodManager.getInstance();
+ if (imm != null) {
+ // onCreateInputConnection is called by InputMethodManager in the middle of
+ // setting up the connection to the IME; wait with requesting the IME until that
+ // work has completed.
+ post(new Runnable() {
+ @Override
+ public void run() {
+ imm.viewClicked(RemoteEditText.this);
+ imm.showSoftInput(RemoteEditText.this, 0);
+ }
+ });
+ }
+ }
+
+ return inputConnection;
+ }
+
+ @Override
+ public void onCommitCompletion(CompletionInfo text) {
+ clearComposingText();
+ setText(text.getText());
+ setSelection(getText().length());
+ }
+
+ void setInnerFocusable(boolean focusable) {
+ setFocusableInTouchMode(focusable);
+ setFocusable(focusable);
+ setCursorVisible(focusable);
+
+ if (focusable) {
+ requestFocus();
+ setBackground(mBackground);
+ } else {
+ setBackground(null);
+ }
+
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/RotationLockController.java b/com/android/systemui/statusbar/policy/RotationLockController.java
new file mode 100644
index 0000000..722874b
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/RotationLockController.java
@@ -0,0 +1,31 @@
+/*
+ * 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.policy;
+
+import com.android.systemui.statusbar.policy.RotationLockController.RotationLockControllerCallback;
+
+public interface RotationLockController extends Listenable,
+ CallbackController<RotationLockControllerCallback> {
+ int getRotationLockOrientation();
+ boolean isRotationLockAffordanceVisible();
+ boolean isRotationLocked();
+ void setRotationLocked(boolean locked);
+
+ public interface RotationLockControllerCallback {
+ void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
new file mode 100644
index 0000000..4f96496
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2010 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 android.content.Context;
+import android.os.UserHandle;
+
+import com.android.internal.view.RotationPolicy;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** Platform implementation of the rotation lock controller. **/
+public final class RotationLockControllerImpl implements RotationLockController {
+ private final Context mContext;
+ private final CopyOnWriteArrayList<RotationLockControllerCallback> mCallbacks =
+ new CopyOnWriteArrayList<RotationLockControllerCallback>();
+
+ private final RotationPolicy.RotationPolicyListener mRotationPolicyListener =
+ new RotationPolicy.RotationPolicyListener() {
+ @Override
+ public void onChange() {
+ notifyChanged();
+ }
+ };
+
+ public RotationLockControllerImpl(Context context) {
+ mContext = context;
+ setListening(true);
+ }
+
+ public void addCallback(RotationLockControllerCallback callback) {
+ mCallbacks.add(callback);
+ notifyChanged(callback);
+ }
+
+ public void removeCallback(RotationLockControllerCallback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ public int getRotationLockOrientation() {
+ return RotationPolicy.getRotationLockOrientation(mContext);
+ }
+
+ public boolean isRotationLocked() {
+ return RotationPolicy.isRotationLocked(mContext);
+ }
+
+ public void setRotationLocked(boolean locked) {
+ RotationPolicy.setRotationLock(mContext, locked);
+ }
+
+ public boolean isRotationLockAffordanceVisible() {
+ return RotationPolicy.isRotationLockToggleVisible(mContext);
+ }
+
+ @Override
+ public void setListening(boolean listening) {
+ if (listening) {
+ RotationPolicy.registerRotationPolicyListener(mContext, mRotationPolicyListener,
+ UserHandle.USER_ALL);
+ } else {
+ RotationPolicy.unregisterRotationPolicyListener(mContext, mRotationPolicyListener);
+ }
+ }
+
+ private void notifyChanged() {
+ for (RotationLockControllerCallback callback : mCallbacks) {
+ notifyChanged(callback);
+ }
+ }
+
+ private void notifyChanged(RotationLockControllerCallback callback) {
+ callback.onRotationLockStateChanged(RotationPolicy.isRotationLocked(mContext),
+ RotationPolicy.isRotationLockToggleVisible(mContext));
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/ScrollAdapter.java b/com/android/systemui/statusbar/policy/ScrollAdapter.java
new file mode 100644
index 0000000..f35e22d
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ScrollAdapter.java
@@ -0,0 +1,40 @@
+/*
+ * 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.policy;
+
+import android.view.View;
+
+/**
+ * A scroll adapter which can be queried for meta information about the scroll state
+ */
+public interface ScrollAdapter {
+
+ /**
+ * @return Whether the view returned by {@link #getHostView()} is scrolled to the top
+ */
+ public boolean isScrolledToTop();
+
+ /**
+ * @return Whether the view returned by {@link #getHostView()} is scrolled to the bottom
+ */
+ public boolean isScrolledToBottom();
+
+ /**
+ * @return The view in which the scrolling is performed
+ */
+ public View getHostView();
+}
diff --git a/com/android/systemui/statusbar/policy/SecurityController.java b/com/android/systemui/statusbar/policy/SecurityController.java
new file mode 100644
index 0000000..1fb9b69
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/SecurityController.java
@@ -0,0 +1,46 @@
+/*
+ * 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.policy;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.SecurityController.SecurityControllerCallback;
+
+public interface SecurityController extends CallbackController<SecurityControllerCallback>,
+ Dumpable {
+ /** Whether the device has device owner, even if not on this user. */
+ boolean isDeviceManaged();
+ boolean hasProfileOwner();
+ boolean hasWorkProfile();
+ String getDeviceOwnerName();
+ String getProfileOwnerName();
+ CharSequence getDeviceOwnerOrganizationName();
+ CharSequence getWorkProfileOrganizationName();
+ boolean isNetworkLoggingEnabled();
+ boolean isVpnEnabled();
+ boolean isVpnRestricted();
+ /** Whether the VPN app should use branded VPN iconography. */
+ boolean isVpnBranded();
+ String getPrimaryVpnName();
+ String getWorkProfileVpnName();
+ boolean hasCACertInCurrentUser();
+ boolean hasCACertInWorkProfile();
+ void onUserSwitched(int newUserId);
+
+ public interface SecurityControllerCallback {
+ void onStateChanged();
+ }
+
+}
diff --git a/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
new file mode 100644
index 0000000..b22ce18
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -0,0 +1,422 @@
+/*
+ * 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.policy;
+
+import android.app.ActivityManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.IConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.security.KeyChain;
+import android.security.KeyChain.KeyChainConnection;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.net.LegacyVpnInfo;
+import com.android.internal.net.VpnConfig;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.settings.CurrentUserTracker;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class SecurityControllerImpl extends CurrentUserTracker implements SecurityController {
+
+ private static final String TAG = "SecurityController";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final NetworkRequest REQUEST = new NetworkRequest.Builder()
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ .build();
+ private static final int NO_NETWORK = -1;
+
+ private static final String VPN_BRANDED_META_DATA = "com.android.systemui.IS_BRANDED";
+
+ private static final int CA_CERT_LOADING_RETRY_TIME_IN_MS = 30_000;
+
+ private final Context mContext;
+ private final ConnectivityManager mConnectivityManager;
+ private final IConnectivityManager mConnectivityManagerService;
+ private final DevicePolicyManager mDevicePolicyManager;
+ private final PackageManager mPackageManager;
+ private final UserManager mUserManager;
+
+ @GuardedBy("mCallbacks")
+ private final ArrayList<SecurityControllerCallback> mCallbacks = new ArrayList<>();
+
+ private SparseArray<VpnConfig> mCurrentVpns = new SparseArray<>();
+ private int mCurrentUserId;
+ private int mVpnUserId;
+
+ // Key: userId, Value: whether the user has CACerts installed
+ // Needs to be cached here since the query has to be asynchronous
+ private ArrayMap<Integer, Boolean> mHasCACerts = new ArrayMap<Integer, Boolean>();
+
+ public SecurityControllerImpl(Context context) {
+ this(context, null);
+ }
+
+ public SecurityControllerImpl(Context context, SecurityControllerCallback callback) {
+ super(context);
+ mContext = context;
+ mDevicePolicyManager = (DevicePolicyManager)
+ context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+ mConnectivityManager = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mConnectivityManagerService = IConnectivityManager.Stub.asInterface(
+ ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
+ mPackageManager = context.getPackageManager();
+ mUserManager = (UserManager)
+ context.getSystemService(Context.USER_SERVICE);
+
+ addCallback(callback);
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(KeyChain.ACTION_TRUST_STORE_CHANGED);
+ context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null,
+ new Handler(Dependency.get(Dependency.BG_LOOPER)));
+
+ // TODO: re-register network callback on user change.
+ mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback);
+ onUserSwitched(ActivityManager.getCurrentUser());
+ startTracking();
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("SecurityController state:");
+ pw.print(" mCurrentVpns={");
+ for (int i = 0 ; i < mCurrentVpns.size(); i++) {
+ if (i > 0) {
+ pw.print(", ");
+ }
+ pw.print(mCurrentVpns.keyAt(i));
+ pw.print('=');
+ pw.print(mCurrentVpns.valueAt(i).user);
+ }
+ pw.println("}");
+ }
+
+ @Override
+ public boolean isDeviceManaged() {
+ return mDevicePolicyManager.isDeviceManaged();
+ }
+
+ @Override
+ public String getDeviceOwnerName() {
+ return mDevicePolicyManager.getDeviceOwnerNameOnAnyUser();
+ }
+
+ @Override
+ public boolean hasProfileOwner() {
+ return mDevicePolicyManager.getProfileOwnerAsUser(mCurrentUserId) != null;
+ }
+
+ @Override
+ public String getProfileOwnerName() {
+ for (int profileId : mUserManager.getProfileIdsWithDisabled(mCurrentUserId)) {
+ String name = mDevicePolicyManager.getProfileOwnerNameAsUser(profileId);
+ if (name != null) {
+ return name;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public CharSequence getDeviceOwnerOrganizationName() {
+ return mDevicePolicyManager.getDeviceOwnerOrganizationName();
+ }
+
+ @Override
+ public CharSequence getWorkProfileOrganizationName() {
+ final int profileId = getWorkProfileUserId(mCurrentUserId);
+ if (profileId == UserHandle.USER_NULL) return null;
+ return mDevicePolicyManager.getOrganizationNameForUser(profileId);
+ }
+
+ @Override
+ public String getPrimaryVpnName() {
+ VpnConfig cfg = mCurrentVpns.get(mVpnUserId);
+ if (cfg != null) {
+ return getNameForVpnConfig(cfg, new UserHandle(mVpnUserId));
+ } else {
+ return null;
+ }
+ }
+
+ private int getWorkProfileUserId(int userId) {
+ for (final UserInfo userInfo : mUserManager.getProfiles(userId)) {
+ if (userInfo.isManagedProfile()) {
+ return userInfo.id;
+ }
+ }
+ return UserHandle.USER_NULL;
+ }
+
+ @Override
+ public boolean hasWorkProfile() {
+ return getWorkProfileUserId(mCurrentUserId) != UserHandle.USER_NULL;
+ }
+
+ @Override
+ public String getWorkProfileVpnName() {
+ final int profileId = getWorkProfileUserId(mVpnUserId);
+ if (profileId == UserHandle.USER_NULL) return null;
+ VpnConfig cfg = mCurrentVpns.get(profileId);
+ if (cfg != null) {
+ return getNameForVpnConfig(cfg, UserHandle.of(profileId));
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isNetworkLoggingEnabled() {
+ return mDevicePolicyManager.isNetworkLoggingEnabled(null);
+ }
+
+ @Override
+ public boolean isVpnEnabled() {
+ for (int profileId : mUserManager.getProfileIdsWithDisabled(mVpnUserId)) {
+ if (mCurrentVpns.get(profileId) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isVpnRestricted() {
+ UserHandle currentUser = new UserHandle(mCurrentUserId);
+ return mUserManager.getUserInfo(mCurrentUserId).isRestricted()
+ || mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN, currentUser);
+ }
+
+ @Override
+ public boolean isVpnBranded() {
+ VpnConfig cfg = mCurrentVpns.get(mVpnUserId);
+ if (cfg == null) {
+ return false;
+ }
+
+ String packageName = getPackageNameForVpnConfig(cfg);
+ if (packageName == null) {
+ return false;
+ }
+
+ return isVpnPackageBranded(packageName);
+ }
+
+ @Override
+ public boolean hasCACertInCurrentUser() {
+ Boolean hasCACerts = mHasCACerts.get(mCurrentUserId);
+ return hasCACerts != null && hasCACerts.booleanValue();
+ }
+
+ @Override
+ public boolean hasCACertInWorkProfile() {
+ int userId = getWorkProfileUserId(mCurrentUserId);
+ if (userId == UserHandle.USER_NULL) return false;
+ Boolean hasCACerts = mHasCACerts.get(userId);
+ return hasCACerts != null && hasCACerts.booleanValue();
+ }
+
+ @Override
+ public void removeCallback(SecurityControllerCallback callback) {
+ synchronized (mCallbacks) {
+ if (callback == null) return;
+ if (DEBUG) Log.d(TAG, "removeCallback " + callback);
+ mCallbacks.remove(callback);
+ }
+ }
+
+ @Override
+ public void addCallback(SecurityControllerCallback callback) {
+ synchronized (mCallbacks) {
+ if (callback == null || mCallbacks.contains(callback)) return;
+ if (DEBUG) Log.d(TAG, "addCallback " + callback);
+ mCallbacks.add(callback);
+ }
+ }
+
+ @Override
+ public void onUserSwitched(int newUserId) {
+ mCurrentUserId = newUserId;
+ final UserInfo newUserInfo = mUserManager.getUserInfo(newUserId);
+ if (newUserInfo.isRestricted()) {
+ // VPN for a restricted profile is routed through its owner user
+ mVpnUserId = newUserInfo.restrictedProfileParentId;
+ } else {
+ mVpnUserId = mCurrentUserId;
+ }
+ refreshCACerts();
+ fireCallbacks();
+ }
+
+ private void refreshCACerts() {
+ new CACertLoader().execute(mCurrentUserId);
+ int workProfileId = getWorkProfileUserId(mCurrentUserId);
+ if (workProfileId != UserHandle.USER_NULL) new CACertLoader().execute(workProfileId);
+ }
+
+ private String getNameForVpnConfig(VpnConfig cfg, UserHandle user) {
+ if (cfg.legacy) {
+ return mContext.getString(R.string.legacy_vpn_name);
+ }
+ // The package name for an active VPN is stored in the 'user' field of its VpnConfig
+ final String vpnPackage = cfg.user;
+ try {
+ Context userContext = mContext.createPackageContextAsUser(mContext.getPackageName(),
+ 0 /* flags */, user);
+ return VpnConfig.getVpnLabel(userContext, vpnPackage).toString();
+ } catch (NameNotFoundException nnfe) {
+ Log.e(TAG, "Package " + vpnPackage + " is not present", nnfe);
+ return null;
+ }
+ }
+
+ private void fireCallbacks() {
+ synchronized (mCallbacks) {
+ for (SecurityControllerCallback callback : mCallbacks) {
+ callback.onStateChanged();
+ }
+ }
+ }
+
+ private void updateState() {
+ // Find all users with an active VPN
+ SparseArray<VpnConfig> vpns = new SparseArray<>();
+ try {
+ for (UserInfo user : mUserManager.getUsers()) {
+ VpnConfig cfg = mConnectivityManagerService.getVpnConfig(user.id);
+ if (cfg == null) {
+ continue;
+ } else if (cfg.legacy) {
+ // Legacy VPNs should do nothing if the network is disconnected. Third-party
+ // VPN warnings need to continue as traffic can still go to the app.
+ LegacyVpnInfo legacyVpn = mConnectivityManagerService.getLegacyVpnInfo(user.id);
+ if (legacyVpn == null || legacyVpn.state != LegacyVpnInfo.STATE_CONNECTED) {
+ continue;
+ }
+ }
+ vpns.put(user.id, cfg);
+ }
+ } catch (RemoteException rme) {
+ // Roll back to previous state
+ Log.e(TAG, "Unable to list active VPNs", rme);
+ return;
+ }
+ mCurrentVpns = vpns;
+ }
+
+ private String getPackageNameForVpnConfig(VpnConfig cfg) {
+ if (cfg.legacy) {
+ return null;
+ }
+ return cfg.user;
+ }
+
+ private boolean isVpnPackageBranded(String packageName) {
+ boolean isBranded;
+ try {
+ ApplicationInfo info = mPackageManager.getApplicationInfo(packageName,
+ PackageManager.GET_META_DATA);
+ if (info == null || info.metaData == null || !info.isSystemApp()) {
+ return false;
+ }
+ isBranded = info.metaData.getBoolean(VPN_BRANDED_META_DATA, false);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ return isBranded;
+ }
+
+ private final NetworkCallback mNetworkCallback = new NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ if (DEBUG) Log.d(TAG, "onAvailable " + network.netId);
+ updateState();
+ fireCallbacks();
+ };
+
+ // TODO Find another way to receive VPN lost. This may be delayed depending on
+ // how long the VPN connection is held on to.
+ @Override
+ public void onLost(Network network) {
+ if (DEBUG) Log.d(TAG, "onLost " + network.netId);
+ updateState();
+ fireCallbacks();
+ };
+ };
+
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override public void onReceive(Context context, Intent intent) {
+ if (KeyChain.ACTION_TRUST_STORE_CHANGED.equals(intent.getAction())) {
+ refreshCACerts();
+ }
+ }
+ };
+
+ protected class CACertLoader extends AsyncTask<Integer, Void, Pair<Integer, Boolean> > {
+
+ @Override
+ protected Pair<Integer, Boolean> doInBackground(Integer... userId) {
+ try (KeyChainConnection conn = KeyChain.bindAsUser(mContext,
+ UserHandle.of(userId[0]))) {
+ boolean hasCACerts = !(conn.getService().getUserCaAliases().getList().isEmpty());
+ return new Pair<Integer, Boolean>(userId[0], hasCACerts);
+ } catch (RemoteException | InterruptedException | AssertionError e) {
+ Log.i(TAG, e.getMessage());
+ new Handler(Dependency.get(Dependency.BG_LOOPER)).postDelayed(
+ () -> new CACertLoader().execute(userId[0]),
+ CA_CERT_LOADING_RETRY_TIME_IN_MS);
+ return new Pair<Integer, Boolean>(userId[0], null);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Pair<Integer, Boolean> result) {
+ if (DEBUG) Log.d(TAG, "onPostExecute " + result);
+ if (result.second != null) {
+ mHasCACerts.put(result.first, result.second);
+ fireCallbacks();
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/SignalController.java b/com/android/systemui/statusbar/policy/SignalController.java
new file mode 100644
index 0000000..91c208d
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/SignalController.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2015 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 android.content.Context;
+import android.text.format.DateFormat;
+import android.util.Log;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.io.PrintWriter;
+import java.util.BitSet;
+
+import static com.android.systemui.statusbar.policy.NetworkControllerImpl.TAG;
+
+
+/**
+ * Common base class for handling signal for both wifi and mobile data.
+ */
+public abstract class SignalController<T extends SignalController.State,
+ I extends SignalController.IconGroup> {
+ // 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 = 64;
+
+ protected static final boolean DEBUG = NetworkControllerImpl.DEBUG;
+ protected static final boolean CHATTY = NetworkControllerImpl.CHATTY;
+
+ 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 NetworkControllerImpl mNetworkController;
+
+ private final CallbackHandler mCallbackHandler;
+
+ // 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, CallbackHandler callbackHandler,
+ NetworkControllerImpl networkController) {
+ mTag = TAG + "." + tag;
+ mNetworkController = networkController;
+ mTransportType = type;
+ mContext = context;
+ mCallbackHandler = callbackHandler;
+ mCurrentState = cleanState();
+ mLastState = cleanState();
+ if (RECORD_HISTORY) {
+ mHistory = new State[HISTORY_SIZE];
+ for (int i = 0; i < HISTORY_SIZE; i++) {
+ mHistory[i] = cleanState();
+ }
+ }
+ }
+
+ public T getState() {
+ return mCurrentState;
+ }
+
+ public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) {
+ mCurrentState.inetCondition = validatedTransports.get(mTransportType) ? 1 : 0;
+ notifyListenersIfNecessary();
+ }
+
+ /**
+ * 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.
+ */
+ public void resetLastState() {
+ mCurrentState.copyFrom(mLastState);
+ }
+
+ /**
+ * 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;
+ }
+
+ 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 {
+ return getIcons().mQsNullState;
+ }
+ }
+
+ /**
+ * 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 {
+ return getIcons().mSbNullState;
+ }
+ }
+
+ /**
+ * 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 notifyListenersIfNecessary() {
+ if (isDirty()) {
+ saveLastState();
+ notifyListeners();
+ }
+ }
+
+ /**
+ * Returns the resource if resId is not 0, and an empty string otherwise.
+ */
+ protected String getStringIfExists(int resId) {
+ return resId != 0 ? mContext.getString(resId) : "";
+ }
+
+ protected I getIcons() {
+ return (I) mCurrentState.iconGroup;
+ }
+
+ /**
+ * 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);
+ }
+
+ 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)]);
+ }
+ }
+ }
+
+ public final void notifyListeners() {
+ notifyListeners(mCallbackHandler);
+ }
+
+ /**
+ * 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(SignalCallback callback);
+
+ /**
+ * 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;
+ }
+
+ @Override
+ public String toString() {
+ return "IconGroup(" + mName + ")";
+ }
+ }
+
+ 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;
+ }
+
+ @Override
+ public String toString() {
+ if (time != 0) {
+ StringBuilder builder = new StringBuilder();
+ toString(builder);
+ return builder.toString();
+ } else {
+ return "Empty " + getClass().getSimpleName();
+ }
+ }
+
+ 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;
+ }
+ 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;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/SplitClockView.java b/com/android/systemui/statusbar/policy/SplitClockView.java
new file mode 100644
index 0000000..9f61574
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/SplitClockView.java
@@ -0,0 +1,145 @@
+/*
+ * 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.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;
+import android.widget.TextClock;
+
+import com.android.systemui.R;
+
+/**
+ * Container for a clock which has two separate views for the clock itself and AM/PM indicator. This
+ * is used to scale the clock independently of AM/PM.
+ */
+public class SplitClockView extends LinearLayout {
+
+ private TextClock mTimeView;
+ private TextClock mAmPmView;
+
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ 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_CONFIGURATION_CHANGED.equals(action)
+ || Intent.ACTION_USER_SWITCHED.equals(action)) {
+ updatePatterns();
+ }
+ }
+ };
+
+ public SplitClockView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mTimeView = findViewById(R.id.time_view);
+ mAmPmView = findViewById(R.id.am_pm_view);
+ mTimeView.setShowCurrentUserTime(true);
+ mAmPmView.setShowCurrentUserTime(true);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ filter.addAction(Intent.ACTION_LOCALE_CHANGED);
+ filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ getContext().registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, null, null);
+
+ updatePatterns();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getContext().unregisterReceiver(mIntentReceiver);
+ }
+
+ private void updatePatterns() {
+ String formatString = DateFormat.getTimeFormatString(getContext(),
+ ActivityManager.getCurrentUser());
+ int index = getAmPmPartEndIndex(formatString);
+ String timeString;
+ String amPmString;
+ if (index == -1) {
+ timeString = formatString;
+ amPmString = "";
+ } else {
+ timeString = formatString.substring(0, index);
+ amPmString = formatString.substring(index);
+ }
+ mTimeView.setFormat12Hour(timeString);
+ mTimeView.setFormat24Hour(timeString);
+ mTimeView.setContentDescriptionFormat12Hour(formatString);
+ mTimeView.setContentDescriptionFormat24Hour(formatString);
+ mAmPmView.setFormat12Hour(amPmString);
+ mAmPmView.setFormat24Hour(amPmString);
+ }
+
+ /**
+ * @return the index where the AM/PM part starts at the end in {@code formatString} including
+ * leading white spaces or {@code -1} if no AM/PM part is found or {@code formatString}
+ * doesn't end with AM/PM part
+ */
+ private static int getAmPmPartEndIndex(String formatString) {
+ boolean hasAmPm = false;
+ int length = formatString.length();
+ for (int i = length - 1; i >= 0; i--) {
+ char c = formatString.charAt(i);
+ boolean isAmPm = c == 'a';
+ boolean isWhitespace = Character.isWhitespace(c);
+ if (isAmPm) {
+ hasAmPm = true;
+ }
+ if (isAmPm || isWhitespace) {
+ continue;
+ }
+ if (i == length - 1) {
+
+ // First character was not AM/PM and not whitespace, so it's not ending with AM/PM.
+ return -1;
+ } else {
+
+ // If we have AM/PM at all, return last index, or -1 to indicate that it's not
+ // ending with AM/PM.
+ return hasAmPm ? i + 1 : -1;
+ }
+ }
+
+ // Only AM/PM and whitespaces? The whole string is AM/PM. Else: Only whitespaces in the
+ // string.
+ return hasAmPm ? 0 : -1;
+ }
+
+}
diff --git a/com/android/systemui/statusbar/policy/TelephonyIcons.java b/com/android/systemui/statusbar/policy/TelephonyIcons.java
new file mode 100644
index 0000000..aaa0568
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/TelephonyIcons.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.MobileSignalController.MobileIconGroup;
+
+class TelephonyIcons {
+ //***** Data connection icons
+
+ static final int QS_DATA_G = R.drawable.ic_qs_signal_g;
+ static final int QS_DATA_3G = R.drawable.ic_qs_signal_3g;
+ static final int QS_DATA_E = R.drawable.ic_qs_signal_e;
+ static final int QS_DATA_H = R.drawable.ic_qs_signal_h;
+ static final int QS_DATA_1X = R.drawable.ic_qs_signal_1x;
+ static final int QS_DATA_4G = R.drawable.ic_qs_signal_4g;
+ static final int QS_DATA_4G_PLUS = R.drawable.ic_qs_signal_4g_plus;
+ static final int QS_DATA_LTE = R.drawable.ic_qs_signal_lte;
+ static final int QS_DATA_LTE_PLUS = R.drawable.ic_qs_signal_lte_plus;
+
+ static final int FLIGHT_MODE_ICON = R.drawable.stat_sys_airplane_mode;
+
+ static final int ICON_LTE = R.drawable.stat_sys_data_fully_connected_lte;
+ static final int ICON_LTE_PLUS = R.drawable.stat_sys_data_fully_connected_lte_plus;
+ 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_4G_PLUS = R.drawable.stat_sys_data_fully_connected_4g_plus;
+ static final int ICON_1X = R.drawable.stat_sys_data_fully_connected_1x;
+
+ static final int ICON_DATA_DISABLED = R.drawable.stat_sys_data_disabled;
+
+ static final int QS_ICON_DATA_DISABLED = R.drawable.ic_qs_data_disabled;
+
+ static final MobileIconGroup CARRIER_NETWORK_CHANGE = new MobileIconGroup(
+ "CARRIER_NETWORK_CHANGE",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+ R.string.accessibility_carrier_network_change_mode,
+ 0,
+ false,
+ 0
+ );
+
+ static final MobileIconGroup THREE_G = new MobileIconGroup(
+ "3G",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+ R.string.accessibility_data_connection_3g,
+ TelephonyIcons.ICON_3G,
+ true,
+ TelephonyIcons.QS_DATA_3G
+ );
+
+ static final MobileIconGroup WFC = new MobileIconGroup(
+ "WFC",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+ 0, 0, false, 0
+ );
+
+ static final MobileIconGroup UNKNOWN = new MobileIconGroup(
+ "Unknown",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+ 0, 0, false, 0
+ );
+
+ static final MobileIconGroup E = new MobileIconGroup(
+ "E",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ 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",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ 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",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ 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",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ 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",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+ R.string.accessibility_data_connection_4g,
+ TelephonyIcons.ICON_4G,
+ true,
+ TelephonyIcons.QS_DATA_4G
+ );
+
+ static final MobileIconGroup FOUR_G_PLUS = new MobileIconGroup(
+ "4G+",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0,0,
+ 0,
+ 0,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+ R.string.accessibility_data_connection_4g_plus,
+ TelephonyIcons.ICON_4G_PLUS,
+ true,
+ TelephonyIcons.QS_DATA_4G_PLUS
+ );
+
+ static final MobileIconGroup LTE = new MobileIconGroup(
+ "LTE",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+ R.string.accessibility_data_connection_lte,
+ TelephonyIcons.ICON_LTE,
+ true,
+ TelephonyIcons.QS_DATA_LTE
+ );
+
+ static final MobileIconGroup LTE_PLUS = new MobileIconGroup(
+ "LTE+",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+ R.string.accessibility_data_connection_lte_plus,
+ TelephonyIcons.ICON_LTE_PLUS,
+ true,
+ TelephonyIcons.QS_DATA_LTE_PLUS
+ );
+
+ static final MobileIconGroup DATA_DISABLED = new MobileIconGroup(
+ "DataDisabled",
+ null,
+ null,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+ 0, 0,
+ 0,
+ 0,
+ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+ R.string.accessibility_cell_data_off,
+ TelephonyIcons.ICON_DATA_DISABLED,
+ false,
+ TelephonyIcons.QS_ICON_DATA_DISABLED
+ );
+}
+
diff --git a/com/android/systemui/statusbar/policy/UserInfoController.java b/com/android/systemui/statusbar/policy/UserInfoController.java
new file mode 100644
index 0000000..1e23a20
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/UserInfoController.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.drawable.Drawable;
+
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
+
+public interface UserInfoController extends CallbackController<OnUserInfoChangedListener> {
+
+ void reloadUserInfo();
+
+ public interface OnUserInfoChangedListener {
+ public void onUserInfoChanged(String name, Drawable picture, String userAccount);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
new file mode 100644
index 0000000..527addf
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
@@ -0,0 +1,230 @@
+/*
+ * 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.policy;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+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.Drawable;
+import android.os.AsyncTask;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.android.internal.util.UserIcons;
+import com.android.settingslib.drawable.UserIconDrawable;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
+
+import java.util.ArrayList;
+
+public class UserInfoControllerImpl implements UserInfoController {
+
+ private static final String TAG = "UserInfoController";
+
+ private final Context mContext;
+ private final ArrayList<OnUserInfoChangedListener> mCallbacks =
+ new ArrayList<OnUserInfoChangedListener>();
+ private AsyncTask<Void, Void, UserInfoQueryResult> mUserInfoTask;
+
+ private String mUserName;
+ private Drawable mUserDrawable;
+ private String mUserAccount;
+
+ public UserInfoControllerImpl(Context context) {
+ mContext = context;
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ mContext.registerReceiver(mReceiver, filter);
+
+ IntentFilter profileFilter = new IntentFilter();
+ profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED);
+ profileFilter.addAction(Intent.ACTION_USER_INFO_CHANGED);
+ mContext.registerReceiverAsUser(mProfileReceiver, UserHandle.ALL, profileFilter,
+ null, null);
+ }
+
+ public void addCallback(OnUserInfoChangedListener callback) {
+ mCallbacks.add(callback);
+ callback.onUserInfoChanged(mUserName, mUserDrawable, mUserAccount);
+ }
+
+ public void removeCallback(OnUserInfoChangedListener callback) {
+ mCallbacks.remove(callback);
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+ reloadUserInfo();
+ }
+ }
+ };
+
+ private final BroadcastReceiver mProfileReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (ContactsContract.Intents.ACTION_PROFILE_CHANGED.equals(action) ||
+ Intent.ACTION_USER_INFO_CHANGED.equals(action)) {
+ try {
+ final int currentUser = ActivityManager.getService().getCurrentUser().id;
+ final int changedUser =
+ intent.getIntExtra(Intent.EXTRA_USER_HANDLE, getSendingUserId());
+ if (changedUser == currentUser) {
+ reloadUserInfo();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't get current user id for profile change", e);
+ }
+ }
+ }
+ };
+
+ public void reloadUserInfo() {
+ if (mUserInfoTask != null) {
+ mUserInfoTask.cancel(false);
+ mUserInfoTask = null;
+ }
+ queryForUserInformation();
+ }
+
+ private void queryForUserInformation() {
+ Context currentUserContext;
+ UserInfo userInfo;
+ try {
+ userInfo = ActivityManager.getService().getCurrentUser();
+ currentUserContext = mContext.createPackageContextAsUser("android", 0,
+ new UserHandle(userInfo.id));
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Couldn't create user context", e);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't get user info", e);
+ throw new RuntimeException(e);
+ }
+ final int userId = userInfo.id;
+ final boolean isGuest = userInfo.isGuest();
+ final String userName = userInfo.name;
+ final boolean lightIcon = mContext.getThemeResId() != R.style.Theme_SystemUI_Light;
+
+ 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, UserInfoQueryResult>() {
+
+ @Override
+ protected UserInfoQueryResult doInBackground(Void... params) {
+ final UserManager um = UserManager.get(mContext);
+
+ // Fall back to the UserManager nickname if we can't read the name from the local
+ // profile below.
+ String name = userName;
+ Drawable avatar = null;
+ Bitmap rawAvatar = um.getUserIcon(userId);
+ if (rawAvatar != null) {
+ avatar = new UserIconDrawable(avatarSize)
+ .setIcon(rawAvatar).setBadgeIfManagedUser(mContext, userId).bake();
+ } else {
+ avatar = UserIcons.getDefaultUserIcon(isGuest? UserHandle.USER_NULL : userId,
+ lightIcon);
+ }
+
+ // If it's a single-user device, get the profile name, since the nickname is not
+ // usually valid
+ if (um.getUsers().size() <= 1) {
+ // 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);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ name = cursor.getString(cursor.getColumnIndex(
+ ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+ String userAccount = um.getUserAccount(userId);
+ return new UserInfoQueryResult(name, avatar, userAccount);
+ }
+
+ @Override
+ protected void onPostExecute(UserInfoQueryResult result) {
+ mUserName = result.getName();
+ mUserDrawable = result.getAvatar();
+ mUserAccount = result.getUserAccount();
+ mUserInfoTask = null;
+ notifyChanged();
+ }
+ };
+ mUserInfoTask.execute();
+ }
+
+ private void notifyChanged() {
+ for (OnUserInfoChangedListener listener : mCallbacks) {
+ listener.onUserInfoChanged(mUserName, mUserDrawable, mUserAccount);
+ }
+ }
+
+ public void onDensityOrFontScaleChanged() {
+ reloadUserInfo();
+ }
+
+ private static class UserInfoQueryResult {
+ private String mName;
+ private Drawable mAvatar;
+ private String mUserAccount;
+
+ public UserInfoQueryResult(String name, Drawable avatar, String userAccount) {
+ mName = name;
+ mAvatar = avatar;
+ mUserAccount = userAccount;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public Drawable getAvatar() {
+ return mAvatar;
+ }
+
+ public String getUserAccount() {
+ return mUserAccount;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/UserSwitcherController.java b/com/android/systemui/statusbar/policy/UserSwitcherController.java
new file mode 100644
index 0000000..700c01a
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -0,0 +1,966 @@
+/*
+ * 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.policy;
+
+import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
+
+import android.R.attr;
+import android.app.ActivityManager;
+import android.app.Dialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.util.UserIcons;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
+import com.android.systemui.GuestResumeSessionReceiver;
+import com.android.systemui.Prefs;
+import com.android.systemui.Prefs.Key;
+import com.android.systemui.R;
+import com.android.systemui.SystemUI;
+import com.android.systemui.SystemUISecondaryUserService;
+import com.android.systemui.plugins.qs.DetailAdapter;
+import com.android.systemui.qs.tiles.UserDetailView;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.util.NotificationChannels;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Keeps a list of all users on the device for user switching.
+ */
+public class UserSwitcherController {
+
+ private static final String TAG = "UserSwitcherController";
+ private static final boolean DEBUG = false;
+ private static final String SIMPLE_USER_SWITCHER_GLOBAL_SETTING =
+ "lockscreenSimpleUserSwitcher";
+ private static final String ACTION_REMOVE_GUEST = "com.android.systemui.REMOVE_GUEST";
+ private static final String ACTION_LOGOUT_USER = "com.android.systemui.LOGOUT_USER";
+ private static final int PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000;
+
+ private static final String TAG_REMOVE_GUEST = "remove_guest";
+ private static final String TAG_LOGOUT_USER = "logout_user";
+
+ private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+
+ protected final Context mContext;
+ protected final UserManager mUserManager;
+ private final ArrayList<WeakReference<BaseUserAdapter>> mAdapters = new ArrayList<>();
+ private final GuestResumeSessionReceiver mGuestResumeSessionReceiver
+ = new GuestResumeSessionReceiver();
+ private final KeyguardMonitor mKeyguardMonitor;
+ protected final Handler mHandler;
+ private final ActivityStarter mActivityStarter;
+
+ private ArrayList<UserRecord> mUsers = new ArrayList<>();
+ private Dialog mExitGuestDialog;
+ private Dialog mAddUserDialog;
+ private int mLastNonGuestUser = UserHandle.USER_SYSTEM;
+ private boolean mResumeUserOnGuestLogout = true;
+ private boolean mSimpleUserSwitcher;
+ private boolean mAddUsersWhenLocked;
+ private boolean mPauseRefreshUsers;
+ private int mSecondaryUser = UserHandle.USER_NULL;
+ private Intent mSecondaryUserServiceIntent;
+ private SparseBooleanArray mForcePictureLoadForUserId = new SparseBooleanArray(2);
+
+ public UserSwitcherController(Context context, KeyguardMonitor keyguardMonitor,
+ Handler handler, ActivityStarter activityStarter) {
+ mContext = context;
+ mGuestResumeSessionReceiver.register(context);
+ mKeyguardMonitor = keyguardMonitor;
+ mHandler = handler;
+ mActivityStarter = activityStarter;
+ mUserManager = UserManager.get(context);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_USER_ADDED);
+ filter.addAction(Intent.ACTION_USER_REMOVED);
+ filter.addAction(Intent.ACTION_USER_INFO_CHANGED);
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ filter.addAction(Intent.ACTION_USER_STOPPED);
+ filter.addAction(Intent.ACTION_USER_UNLOCKED);
+ mContext.registerReceiverAsUser(mReceiver, UserHandle.SYSTEM, filter,
+ null /* permission */, null /* scheduler */);
+
+ mSecondaryUserServiceIntent = new Intent(context, SystemUISecondaryUserService.class);
+
+ filter = new IntentFilter();
+ filter.addAction(ACTION_REMOVE_GUEST);
+ filter.addAction(ACTION_LOGOUT_USER);
+ mContext.registerReceiverAsUser(mReceiver, UserHandle.SYSTEM, filter,
+ PERMISSION_SELF, null /* scheduler */);
+
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(SIMPLE_USER_SWITCHER_GLOBAL_SETTING), true,
+ mSettingsObserver);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.ADD_USERS_WHEN_LOCKED), true,
+ mSettingsObserver);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(
+ Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED),
+ true, mSettingsObserver);
+ // Fetch initial values.
+ mSettingsObserver.onChange(false);
+
+ keyguardMonitor.addCallback(mCallback);
+ listenForCallState();
+
+ refreshUsers(UserHandle.USER_NULL);
+ }
+
+ /**
+ * Refreshes users from UserManager.
+ *
+ * The pictures are only loaded if they have not been loaded yet.
+ *
+ * @param forcePictureLoadForId forces the picture of the given user to be reloaded.
+ */
+ @SuppressWarnings("unchecked")
+ private void refreshUsers(int forcePictureLoadForId) {
+ if (DEBUG) Log.d(TAG, "refreshUsers(forcePictureLoadForId=" + forcePictureLoadForId+")");
+ if (forcePictureLoadForId != UserHandle.USER_NULL) {
+ mForcePictureLoadForUserId.put(forcePictureLoadForId, true);
+ }
+
+ if (mPauseRefreshUsers) {
+ return;
+ }
+
+ boolean forceAllUsers = mForcePictureLoadForUserId.get(UserHandle.USER_ALL);
+ SparseArray<Bitmap> bitmaps = new SparseArray<>(mUsers.size());
+ final int N = mUsers.size();
+ for (int i = 0; i < N; i++) {
+ UserRecord r = mUsers.get(i);
+ if (r == null || r.picture == null || r.info == null || forceAllUsers
+ || mForcePictureLoadForUserId.get(r.info.id)) {
+ continue;
+ }
+ bitmaps.put(r.info.id, r.picture);
+ }
+ mForcePictureLoadForUserId.clear();
+
+ final boolean addUsersWhenLocked = mAddUsersWhenLocked;
+ new AsyncTask<SparseArray<Bitmap>, Void, ArrayList<UserRecord>>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ protected ArrayList<UserRecord> doInBackground(SparseArray<Bitmap>... params) {
+ final SparseArray<Bitmap> bitmaps = params[0];
+ List<UserInfo> infos = mUserManager.getUsers(true);
+ if (infos == null) {
+ return null;
+ }
+ ArrayList<UserRecord> records = new ArrayList<>(infos.size());
+ int currentId = ActivityManager.getCurrentUser();
+ boolean canSwitchUsers = mUserManager.canSwitchUsers();
+ UserInfo currentUserInfo = null;
+ UserRecord guestRecord = null;
+
+ for (UserInfo info : infos) {
+ boolean isCurrent = currentId == info.id;
+ if (isCurrent) {
+ currentUserInfo = info;
+ }
+ boolean switchToEnabled = canSwitchUsers || isCurrent;
+ if (info.isEnabled()) {
+ if (info.isGuest()) {
+ // Tapping guest icon triggers remove and a user switch therefore
+ // the icon shouldn't be enabled even if the user is current
+ guestRecord = new UserRecord(info, null /* picture */,
+ true /* isGuest */, isCurrent, false /* isAddUser */,
+ false /* isRestricted */, canSwitchUsers);
+ } else if (info.supportsSwitchToByUser()) {
+ Bitmap picture = bitmaps.get(info.id);
+ if (picture == null) {
+ picture = mUserManager.getUserIcon(info.id);
+
+ if (picture != null) {
+ int avatarSize = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.max_avatar_size);
+ picture = Bitmap.createScaledBitmap(
+ picture, avatarSize, avatarSize, true);
+ }
+ }
+ int index = isCurrent ? 0 : records.size();
+ records.add(index, new UserRecord(info, picture, false /* isGuest */,
+ isCurrent, false /* isAddUser */, false /* isRestricted */,
+ switchToEnabled));
+ }
+ }
+ }
+ if (records.size() > 1 || guestRecord != null) {
+ Prefs.putBoolean(mContext, Key.SEEN_MULTI_USER, true);
+ }
+
+ boolean systemCanCreateUsers = !mUserManager.hasBaseUserRestriction(
+ UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM);
+ boolean currentUserCanCreateUsers = currentUserInfo != null
+ && (currentUserInfo.isAdmin()
+ || currentUserInfo.id == UserHandle.USER_SYSTEM)
+ && systemCanCreateUsers;
+ boolean anyoneCanCreateUsers = systemCanCreateUsers && addUsersWhenLocked;
+ boolean canCreateGuest = (currentUserCanCreateUsers || anyoneCanCreateUsers)
+ && guestRecord == null;
+ boolean canCreateUser = (currentUserCanCreateUsers || anyoneCanCreateUsers)
+ && mUserManager.canAddMoreUsers();
+ boolean createIsRestricted = !addUsersWhenLocked;
+
+ if (!mSimpleUserSwitcher) {
+ if (guestRecord == null) {
+ if (canCreateGuest) {
+ guestRecord = new UserRecord(null /* info */, null /* picture */,
+ true /* isGuest */, false /* isCurrent */,
+ false /* isAddUser */, createIsRestricted, canSwitchUsers);
+ checkIfAddUserDisallowedByAdminOnly(guestRecord);
+ records.add(guestRecord);
+ }
+ } else {
+ int index = guestRecord.isCurrent ? 0 : records.size();
+ records.add(index, guestRecord);
+ }
+ }
+
+ if (!mSimpleUserSwitcher && canCreateUser) {
+ UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
+ false /* isGuest */, false /* isCurrent */, true /* isAddUser */,
+ createIsRestricted, canSwitchUsers);
+ checkIfAddUserDisallowedByAdminOnly(addUserRecord);
+ records.add(addUserRecord);
+ }
+
+ return records;
+ }
+
+ @Override
+ protected void onPostExecute(ArrayList<UserRecord> userRecords) {
+ if (userRecords != null) {
+ mUsers = userRecords;
+ notifyAdapters();
+ }
+ }
+ }.execute((SparseArray) bitmaps);
+ }
+
+ private void pauseRefreshUsers() {
+ if (!mPauseRefreshUsers) {
+ mHandler.postDelayed(mUnpauseRefreshUsers, PAUSE_REFRESH_USERS_TIMEOUT_MS);
+ mPauseRefreshUsers = true;
+ }
+ }
+
+ private void notifyAdapters() {
+ for (int i = mAdapters.size() - 1; i >= 0; i--) {
+ BaseUserAdapter adapter = mAdapters.get(i).get();
+ if (adapter != null) {
+ adapter.notifyDataSetChanged();
+ } else {
+ mAdapters.remove(i);
+ }
+ }
+ }
+
+ public boolean isSimpleUserSwitcher() {
+ return mSimpleUserSwitcher;
+ }
+
+ public boolean useFullscreenUserSwitcher() {
+ // Use adb to override:
+ // adb shell settings put system enable_fullscreen_user_switcher 0 # Turn it off.
+ // adb shell settings put system enable_fullscreen_user_switcher 1 # Turn it on.
+ // Restart SystemUI or adb reboot.
+ final int DEFAULT = -1;
+ final int overrideUseFullscreenUserSwitcher =
+ Settings.System.getInt(mContext.getContentResolver(),
+ "enable_fullscreen_user_switcher", DEFAULT);
+ if (overrideUseFullscreenUserSwitcher != DEFAULT) {
+ return overrideUseFullscreenUserSwitcher != 0;
+ }
+ // Otherwise default to the build setting.
+ return mContext.getResources().getBoolean(R.bool.config_enableFullscreenUserSwitcher);
+ }
+
+ public void setResumeUserOnGuestLogout(boolean resume) {
+ mResumeUserOnGuestLogout = resume;
+ }
+
+ public void logoutCurrentUser() {
+ int currentUser = ActivityManager.getCurrentUser();
+ if (currentUser != UserHandle.USER_SYSTEM) {
+ pauseRefreshUsers();
+ ActivityManager.logoutCurrentUser();
+ }
+ }
+
+ public void removeUserId(int userId) {
+ if (userId == UserHandle.USER_SYSTEM) {
+ Log.w(TAG, "User " + userId + " could not removed.");
+ return;
+ }
+ if (ActivityManager.getCurrentUser() == userId) {
+ switchToUserId(UserHandle.USER_SYSTEM);
+ }
+ if (mUserManager.removeUser(userId)) {
+ refreshUsers(UserHandle.USER_NULL);
+ }
+ }
+
+ public void switchTo(UserRecord record) {
+ int id;
+ if (record.isGuest && record.info == null) {
+ // No guest user. Create one.
+ UserInfo guest = mUserManager.createGuest(
+ mContext, mContext.getString(R.string.guest_nickname));
+ if (guest == null) {
+ // Couldn't create guest, most likely because there already exists one, we just
+ // haven't reloaded the user list yet.
+ return;
+ }
+ id = guest.id;
+ } else if (record.isAddUser) {
+ showAddUserDialog();
+ return;
+ } else {
+ id = record.info.id;
+ }
+
+ int currUserId = ActivityManager.getCurrentUser();
+ if (currUserId == id) {
+ if (record.isGuest) {
+ showExitGuestDialog(id);
+ }
+ return;
+ }
+
+ if (UserManager.isGuestUserEphemeral()) {
+ // If switching from guest, we want to bring up the guest exit dialog instead of switching
+ UserInfo currUserInfo = mUserManager.getUserInfo(currUserId);
+ if (currUserInfo != null && currUserInfo.isGuest()) {
+ showExitGuestDialog(currUserId, record.resolveId());
+ return;
+ }
+ }
+
+ switchToUserId(id);
+ }
+
+ public void switchTo(int userId) {
+ final int count = mUsers.size();
+ for (int i = 0; i < count; ++i) {
+ UserRecord record = mUsers.get(i);
+ if (record.info != null && record.info.id == userId) {
+ switchTo(record);
+ return;
+ }
+ }
+
+ Log.e(TAG, "Couldn't switch to user, id=" + userId);
+ }
+
+ public int getSwitchableUserCount() {
+ int count = 0;
+ final int N = mUsers.size();
+ for (int i = 0; i < N; ++i) {
+ UserRecord record = mUsers.get(i);
+ if (record.info != null && record.info.supportsSwitchTo()) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ protected void switchToUserId(int id) {
+ try {
+ pauseRefreshUsers();
+ ActivityManager.getService().switchUser(id);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't switch user.", e);
+ }
+ }
+
+ private void showExitGuestDialog(int id) {
+ int newId = UserHandle.USER_SYSTEM;
+ if (mResumeUserOnGuestLogout && mLastNonGuestUser != UserHandle.USER_SYSTEM) {
+ UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser);
+ if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) {
+ newId = info.id;
+ }
+ }
+ showExitGuestDialog(id, newId);
+ }
+
+ protected void showExitGuestDialog(int id, int targetId) {
+ if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
+ mExitGuestDialog.cancel();
+ }
+ mExitGuestDialog = new ExitGuestDialog(mContext, id, targetId);
+ mExitGuestDialog.show();
+ }
+
+ public void showAddUserDialog() {
+ if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
+ mAddUserDialog.cancel();
+ }
+ mAddUserDialog = new AddUserDialog(mContext);
+ mAddUserDialog.show();
+ }
+
+ protected void exitGuest(int id, int targetId) {
+ switchToUserId(targetId);
+ mUserManager.removeUser(id);
+ }
+
+ private void listenForCallState() {
+ TelephonyManager.from(mContext).listen(mPhoneStateListener,
+ PhoneStateListener.LISTEN_CALL_STATE);
+ }
+
+ private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
+ private int mCallState;
+
+ @Override
+ public void onCallStateChanged(int state, String incomingNumber) {
+ if (mCallState == state) return;
+ if (DEBUG) Log.v(TAG, "Call state changed: " + state);
+ mCallState = state;
+ int currentUserId = ActivityManager.getCurrentUser();
+ UserInfo userInfo = mUserManager.getUserInfo(currentUserId);
+ if (userInfo != null && userInfo.isGuest()) {
+ showGuestNotification(currentUserId);
+ }
+ refreshUsers(UserHandle.USER_NULL);
+ }
+ };
+
+ private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) {
+ Log.v(TAG, "Broadcast: a=" + intent.getAction()
+ + " user=" + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1));
+ }
+
+ boolean unpauseRefreshUsers = false;
+ int forcePictureLoadForId = UserHandle.USER_NULL;
+
+ if (ACTION_REMOVE_GUEST.equals(intent.getAction())) {
+ int currentUser = ActivityManager.getCurrentUser();
+ UserInfo userInfo = mUserManager.getUserInfo(currentUser);
+ if (userInfo != null && userInfo.isGuest()) {
+ showExitGuestDialog(currentUser);
+ }
+ return;
+ } else if (ACTION_LOGOUT_USER.equals(intent.getAction())) {
+ logoutCurrentUser();
+ } else if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
+ if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
+ mExitGuestDialog.cancel();
+ mExitGuestDialog = null;
+ }
+
+ final int currentId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ final UserInfo userInfo = mUserManager.getUserInfo(currentId);
+ final int N = mUsers.size();
+ for (int i = 0; i < N; i++) {
+ UserRecord record = mUsers.get(i);
+ if (record.info == null) continue;
+ boolean shouldBeCurrent = record.info.id == currentId;
+ if (record.isCurrent != shouldBeCurrent) {
+ mUsers.set(i, record.copyWithIsCurrent(shouldBeCurrent));
+ }
+ if (shouldBeCurrent && !record.isGuest) {
+ mLastNonGuestUser = record.info.id;
+ }
+ if ((userInfo == null || !userInfo.isAdmin()) && record.isRestricted) {
+ // Immediately remove restricted records in case the AsyncTask is too slow.
+ mUsers.remove(i);
+ i--;
+ }
+ }
+ notifyAdapters();
+
+ // Disconnect from the old secondary user's service
+ if (mSecondaryUser != UserHandle.USER_NULL) {
+ context.stopServiceAsUser(mSecondaryUserServiceIntent,
+ UserHandle.of(mSecondaryUser));
+ mSecondaryUser = UserHandle.USER_NULL;
+ }
+ // Connect to the new secondary user's service (purely to ensure that a persistent
+ // SystemUI application is created for that user)
+ if (userInfo != null && userInfo.id != UserHandle.USER_SYSTEM) {
+ context.startServiceAsUser(mSecondaryUserServiceIntent,
+ UserHandle.of(userInfo.id));
+ mSecondaryUser = userInfo.id;
+ }
+
+ if (UserManager.isSplitSystemUser() && userInfo != null && !userInfo.isGuest()
+ && userInfo.id != UserHandle.USER_SYSTEM) {
+ showLogoutNotification(currentId);
+ }
+ if (userInfo != null && userInfo.isGuest()) {
+ showGuestNotification(currentId);
+ }
+ unpauseRefreshUsers = true;
+ } else if (Intent.ACTION_USER_INFO_CHANGED.equals(intent.getAction())) {
+ forcePictureLoadForId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
+ UserHandle.USER_NULL);
+ } else if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
+ // Unlocking the system user may require a refresh
+ int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+ if (userId != UserHandle.USER_SYSTEM) {
+ return;
+ }
+ }
+ refreshUsers(forcePictureLoadForId);
+ if (unpauseRefreshUsers) {
+ mUnpauseRefreshUsers.run();
+ }
+ }
+
+ private void showLogoutNotification(int userId) {
+ PendingIntent logoutPI = PendingIntent.getBroadcastAsUser(mContext,
+ 0, new Intent(ACTION_LOGOUT_USER), 0, UserHandle.SYSTEM);
+ Notification.Builder builder =
+ new Notification.Builder(mContext, NotificationChannels.GENERAL)
+ .setVisibility(Notification.VISIBILITY_SECRET)
+ .setSmallIcon(R.drawable.ic_person)
+ .setContentTitle(mContext.getString(
+ R.string.user_logout_notification_title))
+ .setContentText(mContext.getString(
+ R.string.user_logout_notification_text))
+ .setContentIntent(logoutPI)
+ .setOngoing(true)
+ .setShowWhen(false)
+ .addAction(R.drawable.ic_delete,
+ mContext.getString(R.string.user_logout_notification_action),
+ logoutPI);
+ SystemUI.overrideNotificationAppName(mContext, builder);
+ NotificationManager.from(mContext).notifyAsUser(TAG_LOGOUT_USER,
+ SystemMessage.NOTE_LOGOUT_USER, builder.build(), new UserHandle(userId));
+ }
+ };
+
+ private void showGuestNotification(int guestUserId) {
+ boolean canSwitchUsers = mUserManager.canSwitchUsers();
+ // Disable 'Remove guest' action if cannot switch users right now
+ PendingIntent removeGuestPI = canSwitchUsers ? PendingIntent.getBroadcastAsUser(mContext,
+ 0, new Intent(ACTION_REMOVE_GUEST), 0, UserHandle.SYSTEM) : null;
+
+ Notification.Builder builder =
+ new Notification.Builder(mContext, NotificationChannels.GENERAL)
+ .setVisibility(Notification.VISIBILITY_SECRET)
+ .setSmallIcon(R.drawable.ic_person)
+ .setContentTitle(mContext.getString(R.string.guest_notification_title))
+ .setContentText(mContext.getString(R.string.guest_notification_text))
+ .setContentIntent(removeGuestPI)
+ .setShowWhen(false)
+ .addAction(R.drawable.ic_delete,
+ mContext.getString(R.string.guest_notification_remove_action),
+ removeGuestPI);
+ SystemUI.overrideNotificationAppName(mContext, builder);
+ NotificationManager.from(mContext).notifyAsUser(TAG_REMOVE_GUEST,
+ SystemMessage.NOTE_REMOVE_GUEST, builder.build(), new UserHandle(guestUserId));
+ }
+
+ private final Runnable mUnpauseRefreshUsers = new Runnable() {
+ @Override
+ public void run() {
+ mHandler.removeCallbacks(this);
+ mPauseRefreshUsers = false;
+ refreshUsers(UserHandle.USER_NULL);
+ }
+ };
+
+ private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
+ public void onChange(boolean selfChange) {
+ mSimpleUserSwitcher = Settings.Global.getInt(mContext.getContentResolver(),
+ SIMPLE_USER_SWITCHER_GLOBAL_SETTING, 0) != 0;
+ mAddUsersWhenLocked = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0;
+ refreshUsers(UserHandle.USER_NULL);
+ };
+ };
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("UserSwitcherController state:");
+ pw.println(" mLastNonGuestUser=" + mLastNonGuestUser);
+ pw.print(" mUsers.size="); pw.println(mUsers.size());
+ for (int i = 0; i < mUsers.size(); i++) {
+ final UserRecord u = mUsers.get(i);
+ pw.print(" "); pw.println(u.toString());
+ }
+ }
+
+ public String getCurrentUserName(Context context) {
+ if (mUsers.isEmpty()) return null;
+ UserRecord item = mUsers.get(0);
+ if (item == null || item.info == null) return null;
+ if (item.isGuest) return context.getString(R.string.guest_nickname);
+ return item.info.name;
+ }
+
+ public void onDensityOrFontScaleChanged() {
+ refreshUsers(UserHandle.USER_ALL);
+ }
+
+ @VisibleForTesting
+ public void addAdapter(WeakReference<BaseUserAdapter> adapter) {
+ mAdapters.add(adapter);
+ }
+
+ @VisibleForTesting
+ public ArrayList<UserRecord> getUsers() {
+ return mUsers;
+ }
+
+ public static abstract class BaseUserAdapter extends BaseAdapter {
+
+ final UserSwitcherController mController;
+ private final KeyguardMonitor mKeyguardMonitor;
+
+ protected BaseUserAdapter(UserSwitcherController controller) {
+ mController = controller;
+ mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
+ controller.addAdapter(new WeakReference<>(this));
+ }
+
+ public int getUserCount() {
+ boolean secureKeyguardShowing = mKeyguardMonitor.isShowing()
+ && mKeyguardMonitor.isSecure()
+ && !mKeyguardMonitor.canSkipBouncer();
+ if (!secureKeyguardShowing) {
+ return mController.getUsers().size();
+ }
+ // The lock screen is secure and showing. Filter out restricted records.
+ final int N = mController.getUsers().size();
+ int count = 0;
+ for (int i = 0; i < N; i++) {
+ if (mController.getUsers().get(i).isGuest) continue;
+ if (mController.getUsers().get(i).isRestricted) {
+ break;
+ } else {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public int getCount() {
+ boolean secureKeyguardShowing = mKeyguardMonitor.isShowing()
+ && mKeyguardMonitor.isSecure()
+ && !mKeyguardMonitor.canSkipBouncer();
+ if (!secureKeyguardShowing) {
+ return mController.getUsers().size();
+ }
+ // The lock screen is secure and showing. Filter out restricted records.
+ final int N = mController.getUsers().size();
+ int count = 0;
+ for (int i = 0; i < N; i++) {
+ if (mController.getUsers().get(i).isRestricted) {
+ break;
+ } else {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public UserRecord getItem(int position) {
+ return mController.getUsers().get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public void switchTo(UserRecord record) {
+ mController.switchTo(record);
+ }
+
+ public String getName(Context context, UserRecord item) {
+ if (item.isGuest) {
+ if (item.isCurrent) {
+ return context.getString(R.string.guest_exit_guest);
+ } else {
+ return context.getString(
+ item.info == null ? R.string.guest_new_guest : R.string.guest_nickname);
+ }
+ } else if (item.isAddUser) {
+ return context.getString(R.string.user_add_user);
+ } else {
+ return item.info.name;
+ }
+ }
+
+ public Drawable getDrawable(Context context, UserRecord item) {
+ if (item.isAddUser) {
+ return context.getDrawable(R.drawable.ic_add_circle_qs);
+ }
+ Drawable icon = UserIcons.getDefaultUserIcon(item.resolveId(), /* light= */ false);
+ if (item.isGuest) {
+ icon.setColorFilter(Utils.getColorAttr(context, android.R.attr.colorForeground),
+ Mode.SRC_IN);
+ }
+ return icon;
+ }
+
+ public void refresh() {
+ mController.refreshUsers(UserHandle.USER_NULL);
+ }
+ }
+
+ private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
+ EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext,
+ UserManager.DISALLOW_ADD_USER, ActivityManager.getCurrentUser());
+ if (admin != null && !RestrictedLockUtils.hasBaseUserRestriction(mContext,
+ UserManager.DISALLOW_ADD_USER, ActivityManager.getCurrentUser())) {
+ record.isDisabledByAdmin = true;
+ record.enforcedAdmin = admin;
+ } else {
+ record.isDisabledByAdmin = false;
+ record.enforcedAdmin = null;
+ }
+ }
+
+ public void startActivity(Intent intent) {
+ mActivityStarter.startActivity(intent, true);
+ }
+
+ public static final class UserRecord {
+ public final UserInfo info;
+ public final Bitmap picture;
+ public final boolean isGuest;
+ public final boolean isCurrent;
+ public final boolean isAddUser;
+ /** If true, the record is only visible to the owner and only when unlocked. */
+ public final boolean isRestricted;
+ public boolean isDisabledByAdmin;
+ public EnforcedAdmin enforcedAdmin;
+ public boolean isSwitchToEnabled;
+
+ public UserRecord(UserInfo info, Bitmap picture, boolean isGuest, boolean isCurrent,
+ boolean isAddUser, boolean isRestricted, boolean isSwitchToEnabled) {
+ this.info = info;
+ this.picture = picture;
+ this.isGuest = isGuest;
+ this.isCurrent = isCurrent;
+ this.isAddUser = isAddUser;
+ this.isRestricted = isRestricted;
+ this.isSwitchToEnabled = isSwitchToEnabled;
+ }
+
+ public UserRecord copyWithIsCurrent(boolean _isCurrent) {
+ return new UserRecord(info, picture, isGuest, _isCurrent, isAddUser, isRestricted,
+ isSwitchToEnabled);
+ }
+
+ public int resolveId() {
+ if (isGuest || info == null) {
+ return UserHandle.USER_NULL;
+ }
+ return info.id;
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("UserRecord(");
+ if (info != null) {
+ sb.append("name=\"").append(info.name).append("\" id=").append(info.id);
+ } else {
+ if (isGuest) {
+ sb.append("<add guest placeholder>");
+ } else if (isAddUser) {
+ sb.append("<add user placeholder>");
+ }
+ }
+ if (isGuest) sb.append(" <isGuest>");
+ if (isAddUser) sb.append(" <isAddUser>");
+ if (isCurrent) sb.append(" <isCurrent>");
+ if (picture != null) sb.append(" <hasPicture>");
+ if (isRestricted) sb.append(" <isRestricted>");
+ if (isDisabledByAdmin) {
+ sb.append(" <isDisabledByAdmin>");
+ sb.append(" enforcedAdmin=").append(enforcedAdmin);
+ }
+ if (isSwitchToEnabled) {
+ sb.append(" <isSwitchToEnabled>");
+ }
+ sb.append(')');
+ return sb.toString();
+ }
+ }
+
+ public final DetailAdapter userDetailAdapter = new DetailAdapter() {
+ private final Intent USER_SETTINGS_INTENT = new Intent(Settings.ACTION_USER_SETTINGS);
+
+ @Override
+ public CharSequence getTitle() {
+ return mContext.getString(R.string.quick_settings_user_title);
+ }
+
+ @Override
+ public View createDetailView(Context context, View convertView, ViewGroup parent) {
+ UserDetailView v;
+ if (!(convertView instanceof UserDetailView)) {
+ v = UserDetailView.inflate(context, parent, false);
+ v.createAndSetAdapter(UserSwitcherController.this);
+ } else {
+ v = (UserDetailView) convertView;
+ }
+ v.refreshAdapter();
+ return v;
+ }
+
+ @Override
+ public Intent getSettingsIntent() {
+ return USER_SETTINGS_INTENT;
+ }
+
+ @Override
+ public Boolean getToggleState() {
+ return null;
+ }
+
+ @Override
+ public void setToggleState(boolean state) {
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return MetricsEvent.QS_USERDETAIL;
+ }
+ };
+
+ private final KeyguardMonitor.Callback mCallback = new KeyguardMonitor.Callback() {
+ @Override
+ public void onKeyguardShowingChanged() {
+
+ // When Keyguard is going away, we don't need to update our items immediately which
+ // helps making the transition faster.
+ if (!mKeyguardMonitor.isShowing()) {
+ mHandler.post(UserSwitcherController.this::notifyAdapters);
+ } else {
+ notifyAdapters();
+ }
+ }
+ };
+
+ private final class ExitGuestDialog extends SystemUIDialog implements
+ DialogInterface.OnClickListener {
+
+ private final int mGuestId;
+ private final int mTargetId;
+
+ public ExitGuestDialog(Context context, int guestId, int targetId) {
+ super(context);
+ setTitle(R.string.guest_exit_guest_dialog_title);
+ setMessage(context.getString(R.string.guest_exit_guest_dialog_message));
+ setButton(DialogInterface.BUTTON_NEGATIVE,
+ context.getString(android.R.string.cancel), this);
+ setButton(DialogInterface.BUTTON_POSITIVE,
+ context.getString(R.string.guest_exit_guest_dialog_remove), this);
+ setCanceledOnTouchOutside(false);
+ mGuestId = guestId;
+ mTargetId = targetId;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == BUTTON_NEGATIVE) {
+ cancel();
+ } else {
+ dismiss();
+ exitGuest(mGuestId, mTargetId);
+ }
+ }
+ }
+
+ private final class AddUserDialog extends SystemUIDialog implements
+ DialogInterface.OnClickListener {
+
+ public AddUserDialog(Context context) {
+ super(context);
+ setTitle(R.string.user_add_user_title);
+ setMessage(context.getString(R.string.user_add_user_message_short));
+ setButton(DialogInterface.BUTTON_NEGATIVE,
+ context.getString(android.R.string.cancel), this);
+ setButton(DialogInterface.BUTTON_POSITIVE,
+ context.getString(android.R.string.ok), this);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == BUTTON_NEGATIVE) {
+ cancel();
+ } else {
+ dismiss();
+ if (ActivityManager.isUserAMonkey()) {
+ return;
+ }
+ UserInfo user = mUserManager.createUser(
+ mContext.getString(R.string.user_new_user_name), 0 /* flags */);
+ if (user == null) {
+ // Couldn't create user, most likely because there are too many, but we haven't
+ // been able to reload the list yet.
+ return;
+ }
+ int id = user.id;
+ Bitmap icon = UserIcons.convertToBitmap(UserIcons.getDefaultUserIcon(
+ id, /* light= */ false));
+ mUserManager.setUserIcon(id, icon);
+ switchToUserId(id);
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/WifiIcons.java b/com/android/systemui/statusbar/policy/WifiIcons.java
new file mode 100644
index 0000000..374408d
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/WifiIcons.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import com.android.systemui.R;
+
+public class WifiIcons {
+ static final int[][] WIFI_SIGNAL_STRENGTH = {
+ { R.drawable.stat_sys_wifi_signal_0,
+ R.drawable.stat_sys_wifi_signal_1,
+ R.drawable.stat_sys_wifi_signal_2,
+ R.drawable.stat_sys_wifi_signal_3,
+ R.drawable.stat_sys_wifi_signal_4 },
+ { R.drawable.stat_sys_wifi_signal_0_fully,
+ R.drawable.stat_sys_wifi_signal_1_fully,
+ R.drawable.stat_sys_wifi_signal_2_fully,
+ R.drawable.stat_sys_wifi_signal_3_fully,
+ R.drawable.stat_sys_wifi_signal_4_fully }
+ };
+
+ public static final int[][] QS_WIFI_SIGNAL_STRENGTH = {
+ { R.drawable.ic_qs_wifi_0,
+ R.drawable.ic_qs_wifi_1,
+ R.drawable.ic_qs_wifi_2,
+ R.drawable.ic_qs_wifi_3,
+ R.drawable.ic_qs_wifi_4 },
+ { R.drawable.ic_qs_wifi_full_0,
+ R.drawable.ic_qs_wifi_full_1,
+ R.drawable.ic_qs_wifi_full_2,
+ R.drawable.ic_qs_wifi_full_3,
+ 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/com/android/systemui/statusbar/policy/WifiSignalController.java b/com/android/systemui/statusbar/policy/WifiSignalController.java
new file mode 100644
index 0000000..2819624
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/WifiSignalController.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2015 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 android.content.Context;
+import android.content.Intent;
+import android.net.NetworkCapabilities;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.AsyncChannel;
+import com.android.settingslib.wifi.WifiStatusTracker;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.util.Objects;
+
+
+public class WifiSignalController extends
+ SignalController<WifiSignalController.WifiState, SignalController.IconGroup> {
+ private final WifiManager mWifiManager;
+ private final AsyncChannel mWifiChannel;
+ private final boolean mHasMobileData;
+ private final WifiStatusTracker mWifiTracker;
+
+ public WifiSignalController(Context context, boolean hasMobileData,
+ CallbackHandler callbackHandler, NetworkControllerImpl networkController) {
+ super("WifiSignalController", context, NetworkCapabilities.TRANSPORT_WIFI,
+ callbackHandler, networkController);
+ mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ mWifiTracker = new WifiStatusTracker(mWifiManager);
+ mHasMobileData = hasMobileData;
+ Handler handler = new WifiHandler(Looper.getMainLooper());
+ mWifiChannel = new AsyncChannel();
+ Messenger wifiMessenger = mWifiManager.getWifiServiceMessenger();
+ if (wifiMessenger != null) {
+ mWifiChannel.connect(context, handler, wifiMessenger);
+ }
+ // 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
+ );
+
+ }
+
+ @Override
+ protected WifiState cleanState() {
+ return new WifiState();
+ }
+
+ @Override
+ public void notifyListeners(SignalCallback callback) {
+ // 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());
+ if (mCurrentState.inetCondition == 0) {
+ contentDescription +=
+ ("," + mContext.getString(R.string.accessibility_quick_settings_no_internet));
+ }
+
+ IconState statusIcon = new IconState(wifiVisible, getCurrentIconId(), contentDescription);
+ IconState qsIcon = new IconState(mCurrentState.connected, getQsCurrentIconId(),
+ contentDescription);
+ callback.setWifiIndicators(mCurrentState.enabled, statusIcon, qsIcon,
+ ssidPresent && mCurrentState.activityIn, ssidPresent && mCurrentState.activityOut,
+ wifiDesc, mCurrentState.isTransient);
+ }
+
+ /**
+ * Extract wifi state directly from broadcasts about changes in wifi state.
+ */
+ public void handleBroadcast(Intent intent) {
+ mWifiTracker.handleBroadcast(intent);
+ mCurrentState.enabled = mWifiTracker.enabled;
+ mCurrentState.connected = mWifiTracker.connected;
+ mCurrentState.ssid = mWifiTracker.ssid;
+ mCurrentState.rssi = mWifiTracker.rssi;
+ mCurrentState.level = mWifiTracker.level;
+ notifyListenersIfNecessary();
+ }
+
+ @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();
+ }
+
+ /**
+ * Handler to receive the data activity on wifi.
+ */
+ private class WifiHandler extends Handler {
+ WifiHandler(Looper looper) {
+ super(looper);
+ }
+
+ @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;
+ }
+ }
+ }
+
+ static class WifiState extends SignalController.State {
+ String ssid;
+ boolean isTransient;
+
+ @Override
+ public void copyFrom(State s) {
+ super.copyFrom(s);
+ WifiState state = (WifiState) s;
+ ssid = state.ssid;
+ isTransient = state.isTransient;
+ }
+
+ @Override
+ protected void toString(StringBuilder builder) {
+ super.toString(builder);
+ builder.append(',').append("ssid=").append(ssid);
+ builder.append(',').append("isTransient=").append(isTransient);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return super.equals(o)
+ && Objects.equals(((WifiState) o).ssid, ssid)
+ && (((WifiState) o).isTransient == isTransient);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/ZenModeController.java b/com/android/systemui/statusbar/policy/ZenModeController.java
new file mode 100644
index 0000000..8777aa6
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ZenModeController.java
@@ -0,0 +1,49 @@
+/*
+ * 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.policy;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ZenRule;
+
+import com.android.systemui.statusbar.policy.ZenModeController.Callback;
+
+public interface ZenModeController extends CallbackController<Callback> {
+ void setZen(int zen, Uri conditionId, String reason);
+ int getZen();
+ ZenRule getManualRule();
+ ZenModeConfig getConfig();
+ long getNextAlarm();
+ boolean isZenAvailable();
+ ComponentName getEffectsSuppressor();
+ boolean isCountdownConditionSupported();
+ int getCurrentUser();
+ boolean isVolumeRestricted();
+
+ public static interface Callback {
+ default void onZenChanged(int zen) {}
+ default void onConditionsChanged(Condition[] conditions) {}
+ default void onNextAlarmChanged() {}
+ default void onZenAvailableChanged(boolean available) {}
+ default void onEffectsSupressorChanged() {}
+ default void onManualRuleChanged(ZenRule rule) {}
+ default void onConfigChanged(ZenModeConfig config) {}
+ }
+
+}
\ No newline at end of file
diff --git a/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
new file mode 100644
index 0000000..0fd2445
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -0,0 +1,282 @@
+/*
+ * 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.policy;
+
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings.Global;
+import android.provider.Settings.Secure;
+import android.service.notification.Condition;
+import android.service.notification.IConditionListener;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ZenRule;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.systemui.qs.GlobalSetting;
+import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.util.Utils;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.Objects;
+
+/** Platform implementation of the zen mode controller. **/
+public class ZenModeControllerImpl extends CurrentUserTracker implements ZenModeController {
+ private static final String TAG = "ZenModeController";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+ private final Context mContext;
+ private final GlobalSetting mModeSetting;
+ private final GlobalSetting mConfigSetting;
+ private final NotificationManager mNoMan;
+ private final LinkedHashMap<Uri, Condition> mConditions = new LinkedHashMap<Uri, Condition>();
+ private final AlarmManager mAlarmManager;
+ private final SetupObserver mSetupObserver;
+ private final UserManager mUserManager;
+
+ private int mUserId;
+ private boolean mRequesting;
+ private boolean mRegistered;
+ private ZenModeConfig mConfig;
+
+ public ZenModeControllerImpl(Context context, Handler handler) {
+ super(context);
+ mContext = context;
+ mModeSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE) {
+ @Override
+ protected void handleValueChanged(int value) {
+ fireZenChanged(value);
+ }
+ };
+ mConfigSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE_CONFIG_ETAG) {
+ @Override
+ protected void handleValueChanged(int value) {
+ updateZenModeConfig();
+ }
+ };
+ mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ mConfig = mNoMan.getZenModeConfig();
+ mModeSetting.setListening(true);
+ mConfigSetting.setListening(true);
+ mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ mSetupObserver = new SetupObserver(handler);
+ mSetupObserver.register();
+ mUserManager = context.getSystemService(UserManager.class);
+ startTracking();
+ }
+
+ @Override
+ public boolean isVolumeRestricted() {
+ return mUserManager.hasUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME,
+ new UserHandle(mUserId));
+ }
+
+ @Override
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ @Override
+ public int getZen() {
+ return mModeSetting.getValue();
+ }
+
+ @Override
+ public void setZen(int zen, Uri conditionId, String reason) {
+ mNoMan.setZenMode(zen, conditionId, reason);
+ }
+
+ @Override
+ public boolean isZenAvailable() {
+ return mSetupObserver.isDeviceProvisioned() && mSetupObserver.isUserSetup();
+ }
+
+ @Override
+ public ZenRule getManualRule() {
+ return mConfig == null ? null : mConfig.manualRule;
+ }
+
+ @Override
+ public ZenModeConfig getConfig() {
+ return mConfig;
+ }
+
+ @Override
+ public long getNextAlarm() {
+ final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(mUserId);
+ return info != null ? info.getTriggerTime() : 0;
+ }
+
+ @Override
+ public void onUserSwitched(int userId) {
+ mUserId = userId;
+ if (mRegistered) {
+ mContext.unregisterReceiver(mReceiver);
+ }
+ 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();
+ }
+
+ @Override
+ public ComponentName getEffectsSuppressor() {
+ return NotificationManager.from(mContext).getEffectsSuppressor();
+ }
+
+ @Override
+ public boolean isCountdownConditionSupported() {
+ return NotificationManager.from(mContext)
+ .isSystemConditionProviderEnabled(ZenModeConfig.COUNTDOWN_PATH);
+ }
+
+ @Override
+ public int getCurrentUser() {
+ return ActivityManager.getCurrentUser();
+ }
+
+ private void fireNextAlarmChanged() {
+ Utils.safeForeach(mCallbacks, c -> c.onNextAlarmChanged());
+ }
+
+ private void fireEffectsSuppressorChanged() {
+ Utils.safeForeach(mCallbacks, c -> c.onEffectsSupressorChanged());
+ }
+
+ private void fireZenChanged(int zen) {
+ Utils.safeForeach(mCallbacks, c -> c.onZenChanged(zen));
+ }
+
+ private void fireZenAvailableChanged(boolean available) {
+ Utils.safeForeach(mCallbacks, c -> c.onZenAvailableChanged(available));
+ }
+
+ private void fireConditionsChanged(Condition[] conditions) {
+ Utils.safeForeach(mCallbacks, c -> c.onConditionsChanged(conditions));
+ }
+
+ private void fireManualRuleChanged(ZenRule rule) {
+ Utils.safeForeach(mCallbacks, c -> c.onManualRuleChanged(rule));
+ }
+
+ @VisibleForTesting
+ protected void fireConfigChanged(ZenModeConfig config) {
+ Utils.safeForeach(mCallbacks, c -> c.onConfigChanged(config));
+ }
+
+ private void updateConditions(Condition[] conditions) {
+ if (conditions == null || conditions.length == 0) return;
+ for (Condition c : conditions) {
+ if ((c.flags & Condition.FLAG_RELEVANT_NOW) == 0) continue;
+ mConditions.put(c.id, c);
+ }
+ fireConditionsChanged(
+ mConditions.values().toArray(new Condition[mConditions.values().size()]));
+ }
+
+ private void updateZenModeConfig() {
+ final ZenModeConfig config = mNoMan.getZenModeConfig();
+ if (Objects.equals(config, mConfig)) return;
+ final ZenRule oldRule = mConfig != null ? mConfig.manualRule : null;
+ mConfig = config;
+ fireConfigChanged(config);
+ final ZenRule newRule = config != null ? config.manualRule : null;
+ if (Objects.equals(oldRule, newRule)) return;
+ fireManualRuleChanged(newRule);
+ }
+
+ private final IConditionListener mListener = new IConditionListener.Stub() {
+ @Override
+ public void onConditionsReceived(Condition[] conditions) {
+ if (DEBUG) Slog.d(TAG, "onConditionsReceived "
+ + (conditions == null ? 0 : conditions.length) + " mRequesting=" + mRequesting);
+ if (!mRequesting) return;
+ updateConditions(conditions);
+ }
+ };
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equals(intent.getAction())) {
+ fireNextAlarmChanged();
+ }
+ if (NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED.equals(intent.getAction())) {
+ fireEffectsSuppressorChanged();
+ }
+ }
+ };
+
+ private final class SetupObserver extends ContentObserver {
+ private final ContentResolver mResolver;
+
+ private boolean mRegistered;
+
+ public SetupObserver(Handler handler) {
+ super(handler);
+ mResolver = mContext.getContentResolver();
+ }
+
+ public boolean isUserSetup() {
+ return Secure.getIntForUser(mResolver, Secure.USER_SETUP_COMPLETE, 0, mUserId) != 0;
+ }
+
+ public boolean isDeviceProvisioned() {
+ return Global.getInt(mResolver, Global.DEVICE_PROVISIONED, 0) != 0;
+ }
+
+ public void register() {
+ if (mRegistered) {
+ mResolver.unregisterContentObserver(this);
+ }
+ mResolver.registerContentObserver(
+ Global.getUriFor(Global.DEVICE_PROVISIONED), false, this);
+ mResolver.registerContentObserver(
+ Secure.getUriFor(Secure.USER_SETUP_COMPLETE), false, this, mUserId);
+ fireZenAvailableChanged(isZenAvailable());
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (Global.getUriFor(Global.DEVICE_PROVISIONED).equals(uri)
+ || Secure.getUriFor(Secure.USER_SETUP_COMPLETE).equals(uri)) {
+ fireZenAvailableChanged(isZenAvailable());
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/AmbientState.java b/com/android/systemui/statusbar/stack/AmbientState.java
new file mode 100644
index 0000000..4d8da44
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/AmbientState.java
@@ -0,0 +1,381 @@
+/*
+ * 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.stack;
+
+import android.content.Context;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ActivatableNotificationView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * A global state to track all input states for the algorithm.
+ */
+public class AmbientState {
+ private ArrayList<View> mDraggedViews = new ArrayList<View>();
+ private int mScrollY;
+ private boolean mDimmed;
+ private ActivatableNotificationView mActivatedChild;
+ private float mOverScrollTopAmount;
+ private float mOverScrollBottomAmount;
+ private int mSpeedBumpIndex = -1;
+ private boolean mDark;
+ private boolean mHideSensitive;
+ private HeadsUpManager mHeadsUpManager;
+ private float mStackTranslation;
+ private int mLayoutHeight;
+ private int mTopPadding;
+ private boolean mShadeExpanded;
+ private float mMaxHeadsUpTranslation;
+ private boolean mDismissAllInProgress;
+ private int mLayoutMinHeight;
+ private NotificationShelf mShelf;
+ private int mZDistanceBetweenElements;
+ private int mBaseZHeight;
+ private int mMaxLayoutHeight;
+ private ActivatableNotificationView mLastVisibleBackgroundChild;
+ private float mCurrentScrollVelocity;
+ private int mStatusBarState;
+ private float mExpandingVelocity;
+ private boolean mPanelTracking;
+ private boolean mExpansionChanging;
+ private boolean mPanelFullWidth;
+ private Collection<HeadsUpManager.HeadsUpEntry> mPulsing;
+ private boolean mUnlockHintRunning;
+ private boolean mQsCustomizerShowing;
+ private int mIntrinsicPadding;
+
+ public AmbientState(Context context) {
+ reload(context);
+ }
+
+ /**
+ * Reload the dimens e.g. if the density changed.
+ */
+ public void reload(Context context) {
+ mZDistanceBetweenElements = Math.max(1, context.getResources()
+ .getDimensionPixelSize(R.dimen.z_distance_between_notifications));
+ mBaseZHeight = 4 * mZDistanceBetweenElements;
+ }
+
+ /**
+ * @return the basic Z height on which notifications remain.
+ */
+ public int getBaseZHeight() {
+ return mBaseZHeight;
+ }
+
+ /**
+ * @return the distance in Z between two overlaying notifications.
+ */
+ public int getZDistanceBetweenElements() {
+ return mZDistanceBetweenElements;
+ }
+
+ public int getScrollY() {
+ return mScrollY;
+ }
+
+ public void setScrollY(int scrollY) {
+ this.mScrollY = scrollY;
+ }
+
+ public void onBeginDrag(View view) {
+ mDraggedViews.add(view);
+ }
+
+ public void onDragFinished(View view) {
+ mDraggedViews.remove(view);
+ }
+
+ public ArrayList<View> getDraggedViews() {
+ return mDraggedViews;
+ }
+
+ /**
+ * @param dimmed Whether we are in a dimmed state (on the lockscreen), where the backgrounds are
+ * translucent and everything is scaled back a bit.
+ */
+ public void setDimmed(boolean dimmed) {
+ mDimmed = dimmed;
+ }
+
+ /** In dark mode, we draw as little as possible, assuming a black background */
+ public void setDark(boolean dark) {
+ mDark = dark;
+ }
+
+ public void setHideSensitive(boolean hideSensitive) {
+ mHideSensitive = hideSensitive;
+ }
+
+ /**
+ * In dimmed mode, a child can be activated, which happens on the first tap of the double-tap
+ * interaction. This child is then scaled normally and its background is fully opaque.
+ */
+ public void setActivatedChild(ActivatableNotificationView activatedChild) {
+ mActivatedChild = activatedChild;
+ }
+
+ public boolean isDimmed() {
+ return mDimmed;
+ }
+
+ public boolean isDark() {
+ return mDark;
+ }
+
+ public boolean isHideSensitive() {
+ return mHideSensitive;
+ }
+
+ public ActivatableNotificationView getActivatedChild() {
+ return mActivatedChild;
+ }
+
+ public void setOverScrollAmount(float amount, boolean onTop) {
+ if (onTop) {
+ mOverScrollTopAmount = amount;
+ } else {
+ mOverScrollBottomAmount = amount;
+ }
+ }
+
+ public float getOverScrollAmount(boolean top) {
+ return top ? mOverScrollTopAmount : mOverScrollBottomAmount;
+ }
+
+ public int getSpeedBumpIndex() {
+ return mSpeedBumpIndex;
+ }
+
+ public void setSpeedBumpIndex(int shelfIndex) {
+ mSpeedBumpIndex = shelfIndex;
+ }
+
+ public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ mHeadsUpManager = headsUpManager;
+ }
+
+ public float getStackTranslation() {
+ return mStackTranslation;
+ }
+
+ public void setStackTranslation(float stackTranslation) {
+ mStackTranslation = stackTranslation;
+ }
+
+ public void setLayoutHeight(int layoutHeight) {
+ mLayoutHeight = layoutHeight;
+ }
+
+ public float getTopPadding() {
+ return mTopPadding;
+ }
+
+ public void setTopPadding(int topPadding) {
+ mTopPadding = topPadding;
+ }
+
+ public int getInnerHeight() {
+ return Math.max(Math.min(mLayoutHeight, mMaxLayoutHeight) - mTopPadding, mLayoutMinHeight);
+ }
+
+ public boolean isShadeExpanded() {
+ return mShadeExpanded;
+ }
+
+ public void setShadeExpanded(boolean shadeExpanded) {
+ mShadeExpanded = shadeExpanded;
+ }
+
+ public void setMaxHeadsUpTranslation(float maxHeadsUpTranslation) {
+ mMaxHeadsUpTranslation = maxHeadsUpTranslation;
+ }
+
+ public float getMaxHeadsUpTranslation() {
+ return mMaxHeadsUpTranslation;
+ }
+
+ public void setDismissAllInProgress(boolean dismissAllInProgress) {
+ mDismissAllInProgress = dismissAllInProgress;
+ }
+
+ public boolean isDismissAllInProgress() {
+ return mDismissAllInProgress;
+ }
+
+ public void setLayoutMinHeight(int layoutMinHeight) {
+ mLayoutMinHeight = layoutMinHeight;
+ }
+
+ public void setShelf(NotificationShelf shelf) {
+ mShelf = shelf;
+ }
+
+ public NotificationShelf getShelf() {
+ return mShelf;
+ }
+
+ public void setLayoutMaxHeight(int maxLayoutHeight) {
+ mMaxLayoutHeight = maxLayoutHeight;
+ }
+
+ /**
+ * Sets the last visible view of the host layout, that has a background, i.e the very last
+ * view in the shade, without the clear all button.
+ */
+ public void setLastVisibleBackgroundChild(
+ ActivatableNotificationView lastVisibleBackgroundChild) {
+ mLastVisibleBackgroundChild = lastVisibleBackgroundChild;
+ }
+
+ public ActivatableNotificationView getLastVisibleBackgroundChild() {
+ return mLastVisibleBackgroundChild;
+ }
+
+ public void setCurrentScrollVelocity(float currentScrollVelocity) {
+ mCurrentScrollVelocity = currentScrollVelocity;
+ }
+
+ public float getCurrentScrollVelocity() {
+ return mCurrentScrollVelocity;
+ }
+
+ public boolean isOnKeyguard() {
+ return mStatusBarState == StatusBarState.KEYGUARD;
+ }
+
+ public void setStatusBarState(int statusBarState) {
+ mStatusBarState = statusBarState;
+ }
+
+ public void setExpandingVelocity(float expandingVelocity) {
+ mExpandingVelocity = expandingVelocity;
+ }
+
+ public void setExpansionChanging(boolean expansionChanging) {
+ mExpansionChanging = expansionChanging;
+ }
+
+ public boolean isExpansionChanging() {
+ return mExpansionChanging;
+ }
+
+ public float getExpandingVelocity() {
+ return mExpandingVelocity;
+ }
+
+ public void setPanelTracking(boolean panelTracking) {
+ mPanelTracking = panelTracking;
+ }
+
+ public boolean hasPulsingNotifications() {
+ return mPulsing != null;
+ }
+
+ public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> hasPulsing) {
+ mPulsing = hasPulsing;
+ }
+
+ public boolean isPulsing(NotificationData.Entry entry) {
+ if (mPulsing == null) {
+ return false;
+ }
+ for (HeadsUpManager.HeadsUpEntry e : mPulsing) {
+ if (e.entry == entry) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isPanelTracking() {
+ return mPanelTracking;
+ }
+
+ public boolean isPanelFullWidth() {
+ return mPanelFullWidth;
+ }
+
+ public void setPanelFullWidth(boolean panelFullWidth) {
+ mPanelFullWidth = panelFullWidth;
+ }
+
+ public void setUnlockHintRunning(boolean unlockHintRunning) {
+ mUnlockHintRunning = unlockHintRunning;
+ }
+
+ public boolean isUnlockHintRunning() {
+ return mUnlockHintRunning;
+ }
+
+ public boolean isQsCustomizerShowing() {
+ return mQsCustomizerShowing;
+ }
+
+ public void setQsCustomizerShowing(boolean qsCustomizerShowing) {
+ mQsCustomizerShowing = qsCustomizerShowing;
+ }
+
+ public void setIntrinsicPadding(int intrinsicPadding) {
+ mIntrinsicPadding = intrinsicPadding;
+ }
+
+ public int getIntrinsicPadding() {
+ return mIntrinsicPadding;
+ }
+
+ /**
+ * Similar to the normal is above shelf logic but doesn't allow it to be above in AOD1.
+ *
+ * @param expandableView the view to check
+ */
+ public boolean isAboveShelf(ExpandableView expandableView) {
+ if (!(expandableView instanceof ExpandableNotificationRow)) {
+ return expandableView.isAboveShelf();
+ }
+ ExpandableNotificationRow row = (ExpandableNotificationRow) expandableView;
+ return row.isAboveShelf() && !isDozingAndNotPulsing(row);
+ }
+
+ /**
+ * @return whether a view is dozing and not pulsing right now
+ */
+ public boolean isDozingAndNotPulsing(ExpandableView view) {
+ if (view instanceof ExpandableNotificationRow) {
+ return isDozingAndNotPulsing((ExpandableNotificationRow) view);
+ }
+ return false;
+ }
+
+ /**
+ * @return whether a row is dozing and not pulsing right now
+ */
+ public boolean isDozingAndNotPulsing(ExpandableNotificationRow row) {
+ return isDark() && !isPulsing(row.getEntry());
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/AnimationFilter.java b/com/android/systemui/statusbar/stack/AnimationFilter.java
new file mode 100644
index 0000000..53377d9
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/AnimationFilter.java
@@ -0,0 +1,181 @@
+/*
+ * 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.stack;
+
+import android.support.v4.util.ArraySet;
+import android.util.Property;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * Filters the animations for only a certain type of properties.
+ */
+public class AnimationFilter {
+ boolean animateAlpha;
+ boolean animateX;
+ boolean animateY;
+ ArraySet<View> animateYViews = new ArraySet<>();
+ boolean animateZ;
+ boolean animateHeight;
+ boolean animateTopInset;
+ boolean animateDimmed;
+ boolean animateDark;
+ boolean animateHideSensitive;
+ public boolean animateShadowAlpha;
+ boolean hasDelays;
+ boolean hasGoToFullShadeEvent;
+ boolean hasHeadsUpDisappearClickEvent;
+ private ArraySet<Property> mAnimatedProperties = new ArraySet<>();
+
+ public AnimationFilter animateAlpha() {
+ animateAlpha = true;
+ return this;
+ }
+
+ public AnimationFilter animateScale() {
+ animate(View.SCALE_X);
+ animate(View.SCALE_Y);
+ return this;
+ }
+
+ public AnimationFilter animateX() {
+ animateX = true;
+ return this;
+ }
+
+ public AnimationFilter animateY() {
+ animateY = true;
+ return this;
+ }
+
+ public AnimationFilter hasDelays() {
+ hasDelays = true;
+ return this;
+ }
+
+ public AnimationFilter animateZ() {
+ animateZ = true;
+ return this;
+ }
+
+ public AnimationFilter animateHeight() {
+ animateHeight = true;
+ return this;
+ }
+
+ public AnimationFilter animateTopInset() {
+ animateTopInset = true;
+ return this;
+ }
+
+ public AnimationFilter animateDimmed() {
+ animateDimmed = true;
+ return this;
+ }
+
+ public AnimationFilter animateDark() {
+ animateDark = true;
+ return this;
+ }
+
+ public AnimationFilter animateHideSensitive() {
+ animateHideSensitive = true;
+ return this;
+ }
+
+ public AnimationFilter animateShadowAlpha() {
+ animateShadowAlpha = true;
+ return this;
+ }
+
+ public AnimationFilter animateY(View view) {
+ animateYViews.add(view);
+ return this;
+ }
+
+ public boolean shouldAnimateY(View view) {
+ return animateY || animateYViews.contains(view);
+ }
+
+ /**
+ * Combines multiple filters into {@code this} filter, using or as the operand .
+ *
+ * @param events The animation events from the filters to combine.
+ */
+ public void applyCombination(ArrayList<NotificationStackScrollLayout.AnimationEvent> events) {
+ reset();
+ int size = events.size();
+ for (int i = 0; i < size; i++) {
+ NotificationStackScrollLayout.AnimationEvent ev = events.get(i);
+ combineFilter(events.get(i).filter);
+ if (ev.animationType ==
+ NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE) {
+ hasGoToFullShadeEvent = true;
+ }
+ if (ev.animationType == NotificationStackScrollLayout.AnimationEvent
+ .ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
+ hasHeadsUpDisappearClickEvent = true;
+ }
+ }
+ }
+
+ public void combineFilter(AnimationFilter filter) {
+ animateAlpha |= filter.animateAlpha;
+ animateX |= filter.animateX;
+ animateY |= filter.animateY;
+ animateYViews.addAll(filter.animateYViews);
+ animateZ |= filter.animateZ;
+ animateHeight |= filter.animateHeight;
+ animateTopInset |= filter.animateTopInset;
+ animateDimmed |= filter.animateDimmed;
+ animateDark |= filter.animateDark;
+ animateHideSensitive |= filter.animateHideSensitive;
+ animateShadowAlpha |= filter.animateShadowAlpha;
+ hasDelays |= filter.hasDelays;
+ mAnimatedProperties.addAll(filter.mAnimatedProperties);
+ }
+
+ public void reset() {
+ animateAlpha = false;
+ animateX = false;
+ animateY = false;
+ animateYViews.clear();
+ animateZ = false;
+ animateHeight = false;
+ animateShadowAlpha = false;
+ animateTopInset = false;
+ animateDimmed = false;
+ animateDark = false;
+ animateHideSensitive = false;
+ hasDelays = false;
+ hasGoToFullShadeEvent = false;
+ hasHeadsUpDisappearClickEvent = false;
+ mAnimatedProperties.clear();
+ }
+
+ public AnimationFilter animate(Property property) {
+ mAnimatedProperties.add(property);
+ return this;
+ }
+
+ public boolean shouldAnimateProperty(Property property) {
+ // TODO: migrate all existing animators to properties
+ return mAnimatedProperties.contains(property);
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/AnimationProperties.java b/com/android/systemui/statusbar/stack/AnimationProperties.java
new file mode 100644
index 0000000..ebb0a6d
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/AnimationProperties.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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.stack;
+
+import android.animation.AnimatorListenerAdapter;
+import android.util.ArrayMap;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import java.util.HashMap;
+
+/**
+ * Properties for a View animation
+ */
+public class AnimationProperties {
+ public long duration;
+ public long delay;
+ private ArrayMap<Property, Interpolator> mInterpolatorMap;
+
+ /**
+ * @return an animation filter for this animation.
+ */
+ public AnimationFilter getAnimationFilter() {
+ return new AnimationFilter();
+ }
+
+ /**
+ * @return a listener that should be run whenever any property finished its animation
+ */
+ public AnimatorListenerAdapter getAnimationFinishListener() {
+ return null;
+ }
+
+ public boolean wasAdded(View view) {
+ return false;
+ }
+
+ /**
+ * Get a custom interpolator for a property instead of the normal one.
+ */
+ public Interpolator getCustomInterpolator(View child, Property property) {
+ return mInterpolatorMap != null ? mInterpolatorMap.get(property) : null;
+ }
+
+
+ public void combineCustomInterpolators(AnimationProperties iconAnimationProperties) {
+ ArrayMap<Property, Interpolator> map = iconAnimationProperties.mInterpolatorMap;
+ if (map != null) {
+ if (mInterpolatorMap == null) {
+ mInterpolatorMap = new ArrayMap<>();
+ }
+ mInterpolatorMap.putAll(map);
+ }
+ }
+
+ /**
+ * Set a custom interpolator to use for all views for a property.
+ */
+ public AnimationProperties setCustomInterpolator(Property property, Interpolator interpolator) {
+ if (mInterpolatorMap == null) {
+ mInterpolatorMap = new ArrayMap<>();
+ }
+ mInterpolatorMap.put(property, interpolator);
+ return this;
+ }
+
+ public AnimationProperties setDuration(long duration) {
+ this.duration = duration;
+ return this;
+ }
+
+ public AnimationProperties setDelay(long delay) {
+ this.delay = delay;
+ return this;
+ }
+
+ public AnimationProperties resetCustomInterpolators() {
+ mInterpolatorMap = null;
+ return this;
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/ExpandableViewState.java b/com/android/systemui/statusbar/stack/ExpandableViewState.java
new file mode 100644
index 0000000..e0fd481
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/ExpandableViewState.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2015 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.stack;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.view.View;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+
+/**
+* A state of an expandable view
+*/
+public class ExpandableViewState extends ViewState {
+
+ private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag;
+ private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
+ private static final int TAG_ANIMATOR_SHADOW_ALPHA = R.id.shadow_alpha_animator_tag;
+ private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
+ private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
+ private static final int TAG_END_SHADOW_ALPHA = R.id.shadow_alpha_animator_end_value_tag;
+ private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag;
+ private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag;
+ private static final int TAG_START_SHADOW_ALPHA = R.id.shadow_alpha_animator_start_value_tag;
+
+ // These are flags such that we can create masks for filtering.
+
+ /**
+ * No known location. This is the default and should not be set after an invocation of the
+ * algorithm.
+ */
+ public static final int LOCATION_UNKNOWN = 0x00;
+
+ /**
+ * The location is the first heads up notification, so on the very top.
+ */
+ public static final int LOCATION_FIRST_HUN = 0x01;
+
+ /**
+ * The location is hidden / scrolled away on the top.
+ */
+ public static final int LOCATION_HIDDEN_TOP = 0x02;
+
+ /**
+ * The location is in the main area of the screen and visible.
+ */
+ public static final int LOCATION_MAIN_AREA = 0x04;
+
+ /**
+ * The location is in the bottom stack and it's peeking
+ */
+ public static final int LOCATION_BOTTOM_STACK_PEEKING = 0x08;
+
+ /**
+ * The location is in the bottom stack and it's hidden.
+ */
+ public static final int LOCATION_BOTTOM_STACK_HIDDEN = 0x10;
+
+ /**
+ * The view isn't laid out at all.
+ */
+ public static final int LOCATION_GONE = 0x40;
+
+ /**
+ * The visible locations of a view.
+ */
+ public static final int VISIBLE_LOCATIONS = ExpandableViewState.LOCATION_FIRST_HUN
+ | ExpandableViewState.LOCATION_MAIN_AREA;
+
+ public int height;
+ public boolean dimmed;
+ public boolean dark;
+ public boolean hideSensitive;
+ public boolean belowSpeedBump;
+ public float shadowAlpha;
+ public boolean inShelf;
+
+ /**
+ * How much the child overlaps with the previous child on top. This is used to
+ * show the background properly when the child on top is translating away.
+ */
+ public int clipTopAmount;
+
+ /**
+ * The index of the view, only accounting for views not equal to GONE
+ */
+ public int notGoneIndex;
+
+ /**
+ * The location this view is currently rendered at.
+ *
+ * <p>See <code>LOCATION_</code> flags.</p>
+ */
+ public int location;
+
+ @Override
+ public void copyFrom(ViewState viewState) {
+ super.copyFrom(viewState);
+ if (viewState instanceof ExpandableViewState) {
+ ExpandableViewState svs = (ExpandableViewState) viewState;
+ height = svs.height;
+ dimmed = svs.dimmed;
+ shadowAlpha = svs.shadowAlpha;
+ dark = svs.dark;
+ hideSensitive = svs.hideSensitive;
+ belowSpeedBump = svs.belowSpeedBump;
+ clipTopAmount = svs.clipTopAmount;
+ notGoneIndex = svs.notGoneIndex;
+ location = svs.location;
+ }
+ }
+
+ /**
+ * Applies a {@link ExpandableViewState} to a {@link ExpandableView}.
+ */
+ @Override
+ public void applyToView(View view) {
+ super.applyToView(view);
+ if (view instanceof ExpandableView) {
+ ExpandableView expandableView = (ExpandableView) view;
+
+ int height = expandableView.getActualHeight();
+ int newHeight = this.height;
+
+ // apply height
+ if (height != newHeight) {
+ expandableView.setActualHeight(newHeight, false /* notifyListeners */);
+ }
+
+ float shadowAlpha = expandableView.getShadowAlpha();
+ float newShadowAlpha = this.shadowAlpha;
+
+ // apply shadowAlpha
+ if (shadowAlpha != newShadowAlpha) {
+ expandableView.setShadowAlpha(newShadowAlpha);
+ }
+
+ // apply dimming
+ expandableView.setDimmed(this.dimmed, false /* animate */);
+
+ // apply hiding sensitive
+ expandableView.setHideSensitive(
+ this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */);
+
+ // apply below shelf speed bump
+ expandableView.setBelowSpeedBump(this.belowSpeedBump);
+
+ // apply dark
+ expandableView.setDark(this.dark, false /* animate */, 0 /* delay */);
+
+ // apply clipping
+ float oldClipTopAmount = expandableView.getClipTopAmount();
+ if (oldClipTopAmount != this.clipTopAmount) {
+ expandableView.setClipTopAmount(this.clipTopAmount);
+ }
+
+ expandableView.setTransformingInShelf(false);
+ expandableView.setInShelf(inShelf);
+ }
+ }
+
+ @Override
+ public void animateTo(View child, AnimationProperties properties) {
+ super.animateTo(child, properties);
+ if (!(child instanceof ExpandableView)) {
+ return;
+ }
+ ExpandableView expandableView = (ExpandableView) child;
+ AnimationFilter animationFilter = properties.getAnimationFilter();
+
+ // start height animation
+ if (this.height != expandableView.getActualHeight()) {
+ startHeightAnimation(expandableView, properties);
+ } else {
+ abortAnimation(child, TAG_ANIMATOR_HEIGHT);
+ }
+
+ // start shadow alpha animation
+ if (this.shadowAlpha != expandableView.getShadowAlpha()) {
+ startShadowAlphaAnimation(expandableView, properties);
+ } else {
+ abortAnimation(child, TAG_ANIMATOR_SHADOW_ALPHA);
+ }
+
+ // start top inset animation
+ if (this.clipTopAmount != expandableView.getClipTopAmount()) {
+ startInsetAnimation(expandableView, properties);
+ } else {
+ abortAnimation(child, TAG_ANIMATOR_TOP_INSET);
+ }
+
+ // start dimmed animation
+ expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed);
+
+ // apply below the speed bump
+ expandableView.setBelowSpeedBump(this.belowSpeedBump);
+
+ // start hiding sensitive animation
+ expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive,
+ properties.delay, properties.duration);
+
+ // start dark animation
+ expandableView.setDark(this.dark, animationFilter.animateDark, properties.delay);
+
+ if (properties.wasAdded(child) && !hidden) {
+ expandableView.performAddAnimation(properties.delay, properties.duration);
+ }
+
+ if (!expandableView.isInShelf() && this.inShelf) {
+ expandableView.setTransformingInShelf(true);
+ }
+ expandableView.setInShelf(this.inShelf);
+ }
+
+ private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) {
+ Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
+ Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
+ int newEndValue = this.height;
+ if (previousEndValue != null && previousEndValue == newEndValue) {
+ return;
+ }
+ ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
+ AnimationFilter filter = properties.getAnimationFilter();
+ if (!filter.animateHeight) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ int relativeDiff = newEndValue - previousEndValue;
+ int newStartValue = previousStartValue + relativeDiff;
+ values[0].setIntValues(newStartValue, newEndValue);
+ child.setTag(TAG_START_HEIGHT, newStartValue);
+ child.setTag(TAG_END_HEIGHT, newEndValue);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ child.setActualHeight(newEndValue, false);
+ return;
+ }
+ }
+
+ ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ child.setActualHeight((int) animation.getAnimatedValue(),
+ false /* notifyListeners */);
+ }
+ });
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+ animator.setDuration(newDuration);
+ if (properties.delay > 0 && (previousAnimator == null
+ || previousAnimator.getAnimatedFraction() == 0)) {
+ animator.setStartDelay(properties.delay);
+ }
+ AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ boolean mWasCancelled;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ child.setTag(TAG_ANIMATOR_HEIGHT, null);
+ child.setTag(TAG_START_HEIGHT, null);
+ child.setTag(TAG_END_HEIGHT, null);
+ child.setActualHeightAnimating(false);
+ if (!mWasCancelled && child instanceof ExpandableNotificationRow) {
+ ((ExpandableNotificationRow) child).setGroupExpansionChanging(
+ false /* isExpansionChanging */);
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mWasCancelled = false;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mWasCancelled = true;
+ }
+ });
+ startAnimator(animator, listener);
+ child.setTag(TAG_ANIMATOR_HEIGHT, animator);
+ child.setTag(TAG_START_HEIGHT, child.getActualHeight());
+ child.setTag(TAG_END_HEIGHT, newEndValue);
+ child.setActualHeightAnimating(true);
+ }
+
+ private void startShadowAlphaAnimation(final ExpandableView child,
+ AnimationProperties properties) {
+ Float previousStartValue = getChildTag(child, TAG_START_SHADOW_ALPHA);
+ Float previousEndValue = getChildTag(child, TAG_END_SHADOW_ALPHA);
+ float newEndValue = this.shadowAlpha;
+ if (previousEndValue != null && previousEndValue == newEndValue) {
+ return;
+ }
+ ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SHADOW_ALPHA);
+ AnimationFilter filter = properties.getAnimationFilter();
+ if (!filter.animateShadowAlpha) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ float relativeDiff = newEndValue - previousEndValue;
+ float newStartValue = previousStartValue + relativeDiff;
+ values[0].setFloatValues(newStartValue, newEndValue);
+ child.setTag(TAG_START_SHADOW_ALPHA, newStartValue);
+ child.setTag(TAG_END_SHADOW_ALPHA, newEndValue);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ child.setShadowAlpha(newEndValue);
+ return;
+ }
+ }
+
+ ValueAnimator animator = ValueAnimator.ofFloat(child.getShadowAlpha(), newEndValue);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ child.setShadowAlpha((float) animation.getAnimatedValue());
+ }
+ });
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+ animator.setDuration(newDuration);
+ if (properties.delay > 0 && (previousAnimator == null
+ || previousAnimator.getAnimatedFraction() == 0)) {
+ animator.setStartDelay(properties.delay);
+ }
+ AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, null);
+ child.setTag(TAG_START_SHADOW_ALPHA, null);
+ child.setTag(TAG_END_SHADOW_ALPHA, null);
+ }
+ });
+ startAnimator(animator, listener);
+ child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, animator);
+ child.setTag(TAG_START_SHADOW_ALPHA, child.getShadowAlpha());
+ child.setTag(TAG_END_SHADOW_ALPHA, newEndValue);
+ }
+
+ private void startInsetAnimation(final ExpandableView child, AnimationProperties properties) {
+ Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET);
+ Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET);
+ int newEndValue = this.clipTopAmount;
+ if (previousEndValue != null && previousEndValue == newEndValue) {
+ return;
+ }
+ ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET);
+ AnimationFilter filter = properties.getAnimationFilter();
+ if (!filter.animateTopInset) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ int relativeDiff = newEndValue - previousEndValue;
+ int newStartValue = previousStartValue + relativeDiff;
+ values[0].setIntValues(newStartValue, newEndValue);
+ child.setTag(TAG_START_TOP_INSET, newStartValue);
+ child.setTag(TAG_END_TOP_INSET, newEndValue);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ child.setClipTopAmount(newEndValue);
+ return;
+ }
+ }
+
+ ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ child.setClipTopAmount((int) animation.getAnimatedValue());
+ }
+ });
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+ animator.setDuration(newDuration);
+ if (properties.delay > 0 && (previousAnimator == null
+ || previousAnimator.getAnimatedFraction() == 0)) {
+ animator.setStartDelay(properties.delay);
+ }
+ AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ child.setTag(TAG_ANIMATOR_TOP_INSET, null);
+ child.setTag(TAG_START_TOP_INSET, null);
+ child.setTag(TAG_END_TOP_INSET, null);
+ }
+ });
+ startAnimator(animator, listener);
+ child.setTag(TAG_ANIMATOR_TOP_INSET, animator);
+ child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount());
+ child.setTag(TAG_END_TOP_INSET, newEndValue);
+ }
+
+ /**
+ * Get the end value of the height animation running on a view or the actualHeight
+ * if no animation is running.
+ */
+ public static int getFinalActualHeight(ExpandableView view) {
+ if (view == null) {
+ return 0;
+ }
+ ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT);
+ if (heightAnimator == null) {
+ return view.getActualHeight();
+ } else {
+ return getChildTag(view, TAG_END_HEIGHT);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/HeadsUpAppearInterpolator.java b/com/android/systemui/statusbar/stack/HeadsUpAppearInterpolator.java
new file mode 100644
index 0000000..05c0099
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/HeadsUpAppearInterpolator.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.stack;
+
+import android.graphics.Path;
+import android.view.animation.PathInterpolator;
+
+/**
+ * An interpolator specifically designed for the appear animation of heads up notifications.
+ */
+public class HeadsUpAppearInterpolator extends PathInterpolator {
+ public HeadsUpAppearInterpolator() {
+ super(getAppearPath());
+ }
+
+ private static Path getAppearPath() {
+ Path path = new Path();
+ path.moveTo(0, 0);
+ float x1 = 250f;
+ float x2 = 150f;
+ float x3 = 100f;
+ float y1 = 90f;
+ float y2 = 78f;
+ float y3 = 80f;
+ float xTot = (x1 + x2 + x3);
+ path.cubicTo(x1 * 0.9f / xTot, 0f,
+ x1 * 0.8f / xTot, y1 / y3,
+ x1 / xTot , y1 / y3);
+ path.cubicTo((x1 + x2 * 0.4f) / xTot, y1 / y3,
+ (x1 + x2 * 0.2f) / xTot, y2 / y3,
+ (x1 + x2) / xTot, y2 / y3);
+ path.cubicTo((x1 + x2 + x3 * 0.4f) / xTot, y2 / y3,
+ (x1 + x2 + x3 * 0.2f) / xTot, 1f,
+ 1f, 1f);
+ return path;
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java b/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
new file mode 100644
index 0000000..fe53104
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
@@ -0,0 +1,1263 @@
+/*
+ * Copyright (C) 2015 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.stack;
+
+import android.app.Notification;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.ColorDrawable;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationHeaderUtil;
+import com.android.systemui.statusbar.notification.HybridGroupManager;
+import com.android.systemui.statusbar.notification.HybridNotificationView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.NotificationViewWrapper;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A container containing child notifications
+ */
+public class NotificationChildrenContainer extends ViewGroup {
+
+ private static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2;
+ private static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5;
+ private static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8;
+ private static final int NUMBER_OF_CHILDREN_WHEN_AMBIENT = 3;
+ private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() {
+ private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
+
+ @Override
+ public AnimationFilter getAnimationFilter() {
+ return mAnimationFilter;
+ }
+ }.setDuration(200);
+
+ private final List<View> mDividers = new ArrayList<>();
+ private final List<ExpandableNotificationRow> mChildren = new ArrayList<>();
+ private final HybridGroupManager mHybridGroupManager;
+ private int mChildPadding;
+ private int mDividerHeight;
+ private float mDividerAlpha;
+ private int mNotificationHeaderMargin;
+
+ private int mNotificatonTopPadding;
+ private float mCollapsedBottompadding;
+ private boolean mChildrenExpanded;
+ private ExpandableNotificationRow mContainingNotification;
+ private TextView mOverflowNumber;
+ private ViewState mGroupOverFlowState;
+ private int mRealHeight;
+ private boolean mUserLocked;
+ private int mActualHeight;
+ private boolean mNeverAppliedGroupState;
+ private int mHeaderHeight;
+
+ /**
+ * Whether or not individual notifications that are part of this container will have shadows.
+ */
+ private boolean mEnableShadowOnChildNotifications;
+
+ private NotificationHeaderView mNotificationHeader;
+ private NotificationViewWrapper mNotificationHeaderWrapper;
+ private NotificationHeaderView mNotificationHeaderLowPriority;
+ private NotificationViewWrapper mNotificationHeaderWrapperLowPriority;
+ private ViewGroup mNotificationHeaderAmbient;
+ private NotificationViewWrapper mNotificationHeaderWrapperAmbient;
+ private NotificationHeaderUtil mHeaderUtil;
+ private ViewState mHeaderViewState;
+ private int mClipBottomAmount;
+ private boolean mIsLowPriority;
+ private OnClickListener mHeaderClickListener;
+ private ViewGroup mCurrentHeader;
+
+ private boolean mShowDividersWhenExpanded;
+ private boolean mHideDividersDuringExpand;
+
+ public NotificationChildrenContainer(Context context) {
+ this(context, null);
+ }
+
+ public NotificationChildrenContainer(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initDimens();
+ mHybridGroupManager = new HybridGroupManager(getContext(), this);
+ }
+
+ private void initDimens() {
+ Resources res = getResources();
+ mChildPadding = res.getDimensionPixelSize(R.dimen.notification_children_padding);
+ mDividerHeight = res.getDimensionPixelSize(
+ R.dimen.notification_children_container_divider_height);
+ mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha);
+ mHeaderHeight = res.getDimensionPixelSize(
+ R.dimen.notification_children_container_header_height);
+ mNotificationHeaderMargin = res.getDimensionPixelSize(
+ R.dimen.notification_children_container_margin_top);
+ mNotificatonTopPadding = res.getDimensionPixelSize(
+ R.dimen.notification_children_container_top_padding);
+ mCollapsedBottompadding = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin_bottom);
+ mEnableShadowOnChildNotifications =
+ res.getBoolean(R.bool.config_enableShadowOnChildNotifications);
+ mShowDividersWhenExpanded =
+ res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded);
+ mHideDividersDuringExpand =
+ res.getBoolean(R.bool.config_hideDividersDuringExpand);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
+ for (int i = 0; i < childCount; i++) {
+ View child = mChildren.get(i);
+ // We need to layout all children even the GONE ones, such that the heights are
+ // calculated correctly as they are used to calculate how many we can fit on the screen
+ child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
+ mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
+ }
+ if (mOverflowNumber != null) {
+ boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ int left = (isRtl ? 0 : getWidth() - mOverflowNumber.getMeasuredWidth());
+ int right = left + mOverflowNumber.getMeasuredWidth();
+ mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight());
+ }
+ if (mNotificationHeader != null) {
+ mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(),
+ mNotificationHeader.getMeasuredHeight());
+ }
+ if (mNotificationHeaderLowPriority != null) {
+ mNotificationHeaderLowPriority.layout(0, 0,
+ mNotificationHeaderLowPriority.getMeasuredWidth(),
+ mNotificationHeaderLowPriority.getMeasuredHeight());
+ }
+ if (mNotificationHeaderAmbient != null) {
+ mNotificationHeaderAmbient.layout(0, 0,
+ mNotificationHeaderAmbient.getMeasuredWidth(),
+ mNotificationHeaderAmbient.getMeasuredHeight());
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
+ boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
+ int size = MeasureSpec.getSize(heightMeasureSpec);
+ int newHeightSpec = heightMeasureSpec;
+ if (hasFixedHeight || isHeightLimited) {
+ newHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
+ }
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ if (mOverflowNumber != null) {
+ mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+ newHeightSpec);
+ }
+ int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
+ int height = mNotificationHeaderMargin + mNotificatonTopPadding;
+ int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
+ int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
+ int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1;
+ for (int i = 0; i < childCount; i++) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ // We need to measure all children even the GONE ones, such that the heights are
+ // calculated correctly as they are used to calculate how many we can fit on the screen.
+ boolean isOverflow = i == overflowIndex;
+ child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null
+ ? mOverflowNumber.getMeasuredWidth()
+ : 0);
+ child.measure(widthMeasureSpec, newHeightSpec);
+ // layout the divider
+ View divider = mDividers.get(i);
+ divider.measure(widthMeasureSpec, dividerHeightSpec);
+ if (child.getVisibility() != GONE) {
+ height += child.getMeasuredHeight() + mDividerHeight;
+ }
+ }
+ mRealHeight = height;
+ if (heightMode != MeasureSpec.UNSPECIFIED) {
+ height = Math.min(height, size);
+ }
+
+ int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
+ if (mNotificationHeader != null) {
+ mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec);
+ }
+ if (mNotificationHeaderLowPriority != null) {
+ headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
+ mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec);
+ }
+ if (mNotificationHeaderAmbient != null) {
+ headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
+ mNotificationHeaderAmbient.measure(widthMeasureSpec, headerHeightSpec);
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ @Override
+ public boolean pointInView(float localX, float localY, float slop) {
+ return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
+ localY < (mRealHeight + slop);
+ }
+
+ /**
+ * Add a child notification to this view.
+ *
+ * @param row the row to add
+ * @param childIndex the index to add it at, if -1 it will be added at the end
+ */
+ public void addNotification(ExpandableNotificationRow row, int childIndex) {
+ int newIndex = childIndex < 0 ? mChildren.size() : childIndex;
+ mChildren.add(newIndex, row);
+ addView(row);
+ row.setUserLocked(mUserLocked);
+
+ View divider = inflateDivider();
+ addView(divider);
+ mDividers.add(newIndex, divider);
+
+ updateGroupOverflow();
+ row.setContentTransformationAmount(0, false /* isLastChild */);
+ }
+
+ public void removeNotification(ExpandableNotificationRow row) {
+ int childIndex = mChildren.indexOf(row);
+ mChildren.remove(row);
+ removeView(row);
+
+ final View divider = mDividers.remove(childIndex);
+ removeView(divider);
+ getOverlay().add(divider);
+ CrossFadeHelper.fadeOut(divider, new Runnable() {
+ @Override
+ public void run() {
+ getOverlay().remove(divider);
+ }
+ });
+
+ row.setSystemChildExpanded(false);
+ row.setUserLocked(false);
+ updateGroupOverflow();
+ if (!row.isRemoved()) {
+ mHeaderUtil.restoreNotificationHeader(row);
+ }
+ }
+
+ /**
+ * @return The number of notification children in the container.
+ */
+ public int getNotificationChildCount() {
+ return mChildren.size();
+ }
+
+ public void recreateNotificationHeader(OnClickListener listener) {
+ mHeaderClickListener = listener;
+ StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
+ final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(),
+ notification.getNotification());
+ RemoteViews header = builder.makeNotificationHeader(false /* ambient */);
+ if (mNotificationHeader == null) {
+ mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
+ final View expandButton = mNotificationHeader.findViewById(
+ com.android.internal.R.id.expand_button);
+ expandButton.setVisibility(VISIBLE);
+ mNotificationHeader.setOnClickListener(mHeaderClickListener);
+ mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
+ mNotificationHeader, mContainingNotification);
+ addView(mNotificationHeader, 0);
+ invalidate();
+ } else {
+ header.reapply(getContext(), mNotificationHeader);
+ }
+ mNotificationHeaderWrapper.onContentUpdated(mContainingNotification);
+ recreateLowPriorityHeader(builder);
+ recreateAmbientHeader(builder);
+ updateHeaderVisibility(false /* animate */);
+ updateChildrenHeaderAppearance();
+ }
+
+ private void recreateAmbientHeader(Notification.Builder builder) {
+ RemoteViews header;
+ StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
+ if (builder == null) {
+ builder = Notification.Builder.recoverBuilder(getContext(),
+ notification.getNotification());
+ }
+ header = builder.makeNotificationHeader(true /* ambient */);
+ if (mNotificationHeaderAmbient == null) {
+ mNotificationHeaderAmbient = (ViewGroup) header.apply(getContext(), this);
+ mNotificationHeaderWrapperAmbient = NotificationViewWrapper.wrap(getContext(),
+ mNotificationHeaderAmbient, mContainingNotification);
+ mNotificationHeaderWrapperAmbient.onContentUpdated(mContainingNotification);
+ addView(mNotificationHeaderAmbient, 0);
+ invalidate();
+ } else {
+ header.reapply(getContext(), mNotificationHeaderAmbient);
+ }
+ resetHeaderVisibilityIfNeeded(mNotificationHeaderAmbient, calculateDesiredHeader());
+ mNotificationHeaderWrapperAmbient.onContentUpdated(mContainingNotification);
+ }
+
+ /**
+ * Recreate the low-priority header.
+ *
+ * @param builder a builder to reuse. Otherwise the builder will be recovered.
+ */
+ private void recreateLowPriorityHeader(Notification.Builder builder) {
+ RemoteViews header;
+ StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
+ if (mIsLowPriority) {
+ if (builder == null) {
+ builder = Notification.Builder.recoverBuilder(getContext(),
+ notification.getNotification());
+ }
+ header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
+ if (mNotificationHeaderLowPriority == null) {
+ mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(),
+ this);
+ final View expandButton = mNotificationHeaderLowPriority.findViewById(
+ com.android.internal.R.id.expand_button);
+ expandButton.setVisibility(VISIBLE);
+ mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
+ mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(),
+ mNotificationHeaderLowPriority, mContainingNotification);
+ addView(mNotificationHeaderLowPriority, 0);
+ invalidate();
+ } else {
+ header.reapply(getContext(), mNotificationHeaderLowPriority);
+ }
+ mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification);
+ resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader());
+ } else {
+ removeView(mNotificationHeaderLowPriority);
+ mNotificationHeaderLowPriority = null;
+ mNotificationHeaderWrapperLowPriority = null;
+ }
+ }
+
+ public void updateChildrenHeaderAppearance() {
+ mHeaderUtil.updateChildrenHeaderAppearance();
+ }
+
+ public void updateGroupOverflow() {
+ int childCount = mChildren.size();
+ int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
+ if (childCount > maxAllowedVisibleChildren) {
+ mOverflowNumber = mHybridGroupManager.bindOverflowNumber(
+ mOverflowNumber, childCount - maxAllowedVisibleChildren);
+ if (mGroupOverFlowState == null) {
+ mGroupOverFlowState = new ViewState();
+ mNeverAppliedGroupState = true;
+ }
+ } else if (mOverflowNumber != null) {
+ removeView(mOverflowNumber);
+ if (isShown()) {
+ final View removedOverflowNumber = mOverflowNumber;
+ addTransientView(removedOverflowNumber, getTransientViewCount());
+ CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() {
+ @Override
+ public void run() {
+ removeTransientView(removedOverflowNumber);
+ }
+ });
+ }
+ mOverflowNumber = null;
+ mGroupOverFlowState = null;
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ updateGroupOverflow();
+ }
+
+ private View inflateDivider() {
+ return LayoutInflater.from(mContext).inflate(
+ R.layout.notification_children_divider, this, false);
+ }
+
+ public List<ExpandableNotificationRow> getNotificationChildren() {
+ return mChildren;
+ }
+
+ /**
+ * Apply the order given in the list to the children.
+ *
+ * @param childOrder the new list order
+ * @param visualStabilityManager
+ * @param callback
+ * @return whether the list order has changed
+ */
+ public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
+ VisualStabilityManager visualStabilityManager,
+ VisualStabilityManager.Callback callback) {
+ if (childOrder == null) {
+ return false;
+ }
+ boolean result = false;
+ for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ ExpandableNotificationRow desiredChild = childOrder.get(i);
+ if (child != desiredChild) {
+ if (visualStabilityManager.canReorderNotification(desiredChild)) {
+ mChildren.remove(desiredChild);
+ mChildren.add(i, desiredChild);
+ result = true;
+ } else {
+ visualStabilityManager.addReorderingAllowedCallback(callback);
+ }
+ }
+ }
+ updateExpansionStates();
+ return result;
+ }
+
+ private void updateExpansionStates() {
+ if (mChildrenExpanded || mUserLocked) {
+ // we don't modify it the group is expanded or if we are expanding it
+ return;
+ }
+ int size = mChildren.size();
+ for (int i = 0; i < size; i++) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ child.setSystemChildExpanded(i == 0 && size == 1);
+ }
+ }
+
+ /**
+ *
+ * @return the intrinsic size of this children container, i.e the natural fully expanded state
+ */
+ public int getIntrinsicHeight() {
+ int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
+ return getIntrinsicHeight(maxAllowedVisibleChildren);
+ }
+
+ /**
+ * @return the intrinsic height with a number of children given
+ * in @param maxAllowedVisibleChildren
+ */
+ private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
+ if (showingAsLowPriority()) {
+ return mNotificationHeaderLowPriority.getHeight();
+ }
+ int intrinsicHeight = mNotificationHeaderMargin;
+ int visibleChildren = 0;
+ int childCount = mChildren.size();
+ boolean firstChild = true;
+ float expandFactor = 0;
+ if (mUserLocked) {
+ expandFactor = getGroupExpandFraction();
+ }
+ boolean childrenExpanded = mChildrenExpanded || mContainingNotification.isShowingAmbient();
+ for (int i = 0; i < childCount; i++) {
+ if (visibleChildren >= maxAllowedVisibleChildren) {
+ break;
+ }
+ if (!firstChild) {
+ if (mUserLocked) {
+ intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
+ expandFactor);
+ } else {
+ intrinsicHeight += childrenExpanded ? mDividerHeight : mChildPadding;
+ }
+ } else {
+ if (mUserLocked) {
+ intrinsicHeight += NotificationUtils.interpolate(
+ 0,
+ mNotificatonTopPadding + mDividerHeight,
+ expandFactor);
+ } else {
+ intrinsicHeight += childrenExpanded
+ ? mNotificatonTopPadding + mDividerHeight
+ : 0;
+ }
+ firstChild = false;
+ }
+ ExpandableNotificationRow child = mChildren.get(i);
+ intrinsicHeight += child.getIntrinsicHeight();
+ visibleChildren++;
+ }
+ if (mUserLocked) {
+ intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottompadding, 0.0f,
+ expandFactor);
+ } else if (!childrenExpanded) {
+ intrinsicHeight += mCollapsedBottompadding;
+ }
+ return intrinsicHeight;
+ }
+
+ /**
+ * Update the state of all its children based on a linear layout algorithm.
+ *
+ * @param resultState the state to update
+ * @param parentState the state of the parent
+ */
+ public void getState(StackScrollState resultState, ExpandableViewState parentState) {
+ int childCount = mChildren.size();
+ int yPosition = mNotificationHeaderMargin;
+ boolean firstChild = true;
+ int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
+ int lastVisibleIndex = maxAllowedVisibleChildren - 1;
+ int firstOverflowIndex = lastVisibleIndex + 1;
+ float expandFactor = 0;
+ boolean expandingToExpandedGroup = mUserLocked && !showingAsLowPriority();
+ if (mUserLocked) {
+ expandFactor = getGroupExpandFraction();
+ firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
+ }
+
+ boolean childrenExpandedAndNotAnimating = mChildrenExpanded
+ && !mContainingNotification.isGroupExpansionChanging();
+ for (int i = 0; i < childCount; i++) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ if (!firstChild) {
+ if (expandingToExpandedGroup) {
+ yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
+ expandFactor);
+ } else {
+ yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding;
+ }
+ } else {
+ if (expandingToExpandedGroup) {
+ yPosition += NotificationUtils.interpolate(
+ 0,
+ mNotificatonTopPadding + mDividerHeight,
+ expandFactor);
+ } else {
+ yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0;
+ }
+ firstChild = false;
+ }
+
+ ExpandableViewState childState = resultState.getViewStateForView(child);
+ int intrinsicHeight = child.getIntrinsicHeight();
+ childState.height = intrinsicHeight;
+ childState.yTranslation = yPosition;
+ childState.hidden = false;
+ // When the group is expanded, the children cast the shadows rather than the parent
+ // so use the parent's elevation here.
+ childState.zTranslation =
+ (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications)
+ ? mContainingNotification.getTranslationZ()
+ : 0;
+ childState.dimmed = parentState.dimmed;
+ childState.dark = parentState.dark;
+ childState.hideSensitive = parentState.hideSensitive;
+ childState.belowSpeedBump = parentState.belowSpeedBump;
+ childState.clipTopAmount = 0;
+ childState.alpha = 0;
+ if (i < firstOverflowIndex) {
+ childState.alpha = showingAsLowPriority() ? expandFactor : 1.0f;
+ } else if (expandFactor == 1.0f && i <= lastVisibleIndex) {
+ childState.alpha = (mActualHeight - childState.yTranslation) / childState.height;
+ childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha));
+ }
+ childState.location = parentState.location;
+ childState.inShelf = parentState.inShelf;
+ yPosition += intrinsicHeight;
+
+ }
+ if (mOverflowNumber != null) {
+ ExpandableNotificationRow overflowView = mChildren.get(Math.min(
+ getMaxAllowedVisibleChildren(true /* likeCollpased */), childCount) - 1);
+ mGroupOverFlowState.copyFrom(resultState.getViewStateForView(overflowView));
+
+ if (mContainingNotification.isShowingAmbient() || !mChildrenExpanded) {
+ HybridNotificationView alignView = null;
+ if (mContainingNotification.isShowingAmbient()) {
+ alignView = overflowView.getAmbientSingleLineView();
+ } else if (mUserLocked) {
+ alignView = overflowView.getSingleLineView();
+ }
+ if (alignView != null) {
+ View mirrorView = alignView.getTextView();
+ if (mirrorView.getVisibility() == GONE) {
+ mirrorView = alignView.getTitleView();
+ }
+ if (mirrorView.getVisibility() == GONE) {
+ mirrorView = alignView;
+ }
+ mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset(
+ mirrorView, overflowView);
+ mGroupOverFlowState.alpha = mirrorView.getAlpha();
+ }
+ } else {
+ mGroupOverFlowState.yTranslation += mNotificationHeaderMargin;
+ mGroupOverFlowState.alpha = 0.0f;
+ }
+ }
+ if (mNotificationHeader != null) {
+ if (mHeaderViewState == null) {
+ mHeaderViewState = new ViewState();
+ }
+ mHeaderViewState.initFrom(mNotificationHeader);
+ mHeaderViewState.zTranslation = childrenExpandedAndNotAnimating
+ ? mContainingNotification.getTranslationZ()
+ : 0;
+ }
+ }
+
+ /**
+ * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
+ * height, children in the group after this are gone.
+ *
+ * @param child the child who's height to adjust.
+ * @param parentHeight the height of the parent.
+ * @param childState the state to update.
+ * @param yPosition the yPosition of the view.
+ * @return true if children after this one should be hidden.
+ */
+ private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
+ int parentHeight, ExpandableViewState childState, int yPosition) {
+ final int top = yPosition + child.getClipTopAmount();
+ final int intrinsicHeight = child.getIntrinsicHeight();
+ final int bottom = top + intrinsicHeight;
+ int newHeight = intrinsicHeight;
+ if (bottom >= parentHeight) {
+ // Child is either clipped or gone
+ newHeight = Math.max((parentHeight - top), 0);
+ }
+ childState.hidden = newHeight == 0;
+ childState.height = newHeight;
+ return childState.height != intrinsicHeight && !childState.hidden;
+ }
+
+ private int getMaxAllowedVisibleChildren() {
+ return getMaxAllowedVisibleChildren(false /* likeCollapsed */);
+ }
+
+ private int getMaxAllowedVisibleChildren(boolean likeCollapsed) {
+ if (mContainingNotification.isShowingAmbient()) {
+ return NUMBER_OF_CHILDREN_WHEN_AMBIENT;
+ }
+ if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked())) {
+ return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
+ }
+ if (mIsLowPriority || !mContainingNotification.isOnKeyguard()
+ && (mContainingNotification.isExpanded() || mContainingNotification.isHeadsUp())) {
+ return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
+ }
+ return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
+ }
+
+ public void applyState(StackScrollState state) {
+ int childCount = mChildren.size();
+ ViewState tmpState = new ViewState();
+ float expandFraction = 0.0f;
+ if (mUserLocked) {
+ expandFraction = getGroupExpandFraction();
+ }
+ final boolean dividersVisible = mUserLocked && !showingAsLowPriority()
+ || (mChildrenExpanded && mShowDividersWhenExpanded)
+ || (mContainingNotification.isGroupExpansionChanging()
+ && !mHideDividersDuringExpand);
+ for (int i = 0; i < childCount; i++) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ ExpandableViewState viewState = state.getViewStateForView(child);
+ viewState.applyToView(child);
+
+ // layout the divider
+ View divider = mDividers.get(i);
+ tmpState.initFrom(divider);
+ tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
+ float alpha = mChildrenExpanded && viewState.alpha != 0 ? mDividerAlpha : 0;
+ if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) {
+ alpha = NotificationUtils.interpolate(0, 0.5f,
+ Math.min(viewState.alpha, expandFraction));
+ }
+ tmpState.hidden = !dividersVisible;
+ tmpState.alpha = alpha;
+ tmpState.applyToView(divider);
+ // There is no fake shadow to be drawn on the children
+ child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
+ }
+ if (mGroupOverFlowState != null) {
+ mGroupOverFlowState.applyToView(mOverflowNumber);
+ mNeverAppliedGroupState = false;
+ }
+ if (mHeaderViewState != null) {
+ mHeaderViewState.applyToView(mNotificationHeader);
+ }
+ updateChildrenClipping();
+ }
+
+ private void updateChildrenClipping() {
+ int childCount = mChildren.size();
+ int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount;
+ for (int i = 0; i < childCount; i++) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ float childTop = child.getTranslationY();
+ float childBottom = childTop + child.getActualHeight();
+ boolean visible = true;
+ int clipBottomAmount = 0;
+ if (childTop > layoutEnd) {
+ visible = false;
+ } else if (childBottom > layoutEnd) {
+ clipBottomAmount = (int) (childBottom - layoutEnd);
+ }
+
+ boolean isVisible = child.getVisibility() == VISIBLE;
+ if (visible != isVisible) {
+ child.setVisibility(visible ? VISIBLE : INVISIBLE);
+ }
+
+ child.setClipBottomAmount(clipBottomAmount);
+ }
+ }
+
+ /**
+ * This is called when the children expansion has changed and positions the children properly
+ * for an appear animation.
+ *
+ * @param state the new state we animate to
+ */
+ public void prepareExpansionChanged(StackScrollState state) {
+ // TODO: do something that makes sense, like placing the invisible views correctly
+ return;
+ }
+
+ public void startAnimationToState(StackScrollState state, AnimationProperties properties) {
+ int childCount = mChildren.size();
+ ViewState tmpState = new ViewState();
+ float expandFraction = getGroupExpandFraction();
+ final boolean dividersVisible = mUserLocked && !showingAsLowPriority()
+ || (mChildrenExpanded && mShowDividersWhenExpanded)
+ || (mContainingNotification.isGroupExpansionChanging()
+ && !mHideDividersDuringExpand);
+ for (int i = childCount - 1; i >= 0; i--) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ ExpandableViewState viewState = state.getViewStateForView(child);
+ viewState.animateTo(child, properties);
+
+ // layout the divider
+ View divider = mDividers.get(i);
+ tmpState.initFrom(divider);
+ tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
+ float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
+ if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) {
+ alpha = NotificationUtils.interpolate(0, 0.5f,
+ Math.min(viewState.alpha, expandFraction));
+ }
+ tmpState.hidden = !dividersVisible;
+ tmpState.alpha = alpha;
+ tmpState.animateTo(divider, properties);
+ // There is no fake shadow to be drawn on the children
+ child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
+ }
+ if (mOverflowNumber != null) {
+ if (mNeverAppliedGroupState) {
+ float alpha = mGroupOverFlowState.alpha;
+ mGroupOverFlowState.alpha = 0;
+ mGroupOverFlowState.applyToView(mOverflowNumber);
+ mGroupOverFlowState.alpha = alpha;
+ mNeverAppliedGroupState = false;
+ }
+ mGroupOverFlowState.animateTo(mOverflowNumber, properties);
+ }
+ if (mNotificationHeader != null) {
+ mHeaderViewState.applyToView(mNotificationHeader);
+ }
+ updateChildrenClipping();
+ }
+
+ public ExpandableNotificationRow getViewAtPosition(float y) {
+ // find the view under the pointer, accounting for GONE views
+ final int count = mChildren.size();
+ for (int childIdx = 0; childIdx < count; childIdx++) {
+ ExpandableNotificationRow slidingChild = mChildren.get(childIdx);
+ float childTop = slidingChild.getTranslationY();
+ float top = childTop + slidingChild.getClipTopAmount();
+ float bottom = childTop + slidingChild.getActualHeight();
+ if (y >= top && y <= bottom) {
+ return slidingChild;
+ }
+ }
+ return null;
+ }
+
+ public void setChildrenExpanded(boolean childrenExpanded) {
+ mChildrenExpanded = childrenExpanded;
+ updateExpansionStates();
+ if (mNotificationHeader != null) {
+ mNotificationHeader.setExpanded(childrenExpanded);
+ }
+ final int count = mChildren.size();
+ for (int childIdx = 0; childIdx < count; childIdx++) {
+ ExpandableNotificationRow child = mChildren.get(childIdx);
+ child.setChildrenExpanded(childrenExpanded, false);
+ }
+ }
+
+ public void setContainingNotification(ExpandableNotificationRow parent) {
+ mContainingNotification = parent;
+ mHeaderUtil = new NotificationHeaderUtil(mContainingNotification);
+ }
+
+ public ExpandableNotificationRow getContainingNotification() {
+ return mContainingNotification;
+ }
+
+ public NotificationHeaderView getHeaderView() {
+ return mNotificationHeader;
+ }
+
+ public NotificationHeaderView getLowPriorityHeaderView() {
+ return mNotificationHeaderLowPriority;
+ }
+
+ @VisibleForTesting
+ public ViewGroup getCurrentHeaderView() {
+ return mCurrentHeader;
+ }
+
+ public void notifyShowAmbientChanged() {
+ updateHeaderVisibility(false);
+ }
+
+ private void updateHeaderVisibility(boolean animate) {
+ ViewGroup desiredHeader;
+ ViewGroup currentHeader = mCurrentHeader;
+ desiredHeader = calculateDesiredHeader();
+
+ if (currentHeader == desiredHeader) {
+ return;
+ }
+ if (desiredHeader == mNotificationHeaderAmbient
+ || currentHeader == mNotificationHeaderAmbient) {
+ animate = false;
+ }
+
+ if (animate) {
+ if (desiredHeader != null && currentHeader != null) {
+ currentHeader.setVisibility(VISIBLE);
+ desiredHeader.setVisibility(VISIBLE);
+ NotificationViewWrapper visibleWrapper = getWrapperForView(desiredHeader);
+ NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader);
+ visibleWrapper.transformFrom(hiddenWrapper);
+ hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false));
+ startChildAlphaAnimations(desiredHeader == mNotificationHeader);
+ } else {
+ animate = false;
+ }
+ }
+ if (!animate) {
+ if (desiredHeader != null) {
+ getWrapperForView(desiredHeader).setVisible(true);
+ desiredHeader.setVisibility(VISIBLE);
+ }
+ if (currentHeader != null) {
+ // Wrapper can be null if we were a low priority notification
+ // and just destroyed it by calling setIsLowPriority(false)
+ NotificationViewWrapper wrapper = getWrapperForView(currentHeader);
+ if (wrapper != null) {
+ wrapper.setVisible(false);
+ }
+ currentHeader.setVisibility(INVISIBLE);
+ }
+ }
+
+ resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader);
+ resetHeaderVisibilityIfNeeded(mNotificationHeaderAmbient, desiredHeader);
+ resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader);
+
+ mCurrentHeader = desiredHeader;
+ }
+
+ private void resetHeaderVisibilityIfNeeded(View header, View desiredHeader) {
+ if (header == null) {
+ return;
+ }
+ if (header != mCurrentHeader && header != desiredHeader) {
+ getWrapperForView(header).setVisible(false);
+ header.setVisibility(INVISIBLE);
+ }
+ if (header == desiredHeader && header.getVisibility() != VISIBLE) {
+ getWrapperForView(header).setVisible(true);
+ header.setVisibility(VISIBLE);
+ }
+ }
+
+ private ViewGroup calculateDesiredHeader() {
+ ViewGroup desiredHeader;
+ if (mContainingNotification.isShowingAmbient()) {
+ desiredHeader = mNotificationHeaderAmbient;
+ } else if (showingAsLowPriority()) {
+ desiredHeader = mNotificationHeaderLowPriority;
+ } else {
+ desiredHeader = mNotificationHeader;
+ }
+ return desiredHeader;
+ }
+
+ private void startChildAlphaAnimations(boolean toVisible) {
+ float target = toVisible ? 1.0f : 0.0f;
+ float start = 1.0f - target;
+ int childCount = mChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) {
+ break;
+ }
+ ExpandableNotificationRow child = mChildren.get(i);
+ child.setAlpha(start);
+ ViewState viewState = new ViewState();
+ viewState.initFrom(child);
+ viewState.alpha = target;
+ ALPHA_FADE_IN.setDelay(i * 50);
+ viewState.animateTo(child, ALPHA_FADE_IN);
+ }
+ }
+
+
+ private void updateHeaderTransformation() {
+ if (mUserLocked && showingAsLowPriority()) {
+ float fraction = getGroupExpandFraction();
+ mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority,
+ fraction);
+ mNotificationHeader.setVisibility(VISIBLE);
+ mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper,
+ fraction);
+ }
+
+ }
+
+ private NotificationViewWrapper getWrapperForView(View visibleHeader) {
+ if (visibleHeader == mNotificationHeader) {
+ return mNotificationHeaderWrapper;
+ }
+ if (visibleHeader == mNotificationHeaderAmbient) {
+ return mNotificationHeaderWrapperAmbient;
+ }
+ return mNotificationHeaderWrapperLowPriority;
+ }
+
+ /**
+ * Called when a groups expansion changes to adjust the background of the header view.
+ *
+ * @param expanded whether the group is expanded.
+ */
+ public void updateHeaderForExpansion(boolean expanded) {
+ if (mNotificationHeader != null) {
+ if (expanded) {
+ ColorDrawable cd = new ColorDrawable();
+ cd.setColor(mContainingNotification.calculateBgColor());
+ mNotificationHeader.setHeaderBackgroundDrawable(cd);
+ } else {
+ mNotificationHeader.setHeaderBackgroundDrawable(null);
+ }
+ }
+ }
+
+ public int getMaxContentHeight() {
+ if (showingAsLowPriority()) {
+ return getMinHeight(NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED, true
+ /* likeHighPriority */);
+ }
+ int maxContentHeight = mNotificationHeaderMargin + mNotificatonTopPadding;
+ int visibleChildren = 0;
+ int childCount = mChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) {
+ break;
+ }
+ ExpandableNotificationRow child = mChildren.get(i);
+ float childHeight = child.isExpanded(true /* allowOnKeyguard */)
+ ? child.getMaxExpandHeight()
+ : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
+ maxContentHeight += childHeight;
+ visibleChildren++;
+ }
+ if (visibleChildren > 0) {
+ maxContentHeight += visibleChildren * mDividerHeight;
+ }
+ return maxContentHeight;
+ }
+
+ public void setActualHeight(int actualHeight) {
+ if (!mUserLocked) {
+ return;
+ }
+ mActualHeight = actualHeight;
+ float fraction = getGroupExpandFraction();
+ boolean showingLowPriority = showingAsLowPriority();
+ updateHeaderTransformation();
+ int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
+ int childCount = mChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ float childHeight;
+ if (showingLowPriority) {
+ childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */);
+ } else if (child.isExpanded(true /* allowOnKeyguard */)) {
+ childHeight = child.getMaxExpandHeight();
+ } else {
+ childHeight = child.getShowingLayout().getMinHeight(
+ true /* likeGroupExpanded */);
+ }
+ if (i < maxAllowedVisibleChildren) {
+ float singleLineHeight = child.getShowingLayout().getMinHeight(
+ false /* likeGroupExpanded */);
+ child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight,
+ childHeight, fraction), false);
+ } else {
+ child.setActualHeight((int) childHeight, false);
+ }
+ }
+ }
+
+ public float getGroupExpandFraction() {
+ int visibleChildrenExpandedHeight = showingAsLowPriority() ? getMaxContentHeight()
+ : getVisibleChildrenExpandHeight();
+ int minExpandHeight = getCollapsedHeight();
+ float factor = (mActualHeight - minExpandHeight)
+ / (float) (visibleChildrenExpandedHeight - minExpandHeight);
+ return Math.max(0.0f, Math.min(1.0f, factor));
+ }
+
+ private int getVisibleChildrenExpandHeight() {
+ int intrinsicHeight = mNotificationHeaderMargin + mNotificatonTopPadding + mDividerHeight;
+ int visibleChildren = 0;
+ int childCount = mChildren.size();
+ int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
+ for (int i = 0; i < childCount; i++) {
+ if (visibleChildren >= maxAllowedVisibleChildren) {
+ break;
+ }
+ ExpandableNotificationRow child = mChildren.get(i);
+ float childHeight = child.isExpanded(true /* allowOnKeyguard */)
+ ? child.getMaxExpandHeight()
+ : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
+ intrinsicHeight += childHeight;
+ visibleChildren++;
+ }
+ return intrinsicHeight;
+ }
+
+ public int getMinHeight() {
+ return getMinHeight(mContainingNotification.isShowingAmbient()
+ ? NUMBER_OF_CHILDREN_WHEN_AMBIENT
+ : NUMBER_OF_CHILDREN_WHEN_COLLAPSED, false /* likeHighPriority */);
+ }
+
+ public int getCollapsedHeight() {
+ return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */),
+ false /* likeHighPriority */);
+ }
+
+ /**
+ * Get the minimum Height for this group.
+ *
+ * @param maxAllowedVisibleChildren the number of children that should be visible
+ * @param likeHighPriority if the height should be calculated as if it were not low priority
+ */
+ private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) {
+ if (!likeHighPriority && showingAsLowPriority()) {
+ return mNotificationHeaderLowPriority.getHeight();
+ }
+ int minExpandHeight = mNotificationHeaderMargin;
+ int visibleChildren = 0;
+ boolean firstChild = true;
+ int childCount = mChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ if (visibleChildren >= maxAllowedVisibleChildren) {
+ break;
+ }
+ if (!firstChild) {
+ minExpandHeight += mChildPadding;
+ } else {
+ firstChild = false;
+ }
+ ExpandableNotificationRow child = mChildren.get(i);
+ minExpandHeight += child.getSingleLineView().getHeight();
+ visibleChildren++;
+ }
+ minExpandHeight += mCollapsedBottompadding;
+ return minExpandHeight;
+ }
+
+ public boolean showingAsLowPriority() {
+ return mIsLowPriority && !mContainingNotification.isExpanded();
+ }
+
+ public void setDark(boolean dark, boolean fade, long delay) {
+ if (mOverflowNumber != null) {
+ mHybridGroupManager.setOverflowNumberDark(mOverflowNumber, dark, fade, delay);
+ }
+ mNotificationHeaderWrapper.setDark(dark, fade, delay);
+ }
+
+ public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
+ if (mNotificationHeader != null) {
+ removeView(mNotificationHeader);
+ mNotificationHeader = null;
+ }
+ if (mNotificationHeaderLowPriority != null) {
+ removeView(mNotificationHeaderLowPriority);
+ mNotificationHeaderLowPriority = null;
+ }
+ if (mNotificationHeaderAmbient != null) {
+ removeView(mNotificationHeaderAmbient);
+ mNotificationHeaderAmbient = null;
+ }
+ recreateNotificationHeader(listener);
+ initDimens();
+ for (int i = 0; i < mDividers.size(); i++) {
+ View prevDivider = mDividers.get(i);
+ int index = indexOfChild(prevDivider);
+ removeView(prevDivider);
+ View divider = inflateDivider();
+ addView(divider, index);
+ mDividers.set(i, divider);
+ }
+ removeView(mOverflowNumber);
+ mOverflowNumber = null;
+ mGroupOverFlowState = null;
+ updateGroupOverflow();
+ }
+
+ public void setUserLocked(boolean userLocked) {
+ mUserLocked = userLocked;
+ if (!mUserLocked) {
+ updateHeaderVisibility(false /* animate */);
+ }
+ int childCount = mChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ child.setUserLocked(userLocked && !showingAsLowPriority());
+ }
+ }
+
+ public void onNotificationUpdated() {
+ mHybridGroupManager.setOverflowNumberColor(mOverflowNumber,
+ mContainingNotification.getNotificationColor(),
+ mContainingNotification.getNotificationColorAmbient());
+ }
+
+ public int getPositionInLinearLayout(View childInGroup) {
+ int position = mNotificationHeaderMargin + mNotificatonTopPadding;
+
+ for (int i = 0; i < mChildren.size(); i++) {
+ ExpandableNotificationRow child = mChildren.get(i);
+ boolean notGone = child.getVisibility() != View.GONE;
+ if (notGone) {
+ position += mDividerHeight;
+ }
+ if (child == childInGroup) {
+ return position;
+ }
+ if (notGone) {
+ position += child.getIntrinsicHeight();
+ }
+ }
+ return 0;
+ }
+
+ public void setIconsVisible(boolean iconsVisible) {
+ if (mNotificationHeaderWrapper != null) {
+ NotificationHeaderView header = mNotificationHeaderWrapper.getNotificationHeader();
+ if (header != null) {
+ header.getIcon().setForceHidden(!iconsVisible);
+ }
+ }
+ if (mNotificationHeaderWrapperLowPriority != null) {
+ NotificationHeaderView header
+ = mNotificationHeaderWrapperLowPriority.getNotificationHeader();
+ if (header != null) {
+ header.getIcon().setForceHidden(!iconsVisible);
+ }
+ }
+ }
+
+ public void setClipBottomAmount(int clipBottomAmount) {
+ mClipBottomAmount = clipBottomAmount;
+ updateChildrenClipping();
+ }
+
+ public void setIsLowPriority(boolean isLowPriority) {
+ mIsLowPriority = isLowPriority;
+ if (mContainingNotification != null) { /* we're not yet set up yet otherwise */
+ recreateLowPriorityHeader(null /* existingBuilder */);
+ updateHeaderVisibility(false /* animate */);
+ }
+ if (mUserLocked) {
+ setUserLocked(mUserLocked);
+ }
+ }
+
+ public NotificationHeaderView getVisibleHeader() {
+ NotificationHeaderView header = mNotificationHeader;
+ if (showingAsLowPriority()) {
+ header = mNotificationHeaderLowPriority;
+ }
+ return header;
+ }
+
+ public void onExpansionChanged() {
+ if (mIsLowPriority) {
+ if (mUserLocked) {
+ setUserLocked(mUserLocked);
+ }
+ updateHeaderVisibility(true /* animate */);
+ }
+ }
+
+ public float getIncreasedPaddingAmount() {
+ if (showingAsLowPriority()) {
+ return 0.0f;
+ }
+ return getGroupExpandFraction();
+ }
+
+ @VisibleForTesting
+ public boolean isUserLocked() {
+ return mUserLocked;
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
new file mode 100644
index 0000000..75532d9
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -0,0 +1,4886 @@
+/*
+ * 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.stack;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeAnimator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.FloatRange;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.service.notification.StatusBarNotification;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Property;
+import android.view.ContextThemeWrapper;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.OverScroller;
+import android.widget.ScrollView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settingslib.Utils;
+import com.android.systemui.ExpandHelper;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.statusbar.ActivatableNotificationView;
+import com.android.systemui.statusbar.DismissView;
+import com.android.systemui.statusbar.EmptyShadeView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationGuts;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.NotificationSnooze;
+import com.android.systemui.statusbar.StackScrollerDecorView;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.FakeShadowView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.phone.ScrimController;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.ScrollAdapter;
+
+import android.support.v4.graphics.ColorUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
+ */
+public class NotificationStackScrollLayout extends ViewGroup
+ implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
+ ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener,
+ NotificationMenuRowPlugin.OnMenuEventListener, ScrollContainer,
+ VisibilityLocationProvider {
+
+ public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
+ private static final String TAG = "StackScroller";
+ private static final boolean DEBUG = false;
+ private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
+ private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
+ private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
+ /**
+ * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ private ExpandHelper mExpandHelper;
+ private NotificationSwipeHelper mSwipeHelper;
+ private boolean mSwipingInProgress;
+ private int mCurrentStackHeight = Integer.MAX_VALUE;
+ private final Paint mBackgroundPaint = new Paint();
+ private final boolean mShouldDrawNotificationBackground;
+
+ private float mExpandedHeight;
+ private int mOwnScrollY;
+ private int mMaxLayoutHeight;
+
+ private VelocityTracker mVelocityTracker;
+ private OverScroller mScroller;
+ private Runnable mFinishScrollingCallback;
+ private int mTouchSlop;
+ private int mMinimumVelocity;
+ private int mMaximumVelocity;
+ private int mOverflingDistance;
+ private float mMaxOverScroll;
+ private boolean mIsBeingDragged;
+ private int mLastMotionY;
+ private int mDownX;
+ private int mActivePointerId = INVALID_POINTER;
+ private boolean mTouchIsClick;
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+
+ private Paint mDebugPaint;
+ private int mContentHeight;
+ private int mCollapsedSize;
+ private int mPaddingBetweenElements;
+ private int mIncreasedPaddingBetweenElements;
+ private int mTopPadding;
+ private int mBottomMargin;
+ private int mBottomInset = 0;
+
+ /**
+ * The algorithm which calculates the properties for our children
+ */
+ protected final StackScrollAlgorithm mStackScrollAlgorithm;
+
+ /**
+ * The current State this Layout is in
+ */
+ private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
+ private final AmbientState mAmbientState;
+ private NotificationGroupManager mGroupManager;
+ private HashSet<View> mChildrenToAddAnimated = new HashSet<>();
+ private ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>();
+ private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<>();
+ private ArrayList<View> mSnappedBackChildren = new ArrayList<>();
+ private ArrayList<View> mDragAnimPendingChildren = new ArrayList<>();
+ private ArrayList<View> mChildrenChangingPositions = new ArrayList<>();
+ private HashSet<View> mFromMoreCardAdditions = new HashSet<>();
+ private ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>();
+ private ArrayList<View> mSwipedOutViews = new ArrayList<>();
+ private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
+ private boolean mAnimationsEnabled;
+ private boolean mChangePositionInProgress;
+ private boolean mChildTransferInProgress;
+
+ /**
+ * The raw amount of the overScroll on the top, which is not rubber-banded.
+ */
+ private float mOverScrolledTopPixels;
+
+ /**
+ * The raw amount of the overScroll on the bottom, which is not rubber-banded.
+ */
+ private float mOverScrolledBottomPixels;
+ 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;
+ private boolean mChildrenUpdateRequested;
+ private boolean mIsExpansionChanging;
+ private boolean mPanelTracking;
+ private boolean mExpandingNotification;
+ private boolean mExpandedInThisMotion;
+ protected boolean mScrollingEnabled;
+ protected DismissView mDismissView;
+ protected EmptyShadeView mEmptyShadeView;
+ private boolean mDismissAllInProgress;
+ private boolean mFadeNotificationsOnDismiss;
+
+ /**
+ * Was the scroller scrolled to the top when the down motion was observed?
+ */
+ private boolean mScrolledToTopOnFirstDown;
+ /**
+ * The minimal amount of over scroll which is needed in order to switch to the quick settings
+ * when over scrolling on a expanded card.
+ */
+ private float mMinTopOverScrollToEscape;
+ private int mIntrinsicPadding;
+ private float mStackTranslation;
+ private float mTopPaddingOverflow;
+ private boolean mDontReportNextOverScroll;
+ private boolean mDontClampNextScroll;
+ private boolean mNeedViewResizeAnimation;
+ private View mExpandedGroupView;
+ private boolean mEverythingNeedsAnimation;
+
+ /**
+ * The maximum scrollPosition which we are allowed to reach when a notification was expanded.
+ * This is needed to avoid scrolling too far after the notification was collapsed in the same
+ * motion.
+ */
+ private int mMaxScrollAfterExpand;
+ private SwipeHelper.LongPressListener mLongPressListener;
+
+ private NotificationMenuRowPlugin mCurrMenuRow;
+ private View mTranslatingParentView;
+ private View mMenuExposedView;
+ boolean mCheckForLeavebehind;
+
+ /**
+ * Should in this touch motion only be scrolling allowed? It's true when the scroller was
+ * animating.
+ */
+ private boolean mOnlyScrollingInThisMotion;
+ private boolean mDisallowDismissInThisMotion;
+ private boolean mInterceptDelegateEnabled;
+ private boolean mDelegateToScrollView;
+ private boolean mDisallowScrollingInThisMotion;
+ private long mGoToFullShadeDelay;
+ private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
+ = new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ updateForcedScroll();
+ updateChildren();
+ mChildrenUpdateRequested = false;
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ return true;
+ }
+ };
+ private StatusBar mStatusBar;
+ private int[] mTempInt2 = new int[2];
+ private boolean mGenerateChildOrderChangedEvent;
+ private HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>();
+ private HashSet<View> mClearOverlayViewsWhenFinished = new HashSet<>();
+ private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations
+ = new HashSet<>();
+ private HeadsUpManager mHeadsUpManager;
+ private boolean mTrackingHeadsUp;
+ private ScrimController mScrimController;
+ private boolean mForceNoOverlappingRendering;
+ private final ArrayList<Pair<ExpandableNotificationRow, Boolean>> mTmpList = new ArrayList<>();
+ private FalsingManager mFalsingManager;
+ private boolean mAnimationRunning;
+ private ViewTreeObserver.OnPreDrawListener mRunningAnimationUpdater
+ = new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ onPreDrawDuringAnimation();
+ return true;
+ }
+ };
+ private Rect mBackgroundBounds = new Rect();
+ private Rect mStartAnimationRect = new Rect();
+ private Rect mEndAnimationRect = new Rect();
+ private Rect mCurrentBounds = new Rect(-1, -1, -1, -1);
+ private boolean mAnimateNextBackgroundBottom;
+ private boolean mAnimateNextBackgroundTop;
+ private ObjectAnimator mBottomAnimator = null;
+ private ObjectAnimator mTopAnimator = null;
+ private ActivatableNotificationView mFirstVisibleBackgroundChild = null;
+ private ActivatableNotificationView mLastVisibleBackgroundChild = null;
+ private int mBgColor;
+ private float mDimAmount;
+ private ValueAnimator mDimAnimator;
+ private ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
+ private Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mDimAnimator = null;
+ }
+ };
+ private ValueAnimator.AnimatorUpdateListener mDimUpdateListener
+ = new ValueAnimator.AnimatorUpdateListener() {
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setDimAmount((Float) animation.getAnimatedValue());
+ }
+ };
+ protected ViewGroup mQsContainer;
+ private boolean mContinuousShadowUpdate;
+ private ViewTreeObserver.OnPreDrawListener mShadowUpdater
+ = new ViewTreeObserver.OnPreDrawListener() {
+
+ @Override
+ public boolean onPreDraw() {
+ updateViewShadows();
+ return true;
+ }
+ };
+ private Comparator<ExpandableView> mViewPositionComparator = new Comparator<ExpandableView>() {
+ @Override
+ public int compare(ExpandableView view, ExpandableView otherView) {
+ float endY = view.getTranslationY() + view.getActualHeight();
+ float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
+ if (endY < otherEndY) {
+ return -1;
+ } else if (endY > otherEndY) {
+ return 1;
+ } else {
+ // The two notifications end at the same location
+ return 0;
+ }
+ }
+ };
+ private PorterDuffXfermode mSrcMode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
+ private Collection<HeadsUpManager.HeadsUpEntry> mPulsing;
+ private boolean mDrawBackgroundAsSrc;
+ private boolean mFadingOut;
+ private boolean mParentNotFullyVisible;
+ private boolean mGroupExpandedForMeasure;
+ private boolean mScrollable;
+ private View mForcedScroll;
+ private float mBackgroundFadeAmount = 1.0f;
+ private static final Property<NotificationStackScrollLayout, Float> BACKGROUND_FADE =
+ new FloatProperty<NotificationStackScrollLayout>("backgroundFade") {
+ @Override
+ public void setValue(NotificationStackScrollLayout object, float value) {
+ object.setBackgroundFadeAmount(value);
+ }
+
+ @Override
+ public Float get(NotificationStackScrollLayout object) {
+ return object.getBackgroundFadeAmount();
+ }
+ };
+ private boolean mUsingLightTheme;
+ private boolean mQsExpanded;
+ private boolean mForwardScrollable;
+ private boolean mBackwardScrollable;
+ private NotificationShelf mShelf;
+ private int mMaxDisplayedNotifications = -1;
+ private int mStatusBarHeight;
+ private int mMinInteractionHeight;
+ private boolean mNoAmbient;
+ private final Rect mClipRect = new Rect();
+ private boolean mIsClipped;
+ private Rect mRequestedClipBounds;
+ private boolean mInHeadsUpPinnedMode;
+ private boolean mHeadsUpAnimatingAway;
+ private int mStatusBarState;
+ private int mCachedBackgroundColor;
+ private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
+ private Runnable mAnimateScroll = this::animateScroll;
+
+ public NotificationStackScrollLayout(Context context) {
+ this(context, null);
+ }
+
+ public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ Resources res = getResources();
+
+ mAmbientState = new AmbientState(context);
+ mBgColor = context.getColor(R.color.notification_shade_background_color);
+ int minHeight = res.getDimensionPixelSize(R.dimen.notification_min_height);
+ int maxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height);
+ mExpandHelper = new ExpandHelper(getContext(), this,
+ minHeight, maxHeight);
+ mExpandHelper.setEventSource(this);
+ mExpandHelper.setScrollAdapter(this);
+ mSwipeHelper = new NotificationSwipeHelper(SwipeHelper.X, this, getContext());
+ mSwipeHelper.setLongPressListener(mLongPressListener);
+ mStackScrollAlgorithm = createStackScrollAlgorithm(context);
+ initView(context);
+ mFalsingManager = FalsingManager.getInstance(context);
+ mShouldDrawNotificationBackground =
+ res.getBoolean(R.bool.config_drawNotificationBackground);
+ mFadeNotificationsOnDismiss =
+ res.getBoolean(R.bool.config_fadeNotificationsOnDismiss);
+
+ updateWillNotDraw();
+ if (DEBUG) {
+ mDebugPaint = new Paint();
+ mDebugPaint.setColor(0xffff0000);
+ mDebugPaint.setStrokeWidth(2);
+ mDebugPaint.setStyle(Paint.Style.STROKE);
+ }
+ }
+
+ public NotificationSwipeActionHelper getSwipeActionHelper() {
+ return mSwipeHelper;
+ }
+
+ @Override
+ public void onMenuClicked(View view, int x, int y, MenuItem item) {
+ if (mLongPressListener == null) {
+ return;
+ }
+ if (view instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ MetricsLogger.action(mContext, MetricsEvent.ACTION_TOUCH_GEAR,
+ row.getStatusBarNotification().getPackageName());
+ }
+ mLongPressListener.onLongPress(view, x, y, item);
+ }
+
+ @Override
+ public void onMenuReset(View row) {
+ if (mTranslatingParentView != null && row == mTranslatingParentView) {
+ mMenuExposedView = null;
+ mTranslatingParentView = null;
+ }
+ }
+
+ @Override
+ public void onMenuShown(View row) {
+ mMenuExposedView = mTranslatingParentView;
+ if (row instanceof ExpandableNotificationRow) {
+ MetricsLogger.action(mContext, MetricsEvent.ACTION_REVEAL_GEAR,
+ ((ExpandableNotificationRow) row).getStatusBarNotification()
+ .getPackageName());
+ }
+ mSwipeHelper.onMenuShown(row);
+ }
+
+ protected void onDraw(Canvas canvas) {
+ if (mShouldDrawNotificationBackground && !mAmbientState.isDark()
+ && mCurrentBounds.top < mCurrentBounds.bottom) {
+ canvas.drawRect(0, mCurrentBounds.top, getWidth(), mCurrentBounds.bottom,
+ mBackgroundPaint);
+ }
+
+ if (DEBUG) {
+ int y = mTopPadding;
+ canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+ y = getLayoutHeight();
+ canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+ y = getHeight() - getEmptyBottomMargin();
+ canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+ }
+ }
+
+ private void updateBackgroundDimming() {
+ // No need to update the background color if it's not being drawn.
+ if (!mShouldDrawNotificationBackground) {
+ return;
+ }
+
+ float alpha = BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
+ alpha *= mBackgroundFadeAmount;
+ // We need to manually blend in the background color
+ int scrimColor = mScrimController.getBackgroundColor();
+ int color = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
+ if (mCachedBackgroundColor != color) {
+ mCachedBackgroundColor = color;
+ mBackgroundPaint.setColor(color);
+ invalidate();
+ }
+ }
+
+ private void initView(Context context) {
+ mScroller = new OverScroller(getContext());
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setClipChildren(false);
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mOverflingDistance = configuration.getScaledOverflingDistance();
+
+ Resources res = context.getResources();
+ mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
+ mStackScrollAlgorithm.initView(context);
+ mAmbientState.reload(context);
+ mPaddingBetweenElements = Math.max(1,
+ res.getDimensionPixelSize(R.dimen.notification_divider_height));
+ mIncreasedPaddingBetweenElements =
+ res.getDimensionPixelSize(R.dimen.notification_divider_height_increased);
+ mMinTopOverScrollToEscape = res.getDimensionPixelSize(
+ R.dimen.min_top_overscroll_to_qs);
+ mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height);
+ mBottomMargin = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom);
+ mMinInteractionHeight = res.getDimensionPixelSize(
+ R.dimen.notification_min_interaction_height);
+ }
+
+ public void setDrawBackgroundAsSrc(boolean asSrc) {
+ mDrawBackgroundAsSrc = asSrc;
+ updateSrcDrawing();
+ }
+
+ private void updateSrcDrawing() {
+ if (!mShouldDrawNotificationBackground) {
+ return;
+ }
+
+ mBackgroundPaint.setXfermode(mDrawBackgroundAsSrc && !mFadingOut && !mParentNotFullyVisible
+ ? mSrcMode : null);
+ invalidate();
+ }
+
+ private void notifyHeightChangeListener(ExpandableView view) {
+ if (mOnHeightChangedListener != null) {
+ mOnHeightChangedListener.onHeightChanged(view, false /* needsAnimation */);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ // We need to measure all children even the GONE ones, such that the heights are calculated
+ // correctly as they are used to calculate how many we can fit on the screen.
+ final int size = getChildCount();
+ for (int i = 0; i < size; i++) {
+ measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // we layout all our children centered on the top
+ float centerX = getWidth() / 2.0f;
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ // We need to layout all children even the GONE ones, such that the heights are
+ // calculated correctly as they are used to calculate how many we can fit on the screen
+ float width = child.getMeasuredWidth();
+ float height = child.getMeasuredHeight();
+ child.layout((int) (centerX - width / 2.0f),
+ 0,
+ (int) (centerX + width / 2.0f),
+ (int) height);
+ }
+ setMaxLayoutHeight(getHeight());
+ updateContentHeight();
+ clampScrollPosition();
+ requestChildrenUpdate();
+ updateFirstAndLastBackgroundViews();
+ updateAlgorithmLayoutMinHeight();
+ }
+
+ private void requestAnimationOnViewResize(ExpandableNotificationRow row) {
+ if (mAnimationsEnabled && (mIsExpanded || row != null && row.isPinned())) {
+ mNeedViewResizeAnimation = true;
+ mNeedsAnimation = true;
+ }
+ }
+
+ public void updateSpeedBumpIndex(int newIndex, boolean noAmbient) {
+ mAmbientState.setSpeedBumpIndex(newIndex);
+ mNoAmbient = noAmbient;
+ }
+
+ public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public boolean isInVisibleLocation(ExpandableNotificationRow row) {
+ ExpandableViewState childViewState = mCurrentStackScrollState.getViewStateForView(row);
+ if (childViewState == null) {
+ return false;
+ }
+ if ((childViewState.location &= ExpandableViewState.VISIBLE_LOCATIONS) == 0) {
+ return false;
+ }
+ if (row.getVisibility() != View.VISIBLE) {
+ return false;
+ }
+ return true;
+ }
+
+ private void setMaxLayoutHeight(int maxLayoutHeight) {
+ mMaxLayoutHeight = maxLayoutHeight;
+ mShelf.setMaxLayoutHeight(maxLayoutHeight);
+ updateAlgorithmHeightAndPadding();
+ }
+
+ private void updateAlgorithmHeightAndPadding() {
+ mAmbientState.setLayoutHeight(getLayoutHeight());
+ updateAlgorithmLayoutMinHeight();
+ mAmbientState.setTopPadding(mTopPadding);
+ }
+
+ private void updateAlgorithmLayoutMinHeight() {
+ mAmbientState.setLayoutMinHeight(mQsExpanded && !onKeyguard() ? getLayoutMinHeight() : 0);
+ }
+
+ /**
+ * Updates the children views according to the stack scroll algorithm. Call this whenever
+ * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
+ */
+ private void updateChildren() {
+ updateScrollStateForAddedChildren();
+ mAmbientState.setCurrentScrollVelocity(mScroller.isFinished()
+ ? 0
+ : mScroller.getCurrVelocity());
+ mAmbientState.setScrollY(mOwnScrollY);
+ mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
+ if (!isCurrentlyAnimating() && !mNeedsAnimation) {
+ applyCurrentState();
+ } else {
+ startAnimationToState();
+ }
+ }
+
+ private void onPreDrawDuringAnimation() {
+ mShelf.updateAppearance();
+ if (!mNeedsAnimation && !mChildrenUpdateRequested) {
+ updateBackground();
+ }
+ }
+
+ private void updateScrollStateForAddedChildren() {
+ if (mChildrenToAddAnimated.isEmpty()) {
+ return;
+ }
+ for (int i = 0; i < getChildCount(); i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (mChildrenToAddAnimated.contains(child)) {
+ int startingPosition = getPositionInLinearLayout(child);
+ float increasedPaddingAmount = child.getIncreasedPaddingAmount();
+ int padding = increasedPaddingAmount == 1.0f ? mIncreasedPaddingBetweenElements
+ : increasedPaddingAmount == -1.0f ? 0 : mPaddingBetweenElements;
+ int childHeight = getIntrinsicHeight(child) + padding;
+ if (startingPosition < mOwnScrollY) {
+ // This child starts off screen, so let's keep it offscreen to keep the others visible
+
+ setOwnScrollY(mOwnScrollY + childHeight);
+ }
+ }
+ }
+ clampScrollPosition();
+ }
+
+ private void updateForcedScroll() {
+ if (mForcedScroll != null && (!mForcedScroll.hasFocus()
+ || !mForcedScroll.isAttachedToWindow())) {
+ mForcedScroll = null;
+ }
+ if (mForcedScroll != null) {
+ ExpandableView expandableView = (ExpandableView) mForcedScroll;
+ int positionInLinearLayout = getPositionInLinearLayout(expandableView);
+ int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
+ int outOfViewScroll = positionInLinearLayout + expandableView.getIntrinsicHeight();
+
+ targetScroll = Math.max(0, Math.min(targetScroll, getScrollRange()));
+
+ // Only apply the scroll if we're scrolling the view upwards, or the view is so far up
+ // that it is not visible anymore.
+ if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
+ setOwnScrollY(targetScroll);
+ }
+ }
+ }
+
+ private void requestChildrenUpdate() {
+ if (!mChildrenUpdateRequested) {
+ getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
+ mChildrenUpdateRequested = true;
+ invalidate();
+ }
+ }
+
+ private boolean isCurrentlyAnimating() {
+ return mStateAnimator.isRunning();
+ }
+
+ private void clampScrollPosition() {
+ int scrollRange = getScrollRange();
+ if (scrollRange < mOwnScrollY) {
+ setOwnScrollY(scrollRange);
+ }
+ }
+
+ public int getTopPadding() {
+ return mTopPadding;
+ }
+
+ private void setTopPadding(int topPadding, boolean animate) {
+ if (mTopPadding != topPadding) {
+ mTopPadding = topPadding;
+ updateAlgorithmHeightAndPadding();
+ updateContentHeight();
+ if (animate && mAnimationsEnabled && mIsExpanded) {
+ mTopPaddingNeedsAnimation = true;
+ mNeedsAnimation = true;
+ }
+ requestChildrenUpdate();
+ notifyHeightChangeListener(null);
+ }
+ }
+
+ /**
+ * Update the height of the panel.
+ *
+ * @param height the expanded height of the panel
+ */
+ public void setExpandedHeight(float height) {
+ mExpandedHeight = height;
+ setIsExpanded(height > 0);
+ int minExpansionHeight = getMinExpansionHeight();
+ if (height < minExpansionHeight) {
+ mClipRect.left = 0;
+ mClipRect.right = getWidth();
+ mClipRect.top = 0;
+ mClipRect.bottom = (int) height;
+ height = minExpansionHeight;
+ setRequestedClipBounds(mClipRect);
+ } else {
+ setRequestedClipBounds(null);
+ }
+ int stackHeight;
+ float translationY;
+ float appearEndPosition = getAppearEndPosition();
+ float appearStartPosition = getAppearStartPosition();
+ if (height >= appearEndPosition) {
+ translationY = 0;
+ stackHeight = (int) height;
+ } else {
+ float appearFraction = getAppearFraction(height);
+ if (appearFraction >= 0) {
+ translationY = NotificationUtils.interpolate(getExpandTranslationStart(), 0,
+ appearFraction);
+ } else {
+ // This may happen when pushing up a heads up. We linearly push it up from the
+ // start
+ translationY = height - appearStartPosition + getExpandTranslationStart();
+ }
+ stackHeight = (int) (height - translationY);
+ }
+ if (stackHeight != mCurrentStackHeight) {
+ mCurrentStackHeight = stackHeight;
+ updateAlgorithmHeightAndPadding();
+ requestChildrenUpdate();
+ }
+ setStackTranslation(translationY);
+ }
+
+ private void setRequestedClipBounds(Rect clipRect) {
+ mRequestedClipBounds = clipRect;
+ updateClipping();
+ }
+
+ public void updateClipping() {
+ boolean clipped = mRequestedClipBounds != null && !mInHeadsUpPinnedMode
+ && !mHeadsUpAnimatingAway;
+ if (mIsClipped != clipped) {
+ mIsClipped = clipped;
+ updateFadingState();
+ }
+ if (clipped) {
+ setClipBounds(mRequestedClipBounds);
+ } else {
+ setClipBounds(null);
+ }
+ }
+
+ /**
+ * @return The translation at the beginning when expanding.
+ * Measured relative to the resting position.
+ */
+ private float getExpandTranslationStart() {
+ return - mTopPadding;
+ }
+
+ /**
+ * @return the position from where the appear transition starts when expanding.
+ * Measured in absolute height.
+ */
+ private float getAppearStartPosition() {
+ if (mTrackingHeadsUp && mFirstVisibleBackgroundChild != null) {
+ if (mAmbientState.isAboveShelf(mFirstVisibleBackgroundChild)) {
+ // If we ever expanded beyond the first notification, it's allowed to merge into
+ // the shelf
+ return mFirstVisibleBackgroundChild.getPinnedHeadsUpHeight();
+ }
+ }
+ return getMinExpansionHeight();
+ }
+
+ /**
+ * @return the position from where the appear transition ends when expanding.
+ * Measured in absolute height.
+ */
+ private float getAppearEndPosition() {
+ int appearPosition;
+ int notGoneChildCount = getNotGoneChildCount();
+ if (mEmptyShadeView.getVisibility() == GONE && notGoneChildCount != 0) {
+ int minNotificationsForShelf = 1;
+ if (mTrackingHeadsUp
+ || (mHeadsUpManager.hasPinnedHeadsUp() && !mAmbientState.isDark())) {
+ appearPosition = mHeadsUpManager.getTopHeadsUpPinnedHeight();
+ minNotificationsForShelf = 2;
+ } else {
+ appearPosition = 0;
+ }
+ if (notGoneChildCount >= minNotificationsForShelf
+ && mShelf.getVisibility() != GONE) {
+ appearPosition += mShelf.getIntrinsicHeight();
+ }
+ } else {
+ appearPosition = mEmptyShadeView.getHeight();
+ }
+ return appearPosition + (onKeyguard() ? mTopPadding : mIntrinsicPadding);
+ }
+
+ /**
+ * @param height the height of the panel
+ * @return the fraction of the appear animation that has been performed
+ */
+ public float getAppearFraction(float height) {
+ float appearEndPosition = getAppearEndPosition();
+ float appearStartPosition = getAppearStartPosition();
+ return (height - appearStartPosition)
+ / (appearEndPosition - appearStartPosition);
+ }
+
+ public float getStackTranslation() {
+ return mStackTranslation;
+ }
+
+ private void setStackTranslation(float stackTranslation) {
+ if (stackTranslation != mStackTranslation) {
+ mStackTranslation = stackTranslation;
+ mAmbientState.setStackTranslation(stackTranslation);
+ requestChildrenUpdate();
+ }
+ }
+
+ /**
+ * Get the current height of the view. This is at most the msize of the view given by a the
+ * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
+ *
+ * @return either the layout height or the externally defined height, whichever is smaller
+ */
+ private int getLayoutHeight() {
+ return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
+ }
+
+ public int getFirstItemMinHeight() {
+ final ExpandableView firstChild = getFirstChildNotGone();
+ return firstChild != null ? firstChild.getMinHeight() : mCollapsedSize;
+ }
+
+ public void setLongPressListener(SwipeHelper.LongPressListener listener) {
+ mSwipeHelper.setLongPressListener(listener);
+ mLongPressListener = listener;
+ }
+
+ public void setQsContainer(ViewGroup qsContainer) {
+ mQsContainer = qsContainer;
+ }
+
+ @Override
+ public void onChildDismissed(View v) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ if (!row.isDismissed()) {
+ handleChildDismissed(v);
+ }
+ ViewGroup transientContainer = row.getTransientContainer();
+ if (transientContainer != null) {
+ transientContainer.removeTransientView(v);
+ }
+ }
+
+ private void handleChildDismissed(View v) {
+ if (mDismissAllInProgress) {
+ return;
+ }
+ setSwipingInProgress(false);
+ if (mDragAnimPendingChildren.contains(v)) {
+ // We start the swipe and finish it in the same frame, we don't want any animation
+ // for the drag
+ mDragAnimPendingChildren.remove(v);
+ }
+ mSwipedOutViews.add(v);
+ mAmbientState.onDragFinished(v);
+ updateContinuousShadowDrawing();
+ if (v instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ if (row.isHeadsUp()) {
+ mHeadsUpManager.addSwipedOutNotification(row.getStatusBarNotification().getKey());
+ }
+ }
+ performDismiss(v, mGroupManager, false /* fromAccessibility */);
+
+ mFalsingManager.onNotificationDismissed();
+ if (mFalsingManager.shouldEnforceBouncer()) {
+ mStatusBar.executeRunnableDismissingKeyguard(null, null /* cancelAction */,
+ false /* dismissShade */, true /* afterKeyguardGone */, false /* deferred */);
+ }
+ }
+
+ public static void performDismiss(View v, NotificationGroupManager groupManager,
+ boolean fromAccessibility) {
+ if (!(v instanceof ExpandableNotificationRow)) {
+ return;
+ }
+ ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ if (groupManager.isOnlyChildInGroup(row.getStatusBarNotification())) {
+ ExpandableNotificationRow groupSummary =
+ groupManager.getLogicalGroupSummary(row.getStatusBarNotification());
+ if (groupSummary.isClearable()) {
+ performDismiss(groupSummary, groupManager, fromAccessibility);
+ }
+ }
+ row.setDismissed(true, fromAccessibility);
+ if (row.isClearable()) {
+ row.performDismiss();
+ }
+ if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
+ }
+
+ @Override
+ public void onChildSnappedBack(View animView, float targetLeft) {
+ mAmbientState.onDragFinished(animView);
+ updateContinuousShadowDrawing();
+ if (!mDragAnimPendingChildren.contains(animView)) {
+ if (mAnimationsEnabled) {
+ mSnappedBackChildren.add(animView);
+ mNeedsAnimation = true;
+ }
+ requestChildrenUpdate();
+ } else {
+ // We start the swipe and snap back in the same frame, we don't want any animation
+ mDragAnimPendingChildren.remove(animView);
+ }
+ if (mCurrMenuRow != null && targetLeft == 0) {
+ mCurrMenuRow.resetMenu();
+ mCurrMenuRow = null;
+ }
+ }
+
+ @Override
+ public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
+ if (!mIsExpanded && isPinnedHeadsUp(animView) && canChildBeDismissed(animView)) {
+ mScrimController.setTopHeadsUpDragAmount(animView,
+ Math.min(Math.abs(swipeProgress / 2f - 1.0f), 1.0f));
+ }
+ // Returning true prevents alpha fading.
+ return !mFadeNotificationsOnDismiss;
+ }
+
+ @Override
+ public void onBeginDrag(View v) {
+ mFalsingManager.onNotificatonStartDismissing();
+ setSwipingInProgress(true);
+ mAmbientState.onBeginDrag(v);
+ updateContinuousShadowDrawing();
+ if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) {
+ mDragAnimPendingChildren.add(v);
+ mNeedsAnimation = true;
+ }
+ requestChildrenUpdate();
+ }
+
+ public static boolean isPinnedHeadsUp(View v) {
+ if (v instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ return row.isHeadsUp() && row.isPinned();
+ }
+ return false;
+ }
+
+ private boolean isHeadsUp(View v) {
+ if (v instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ return row.isHeadsUp();
+ }
+ return false;
+ }
+
+ @Override
+ public void onDragCancelled(View v) {
+ mFalsingManager.onNotificatonStopDismissing();
+ setSwipingInProgress(false);
+ }
+
+ @Override
+ public float getFalsingThresholdFactor() {
+ return mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+ }
+
+ @Override
+ public View getChildAtPosition(MotionEvent ev) {
+ View child = getChildAtPosition(ev.getX(), ev.getY());
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ ExpandableNotificationRow parent = row.getNotificationParent();
+ if (parent != null && parent.areChildrenExpanded()
+ && (parent.areGutsExposed()
+ || mMenuExposedView == parent
+ || (parent.getNotificationChildren().size() == 1
+ && parent.isClearable()))) {
+ // In this case the group is expanded and showing the menu for the
+ // group, further interaction should apply to the group, not any
+ // child notifications so we use the parent of the child. We also do the same
+ // if we only have a single child.
+ child = parent;
+ }
+ }
+ return child;
+ }
+
+ 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) {
+ continue;
+ }
+ float childTop = slidingChild.getTranslationY();
+ float top = childTop + slidingChild.getClipTopAmount();
+ float bottom = childTop + slidingChild.getActualHeight()
+ - slidingChild.getClipBottomAmount();
+
+ float dist = Math.min(Math.abs(top - localTouchY), Math.abs(bottom - localTouchY));
+ if (dist < minDist) {
+ closestChild = slidingChild;
+ minDist = dist;
+ }
+ }
+ return closestChild;
+ }
+
+ @Override
+ public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
+ getLocationOnScreen(mTempInt2);
+ return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
+ }
+
+ @Override
+ public ExpandableView getChildAtPosition(float touchX, float touchY) {
+ return getChildAtPosition(touchX, touchY, true /* requireMinHeight */);
+
+ }
+
+ /**
+ * Get the child at a certain screen location.
+ *
+ * @param touchX the x coordinate
+ * @param touchY the y coordinate
+ * @param requireMinHeight Whether a minimum height is required for a child to be returned.
+ * @return the child at the given location.
+ */
+ private ExpandableView getChildAtPosition(float touchX, float touchY,
+ boolean requireMinHeight) {
+ // find the view under the pointer, accounting for GONE views
+ final int count = getChildCount();
+ for (int childIdx = 0; childIdx < count; childIdx++) {
+ ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
+ if (slidingChild.getVisibility() != VISIBLE
+ || slidingChild instanceof StackScrollerDecorView) {
+ continue;
+ }
+ float childTop = slidingChild.getTranslationY();
+ float top = childTop + slidingChild.getClipTopAmount();
+ float bottom = childTop + slidingChild.getActualHeight()
+ - slidingChild.getClipBottomAmount();
+
+ // Allow the full width of this view to prevent gesture conflict on Keyguard (phone and
+ // camera affordance).
+ int left = 0;
+ int right = getWidth();
+
+ if ((bottom - top >= mMinInteractionHeight || !requireMinHeight)
+ && touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
+ if (slidingChild instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) slidingChild;
+ if (!mIsExpanded && row.isHeadsUp() && row.isPinned()
+ && mHeadsUpManager.getTopEntry().entry.row != row
+ && mGroupManager.getGroupSummary(
+ mHeadsUpManager.getTopEntry().entry.row.getStatusBarNotification())
+ != row) {
+ continue;
+ }
+ return row.getViewAtPosition(touchY - childTop);
+ }
+ return slidingChild;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean canChildBeExpanded(View v) {
+ return v instanceof ExpandableNotificationRow
+ && ((ExpandableNotificationRow) v).isExpandable()
+ && !((ExpandableNotificationRow) v).areGutsExposed()
+ && (mIsExpanded || !((ExpandableNotificationRow) v).isPinned());
+ }
+
+ /* Only ever called as a consequence of an expansion gesture in the shade. */
+ @Override
+ public void setUserExpandedChild(View v, boolean userExpanded) {
+ if (v instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ if (userExpanded && onKeyguard()) {
+ // Due to a race when locking the screen while touching, a notification may be
+ // expanded even after we went back to keyguard. An example of this happens if
+ // you click in the empty space while expanding a group.
+
+ // We also need to un-user lock it here, since otherwise the content height
+ // calculated might be wrong. We also can't invert the two calls since
+ // un-userlocking it will trigger a layout switch in the content view.
+ row.setUserLocked(false);
+ updateContentHeight();
+ notifyHeightChangeListener(row);
+ return;
+ }
+ row.setUserExpanded(userExpanded, true /* allowChildrenExpansion */);
+ row.onExpandedByGesture(userExpanded);
+ }
+ }
+
+ @Override
+ public void setExpansionCancelled(View v) {
+ if (v instanceof ExpandableNotificationRow) {
+ ((ExpandableNotificationRow) v).setGroupExpansionChanging(false);
+ }
+ }
+
+ @Override
+ public void setUserLockedChild(View v, boolean userLocked) {
+ if (v instanceof ExpandableNotificationRow) {
+ ((ExpandableNotificationRow) v).setUserLocked(userLocked);
+ }
+ removeLongPressCallback();
+ requestDisallowInterceptTouchEvent(true);
+ }
+
+ @Override
+ public void expansionStateChanged(boolean isExpanding) {
+ mExpandingNotification = isExpanding;
+ if (!mExpandedInThisMotion) {
+ mMaxScrollAfterExpand = mOwnScrollY;
+ mExpandedInThisMotion = true;
+ }
+ }
+
+ @Override
+ public int getMaxExpandHeight(ExpandableView view) {
+ return view.getMaxContentHeight();
+ }
+
+ public void setScrollingEnabled(boolean enable) {
+ mScrollingEnabled = enable;
+ }
+
+ @Override
+ public void lockScrollTo(View v) {
+ if (mForcedScroll == v) {
+ return;
+ }
+ mForcedScroll = v;
+ scrollTo(v);
+ }
+
+ @Override
+ public boolean scrollTo(View v) {
+ ExpandableView expandableView = (ExpandableView) v;
+ int positionInLinearLayout = getPositionInLinearLayout(v);
+ int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
+ int outOfViewScroll = positionInLinearLayout + expandableView.getIntrinsicHeight();
+
+ // Only apply the scroll if we're scrolling the view upwards, or the view is so far up
+ // that it is not visible anymore.
+ if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
+ mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScroll - mOwnScrollY);
+ mDontReportNextOverScroll = true;
+ animateScroll();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return the scroll necessary to make the bottom edge of {@param v} align with the top of
+ * the IME.
+ */
+ private int targetScrollForView(ExpandableView v, int positionInLinearLayout) {
+ return positionInLinearLayout + v.getIntrinsicHeight() +
+ getImeInset() - getHeight() + getTopPadding();
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ mBottomInset = insets.getSystemWindowInsetBottom();
+
+ int range = getScrollRange();
+ if (mOwnScrollY > range) {
+ // HACK: We're repeatedly getting staggered insets here while the IME is
+ // animating away. To work around that we'll wait until things have settled.
+ removeCallbacks(mReclamp);
+ postDelayed(mReclamp, 50);
+ } else if (mForcedScroll != null) {
+ // The scroll was requested before we got the actual inset - in case we need
+ // to scroll up some more do so now.
+ scrollTo(mForcedScroll);
+ }
+ return insets;
+ }
+
+ private Runnable mReclamp = new Runnable() {
+ @Override
+ public void run() {
+ int range = getScrollRange();
+ mScroller.startScroll(mScrollX, mOwnScrollY, 0, range - mOwnScrollY);
+ mDontReportNextOverScroll = true;
+ mDontClampNextScroll = true;
+ animateScroll();
+ }
+ };
+
+ public void setExpandingEnabled(boolean enable) {
+ mExpandHelper.setEnabled(enable);
+ }
+
+ private boolean isScrollingEnabled() {
+ return mScrollingEnabled;
+ }
+
+ @Override
+ public boolean canChildBeDismissed(View v) {
+ return StackScrollAlgorithm.canChildBeDismissed(v);
+ }
+
+ @Override
+ public boolean isAntiFalsingNeeded() {
+ return onKeyguard();
+ }
+
+ private boolean onKeyguard() {
+ return mStatusBarState == StatusBarState.KEYGUARD;
+ }
+
+ private void setSwipingInProgress(boolean isSwiped) {
+ mSwipingInProgress = isSwiped;
+ if(isSwiped) {
+ requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ float densityScale = getResources().getDisplayMetrics().density;
+ mSwipeHelper.setDensityScale(densityScale);
+ float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
+ mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
+ initView(getContext());
+ }
+
+ public void dismissViewAnimated(View child, Runnable endRunnable, int delay, long duration) {
+ mSwipeHelper.dismissChild(child, 0, endRunnable, delay, true, duration,
+ true /* isDismissAll */);
+ }
+
+ public void snapViewIfNeeded(ExpandableNotificationRow child) {
+ boolean animate = mIsExpanded || isPinnedHeadsUp(child);
+ // If the child is showing the notification menu snap to that
+ float targetLeft = child.getProvider().isMenuVisible() ? child.getTranslation() : 0;
+ mSwipeHelper.snapChildIfNeeded(child, animate, targetLeft);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || ev.getActionMasked()== MotionEvent.ACTION_UP;
+ handleEmptySpaceClick(ev);
+ boolean expandWantsIt = false;
+ if (mIsExpanded && !mSwipingInProgress && !mOnlyScrollingInThisMotion) {
+ if (isCancelOrUp) {
+ mExpandHelper.onlyObserveMovements(false);
+ }
+ boolean wasExpandingBefore = mExpandingNotification;
+ expandWantsIt = mExpandHelper.onTouchEvent(ev);
+ if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore
+ && !mDisallowScrollingInThisMotion) {
+ dispatchDownEventToScroller(ev);
+ }
+ }
+ boolean scrollerWantsIt = false;
+ if (mIsExpanded && !mSwipingInProgress && !mExpandingNotification
+ && !mDisallowScrollingInThisMotion) {
+ scrollerWantsIt = onScrollTouch(ev);
+ }
+ boolean horizontalSwipeWantsIt = false;
+ if (!mIsBeingDragged
+ && !mExpandingNotification
+ && !mExpandedInThisMotion
+ && !mOnlyScrollingInThisMotion
+ && !mDisallowDismissInThisMotion) {
+ horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
+ }
+
+ // Check if we need to clear any snooze leavebehinds
+ NotificationGuts guts = mStatusBar.getExposedGuts();
+ if (guts != null && !isTouchInView(ev, guts)
+ && guts.getGutsContent() instanceof NotificationSnooze) {
+ NotificationSnooze ns = (NotificationSnooze) guts.getGutsContent();
+ if ((ns.isExpanded() && isCancelOrUp)
+ || (!horizontalSwipeWantsIt && scrollerWantsIt)) {
+ // If the leavebehind is expanded we clear it on the next up event, otherwise we
+ // clear it on the next non-horizontal swipe or expand event.
+ checkSnoozeLeavebehind();
+ }
+ }
+ if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
+ mCheckForLeavebehind = true;
+ }
+ return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev);
+ }
+
+ private void dispatchDownEventToScroller(MotionEvent ev) {
+ MotionEvent downEvent = MotionEvent.obtain(ev);
+ downEvent.setAction(MotionEvent.ACTION_DOWN);
+ onScrollTouch(downEvent);
+ downEvent.recycle();
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (!isScrollingEnabled() || !mIsExpanded || mSwipingInProgress || mExpandingNotification
+ || mDisallowScrollingInThisMotion) {
+ return false;
+ }
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_SCROLL: {
+ if (!mIsBeingDragged) {
+ final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ if (vscroll != 0) {
+ final int delta = (int) (vscroll * getVerticalScrollFactor());
+ final int range = getScrollRange();
+ int oldScrollY = mOwnScrollY;
+ int newScrollY = oldScrollY - delta;
+ if (newScrollY < 0) {
+ newScrollY = 0;
+ } else if (newScrollY > range) {
+ newScrollY = range;
+ }
+ if (newScrollY != oldScrollY) {
+ setOwnScrollY(newScrollY);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ private boolean onScrollTouch(MotionEvent ev) {
+ if (!isScrollingEnabled()) {
+ return false;
+ }
+ if (isInsideQsContainer(ev) && !mIsBeingDragged) {
+ return false;
+ }
+ mForcedScroll = null;
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+
+ final int action = ev.getAction();
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ if (getChildCount() == 0 || !isInContentBounds(ev)) {
+ return false;
+ }
+ boolean isBeingDragged = !mScroller.isFinished();
+ setIsBeingDragged(isBeingDragged);
+ /*
+ * If being flinged and user touches, stop the fling. isFinished
+ * will be false if being flinged.
+ */
+ if (!mScroller.isFinished()) {
+ mScroller.forceFinished(true);
+ }
+
+ // Remember where the motion event started
+ mLastMotionY = (int) ev.getY();
+ mDownX = (int) ev.getX();
+ mActivePointerId = ev.getPointerId(0);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE:
+ final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (activePointerIndex == -1) {
+ Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
+ break;
+ }
+
+ final int y = (int) ev.getY(activePointerIndex);
+ final int x = (int) ev.getX(activePointerIndex);
+ int deltaY = mLastMotionY - y;
+ final int xDiff = Math.abs(x - mDownX);
+ final int yDiff = Math.abs(deltaY);
+ if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) {
+ setIsBeingDragged(true);
+ if (deltaY > 0) {
+ deltaY -= mTouchSlop;
+ } else {
+ deltaY += mTouchSlop;
+ }
+ }
+ if (mIsBeingDragged) {
+ // Scroll to follow the motion event
+ mLastMotionY = y;
+ int range = getScrollRange();
+ if (mExpandedInThisMotion) {
+ range = Math.min(range, mMaxScrollAfterExpand);
+ }
+
+ float scrollAmount;
+ if (deltaY < 0) {
+ scrollAmount = overScrollDown(deltaY);
+ } else {
+ scrollAmount = overScrollUp(deltaY, range);
+ }
+
+ // Calling customOverScrollBy will call onCustomOverScrolled, which
+ // sets the scrolling if applicable.
+ if (scrollAmount != 0.0f) {
+ // The scrolling motion could not be compensated with the
+ // existing overScroll, we have to scroll the view
+ customOverScrollBy((int) scrollAmount, mOwnScrollY,
+ range, getHeight() / 2);
+ // If we're scrolling, leavebehinds should be dismissed
+ checkSnoozeLeavebehind();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mIsBeingDragged) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
+
+ if (shouldOverScrollFling(initialVelocity)) {
+ onOverScrollFling(true, initialVelocity);
+ } else {
+ if (getChildCount() > 0) {
+ if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+ float currentOverScrollTop = getCurrentOverScrollAmount(true);
+ if (currentOverScrollTop == 0.0f || initialVelocity > 0) {
+ fling(-initialVelocity);
+ } else {
+ onOverScrollFling(false, initialVelocity);
+ }
+ } else {
+ if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
+ getScrollRange())) {
+ animateScroll();
+ }
+ }
+ }
+ }
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ }
+
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsBeingDragged && getChildCount() > 0) {
+ if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
+ animateScroll();
+ }
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int index = ev.getActionIndex();
+ mLastMotionY = (int) ev.getY(index);
+ mDownX = (int) ev.getX(index);
+ mActivePointerId = ev.getPointerId(index);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
+ mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
+ break;
+ }
+ return true;
+ }
+
+ protected boolean isInsideQsContainer(MotionEvent ev) {
+ return ev.getY() < mQsContainer.getBottom();
+ }
+
+ private void onOverScrollFling(boolean open, int initialVelocity) {
+ if (mOverscrollTopChangedListener != null) {
+ mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open);
+ }
+ mDontReportNextOverScroll = true;
+ setOverScrollAmount(0.0f, true, false);
+ }
+
+ /**
+ * Perform a scroll upwards and adapt the overscroll amounts accordingly
+ *
+ * @param deltaY The amount to scroll upwards, has to be positive.
+ * @return The amount of scrolling to be performed by the scroller,
+ * not handled by the overScroll amount.
+ */
+ private float overScrollUp(int deltaY, int range) {
+ deltaY = Math.max(deltaY, 0);
+ float currentTopAmount = getCurrentOverScrollAmount(true);
+ float newTopAmount = currentTopAmount - deltaY;
+ if (currentTopAmount > 0) {
+ setOverScrollAmount(newTopAmount, true /* onTop */,
+ false /* animate */);
+ }
+ // Top overScroll might not grab all scrolling motion,
+ // we have to scroll as well.
+ float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
+ float newScrollY = mOwnScrollY + scrollAmount;
+ if (newScrollY > range) {
+ if (!mExpandedInThisMotion) {
+ float currentBottomPixels = getCurrentOverScrolledPixels(false);
+ // We overScroll on the top
+ setOverScrolledPixels(currentBottomPixels + newScrollY - range,
+ false /* onTop */,
+ false /* animate */);
+ }
+ setOwnScrollY(range);
+ scrollAmount = 0.0f;
+ }
+ return scrollAmount;
+ }
+
+ /**
+ * Perform a scroll downward and adapt the overscroll amounts accordingly
+ *
+ * @param deltaY The amount to scroll downwards, has to be negative.
+ * @return The amount of scrolling to be performed by the scroller,
+ * not handled by the overScroll amount.
+ */
+ private float overScrollDown(int deltaY) {
+ deltaY = Math.min(deltaY, 0);
+ float currentBottomAmount = getCurrentOverScrollAmount(false);
+ float newBottomAmount = currentBottomAmount + deltaY;
+ if (currentBottomAmount > 0) {
+ setOverScrollAmount(newBottomAmount, false /* onTop */,
+ false /* animate */);
+ }
+ // Bottom overScroll might not grab all scrolling motion,
+ // we have to scroll as well.
+ float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
+ float newScrollY = mOwnScrollY + scrollAmount;
+ if (newScrollY < 0) {
+ float currentTopPixels = getCurrentOverScrolledPixels(true);
+ // We overScroll on the top
+ setOverScrolledPixels(currentTopPixels - newScrollY,
+ true /* onTop */,
+ false /* animate */);
+ setOwnScrollY(0);
+ scrollAmount = 0.0f;
+ }
+ return scrollAmount;
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+ MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ // TODO: Make this decision more intelligent.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mLastMotionY = (int) ev.getY(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ }
+ }
+
+ private void initVelocityTrackerIfNotExists() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ }
+
+ private void recycleVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private void initOrResetVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ } else {
+ mVelocityTracker.clear();
+ }
+ }
+
+ public void setFinishScrollingCallback(Runnable runnable) {
+ mFinishScrollingCallback = runnable;
+ }
+
+ private void animateScroll() {
+ if (mScroller.computeScrollOffset()) {
+ int oldY = mOwnScrollY;
+ int y = mScroller.getCurrY();
+
+ if (oldY != y) {
+ int range = getScrollRange();
+ if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
+ float currVelocity = mScroller.getCurrVelocity();
+ if (currVelocity >= mMinimumVelocity) {
+ mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
+ }
+ }
+
+ if (mDontClampNextScroll) {
+ range = Math.max(range, oldY);
+ }
+ customOverScrollBy(y - oldY, oldY, range,
+ (int) (mMaxOverScroll));
+ }
+
+ postOnAnimation(mAnimateScroll);
+ } else {
+ mDontClampNextScroll = false;
+ if (mFinishScrollingCallback != null) {
+ mFinishScrollingCallback.run();
+ }
+ }
+ }
+
+ private boolean customOverScrollBy(int deltaY, int scrollY, int scrollRangeY,
+ int maxOverScrollY) {
+
+ int newScrollY = scrollY + deltaY;
+ final int top = -maxOverScrollY;
+ final int bottom = maxOverScrollY + scrollRangeY;
+
+ boolean clampedY = false;
+ if (newScrollY > bottom) {
+ newScrollY = bottom;
+ clampedY = true;
+ } else if (newScrollY < top) {
+ newScrollY = top;
+ clampedY = true;
+ }
+
+ onCustomOverScrolled(newScrollY, clampedY);
+
+ return clampedY;
+ }
+
+ /**
+ * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded
+ * overscroll effect based on numPixels. By default this will also cancel animations on the
+ * same overScroll edge.
+ *
+ * @param numPixels The amount of pixels to overScroll by. These will be scaled according to
+ * the rubber-banding logic.
+ * @param onTop Should the effect be applied on top of the scroller.
+ * @param animate Should an animation be performed.
+ */
+ public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) {
+ setOverScrollAmount(numPixels * getRubberBandFactor(onTop), onTop, animate, true);
+ }
+
+ /**
+ * Set the effective overScroll amount which will be directly reflected in the layout.
+ * By default this will also cancel animations on the same overScroll edge.
+ *
+ * @param amount The amount to overScroll by.
+ * @param onTop Should the effect be applied on top of the scroller.
+ * @param animate Should an animation be performed.
+ */
+ public void setOverScrollAmount(float amount, boolean onTop, boolean animate) {
+ setOverScrollAmount(amount, onTop, animate, true);
+ }
+
+ /**
+ * Set the effective overScroll amount which will be directly reflected in the layout.
+ *
+ * @param amount The amount to overScroll by.
+ * @param onTop Should the effect be applied on top of the scroller.
+ * @param animate Should an animation be performed.
+ * @param cancelAnimators Should running animations be cancelled.
+ */
+ public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
+ boolean cancelAnimators) {
+ setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop));
+ }
+
+ /**
+ * Set the effective overScroll amount which will be directly reflected in the layout.
+ *
+ * @param amount The amount to overScroll by.
+ * @param onTop Should the effect be applied on top of the scroller.
+ * @param animate Should an animation be performed.
+ * @param cancelAnimators Should running animations be cancelled.
+ * @param isRubberbanded The value which will be passed to
+ * {@link OnOverscrollTopChangedListener#onOverscrollTopChanged}
+ */
+ public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
+ boolean cancelAnimators, boolean isRubberbanded) {
+ if (cancelAnimators) {
+ mStateAnimator.cancelOverScrollAnimators(onTop);
+ }
+ setOverScrollAmountInternal(amount, onTop, animate, isRubberbanded);
+ }
+
+ private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate,
+ boolean isRubberbanded) {
+ amount = Math.max(0, amount);
+ if (animate) {
+ mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded);
+ } else {
+ setOverScrolledPixels(amount / getRubberBandFactor(onTop), onTop);
+ mAmbientState.setOverScrollAmount(amount, onTop);
+ if (onTop) {
+ notifyOverscrollTopListener(amount, isRubberbanded);
+ }
+ requestChildrenUpdate();
+ }
+ }
+
+ private void notifyOverscrollTopListener(float amount, boolean isRubberbanded) {
+ mExpandHelper.onlyObserveMovements(amount > 1.0f);
+ if (mDontReportNextOverScroll) {
+ mDontReportNextOverScroll = false;
+ return;
+ }
+ if (mOverscrollTopChangedListener != null) {
+ mOverscrollTopChangedListener.onOverscrollTopChanged(amount, isRubberbanded);
+ }
+ }
+
+ public void setOverscrollTopChangedListener(
+ OnOverscrollTopChangedListener overscrollTopChangedListener) {
+ mOverscrollTopChangedListener = overscrollTopChangedListener;
+ }
+
+ public float getCurrentOverScrollAmount(boolean top) {
+ return mAmbientState.getOverScrollAmount(top);
+ }
+
+ public float getCurrentOverScrolledPixels(boolean top) {
+ return top? mOverScrolledTopPixels : mOverScrolledBottomPixels;
+ }
+
+ private void setOverScrolledPixels(float amount, boolean onTop) {
+ if (onTop) {
+ mOverScrolledTopPixels = amount;
+ } else {
+ mOverScrolledBottomPixels = amount;
+ }
+ }
+
+ private void onCustomOverScrolled(int scrollY, boolean clampedY) {
+ // Treat animating scrolls differently; see #computeScroll() for why.
+ if (!mScroller.isFinished()) {
+ setOwnScrollY(scrollY);
+ if (clampedY) {
+ springBack();
+ } else {
+ float overScrollTop = getCurrentOverScrollAmount(true);
+ if (mOwnScrollY < 0) {
+ notifyOverscrollTopListener(-mOwnScrollY, isRubberbanded(true));
+ } else {
+ notifyOverscrollTopListener(overScrollTop, isRubberbanded(true));
+ }
+ }
+ } else {
+ setOwnScrollY(scrollY);
+ }
+ }
+
+ private void springBack() {
+ int scrollRange = getScrollRange();
+ boolean overScrolledTop = mOwnScrollY <= 0;
+ boolean overScrolledBottom = mOwnScrollY >= scrollRange;
+ if (overScrolledTop || overScrolledBottom) {
+ boolean onTop;
+ float newAmount;
+ if (overScrolledTop) {
+ onTop = true;
+ newAmount = -mOwnScrollY;
+ setOwnScrollY(0);
+ mDontReportNextOverScroll = true;
+ } else {
+ onTop = false;
+ newAmount = mOwnScrollY - scrollRange;
+ setOwnScrollY(scrollRange);
+ }
+ setOverScrollAmount(newAmount, onTop, false);
+ setOverScrollAmount(0.0f, onTop, true);
+ mScroller.forceFinished(true);
+ }
+ }
+
+ private int getScrollRange() {
+ int contentHeight = getContentHeight();
+ int scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight);
+ int imeInset = getImeInset();
+ scrollRange += Math.min(imeInset, Math.max(0,
+ getContentHeight() - (getHeight() - imeInset)));
+ return scrollRange;
+ }
+
+ private int getImeInset() {
+ return Math.max(0, mBottomInset - (getRootView().getHeight() - getHeight()));
+ }
+
+ /**
+ * @return the first child which has visibility unequal to GONE
+ */
+ public ExpandableView getFirstChildNotGone() {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE && child != mShelf) {
+ return (ExpandableView) child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return the child before the given view which has visibility unequal to GONE
+ */
+ public ExpandableView getViewBeforeView(ExpandableView view) {
+ ExpandableView previousView = null;
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child == view) {
+ return previousView;
+ }
+ if (child.getVisibility() != View.GONE) {
+ previousView = (ExpandableView) child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The first child which has visibility unequal to GONE which is currently below the
+ * given translationY or equal to it.
+ */
+ private View getFirstChildBelowTranlsationY(float translationY, boolean ignoreChildren) {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == View.GONE) {
+ continue;
+ }
+ float rowTranslation = child.getTranslationY();
+ if (rowTranslation >= translationY) {
+ return child;
+ } else if (!ignoreChildren && child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
+ List<ExpandableNotificationRow> notificationChildren =
+ row.getNotificationChildren();
+ for (int childIndex = 0; childIndex < notificationChildren.size();
+ childIndex++) {
+ ExpandableNotificationRow rowChild = notificationChildren.get(childIndex);
+ if (rowChild.getTranslationY() + rowTranslation >= translationY) {
+ return rowChild;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return the last child which has visibility unequal to GONE
+ */
+ public View getLastChildNotGone() {
+ int childCount = getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE && child != mShelf) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return the number of children which have visibility unequal to GONE
+ */
+ public int getNotGoneChildCount() {
+ int childCount = getChildCount();
+ int count = 0;
+ for (int i = 0; i < childCount; i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (child.getVisibility() != View.GONE && !child.willBeGone() && child != mShelf) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public int getContentHeight() {
+ return mContentHeight;
+ }
+
+ private void updateContentHeight() {
+ int height = 0;
+ float previousPaddingRequest = mPaddingBetweenElements;
+ float previousPaddingAmount = 0.0f;
+ int numShownItems = 0;
+ boolean finish = false;
+ int maxDisplayedNotifications = mAmbientState.isDark()
+ ? (hasPulsingNotifications() ? 1 : 0)
+ : mMaxDisplayedNotifications;
+
+ for (int i = 0; i < getChildCount(); i++) {
+ ExpandableView expandableView = (ExpandableView) getChildAt(i);
+ if (expandableView.getVisibility() != View.GONE
+ && !expandableView.hasNoContentHeight()) {
+ boolean limitReached = maxDisplayedNotifications != -1
+ && numShownItems >= maxDisplayedNotifications;
+ boolean notificationOnAmbientThatIsNotPulsing = mAmbientState.isDark()
+ && hasPulsingNotifications()
+ && expandableView instanceof ExpandableNotificationRow
+ && !isPulsing(((ExpandableNotificationRow) expandableView).getEntry());
+ if (limitReached || notificationOnAmbientThatIsNotPulsing) {
+ expandableView = mShelf;
+ finish = true;
+ }
+ float increasedPaddingAmount = expandableView.getIncreasedPaddingAmount();
+ float padding;
+ if (increasedPaddingAmount >= 0.0f) {
+ padding = (int) NotificationUtils.interpolate(
+ previousPaddingRequest,
+ mIncreasedPaddingBetweenElements,
+ increasedPaddingAmount);
+ previousPaddingRequest = (int) NotificationUtils.interpolate(
+ mPaddingBetweenElements,
+ mIncreasedPaddingBetweenElements,
+ increasedPaddingAmount);
+ } else {
+ int ownPadding = (int) NotificationUtils.interpolate(
+ 0,
+ mPaddingBetweenElements,
+ 1.0f + increasedPaddingAmount);
+ if (previousPaddingAmount > 0.0f) {
+ padding = (int) NotificationUtils.interpolate(
+ ownPadding,
+ mIncreasedPaddingBetweenElements,
+ previousPaddingAmount);
+ } else {
+ padding = ownPadding;
+ }
+ previousPaddingRequest = ownPadding;
+ }
+ if (height != 0) {
+ height += padding;
+ }
+ previousPaddingAmount = increasedPaddingAmount;
+ height += expandableView.getIntrinsicHeight();
+ numShownItems++;
+ if (finish) {
+ break;
+ }
+ }
+ }
+ mContentHeight = height + mTopPadding + mBottomMargin;
+ updateScrollability();
+ clampScrollPosition();
+ mAmbientState.setLayoutMaxHeight(mContentHeight);
+ }
+
+ private boolean isPulsing(NotificationData.Entry entry) {
+ return mAmbientState.isPulsing(entry);
+ }
+
+ public boolean hasPulsingNotifications() {
+ return mPulsing != null;
+ }
+
+ private void updateScrollability() {
+ boolean scrollable = getScrollRange() > 0;
+ if (scrollable != mScrollable) {
+ mScrollable = scrollable;
+ setFocusable(scrollable);
+ updateForwardAndBackwardScrollability();
+ }
+ }
+
+ private void updateForwardAndBackwardScrollability() {
+ boolean forwardScrollable = mScrollable && mOwnScrollY < getScrollRange();
+ boolean backwardsScrollable = mScrollable && mOwnScrollY > 0;
+ boolean changed = forwardScrollable != mForwardScrollable
+ || backwardsScrollable != mBackwardScrollable;
+ mForwardScrollable = forwardScrollable;
+ mBackwardScrollable = backwardsScrollable;
+ if (changed) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ }
+ }
+
+ private void updateBackground() {
+ // No need to update the background color if it's not being drawn.
+ if (!mShouldDrawNotificationBackground || mAmbientState.isDark()) {
+ return;
+ }
+
+ updateBackgroundBounds();
+ if (!mCurrentBounds.equals(mBackgroundBounds)) {
+ boolean animate = mAnimateNextBackgroundTop || mAnimateNextBackgroundBottom
+ || areBoundsAnimating();
+ if (!isExpanded()) {
+ abortBackgroundAnimators();
+ animate = false;
+ }
+ if (animate) {
+ startBackgroundAnimation();
+ } else {
+ mCurrentBounds.set(mBackgroundBounds);
+ applyCurrentBackgroundBounds();
+ }
+ } else {
+ abortBackgroundAnimators();
+ }
+ mAnimateNextBackgroundBottom = false;
+ mAnimateNextBackgroundTop = false;
+ }
+
+ private void abortBackgroundAnimators() {
+ if (mBottomAnimator != null) {
+ mBottomAnimator.cancel();
+ }
+ if (mTopAnimator != null) {
+ mTopAnimator.cancel();
+ }
+ }
+
+ private boolean areBoundsAnimating() {
+ return mBottomAnimator != null || mTopAnimator != null;
+ }
+
+ private void startBackgroundAnimation() {
+ // left and right are always instantly applied
+ mCurrentBounds.left = mBackgroundBounds.left;
+ mCurrentBounds.right = mBackgroundBounds.right;
+ startBottomAnimation();
+ startTopAnimation();
+ }
+
+ private void startTopAnimation() {
+ int previousEndValue = mEndAnimationRect.top;
+ int newEndValue = mBackgroundBounds.top;
+ ObjectAnimator previousAnimator = mTopAnimator;
+ if (previousAnimator != null && previousEndValue == newEndValue) {
+ return;
+ }
+ if (!mAnimateNextBackgroundTop) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ int previousStartValue = mStartAnimationRect.top;
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ values[0].setIntValues(previousStartValue, newEndValue);
+ mStartAnimationRect.top = previousStartValue;
+ mEndAnimationRect.top = newEndValue;
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ setBackgroundTop(newEndValue);
+ return;
+ }
+ }
+ if (previousAnimator != null) {
+ previousAnimator.cancel();
+ }
+ ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundTop",
+ mCurrentBounds.top, newEndValue);
+ Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN;
+ animator.setInterpolator(interpolator);
+ animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mStartAnimationRect.top = -1;
+ mEndAnimationRect.top = -1;
+ mTopAnimator = null;
+ }
+ });
+ animator.start();
+ mStartAnimationRect.top = mCurrentBounds.top;
+ mEndAnimationRect.top = newEndValue;
+ mTopAnimator = animator;
+ }
+
+ private void startBottomAnimation() {
+ int previousStartValue = mStartAnimationRect.bottom;
+ int previousEndValue = mEndAnimationRect.bottom;
+ int newEndValue = mBackgroundBounds.bottom;
+ ObjectAnimator previousAnimator = mBottomAnimator;
+ if (previousAnimator != null && previousEndValue == newEndValue) {
+ return;
+ }
+ if (!mAnimateNextBackgroundBottom) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ values[0].setIntValues(previousStartValue, newEndValue);
+ mStartAnimationRect.bottom = previousStartValue;
+ mEndAnimationRect.bottom = newEndValue;
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ setBackgroundBottom(newEndValue);
+ return;
+ }
+ }
+ if (previousAnimator != null) {
+ previousAnimator.cancel();
+ }
+ ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundBottom",
+ mCurrentBounds.bottom, newEndValue);
+ Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN;
+ animator.setInterpolator(interpolator);
+ animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mStartAnimationRect.bottom = -1;
+ mEndAnimationRect.bottom = -1;
+ mBottomAnimator = null;
+ }
+ });
+ animator.start();
+ mStartAnimationRect.bottom = mCurrentBounds.bottom;
+ mEndAnimationRect.bottom = newEndValue;
+ mBottomAnimator = animator;
+ }
+
+ private void setBackgroundTop(int top) {
+ mCurrentBounds.top = top;
+ applyCurrentBackgroundBounds();
+ }
+
+ public void setBackgroundBottom(int bottom) {
+ mCurrentBounds.bottom = bottom;
+ applyCurrentBackgroundBounds();
+ }
+
+ private void applyCurrentBackgroundBounds() {
+ // If the background of the notification is not being drawn, then there is no need to
+ // exclude an area in the scrim. Rather, the scrim's color should serve as the background.
+ if (!mShouldDrawNotificationBackground) {
+ return;
+ }
+
+ mScrimController.setExcludedBackgroundArea(
+ mFadingOut || mParentNotFullyVisible || mAmbientState.isDark() || mIsClipped ? null
+ : mCurrentBounds);
+ invalidate();
+ }
+
+ /**
+ * Update the background bounds to the new desired bounds
+ */
+ private void updateBackgroundBounds() {
+ if (mAmbientState.isPanelFullWidth()) {
+ mBackgroundBounds.left = 0;
+ mBackgroundBounds.right = getWidth();
+ } else {
+ getLocationInWindow(mTempInt2);
+ mBackgroundBounds.left = mTempInt2[0];
+ mBackgroundBounds.right = mTempInt2[0] + getWidth();
+ }
+ if (!mIsExpanded) {
+ mBackgroundBounds.top = 0;
+ mBackgroundBounds.bottom = 0;
+ return;
+ }
+ ActivatableNotificationView firstView = mFirstVisibleBackgroundChild;
+ int top = 0;
+ if (firstView != null) {
+ // Round Y up to avoid seeing the background during animation
+ int finalTranslationY = (int) Math.ceil(ViewState.getFinalTranslationY(firstView));
+ if (mAnimateNextBackgroundTop
+ || mTopAnimator == null && mCurrentBounds.top == finalTranslationY
+ || mTopAnimator != null && mEndAnimationRect.top == finalTranslationY) {
+ // we're ending up at the same location as we are now, lets just skip the animation
+ top = finalTranslationY;
+ } else {
+ top = (int) Math.ceil(firstView.getTranslationY());
+ }
+ }
+ ActivatableNotificationView lastView =
+ mShelf.hasItemsInStableShelf() && mShelf.getVisibility() != GONE
+ ? mShelf
+ : mLastVisibleBackgroundChild;
+ int bottom;
+ if (lastView != null) {
+ int finalTranslationY;
+ if (lastView == mShelf) {
+ finalTranslationY = (int) mShelf.getTranslationY();
+ } else {
+ finalTranslationY = (int) ViewState.getFinalTranslationY(lastView);
+ }
+ int finalHeight = ExpandableViewState.getFinalActualHeight(lastView);
+ int finalBottom = finalTranslationY + finalHeight - lastView.getClipBottomAmount();
+ finalBottom = Math.min(finalBottom, getHeight());
+ if (mAnimateNextBackgroundBottom
+ || mBottomAnimator == null && mCurrentBounds.bottom == finalBottom
+ || mBottomAnimator != null && mEndAnimationRect.bottom == finalBottom) {
+ // we're ending up at the same location as we are now, lets just skip the animation
+ bottom = finalBottom;
+ } else {
+ bottom = (int) (lastView.getTranslationY() + lastView.getActualHeight()
+ - lastView.getClipBottomAmount());
+ bottom = Math.min(bottom, getHeight());
+ }
+ } else {
+ top = mTopPadding;
+ bottom = top;
+ }
+ if (mStatusBarState != StatusBarState.KEYGUARD) {
+ top = (int) Math.max(mTopPadding + mStackTranslation, top);
+ } else {
+ // otherwise the animation from the shade to the keyguard will jump as it's maxed
+ top = Math.max(0, top);
+ }
+ mBackgroundBounds.top = top;
+ mBackgroundBounds.bottom = Math.min(getHeight(), Math.max(bottom, top));
+ }
+
+ private ActivatableNotificationView getFirstPinnedHeadsUp() {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE
+ && child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ if (row.isPinned()) {
+ return row;
+ }
+ }
+ }
+ return null;
+ }
+
+ private ActivatableNotificationView getLastChildWithBackground() {
+ int childCount = getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE && child instanceof ActivatableNotificationView
+ && child != mShelf) {
+ return (ActivatableNotificationView) child;
+ }
+ }
+ return null;
+ }
+
+ private ActivatableNotificationView getFirstChildWithBackground() {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE && child instanceof ActivatableNotificationView
+ && child != mShelf) {
+ return (ActivatableNotificationView) child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Fling the scroll view
+ *
+ * @param velocityY The initial velocity in the Y direction. Positive
+ * numbers mean that the finger/cursor is moving down the screen,
+ * which means we want to scroll towards the top.
+ */
+ protected void fling(int velocityY) {
+ if (getChildCount() > 0) {
+ int scrollRange = getScrollRange();
+
+ float topAmount = getCurrentOverScrollAmount(true);
+ float bottomAmount = getCurrentOverScrollAmount(false);
+ if (velocityY < 0 && topAmount > 0) {
+ setOwnScrollY(mOwnScrollY - (int) topAmount);
+ mDontReportNextOverScroll = true;
+ setOverScrollAmount(0, true, false);
+ mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */)
+ * mOverflingDistance + topAmount;
+ } else if (velocityY > 0 && bottomAmount > 0) {
+ setOwnScrollY((int) (mOwnScrollY + bottomAmount));
+ setOverScrollAmount(0, false, false);
+ mMaxOverScroll = Math.abs(velocityY) / 1000f
+ * getRubberBandFactor(false /* onTop */) * mOverflingDistance
+ + bottomAmount;
+ } else {
+ // it will be set once we reach the boundary
+ mMaxOverScroll = 0.0f;
+ }
+ int minScrollY = Math.max(0, scrollRange);
+ if (mExpandedInThisMotion) {
+ minScrollY = Math.min(minScrollY, mMaxScrollAfterExpand);
+ }
+ mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0, minScrollY, 0,
+ mExpandedInThisMotion && mOwnScrollY >= 0 ? 0 : Integer.MAX_VALUE / 2);
+
+ animateScroll();
+ }
+ }
+
+ /**
+ * @return Whether a fling performed on the top overscroll edge lead to the expanded
+ * overScroll view (i.e QS).
+ */
+ private boolean shouldOverScrollFling(int initialVelocity) {
+ float topOverScroll = getCurrentOverScrollAmount(true);
+ return mScrolledToTopOnFirstDown
+ && !mExpandedInThisMotion
+ && topOverScroll > mMinTopOverScrollToEscape
+ && initialVelocity > 0;
+ }
+
+ /**
+ * Updates the top padding of the notifications, taking {@link #getIntrinsicPadding()} into
+ * account.
+ *
+ * @param qsHeight the top padding imposed by the quick settings panel
+ * @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, boolean animate,
+ boolean ignoreIntrinsicPadding) {
+ int topPadding = (int) qsHeight;
+ int minStackHeight = getLayoutMinHeight();
+ if (topPadding + minStackHeight > getHeight()) {
+ mTopPaddingOverflow = topPadding + minStackHeight - getHeight();
+ } else {
+ mTopPaddingOverflow = 0;
+ }
+ setTopPadding(ignoreIntrinsicPadding ? topPadding : clampPadding(topPadding),
+ animate);
+ setExpandedHeight(mExpandedHeight);
+ }
+
+ public int getLayoutMinHeight() {
+ return mShelf.getVisibility() == GONE ? 0 : mShelf.getIntrinsicHeight();
+ }
+
+ public int getFirstChildIntrinsicHeight() {
+ final ExpandableView firstChild = getFirstChildNotGone();
+ int firstChildMinHeight = firstChild != null
+ ? firstChild.getIntrinsicHeight()
+ : mEmptyShadeView != null
+ ? mEmptyShadeView.getIntrinsicHeight()
+ : mCollapsedSize;
+ if (mOwnScrollY > 0) {
+ firstChildMinHeight = Math.max(firstChildMinHeight - mOwnScrollY, mCollapsedSize);
+ }
+ return firstChildMinHeight;
+ }
+
+ public float getTopPaddingOverflow() {
+ return mTopPaddingOverflow;
+ }
+
+ public int getPeekHeight() {
+ final ExpandableView firstChild = getFirstChildNotGone();
+ final int firstChildMinHeight = firstChild != null ? firstChild.getCollapsedHeight()
+ : mCollapsedSize;
+ int shelfHeight = 0;
+ if (mLastVisibleBackgroundChild != null && mShelf.getVisibility() != GONE) {
+ shelfHeight = mShelf.getIntrinsicHeight();
+ }
+ return mIntrinsicPadding + firstChildMinHeight + shelfHeight;
+ }
+
+ private int clampPadding(int desiredPadding) {
+ return Math.max(desiredPadding, mIntrinsicPadding);
+ }
+
+ private float getRubberBandFactor(boolean onTop) {
+ if (!onTop) {
+ return RUBBER_BAND_FACTOR_NORMAL;
+ }
+ if (mExpandedInThisMotion) {
+ return RUBBER_BAND_FACTOR_AFTER_EXPAND;
+ } else if (mIsExpansionChanging || mPanelTracking) {
+ return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND;
+ } else if (mScrolledToTopOnFirstDown) {
+ return 1.0f;
+ }
+ return RUBBER_BAND_FACTOR_NORMAL;
+ }
+
+ /**
+ * Accompanying function for {@link #getRubberBandFactor}: Returns true if the overscroll is
+ * rubberbanded, false if it is technically an overscroll but rather a motion to expand the
+ * overscroll view (e.g. expand QS).
+ */
+ private boolean isRubberbanded(boolean onTop) {
+ return !onTop || mExpandedInThisMotion || mIsExpansionChanging || mPanelTracking
+ || !mScrolledToTopOnFirstDown;
+ }
+
+ private void endDrag() {
+ setIsBeingDragged(false);
+
+ recycleVelocityTracker();
+
+ if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
+ setOverScrollAmount(0, true /* onTop */, true /* animate */);
+ }
+ if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
+ setOverScrollAmount(0, false /* onTop */, true /* animate */);
+ }
+ }
+
+ private void transformTouchEvent(MotionEvent ev, View sourceView, View targetView) {
+ ev.offsetLocation(sourceView.getX(), sourceView.getY());
+ ev.offsetLocation(-targetView.getX(), -targetView.getY());
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ initDownStates(ev);
+ handleEmptySpaceClick(ev);
+ boolean expandWantsIt = false;
+ if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) {
+ expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev);
+ }
+ boolean scrollWantsIt = false;
+ if (!mSwipingInProgress && !mExpandingNotification) {
+ scrollWantsIt = onInterceptTouchEventScroll(ev);
+ }
+ boolean swipeWantsIt = false;
+ if (!mIsBeingDragged
+ && !mExpandingNotification
+ && !mExpandedInThisMotion
+ && !mOnlyScrollingInThisMotion
+ && !mDisallowDismissInThisMotion) {
+ swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
+ }
+ // Check if we need to clear any snooze leavebehinds
+ boolean isUp = ev.getActionMasked() == MotionEvent.ACTION_UP;
+ NotificationGuts guts = mStatusBar.getExposedGuts();
+ if (!isTouchInView(ev, guts) && isUp && !swipeWantsIt && !expandWantsIt
+ && !scrollWantsIt) {
+ mCheckForLeavebehind = false;
+ mStatusBar.closeAndSaveGuts(true /* removeLeavebehind */, false /* force */,
+ false /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */);
+ }
+ if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
+ mCheckForLeavebehind = true;
+ }
+ 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 (mStatusBarState != 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;
+ mDisallowDismissInThisMotion = false;
+ mTouchIsClick = true;
+ mInitialTouchX = ev.getX();
+ mInitialTouchY = ev.getY();
+ }
+ }
+
+ public void setChildTransferInProgress(boolean childTransferInProgress) {
+ mChildTransferInProgress = childTransferInProgress;
+ }
+
+ @Override
+ public void onViewRemoved(View child) {
+ super.onViewRemoved(child);
+ // we only call our internal methods if this is actually a removal and not just a
+ // notification which becomes a child notification
+ if (!mChildTransferInProgress) {
+ onViewRemovedInternal(child, this);
+ }
+ }
+
+ /**
+ * Called when a notification is removed from the shade. This cleans up the state for a given
+ * view.
+ */
+ public void cleanUpViewState(View child) {
+ if (child == mTranslatingParentView) {
+ mTranslatingParentView = null;
+ }
+ mCurrentStackScrollState.removeViewStateForView(child);
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ if (disallowIntercept) {
+ mSwipeHelper.removeLongPressCallback();
+ }
+ }
+
+ private void onViewRemovedInternal(View child, ViewGroup container) {
+ if (mChangePositionInProgress) {
+ // This is only a position change, don't do anything special
+ return;
+ }
+ ExpandableView expandableView = (ExpandableView) child;
+ expandableView.setOnHeightChangedListener(null);
+ mCurrentStackScrollState.removeViewStateForView(child);
+ updateScrollStateForRemovedChild(expandableView);
+ boolean animationGenerated = generateRemoveAnimation(child);
+ if (animationGenerated) {
+ if (!mSwipedOutViews.contains(child)) {
+ container.getOverlay().add(child);
+ } else if (Math.abs(expandableView.getTranslation()) != expandableView.getWidth()) {
+ container.addTransientView(child, 0);
+ expandableView.setTransientContainer(container);
+ }
+ } else {
+ mSwipedOutViews.remove(child);
+ }
+ updateAnimationState(false, child);
+
+ focusNextViewIfFocused(child);
+ }
+
+ private void focusNextViewIfFocused(View view) {
+ if (view instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ if (row.shouldRefocusOnDismiss()) {
+ View nextView = row.getChildAfterViewWhenDismissed();
+ if (nextView == null) {
+ View groupParentWhenDismissed = row.getGroupParentWhenDismissed();
+ nextView = getFirstChildBelowTranlsationY(groupParentWhenDismissed != null
+ ? groupParentWhenDismissed.getTranslationY()
+ : view.getTranslationY(), true /* ignoreChildren */);
+ }
+ if (nextView != null) {
+ nextView.requestAccessibilityFocus();
+ }
+ }
+ }
+
+ }
+
+ private boolean isChildInGroup(View child) {
+ return child instanceof ExpandableNotificationRow
+ && mGroupManager.isChildInGroupWithSummary(
+ ((ExpandableNotificationRow) child).getStatusBarNotification());
+ }
+
+ /**
+ * Generate a remove animation for a child view.
+ *
+ * @param child The view to generate the remove animation for.
+ * @return Whether an animation was generated.
+ */
+ private boolean generateRemoveAnimation(View child) {
+ if (removeRemovedChildFromHeadsUpChangeAnimations(child)) {
+ mAddedHeadsUpChildren.remove(child);
+ return false;
+ }
+ if (isClickedHeadsUp(child)) {
+ // An animation is already running, add it to the Overlay
+ mClearOverlayViewsWhenFinished.add(child);
+ return true;
+ }
+ if (mIsExpanded && mAnimationsEnabled && !isChildInInvisibleGroup(child)) {
+ if (!mChildrenToAddAnimated.contains(child)) {
+ // Generate Animations
+ mChildrenToRemoveAnimated.add(child);
+ mNeedsAnimation = true;
+ return true;
+ } else {
+ mChildrenToAddAnimated.remove(child);
+ mFromMoreCardAdditions.remove(child);
+ return false;
+ }
+ }
+ return false;
+ }
+
+ private boolean isClickedHeadsUp(View child) {
+ return HeadsUpManager.isClickedHeadsUpNotification(child);
+ }
+
+ /**
+ * Remove a removed child view from the heads up animations if it was just added there
+ *
+ * @return whether any child was removed from the list to animate
+ */
+ private boolean removeRemovedChildFromHeadsUpChangeAnimations(View child) {
+ boolean hasAddEvent = false;
+ for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) {
+ ExpandableNotificationRow row = eventPair.first;
+ boolean isHeadsUp = eventPair.second;
+ if (child == row) {
+ mTmpList.add(eventPair);
+ hasAddEvent |= isHeadsUp;
+ }
+ }
+ if (hasAddEvent) {
+ // This child was just added lets remove all events.
+ mHeadsUpChangeAnimations.removeAll(mTmpList);
+ ((ExpandableNotificationRow ) child).setHeadsUpAnimatingAway(false);
+ }
+ mTmpList.clear();
+ return hasAddEvent;
+ }
+
+ /**
+ * @param child the child to query
+ * @return whether a view is not a top level child but a child notification and that group is
+ * not expanded
+ */
+ private boolean isChildInInvisibleGroup(View child) {
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ ExpandableNotificationRow groupSummary =
+ mGroupManager.getGroupSummary(row.getStatusBarNotification());
+ if (groupSummary != null && groupSummary != row) {
+ return row.getVisibility() == View.INVISIBLE;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Updates the scroll position when a child was removed
+ *
+ * @param removedChild the removed child
+ */
+ private void updateScrollStateForRemovedChild(ExpandableView removedChild) {
+ int startingPosition = getPositionInLinearLayout(removedChild);
+ float increasedPaddingAmount = removedChild.getIncreasedPaddingAmount();
+ int padding;
+ if (increasedPaddingAmount >= 0) {
+ padding = (int) NotificationUtils.interpolate(
+ mPaddingBetweenElements,
+ mIncreasedPaddingBetweenElements,
+ increasedPaddingAmount);
+ } else {
+ padding = (int) NotificationUtils.interpolate(
+ 0,
+ mPaddingBetweenElements,
+ 1.0f + increasedPaddingAmount);
+ }
+ int childHeight = getIntrinsicHeight(removedChild) + padding;
+ int endPosition = startingPosition + childHeight;
+ if (endPosition <= mOwnScrollY) {
+ // This child is fully scrolled of the top, so we have to deduct its height from the
+ // scrollPosition
+ setOwnScrollY(mOwnScrollY - childHeight);
+ } else if (startingPosition < mOwnScrollY) {
+ // This child is currently being scrolled into, set the scroll position to the start of
+ // this child
+ setOwnScrollY(startingPosition);
+ }
+ }
+
+ private int getIntrinsicHeight(View view) {
+ if (view instanceof ExpandableView) {
+ ExpandableView expandableView = (ExpandableView) view;
+ return expandableView.getIntrinsicHeight();
+ }
+ return view.getHeight();
+ }
+
+ public int getPositionInLinearLayout(View requestedView) {
+ ExpandableNotificationRow childInGroup = null;
+ ExpandableNotificationRow requestedRow = null;
+ if (isChildInGroup(requestedView)) {
+ // We're asking for a child in a group. Calculate the position of the parent first,
+ // then within the parent.
+ childInGroup = (ExpandableNotificationRow) requestedView;
+ requestedView = requestedRow = childInGroup.getNotificationParent();
+ }
+ int position = 0;
+ float previousPaddingRequest = mPaddingBetweenElements;
+ float previousPaddingAmount = 0.0f;
+ for (int i = 0; i < getChildCount(); i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ boolean notGone = child.getVisibility() != View.GONE;
+ if (notGone && !child.hasNoContentHeight()) {
+ float increasedPaddingAmount = child.getIncreasedPaddingAmount();
+ float padding;
+ if (increasedPaddingAmount >= 0.0f) {
+ padding = (int) NotificationUtils.interpolate(
+ previousPaddingRequest,
+ mIncreasedPaddingBetweenElements,
+ increasedPaddingAmount);
+ previousPaddingRequest = (int) NotificationUtils.interpolate(
+ mPaddingBetweenElements,
+ mIncreasedPaddingBetweenElements,
+ increasedPaddingAmount);
+ } else {
+ int ownPadding = (int) NotificationUtils.interpolate(
+ 0,
+ mPaddingBetweenElements,
+ 1.0f + increasedPaddingAmount);
+ if (previousPaddingAmount > 0.0f) {
+ padding = (int) NotificationUtils.interpolate(
+ ownPadding,
+ mIncreasedPaddingBetweenElements,
+ previousPaddingAmount);
+ } else {
+ padding = ownPadding;
+ }
+ previousPaddingRequest = ownPadding;
+ }
+ if (position != 0) {
+ position += padding;
+ }
+ previousPaddingAmount = increasedPaddingAmount;
+ }
+ if (child == requestedView) {
+ if (requestedRow != null) {
+ position += requestedRow.getPositionOfChild(childInGroup);
+ }
+ return position;
+ }
+ if (notGone) {
+ position += getIntrinsicHeight(child);
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ super.onViewAdded(child);
+ onViewAddedInternal(child);
+ }
+
+ private void updateFirstAndLastBackgroundViews() {
+ ActivatableNotificationView firstChild = getFirstChildWithBackground();
+ ActivatableNotificationView lastChild = getLastChildWithBackground();
+ if (mAnimationsEnabled && mIsExpanded) {
+ mAnimateNextBackgroundTop = firstChild != mFirstVisibleBackgroundChild;
+ mAnimateNextBackgroundBottom = lastChild != mLastVisibleBackgroundChild;
+ } else {
+ mAnimateNextBackgroundTop = false;
+ mAnimateNextBackgroundBottom = false;
+ }
+ mFirstVisibleBackgroundChild = firstChild;
+ mLastVisibleBackgroundChild = lastChild;
+ mAmbientState.setLastVisibleBackgroundChild(lastChild);
+ }
+
+ private void onViewAddedInternal(View child) {
+ updateHideSensitiveForChild(child);
+ ((ExpandableView) child).setOnHeightChangedListener(this);
+ generateAddAnimation(child, false /* fromMoreCard */);
+ updateAnimationState(child);
+ updateChronometerForChild(child);
+ }
+
+ private void updateHideSensitiveForChild(View child) {
+ if (child instanceof ExpandableView) {
+ ExpandableView expandableView = (ExpandableView) child;
+ expandableView.setHideSensitiveForIntrinsicHeight(mAmbientState.isHideSensitive());
+ }
+ }
+
+ public void notifyGroupChildRemoved(View row, ViewGroup childrenContainer) {
+ onViewRemovedInternal(row, childrenContainer);
+ }
+
+ public void notifyGroupChildAdded(View row) {
+ onViewAddedInternal(row);
+ }
+
+ public void setAnimationsEnabled(boolean animationsEnabled) {
+ mAnimationsEnabled = animationsEnabled;
+ updateNotificationAnimationStates();
+ }
+
+ private void updateNotificationAnimationStates() {
+ boolean running = mAnimationsEnabled || hasPulsingNotifications();
+ mShelf.setAnimationsEnabled(running);
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ running &= mIsExpanded || isPinnedHeadsUp(child);
+ updateAnimationState(running, child);
+ }
+ }
+
+ private void updateAnimationState(View child) {
+ updateAnimationState((mAnimationsEnabled || hasPulsingNotifications())
+ && (mIsExpanded || isPinnedHeadsUp(child)), child);
+ }
+
+
+ private void updateAnimationState(boolean running, View child) {
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ row.setIconAnimationRunning(running);
+ }
+ }
+
+ public boolean isAddOrRemoveAnimationPending() {
+ return mNeedsAnimation
+ && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
+ }
+ /**
+ * Generate an animation for an added child view.
+ *
+ * @param child The view to be added.
+ * @param fromMoreCard Whether this add is coming from the "more" card on lockscreen.
+ */
+ public void generateAddAnimation(View child, boolean fromMoreCard) {
+ if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) {
+ // Generate Animations
+ mChildrenToAddAnimated.add(child);
+ if (fromMoreCard) {
+ mFromMoreCardAdditions.add(child);
+ }
+ mNeedsAnimation = true;
+ }
+ if (isHeadsUp(child) && mAnimationsEnabled && !mChangePositionInProgress) {
+ mAddedHeadsUpChildren.add(child);
+ mChildrenToAddAnimated.remove(child);
+ }
+ }
+
+ /**
+ * Change the position of child to a new location
+ *
+ * @param child the view to change the position for
+ * @param newIndex the new index
+ */
+ public void changeViewPosition(View child, int newIndex) {
+ int currentIndex = indexOfChild(child);
+ if (child != null && child.getParent() == this && currentIndex != newIndex) {
+ mChangePositionInProgress = true;
+ ((ExpandableView)child).setChangingPosition(true);
+ removeView(child);
+ addView(child, newIndex);
+ ((ExpandableView)child).setChangingPosition(false);
+ mChangePositionInProgress = false;
+ if (mIsExpanded && mAnimationsEnabled && child.getVisibility() != View.GONE) {
+ mChildrenChangingPositions.add(child);
+ mNeedsAnimation = true;
+ }
+ }
+ }
+
+ private void startAnimationToState() {
+ if (mNeedsAnimation) {
+ generateChildHierarchyEvents();
+ mNeedsAnimation = false;
+ }
+ if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
+ setAnimationRunning(true);
+ mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState,
+ mGoToFullShadeDelay);
+ mAnimationEvents.clear();
+ updateBackground();
+ updateViewShadows();
+ } else {
+ applyCurrentState();
+ }
+ mGoToFullShadeDelay = 0;
+ }
+
+ private void generateChildHierarchyEvents() {
+ generateHeadsUpAnimationEvents();
+ generateChildRemovalEvents();
+ generateChildAdditionEvents();
+ generatePositionChangeEvents();
+ generateSnapBackEvents();
+ generateDragEvents();
+ generateTopPaddingEvent();
+ generateActivateEvent();
+ generateDimmedEvent();
+ generateHideSensitiveEvent();
+ generateDarkEvent();
+ generateGoToFullShadeEvent();
+ generateViewResizeEvent();
+ generateGroupExpansionEvent();
+ generateAnimateEverythingEvent();
+ mNeedsAnimation = false;
+ }
+
+ private void generateHeadsUpAnimationEvents() {
+ for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) {
+ ExpandableNotificationRow row = eventPair.first;
+ boolean isHeadsUp = eventPair.second;
+ int type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER;
+ boolean onBottom = false;
+ boolean pinnedAndClosed = row.isPinned() && !mIsExpanded;
+ if (!mIsExpanded && !isHeadsUp) {
+ type = row.wasJustClicked()
+ ? AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
+ : AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
+ if (row.isChildInGroup()) {
+ // We can otherwise get stuck in there if it was just isolated
+ row.setHeadsUpAnimatingAway(false);
+ }
+ } else {
+ ExpandableViewState viewState = mCurrentStackScrollState.getViewStateForView(row);
+ if (viewState == null) {
+ // A view state was never generated for this view, so we don't need to animate
+ // this. This may happen with notification children.
+ continue;
+ }
+ if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) {
+ if (pinnedAndClosed || shouldHunAppearFromBottom(viewState)) {
+ // Our custom add animation
+ type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
+ } else {
+ // Normal add animation
+ type = AnimationEvent.ANIMATION_TYPE_ADD;
+ }
+ onBottom = !pinnedAndClosed;
+ }
+ }
+ AnimationEvent event = new AnimationEvent(row, type);
+ event.headsUpFromBottom = onBottom;
+ mAnimationEvents.add(event);
+ }
+ mHeadsUpChangeAnimations.clear();
+ mAddedHeadsUpChildren.clear();
+ }
+
+ private boolean shouldHunAppearFromBottom(ExpandableViewState viewState) {
+ if (viewState.yTranslation + viewState.height < mAmbientState.getMaxHeadsUpTranslation()) {
+ return false;
+ }
+ return true;
+ }
+
+ private void generateGroupExpansionEvent() {
+ // Generate a group expansion/collapsing event if there is such a group at all
+ if (mExpandedGroupView != null) {
+ mAnimationEvents.add(new AnimationEvent(mExpandedGroupView,
+ AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED));
+ mExpandedGroupView = null;
+ }
+ }
+
+ private void generateViewResizeEvent() {
+ if (mNeedViewResizeAnimation) {
+ mAnimationEvents.add(
+ new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_VIEW_RESIZE));
+ }
+ mNeedViewResizeAnimation = false;
+ }
+
+ private void generateSnapBackEvents() {
+ for (View child : mSnappedBackChildren) {
+ mAnimationEvents.add(new AnimationEvent(child,
+ AnimationEvent.ANIMATION_TYPE_SNAP_BACK));
+ }
+ mSnappedBackChildren.clear();
+ }
+
+ private void generateDragEvents() {
+ for (View child : mDragAnimPendingChildren) {
+ mAnimationEvents.add(new AnimationEvent(child,
+ AnimationEvent.ANIMATION_TYPE_START_DRAG));
+ }
+ mDragAnimPendingChildren.clear();
+ }
+
+ private void generateChildRemovalEvents() {
+ for (View child : mChildrenToRemoveAnimated) {
+ boolean childWasSwipedOut = mSwipedOutViews.contains(child);
+
+ // we need to know the view after this one
+ float removedTranslation = child.getTranslationY();
+ boolean ignoreChildren = true;
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ if (row.isRemoved() && row.wasChildInGroupWhenRemoved()) {
+ removedTranslation = row.getTranslationWhenRemoved();
+ ignoreChildren = false;
+ }
+ childWasSwipedOut |= Math.abs(row.getTranslation()) == row.getWidth();
+ }
+ if (!childWasSwipedOut) {
+ Rect clipBounds = child.getClipBounds();
+ childWasSwipedOut = clipBounds.height() == 0;
+ }
+ int animationType = childWasSwipedOut
+ ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
+ : AnimationEvent.ANIMATION_TYPE_REMOVE;
+ AnimationEvent event = new AnimationEvent(child, animationType);
+ event.viewAfterChangingView = getFirstChildBelowTranlsationY(removedTranslation,
+ ignoreChildren);
+ mAnimationEvents.add(event);
+ mSwipedOutViews.remove(child);
+ }
+ mChildrenToRemoveAnimated.clear();
+ }
+
+ private void generatePositionChangeEvents() {
+ for (View child : mChildrenChangingPositions) {
+ mAnimationEvents.add(new AnimationEvent(child,
+ AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
+ }
+ mChildrenChangingPositions.clear();
+ if (mGenerateChildOrderChangedEvent) {
+ mAnimationEvents.add(new AnimationEvent(null,
+ AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
+ mGenerateChildOrderChangedEvent = false;
+ }
+ }
+
+ private void generateChildAdditionEvents() {
+ for (View child : mChildrenToAddAnimated) {
+ if (mFromMoreCardAdditions.contains(child)) {
+ mAnimationEvents.add(new AnimationEvent(child,
+ AnimationEvent.ANIMATION_TYPE_ADD,
+ StackStateAnimator.ANIMATION_DURATION_STANDARD));
+ } else {
+ mAnimationEvents.add(new AnimationEvent(child,
+ AnimationEvent.ANIMATION_TYPE_ADD));
+ }
+ }
+ mChildrenToAddAnimated.clear();
+ mFromMoreCardAdditions.clear();
+ }
+
+ private void generateTopPaddingEvent() {
+ if (mTopPaddingNeedsAnimation) {
+ mAnimationEvents.add(
+ new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED));
+ }
+ mTopPaddingNeedsAnimation = false;
+ }
+
+ private void generateActivateEvent() {
+ if (mActivateNeedsAnimation) {
+ mAnimationEvents.add(
+ new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD));
+ }
+ mActivateNeedsAnimation = false;
+ }
+
+ private void generateAnimateEverythingEvent() {
+ if (mEverythingNeedsAnimation) {
+ mAnimationEvents.add(
+ new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_EVERYTHING));
+ }
+ mEverythingNeedsAnimation = false;
+ }
+
+ private void generateDimmedEvent() {
+ if (mDimmedNeedsAnimation) {
+ mAnimationEvents.add(
+ new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED));
+ }
+ mDimmedNeedsAnimation = false;
+ }
+
+ private void generateHideSensitiveEvent() {
+ if (mHideSensitiveNeedsAnimation) {
+ mAnimationEvents.add(
+ new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_HIDE_SENSITIVE));
+ }
+ mHideSensitiveNeedsAnimation = false;
+ }
+
+ private void generateDarkEvent() {
+ if (mDarkNeedsAnimation) {
+ AnimationEvent ev = new AnimationEvent(null,
+ AnimationEvent.ANIMATION_TYPE_DARK,
+ new AnimationFilter()
+ .animateDark()
+ .animateY(mShelf));
+ ev.darkAnimationOriginIndex = mDarkAnimationOriginIndex;
+ mAnimationEvents.add(ev);
+ startBackgroundFadeIn();
+ }
+ mDarkNeedsAnimation = false;
+ }
+
+ private void generateGoToFullShadeEvent() {
+ if (mGoToFullShadeNeedsAnimation) {
+ mAnimationEvents.add(
+ new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE));
+ }
+ mGoToFullShadeNeedsAnimation = false;
+ }
+
+ private boolean onInterceptTouchEventScroll(MotionEvent ev) {
+ if (!isScrollingEnabled()) {
+ return false;
+ }
+ /*
+ * This method JUST determines whether we want to intercept the motion.
+ * If we return true, onMotionEvent will be called and we do the actual
+ * scrolling there.
+ */
+
+ /*
+ * Shortcut the most recurring case: the user is in the dragging
+ * state and is moving their finger. We want to intercept this
+ * motion.
+ */
+ final int action = ev.getAction();
+ if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
+ return true;
+ }
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_MOVE: {
+ /*
+ * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+ * whether the user has moved far enough from the original down touch.
+ */
+
+ /*
+ * Locally do absolute value. mLastMotionY is set to the y value
+ * of the down event.
+ */
+ final int activePointerId = mActivePointerId;
+ if (activePointerId == INVALID_POINTER) {
+ // If we don't have a valid id, the touch down wasn't on content.
+ break;
+ }
+
+ final int pointerIndex = ev.findPointerIndex(activePointerId);
+ if (pointerIndex == -1) {
+ Log.e(TAG, "Invalid pointerId=" + activePointerId
+ + " in onInterceptTouchEvent");
+ break;
+ }
+
+ final int y = (int) ev.getY(pointerIndex);
+ final int x = (int) ev.getX(pointerIndex);
+ final int yDiff = Math.abs(y - mLastMotionY);
+ final int xDiff = Math.abs(x - mDownX);
+ if (yDiff > mTouchSlop && yDiff > xDiff) {
+ setIsBeingDragged(true);
+ mLastMotionY = y;
+ mDownX = x;
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_DOWN: {
+ final int y = (int) ev.getY();
+ mScrolledToTopOnFirstDown = isScrolledToTop();
+ if (getChildAtPosition(ev.getX(), y, false /* requireMinHeight */) == null) {
+ setIsBeingDragged(false);
+ recycleVelocityTracker();
+ break;
+ }
+
+ /*
+ * Remember location of down touch.
+ * ACTION_DOWN always refers to pointer index 0.
+ */
+ mLastMotionY = y;
+ mDownX = (int) ev.getX();
+ mActivePointerId = ev.getPointerId(0);
+
+ initOrResetVelocityTracker();
+ mVelocityTracker.addMovement(ev);
+ /*
+ * If being flinged and user touches the screen, initiate drag;
+ * otherwise don't. mScroller.isFinished should be false when
+ * being flinged.
+ */
+ boolean isBeingDragged = !mScroller.isFinished();
+ setIsBeingDragged(isBeingDragged);
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ /* Release the drag */
+ setIsBeingDragged(false);
+ mActivePointerId = INVALID_POINTER;
+ recycleVelocityTracker();
+ if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
+ animateScroll();
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+ }
+
+ /*
+ * The only time we want to intercept motion events is if we are in the
+ * drag mode.
+ */
+ return mIsBeingDragged;
+ }
+
+ protected StackScrollAlgorithm createStackScrollAlgorithm(Context context) {
+ return new StackScrollAlgorithm(context);
+ }
+
+ /**
+ * @return Whether the specified motion event is actually happening over the content.
+ */
+ private boolean isInContentBounds(MotionEvent event) {
+ 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) {
+ mIsBeingDragged = isDragged;
+ if (isDragged) {
+ requestDisallowInterceptTouchEvent(true);
+ removeLongPressCallback();
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+ if (!hasWindowFocus) {
+ removeLongPressCallback();
+ }
+ }
+
+ @Override
+ public void clearChildFocus(View child) {
+ super.clearChildFocus(child);
+ if (mForcedScroll == child) {
+ mForcedScroll = null;
+ }
+ }
+
+ @Override
+ public void requestDisallowLongPress() {
+ removeLongPressCallback();
+ }
+
+ @Override
+ public void requestDisallowDismiss() {
+ mDisallowDismissInThisMotion = true;
+ }
+
+ public void removeLongPressCallback() {
+ mSwipeHelper.removeLongPressCallback();
+ }
+
+ @Override
+ public boolean isScrolledToTop() {
+ return mOwnScrollY == 0;
+ }
+
+ @Override
+ public boolean isScrolledToBottom() {
+ return mOwnScrollY >= getScrollRange();
+ }
+
+ @Override
+ public View getHostView() {
+ return this;
+ }
+
+ public int getEmptyBottomMargin() {
+ return Math.max(mMaxLayoutHeight - mContentHeight, 0);
+ }
+
+ public void checkSnoozeLeavebehind() {
+ if (mCheckForLeavebehind) {
+ mStatusBar.closeAndSaveGuts(true /* removeLeavebehind */, false /* force */,
+ false /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */);
+ mCheckForLeavebehind = false;
+ }
+ }
+
+ public void resetCheckSnoozeLeavebehind() {
+ mCheckForLeavebehind = true;
+ }
+
+ public void onExpansionStarted() {
+ mIsExpansionChanging = true;
+ mAmbientState.setExpansionChanging(true);
+ checkSnoozeLeavebehind();
+ }
+
+ public void onExpansionStopped() {
+ mIsExpansionChanging = false;
+ resetCheckSnoozeLeavebehind();
+ mAmbientState.setExpansionChanging(false);
+ if (!mIsExpanded) {
+ setOwnScrollY(0);
+ mStatusBar.resetUserExpandedStates();
+ clearTemporaryViews();
+ clearUserLockedViews();
+ }
+ }
+
+ private void clearUserLockedViews() {
+ for (int i = 0; i < getChildCount(); i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ row.setUserLocked(false);
+ }
+ }
+ }
+
+ private void clearTemporaryViews() {
+ // lets make sure nothing is in the overlay / transient anymore
+ clearTemporaryViews(this);
+ for (int i = 0; i < getChildCount(); i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ clearTemporaryViews(row.getChildrenContainer());
+ }
+ }
+ }
+
+ private void clearTemporaryViews(ViewGroup viewGroup) {
+ while (viewGroup != null && viewGroup.getTransientViewCount() != 0) {
+ viewGroup.removeTransientView(viewGroup.getTransientView(0));
+ }
+ if (viewGroup != null) {
+ viewGroup.getOverlay().clear();
+ }
+ }
+
+ public void onPanelTrackingStarted() {
+ mPanelTracking = true;
+ mAmbientState.setPanelTracking(true);
+ }
+ public void onPanelTrackingStopped() {
+ mPanelTracking = false;
+ mAmbientState.setPanelTracking(false);
+ }
+
+ public void resetScrollPosition() {
+ mScroller.abortAnimation();
+ setOwnScrollY(0);
+ }
+
+ private void setIsExpanded(boolean isExpanded) {
+ boolean changed = isExpanded != mIsExpanded;
+ mIsExpanded = isExpanded;
+ mStackScrollAlgorithm.setIsExpanded(isExpanded);
+ if (changed) {
+ if (!mIsExpanded) {
+ mGroupManager.collapseAllGroups();
+ mExpandHelper.cancelImmediately();
+ }
+ updateNotificationAnimationStates();
+ updateChronometers();
+ requestChildrenUpdate();
+ }
+ }
+
+ private void updateChronometers() {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ updateChronometerForChild(getChildAt(i));
+ }
+ }
+
+ private void updateChronometerForChild(View child) {
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ row.setChronometerRunning(mIsExpanded);
+ }
+ }
+
+ @Override
+ public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
+ updateContentHeight();
+ updateScrollPositionOnExpandInBottom(view);
+ clampScrollPosition();
+ notifyHeightChangeListener(view);
+ ExpandableNotificationRow row = view instanceof ExpandableNotificationRow
+ ? (ExpandableNotificationRow) view
+ : null;
+ if (row != null && (row == mFirstVisibleBackgroundChild
+ || row.getNotificationParent() == mFirstVisibleBackgroundChild)) {
+ updateAlgorithmLayoutMinHeight();
+ }
+ if (needsAnimation) {
+ requestAnimationOnViewResize(row);
+ }
+ requestChildrenUpdate();
+ }
+
+ @Override
+ public void onReset(ExpandableView view) {
+ updateAnimationState(view);
+ updateChronometerForChild(view);
+ }
+
+ private void updateScrollPositionOnExpandInBottom(ExpandableView view) {
+ if (view instanceof ExpandableNotificationRow && !onKeyguard()) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ if (row.isUserLocked() && row != getFirstChildNotGone()) {
+ if (row.isSummaryWithChildren()) {
+ return;
+ }
+ // We are actually expanding this view
+ float endPosition = row.getTranslationY() + row.getActualHeight();
+ if (row.isChildInGroup()) {
+ endPosition += row.getNotificationParent().getTranslationY();
+ }
+ int layoutEnd = mMaxLayoutHeight + (int) mStackTranslation;
+ if (row != mLastVisibleBackgroundChild && mShelf.getVisibility() != GONE) {
+ layoutEnd -= mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
+ }
+ if (endPosition > layoutEnd) {
+ setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd));
+ mDisallowScrollingInThisMotion = true;
+ }
+ }
+ }
+ }
+
+ public void setOnHeightChangedListener(
+ ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
+ this.mOnHeightChangedListener = mOnHeightChangedListener;
+ }
+
+ public void setOnEmptySpaceClickListener(OnEmptySpaceClickListener listener) {
+ mOnEmptySpaceClickListener = listener;
+ }
+
+ public void onChildAnimationFinished() {
+ setAnimationRunning(false);
+ requestChildrenUpdate();
+ runAnimationFinishedRunnables();
+ clearViewOverlays();
+ clearHeadsUpDisappearRunning();
+ }
+
+ private void clearHeadsUpDisappearRunning() {
+ for (int i = 0; i < getChildCount(); i++) {
+ View view = getChildAt(i);
+ if (view instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ row.setHeadsUpAnimatingAway(false);
+ if (row.isSummaryWithChildren()) {
+ for (ExpandableNotificationRow child : row.getNotificationChildren()) {
+ child.setHeadsUpAnimatingAway(false);
+ }
+ }
+ }
+ }
+ }
+
+ private void clearViewOverlays() {
+ for (View view : mClearOverlayViewsWhenFinished) {
+ StackStateAnimator.removeFromOverlay(view);
+ }
+ mClearOverlayViewsWhenFinished.clear();
+ }
+
+ private void runAnimationFinishedRunnables() {
+ for (Runnable runnable : mAnimationFinishedRunnables) {
+ runnable.run();
+ }
+ mAnimationFinishedRunnables.clear();
+ }
+
+ /**
+ * See {@link AmbientState#setDimmed}.
+ */
+ public void setDimmed(boolean dimmed, boolean animate) {
+ dimmed &= onKeyguard();
+ mAmbientState.setDimmed(dimmed);
+ if (animate && mAnimationsEnabled) {
+ mDimmedNeedsAnimation = true;
+ mNeedsAnimation = true;
+ animateDimmed(dimmed);
+ } else {
+ setDimAmount(dimmed ? 1.0f : 0.0f);
+ }
+ requestChildrenUpdate();
+ }
+
+ @VisibleForTesting
+ boolean isDimmed() {
+ return mAmbientState.isDimmed();
+ }
+
+ private void setDimAmount(float dimAmount) {
+ mDimAmount = dimAmount;
+ updateBackgroundDimming();
+ }
+
+ private void animateDimmed(boolean dimmed) {
+ if (mDimAnimator != null) {
+ mDimAnimator.cancel();
+ }
+ float target = dimmed ? 1.0f : 0.0f;
+ if (target == mDimAmount) {
+ return;
+ }
+ mDimAnimator = TimeAnimator.ofFloat(mDimAmount, target);
+ mDimAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED);
+ mDimAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mDimAnimator.addListener(mDimEndListener);
+ mDimAnimator.addUpdateListener(mDimUpdateListener);
+ mDimAnimator.start();
+ }
+
+ public void setHideSensitive(boolean hideSensitive, boolean animate) {
+ if (hideSensitive != mAmbientState.isHideSensitive()) {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ ExpandableView v = (ExpandableView) getChildAt(i);
+ v.setHideSensitiveForIntrinsicHeight(hideSensitive);
+ }
+ mAmbientState.setHideSensitive(hideSensitive);
+ if (animate && mAnimationsEnabled) {
+ mHideSensitiveNeedsAnimation = true;
+ mNeedsAnimation = true;
+ }
+ requestChildrenUpdate();
+ }
+ }
+
+ /**
+ * See {@link AmbientState#setActivatedChild}.
+ */
+ public void setActivatedChild(ActivatableNotificationView activatedChild) {
+ mAmbientState.setActivatedChild(activatedChild);
+ if (mAnimationsEnabled) {
+ mActivateNeedsAnimation = true;
+ mNeedsAnimation = true;
+ }
+ requestChildrenUpdate();
+ }
+
+ public ActivatableNotificationView getActivatedChild() {
+ return mAmbientState.getActivatedChild();
+ }
+
+ private void applyCurrentState() {
+ mCurrentStackScrollState.apply();
+ if (mListener != null) {
+ mListener.onChildLocationsChanged(this);
+ }
+ runAnimationFinishedRunnables();
+ setAnimationRunning(false);
+ updateBackground();
+ updateViewShadows();
+ }
+
+ private void updateViewShadows() {
+ // we need to work around an issue where the shadow would not cast between siblings when
+ // their z difference is between 0 and 0.1
+
+ // Lefts first sort by Z difference
+ for (int i = 0; i < getChildCount(); i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ mTmpSortedChildren.add(child);
+ }
+ }
+ Collections.sort(mTmpSortedChildren, mViewPositionComparator);
+
+ // Now lets update the shadow for the views
+ ExpandableView previous = null;
+ for (int i = 0; i < mTmpSortedChildren.size(); i++) {
+ ExpandableView expandableView = mTmpSortedChildren.get(i);
+ float translationZ = expandableView.getTranslationZ();
+ float otherZ = previous == null ? translationZ : previous.getTranslationZ();
+ float diff = otherZ - translationZ;
+ if (diff <= 0.0f || diff >= FakeShadowView.SHADOW_SIBLING_TRESHOLD) {
+ // There is no fake shadow to be drawn
+ expandableView.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
+ } else {
+ float yLocation = previous.getTranslationY() + previous.getActualHeight() -
+ expandableView.getTranslationY() - previous.getExtraBottomPadding();
+ expandableView.setFakeShadowIntensity(
+ diff / FakeShadowView.SHADOW_SIBLING_TRESHOLD,
+ previous.getOutlineAlpha(), (int) yLocation,
+ previous.getOutlineTranslation());
+ }
+ previous = expandableView;
+ }
+
+ mTmpSortedChildren.clear();
+ }
+
+ /**
+ * Update colors of "dismiss" and "empty shade" views.
+ *
+ * @param lightTheme True if light theme should be used.
+ */
+ public void updateDecorViews(boolean lightTheme) {
+ if (lightTheme == mUsingLightTheme) {
+ return;
+ }
+ mUsingLightTheme = lightTheme;
+ Context context = new ContextThemeWrapper(mContext,
+ lightTheme ? R.style.Theme_SystemUI_Light : R.style.Theme_SystemUI);
+ final int textColor = Utils.getColorAttr(context, R.attr.wallpaperTextColor);
+ mDismissView.setTextColor(textColor);
+ mEmptyShadeView.setTextColor(textColor);
+ }
+
+ public void goToFullShade(long delay) {
+ if (mDismissView != null) {
+ mDismissView.setInvisible();
+ }
+ mEmptyShadeView.setInvisible();
+ mGoToFullShadeNeedsAnimation = true;
+ mGoToFullShadeDelay = delay;
+ mNeedsAnimation = true;
+ requestChildrenUpdate();
+ }
+
+ public void cancelExpandHelper() {
+ mExpandHelper.cancel();
+ }
+
+ public void setIntrinsicPadding(int intrinsicPadding) {
+ mIntrinsicPadding = intrinsicPadding;
+ mAmbientState.setIntrinsicPadding(intrinsicPadding);
+ }
+
+ public int getIntrinsicPadding() {
+ return mIntrinsicPadding;
+ }
+
+ /**
+ * @return the y position of the first notification
+ */
+ public float getNotificationsTopY() {
+ return mTopPadding + getStackTranslation();
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return true;
+ }
+
+ /**
+ * See {@link AmbientState#setDark}.
+ */
+ public void setDark(boolean dark, boolean animate, @Nullable PointF touchWakeUpScreenLocation) {
+ if (mAmbientState.isDark() == dark) {
+ return;
+ }
+ mAmbientState.setDark(dark);
+ if (animate && mAnimationsEnabled) {
+ mDarkNeedsAnimation = true;
+ mDarkAnimationOriginIndex = findDarkAnimationOriginIndex(touchWakeUpScreenLocation);
+ mNeedsAnimation = true;
+ setBackgroundFadeAmount(0.0f);
+ } else if (!dark) {
+ setBackgroundFadeAmount(1.0f);
+ }
+ requestChildrenUpdate();
+ if (dark) {
+ mScrimController.setExcludedBackgroundArea(null);
+ } else {
+ updateBackground();
+ }
+
+ updateWillNotDraw();
+ updateContentHeight();
+ notifyHeightChangeListener(mShelf);
+ }
+
+ /**
+ * Updates whether or not this Layout will perform its own custom drawing (i.e. whether or
+ * not {@link #onDraw(Canvas)} is called). This method should be called whenever the
+ * {@link #mAmbientState}'s dark mode is toggled.
+ */
+ private void updateWillNotDraw() {
+ boolean willDraw = !mAmbientState.isDark() && mShouldDrawNotificationBackground || DEBUG;
+ setWillNotDraw(!willDraw);
+ }
+
+ private void setBackgroundFadeAmount(float fadeAmount) {
+ mBackgroundFadeAmount = fadeAmount;
+ updateBackgroundDimming();
+ }
+
+ public float getBackgroundFadeAmount() {
+ return mBackgroundFadeAmount;
+ }
+
+ private void startBackgroundFadeIn() {
+ ObjectAnimator fadeAnimator = ObjectAnimator.ofFloat(this, BACKGROUND_FADE, 0f, 1f);
+ fadeAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
+ fadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
+ fadeAnimator.start();
+ }
+
+ private int findDarkAnimationOriginIndex(@Nullable PointF screenLocation) {
+ if (screenLocation == null || screenLocation.y < mTopPadding) {
+ 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(@NonNull DismissView dismissView) {
+ int index = -1;
+ if (mDismissView != null) {
+ index = indexOfChild(mDismissView);
+ removeView(mDismissView);
+ }
+ mDismissView = dismissView;
+ addView(mDismissView, index);
+ }
+
+ public void setEmptyShadeView(EmptyShadeView emptyShadeView) {
+ int index = -1;
+ if (mEmptyShadeView != null) {
+ index = indexOfChild(mEmptyShadeView);
+ removeView(mEmptyShadeView);
+ }
+ mEmptyShadeView = emptyShadeView;
+ addView(mEmptyShadeView, index);
+ }
+
+ public void updateEmptyShadeView(boolean visible) {
+ int oldVisibility = mEmptyShadeView.willBeGone() ? GONE : mEmptyShadeView.getVisibility();
+ int newVisibility = visible ? VISIBLE : GONE;
+ if (oldVisibility != newVisibility) {
+ if (newVisibility != GONE) {
+ if (mEmptyShadeView.willBeGone()) {
+ mEmptyShadeView.cancelAnimation();
+ } else {
+ mEmptyShadeView.setInvisible();
+ }
+ mEmptyShadeView.setVisibility(newVisibility);
+ mEmptyShadeView.setWillBeGone(false);
+ updateContentHeight();
+ notifyHeightChangeListener(mEmptyShadeView);
+ } else {
+ Runnable onFinishedRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mEmptyShadeView.setVisibility(GONE);
+ mEmptyShadeView.setWillBeGone(false);
+ updateContentHeight();
+ notifyHeightChangeListener(mEmptyShadeView);
+ }
+ };
+ if (mAnimationsEnabled && mIsExpanded) {
+ mEmptyShadeView.setWillBeGone(true);
+ mEmptyShadeView.performVisibilityAnimation(false, onFinishedRunnable);
+ } else {
+ mEmptyShadeView.setInvisible();
+ onFinishedRunnable.run();
+ }
+ }
+ }
+ }
+
+ public void updateDismissView(boolean visible) {
+ if (mDismissView == null) {
+ return;
+ }
+
+ int oldVisibility = mDismissView.willBeGone() ? GONE : mDismissView.getVisibility();
+ int newVisibility = visible ? VISIBLE : GONE;
+ if (oldVisibility != newVisibility) {
+ if (newVisibility != GONE) {
+ if (mDismissView.willBeGone()) {
+ mDismissView.cancelAnimation();
+ } else {
+ mDismissView.setInvisible();
+ }
+ mDismissView.setVisibility(newVisibility);
+ mDismissView.setWillBeGone(false);
+ updateContentHeight();
+ notifyHeightChangeListener(mDismissView);
+ } else {
+ Runnable dimissHideFinishRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mDismissView.setVisibility(GONE);
+ mDismissView.setWillBeGone(false);
+ updateContentHeight();
+ notifyHeightChangeListener(mDismissView);
+ }
+ };
+ if (mDismissView.isButtonVisible() && mIsExpanded && mAnimationsEnabled) {
+ mDismissView.setWillBeGone(true);
+ mDismissView.performVisibilityAnimation(false, dimissHideFinishRunnable);
+ } else {
+ dimissHideFinishRunnable.run();
+ }
+ }
+ }
+ }
+
+ public void setDismissAllInProgress(boolean dismissAllInProgress) {
+ mDismissAllInProgress = dismissAllInProgress;
+ mAmbientState.setDismissAllInProgress(dismissAllInProgress);
+ handleDismissAllClipping();
+ }
+
+ private void handleDismissAllClipping() {
+ final int count = getChildCount();
+ boolean previousChildWillBeDismissed = false;
+ for (int i = 0; i < count; i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ if (mDismissAllInProgress && previousChildWillBeDismissed) {
+ child.setMinClipTopAmount(child.getClipTopAmount());
+ } else {
+ child.setMinClipTopAmount(0);
+ }
+ previousChildWillBeDismissed = canChildBeDismissed(child);
+ }
+ }
+
+ public boolean isDismissViewNotGone() {
+ return mDismissView != null
+ && mDismissView.getVisibility() != View.GONE
+ && !mDismissView.willBeGone();
+ }
+
+ public boolean isDismissViewVisible() {
+ return mDismissView != null && mDismissView.isVisible();
+ }
+
+ public int getDismissViewHeight() {
+ return mDismissView == null ? 0 : mDismissView.getHeight() + mPaddingBetweenElements;
+ }
+
+ public int getEmptyShadeViewHeight() {
+ return mEmptyShadeView.getHeight();
+ }
+
+ public float getBottomMostNotificationBottom() {
+ final int count = getChildCount();
+ float max = 0;
+ for (int childIdx = 0; childIdx < count; childIdx++) {
+ ExpandableView child = (ExpandableView) getChildAt(childIdx);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ float bottom = child.getTranslationY() + child.getActualHeight()
+ - child.getClipBottomAmount();
+ if (bottom > max) {
+ max = bottom;
+ }
+ }
+ return max + getStackTranslation();
+ }
+
+ public void setStatusBar(StatusBar statusBar) {
+ this.mStatusBar = statusBar;
+ }
+
+ public void setGroupManager(NotificationGroupManager groupManager) {
+ this.mGroupManager = groupManager;
+ }
+
+ public void onGoToKeyguard() {
+ requestAnimateEverything();
+ }
+
+ private void requestAnimateEverything() {
+ if (mIsExpanded && mAnimationsEnabled) {
+ mEverythingNeedsAnimation = true;
+ mNeedsAnimation = true;
+ requestChildrenUpdate();
+ }
+ }
+
+ public boolean isBelowLastNotification(float touchX, float touchY) {
+ int childCount = getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (child.getVisibility() != View.GONE) {
+ float childTop = child.getY();
+ if (childTop > touchY) {
+ // we are above a notification entirely let's abort
+ return false;
+ }
+ boolean belowChild = touchY > childTop + child.getActualHeight()
+ - child.getClipBottomAmount();
+ if (child == mDismissView) {
+ if(!belowChild && !mDismissView.isOnEmptySpace(touchX - mDismissView.getX(),
+ touchY - childTop)) {
+ // We clicked on the dismiss button
+ return false;
+ }
+ } else if (child == mEmptyShadeView) {
+ // We arrived at the empty shade view, for which we accept all clicks
+ return true;
+ } else if (!belowChild){
+ // We are on a child
+ return false;
+ }
+ }
+ }
+ return touchY > mTopPadding + mStackTranslation;
+ }
+
+ @Override
+ public void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded) {
+ boolean animated = !mGroupExpandedForMeasure && mAnimationsEnabled
+ && (mIsExpanded || changedRow.isPinned());
+ if (animated) {
+ mExpandedGroupView = changedRow;
+ mNeedsAnimation = true;
+ }
+ changedRow.setChildrenExpanded(expanded, animated);
+ if (!mGroupExpandedForMeasure) {
+ onHeightChanged(changedRow, false /* needsAnimation */);
+ }
+ runAfterAnimationFinished(new Runnable() {
+ @Override
+ public void run() {
+ changedRow.onFinishedExpansionChange();
+ }
+ });
+ }
+
+ @Override
+ public void onGroupCreatedFromChildren(NotificationGroupManager.NotificationGroup group) {
+ mStatusBar.requestNotificationUpdate();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setScrollable(mScrollable);
+ event.setScrollX(mScrollX);
+ event.setScrollY(mOwnScrollY);
+ event.setMaxScrollX(mScrollX);
+ event.setMaxScrollY(getScrollRange());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ if (mScrollable) {
+ info.setScrollable(true);
+ if (mBackwardScrollable) {
+ info.addAction(
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP);
+ }
+ if (mForwardScrollable) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN);
+ }
+ }
+ // Talkback only listenes to scroll events of certain classes, let's make us a scrollview
+ info.setClassName(ScrollView.class.getName());
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ if (!isEnabled()) {
+ return false;
+ }
+ int direction = -1;
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ // fall through
+ case android.R.id.accessibilityActionScrollDown:
+ direction = 1;
+ // fall through
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+ // fall through
+ case android.R.id.accessibilityActionScrollUp:
+ final int viewportHeight = getHeight() - mPaddingBottom - mTopPadding - mPaddingTop
+ - mShelf.getIntrinsicHeight();
+ final int targetScrollY = Math.max(0,
+ Math.min(mOwnScrollY + direction * viewportHeight, getScrollRange()));
+ if (targetScrollY != mOwnScrollY) {
+ mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScrollY - mOwnScrollY);
+ animateScroll();
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public void onGroupsChanged() {
+ mStatusBar.requestNotificationUpdate();
+ }
+
+ public void generateChildOrderChangedEvent() {
+ if (mIsExpanded && mAnimationsEnabled) {
+ mGenerateChildOrderChangedEvent = true;
+ mNeedsAnimation = true;
+ requestChildrenUpdate();
+ }
+ }
+
+ public void runAfterAnimationFinished(Runnable runnable) {
+ mAnimationFinishedRunnables.add(runnable);
+ }
+
+ public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ mHeadsUpManager = headsUpManager;
+ mAmbientState.setHeadsUpManager(headsUpManager);
+ }
+
+ public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) {
+ if (mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed)) {
+ mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp));
+ mNeedsAnimation = true;
+ if (!mIsExpanded && !isHeadsUp) {
+ row.setHeadsUpAnimatingAway(true);
+ }
+ requestChildrenUpdate();
+ }
+ }
+
+ public void setShadeExpanded(boolean shadeExpanded) {
+ mAmbientState.setShadeExpanded(shadeExpanded);
+ mStateAnimator.setShadeExpanded(shadeExpanded);
+ }
+
+ /**
+ * Set the boundary for the bottom heads up position. The heads up will always be above this
+ * position.
+ *
+ * @param height the height of the screen
+ * @param bottomBarHeight the height of the bar on the bottom
+ */
+ public void setHeadsUpBoundaries(int height, int bottomBarHeight) {
+ mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight);
+ mStateAnimator.setHeadsUpAppearHeightBottom(height);
+ requestChildrenUpdate();
+ }
+
+ public void setTrackingHeadsUp(boolean trackingHeadsUp) {
+ mTrackingHeadsUp = trackingHeadsUp;
+ }
+
+ public void setScrimController(ScrimController scrimController) {
+ mScrimController = scrimController;
+ mScrimController.setScrimBehindChangeRunnable(this::updateBackgroundDimming);
+ }
+
+ public void forceNoOverlappingRendering(boolean force) {
+ mForceNoOverlappingRendering = force;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return !mForceNoOverlappingRendering && super.hasOverlappingRendering();
+ }
+
+ public void setAnimationRunning(boolean animationRunning) {
+ if (animationRunning != mAnimationRunning) {
+ if (animationRunning) {
+ getViewTreeObserver().addOnPreDrawListener(mRunningAnimationUpdater);
+ } else {
+ getViewTreeObserver().removeOnPreDrawListener(mRunningAnimationUpdater);
+ }
+ mAnimationRunning = animationRunning;
+ updateContinuousShadowDrawing();
+ }
+ }
+
+ public boolean isExpanded() {
+ return mIsExpanded;
+ }
+
+ public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) {
+ if (mPulsing == null && pulsing == null) {
+ return;
+ }
+ mPulsing = pulsing;
+ mAmbientState.setPulsing(pulsing);
+ updateNotificationAnimationStates();
+ updateContentHeight();
+ notifyHeightChangeListener(mShelf);
+ requestChildrenUpdate();
+ }
+
+ public void setFadingOut(boolean fadingOut) {
+ if (fadingOut != mFadingOut) {
+ mFadingOut = fadingOut;
+ updateFadingState();
+ }
+ }
+
+ public void setParentNotFullyVisible(boolean parentNotFullyVisible) {
+ if (mScrimController == null) {
+ // we're not set up yet.
+ return;
+ }
+ if (parentNotFullyVisible != mParentNotFullyVisible) {
+ mParentNotFullyVisible = parentNotFullyVisible;
+ updateFadingState();
+ }
+ }
+
+ private void updateFadingState() {
+ applyCurrentBackgroundBounds();
+ updateSrcDrawing();
+ }
+
+ @Override
+ public void setAlpha(@FloatRange(from = 0.0, to = 1.0) float alpha) {
+ super.setAlpha(alpha);
+ setFadingOut(alpha != 1.0f);
+ }
+
+ public void setQsExpanded(boolean qsExpanded) {
+ mQsExpanded = qsExpanded;
+ updateAlgorithmLayoutMinHeight();
+ }
+
+ public void setOwnScrollY(int ownScrollY) {
+ if (ownScrollY != mOwnScrollY) {
+ // We still want to call the normal scrolled changed for accessibility reasons
+ onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY);
+ mOwnScrollY = ownScrollY;
+ updateForwardAndBackwardScrollability();
+ requestChildrenUpdate();
+ }
+ }
+
+ public void setShelf(NotificationShelf shelf) {
+ int index = -1;
+ if (mShelf != null) {
+ index = indexOfChild(mShelf);
+ removeView(mShelf);
+ }
+ mShelf = shelf;
+ addView(mShelf, index);
+ mAmbientState.setShelf(shelf);
+ mStateAnimator.setShelf(shelf);
+ shelf.bind(mAmbientState, this);
+ }
+
+ public NotificationShelf getNotificationShelf() {
+ return mShelf;
+ }
+
+ public void setMaxDisplayedNotifications(int maxDisplayedNotifications) {
+ if (mMaxDisplayedNotifications != maxDisplayedNotifications) {
+ mMaxDisplayedNotifications = maxDisplayedNotifications;
+ updateContentHeight();
+ notifyHeightChangeListener(mShelf);
+ }
+ }
+
+ public int getMinExpansionHeight() {
+ return mShelf.getIntrinsicHeight() - (mShelf.getIntrinsicHeight() - mStatusBarHeight) / 2;
+ }
+
+ public void setInHeadsUpPinnedMode(boolean inHeadsUpPinnedMode) {
+ mInHeadsUpPinnedMode = inHeadsUpPinnedMode;
+ updateClipping();
+ }
+
+ public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
+ mHeadsUpAnimatingAway = headsUpAnimatingAway;
+ updateClipping();
+ }
+
+ public void setStatusBarState(int statusBarState) {
+ mStatusBarState = statusBarState;
+ mAmbientState.setStatusBarState(statusBarState);
+ }
+
+ public void setExpandingVelocity(float expandingVelocity) {
+ mAmbientState.setExpandingVelocity(expandingVelocity);
+ }
+
+ public float getOpeningHeight() {
+ if (mEmptyShadeView.getVisibility() == GONE) {
+ return getMinExpansionHeight();
+ } else {
+ return getAppearEndPosition();
+ }
+ }
+
+ public void setIsFullWidth(boolean isFullWidth) {
+ mAmbientState.setPanelFullWidth(isFullWidth);
+ }
+
+ public void setUnlockHintRunning(boolean running) {
+ mAmbientState.setUnlockHintRunning(running);
+ }
+
+ public void setQsCustomizerShowing(boolean isShowing) {
+ mAmbientState.setQsCustomizerShowing(isShowing);
+ requestChildrenUpdate();
+ }
+
+ public void setHeadsUpGoingAwayAnimationsAllowed(boolean headsUpGoingAwayAnimationsAllowed) {
+ mHeadsUpGoingAwayAnimationsAllowed = headsUpGoingAwayAnimationsAllowed;
+ }
+
+ public void setDarkShelfOffsetX(int shelfOffsetX) {
+ mShelf.setDarkOffsetX(shelfOffsetX);
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println(String.format("[%s: pulsing=%s qsCustomizerShowing=%s visibility=%s"
+ + " alpha:%f scrollY:%d]",
+ this.getClass().getSimpleName(),
+ mPulsing != null ?"T":"f",
+ mAmbientState.isQsCustomizerShowing() ? "T":"f",
+ getVisibility() == View.VISIBLE ? "visible"
+ : getVisibility() == View.GONE ? "gone"
+ : "invisible",
+ getAlpha(),
+ mAmbientState.getScrollY()));
+ }
+
+ public void setTouchActive(boolean touchActive) {
+ mShelf.setTouchActive(touchActive);
+ }
+
+ /**
+ * A listener that is notified when some child locations might have changed.
+ */
+ public interface OnChildLocationsChangedListener {
+ void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
+ }
+
+ /**
+ * A listener that is notified when the empty space below the notifications is clicked on
+ */
+ public interface OnEmptySpaceClickListener {
+ void onEmptySpaceClicked(float x, float y);
+ }
+
+ /**
+ * A listener that gets notified when the overscroll at the top has changed.
+ */
+ public interface OnOverscrollTopChangedListener {
+
+ /**
+ * Notifies a listener that the overscroll has changed.
+ *
+ * @param amount the amount of overscroll, in pixels
+ * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
+ * unrubberbanded motion to directly expand overscroll view (e.g expand
+ * QS)
+ */
+ void onOverscrollTopChanged(float amount, boolean isRubberbanded);
+
+ /**
+ * Notify a listener that the scroller wants to escape from the scrolling motion and
+ * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
+ *
+ * @param velocity The velocity that the Scroller had when over flinging
+ * @param open Should the fling open or close the overscroll view.
+ */
+ void flingTopOverscroll(float velocity, boolean open);
+ }
+
+ private class NotificationSwipeHelper extends SwipeHelper
+ implements NotificationSwipeActionHelper {
+ private static final long COVER_MENU_DELAY = 4000;
+ private Runnable mFalsingCheck;
+ private Handler mHandler;
+
+ public NotificationSwipeHelper(int swipeDirection, Callback callback, Context context) {
+ super(swipeDirection, callback, context);
+ mHandler = new Handler();
+ mFalsingCheck = new Runnable() {
+ @Override
+ public void run() {
+ resetExposedMenuView(true /* animate */, true /* force */);
+ }
+ };
+ }
+
+ @Override
+ public void onDownUpdate(View currView, MotionEvent ev) {
+ mTranslatingParentView = currView;
+ if (mCurrMenuRow != null) {
+ mCurrMenuRow.onTouchEvent(currView, ev, 0 /* velocity */);
+ }
+ mCurrMenuRow = null;
+ mHandler.removeCallbacks(mFalsingCheck);
+
+ // Slide back any notifications that might be showing a menu
+ resetExposedMenuView(true /* animate */, false /* force */);
+
+ if (currView instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) currView;
+ mCurrMenuRow = row.createMenu();
+ mCurrMenuRow.setSwipeActionHelper(NotificationSwipeHelper.this);
+ mCurrMenuRow.setMenuClickListener(NotificationStackScrollLayout.this);
+ mCurrMenuRow.onTouchEvent(currView, ev, 0 /* velocity */);
+ }
+ }
+
+ @Override
+ public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) {
+ mHandler.removeCallbacks(mFalsingCheck);
+ if (mCurrMenuRow != null) {
+ mCurrMenuRow.onTouchEvent(view, ev, 0 /* velocity */);
+ }
+ }
+
+ @Override
+ public boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
+ float translation) {
+ if (mCurrMenuRow != null) {
+ return mCurrMenuRow.onTouchEvent(animView, ev, velocity);
+ }
+ return false;
+ }
+
+ @Override
+ public void dismissChild(final View view, float velocity,
+ boolean useAccelerateInterpolator) {
+ super.dismissChild(view, velocity, useAccelerateInterpolator);
+ if (mIsExpanded) {
+ // We don't want to quick-dismiss when it's a heads up as this might lead to closing
+ // of the panel early.
+ handleChildDismissed(view);
+ }
+ mStatusBar.closeAndSaveGuts(true /* removeLeavebehind */, false /* force */,
+ false /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */);
+ handleMenuCoveredOrDismissed();
+ }
+
+ @Override
+ public void snapChild(final View animView, final float targetLeft, float velocity) {
+ super.snapChild(animView, targetLeft, velocity);
+ onDragCancelled(animView);
+ if (targetLeft == 0) {
+ handleMenuCoveredOrDismissed();
+ }
+ }
+
+ @Override
+ public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) {
+ mStatusBar.setNotificationSnoozed(sbn, snoozeOption);
+ }
+
+ public boolean isFalseGesture(MotionEvent ev) {
+ return super.isFalseGesture(ev);
+ }
+
+ private void handleMenuCoveredOrDismissed() {
+ if (mMenuExposedView != null && mMenuExposedView == mTranslatingParentView) {
+ mMenuExposedView = null;
+ }
+ }
+
+ @Override
+ public Animator getViewTranslationAnimator(View v, float target,
+ AnimatorUpdateListener listener) {
+ if (v instanceof ExpandableNotificationRow) {
+ return ((ExpandableNotificationRow) v).getTranslateViewAnimator(target, listener);
+ } else {
+ return super.getViewTranslationAnimator(v, target, listener);
+ }
+ }
+
+ @Override
+ public void setTranslation(View v, float translate) {
+ ((ExpandableView) v).setTranslation(translate);
+ }
+
+ @Override
+ public float getTranslation(View v) {
+ return ((ExpandableView) v).getTranslation();
+ }
+
+ @Override
+ public void dismiss(View animView, float velocity) {
+ dismissChild(animView, velocity,
+ !swipedFastEnough(0, 0) /* useAccelerateInterpolator */);
+ }
+
+ @Override
+ public void snap(View animView, float targetLeft, float velocity) {
+ snapChild(animView, targetLeft, velocity);
+ }
+
+ @Override
+ public boolean swipedFarEnough(float translation, float viewSize) {
+ return swipedFarEnough();
+ }
+
+ @Override
+ public boolean swipedFastEnough(float translation, float velocity) {
+ return swipedFastEnough();
+ }
+
+ @Override
+ public float getMinDismissVelocity() {
+ return getEscapeVelocity();
+ }
+
+ public void onMenuShown(View animView) {
+ onDragCancelled(animView);
+
+ // If we're on the lockscreen we want to false this.
+ if (isAntiFalsingNeeded()) {
+ mHandler.removeCallbacks(mFalsingCheck);
+ mHandler.postDelayed(mFalsingCheck, COVER_MENU_DELAY);
+ }
+ }
+
+ public void closeControlsIfOutsideTouch(MotionEvent ev) {
+ NotificationGuts guts = mStatusBar.getExposedGuts();
+ View view = null;
+ if (guts != null && !guts.getGutsContent().isLeavebehind()) {
+ // Only close visible guts if they're not a leavebehind.
+ view = guts;
+ } else if (mCurrMenuRow != null && mCurrMenuRow.isMenuVisible()
+ && mTranslatingParentView != null) {
+ // Checking menu
+ view = mTranslatingParentView;
+ }
+ if (view != null && !isTouchInView(ev, view)) {
+ // Touch was outside visible guts / menu notification, close what's visible
+ mStatusBar.closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
+ true /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */);
+ resetExposedMenuView(true /* animate */, true /* force */);
+ }
+ }
+
+ public void resetExposedMenuView(boolean animate, boolean force) {
+ if (mMenuExposedView == null
+ || (!force && mMenuExposedView == mTranslatingParentView)) {
+ // If no menu is showing or it's showing for this view we do nothing.
+ return;
+ }
+ final View prevMenuExposedView = mMenuExposedView;
+ if (animate) {
+ Animator anim = getViewTranslationAnimator(prevMenuExposedView,
+ 0 /* leftTarget */, null /* updateListener */);
+ if (anim != null) {
+ anim.start();
+ }
+ } else if (mMenuExposedView instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) mMenuExposedView;
+ if (!row.isRemoved()) {
+ row.resetTranslation();
+ }
+ }
+ mMenuExposedView = null;
+ }
+ }
+
+ private boolean isTouchInView(MotionEvent ev, View view) {
+ if (view == null) {
+ return false;
+ }
+ final int height = (view instanceof ExpandableView)
+ ? ((ExpandableView) view).getActualHeight()
+ : view.getHeight();
+ final int rx = (int) ev.getRawX();
+ final int ry = (int) ev.getRawY();
+ view.getLocationOnScreen(mTempInt2);
+ final int x = mTempInt2[0];
+ final int y = mTempInt2[1];
+ Rect rect = new Rect(x, y, x + view.getWidth(), y + height);
+ boolean ret = rect.contains(rx, ry);
+ return ret;
+ }
+
+ private void updateContinuousShadowDrawing() {
+ boolean continuousShadowUpdate = mAnimationRunning
+ || !mAmbientState.getDraggedViews().isEmpty();
+ if (continuousShadowUpdate != mContinuousShadowUpdate) {
+ if (continuousShadowUpdate) {
+ getViewTreeObserver().addOnPreDrawListener(mShadowUpdater);
+ } else {
+ getViewTreeObserver().removeOnPreDrawListener(mShadowUpdater);
+ }
+ mContinuousShadowUpdate = continuousShadowUpdate;
+ }
+ }
+
+ public void resetExposedMenuView(boolean animate, boolean force) {
+ mSwipeHelper.resetExposedMenuView(animate, force);
+ }
+
+ public void closeControlsIfOutsideTouch(MotionEvent ev) {
+ mSwipeHelper.closeControlsIfOutsideTouch(ev);
+ }
+
+ static class AnimationEvent {
+
+ static AnimationFilter[] FILTERS = new AnimationFilter[] {
+
+ // ANIMATION_TYPE_ADD
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ()
+ .hasDelays(),
+
+ // ANIMATION_TYPE_REMOVE
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ()
+ .hasDelays(),
+
+ // ANIMATION_TYPE_REMOVE_SWIPED_OUT
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ()
+ .hasDelays(),
+
+ // ANIMATION_TYPE_TOP_PADDING_CHANGED
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateDimmed()
+ .animateZ(),
+
+ // ANIMATION_TYPE_START_DRAG
+ new AnimationFilter()
+ .animateShadowAlpha(),
+
+ // ANIMATION_TYPE_SNAP_BACK
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight(),
+
+ // ANIMATION_TYPE_ACTIVATED_CHILD
+ new AnimationFilter()
+ .animateZ(),
+
+ // ANIMATION_TYPE_DIMMED
+ new AnimationFilter()
+ .animateDimmed(),
+
+ // ANIMATION_TYPE_CHANGE_POSITION
+ new AnimationFilter()
+ .animateAlpha() // maybe the children change positions
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ(),
+
+ // ANIMATION_TYPE_DARK
+ null, // Unused
+
+ // ANIMATION_TYPE_GO_TO_FULL_SHADE
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateDimmed()
+ .animateZ()
+ .hasDelays(),
+
+ // ANIMATION_TYPE_HIDE_SENSITIVE
+ new AnimationFilter()
+ .animateHideSensitive(),
+
+ // ANIMATION_TYPE_VIEW_RESIZE
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ(),
+
+ // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
+ new AnimationFilter()
+ .animateAlpha()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ(),
+
+ // ANIMATION_TYPE_HEADS_UP_APPEAR
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ(),
+
+ // ANIMATION_TYPE_HEADS_UP_DISAPPEAR
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ(),
+
+ // ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ()
+ .hasDelays(),
+
+ // ANIMATION_TYPE_HEADS_UP_OTHER
+ new AnimationFilter()
+ .animateShadowAlpha()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ(),
+
+ // ANIMATION_TYPE_EVERYTHING
+ new AnimationFilter()
+ .animateAlpha()
+ .animateShadowAlpha()
+ .animateDark()
+ .animateDimmed()
+ .animateHideSensitive()
+ .animateHeight()
+ .animateTopInset()
+ .animateY()
+ .animateZ(),
+ };
+
+ static int[] LENGTHS = new int[] {
+
+ // ANIMATION_TYPE_ADD
+ StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
+
+ // ANIMATION_TYPE_REMOVE
+ StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
+
+ // ANIMATION_TYPE_REMOVE_SWIPED_OUT
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+ // ANIMATION_TYPE_TOP_PADDING_CHANGED
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+ // ANIMATION_TYPE_START_DRAG
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+ // ANIMATION_TYPE_SNAP_BACK
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+ // ANIMATION_TYPE_ACTIVATED_CHILD
+ StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
+
+ // ANIMATION_TYPE_DIMMED
+ StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
+
+ // ANIMATION_TYPE_CHANGE_POSITION
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+ // ANIMATION_TYPE_DARK
+ StackStateAnimator.ANIMATION_DURATION_WAKEUP,
+
+ // ANIMATION_TYPE_GO_TO_FULL_SHADE
+ StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE,
+
+ // ANIMATION_TYPE_HIDE_SENSITIVE
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+ // ANIMATION_TYPE_VIEW_RESIZE
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+ // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+ // ANIMATION_TYPE_HEADS_UP_APPEAR
+ StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR,
+
+ // ANIMATION_TYPE_HEADS_UP_DISAPPEAR
+ StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
+
+ // ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
+ StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
+
+ // ANIMATION_TYPE_HEADS_UP_OTHER
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+ // ANIMATION_TYPE_EVERYTHING
+ StackStateAnimator.ANIMATION_DURATION_STANDARD,
+ };
+
+ static final int ANIMATION_TYPE_ADD = 0;
+ static final int ANIMATION_TYPE_REMOVE = 1;
+ static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
+ static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
+ static final int ANIMATION_TYPE_START_DRAG = 4;
+ static final int ANIMATION_TYPE_SNAP_BACK = 5;
+ static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
+ static final int ANIMATION_TYPE_DIMMED = 7;
+ static final int ANIMATION_TYPE_CHANGE_POSITION = 8;
+ static final int ANIMATION_TYPE_DARK = 9;
+ static final int ANIMATION_TYPE_GO_TO_FULL_SHADE = 10;
+ static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11;
+ static final int ANIMATION_TYPE_VIEW_RESIZE = 12;
+ static final int ANIMATION_TYPE_GROUP_EXPANSION_CHANGED = 13;
+ static final int ANIMATION_TYPE_HEADS_UP_APPEAR = 14;
+ static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR = 15;
+ static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK = 16;
+ static final int ANIMATION_TYPE_HEADS_UP_OTHER = 17;
+ static final int ANIMATION_TYPE_EVERYTHING = 18;
+
+ 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;
+ boolean headsUpFromBottom;
+
+ AnimationEvent(View view, int type) {
+ this(view, type, LENGTHS[type]);
+ }
+
+ AnimationEvent(View view, int type, AnimationFilter filter) {
+ this(view, type, LENGTHS[type], filter);
+ }
+
+ AnimationEvent(View view, int type, long length) {
+ this(view, type, length, FILTERS[type]);
+ }
+
+ AnimationEvent(View view, int type, long length, AnimationFilter filter) {
+ eventStartTime = AnimationUtils.currentAnimationTimeMillis();
+ changingView = view;
+ animationType = type;
+ this.length = length;
+ this.filter = filter;
+ }
+
+ /**
+ * Combines the length of several animation events into a single value.
+ *
+ * @param events The events of the lengths to combine.
+ * @return The combined length. Depending on the event types, this might be the maximum of
+ * all events or the length of a specific event.
+ */
+ static long combineLength(ArrayList<AnimationEvent> events) {
+ long length = 0;
+ int size = events.size();
+ for (int i = 0; i < size; i++) {
+ AnimationEvent event = events.get(i);
+ length = Math.max(length, event.length);
+ if (event.animationType == ANIMATION_TYPE_GO_TO_FULL_SHADE) {
+ return event.length;
+ }
+ }
+ return length;
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/ScrollContainer.java b/com/android/systemui/statusbar/stack/ScrollContainer.java
new file mode 100644
index 0000000..b9d12ce
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/ScrollContainer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 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.stack;
+
+import android.view.View;
+
+/**
+ * Interface for container layouts that scroll and listen for long presses. A child that
+ * wants to handle long press can use this to cancel the parents long press logic or request
+ * to be made visible by scrolling to it.
+ */
+public interface ScrollContainer {
+ /**
+ * Request that the view does not perform long press for the current touch.
+ */
+ void requestDisallowLongPress();
+
+ /**
+ * Request that the view is made visible by scrolling to it.
+ * Return true if it scrolls.
+ */
+ boolean scrollTo(View v);
+
+ /**
+ * Like {@link #scrollTo(View)}, but keeps the scroll locked until the user
+ * scrolls, or {@param v} loses focus or is detached.
+ */
+ void lockScrollTo(View v);
+
+ /**
+ * Request that the view does not dismiss for the current touch.
+ */
+ void requestDisallowDismiss();
+}
diff --git a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
new file mode 100644
index 0000000..c060b08
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -0,0 +1,599 @@
+/*
+ * 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.stack;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.DismissView;
+import com.android.systemui.statusbar.EmptyShadeView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * The Algorithm of the {@link com.android.systemui.statusbar.stack
+ * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
+ * .stack.StackScrollState}
+ */
+public class StackScrollAlgorithm {
+
+ private static final String LOG_TAG = "StackScrollAlgorithm";
+
+ private int mPaddingBetweenElements;
+ private int mIncreasedPaddingBetweenElements;
+ private int mCollapsedSize;
+
+ private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
+ private boolean mIsExpanded;
+ private boolean mClipNotificationScrollToTop;
+ private int mStatusBarHeight;
+
+ public StackScrollAlgorithm(Context context) {
+ initView(context);
+ }
+
+ public void initView(Context context) {
+ initConstants(context);
+ }
+
+ private void initConstants(Context context) {
+ Resources res = context.getResources();
+ mPaddingBetweenElements = res.getDimensionPixelSize(
+ R.dimen.notification_divider_height);
+ mIncreasedPaddingBetweenElements =
+ res.getDimensionPixelSize(R.dimen.notification_divider_height_increased);
+ mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
+ mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height);
+ mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
+ }
+
+ public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
+ // The state of the local variables are saved in an algorithmState to easily subdivide it
+ // into multiple phases.
+ StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
+
+ // First we reset the view states to their default values.
+ resultState.resetViewStates();
+
+ initAlgorithmState(resultState, algorithmState, ambientState);
+
+ updatePositionsForState(resultState, algorithmState, ambientState);
+
+ updateZValuesForState(resultState, algorithmState, ambientState);
+
+ updateHeadsUpStates(resultState, algorithmState, ambientState);
+
+ handleDraggedViews(ambientState, resultState, algorithmState);
+ updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
+ updateClipping(resultState, algorithmState, ambientState);
+ updateSpeedBumpState(resultState, algorithmState, ambientState);
+ updateShelfState(resultState, ambientState);
+ getNotificationChildrenStates(resultState, algorithmState);
+ }
+
+ private void getNotificationChildrenStates(StackScrollState resultState,
+ StackScrollAlgorithmState algorithmState) {
+ int childCount = algorithmState.visibleChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ ExpandableView v = algorithmState.visibleChildren.get(i);
+ if (v instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ row.getChildrenStates(resultState);
+ }
+ }
+ }
+
+ private void updateSpeedBumpState(StackScrollState resultState,
+ StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+ int childCount = algorithmState.visibleChildren.size();
+ int belowSpeedBump = ambientState.getSpeedBumpIndex();
+ for (int i = 0; i < childCount; i++) {
+ View child = algorithmState.visibleChildren.get(i);
+ ExpandableViewState childViewState = resultState.getViewStateForView(child);
+
+ // The speed bump can also be gone, so equality needs to be taken when comparing
+ // indices.
+ childViewState.belowSpeedBump = i >= belowSpeedBump;
+ }
+
+ }
+ private void updateShelfState(StackScrollState resultState, AmbientState ambientState) {
+ NotificationShelf shelf = ambientState.getShelf();
+ shelf.updateState(resultState, ambientState);
+ }
+
+ private void updateClipping(StackScrollState resultState,
+ StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+ float drawStart = !ambientState.isOnKeyguard() ? ambientState.getTopPadding()
+ + ambientState.getStackTranslation() : 0;
+ float previousNotificationEnd = 0;
+ float previousNotificationStart = 0;
+ int childCount = algorithmState.visibleChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ ExpandableView child = algorithmState.visibleChildren.get(i);
+ ExpandableViewState state = resultState.getViewStateForView(child);
+ if (!child.mustStayOnScreen()) {
+ previousNotificationEnd = Math.max(drawStart, previousNotificationEnd);
+ previousNotificationStart = Math.max(drawStart, previousNotificationStart);
+ }
+ float newYTranslation = state.yTranslation;
+ float newHeight = state.height;
+ float newNotificationEnd = newYTranslation + newHeight;
+ boolean isHeadsUp = (child instanceof ExpandableNotificationRow)
+ && ((ExpandableNotificationRow) child).isPinned();
+ if (mClipNotificationScrollToTop
+ && !state.inShelf && newYTranslation < previousNotificationEnd
+ && (!isHeadsUp || ambientState.isShadeExpanded())) {
+ // The previous view is overlapping on top, clip!
+ float overlapAmount = previousNotificationEnd - newYTranslation;
+ state.clipTopAmount = (int) overlapAmount;
+ } else {
+ state.clipTopAmount = 0;
+ }
+
+ if (!child.isTransparent()) {
+ // Only update the previous values if we are not transparent,
+ // otherwise we would clip to a transparent view.
+ previousNotificationEnd = newNotificationEnd;
+ previousNotificationStart = newYTranslation;
+ }
+ }
+ }
+
+ public static boolean canChildBeDismissed(View v) {
+ if (!(v instanceof ExpandableNotificationRow)) {
+ return false;
+ }
+ ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ if (row.areGutsExposed()) {
+ return false;
+ }
+ return row.canViewBeDismissed();
+ }
+
+ /**
+ * Updates the dimmed, activated and hiding sensitive states of the children.
+ */
+ private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
+ StackScrollState resultState, StackScrollAlgorithmState algorithmState) {
+ boolean dimmed = ambientState.isDimmed();
+ boolean dark = ambientState.isDark();
+ boolean hideSensitive = ambientState.isHideSensitive();
+ View activatedChild = ambientState.getActivatedChild();
+ int childCount = algorithmState.visibleChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ View child = algorithmState.visibleChildren.get(i);
+ ExpandableViewState childViewState = resultState.getViewStateForView(child);
+ childViewState.dimmed = dimmed;
+ childViewState.dark = dark;
+ childViewState.hideSensitive = hideSensitive;
+ boolean isActivatedChild = activatedChild == child;
+ if (dimmed && isActivatedChild) {
+ childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements();
+ }
+ }
+ }
+
+ /**
+ * Handle the special state when views are being dragged
+ */
+ private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
+ StackScrollAlgorithmState algorithmState) {
+ ArrayList<View> draggedViews = ambientState.getDraggedViews();
+ for (View draggedView : draggedViews) {
+ int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
+ if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
+ View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
+ if (!draggedViews.contains(nextChild)) {
+ // only if the view is not dragged itself we modify its state to be fully
+ // visible
+ ExpandableViewState viewState = resultState.getViewStateForView(
+ nextChild);
+ // The child below the dragged one must be fully visible
+ if (ambientState.isShadeExpanded()) {
+ viewState.shadowAlpha = 1;
+ viewState.hidden = false;
+ }
+ }
+
+ // Lets set the alpha to the one it currently has, as its currently being dragged
+ ExpandableViewState viewState = resultState.getViewStateForView(draggedView);
+ // The dragged child should keep the set alpha
+ viewState.alpha = draggedView.getAlpha();
+ }
+ }
+ }
+
+ /**
+ * Initialize the algorithm state like updating the visible children.
+ */
+ private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state,
+ AmbientState ambientState) {
+ float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
+
+ int scrollY = ambientState.getScrollY();
+
+ // Due to the overScroller, the stackscroller can have negative scroll state. This is
+ // already accounted for by the top padding and doesn't need an additional adaption
+ scrollY = Math.max(0, scrollY);
+ state.scrollY = (int) (scrollY + bottomOverScroll);
+
+ //now init the visible children and update paddings
+ ViewGroup hostView = resultState.getHostView();
+ int childCount = hostView.getChildCount();
+ state.visibleChildren.clear();
+ state.visibleChildren.ensureCapacity(childCount);
+ state.paddingMap.clear();
+ int notGoneIndex = 0;
+ ExpandableView lastView = null;
+ int firstHiddenIndex = ambientState.isDark()
+ ? (ambientState.hasPulsingNotifications() ? 1 : 0)
+ : childCount;
+
+ // The goal here is to fill the padding map, by iterating over how much padding each child
+ // needs. The map is thereby reused, by first filling it with the padding amount and when
+ // iterating over it again, it's filled with the actual resolved value.
+
+ for (int i = 0; i < childCount; i++) {
+ ExpandableView v = (ExpandableView) hostView.getChildAt(i);
+ if (v.getVisibility() != View.GONE) {
+ if (v == ambientState.getShelf()) {
+ continue;
+ }
+ if (i >= firstHiddenIndex) {
+ // we need normal padding now, to be in sync with what the stack calculates
+ lastView = null;
+ ExpandableViewState viewState = resultState.getViewStateForView(v);
+ viewState.hidden = true;
+ }
+ notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
+ float increasedPadding = v.getIncreasedPaddingAmount();
+ if (increasedPadding != 0.0f) {
+ state.paddingMap.put(v, increasedPadding);
+ if (lastView != null) {
+ Float prevValue = state.paddingMap.get(lastView);
+ float newValue = getPaddingForValue(increasedPadding);
+ if (prevValue != null) {
+ float prevPadding = getPaddingForValue(prevValue);
+ if (increasedPadding > 0) {
+ newValue = NotificationUtils.interpolate(
+ prevPadding,
+ newValue,
+ increasedPadding);
+ } else if (prevValue > 0) {
+ newValue = NotificationUtils.interpolate(
+ newValue,
+ prevPadding,
+ prevValue);
+ }
+ }
+ state.paddingMap.put(lastView, newValue);
+ }
+ } else if (lastView != null) {
+
+ // Let's now resolve the value to an actual padding
+ float newValue = getPaddingForValue(state.paddingMap.get(lastView));
+ state.paddingMap.put(lastView, newValue);
+ }
+ if (v instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+
+ // handle the notgoneIndex for the children as well
+ List<ExpandableNotificationRow> children =
+ row.getNotificationChildren();
+ if (row.isSummaryWithChildren() && children != null) {
+ for (ExpandableNotificationRow childRow : children) {
+ if (childRow.getVisibility() != View.GONE) {
+ ExpandableViewState childState
+ = resultState.getViewStateForView(childRow);
+ childState.notGoneIndex = notGoneIndex;
+ notGoneIndex++;
+ }
+ }
+ }
+ }
+ lastView = v;
+ }
+ }
+ }
+
+ private float getPaddingForValue(Float increasedPadding) {
+ if (increasedPadding == null) {
+ return mPaddingBetweenElements;
+ } else if (increasedPadding >= 0.0f) {
+ return NotificationUtils.interpolate(
+ mPaddingBetweenElements,
+ mIncreasedPaddingBetweenElements,
+ increasedPadding);
+ } else {
+ return NotificationUtils.interpolate(
+ 0,
+ mPaddingBetweenElements,
+ 1.0f + increasedPadding);
+ }
+ }
+
+ private int updateNotGoneIndex(StackScrollState resultState,
+ StackScrollAlgorithmState state, int notGoneIndex,
+ ExpandableView v) {
+ ExpandableViewState viewState = resultState.getViewStateForView(v);
+ viewState.notGoneIndex = notGoneIndex;
+ state.visibleChildren.add(v);
+ notGoneIndex++;
+ return notGoneIndex;
+ }
+
+ /**
+ * Determine the positions for the views. This is the main part of the algorithm.
+ *
+ * @param resultState The result state to update if a change to the properties of a child occurs
+ * @param algorithmState The state in which the current pass of the algorithm is currently in
+ * @param ambientState The current ambient state
+ */
+ private void updatePositionsForState(StackScrollState resultState,
+ StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+
+ // The y coordinate of the current child.
+ float currentYPosition = -algorithmState.scrollY;
+ int childCount = algorithmState.visibleChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ currentYPosition = updateChild(i, resultState, algorithmState, ambientState,
+ currentYPosition);
+ }
+ }
+
+ protected float updateChild(int i, StackScrollState resultState,
+ StackScrollAlgorithmState algorithmState, AmbientState ambientState,
+ float currentYPosition) {
+ ExpandableView child = algorithmState.visibleChildren.get(i);
+ ExpandableViewState childViewState = resultState.getViewStateForView(child);
+ childViewState.location = ExpandableViewState.LOCATION_UNKNOWN;
+ int paddingAfterChild = getPaddingAfterChild(algorithmState, child);
+ int childHeight = getMaxAllowedChildHeight(child);
+ childViewState.yTranslation = currentYPosition;
+ boolean isDismissView = child instanceof DismissView;
+ boolean isEmptyShadeView = child instanceof EmptyShadeView;
+
+ childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
+ if (isDismissView) {
+ childViewState.yTranslation = Math.min(childViewState.yTranslation,
+ ambientState.getInnerHeight() - childHeight);
+ } else if (isEmptyShadeView) {
+ childViewState.yTranslation = ambientState.getInnerHeight() - childHeight
+ + ambientState.getStackTranslation() * 0.25f;
+ } else {
+ clampPositionToShelf(childViewState, ambientState);
+ }
+
+ currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild;
+ if (currentYPosition <= 0) {
+ childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
+ }
+ if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) {
+ Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
+ }
+
+ childViewState.yTranslation += ambientState.getTopPadding()
+ + ambientState.getStackTranslation();
+ return currentYPosition;
+ }
+
+ protected int getPaddingAfterChild(StackScrollAlgorithmState algorithmState,
+ ExpandableView child) {
+ return algorithmState.getPaddingAfterChild(child);
+ }
+
+ private void updateHeadsUpStates(StackScrollState resultState,
+ StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+ int childCount = algorithmState.visibleChildren.size();
+ ExpandableNotificationRow topHeadsUpEntry = null;
+ for (int i = 0; i < childCount; i++) {
+ View child = algorithmState.visibleChildren.get(i);
+ if (!(child instanceof ExpandableNotificationRow)) {
+ break;
+ }
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ if (!row.isHeadsUp()) {
+ break;
+ }
+ ExpandableViewState childState = resultState.getViewStateForView(row);
+ if (topHeadsUpEntry == null) {
+ topHeadsUpEntry = row;
+ childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
+ }
+ boolean isTopEntry = topHeadsUpEntry == row;
+ float unmodifiedEndLocation = childState.yTranslation + childState.height;
+ if (mIsExpanded) {
+ // Ensure that the heads up is always visible even when scrolled off
+ clampHunToTop(ambientState, row, childState);
+ if (i == 0 && ambientState.isAboveShelf(row)) {
+ // the first hun can't get off screen.
+ clampHunToMaxTranslation(ambientState, row, childState);
+ childState.hidden = false;
+ }
+ }
+ if (row.isPinned()) {
+ childState.yTranslation = Math.max(childState.yTranslation, 0);
+ childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
+ childState.hidden = false;
+ ExpandableViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
+ if (!isTopEntry && (!mIsExpanded
+ || unmodifiedEndLocation < topState.yTranslation + topState.height)) {
+ // Ensure that a headsUp doesn't vertically extend further than the heads-up at
+ // the top most z-position
+ childState.height = row.getIntrinsicHeight();
+ childState.yTranslation = topState.yTranslation + topState.height
+ - childState.height;
+ }
+ }
+ if (row.isHeadsUpAnimatingAway()) {
+ childState.hidden = false;
+ }
+ }
+ }
+
+ private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
+ ExpandableViewState childState) {
+ float newTranslation = Math.max(ambientState.getTopPadding()
+ + ambientState.getStackTranslation(), childState.yTranslation);
+ childState.height = (int) Math.max(childState.height - (newTranslation
+ - childState.yTranslation), row.getCollapsedHeight());
+ childState.yTranslation = newTranslation;
+ }
+
+ private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
+ ExpandableViewState childState) {
+ float newTranslation;
+ float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
+ float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
+ + ambientState.getStackTranslation();
+ maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
+ float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
+ newTranslation = Math.min(childState.yTranslation, bottomPosition);
+ childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
+ - newTranslation);
+ childState.yTranslation = newTranslation;
+ }
+
+ /**
+ * Clamp the height of the child down such that its end is at most on the beginning of
+ * the shelf.
+ *
+ * @param childViewState the view state of the child
+ * @param ambientState the ambient state
+ */
+ private void clampPositionToShelf(ExpandableViewState childViewState,
+ AmbientState ambientState) {
+ int shelfStart = ambientState.getInnerHeight()
+ - ambientState.getShelf().getIntrinsicHeight();
+ childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart);
+ if (childViewState.yTranslation >= shelfStart) {
+ childViewState.hidden = true;
+ childViewState.inShelf = true;
+ }
+ if (!ambientState.isShadeExpanded()) {
+ childViewState.height = (int) (mStatusBarHeight - childViewState.yTranslation);
+ }
+ }
+
+ protected int getMaxAllowedChildHeight(View child) {
+ if (child instanceof ExpandableView) {
+ ExpandableView expandableView = (ExpandableView) child;
+ return expandableView.getIntrinsicHeight();
+ }
+ return child == null? mCollapsedSize : child.getHeight();
+ }
+
+ /**
+ * Calculate the Z positions for all children based on the number of items in both stacks and
+ * save it in the resultState
+ * @param resultState The result state to update the zTranslation values
+ * @param algorithmState The state in which the current pass of the algorithm is currently in
+ * @param ambientState The ambient state of the algorithm
+ */
+ private void updateZValuesForState(StackScrollState resultState,
+ StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+ int childCount = algorithmState.visibleChildren.size();
+ float childrenOnTop = 0.0f;
+ for (int i = childCount - 1; i >= 0; i--) {
+ childrenOnTop = updateChildZValue(i, childrenOnTop,
+ resultState, algorithmState, ambientState);
+ }
+ }
+
+ protected float updateChildZValue(int i, float childrenOnTop,
+ StackScrollState resultState, StackScrollAlgorithmState algorithmState,
+ AmbientState ambientState) {
+ ExpandableView child = algorithmState.visibleChildren.get(i);
+ ExpandableViewState childViewState = resultState.getViewStateForView(child);
+ int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements();
+ float baseZ = ambientState.getBaseZHeight();
+ if (child.mustStayOnScreen() && !ambientState.isDozingAndNotPulsing(child)
+ && childViewState.yTranslation < ambientState.getTopPadding()
+ + ambientState.getStackTranslation()) {
+ if (childrenOnTop != 0.0f) {
+ childrenOnTop++;
+ } else {
+ float overlap = ambientState.getTopPadding()
+ + ambientState.getStackTranslation() - childViewState.yTranslation;
+ childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
+ }
+ childViewState.zTranslation = baseZ
+ + childrenOnTop * zDistanceBetweenElements;
+ } else if (i == 0 && ambientState.isAboveShelf(child)) {
+ // In case this is a new view that has never been measured before, we don't want to
+ // elevate if we are currently expanded more then the notification
+ int shelfHeight = ambientState.getShelf().getIntrinsicHeight();
+ float shelfStart = ambientState.getInnerHeight()
+ - shelfHeight + ambientState.getTopPadding()
+ + ambientState.getStackTranslation();
+ float notificationEnd = childViewState.yTranslation + child.getPinnedHeadsUpHeight()
+ + mPaddingBetweenElements;
+ if (shelfStart > notificationEnd) {
+ childViewState.zTranslation = baseZ;
+ } else {
+ float factor = (notificationEnd - shelfStart) / shelfHeight;
+ factor = Math.min(factor, 1.0f);
+ childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements;
+ }
+ } else {
+ childViewState.zTranslation = baseZ;
+ }
+ return childrenOnTop;
+ }
+
+ public void setIsExpanded(boolean isExpanded) {
+ this.mIsExpanded = isExpanded;
+ }
+
+ public class StackScrollAlgorithmState {
+
+ /**
+ * The scroll position of the algorithm
+ */
+ public int scrollY;
+
+ /**
+ * The children from the host view which are not gone.
+ */
+ public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
+
+ /**
+ * The padding after each child measured in pixels.
+ */
+ public final HashMap<ExpandableView, Float> paddingMap = new HashMap<>();
+
+ public int getPaddingAfterChild(ExpandableView child) {
+ Float padding = paddingMap.get(child);
+ if (padding == null) {
+ // Should only happen for the last view
+ return mPaddingBetweenElements;
+ }
+ return (int) padding.floatValue();
+ }
+ }
+
+}
diff --git a/com/android/systemui/statusbar/stack/StackScrollState.java b/com/android/systemui/statusbar/stack/StackScrollState.java
new file mode 100644
index 0000000..e3c746b
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/StackScrollState.java
@@ -0,0 +1,117 @@
+/*
+ * 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.stack;
+
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+
+import java.util.List;
+import java.util.WeakHashMap;
+
+/**
+ * A state of a {@link com.android.systemui.statusbar.stack.NotificationStackScrollLayout} which
+ * can be applied to a viewGroup.
+ */
+public class StackScrollState {
+
+ private static final String CHILD_NOT_FOUND_TAG = "StackScrollStateNoSuchChild";
+
+ private final ViewGroup mHostView;
+ private WeakHashMap<ExpandableView, ExpandableViewState> mStateMap;
+
+ public StackScrollState(ViewGroup hostView) {
+ mHostView = hostView;
+ mStateMap = new WeakHashMap<>();
+ }
+
+ public ViewGroup getHostView() {
+ return mHostView;
+ }
+
+ public void resetViewStates() {
+ int numChildren = mHostView.getChildCount();
+ for (int i = 0; i < numChildren; i++) {
+ ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
+ resetViewState(child);
+
+ // handling reset for child notifications
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+ List<ExpandableNotificationRow> children =
+ row.getNotificationChildren();
+ if (row.isSummaryWithChildren() && children != null) {
+ for (ExpandableNotificationRow childRow : children) {
+ resetViewState(childRow);
+ }
+ }
+ }
+ }
+ }
+
+ private void resetViewState(ExpandableView view) {
+ ExpandableViewState viewState = mStateMap.get(view);
+ if (viewState == null) {
+ viewState = view.createNewViewState(this);
+ mStateMap.put(view, viewState);
+ }
+ // initialize with the default values of the view
+ viewState.height = view.getIntrinsicHeight();
+ viewState.gone = view.getVisibility() == View.GONE;
+ viewState.alpha = 1f;
+ viewState.shadowAlpha = 1f;
+ viewState.notGoneIndex = -1;
+ viewState.xTranslation = view.getTranslationX();
+ viewState.hidden = false;
+ viewState.scaleX = view.getScaleX();
+ viewState.scaleY = view.getScaleY();
+ viewState.inShelf = false;
+ }
+
+ public ExpandableViewState getViewStateForView(View requestedView) {
+ return mStateMap.get(requestedView);
+ }
+
+ public void removeViewStateForView(View child) {
+ mStateMap.remove(child);
+ }
+
+ /**
+ * Apply the properties saved in {@link #mStateMap} to the children of the {@link #mHostView}.
+ * The properties are only applied if they effectively changed.
+ */
+ public void apply() {
+ int numChildren = mHostView.getChildCount();
+ for (int i = 0; i < numChildren; i++) {
+ ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
+ ExpandableViewState state = mStateMap.get(child);
+ if (state == null) {
+ Log.wtf(CHILD_NOT_FOUND_TAG, "No child state was found when applying this state " +
+ "to the hostView");
+ continue;
+ }
+ if (state.gone) {
+ continue;
+ }
+ state.applyToView(child);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/StackStateAnimator.java b/com/android/systemui/statusbar/stack/StackStateAnimator.java
new file mode 100644
index 0000000..f78a718
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -0,0 +1,521 @@
+/*
+ * 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.stack;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationShelf;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Stack;
+
+/**
+ * An stack state animator which handles animations to new StackScrollStates
+ */
+public class StackStateAnimator {
+
+ public static final int ANIMATION_DURATION_STANDARD = 360;
+ public static final int ANIMATION_DURATION_WAKEUP = 200;
+ public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
+ public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
+ public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
+ public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
+ public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 650;
+ public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 230;
+ 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 DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
+ public static final int ANIMATION_DELAY_HEADS_UP = 120;
+
+ private final int mGoToFullShadeAppearingTranslation;
+ private final ExpandableViewState mTmpState = new ExpandableViewState();
+ private final AnimationProperties mAnimationProperties;
+ public NotificationStackScrollLayout mHostLayout;
+ private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
+ new ArrayList<>();
+ private ArrayList<View> mNewAddChildren = new ArrayList<>();
+ private HashSet<View> mHeadsUpAppearChildren = new HashSet<>();
+ private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>();
+ private HashSet<Animator> mAnimatorSet = new HashSet<>();
+ private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
+ private AnimationFilter mAnimationFilter = new AnimationFilter();
+ private long mCurrentLength;
+ private long mCurrentAdditionalDelay;
+
+ /** The current index for the last child which was not added in this event set. */
+ private int mCurrentLastNotAddedIndex;
+ private ValueAnimator mTopOverScrollAnimator;
+ private ValueAnimator mBottomOverScrollAnimator;
+ private int mHeadsUpAppearHeightBottom;
+ private boolean mShadeExpanded;
+ private ArrayList<View> mChildrenToClearFromOverlay = new ArrayList<>();
+ private NotificationShelf mShelf;
+
+ public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
+ mHostLayout = hostLayout;
+ mGoToFullShadeAppearingTranslation =
+ hostLayout.getContext().getResources().getDimensionPixelSize(
+ R.dimen.go_to_full_shade_appearing_translation);
+ mAnimationProperties = new AnimationProperties() {
+ @Override
+ public AnimationFilter getAnimationFilter() {
+ return mAnimationFilter;
+ }
+
+ @Override
+ public AnimatorListenerAdapter getAnimationFinishListener() {
+ return getGlobalAnimationFinishedListener();
+ }
+
+ @Override
+ public boolean wasAdded(View view) {
+ return mNewAddChildren.contains(view);
+ }
+
+ @Override
+ public Interpolator getCustomInterpolator(View child, Property property) {
+ if (mHeadsUpAppearChildren.contains(child) && View.TRANSLATION_Y.equals(property)) {
+ return Interpolators.HEADS_UP_APPEAR;
+ }
+ return null;
+ }
+ };
+ }
+
+ public boolean isRunning() {
+ return !mAnimatorSet.isEmpty();
+ }
+
+ public void startAnimationForEvents(
+ ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
+ StackScrollState finalState, long additionalDelay) {
+
+ processAnimationEvents(mAnimationEvents, finalState);
+
+ int childCount = mHostLayout.getChildCount();
+ mAnimationFilter.applyCombination(mNewEvents);
+ mCurrentAdditionalDelay = additionalDelay;
+ mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
+ mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState);
+ for (int i = 0; i < childCount; i++) {
+ final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
+
+ ExpandableViewState viewState = finalState.getViewStateForView(child);
+ if (viewState == null || child.getVisibility() == View.GONE
+ || applyWithoutAnimation(child, viewState, finalState)) {
+ continue;
+ }
+
+ initAnimationProperties(finalState, child, viewState);
+ viewState.animateTo(child, mAnimationProperties);
+ }
+ if (!isRunning()) {
+ // no child has preformed any animation, lets finish
+ onAnimationFinished();
+ }
+ mHeadsUpAppearChildren.clear();
+ mHeadsUpDisappearChildren.clear();
+ mNewEvents.clear();
+ mNewAddChildren.clear();
+ }
+
+ private void initAnimationProperties(StackScrollState finalState, ExpandableView child,
+ ExpandableViewState viewState) {
+ boolean wasAdded = mAnimationProperties.wasAdded(child);
+ mAnimationProperties.duration = mCurrentLength;
+ adaptDurationWhenGoingToFullShade(child, viewState, wasAdded);
+ mAnimationProperties.delay = 0;
+ if (wasAdded || mAnimationFilter.hasDelays
+ && (viewState.yTranslation != child.getTranslationY()
+ || viewState.zTranslation != child.getTranslationZ()
+ || viewState.alpha != child.getAlpha()
+ || viewState.height != child.getActualHeight()
+ || viewState.clipTopAmount != child.getClipTopAmount()
+ || viewState.dark != child.isDark()
+ || viewState.shadowAlpha != child.getShadowAlpha())) {
+ mAnimationProperties.delay = mCurrentAdditionalDelay
+ + calculateChildAnimationDelay(viewState, finalState);
+ }
+ }
+
+ private void adaptDurationWhenGoingToFullShade(ExpandableView child,
+ ExpandableViewState viewState, boolean wasAdded) {
+ if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) {
+ child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation);
+ float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex;
+ longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f);
+ mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 +
+ (long) (100 * longerDurationFactor);
+ }
+ }
+
+ /**
+ * Determines if a view should not perform an animation and applies it directly.
+ *
+ * @return true if no animation should be performed
+ */
+ private boolean applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState,
+ StackScrollState finalState) {
+ if (mShadeExpanded) {
+ return false;
+ }
+ if (ViewState.isAnimatingY(child)) {
+ // A Y translation animation is running
+ return false;
+ }
+ if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) {
+ // This is a heads up animation
+ return false;
+ }
+ if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) {
+ // This is another headsUp which might move. Let's animate!
+ return false;
+ }
+ viewState.applyToView(child);
+ return true;
+ }
+
+ private int findLastNotAddedIndex(StackScrollState finalState) {
+ int childCount = mHostLayout.getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
+
+ ExpandableViewState viewState = finalState.getViewStateForView(child);
+ if (viewState == null || child.getVisibility() == View.GONE) {
+ continue;
+ }
+ if (!mNewAddChildren.contains(child)) {
+ return viewState.notGoneIndex;
+ }
+ }
+ return -1;
+ }
+
+ private long calculateChildAnimationDelay(ExpandableViewState viewState,
+ StackScrollState finalState) {
+ if (mAnimationFilter.hasGoToFullShadeEvent) {
+ return calculateDelayGoToFullShade(viewState);
+ }
+ if (mAnimationFilter.hasHeadsUpDisappearClickEvent) {
+ return ANIMATION_DELAY_HEADS_UP;
+ }
+ long minDelay = 0;
+ for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
+ long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
+ switch (event.animationType) {
+ case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
+ int ownIndex = viewState.notGoneIndex;
+ int changingIndex = finalState
+ .getViewStateForView(event.changingView).notGoneIndex;
+ int difference = Math.abs(ownIndex - changingIndex);
+ difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
+ difference - 1));
+ long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
+ minDelay = Math.max(delay, minDelay);
+ break;
+ }
+ case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
+ delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
+ case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
+ int ownIndex = viewState.notGoneIndex;
+ boolean noNextView = event.viewAfterChangingView == null;
+ View viewAfterChangingView = noNextView
+ ? mHostLayout.getLastChildNotGone()
+ : event.viewAfterChangingView;
+ if (viewAfterChangingView == null) {
+ // This can happen when the last view in the list is removed.
+ // Since the shelf is still around and the only view, the code still goes
+ // in here and tries to calculate the delay for it when case its properties
+ // have changed.
+ continue;
+ }
+ int nextIndex = finalState
+ .getViewStateForView(viewAfterChangingView).notGoneIndex;
+ if (ownIndex >= nextIndex) {
+ // we only have the view afterwards
+ ownIndex++;
+ }
+ int difference = Math.abs(ownIndex - nextIndex);
+ difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
+ difference - 1));
+ long delay = difference * delayPerElement;
+ minDelay = Math.max(delay, minDelay);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return minDelay;
+ }
+
+ private long calculateDelayGoToFullShade(ExpandableViewState viewState) {
+ int shelfIndex = mShelf.getNotGoneIndex();
+ float index = viewState.notGoneIndex;
+ long result = 0;
+ if (index > shelfIndex) {
+ float diff = index - shelfIndex;
+ diff = (float) Math.pow(diff, 0.7f);
+ result += (long) (diff * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE * 0.25);
+ index = shelfIndex;
+ }
+ index = (float) Math.pow(index, 0.7f);
+ result += (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
+ return result;
+ }
+
+ /**
+ * @return an adapter which ensures that onAnimationFinished is called once no animation is
+ * running anymore
+ */
+ private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
+ if (!mAnimationListenerPool.empty()) {
+ return mAnimationListenerPool.pop();
+ }
+
+ // We need to create a new one, no reusable ones found
+ return new AnimatorListenerAdapter() {
+ private boolean mWasCancelled;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimatorSet.remove(animation);
+ if (mAnimatorSet.isEmpty() && !mWasCancelled) {
+ onAnimationFinished();
+ }
+ mAnimationListenerPool.push(this);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mWasCancelled = true;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mWasCancelled = false;
+ mAnimatorSet.add(animation);
+ }
+ };
+ }
+
+ private void onAnimationFinished() {
+ mHostLayout.onChildAnimationFinished();
+ for (View v : mChildrenToClearFromOverlay) {
+ removeFromOverlay(v);
+ }
+ mChildrenToClearFromOverlay.clear();
+ }
+
+ /**
+ * Process the animationEvents for a new animation
+ *
+ * @param animationEvents the animation events for the animation to perform
+ * @param finalState the final state to animate to
+ */
+ private void processAnimationEvents(
+ ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
+ StackScrollState finalState) {
+ for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
+ final ExpandableView changingView = (ExpandableView) event.changingView;
+ if (event.animationType ==
+ NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
+
+ // This item is added, initialize it's properties.
+ ExpandableViewState viewState = finalState
+ .getViewStateForView(changingView);
+ if (viewState == null) {
+ // The position for this child was never generated, let's continue.
+ continue;
+ }
+ viewState.applyToView(changingView);
+ mNewAddChildren.add(changingView);
+
+ } else if (event.animationType ==
+ NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
+ if (changingView.getVisibility() != View.VISIBLE) {
+ removeFromOverlay(changingView);
+ continue;
+ }
+
+ // Find the amount to translate up. This is needed in order to understand the
+ // direction of the remove animation (either downwards or upwards)
+ ExpandableViewState viewState = finalState
+ .getViewStateForView(event.viewAfterChangingView);
+ int actualHeight = changingView.getActualHeight();
+ // upwards by default
+ float translationDirection = -1.0f;
+ if (viewState != null) {
+ float ownPosition = changingView.getTranslationY();
+ if (changingView instanceof ExpandableNotificationRow
+ && event.viewAfterChangingView instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow changingRow =
+ (ExpandableNotificationRow) changingView;
+ ExpandableNotificationRow nextRow =
+ (ExpandableNotificationRow) event.viewAfterChangingView;
+ if (changingRow.isRemoved()
+ && changingRow.wasChildInGroupWhenRemoved()
+ && !nextRow.isChildInGroup()) {
+ // the next row isn't actually a child from a group! Let's
+ // compare absolute positions!
+ ownPosition = changingRow.getTranslationWhenRemoved();
+ }
+ }
+ // there was a view after this one, Approximate the distance the next child
+ // travelled
+ translationDirection = ((viewState.yTranslation
+ - (ownPosition + actualHeight / 2.0f)) * 2 /
+ actualHeight);
+ translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
+
+ }
+ changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
+ translationDirection, new Runnable() {
+ @Override
+ public void run() {
+ // remove the temporary overlay
+ removeFromOverlay(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 was fully swiped out. So let's remove it
+ mHostLayout.getOverlay().remove(changingView);
+ if (Math.abs(changingView.getTranslation()) == changingView.getWidth()
+ && changingView.getTransientContainer() != null) {
+ changingView.getTransientContainer().removeTransientView(changingView);
+ }
+ } else if (event.animationType == NotificationStackScrollLayout
+ .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
+ row.prepareExpansionChanged(finalState);
+ } else if (event.animationType == NotificationStackScrollLayout
+ .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
+ // This item is added, initialize it's properties.
+ ExpandableViewState viewState = finalState.getViewStateForView(changingView);
+ mTmpState.copyFrom(viewState);
+ if (event.headsUpFromBottom) {
+ mTmpState.yTranslation = mHeadsUpAppearHeightBottom;
+ } else {
+ mTmpState.yTranslation = -mTmpState.height;
+ }
+ mHeadsUpAppearChildren.add(changingView);
+ mTmpState.applyToView(changingView);
+ } else if (event.animationType == NotificationStackScrollLayout
+ .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR ||
+ event.animationType == NotificationStackScrollLayout
+ .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
+ mHeadsUpDisappearChildren.add(changingView);
+ if (changingView.getParent() == null) {
+ // This notification was actually removed, so we need to add it to the overlay
+ mHostLayout.getOverlay().add(changingView);
+ mTmpState.initFrom(changingView);
+ mTmpState.yTranslation = -changingView.getActualHeight();
+ // We temporarily enable Y animations, the real filter will be combined
+ // afterwards anyway
+ mAnimationFilter.animateY = true;
+ mAnimationProperties.delay =
+ event.animationType == NotificationStackScrollLayout
+ .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
+ ? ANIMATION_DELAY_HEADS_UP
+ : 0;
+ mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR;
+ mTmpState.animateTo(changingView, mAnimationProperties);
+ mChildrenToClearFromOverlay.add(changingView);
+ }
+ }
+ mNewEvents.add(event);
+ }
+ }
+
+ public static void removeFromOverlay(View changingView) {
+ ViewGroup parent = (ViewGroup) changingView.getParent();
+ if (parent != null) {
+ parent.removeView(changingView);
+ }
+ }
+
+ public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
+ final boolean isRubberbanded) {
+ final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
+ if (targetAmount == startOverScrollAmount) {
+ return;
+ }
+ cancelOverScrollAnimators(onTop);
+ ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
+ targetAmount);
+ overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
+ overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float currentOverScroll = (float) animation.getAnimatedValue();
+ mHostLayout.setOverScrollAmount(
+ currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
+ isRubberbanded);
+ }
+ });
+ overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ overScrollAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (onTop) {
+ mTopOverScrollAnimator = null;
+ } else {
+ mBottomOverScrollAnimator = null;
+ }
+ }
+ });
+ overScrollAnimator.start();
+ if (onTop) {
+ mTopOverScrollAnimator = overScrollAnimator;
+ } else {
+ mBottomOverScrollAnimator = overScrollAnimator;
+ }
+ }
+
+ public void cancelOverScrollAnimators(boolean onTop) {
+ ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
+ if (currentAnimator != null) {
+ currentAnimator.cancel();
+ }
+ }
+
+ public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
+ mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
+ }
+
+ public void setShadeExpanded(boolean shadeExpanded) {
+ mShadeExpanded = shadeExpanded;
+ }
+
+ public void setShelf(NotificationShelf shelf) {
+ mShelf = shelf;
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/ViewState.java b/com/android/systemui/statusbar/stack/ViewState.java
new file mode 100644
index 0000000..27b730c
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/ViewState.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2015 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.stack;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.app.Notification;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.PropertyAnimator;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+/**
+ * A state of a view. This can be used to apply a set of view properties to a view with
+ * {@link com.android.systemui.statusbar.stack.StackScrollState} or start animations with
+ * {@link com.android.systemui.statusbar.stack.StackStateAnimator}.
+*/
+public class ViewState {
+
+ /**
+ * Some animation properties that can be used to update running animations but not creating
+ * any new ones.
+ */
+ protected static final AnimationProperties NO_NEW_ANIMATIONS = new AnimationProperties() {
+ AnimationFilter mAnimationFilter = new AnimationFilter();
+ @Override
+ public AnimationFilter getAnimationFilter() {
+ return mAnimationFilter;
+ }
+ };
+ private static final int TAG_ANIMATOR_TRANSLATION_X = R.id.translation_x_animator_tag;
+ private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
+ private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
+ private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag;
+ private static final int TAG_END_TRANSLATION_X = R.id.translation_x_animator_end_value_tag;
+ private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag;
+ private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag;
+ private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag;
+ private static final int TAG_START_TRANSLATION_X = R.id.translation_x_animator_start_value_tag;
+ private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag;
+ private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
+ private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
+
+ private static final PropertyAnimator.AnimatableProperty SCALE_X_PROPERTY
+ = new PropertyAnimator.AnimatableProperty() {
+
+ @Override
+ public int getAnimationStartTag() {
+ return R.id.scale_x_animator_start_value_tag;
+ }
+
+ @Override
+ public int getAnimationEndTag() {
+ return R.id.scale_x_animator_end_value_tag;
+ }
+
+ @Override
+ public int getAnimatorTag() {
+ return R.id.scale_x_animator_tag;
+ }
+
+ @Override
+ public Property getProperty() {
+ return View.SCALE_X;
+ }
+ };
+
+ private static final PropertyAnimator.AnimatableProperty SCALE_Y_PROPERTY
+ = new PropertyAnimator.AnimatableProperty() {
+
+ @Override
+ public int getAnimationStartTag() {
+ return R.id.scale_y_animator_start_value_tag;
+ }
+
+ @Override
+ public int getAnimationEndTag() {
+ return R.id.scale_y_animator_end_value_tag;
+ }
+
+ @Override
+ public int getAnimatorTag() {
+ return R.id.scale_y_animator_tag;
+ }
+
+ @Override
+ public Property getProperty() {
+ return View.SCALE_Y;
+ }
+ };
+
+ public float alpha;
+ public float xTranslation;
+ public float yTranslation;
+ public float zTranslation;
+ public boolean gone;
+ public boolean hidden;
+ public float scaleX = 1.0f;
+ public float scaleY = 1.0f;
+
+ public void copyFrom(ViewState viewState) {
+ alpha = viewState.alpha;
+ xTranslation = viewState.xTranslation;
+ yTranslation = viewState.yTranslation;
+ zTranslation = viewState.zTranslation;
+ gone = viewState.gone;
+ hidden = viewState.hidden;
+ scaleX = viewState.scaleX;
+ scaleY = viewState.scaleY;
+ }
+
+ public void initFrom(View view) {
+ alpha = view.getAlpha();
+ xTranslation = view.getTranslationX();
+ yTranslation = view.getTranslationY();
+ zTranslation = view.getTranslationZ();
+ gone = view.getVisibility() == View.GONE;
+ hidden = view.getVisibility() == View.INVISIBLE;
+ scaleX = view.getScaleX();
+ scaleY = view.getScaleY();
+ }
+
+ /**
+ * Applies a {@link ViewState} to a normal view.
+ */
+ public void applyToView(View view) {
+ if (this.gone) {
+ // don't do anything with it
+ return;
+ }
+
+ // apply xTranslation
+ boolean animatingX = isAnimating(view, TAG_ANIMATOR_TRANSLATION_X);
+ if (animatingX) {
+ updateAnimationX(view);
+ } else if (view.getTranslationX() != this.xTranslation){
+ view.setTranslationX(this.xTranslation);
+ }
+
+ // apply yTranslation
+ boolean animatingY = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Y);
+ if (animatingY) {
+ updateAnimationY(view);
+ } else if (view.getTranslationY() != this.yTranslation) {
+ view.setTranslationY(this.yTranslation);
+ }
+
+ // apply zTranslation
+ boolean animatingZ = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Z);
+ if (animatingZ) {
+ updateAnimationZ(view);
+ } else if (view.getTranslationZ() != this.zTranslation) {
+ view.setTranslationZ(this.zTranslation);
+ }
+
+ // apply scaleX
+ boolean animatingScaleX = isAnimating(view, SCALE_X_PROPERTY);
+ if (animatingScaleX) {
+ updateAnimation(view, SCALE_X_PROPERTY, scaleX);
+ } else if (view.getScaleX() != scaleX) {
+ view.setScaleX(scaleX);
+ }
+
+ // apply scaleY
+ boolean animatingScaleY = isAnimating(view, SCALE_Y_PROPERTY);
+ if (animatingScaleY) {
+ updateAnimation(view, SCALE_Y_PROPERTY, scaleY);
+ } else if (view.getScaleY() != scaleY) {
+ view.setScaleY(scaleY);
+ }
+
+ int oldVisibility = view.getVisibility();
+ boolean becomesInvisible = this.alpha == 0.0f
+ || (this.hidden && (!isAnimating(view) || oldVisibility != View.VISIBLE));
+ boolean animatingAlpha = isAnimating(view, TAG_ANIMATOR_ALPHA);
+ if (animatingAlpha) {
+ updateAlphaAnimation(view);
+ } else if (view.getAlpha() != this.alpha) {
+ // apply layer type
+ boolean becomesFullyVisible = this.alpha == 1.0f;
+ boolean newLayerTypeIsHardware = !becomesInvisible && !becomesFullyVisible
+ && view.hasOverlappingRendering();
+ int layerType = view.getLayerType();
+ int newLayerType = newLayerTypeIsHardware
+ ? View.LAYER_TYPE_HARDWARE
+ : View.LAYER_TYPE_NONE;
+ if (layerType != newLayerType) {
+ view.setLayerType(newLayerType, null);
+ }
+
+ // apply alpha
+ view.setAlpha(this.alpha);
+ }
+
+ // apply visibility
+ int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE;
+ if (newVisibility != oldVisibility) {
+ if (!(view instanceof ExpandableView) || !((ExpandableView) view).willBeGone()) {
+ // We don't want views to change visibility when they are animating to GONE
+ view.setVisibility(newVisibility);
+ }
+ }
+ }
+
+ public boolean isAnimating(View view) {
+ if (isAnimating(view, TAG_ANIMATOR_TRANSLATION_X)) {
+ return true;
+ }
+ if (isAnimating(view, TAG_ANIMATOR_TRANSLATION_Y)) {
+ return true;
+ }
+ if (isAnimating(view, TAG_ANIMATOR_TRANSLATION_Z)) {
+ return true;
+ }
+ if (isAnimating(view, TAG_ANIMATOR_ALPHA)) {
+ return true;
+ }
+ if (isAnimating(view, SCALE_X_PROPERTY)) {
+ return true;
+ }
+ if (isAnimating(view, SCALE_Y_PROPERTY)) {
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean isAnimating(View view, int tag) {
+ return getChildTag(view, tag) != null;
+ }
+
+ public static boolean isAnimating(View view, PropertyAnimator.AnimatableProperty property) {
+ return getChildTag(view, property.getAnimatorTag()) != null;
+ }
+
+ /**
+ * Start an animation to this viewstate
+ * @param child the view to animate
+ * @param animationProperties the properties of the animation
+ */
+ public void animateTo(View child, AnimationProperties animationProperties) {
+ boolean wasVisible = child.getVisibility() == View.VISIBLE;
+ final float alpha = this.alpha;
+ if (!wasVisible && (alpha != 0 || child.getAlpha() != 0)
+ && !this.gone && !this.hidden) {
+ child.setVisibility(View.VISIBLE);
+ }
+ float childAlpha = child.getAlpha();
+ boolean alphaChanging = this.alpha != childAlpha;
+ if (child instanceof ExpandableView) {
+ // We don't want views to change visibility when they are animating to GONE
+ alphaChanging &= !((ExpandableView) child).willBeGone();
+ }
+
+ // start translationX animation
+ if (child.getTranslationX() != this.xTranslation) {
+ startXTranslationAnimation(child, animationProperties);
+ } else {
+ abortAnimation(child, TAG_ANIMATOR_TRANSLATION_X);
+ }
+
+ // start translationY animation
+ if (child.getTranslationY() != this.yTranslation) {
+ startYTranslationAnimation(child, animationProperties);
+ } else {
+ abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Y);
+ }
+
+ // start translationZ animation
+ if (child.getTranslationZ() != this.zTranslation) {
+ startZTranslationAnimation(child, animationProperties);
+ } else {
+ abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Z);
+ }
+
+ // start scaleX animation
+ if (child.getScaleX() != scaleX) {
+ PropertyAnimator.startAnimation(child, SCALE_X_PROPERTY, scaleX, animationProperties);
+ } else {
+ abortAnimation(child, SCALE_X_PROPERTY.getAnimatorTag());
+ }
+
+ // start scaleX animation
+ if (child.getScaleY() != scaleY) {
+ PropertyAnimator.startAnimation(child, SCALE_Y_PROPERTY, scaleY, animationProperties);
+ } else {
+ abortAnimation(child, SCALE_Y_PROPERTY.getAnimatorTag());
+ }
+
+ // start alpha animation
+ if (alphaChanging) {
+ startAlphaAnimation(child, animationProperties);
+ } else {
+ abortAnimation(child, TAG_ANIMATOR_ALPHA);
+ }
+ }
+
+ private void updateAlphaAnimation(View view) {
+ startAlphaAnimation(view, NO_NEW_ANIMATIONS);
+ }
+
+ private void startAlphaAnimation(final View child, AnimationProperties properties) {
+ Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
+ Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
+ final float newEndValue = this.alpha;
+ if (previousEndValue != null && previousEndValue == newEndValue) {
+ return;
+ }
+ ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
+ AnimationFilter filter = properties.getAnimationFilter();
+ if (!filter.animateAlpha) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ float relativeDiff = newEndValue - previousEndValue;
+ float newStartValue = previousStartValue + relativeDiff;
+ values[0].setFloatValues(newStartValue, newEndValue);
+ child.setTag(TAG_START_ALPHA, newStartValue);
+ child.setTag(TAG_END_ALPHA, newEndValue);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ child.setAlpha(newEndValue);
+ if (newEndValue == 0) {
+ child.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA,
+ child.getAlpha(), newEndValue);
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ // Handle layer type
+ child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ animator.addListener(new AnimatorListenerAdapter() {
+ public boolean mWasCancelled;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ child.setLayerType(View.LAYER_TYPE_NONE, null);
+ 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);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mWasCancelled = true;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mWasCancelled = false;
+ }
+ });
+ long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+ animator.setDuration(newDuration);
+ if (properties.delay > 0 && (previousAnimator == null
+ || previousAnimator.getAnimatedFraction() == 0)) {
+ animator.setStartDelay(properties.delay);
+ }
+ AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+
+ startAnimator(animator, listener);
+ child.setTag(TAG_ANIMATOR_ALPHA, animator);
+ child.setTag(TAG_START_ALPHA, child.getAlpha());
+ child.setTag(TAG_END_ALPHA, newEndValue);
+ }
+
+ private void updateAnimationZ(View view) {
+ startZTranslationAnimation(view, NO_NEW_ANIMATIONS);
+ }
+
+ private void updateAnimation(View view, PropertyAnimator.AnimatableProperty property,
+ float endValue) {
+ PropertyAnimator.startAnimation(view, property, endValue, NO_NEW_ANIMATIONS);
+ }
+
+ private void startZTranslationAnimation(final View child, AnimationProperties properties) {
+ Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
+ Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
+ float newEndValue = this.zTranslation;
+ if (previousEndValue != null && previousEndValue == newEndValue) {
+ return;
+ }
+ ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
+ AnimationFilter filter = properties.getAnimationFilter();
+ if (!filter.animateZ) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ float relativeDiff = newEndValue - previousEndValue;
+ float newStartValue = previousStartValue + relativeDiff;
+ values[0].setFloatValues(newStartValue, newEndValue);
+ child.setTag(TAG_START_TRANSLATION_Z, newStartValue);
+ child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ child.setTranslationZ(newEndValue);
+ }
+ }
+
+ ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
+ child.getTranslationZ(), newEndValue);
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+ animator.setDuration(newDuration);
+ if (properties.delay > 0 && (previousAnimator == null
+ || previousAnimator.getAnimatedFraction() == 0)) {
+ animator.setStartDelay(properties.delay);
+ }
+ AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
+ child.setTag(TAG_START_TRANSLATION_Z, null);
+ child.setTag(TAG_END_TRANSLATION_Z, null);
+ }
+ });
+ startAnimator(animator, listener);
+ child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
+ child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
+ child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
+ }
+
+ private void updateAnimationX(View view) {
+ startXTranslationAnimation(view, NO_NEW_ANIMATIONS);
+ }
+
+ private void startXTranslationAnimation(final View child, AnimationProperties properties) {
+ Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_X);
+ Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_X);
+ float newEndValue = this.xTranslation;
+ if (previousEndValue != null && previousEndValue == newEndValue) {
+ return;
+ }
+ ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_X);
+ AnimationFilter filter = properties.getAnimationFilter();
+ if (!filter.animateX) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ float relativeDiff = newEndValue - previousEndValue;
+ float newStartValue = previousStartValue + relativeDiff;
+ values[0].setFloatValues(newStartValue, newEndValue);
+ child.setTag(TAG_START_TRANSLATION_X, newStartValue);
+ child.setTag(TAG_END_TRANSLATION_X, newEndValue);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ child.setTranslationX(newEndValue);
+ return;
+ }
+ }
+
+ ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_X,
+ child.getTranslationX(), newEndValue);
+ Interpolator customInterpolator = properties.getCustomInterpolator(child,
+ View.TRANSLATION_X);
+ Interpolator interpolator = customInterpolator != null ? customInterpolator
+ : Interpolators.FAST_OUT_SLOW_IN;
+ animator.setInterpolator(interpolator);
+ long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+ animator.setDuration(newDuration);
+ if (properties.delay > 0 && (previousAnimator == null
+ || previousAnimator.getAnimatedFraction() == 0)) {
+ animator.setStartDelay(properties.delay);
+ }
+ AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ child.setTag(TAG_ANIMATOR_TRANSLATION_X, null);
+ child.setTag(TAG_START_TRANSLATION_X, null);
+ child.setTag(TAG_END_TRANSLATION_X, null);
+ }
+ });
+ startAnimator(animator, listener);
+ child.setTag(TAG_ANIMATOR_TRANSLATION_X, animator);
+ child.setTag(TAG_START_TRANSLATION_X, child.getTranslationX());
+ child.setTag(TAG_END_TRANSLATION_X, newEndValue);
+ }
+
+ private void updateAnimationY(View view) {
+ startYTranslationAnimation(view, NO_NEW_ANIMATIONS);
+ }
+
+ private void startYTranslationAnimation(final View child, AnimationProperties properties) {
+ Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
+ Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
+ float newEndValue = this.yTranslation;
+ if (previousEndValue != null && previousEndValue == newEndValue) {
+ return;
+ }
+ ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
+ AnimationFilter filter = properties.getAnimationFilter();
+ if (!filter.shouldAnimateY(child)) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ float relativeDiff = newEndValue - previousEndValue;
+ float newStartValue = previousStartValue + relativeDiff;
+ values[0].setFloatValues(newStartValue, newEndValue);
+ child.setTag(TAG_START_TRANSLATION_Y, newStartValue);
+ child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ child.setTranslationY(newEndValue);
+ return;
+ }
+ }
+
+ ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
+ child.getTranslationY(), newEndValue);
+ Interpolator customInterpolator = properties.getCustomInterpolator(child,
+ View.TRANSLATION_Y);
+ Interpolator interpolator = customInterpolator != null ? customInterpolator
+ : Interpolators.FAST_OUT_SLOW_IN;
+ animator.setInterpolator(interpolator);
+ long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+ animator.setDuration(newDuration);
+ if (properties.delay > 0 && (previousAnimator == null
+ || previousAnimator.getAnimatedFraction() == 0)) {
+ animator.setStartDelay(properties.delay);
+ }
+ AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ HeadsUpManager.setIsClickedNotification(child, false);
+ child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
+ child.setTag(TAG_START_TRANSLATION_Y, null);
+ child.setTag(TAG_END_TRANSLATION_Y, null);
+ onYTranslationAnimationFinished(child);
+ }
+ });
+ startAnimator(animator, listener);
+ child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
+ child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
+ child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
+ }
+
+ protected void onYTranslationAnimationFinished(View view) {
+ if (hidden && !gone) {
+ view.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ public static void startAnimator(Animator animator, AnimatorListenerAdapter listener) {
+ if (listener != null) {
+ // Even if there's a delay we'd want to notify it of the start immediately.
+ listener.onAnimationStart(animator);
+ }
+ animator.start();
+ }
+
+ public static <T> T getChildTag(View child, int tag) {
+ return (T) child.getTag(tag);
+ }
+
+ protected void abortAnimation(View child, int animatorTag) {
+ Animator previousAnimator = getChildTag(child, animatorTag);
+ if (previousAnimator != null) {
+ previousAnimator.cancel();
+ }
+ }
+
+ /**
+ * Cancel the previous animator and get the duration of the new animation.
+ *
+ * @param duration the new duration
+ * @param previousAnimator the animator which was running before
+ * @return the new duration
+ */
+ public static long cancelAnimatorAndGetNewDuration(long duration,
+ ValueAnimator previousAnimator) {
+ long newDuration = duration;
+ if (previousAnimator != null) {
+ // We take either the desired length of the new animation or the remaining time of
+ // the previous animator, whichever is longer.
+ newDuration = Math.max(previousAnimator.getDuration()
+ - previousAnimator.getCurrentPlayTime(), newDuration);
+ previousAnimator.cancel();
+ }
+ return newDuration;
+ }
+
+ /**
+ * Get the end value of the yTranslation animation running on a view or the yTranslation
+ * if no animation is running.
+ */
+ public static float getFinalTranslationY(View view) {
+ if (view == null) {
+ return 0;
+ }
+ ValueAnimator yAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Y);
+ if (yAnimator == null) {
+ return view.getTranslationY();
+ } else {
+ return getChildTag(view, TAG_END_TRANSLATION_Y);
+ }
+ }
+
+ /**
+ * Get the end value of the zTranslation animation running on a view or the zTranslation
+ * if no animation is running.
+ */
+ public static float getFinalTranslationZ(View view) {
+ if (view == null) {
+ return 0;
+ }
+ ValueAnimator zAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Z);
+ if (zAnimator == null) {
+ return view.getTranslationZ();
+ } else {
+ return getChildTag(view, TAG_END_TRANSLATION_Z);
+ }
+ }
+
+ public static boolean isAnimatingY(View child) {
+ return getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y) != null;
+ }
+
+ public void cancelAnimations(View view) {
+ Animator animator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_X);
+ if (animator != null) {
+ animator.cancel();
+ }
+ animator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Y);
+ if (animator != null) {
+ animator.cancel();
+ }
+ animator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Z);
+ if (animator != null) {
+ animator.cancel();
+ }
+ animator = getChildTag(view, TAG_ANIMATOR_ALPHA);
+ if (animator != null) {
+ animator.cancel();
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/tv/TvStatusBar.java b/com/android/systemui/statusbar/tv/TvStatusBar.java
new file mode 100644
index 0000000..b5d92a5
--- /dev/null
+++ b/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 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.tv;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.SystemUI;
+import com.android.systemui.pip.tv.PipManager;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.CommandQueue.Callbacks;
+
+import java.util.ArrayList;
+
+/**
+ * Status bar implementation for "large screen" products that mostly present no on-screen nav
+ */
+
+public class TvStatusBar extends SystemUI implements Callbacks {
+
+ private IStatusBarService mBarService;
+
+ @Override
+ public void start() {
+ putComponent(TvStatusBar.class, this);
+ CommandQueue commandQueue = getComponent(CommandQueue.class);
+ commandQueue.addCallbacks(this);
+ int[] switches = new int[9];
+ ArrayList<IBinder> binders = new ArrayList<>();
+ ArrayList<String> iconSlots = new ArrayList<>();
+ ArrayList<StatusBarIcon> icons = new ArrayList<>();
+ Rect fullscreenStackBounds = new Rect();
+ Rect dockedStackBounds = new Rect();
+ mBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ try {
+ mBarService.registerStatusBar(commandQueue, iconSlots, icons, switches, binders,
+ fullscreenStackBounds, dockedStackBounds);
+ } catch (RemoteException ex) {
+ // If the system process isn't there we're doomed anyway.
+ }
+ }
+
+}