diff options
Diffstat (limited to 'packages/SystemUI/src')
102 files changed, 15899 insertions, 2171 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java index 13aafb2..19d06be 100755 --- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java @@ -27,7 +27,6 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.os.BatteryManager; @@ -40,20 +39,20 @@ public class BatteryMeterView extends View implements DemoMode { public static final String TAG = BatteryMeterView.class.getSimpleName(); public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST"; - public static final boolean ENABLE_PERCENT = true; - public static final boolean SINGLE_DIGIT_PERCENT = false; - public static final boolean SHOW_100_PERCENT = false; + private static final boolean ENABLE_PERCENT = true; + private static final boolean SINGLE_DIGIT_PERCENT = false; + private static final boolean SHOW_100_PERCENT = false; - public static final int FULL = 96; - public static final int EMPTY = 4; + private static final int FULL = 96; + private static final int EMPTY = 4; - public static final float SUBPIXEL = 0.4f; // inset rects for softer edges + private static final float SUBPIXEL = 0.4f; // inset rects for softer edges + private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction - int[] mColors; + private final int[] mColors; boolean mShowPercent = true; - Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint; - int mButtonHeight; + private final Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint; private float mTextHeight, mWarningTextHeight; private int mHeight; @@ -65,9 +64,12 @@ public class BatteryMeterView extends View implements DemoMode { private final RectF mFrame = new RectF(); private final RectF mButtonFrame = new RectF(); - private final RectF mClipFrame = new RectF(); private final RectF mBoltFrame = new RectF(); + private final Path mShapePath = new Path(); + private final Path mClipPath = new Path(); + private final Path mTextPath = new Path(); + private class BatteryTracker extends BroadcastReceiver { public static final int UNKNOWN_LEVEL = -1; @@ -176,6 +178,10 @@ public class BatteryMeterView extends View implements DemoMode { super(context, attrs, defStyle); final Resources res = context.getResources(); + TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView, + defStyle, 0); + final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor, + res.getColor(R.color.batterymeter_frame_color)); TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels); TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values); @@ -187,17 +193,16 @@ public class BatteryMeterView extends View implements DemoMode { } levels.recycle(); colors.recycle(); + atts.recycle(); mShowPercent = ENABLE_PERCENT && 0 != Settings.System.getInt( context.getContentResolver(), "status_bar_show_battery_percent", 0); - mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol); mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mFramePaint.setColor(res.getColor(R.color.batterymeter_frame_color)); + mFramePaint.setColor(frameColor); mFramePaint.setDither(true); mFramePaint.setStrokeWidth(0); mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE); - mFramePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP)); mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBatteryPaint.setDither(true); @@ -205,8 +210,7 @@ public class BatteryMeterView extends View implements DemoMode { mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE); mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mTextPaint.setColor(0xFFFFFFFF); - Typeface font = Typeface.create("sans-serif-condensed", Typeface.NORMAL); + Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD); mTextPaint.setTypeface(font); mTextPaint.setTextAlign(Paint.Align.CENTER); @@ -218,11 +222,9 @@ public class BatteryMeterView extends View implements DemoMode { mChargeColor = getResources().getColor(R.color.batterymeter_charge_color); - mBoltPaint = new Paint(); - mBoltPaint.setAntiAlias(true); + mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBoltPaint.setColor(res.getColor(R.color.batterymeter_bolt_color)); mBoltPoints = loadBoltPoints(res); - setLayerType(View.LAYER_TYPE_SOFTWARE, null); } private static float[] loadBoltPoints(Resources res) { @@ -270,36 +272,34 @@ public class BatteryMeterView extends View implements DemoMode { final int pl = getPaddingLeft(); final int pr = getPaddingRight(); final int pb = getPaddingBottom(); - int height = mHeight - pt - pb; - int width = mWidth - pl - pr; + final int height = mHeight - pt - pb; + final int width = mWidth - pl - pr; - mButtonHeight = (int) (height * 0.12f); + final int buttonHeight = (int) (height * 0.12f); mFrame.set(0, 0, width, height); mFrame.offset(pl, pt); + // button-frame: area above the battery body mButtonFrame.set( mFrame.left + width * 0.25f, mFrame.top, mFrame.right - width * 0.25f, - mFrame.top + mButtonHeight + 5 /*cover frame border of intersecting area*/); + mFrame.top + buttonHeight); mButtonFrame.top += SUBPIXEL; mButtonFrame.left += SUBPIXEL; mButtonFrame.right -= SUBPIXEL; - mFrame.top += mButtonHeight; + // frame: battery body area + mFrame.top += buttonHeight; mFrame.left += SUBPIXEL; mFrame.top += SUBPIXEL; mFrame.right -= SUBPIXEL; mFrame.bottom -= SUBPIXEL; - // first, draw the battery shape - c.drawRect(mFrame, mFramePaint); - - // fill 'er up - final int color = tracker.plugged ? mChargeColor : getColorForLevel(level); - mBatteryPaint.setColor(color); + // set the battery charging color + mBatteryPaint.setColor(tracker.plugged ? mChargeColor : getColorForLevel(level)); if (level >= FULL) { drawFrac = 1f; @@ -307,18 +307,23 @@ public class BatteryMeterView extends View implements DemoMode { drawFrac = 0f; } - c.drawRect(mButtonFrame, drawFrac == 1f ? mBatteryPaint : mFramePaint); - - mClipFrame.set(mFrame); - mClipFrame.top += (mFrame.height() * (1f - drawFrac)); - - c.save(Canvas.CLIP_SAVE_FLAG); - c.clipRect(mClipFrame); - c.drawRect(mFrame, mBatteryPaint); - c.restore(); + final float levelTop = drawFrac == 1f ? mButtonFrame.top + : (mFrame.top + (mFrame.height() * (1f - drawFrac))); + + // define the battery shape + mShapePath.reset(); + mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top); + mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top); + mShapePath.lineTo(mButtonFrame.right, mFrame.top); + mShapePath.lineTo(mFrame.right, mFrame.top); + mShapePath.lineTo(mFrame.right, mFrame.bottom); + mShapePath.lineTo(mFrame.left, mFrame.bottom); + mShapePath.lineTo(mFrame.left, mFrame.top); + mShapePath.lineTo(mButtonFrame.left, mFrame.top); + mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top); if (tracker.plugged) { - // draw the bolt + // define the bolt shape final float bl = mFrame.left + mFrame.width() / 4.5f; final float bt = mFrame.top + mFrame.height() / 6f; final float br = mFrame.right - mFrame.width() / 7f; @@ -339,24 +344,61 @@ public class BatteryMeterView extends View implements DemoMode { mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); } - c.drawPath(mBoltPath, mBoltPaint); - } else if (level <= EMPTY) { - final float x = mWidth * 0.5f; - final float y = (mHeight + mWarningTextHeight) * 0.48f; - c.drawText(mWarningString, x, y, mWarningTextPaint); - } else if (mShowPercent && !(tracker.level == 100 && !SHOW_100_PERCENT)) { + + float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top); + boltPct = Math.min(Math.max(boltPct, 0), 1); + if (boltPct <= BOLT_LEVEL_THRESHOLD) { + // draw the bolt if opaque + c.drawPath(mBoltPath, mBoltPaint); + } else { + // otherwise cut the bolt out of the overall shape + mShapePath.op(mBoltPath, Path.Op.DIFFERENCE); + } + } + + // compute percentage text + boolean pctOpaque = false; + float pctX = 0, pctY = 0; + String pctText = null; + if (!tracker.plugged && level > EMPTY && mShowPercent + && !(tracker.level == 100 && !SHOW_100_PERCENT)) { + mTextPaint.setColor(getColorForLevel(level)); mTextPaint.setTextSize(height * (SINGLE_DIGIT_PERCENT ? 0.75f : (tracker.level == 100 ? 0.38f : 0.5f))); mTextHeight = -mTextPaint.getFontMetrics().ascent; + pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); + pctX = mWidth * 0.5f; + pctY = (mHeight + mTextHeight) * 0.47f; + pctOpaque = levelTop > pctY; + if (!pctOpaque) { + mTextPath.reset(); + mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath); + // cut the percentage text out of the overall shape + mShapePath.op(mTextPath, Path.Op.DIFFERENCE); + } + } - final String str = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); - final float x = mWidth * 0.5f; - final float y = (mHeight + mTextHeight) * 0.47f; - c.drawText(str, - x, - y, - mTextPaint); + // draw the battery shape background + c.drawPath(mShapePath, mFramePaint); + + // draw the battery shape, clipped to charging level + mFrame.top = levelTop; + mClipPath.reset(); + mClipPath.addRect(mFrame, Path.Direction.CCW); + mShapePath.op(mClipPath, Path.Op.INTERSECT); + c.drawPath(mShapePath, mBatteryPaint); + + if (!tracker.plugged) { + if (level <= EMPTY) { + // draw the warning text + final float x = mWidth * 0.5f; + final float y = (mHeight + mWarningTextHeight) * 0.48f; + c.drawText(mWarningString, x, y, mWarningTextPaint); + } else if (pctOpaque) { + // draw the percentage text + c.drawText(pctText, pctX, pctY, mTextPaint); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/DemoMode.java b/packages/SystemUI/src/com/android/systemui/DemoMode.java index 8d271e4..c16c3a1 100644 --- a/packages/SystemUI/src/com/android/systemui/DemoMode.java +++ b/packages/SystemUI/src/com/android/systemui/DemoMode.java @@ -31,4 +31,5 @@ public interface DemoMode { public static final String COMMAND_NETWORK = "network"; public static final String COMMAND_BARS = "bars"; public static final String COMMAND_STATUS = "status"; + public static final String COMMAND_NOTIFICATIONS = "notifications"; } diff --git a/packages/SystemUI/src/com/android/systemui/DessertCase.java b/packages/SystemUI/src/com/android/systemui/DessertCase.java index d797e38..a96f024 100644 --- a/packages/SystemUI/src/com/android/systemui/DessertCase.java +++ b/packages/SystemUI/src/com/android/systemui/DessertCase.java @@ -16,13 +16,10 @@ package com.android.systemui; -import android.animation.ObjectAnimator; import android.app.Activity; import android.content.ComponentName; import android.content.pm.PackageManager; -import android.os.Handler; import android.util.Slog; -import android.view.animation.DecelerateInterpolator; public class DessertCase extends Activity { DessertCaseView mView; diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java index e1a4bb2..61c268e 100644 --- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java +++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java @@ -19,9 +19,9 @@ package com.android.systemui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; +import android.media.AudioManager; import android.os.Vibrator; import android.util.Log; import android.view.Gravity; @@ -31,7 +31,10 @@ import android.view.ScaleGestureDetector.OnScaleGestureListener; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewConfiguration; -import android.view.ViewGroup; + +import com.android.systemui.statusbar.ExpandableView; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.policy.ScrollAdapter; public class ExpandHelper implements Gefingerpoken, OnClickListener { public interface Callback { @@ -78,8 +81,6 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { private boolean mHasPopped; private View mEventSource; private View mCurrView; - private View mCurrViewTopGlow; - private View mCurrViewBottomGlow; private float mOldHeight; private float mNaturalHeight; private float mInitialTouchFocusY; @@ -96,9 +97,6 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { private ScaleGestureDetector mSGD; private ViewScaler mScaler; private ObjectAnimator mScaleAnimation; - private AnimatorSet mGlowAnimationSet; - private ObjectAnimator mGlowTopAnimation; - private ObjectAnimator mGlowBottomAnimation; private Vibrator mVibrator; private int mSmallSize; @@ -107,7 +105,7 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { private int mGravity; - private View mScrollView; + private ScrollAdapter mScrollAdapter; private OnScaleGestureListener mScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() { @@ -118,9 +116,7 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { float focusY = detector.getFocusY(); final View underFocus = findView(focusX, focusY); - if (underFocus != null) { - startExpanding(underFocus, STRETCH); - } + startExpanding(underFocus, STRETCH); return mExpanding; } @@ -136,41 +132,21 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { }; private class ViewScaler { - View mView; + ExpandableView mView; public ViewScaler() {} - public void setView(View v) { + public void setView(ExpandableView v) { mView = v; } public void setHeight(float h) { if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h); - ViewGroup.LayoutParams lp = mView.getLayoutParams(); - lp.height = (int)h; - mView.setLayoutParams(lp); - mView.requestLayout(); + mView.setActualHeight((int) h); } public float getHeight() { - int height = mView.getLayoutParams().height; - if (height < 0) { - height = mView.getMeasuredHeight(); - } - return height; + return mView.getActualHeight(); } public int getNaturalHeight(int maximum) { - ViewGroup.LayoutParams lp = mView.getLayoutParams(); - if (DEBUG_SCALE) Log.v(TAG, "Inspecting a child of type: " + - mView.getClass().getName()); - int oldHeight = lp.height; - lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; - mView.setLayoutParams(lp); - mView.measure( - View.MeasureSpec.makeMeasureSpec(mView.getMeasuredWidth(), - View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(maximum, - View.MeasureSpec.AT_MOST)); - lp.height = oldHeight; - mView.setLayoutParams(lp); - return mView.getMeasuredHeight(); + return Math.min(maximum, mView.getMaxHeight()); } } @@ -214,14 +190,6 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { } }; - mGlowTopAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f); - mGlowTopAnimation.addListener(glowVisibilityController); - mGlowBottomAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f); - mGlowBottomAnimation.addListener(glowVisibilityController); - mGlowAnimationSet = new AnimatorSet(); - mGlowAnimationSet.play(mGlowTopAnimation).with(mGlowBottomAnimation); - mGlowAnimationSet.setDuration(GLOW_DURATION); - final ViewConfiguration configuration = ViewConfiguration.get(mContext); mTouchSlop = configuration.getScaledTouchSlop(); @@ -242,7 +210,6 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { float newHeight = clamp(target); mScaler.setHeight(newHeight); - setGlow(calculateGlow(target, newHeight)); mLastFocusY = mSGD.getFocusY(); mLastSpanY = mSGD.getCurrentSpan(); } @@ -300,8 +267,8 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { mGravity = gravity; } - public void setScrollView(View scrollView) { - mScrollView = scrollView; + public void setScrollAdapter(ScrollAdapter adapter) { + mScrollAdapter = adapter; } private float calculateGlow(float target, float actual) { @@ -313,37 +280,6 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { return (GLOW_BASE + strength * (1f - GLOW_BASE)); } - public void setGlow(float glow) { - if (!mGlowAnimationSet.isRunning() || glow == 0f) { - if (mGlowAnimationSet.isRunning()) { - mGlowAnimationSet.end(); - } - if (mCurrViewTopGlow != null && mCurrViewBottomGlow != null) { - if (glow == 0f || mCurrViewTopGlow.getAlpha() == 0f) { - // animate glow in and out - mGlowTopAnimation.setTarget(mCurrViewTopGlow); - mGlowBottomAnimation.setTarget(mCurrViewBottomGlow); - mGlowTopAnimation.setFloatValues(glow); - mGlowBottomAnimation.setFloatValues(glow); - mGlowAnimationSet.setupStartValues(); - mGlowAnimationSet.start(); - } else { - // set it explicitly in reponse to touches. - mCurrViewTopGlow.setAlpha(glow); - mCurrViewBottomGlow.setAlpha(glow); - handleGlowVisibility(); - } - } - } - } - - private void handleGlowVisibility() { - mCurrViewTopGlow.setVisibility(mCurrViewTopGlow.getAlpha() <= 0.0f ? - View.INVISIBLE : View.VISIBLE); - mCurrViewBottomGlow.setVisibility(mCurrViewBottomGlow.getAlpha() <= 0.0f ? - View.INVISIBLE : View.VISIBLE); - } - @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); @@ -378,12 +314,10 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)"); final View underFocus = findView(x, y); - if (underFocus != null) { - startExpanding(underFocus, PULL); - } + startExpanding(underFocus, PULL); return true; } - if (mScrollView != null && mScrollView.getScrollY() > 0) { + if (mScrollAdapter != null && !mScrollAdapter.isScrolledToTop()) { return false; } // Now look for other gestures @@ -395,8 +329,7 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)"); mLastMotionY = y; final View underFocus = findView(x, y); - if (underFocus != null) { - startExpanding(underFocus, BLINDS); + if (startExpanding(underFocus, BLINDS)) { mInitialTouchY = mLastMotionY; mHasPopped = false; } @@ -406,7 +339,8 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { } case MotionEvent.ACTION_DOWN: - mWatchingForPull = isInside(mScrollView, x, y); + mWatchingForPull = mScrollAdapter != null && + isInside(mScrollAdapter.getHostView(), x, y); mLastMotionY = y; break; @@ -456,9 +390,6 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { if (mHasPopped) { mScaler.setHeight(newHeight); - setGlow(GLOW_BASE); - } else { - setGlow(calculateGlow(4f * pull, 0f)); } final int x = (int) mSGD.getFocusX(); @@ -498,17 +429,22 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { return true; } - private void startExpanding(View v, int expandType) { + /** + * @return True if the view is expandable, false otherwise. + */ + private boolean startExpanding(View v, int expandType) { + if (!(v instanceof ExpandableNotificationRow)) { + return false; + } mExpansionStyle = expandType; - if (mExpanding && v == mCurrView) { - return; + if (mExpanding && v == mCurrView) { + return true; } mExpanding = true; if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v); mCallback.setUserLockedChild(v, true); setView(v); - setGlow(GLOW_BASE); - mScaler.setView(v); + mScaler.setView((ExpandableView) v); mOldHeight = mScaler.getHeight(); if (mCallback.canChildBeExpanded(v)) { if (DEBUG) Log.d(TAG, "working on an expandable child"); @@ -520,6 +456,7 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight + " mNaturalHeight: " + mNaturalHeight); v.getParent().requestDisallowInterceptTouchEvent(true); + return true; } private void finishExpanding(boolean force) { @@ -539,14 +476,22 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { if (mScaleAnimation.isRunning()) { mScaleAnimation.cancel(); } - setGlow(0f); - mCallback.setUserExpandedChild(mCurrView, h == mNaturalHeight); + mCallback.setUserExpandedChild(mCurrView, targetHeight == mNaturalHeight); if (targetHeight != currentHeight) { mScaleAnimation.setFloatValues(targetHeight); mScaleAnimation.setupStartValues(); + final View scaledView = mCurrView; + mScaleAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCallback.setUserLockedChild(scaledView, false); + mScaleAnimation.removeListener(this); + } + }); mScaleAnimation.start(); + } else { + mCallback.setUserLockedChild(mCurrView, false); } - mCallback.setUserLockedChild(mCurrView, false); mExpanding = false; mExpansionStyle = NONE; @@ -560,23 +505,11 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { private void clearView() { mCurrView = null; - mCurrViewTopGlow = null; - mCurrViewBottomGlow = null; + } private void setView(View v) { mCurrView = v; - if (v instanceof ViewGroup) { - ViewGroup g = (ViewGroup) v; - mCurrViewTopGlow = g.findViewById(R.id.top_glow); - mCurrViewBottomGlow = g.findViewById(R.id.bottom_glow); - if (DEBUG) { - String debugLog = "Looking for glows: " + - (mCurrViewTopGlow != null ? "found top " : "didn't find top") + - (mCurrViewBottomGlow != null ? "found bottom " : "didn't find bottom"); - Log.v(TAG, debugLog); - } - } } @Override @@ -605,7 +538,7 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { mVibrator = (android.os.Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); } - mVibrator.vibrate(duration); + mVibrator.vibrate(duration, AudioManager.STREAM_SYSTEM); } } diff --git a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java index e323591..4857adc 100644 --- a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java @@ -537,7 +537,7 @@ public class ImageWallpaper extends WallpaperService { checkGlError(); - if (w < 0 || h < 0) { + if (w > 0 || h > 0) { glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); } diff --git a/packages/SystemUI/src/com/android/systemui/LoadAverageService.java b/packages/SystemUI/src/com/android/systemui/LoadAverageService.java index 610e42b..59ffe03 100644 --- a/packages/SystemUI/src/com/android/systemui/LoadAverageService.java +++ b/packages/SystemUI/src/com/android/systemui/LoadAverageService.java @@ -195,9 +195,10 @@ public class LoadAverageService extends Service { int systemW = (systemTime*W)/totalTime; int irqW = ((iowaitTime+irqTime+softIrqTime)*W)/totalTime; - int x = RIGHT - mPaddingRight; - int top = mPaddingTop + 2; - int bottom = mPaddingTop + mFH - 2; + int paddingRight = getPaddingRight(); + int x = RIGHT - paddingRight; + int top = getPaddingTop() + 2; + int bottom = getPaddingTop() + mFH - 2; if (irqW > 0) { canvas.drawRect(x-irqW, top, x, bottom, mIrqPaint); @@ -212,16 +213,16 @@ public class LoadAverageService extends Service { x -= userW; } - int y = mPaddingTop - (int)mAscent; - canvas.drawText(stats.mLoadText, RIGHT-mPaddingRight-stats.mLoadWidth-1, + int y = getPaddingTop() - (int)mAscent; + canvas.drawText(stats.mLoadText, RIGHT-paddingRight-stats.mLoadWidth-1, y-1, mShadowPaint); - canvas.drawText(stats.mLoadText, RIGHT-mPaddingRight-stats.mLoadWidth-1, + canvas.drawText(stats.mLoadText, RIGHT-paddingRight-stats.mLoadWidth-1, y+1, mShadowPaint); - canvas.drawText(stats.mLoadText, RIGHT-mPaddingRight-stats.mLoadWidth+1, + canvas.drawText(stats.mLoadText, RIGHT-paddingRight-stats.mLoadWidth+1, y-1, mShadow2Paint); - canvas.drawText(stats.mLoadText, RIGHT-mPaddingRight-stats.mLoadWidth+1, + canvas.drawText(stats.mLoadText, RIGHT-paddingRight-stats.mLoadWidth+1, y+1, mShadow2Paint); - canvas.drawText(stats.mLoadText, RIGHT-mPaddingRight-stats.mLoadWidth, + canvas.drawText(stats.mLoadText, RIGHT-paddingRight-stats.mLoadWidth, y, mLoadPaint); int N = stats.countWorkingStats(); @@ -233,7 +234,7 @@ public class LoadAverageService extends Service { userW = (st.rel_utime*W)/totalTime; systemW = (st.rel_stime*W)/totalTime; - x = RIGHT - mPaddingRight; + x = RIGHT - paddingRight; if (systemW > 0) { canvas.drawRect(x-systemW, top, x, bottom, mSystemPaint); x -= systemW; @@ -243,18 +244,18 @@ public class LoadAverageService extends Service { x -= userW; } - canvas.drawText(st.name, RIGHT-mPaddingRight-st.nameWidth-1, + canvas.drawText(st.name, RIGHT-paddingRight-st.nameWidth-1, y-1, mShadowPaint); - canvas.drawText(st.name, RIGHT-mPaddingRight-st.nameWidth-1, + canvas.drawText(st.name, RIGHT-paddingRight-st.nameWidth-1, y+1, mShadowPaint); - canvas.drawText(st.name, RIGHT-mPaddingRight-st.nameWidth+1, + canvas.drawText(st.name, RIGHT-paddingRight-st.nameWidth+1, y-1, mShadow2Paint); - canvas.drawText(st.name, RIGHT-mPaddingRight-st.nameWidth+1, + canvas.drawText(st.name, RIGHT-paddingRight-st.nameWidth+1, y+1, mShadow2Paint); Paint p = mLoadPaint; if (st.added) p = mAddedPaint; if (st.removed) p = mRemovedPaint; - canvas.drawText(st.name, RIGHT-mPaddingRight-st.nameWidth, y, p); + canvas.drawText(st.name, RIGHT-paddingRight-st.nameWidth, y, p); } } @@ -270,8 +271,8 @@ public class LoadAverageService extends Service { } } - int neededWidth = mPaddingLeft + mPaddingRight + maxWidth; - int neededHeight = mPaddingTop + mPaddingBottom + (mFH*(1+NW)); + int neededWidth = getPaddingLeft() + getPaddingRight() + maxWidth; + int neededHeight = getPaddingTop() + getPaddingBottom() + (mFH*(1+NW)); if (neededWidth != mNeededWidth || neededHeight != mNeededHeight) { mNeededWidth = neededWidth; mNeededHeight = neededHeight; diff --git a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java index c7f0e17..09aa60f 100644 --- a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java @@ -25,6 +25,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.media.AudioManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -135,7 +136,7 @@ public class SearchPanelView extends FrameLayout implements public void onTrigger(View v, final int target) { final int resId = mGlowPadView.getResourceIdForTarget(target); switch (resId) { - case com.android.internal.R.drawable.ic_action_assist_generic: + case R.drawable.ic_action_assist_generic: mWaitingForLaunch = true; startAssistActivity(); vibrate(); @@ -175,7 +176,7 @@ public class SearchPanelView extends FrameLayout implements ComponentName component = intent.getComponent(); if (component == null || !mGlowPadView.replaceTargetDrawablesIfPresent(component, ASSIST_ICON_METADATA_NAME, - com.android.internal.R.drawable.ic_action_assist_generic)) { + R.drawable.ic_action_assist_generic)) { if (DEBUG) Log.v(TAG, "Couldn't grab icon for component " + component); } } @@ -207,7 +208,8 @@ public class SearchPanelView extends FrameLayout implements Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0) { Resources res = context.getResources(); Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); - vibrator.vibrate(res.getInteger(R.integer.config_search_panel_view_vibration_duration)); + vibrator.vibrate(res.getInteger(R.integer.config_search_panel_view_vibration_duration), + AudioManager.STREAM_SYSTEM); } } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUI.java b/packages/SystemUI/src/com/android/systemui/SystemUI.java index cb624ad..85befff 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUI.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUI.java @@ -35,6 +35,9 @@ public abstract class SystemUI { public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { } + protected void onBootCompleted() { + } + @SuppressWarnings("unchecked") public <T> T getComponent(Class<T> interfaceType) { return (T) (mComponents != null ? mComponents.get(interfaceType) : null); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java new file mode 100644 index 0000000..103991a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.os.SystemProperties; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; + +/** + * Application class for SystemUI. + */ +public class SystemUIApplication extends Application { + + private static final String TAG = "SystemUIService"; + private static final boolean DEBUG = false; + + /** + * The classes of the stuff to start. + */ + private final Class<?>[] SERVICES = new Class[] { + com.android.systemui.keyguard.KeyguardViewMediator.class, + com.android.systemui.recent.Recents.class, + com.android.systemui.statusbar.SystemBars.class, + com.android.systemui.usb.StorageNotification.class, + com.android.systemui.power.PowerUI.class, + com.android.systemui.media.RingtonePlayer.class, + com.android.systemui.settings.SettingsUI.class, + }; + + /** + * Hold a reference on the stuff we start. + */ + private final SystemUI[] mServices = new SystemUI[SERVICES.length]; + private boolean mServicesStarted; + private boolean mBootCompleted; + private final Map<Class<?>, Object> mComponents = new HashMap<Class<?>, Object>(); + + @Override + public void onCreate() { + super.onCreate(); + // Set the application theme that is inherited by all services. Note that setting the + // application theme in the manifest does only work for activities. Keep this in sync with + // the theme set there. + setTheme(R.style.systemui_theme); + + registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mBootCompleted) return; + + if (DEBUG) Log.v(TAG, "BOOT_COMPLETED received"); + unregisterReceiver(this); + mBootCompleted = true; + if (mServicesStarted) { + final int N = mServices.length; + for (int i = 0; i < N; i++) { + mServices[i].onBootCompleted(); + } + } + } + }, new IntentFilter(Intent.ACTION_BOOT_COMPLETED)); + } + + /** + * Makes sure that all the SystemUI services are running. If they are already running, this is a + * no-op. This is needed to conditinally start all the services, as we only need to have it in + * the main process. + * + * <p>This method must only be called from the main thread.</p> + */ + public void startServicesIfNeeded() { + if (mServicesStarted) { + return; + } + + if (!mBootCompleted) { + // check to see if maybe it was already completed long before we began + // see ActivityManagerService.finishBooting() + if ("1".equals(SystemProperties.get("sys.boot_completed"))) { + mBootCompleted = true; + if (DEBUG) Log.v(TAG, "BOOT_COMPLETED was already sent"); + } + } + + Log.v(TAG, "Starting SystemUI services."); + final int N = SERVICES.length; + for (int i=0; i<N; i++) { + Class<?> cl = SERVICES[i]; + if (DEBUG) Log.d(TAG, "loading: " + cl); + try { + mServices[i] = (SystemUI)cl.newInstance(); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } catch (InstantiationException ex) { + throw new RuntimeException(ex); + } + mServices[i].mContext = this; + mServices[i].mComponents = mComponents; + if (DEBUG) Log.d(TAG, "running: " + mServices[i]); + mServices[i].start(); + + if (mBootCompleted) { + mServices[i].onBootCompleted(); + } + } + mServicesStarted = true; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + if (mServicesStarted) { + int len = mServices.length; + for (int i = 0; i < len; i++) { + mServices[i].onConfigurationChanged(newConfig); + } + } + } + + @SuppressWarnings("unchecked") + public <T> T getComponent(Class<T> interfaceType) { + return (T) mComponents.get(interfaceType); + } + + public SystemUI[] getServices() { + return mServices; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index ca5f7d1..05e5f6b 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -18,65 +18,19 @@ package com.android.systemui; import android.app.Service; import android.content.Intent; -import android.content.res.Configuration; import android.os.IBinder; -import android.util.Log; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.HashMap; public class SystemUIService extends Service { - private static final String TAG = "SystemUIService"; - - /** - * The classes of the stuff to start. - */ - private final Class<?>[] SERVICES = new Class[] { - com.android.systemui.recent.Recents.class, - com.android.systemui.statusbar.SystemBars.class, - com.android.systemui.usb.StorageNotification.class, - com.android.systemui.power.PowerUI.class, - com.android.systemui.media.RingtonePlayer.class, - com.android.systemui.settings.SettingsUI.class, - }; - - /** - * Hold a reference on the stuff we start. - */ - private final SystemUI[] mServices = new SystemUI[SERVICES.length]; @Override public void onCreate() { - HashMap<Class<?>, Object> components = new HashMap<Class<?>, Object>(); - final int N = SERVICES.length; - for (int i=0; i<N; i++) { - Class<?> cl = SERVICES[i]; - Log.d(TAG, "loading: " + cl); - try { - mServices[i] = (SystemUI)cl.newInstance(); - } catch (IllegalAccessException ex) { - throw new RuntimeException(ex); - } catch (InstantiationException ex) { - throw new RuntimeException(ex); - } - mServices[i].mContext = this; - mServices[i].mComponents = components; - Log.d(TAG, "running: " + mServices[i]); - mServices[i].start(); - } - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - for (SystemUI ui: mServices) { - ui.onConfigurationChanged(newConfig); - } + super.onCreate(); + ((SystemUIApplication) getApplication()).startServicesIfNeeded(); } - /** - * Nobody binds to us. - */ @Override public IBinder onBind(Intent intent) { return null; @@ -84,14 +38,15 @@ public class SystemUIService extends Service { @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + SystemUI[] services = ((SystemUIApplication) getApplication()).getServices(); if (args == null || args.length == 0) { - for (SystemUI ui: mServices) { + for (SystemUI ui: services) { pw.println("dumping service: " + ui.getClass().getName()); ui.dump(fd, pw, args); } } else { String svc = args[0]; - for (SystemUI ui: mServices) { + for (SystemUI ui: services) { String name = ui.getClass().getName(); if (name.endsWith(svc)) { ui.dump(fd, pw, args); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java new file mode 100644 index 0000000..41c0e78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -0,0 +1,207 @@ +/* + * 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.keyguard; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; +import android.os.Debug; +import android.os.IBinder; +import android.util.Log; +import android.view.MotionEvent; + +import com.android.internal.policy.IKeyguardExitCallback; +import com.android.internal.policy.IKeyguardService; +import com.android.internal.policy.IKeyguardServiceConstants; +import com.android.internal.policy.IKeyguardShowCallback; +import com.android.systemui.SystemUIApplication; + +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +public class KeyguardService extends Service { + static final String TAG = "KeyguardService"; + static final String PERMISSION = android.Manifest.permission.CONTROL_KEYGUARD; + + private KeyguardViewMediator mKeyguardViewMediator; + + @Override + public void onCreate() { + ((SystemUIApplication) getApplication()).startServicesIfNeeded(); + mKeyguardViewMediator = + ((SystemUIApplication) getApplication()).getComponent(KeyguardViewMediator.class); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + void checkPermission() { + if (getBaseContext().checkCallingOrSelfPermission(PERMISSION) != PERMISSION_GRANTED) { + Log.w(TAG, "Caller needs permission '" + PERMISSION + "' to call " + Debug.getCaller()); + throw new SecurityException("Access denied to process: " + Binder.getCallingPid() + + ", must have permission " + PERMISSION); + } + } + + private final IKeyguardService.Stub mBinder = new IKeyguardService.Stub() { + + private boolean mIsOccluded; + + @Override + public boolean isShowing() { + return mKeyguardViewMediator.isShowing(); + } + + @Override + public boolean isSecure() { + return mKeyguardViewMediator.isSecure(); + } + + @Override + public boolean isShowingAndNotOccluded() { + return mKeyguardViewMediator.isShowingAndNotOccluded(); + } + + @Override + public boolean isInputRestricted() { + return mKeyguardViewMediator.isInputRestricted(); + } + + @Override + public void verifyUnlock(IKeyguardExitCallback callback) { + checkPermission(); + mKeyguardViewMediator.verifyUnlock(callback); + } + + @Override + public void keyguardDone(boolean authenticated, boolean wakeup) { + checkPermission(); + mKeyguardViewMediator.keyguardDone(authenticated, wakeup); + } + + @Override + public int setOccluded(boolean isOccluded) { + checkPermission(); + synchronized (this) { + int result; + if (isOccluded && mKeyguardViewMediator.isShowing() + && !mIsOccluded) { + result = IKeyguardServiceConstants + .KEYGUARD_SERVICE_SET_OCCLUDED_RESULT_UNSET_FLAGS; + } else if (!isOccluded && mKeyguardViewMediator.isShowing() + && mIsOccluded) { + result = IKeyguardServiceConstants + .KEYGUARD_SERVICE_SET_OCCLUDED_RESULT_SET_FLAGS; + } else { + result = IKeyguardServiceConstants.KEYGUARD_SERVICE_SET_OCCLUDED_RESULT_NONE; + } + if (mIsOccluded != isOccluded) { + mKeyguardViewMediator.setOccluded(isOccluded); + + // Cache the value so we always have a fresh view in whether Keyguard is occluded. + // If we would just call mKeyguardViewMediator.isOccluded(), this might be stale + // because that value gets updated in another thread. + mIsOccluded = isOccluded; + } + return result; + } + } + + @Override + public void dismiss() { + checkPermission(); + mKeyguardViewMediator.dismiss(); + } + + @Override + public void onDreamingStarted() { + checkPermission(); + mKeyguardViewMediator.onDreamingStarted(); + } + + @Override + public void onDreamingStopped() { + checkPermission(); + mKeyguardViewMediator.onDreamingStopped(); + } + + @Override + public void onScreenTurnedOff(int reason) { + checkPermission(); + mKeyguardViewMediator.onScreenTurnedOff(reason); + } + + @Override + public void onScreenTurnedOn(IKeyguardShowCallback callback) { + checkPermission(); + mKeyguardViewMediator.onScreenTurnedOn(callback); + } + + @Override + public void setKeyguardEnabled(boolean enabled) { + checkPermission(); + mKeyguardViewMediator.setKeyguardEnabled(enabled); + } + + @Override + public boolean isDismissable() { + return mKeyguardViewMediator.isDismissable(); + } + + @Override + public void onSystemReady() { + checkPermission(); + mKeyguardViewMediator.onSystemReady(); + } + + @Override + public void doKeyguardTimeout(Bundle options) { + checkPermission(); + mKeyguardViewMediator.doKeyguardTimeout(options); + } + + @Override + public void setCurrentUser(int userId) { + checkPermission(); + mKeyguardViewMediator.setCurrentUser(userId); + } + + @Override + public void showAssistant() { + checkPermission(); + } + + @Override + public void dispatch(MotionEvent event) { + checkPermission(); + } + + @Override + public void launchCamera() { + checkPermission(); + } + + @Override + public void onBootCompleted() { + checkPermission(); + mKeyguardViewMediator.onBootCompleted(); + } + }; +} + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java new file mode 100644 index 0000000..ffdb620 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -0,0 +1,1332 @@ +/* + * 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.keyguard; + +import android.app.Activity; +import android.app.ActivityManagerNative; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.SearchManager; +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.media.AudioManager; +import android.media.SoundPool; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.util.EventLog; +import android.util.Log; +import android.util.Slog; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.WindowManagerPolicy; + +import com.android.internal.policy.IKeyguardExitCallback; +import com.android.internal.policy.IKeyguardShowCallback; +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardDisplayManager; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.keyguard.MultiUserAvatarCache; +import com.android.keyguard.ViewMediatorCallback; +import com.android.keyguard.analytics.KeyguardAnalytics; +import com.android.keyguard.analytics.Session; +import com.android.systemui.SystemUI; +import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; +import com.android.systemui.statusbar.phone.StatusBarWindowManager; + +import java.io.File; + +import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; +import static com.android.keyguard.analytics.KeyguardAnalytics.SessionTypeAdapter; + + +/** + * Mediates requests related to the keyguard. This includes queries about the + * state of the keyguard, power management events that effect whether the keyguard + * should be shown or reset, callbacks to the phone window manager to notify + * it of when the keyguard is showing, and events from the keyguard view itself + * stating that the keyguard was succesfully unlocked. + * + * Note that the keyguard view is shown when the screen is off (as appropriate) + * so that once the screen comes on, it will be ready immediately. + * + * Example queries about the keyguard: + * - is {movement, key} one that should wake the keygaurd? + * - is the keyguard showing? + * - are input events restricted due to the state of the keyguard? + * + * Callbacks to the phone window manager: + * - the keyguard is showing + * + * Example external events that translate to keyguard view changes: + * - screen turned off -> reset the keyguard, and show it so it will be ready + * next time the screen turns on + * - keyboard is slid open -> if the keyguard is not secure, hide it + * + * Events from the keyguard view: + * - user succesfully unlocked keyguard -> hide keyguard view, and no longer + * restrict input events. + * + * Note: in addition to normal power managment events that effect the state of + * whether the keyguard should be showing, external apps and services may request + * that the keyguard be disabled via {@link #setKeyguardEnabled(boolean)}. When + * false, this will override all other conditions for turning on the keyguard. + * + * Threading and synchronization: + * This class is created by the initialization routine of the {@link android.view.WindowManagerPolicy}, + * and runs on its thread. The keyguard UI is created from that thread in the + * constructor of this class. The apis may be called from other threads, including the + * {@link com.android.server.input.InputManagerService}'s and {@link android.view.WindowManager}'s. + * Therefore, methods on this class are synchronized, and any action that is pointed + * directly to the keyguard UI is posted to a {@link android.os.Handler} to ensure it is taken on the UI + * thread of the keyguard. + */ +public class KeyguardViewMediator extends SystemUI { + private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000; + final static boolean DEBUG = false; + private static final boolean ENABLE_ANALYTICS = Build.IS_DEBUGGABLE; + private final static boolean DBG_WAKE = false; + + private final static String TAG = "KeyguardViewMediator"; + + private static final String DELAYED_KEYGUARD_ACTION = + "com.android.internal.policy.impl.PhoneWindowManager.DELAYED_KEYGUARD"; + + // used for handler messages + private static final int SHOW = 2; + private static final int HIDE = 3; + private static final int RESET = 4; + private static final int VERIFY_UNLOCK = 5; + private static final int NOTIFY_SCREEN_OFF = 6; + private static final int NOTIFY_SCREEN_ON = 7; + private static final int KEYGUARD_DONE = 9; + private static final int KEYGUARD_DONE_DRAWING = 10; + private static final int KEYGUARD_DONE_AUTHENTICATING = 11; + private static final int SET_OCCLUDED = 12; + private static final int KEYGUARD_TIMEOUT = 13; + private static final int DISMISS = 17; + + /** + * The default amount of time we stay awake (used for all key input) + */ + public static final int AWAKE_INTERVAL_DEFAULT_MS = 10000; + + /** + * How long to wait after the screen turns off due to timeout before + * turning on the keyguard (i.e, the user has this much time to turn + * the screen back on without having to face the keyguard). + */ + private static final int KEYGUARD_LOCK_AFTER_DELAY_DEFAULT = 5000; + + /** + * How long we'll wait for the {@link ViewMediatorCallback#keyguardDoneDrawing()} + * callback before unblocking a call to {@link #setKeyguardEnabled(boolean)} + * that is reenabling the keyguard. + */ + private static final int KEYGUARD_DONE_DRAWING_TIMEOUT_MS = 2000; + + /** + * Secure setting whether analytics are collected on the keyguard. + */ + private static final String KEYGUARD_ANALYTICS_SETTING = "keyguard_analytics"; + + /** The stream type that the lock sounds are tied to. */ + private int mMasterStreamType; + + private AlarmManager mAlarmManager; + private AudioManager mAudioManager; + private StatusBarManager mStatusBarManager; + private boolean mSwitchingUser; + + private boolean mSystemReady; + + // Whether the next call to playSounds() should be skipped. Defaults to + // true because the first lock (on boot) should be silent. + private boolean mSuppressNextLockSound = true; + + + /** High level access to the power manager for WakeLocks */ + private PowerManager mPM; + + /** UserManager for querying number of users */ + private UserManager mUserManager; + + /** SearchManager for determining whether or not search assistant is available */ + private SearchManager mSearchManager; + + /** + * Used to keep the device awake while to ensure the keyguard finishes opening before + * we sleep. + */ + private PowerManager.WakeLock mShowKeyguardWakeLock; + + private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + + private KeyguardAnalytics mKeyguardAnalytics; + + // these are protected by synchronized (this) + + /** + * External apps (like the phone app) can tell us to disable the keygaurd. + */ + private boolean mExternallyEnabled = true; + + /** + * Remember if an external call to {@link #setKeyguardEnabled} with value + * false caused us to hide the keyguard, so that we need to reshow it once + * the keygaurd is reenabled with another call with value true. + */ + private boolean mNeedToReshowWhenReenabled = false; + + // cached value of whether we are showing (need to know this to quickly + // answer whether the input should be restricted) + private boolean mShowing; + + // true if the keyguard is hidden by another window + private boolean mOccluded = false; + + /** + * Helps remember whether the screen has turned on since the last time + * it turned off due to timeout. see {@link #onScreenTurnedOff(int)} + */ + private int mDelayedShowingSequence; + + /** + * If the user has disabled the keyguard, then requests to exit, this is + * how we'll ultimately let them know whether it was successful. We use this + * var being non-null as an indicator that there is an in progress request. + */ + private IKeyguardExitCallback mExitSecureCallback; + + // the properties of the keyguard + + private KeyguardUpdateMonitor mUpdateMonitor; + + private boolean mScreenOn; + + // last known state of the cellular connection + private String mPhoneState = TelephonyManager.EXTRA_STATE_IDLE; + + /** + * we send this intent when the keyguard is dismissed. + */ + private static final Intent USER_PRESENT_INTENT = new Intent(Intent.ACTION_USER_PRESENT) + .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING + | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + + /** + * {@link #setKeyguardEnabled} waits on this condition when it reenables + * the keyguard. + */ + private boolean mWaitingUntilKeyguardVisible = false; + private LockPatternUtils mLockPatternUtils; + private boolean mKeyguardDonePending = false; + + private SoundPool mLockSounds; + private int mLockSoundId; + private int mUnlockSoundId; + private int mLockSoundStreamId; + + /** + * The volume applied to the lock/unlock sounds. + */ + private float mLockSoundVolume; + + /** + * For managing external displays + */ + private KeyguardDisplayManager mKeyguardDisplayManager; + + KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { + + @Override + public void onUserSwitching(int userId) { + // Note that the mLockPatternUtils user has already been updated from setCurrentUser. + // We need to force a reset of the views, since lockNow (called by + // ActivityManagerService) will not reconstruct the keyguard if it is already showing. + synchronized (KeyguardViewMediator.this) { + mSwitchingUser = true; + resetStateLocked(); + adjustStatusBarLocked(); + // When we switch users we want to bring the new user to the biometric unlock even + // if the current user has gone to the backup. + KeyguardUpdateMonitor.getInstance(mContext).setAlternateUnlockEnabled(true); + } + } + + @Override + public void onUserSwitchComplete(int userId) { + mSwitchingUser = false; + } + + @Override + public void onUserRemoved(int userId) { + mLockPatternUtils.removeUser(userId); + MultiUserAvatarCache.getInstance().clear(userId); + } + + @Override + public void onUserInfoChanged(int userId) { + MultiUserAvatarCache.getInstance().clear(userId); + } + + @Override + public void onPhoneStateChanged(int phoneState) { + synchronized (KeyguardViewMediator.this) { + if (TelephonyManager.CALL_STATE_IDLE == phoneState // call ending + && !mScreenOn // screen off + && mExternallyEnabled) { // not disabled by any app + + // note: this is a way to gracefully reenable the keyguard when the call + // ends and the screen is off without always reenabling the keyguard + // each time the screen turns off while in call (and having an occasional ugly + // flicker while turning back on the screen and disabling the keyguard again). + if (DEBUG) Log.d(TAG, "screen is off and call ended, let's make sure the " + + "keyguard is showing"); + doKeyguardLocked(null); + } + } + } + + @Override + public void onClockVisibilityChanged() { + adjustStatusBarLocked(); + } + + @Override + public void onDeviceProvisioned() { + sendUserPresentBroadcast(); + } + + @Override + public void onSimStateChanged(IccCardConstants.State simState) { + if (DEBUG) Log.d(TAG, "onSimStateChanged: " + simState); + + switch (simState) { + case NOT_READY: + case ABSENT: + // only force lock screen in case of missing sim if user hasn't + // gone through setup wizard + synchronized (this) { + if (!mUpdateMonitor.isDeviceProvisioned()) { + if (!isShowing()) { + if (DEBUG) Log.d(TAG, "ICC_ABSENT isn't showing," + + " we need to show the keyguard since the " + + "device isn't provisioned yet."); + doKeyguardLocked(null); + } else { + resetStateLocked(); + } + } + } + break; + case PIN_REQUIRED: + case PUK_REQUIRED: + synchronized (this) { + if (!isShowing()) { + if (DEBUG) Log.d(TAG, "INTENT_VALUE_ICC_LOCKED and keygaurd isn't " + + "showing; need to show keyguard so user can enter sim pin"); + doKeyguardLocked(null); + } else { + resetStateLocked(); + } + } + break; + case PERM_DISABLED: + synchronized (this) { + if (!isShowing()) { + if (DEBUG) Log.d(TAG, "PERM_DISABLED and " + + "keygaurd isn't showing."); + doKeyguardLocked(null); + } else { + if (DEBUG) Log.d(TAG, "PERM_DISABLED, resetStateLocked to" + + "show permanently disabled message in lockscreen."); + resetStateLocked(); + } + } + break; + case READY: + synchronized (this) { + if (isShowing()) { + resetStateLocked(); + } + } + break; + } + } + + }; + + ViewMediatorCallback mViewMediatorCallback = new ViewMediatorCallback() { + + public void userActivity() { + KeyguardViewMediator.this.userActivity(); + } + + public void userActivity(long holdMs) { + KeyguardViewMediator.this.userActivity(holdMs); + } + + public void keyguardDone(boolean authenticated) { + KeyguardViewMediator.this.keyguardDone(authenticated, true); + } + + public void keyguardDoneDrawing() { + mHandler.sendEmptyMessage(KEYGUARD_DONE_DRAWING); + } + + @Override + public void setNeedsInput(boolean needsInput) { + mStatusBarKeyguardViewManager.setNeedsInput(needsInput); + } + + @Override + public void onUserActivityTimeoutChanged() { + mStatusBarKeyguardViewManager.updateUserActivityTimeout(); + } + + @Override + public void keyguardDonePending() { + mKeyguardDonePending = true; + } + + @Override + public void keyguardGone() { + mKeyguardDisplayManager.hide(); + } + }; + + private void userActivity() { + userActivity(AWAKE_INTERVAL_DEFAULT_MS); + } + + public void userActivity(long holdMs) { + // We ignore the hold time. Eventually we should remove it. + // Instead, the keyguard window has an explicit user activity timeout set on it. + mPM.userActivity(SystemClock.uptimeMillis(), false); + } + + private void setup() { + mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard"); + mShowKeyguardWakeLock.setReferenceCounted(false); + + mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(DELAYED_KEYGUARD_ACTION)); + + mKeyguardDisplayManager = new KeyguardDisplayManager(mContext); + + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + + mUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); + + mLockPatternUtils = new LockPatternUtils(mContext); + mLockPatternUtils.setCurrentUser(UserHandle.USER_OWNER); + + // Assume keyguard is showing (unless it's disabled) until we know for sure... + mShowing = (mUpdateMonitor.isDeviceProvisioned() || mLockPatternUtils.isSecure()) + && !mLockPatternUtils.isLockScreenDisabled(); + + mStatusBarKeyguardViewManager = new StatusBarKeyguardViewManager(mContext, + mViewMediatorCallback, mLockPatternUtils); + final ContentResolver cr = mContext.getContentResolver(); + + if (ENABLE_ANALYTICS && !LockPatternUtils.isSafeModeEnabled() && + Settings.Secure.getInt(cr, KEYGUARD_ANALYTICS_SETTING, 0) == 1) { + mKeyguardAnalytics = new KeyguardAnalytics(mContext, new SessionTypeAdapter() { + + @Override + public int getSessionType() { + return mLockPatternUtils.isSecure() && !mUpdateMonitor.getUserHasTrust( + mLockPatternUtils.getCurrentUser()) + ? Session.TYPE_KEYGUARD_SECURE + : Session.TYPE_KEYGUARD_INSECURE; + } + }, new File(mContext.getCacheDir(), "keyguard_analytics.bin")); + } else { + mKeyguardAnalytics = null; + } + + mScreenOn = mPM.isScreenOn(); + + mLockSounds = new SoundPool(1, AudioManager.STREAM_SYSTEM, 0); + String soundPath = Settings.Global.getString(cr, Settings.Global.LOCK_SOUND); + if (soundPath != null) { + mLockSoundId = mLockSounds.load(soundPath, 1); + } + if (soundPath == null || mLockSoundId == 0) { + Log.w(TAG, "failed to load lock sound from " + soundPath); + } + soundPath = Settings.Global.getString(cr, Settings.Global.UNLOCK_SOUND); + if (soundPath != null) { + mUnlockSoundId = mLockSounds.load(soundPath, 1); + } + if (soundPath == null || mUnlockSoundId == 0) { + Log.w(TAG, "failed to load unlock sound from " + soundPath); + } + int lockSoundDefaultAttenuation = mContext.getResources().getInteger( + com.android.internal.R.integer.config_lockSoundVolumeDb); + mLockSoundVolume = (float)Math.pow(10, (float)lockSoundDefaultAttenuation/20); + } + + @Override + public void start() { + setup(); + putComponent(KeyguardViewMediator.class, this); + } + + /** + * Let us know that the system is ready after startup. + */ + public void onSystemReady() { + mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); + synchronized (this) { + if (DEBUG) Log.d(TAG, "onSystemReady"); + mSystemReady = true; + mUpdateMonitor.registerCallback(mUpdateCallback); + + // Suppress biometric unlock right after boot until things have settled if it is the + // selected security method, otherwise unsuppress it. It must be unsuppressed if it is + // not the selected security method for the following reason: if the user starts + // without a screen lock selected, the biometric unlock would be suppressed the first + // time they try to use it. + // + // Note that the biometric unlock will still not show if it is not the selected method. + // Calling setAlternateUnlockEnabled(true) simply says don't suppress it if it is the + // selected method. + if (mLockPatternUtils.usingBiometricWeak() + && mLockPatternUtils.isBiometricWeakInstalled()) { + if (DEBUG) Log.d(TAG, "suppressing biometric unlock during boot"); + mUpdateMonitor.setAlternateUnlockEnabled(false); + } else { + mUpdateMonitor.setAlternateUnlockEnabled(true); + } + + doKeyguardLocked(null); + } + // Most services aren't available until the system reaches the ready state, so we + // send it here when the device first boots. + maybeSendUserPresentBroadcast(); + } + + /** + * Called to let us know the screen was turned off. + * @param why either {@link android.view.WindowManagerPolicy#OFF_BECAUSE_OF_USER}, + * {@link android.view.WindowManagerPolicy#OFF_BECAUSE_OF_TIMEOUT} or + * {@link android.view.WindowManagerPolicy#OFF_BECAUSE_OF_PROX_SENSOR}. + */ + public void onScreenTurnedOff(int why) { + synchronized (this) { + mScreenOn = false; + if (DEBUG) Log.d(TAG, "onScreenTurnedOff(" + why + ")"); + + mKeyguardDonePending = false; + + // Lock immediately based on setting if secure (user has a pin/pattern/password). + // This also "locks" the device when not secure to provide easy access to the + // camera while preventing unwanted input. + final boolean lockImmediately = + mLockPatternUtils.getPowerButtonInstantlyLocks() || !mLockPatternUtils.isSecure(); + + if (mExitSecureCallback != null) { + if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled"); + try { + mExitSecureCallback.onKeyguardExitResult(false); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); + } + mExitSecureCallback = null; + if (!mExternallyEnabled) { + hideLocked(); + } + } else if (mShowing) { + notifyScreenOffLocked(); + resetStateLocked(); + } else if (why == WindowManagerPolicy.OFF_BECAUSE_OF_TIMEOUT + || (why == WindowManagerPolicy.OFF_BECAUSE_OF_USER && !lockImmediately)) { + doKeyguardLaterLocked(); + } else if (why == WindowManagerPolicy.OFF_BECAUSE_OF_PROX_SENSOR) { + // Do not enable the keyguard if the prox sensor forced the screen off. + } else { + doKeyguardLocked(null); + } + if (ENABLE_ANALYTICS && mKeyguardAnalytics != null) { + mKeyguardAnalytics.getCallback().onScreenOff(); + } + } + KeyguardUpdateMonitor.getInstance(mContext).dispatchScreenTurndOff(why); + } + + private void doKeyguardLaterLocked() { + // if the screen turned off because of timeout or the user hit the power button + // and we don't need to lock immediately, set an alarm + // to enable it a little bit later (i.e, give the user a chance + // to turn the screen back on within a certain window without + // having to unlock the screen) + final ContentResolver cr = mContext.getContentResolver(); + + // From DisplaySettings + long displayTimeout = Settings.System.getInt(cr, SCREEN_OFF_TIMEOUT, + KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT); + + // From SecuritySettings + final long lockAfterTimeout = Settings.Secure.getInt(cr, + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KEYGUARD_LOCK_AFTER_DELAY_DEFAULT); + + // From DevicePolicyAdmin + final long policyTimeout = mLockPatternUtils.getDevicePolicyManager() + .getMaximumTimeToLock(null, mLockPatternUtils.getCurrentUser()); + + long timeout; + if (policyTimeout > 0) { + // policy in effect. Make sure we don't go beyond policy limit. + displayTimeout = Math.max(displayTimeout, 0); // ignore negative values + timeout = Math.min(policyTimeout - displayTimeout, lockAfterTimeout); + } else { + timeout = lockAfterTimeout; + } + + if (timeout <= 0) { + // Lock now + mSuppressNextLockSound = true; + doKeyguardLocked(null); + } else { + // Lock in the future + long when = SystemClock.elapsedRealtime() + timeout; + Intent intent = new Intent(DELAYED_KEYGUARD_ACTION); + intent.putExtra("seq", mDelayedShowingSequence); + PendingIntent sender = PendingIntent.getBroadcast(mContext, + 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, when, sender); + if (DEBUG) Log.d(TAG, "setting alarm to turn off keyguard, seq = " + + mDelayedShowingSequence); + } + } + + private void cancelDoKeyguardLaterLocked() { + mDelayedShowingSequence++; + } + + /** + * Let's us know the screen was turned on. + */ + public void onScreenTurnedOn(IKeyguardShowCallback callback) { + synchronized (this) { + mScreenOn = true; + cancelDoKeyguardLaterLocked(); + if (DEBUG) Log.d(TAG, "onScreenTurnedOn, seq = " + mDelayedShowingSequence); + if (callback != null) { + notifyScreenOnLocked(callback); + } + } + KeyguardUpdateMonitor.getInstance(mContext).dispatchScreenTurnedOn(); + maybeSendUserPresentBroadcast(); + } + + private void maybeSendUserPresentBroadcast() { + if (mSystemReady && mLockPatternUtils.isLockScreenDisabled() + && !mUserManager.isUserSwitcherEnabled()) { + // Lock screen is disabled because the user has set the preference to "None". + // In this case, send out ACTION_USER_PRESENT here instead of in + // handleKeyguardDone() + sendUserPresentBroadcast(); + } + } + + /** + * A dream started. We should lock after the usual screen-off lock timeout but only + * if there is a secure lock pattern. + */ + public void onDreamingStarted() { + synchronized (this) { + if (mScreenOn && mLockPatternUtils.isSecure()) { + doKeyguardLaterLocked(); + } + } + } + + /** + * A dream stopped. + */ + public void onDreamingStopped() { + synchronized (this) { + if (mScreenOn) { + cancelDoKeyguardLaterLocked(); + } + } + } + + /** + * Same semantics as {@link android.view.WindowManagerPolicy#enableKeyguard}; provide + * a way for external stuff to override normal keyguard behavior. For instance + * the phone app disables the keyguard when it receives incoming calls. + */ + public void setKeyguardEnabled(boolean enabled) { + synchronized (this) { + if (DEBUG) Log.d(TAG, "setKeyguardEnabled(" + enabled + ")"); + + mExternallyEnabled = enabled; + + if (!enabled && mShowing) { + if (mExitSecureCallback != null) { + if (DEBUG) Log.d(TAG, "in process of verifyUnlock request, ignoring"); + // we're in the process of handling a request to verify the user + // can get past the keyguard. ignore extraneous requests to disable / reenable + return; + } + + // hiding keyguard that is showing, remember to reshow later + if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, " + + "disabling status bar expansion"); + mNeedToReshowWhenReenabled = true; + hideLocked(); + } else if (enabled && mNeedToReshowWhenReenabled) { + // reenabled after previously hidden, reshow + if (DEBUG) Log.d(TAG, "previously hidden, reshowing, reenabling " + + "status bar expansion"); + mNeedToReshowWhenReenabled = false; + + if (mExitSecureCallback != null) { + if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting"); + try { + mExitSecureCallback.onKeyguardExitResult(false); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); + } + mExitSecureCallback = null; + resetStateLocked(); + } else { + showLocked(null); + + // block until we know the keygaurd is done drawing (and post a message + // to unblock us after a timeout so we don't risk blocking too long + // and causing an ANR). + mWaitingUntilKeyguardVisible = true; + mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, KEYGUARD_DONE_DRAWING_TIMEOUT_MS); + if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false"); + while (mWaitingUntilKeyguardVisible) { + try { + wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible"); + } + } + } + } + + /** + * @see android.app.KeyguardManager#exitKeyguardSecurely + */ + public void verifyUnlock(IKeyguardExitCallback callback) { + synchronized (this) { + if (DEBUG) Log.d(TAG, "verifyUnlock"); + if (!mUpdateMonitor.isDeviceProvisioned()) { + // don't allow this api when the device isn't provisioned + if (DEBUG) Log.d(TAG, "ignoring because device isn't provisioned"); + try { + callback.onKeyguardExitResult(false); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); + } + } else if (mExternallyEnabled) { + // this only applies when the user has externally disabled the + // keyguard. this is unexpected and means the user is not + // using the api properly. + Log.w(TAG, "verifyUnlock called when not externally disabled"); + try { + callback.onKeyguardExitResult(false); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); + } + } else if (mExitSecureCallback != null) { + // already in progress with someone else + try { + callback.onKeyguardExitResult(false); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); + } + } else { + mExitSecureCallback = callback; + verifyUnlockLocked(); + } + } + } + + /** + * Is the keyguard currently showing? + */ + public boolean isShowing() { + return mShowing; + } + + public boolean isOccluded() { + return mOccluded; + } + + /** + * Is the keyguard currently showing and not being force hidden? + */ + public boolean isShowingAndNotOccluded() { + return mShowing && !mOccluded; + } + + /** + * Notify us when the keyguard is occluded by another window + */ + public void setOccluded(boolean isOccluded) { + if (DEBUG) Log.d(TAG, "setOccluded " + isOccluded); + mUpdateMonitor.sendKeyguardVisibilityChanged(!isOccluded); + mHandler.removeMessages(SET_OCCLUDED); + Message msg = mHandler.obtainMessage(SET_OCCLUDED, (isOccluded ? 1 : 0), 0); + mHandler.sendMessage(msg); + } + + /** + * Handles SET_OCCLUDED message sent by setOccluded() + */ + private void handleSetOccluded(boolean isOccluded) { + synchronized (KeyguardViewMediator.this) { + if (mOccluded != isOccluded) { + mOccluded = isOccluded; + mStatusBarKeyguardViewManager.setOccluded(isOccluded); + updateActivityLockScreenState(); + adjustStatusBarLocked(); + } + if (ENABLE_ANALYTICS && mKeyguardAnalytics != null) { + mKeyguardAnalytics.getCallback().onSetOccluded(isOccluded); + } + } + } + + /** + * Used by PhoneWindowManager to enable the keyguard due to a user activity timeout. + * This must be safe to call from any thread and with any window manager locks held. + */ + public void doKeyguardTimeout(Bundle options) { + mHandler.removeMessages(KEYGUARD_TIMEOUT); + Message msg = mHandler.obtainMessage(KEYGUARD_TIMEOUT, options); + mHandler.sendMessage(msg); + } + + /** + * Given the state of the keyguard, is the input restricted? + * Input is restricted when the keyguard is showing, or when the keyguard + * was suppressed by an app that disabled the keyguard or we haven't been provisioned yet. + */ + public boolean isInputRestricted() { + return mShowing || mNeedToReshowWhenReenabled || !mUpdateMonitor.isDeviceProvisioned(); + } + + /** + * Enable the keyguard if the settings are appropriate. + */ + private void doKeyguardLocked(Bundle options) { + // if another app is disabling us, don't show + if (!mExternallyEnabled) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled"); + + // note: we *should* set mNeedToReshowWhenReenabled=true here, but that makes + // for an occasional ugly flicker in this situation: + // 1) receive a call with the screen on (no keyguard) or make a call + // 2) screen times out + // 3) user hits key to turn screen back on + // instead, we reenable the keyguard when we know the screen is off and the call + // ends (see the broadcast receiver below) + // TODO: clean this up when we have better support at the window manager level + // for apps that wish to be on top of the keyguard + return; + } + + // if the keyguard is already showing, don't bother + if (mStatusBarKeyguardViewManager.isShowing()) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); + return; + } + + // if the setup wizard hasn't run yet, don't show + final boolean requireSim = !SystemProperties.getBoolean("keyguard.no_require_sim", + false); + final boolean provisioned = mUpdateMonitor.isDeviceProvisioned(); + final IccCardConstants.State state = mUpdateMonitor.getSimState(); + final boolean lockedOrMissing = state.isPinLocked() + || ((state == IccCardConstants.State.ABSENT + || state == IccCardConstants.State.PERM_DISABLED) + && requireSim); + + if (!lockedOrMissing && !provisioned) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because device isn't provisioned" + + " and the sim is not locked or missing"); + return; + } + + if (!mUserManager.isUserSwitcherEnabled() + && mLockPatternUtils.isLockScreenDisabled() && !lockedOrMissing) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off"); + return; + } + + if (mLockPatternUtils.checkVoldPassword()) { + if (DEBUG) Log.d(TAG, "Not showing lock screen since just decrypted"); + // Without this, settings is not enabled until the lock screen first appears + hideLocked(); + return; + } + + if (DEBUG) Log.d(TAG, "doKeyguard: showing the lock screen"); + showLocked(options); + } + + /** + * Dismiss the keyguard through the security layers. + */ + public void handleDismiss() { + if (mShowing && !mOccluded) { + mStatusBarKeyguardViewManager.dismiss(); + } + } + + public void dismiss() { + mHandler.sendEmptyMessage(DISMISS); + } + + /** + * Send message to keyguard telling it to reset its state. + * @see #handleReset + */ + private void resetStateLocked() { + if (DEBUG) Log.e(TAG, "resetStateLocked"); + Message msg = mHandler.obtainMessage(RESET); + mHandler.sendMessage(msg); + } + + /** + * Send message to keyguard telling it to verify unlock + * @see #handleVerifyUnlock() + */ + private void verifyUnlockLocked() { + if (DEBUG) Log.d(TAG, "verifyUnlockLocked"); + mHandler.sendEmptyMessage(VERIFY_UNLOCK); + } + + + /** + * Send a message to keyguard telling it the screen just turned on. + * @see #onScreenTurnedOff(int) + * @see #handleNotifyScreenOff + */ + private void notifyScreenOffLocked() { + if (DEBUG) Log.d(TAG, "notifyScreenOffLocked"); + mHandler.sendEmptyMessage(NOTIFY_SCREEN_OFF); + } + + /** + * Send a message to keyguard telling it the screen just turned on. + * @see #onScreenTurnedOn + * @see #handleNotifyScreenOn + */ + private void notifyScreenOnLocked(IKeyguardShowCallback result) { + if (DEBUG) Log.d(TAG, "notifyScreenOnLocked"); + Message msg = mHandler.obtainMessage(NOTIFY_SCREEN_ON, result); + mHandler.sendMessage(msg); + } + + /** + * Send message to keyguard telling it to show itself + * @see #handleShow + */ + private void showLocked(Bundle options) { + if (DEBUG) Log.d(TAG, "showLocked"); + // ensure we stay awake until we are finished displaying the keyguard + mShowKeyguardWakeLock.acquire(); + Message msg = mHandler.obtainMessage(SHOW, options); + mHandler.sendMessage(msg); + } + + /** + * Send message to keyguard telling it to hide itself + * @see #handleHide() + */ + private void hideLocked() { + if (DEBUG) Log.d(TAG, "hideLocked"); + Message msg = mHandler.obtainMessage(HIDE); + mHandler.sendMessage(msg); + } + + public boolean isSecure() { + return mLockPatternUtils.isSecure() + || KeyguardUpdateMonitor.getInstance(mContext).isSimPinSecure(); + } + + /** + * Update the newUserId. Call while holding WindowManagerService lock. + * NOTE: Should only be called by KeyguardViewMediator in response to the user id changing. + * + * @param newUserId The id of the incoming user. + */ + public void setCurrentUser(int newUserId) { + mLockPatternUtils.setCurrentUser(newUserId); + } + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DELAYED_KEYGUARD_ACTION.equals(intent.getAction())) { + final int sequence = intent.getIntExtra("seq", 0); + if (DEBUG) Log.d(TAG, "received DELAYED_KEYGUARD_ACTION with seq = " + + sequence + ", mDelayedShowingSequence = " + mDelayedShowingSequence); + synchronized (KeyguardViewMediator.this) { + if (mDelayedShowingSequence == sequence) { + // Don't play lockscreen SFX if the screen went off due to timeout. + mSuppressNextLockSound = true; + doKeyguardLocked(null); + } + } + } + } + }; + + public void keyguardDone(boolean authenticated, boolean wakeup) { + if (DEBUG) Log.d(TAG, "keyguardDone(" + authenticated + ")"); + EventLog.writeEvent(70000, 2); + synchronized (this) { + mKeyguardDonePending = false; + } + Message msg = mHandler.obtainMessage(KEYGUARD_DONE, authenticated ? 1 : 0, wakeup ? 1 : 0); + mHandler.sendMessage(msg); + } + + /** + * This handler will be associated with the policy thread, which will also + * be the UI thread of the keyguard. Since the apis of the policy, and therefore + * this class, can be called by other threads, any action that directly + * interacts with the keyguard ui should be posted to this handler, rather + * than called directly. + */ + private Handler mHandler = new Handler(Looper.myLooper(), null, true /*async*/) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SHOW: + handleShow((Bundle) msg.obj); + break; + case HIDE: + handleHide(); + break; + case RESET: + handleReset(); + break; + case VERIFY_UNLOCK: + handleVerifyUnlock(); + break; + case NOTIFY_SCREEN_OFF: + handleNotifyScreenOff(); + break; + case NOTIFY_SCREEN_ON: + handleNotifyScreenOn((IKeyguardShowCallback) msg.obj); + break; + case KEYGUARD_DONE: + handleKeyguardDone(msg.arg1 != 0, msg.arg2 != 0); + break; + case KEYGUARD_DONE_DRAWING: + handleKeyguardDoneDrawing(); + break; + case KEYGUARD_DONE_AUTHENTICATING: + keyguardDone(true, true); + break; + case SET_OCCLUDED: + handleSetOccluded(msg.arg1 != 0); + break; + case KEYGUARD_TIMEOUT: + synchronized (KeyguardViewMediator.this) { + doKeyguardLocked((Bundle) msg.obj); + } + break; + case DISMISS: + handleDismiss(); + break; + } + } + }; + + /** + * @see #keyguardDone + * @see #KEYGUARD_DONE + */ + private void handleKeyguardDone(boolean authenticated, boolean wakeup) { + if (DEBUG) Log.d(TAG, "handleKeyguardDone"); + + if (authenticated) { + mUpdateMonitor.clearFailedUnlockAttempts(); + } + + if (mExitSecureCallback != null) { + try { + mExitSecureCallback.onKeyguardExitResult(authenticated); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onKeyguardExitResult(" + authenticated + ")", e); + } + + mExitSecureCallback = null; + + if (authenticated) { + // after succesfully exiting securely, no need to reshow + // the keyguard when they've released the lock + mExternallyEnabled = true; + mNeedToReshowWhenReenabled = false; + } + } + + handleHide(); + sendUserPresentBroadcast(); + } + + private void sendUserPresentBroadcast() { + final UserHandle currentUser = new UserHandle(mLockPatternUtils.getCurrentUser()); + mContext.sendBroadcastAsUser(USER_PRESENT_INTENT, currentUser); + } + + /** + * @see #keyguardDone + * @see #KEYGUARD_DONE_DRAWING + */ + private void handleKeyguardDoneDrawing() { + synchronized(this) { + if (DEBUG) Log.d(TAG, "handleKeyguardDoneDrawing"); + if (mWaitingUntilKeyguardVisible) { + if (DEBUG) Log.d(TAG, "handleKeyguardDoneDrawing: notifying mWaitingUntilKeyguardVisible"); + mWaitingUntilKeyguardVisible = false; + notifyAll(); + + // there will usually be two of these sent, one as a timeout, and one + // as a result of the callback, so remove any remaining messages from + // the queue + mHandler.removeMessages(KEYGUARD_DONE_DRAWING); + } + } + } + + private void playSounds(boolean locked) { + // User feedback for keyguard. + + if (mSuppressNextLockSound) { + mSuppressNextLockSound = false; + return; + } + + final ContentResolver cr = mContext.getContentResolver(); + if (Settings.System.getInt(cr, Settings.System.LOCKSCREEN_SOUNDS_ENABLED, 1) == 1) { + final int whichSound = locked + ? mLockSoundId + : mUnlockSoundId; + mLockSounds.stop(mLockSoundStreamId); + // Init mAudioManager + if (mAudioManager == null) { + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + if (mAudioManager == null) return; + mMasterStreamType = mAudioManager.getMasterStreamType(); + } + // If the stream is muted, don't play the sound + if (mAudioManager.isStreamMute(mMasterStreamType)) return; + + mLockSoundStreamId = mLockSounds.play(whichSound, + mLockSoundVolume, mLockSoundVolume, 1/*priortiy*/, 0/*loop*/, 1.0f/*rate*/); + } + } + + private void updateActivityLockScreenState() { + try { + ActivityManagerNative.getDefault().setLockScreenShown(mShowing && !mOccluded); + } catch (RemoteException e) { + } + } + + /** + * Handle message sent by {@link #showLocked}. + * @see #SHOW + */ + private void handleShow(Bundle options) { + synchronized (KeyguardViewMediator.this) { + if (!mSystemReady) { + if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready."); + return; + } else { + if (DEBUG) Log.d(TAG, "handleShow"); + } + + mStatusBarKeyguardViewManager.show(options); + mShowing = true; + mKeyguardDonePending = false; + updateActivityLockScreenState(); + adjustStatusBarLocked(); + userActivity(); + + // Do this at the end to not slow down display of the keyguard. + playSounds(true); + + mShowKeyguardWakeLock.release(); + } + mKeyguardDisplayManager.show(); + } + + /** + * Handle message sent by {@link #hideLocked()} + * @see #HIDE + */ + private void handleHide() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleHide"); + + // only play "unlock" noises if not on a call (since the incall UI + // disables the keyguard) + if (TelephonyManager.EXTRA_STATE_IDLE.equals(mPhoneState)) { + playSounds(false); + } + + mStatusBarKeyguardViewManager.hide(); + mShowing = false; + mKeyguardDonePending = false; + updateActivityLockScreenState(); + adjustStatusBarLocked(); + } + } + + private void adjustStatusBarLocked() { + if (mStatusBarManager == null) { + mStatusBarManager = (StatusBarManager) + mContext.getSystemService(Context.STATUS_BAR_SERVICE); + } + if (mStatusBarManager == null) { + Log.w(TAG, "Could not get status bar manager"); + } else { + // Disable aspects of the system/status/navigation bars that must not be re-enabled by + // windows that appear on top, ever + int flags = StatusBarManager.DISABLE_NONE; + if (mShowing) { + // Permanently disable components not available when keyguard is enabled + // (like recents). Temporary enable/disable (e.g. the "back" button) are + // done in KeyguardHostView. + flags |= StatusBarManager.DISABLE_RECENT; + if (!isAssistantAvailable()) { + flags |= StatusBarManager.DISABLE_SEARCH; + } + } + if (isShowingAndNotOccluded()) { + flags |= StatusBarManager.DISABLE_HOME; + } + + if (DEBUG) { + Log.d(TAG, "adjustStatusBarLocked: mShowing=" + mShowing + " mOccluded=" + mOccluded + + " isSecure=" + isSecure() + " --> flags=0x" + Integer.toHexString(flags)); + } + + if (!(mContext instanceof Activity)) { + mStatusBarManager.disable(flags); + } + } + } + + /** + * Handle message sent by {@link #resetStateLocked} + * @see #RESET + */ + private void handleReset() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleReset"); + mStatusBarKeyguardViewManager.reset(); + } + } + + /** + * Handle message sent by {@link #verifyUnlock} + * @see #VERIFY_UNLOCK + */ + private void handleVerifyUnlock() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleVerifyUnlock"); + mStatusBarKeyguardViewManager.verifyUnlock(); + mShowing = true; + updateActivityLockScreenState(); + } + } + + /** + * Handle message sent by {@link #notifyScreenOffLocked()} + * @see #NOTIFY_SCREEN_OFF + */ + private void handleNotifyScreenOff() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleNotifyScreenOff"); + mStatusBarKeyguardViewManager.onScreenTurnedOff(); + } + } + + /** + * Handle message sent by {@link #notifyScreenOnLocked} + * @see #NOTIFY_SCREEN_ON + */ + private void handleNotifyScreenOn(IKeyguardShowCallback callback) { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleNotifyScreenOn"); + mStatusBarKeyguardViewManager.onScreenTurnedOn(callback); + } + } + + public boolean isDismissable() { + return mKeyguardDonePending || !isSecure(); + } + + private boolean isAssistantAvailable() { + return mSearchManager != null + && mSearchManager.getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null; + } + + public void onBootCompleted() { + mUpdateMonitor.dispatchBootCompleted(); + } + + public StatusBarKeyguardViewManager registerStatusBar(PhoneStatusBar phoneStatusBar, + ViewGroup container, StatusBarWindowManager statusBarWindowManager) { + mStatusBarKeyguardViewManager.registerStatusBar(phoneStatusBar, container, + statusBarWindowManager); + return mStatusBarKeyguardViewManager; + } + + public ViewMediatorCallback getViewMediatorCallback() { + return mViewMediatorCallback; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerDialogWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerDialogWarnings.java new file mode 100644 index 0000000..feec87c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/power/PowerDialogWarnings.java @@ -0,0 +1,218 @@ +/* + * 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.power; + +import android.app.AlertDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Slog; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import com.android.systemui.R; + +import java.io.PrintWriter; + +public class PowerDialogWarnings implements PowerUI.WarningsUI { + private static final String TAG = PowerUI.TAG + ".Dialog"; + private static final boolean DEBUG = PowerUI.DEBUG; + + private final Context mContext; + + private int mBatteryLevel; + private int mBucket; + private long mScreenOffTime; + + private AlertDialog mInvalidChargerDialog; + private AlertDialog mLowBatteryDialog; + private TextView mBatteryLevelTextView; + + public PowerDialogWarnings(Context context) { + mContext = context; + } + + @Override + public void dump(PrintWriter pw) { + pw.print("mInvalidChargerDialog="); + pw.println(mInvalidChargerDialog == null ? "null" : mInvalidChargerDialog.toString()); + pw.print("mLowBatteryDialog="); + pw.println(mLowBatteryDialog == null ? "null" : mLowBatteryDialog.toString()); + } + + @Override + public void update(int batteryLevel, int bucket, long screenOffTime) { + mBatteryLevel = batteryLevel; + mBucket = bucket; + mScreenOffTime = screenOffTime; + } + + @Override + public boolean isInvalidChargerWarningShowing() { + return mInvalidChargerDialog != null; + } + + @Override + public void updateLowBatteryWarning() { + if (mBatteryLevelTextView != null) { + showLowBatteryWarning(false /*playSound*/); + } + } + + @Override + public void dismissLowBatteryWarning() { + if (mLowBatteryDialog != null) { + Slog.i(TAG, "closing low battery warning: level=" + mBatteryLevel); + mLowBatteryDialog.dismiss(); + } + } + + @Override + public void showLowBatteryWarning(boolean playSound) { + Slog.i(TAG, + ((mBatteryLevelTextView == null) ? "showing" : "updating") + + " low battery warning: level=" + mBatteryLevel + + " [" + mBucket + "]"); + + CharSequence levelText = mContext.getString( + R.string.battery_low_percent_format, mBatteryLevel); + + if (mBatteryLevelTextView != null) { + mBatteryLevelTextView.setText(levelText); + } else { + View v = View.inflate(mContext, R.layout.battery_low, null); + mBatteryLevelTextView = (TextView)v.findViewById(R.id.level_percent); + + mBatteryLevelTextView.setText(levelText); + + AlertDialog.Builder b = new AlertDialog.Builder(mContext); + b.setCancelable(true); + b.setTitle(R.string.battery_low_title); + b.setView(v); + b.setIconAttribute(android.R.attr.alertDialogIcon); + b.setPositiveButton(android.R.string.ok, null); + + final Intent intent = new Intent(Intent.ACTION_POWER_USAGE_SUMMARY); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_MULTIPLE_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + | Intent.FLAG_ACTIVITY_NO_HISTORY); + if (intent.resolveActivity(mContext.getPackageManager()) != null) { + b.setNegativeButton(R.string.battery_low_why, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + dismissLowBatteryWarning(); + } + }); + } + + AlertDialog d = b.create(); + d.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + mLowBatteryDialog = null; + mBatteryLevelTextView = null; + } + }); + d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + d.getWindow().getAttributes().privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + d.show(); + mLowBatteryDialog = d; + if (playSound) { + playLowBatterySound(); + } + } + } + + private void playLowBatterySound() { + final ContentResolver cr = mContext.getContentResolver(); + + final int silenceAfter = Settings.Global.getInt(cr, + Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0); + final long offTime = SystemClock.elapsedRealtime() - mScreenOffTime; + if (silenceAfter > 0 + && mScreenOffTime > 0 + && offTime > silenceAfter) { + Slog.i(TAG, "screen off too long (" + offTime + "ms, limit " + silenceAfter + + "ms): not waking up the user with low battery sound"); + return; + } + + if (DEBUG) { + Slog.d(TAG, "playing low battery sound. pick-a-doop!"); // WOMP-WOMP is deprecated + } + + if (Settings.Global.getInt(cr, Settings.Global.POWER_SOUNDS_ENABLED, 1) == 1) { + final String soundPath = Settings.Global.getString(cr, + Settings.Global.LOW_BATTERY_SOUND); + if (soundPath != null) { + final Uri soundUri = Uri.parse("file://" + soundPath); + if (soundUri != null) { + final Ringtone sfx = RingtoneManager.getRingtone(mContext, soundUri); + if (sfx != null) { + sfx.setStreamType(AudioManager.STREAM_SYSTEM); + sfx.play(); + } + } + } + } + } + + @Override + public void dismissInvalidChargerWarning() { + if (mInvalidChargerDialog != null) { + mInvalidChargerDialog.dismiss(); + } + } + + @Override + public void showInvalidChargerWarning() { + Slog.d(TAG, "showing invalid charger dialog"); + + dismissLowBatteryWarning(); + + AlertDialog.Builder b = new AlertDialog.Builder(mContext); + b.setCancelable(true); + b.setMessage(R.string.invalid_charger); + b.setIconAttribute(android.R.attr.alertDialogIcon); + b.setPositiveButton(android.R.string.ok, null); + + AlertDialog d = b.create(); + d.setOnDismissListener(new DialogInterface.OnDismissListener() { + public void onDismiss(DialogInterface dialog) { + mInvalidChargerDialog = null; + mBatteryLevelTextView = null; + } + }); + + d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + d.show(); + mInvalidChargerDialog = d; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 28c2772..0fb0f8b 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -16,29 +16,17 @@ package com.android.systemui.power; -import android.app.AlertDialog; import android.content.BroadcastReceiver; -import android.content.ContentResolver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.media.AudioManager; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; import android.os.BatteryManager; import android.os.Handler; import android.os.PowerManager; import android.os.SystemClock; -import android.os.UserHandle; import android.provider.Settings; import android.util.Slog; -import android.view.View; -import android.view.WindowManager; -import android.widget.TextView; -import com.android.systemui.R; import com.android.systemui.SystemUI; import java.io.FileDescriptor; @@ -50,19 +38,17 @@ public class PowerUI extends SystemUI { static final boolean DEBUG = false; - Handler mHandler = new Handler(); + private WarningsUI mWarnings; - int mBatteryLevel = 100; - int mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN; - int mPlugType = 0; - int mInvalidCharger = 0; + private final Handler mHandler = new Handler(); - int mLowBatteryAlertCloseLevel; - int[] mLowBatteryReminderLevels = new int[2]; + private int mBatteryLevel = 100; + private int mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN; + private int mPlugType = 0; + private int mInvalidCharger = 0; - AlertDialog mInvalidChargerDialog; - AlertDialog mLowBatteryDialog; - TextView mBatteryLevelTextView; + private int mLowBatteryAlertCloseLevel; + private final int[] mLowBatteryReminderLevels = new int[2]; private long mScreenOffTime = -1; @@ -77,6 +63,7 @@ public class PowerUI extends SystemUI { final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mScreenOffTime = pm.isScreenOn() ? -1 : SystemClock.elapsedRealtime(); + mWarnings = new PowerDialogWarnings(mContext); // Register for Intent broadcasts for... IntentFilter filter = new IntentFilter(); @@ -145,13 +132,14 @@ public class PowerUI extends SystemUI { Slog.d(TAG, "plugged " + oldPlugged + " --> " + plugged); } + mWarnings.update(mBatteryLevel, bucket, mScreenOffTime); if (oldInvalidCharger == 0 && mInvalidCharger != 0) { Slog.d(TAG, "showing invalid charger warning"); - showInvalidChargerDialog(); + mWarnings.showInvalidChargerWarning(); return; } else if (oldInvalidCharger != 0 && mInvalidCharger == 0) { - dismissInvalidChargerDialog(); - } else if (mInvalidChargerDialog != null) { + mWarnings.dismissInvalidChargerWarning(); + } else if (mWarnings.isInvalidChargerWarningShowing()) { // if invalid charger is showing, don't show low battery return; } @@ -160,16 +148,13 @@ public class PowerUI extends SystemUI { && (bucket < oldBucket || oldPlugged) && mBatteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN && bucket < 0) { - showLowBatteryWarning(); - // only play SFX when the dialog comes up or the bucket changes - if (bucket != oldBucket || oldPlugged) { - playLowBatterySound(); - } + final boolean playSound = bucket != oldBucket || oldPlugged; + mWarnings.showLowBatteryWarning(playSound); } else if (plugged || (bucket > oldBucket && bucket > 0)) { - dismissLowBatteryWarning(); - } else if (mBatteryLevelTextView != null) { - showLowBatteryWarning(); + mWarnings.dismissLowBatteryWarning(); + } else { + mWarnings.updateLowBatteryWarning(); } } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { mScreenOffTime = SystemClock.elapsedRealtime(); @@ -181,142 +166,11 @@ public class PowerUI extends SystemUI { } }; - void dismissLowBatteryWarning() { - if (mLowBatteryDialog != null) { - Slog.i(TAG, "closing low battery warning: level=" + mBatteryLevel); - mLowBatteryDialog.dismiss(); - } - } - - void showLowBatteryWarning() { - Slog.i(TAG, - ((mBatteryLevelTextView == null) ? "showing" : "updating") - + " low battery warning: level=" + mBatteryLevel - + " [" + findBatteryLevelBucket(mBatteryLevel) + "]"); - - CharSequence levelText = mContext.getString( - R.string.battery_low_percent_format, mBatteryLevel); - - if (mBatteryLevelTextView != null) { - mBatteryLevelTextView.setText(levelText); - } else { - View v = View.inflate(mContext, R.layout.battery_low, null); - mBatteryLevelTextView = (TextView)v.findViewById(R.id.level_percent); - - mBatteryLevelTextView.setText(levelText); - - AlertDialog.Builder b = new AlertDialog.Builder(mContext); - b.setCancelable(true); - b.setTitle(R.string.battery_low_title); - b.setView(v); - b.setIconAttribute(android.R.attr.alertDialogIcon); - b.setPositiveButton(android.R.string.ok, null); - - final Intent intent = new Intent(Intent.ACTION_POWER_USAGE_SUMMARY); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_MULTIPLE_TASK - | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - | Intent.FLAG_ACTIVITY_NO_HISTORY); - if (intent.resolveActivity(mContext.getPackageManager()) != null) { - b.setNegativeButton(R.string.battery_low_why, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mContext.startActivityAsUser(intent, UserHandle.CURRENT); - dismissLowBatteryWarning(); - } - }); - } - - AlertDialog d = b.create(); - d.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - mLowBatteryDialog = null; - mBatteryLevelTextView = null; - } - }); - d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - d.getWindow().getAttributes().privateFlags |= - WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; - d.show(); - mLowBatteryDialog = d; - } - } - - void playLowBatterySound() { - final ContentResolver cr = mContext.getContentResolver(); - - final int silenceAfter = Settings.Global.getInt(cr, - Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0); - final long offTime = SystemClock.elapsedRealtime() - mScreenOffTime; - if (silenceAfter > 0 - && mScreenOffTime > 0 - && offTime > silenceAfter) { - Slog.i(TAG, "screen off too long (" + offTime + "ms, limit " + silenceAfter - + "ms): not waking up the user with low battery sound"); - return; - } - - if (DEBUG) { - Slog.d(TAG, "playing low battery sound. pick-a-doop!"); // WOMP-WOMP is deprecated - } - - if (Settings.Global.getInt(cr, Settings.Global.POWER_SOUNDS_ENABLED, 1) == 1) { - final String soundPath = Settings.Global.getString(cr, - Settings.Global.LOW_BATTERY_SOUND); - if (soundPath != null) { - final Uri soundUri = Uri.parse("file://" + soundPath); - if (soundUri != null) { - final Ringtone sfx = RingtoneManager.getRingtone(mContext, soundUri); - if (sfx != null) { - sfx.setStreamType(AudioManager.STREAM_SYSTEM); - sfx.play(); - } - } - } - } - } - - void dismissInvalidChargerDialog() { - if (mInvalidChargerDialog != null) { - mInvalidChargerDialog.dismiss(); - } - } - - void showInvalidChargerDialog() { - Slog.d(TAG, "showing invalid charger dialog"); - - dismissLowBatteryWarning(); - - AlertDialog.Builder b = new AlertDialog.Builder(mContext); - b.setCancelable(true); - b.setMessage(R.string.invalid_charger); - b.setIconAttribute(android.R.attr.alertDialogIcon); - b.setPositiveButton(android.R.string.ok, null); - - AlertDialog d = b.create(); - d.setOnDismissListener(new DialogInterface.OnDismissListener() { - public void onDismiss(DialogInterface dialog) { - mInvalidChargerDialog = null; - mBatteryLevelTextView = null; - } - }); - - d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - d.show(); - mInvalidChargerDialog = d; - } - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.print("mLowBatteryAlertCloseLevel="); pw.println(mLowBatteryAlertCloseLevel); pw.print("mLowBatteryReminderLevels="); pw.println(Arrays.toString(mLowBatteryReminderLevels)); - pw.print("mInvalidChargerDialog="); - pw.println(mInvalidChargerDialog == null ? "null" : mInvalidChargerDialog.toString()); - pw.print("mLowBatteryDialog="); - pw.println(mLowBatteryDialog == null ? "null" : mLowBatteryDialog.toString()); pw.print("mBatteryLevel="); pw.println(Integer.toString(mBatteryLevel)); pw.print("mBatteryStatus="); @@ -338,6 +192,18 @@ public class PowerUI extends SystemUI { Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0)); pw.print("bucket: "); pw.println(Integer.toString(findBatteryLevelBucket(mBatteryLevel))); + mWarnings.dump(pw); + } + + public interface WarningsUI { + void update(int batteryLevel, int bucket, long screenOffTime); + void dismissLowBatteryWarning(); + void showLowBatteryWarning(boolean playSound); + void dismissInvalidChargerWarning(); + void showInvalidChargerWarning(); + void updateLowBatteryWarning(); + boolean isInvalidChargerWarningShowing(); + void dump(PrintWriter pw); } } diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentTasksLoader.java b/packages/SystemUI/src/com/android/systemui/recent/RecentTasksLoader.java index c714d8b..aa4e69a 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentTasksLoader.java +++ b/packages/SystemUI/src/com/android/systemui/recent/RecentTasksLoader.java @@ -17,10 +17,12 @@ package com.android.systemui.recent; import android.app.ActivityManager; +import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; @@ -30,7 +32,9 @@ import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Handler; import android.os.Process; +import android.os.RemoteException; import android.os.UserHandle; +import android.os.UserManager; import android.util.Log; import android.view.MotionEvent; import android.view.View; @@ -156,15 +160,20 @@ public class RecentTasksLoader implements View.OnTouchListener { // Create an TaskDescription, returning null if the title or icon is null TaskDescription createTaskDescription(int taskId, int persistentTaskId, Intent baseIntent, - ComponentName origActivity, CharSequence description) { + ComponentName origActivity, CharSequence description, int userId) { Intent intent = new Intent(baseIntent); if (origActivity != null) { intent.setComponent(origActivity); } final PackageManager pm = mContext.getPackageManager(); + final IPackageManager ipm = AppGlobals.getPackageManager(); intent.setFlags((intent.getFlags()&~Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) | Intent.FLAG_ACTIVITY_NEW_TASK); - final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); + ResolveInfo resolveInfo = null; + try { + resolveInfo = ipm.resolveIntent(intent, null, 0, userId); + } catch (RemoteException re) { + } if (resolveInfo != null) { final ActivityInfo info = resolveInfo.activityInfo; final String title = info.loadLabel(pm).toString(); @@ -175,7 +184,7 @@ public class RecentTasksLoader implements View.OnTouchListener { TaskDescription item = new TaskDescription(taskId, persistentTaskId, resolveInfo, baseIntent, info.packageName, - description); + description, userId); item.setLabel(title); return item; @@ -192,7 +201,11 @@ public class RecentTasksLoader implements View.OnTouchListener { final PackageManager pm = mContext.getPackageManager(); Bitmap thumbnail = am.getTaskTopThumbnail(td.persistentTaskId); Drawable icon = getFullResIcon(td.resolveInfo, pm); - + if (td.userId != UserHandle.myUserId()) { + // Need to badge the icon + final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + icon = um.getBadgedDrawableForUser(icon, new UserHandle(td.userId)); + } if (DEBUG) Log.v(TAG, "Loaded bitmap for task " + td + ": " + thumbnail); synchronized (td) { @@ -367,8 +380,9 @@ public class RecentTasksLoader implements View.OnTouchListener { public TaskDescription loadFirstTask() { final ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); - final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasksForUser( - 1, ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier()); + final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasksForUser(1, + ActivityManager.RECENT_IGNORE_UNAVAILABLE | ActivityManager.RECENT_INCLUDE_PROFILES, + UserHandle.CURRENT.getIdentifier()); TaskDescription item = null; if (recentTasks.size() > 0) { ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(0); @@ -390,7 +404,8 @@ public class RecentTasksLoader implements View.OnTouchListener { item = createTaskDescription(recentInfo.id, recentInfo.persistentId, recentInfo.baseIntent, - recentInfo.origActivity, recentInfo.description); + recentInfo.origActivity, recentInfo.description, + recentInfo.userId); if (item != null) { loadThumbnailAndIcon(item); } @@ -439,7 +454,8 @@ public class RecentTasksLoader implements View.OnTouchListener { mContext.getSystemService(Context.ACTIVITY_SERVICE); final List<ActivityManager.RecentTaskInfo> recentTasks = - am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE); + am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE + | ActivityManager.RECENT_INCLUDE_PROFILES); int numTasks = recentTasks.size(); ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_HOME).resolveActivityInfo(pm, 0); @@ -472,7 +488,8 @@ public class RecentTasksLoader implements View.OnTouchListener { TaskDescription item = createTaskDescription(recentInfo.id, recentInfo.persistentId, recentInfo.baseIntent, - recentInfo.origActivity, recentInfo.description); + recentInfo.origActivity, recentInfo.description, + recentInfo.userId); if (item != null) { while (true) { diff --git a/packages/SystemUI/src/com/android/systemui/recent/Recents.java b/packages/SystemUI/src/com/android/systemui/recent/Recents.java index f5670e1..ae18aa8 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/Recents.java +++ b/packages/SystemUI/src/com/android/systemui/recent/Recents.java @@ -26,27 +26,55 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.SystemProperties; import android.os.UserHandle; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.View; - import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.SystemUI; +import com.android.systemui.recents.AlternateRecentsComponent; + public class Recents extends SystemUI implements RecentsComponent { private static final String TAG = "Recents"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; + + // Which recents to use + boolean mUseAlternateRecents; + AlternateRecentsComponent mAlternateRecents; + boolean mBootCompleted = false; @Override public void start() { + Configuration config = mContext.getResources().getConfiguration(); + mUseAlternateRecents = (config.smallestScreenWidthDp < 600); + if (mUseAlternateRecents) { + if (mAlternateRecents == null) { + mAlternateRecents = new AlternateRecentsComponent(mContext); + } + mAlternateRecents.onStart(); + } + putComponent(RecentsComponent.class, this); } @Override + protected void onBootCompleted() { + mBootCompleted = true; + } + + @Override public void toggleRecents(Display display, int layoutDirection, View statusBarView) { + if (mUseAlternateRecents) { + // Launch the alternate recents if required + mAlternateRecents.onToggleRecents(display, layoutDirection, statusBarView); + return; + } + if (DEBUG) Log.d(TAG, "toggle recents panel"); try { TaskDescription firstTask = RecentTasksLoader.getInstance(mContext).getFirstTask(); @@ -177,13 +205,11 @@ public class Recents extends SystemUI implements RecentsComponent { Intent intent = new Intent(RecentsActivity.WINDOW_ANIMATION_START_INTENT); intent.setPackage("com.android.systemui"); - mContext.sendBroadcastAsUser(intent, - new UserHandle(UserHandle.USER_CURRENT)); + sendBroadcastSafely(intent); } }); intent.putExtra(RecentsActivity.WAITING_FOR_WINDOW_ANIMATION_PARAM, true); - mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle( - UserHandle.USER_CURRENT)); + startActivitySafely(intent, opts.toBundle()); } } catch (ActivityNotFoundException e) { Log.e(TAG, "Failed to launch RecentAppsIntent", e); @@ -191,32 +217,66 @@ public class Recents extends SystemUI implements RecentsComponent { } @Override + protected void onConfigurationChanged(Configuration newConfig) { + if (mUseAlternateRecents) { + mAlternateRecents.onConfigurationChanged(newConfig); + } + } + + @Override public void preloadRecentTasksList() { - if (DEBUG) Log.d(TAG, "preloading recents"); - Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT); - intent.setClassName("com.android.systemui", - "com.android.systemui.recent.RecentsPreloadReceiver"); - mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + if (mUseAlternateRecents) { + mAlternateRecents.onPreloadRecents(); + } else { + Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT); + intent.setClassName("com.android.systemui", + "com.android.systemui.recent.RecentsPreloadReceiver"); + sendBroadcastSafely(intent); - RecentTasksLoader.getInstance(mContext).preloadFirstTask(); + RecentTasksLoader.getInstance(mContext).preloadFirstTask(); + } } @Override public void cancelPreloadingRecentTasksList() { - if (DEBUG) Log.d(TAG, "cancel preloading recents"); - Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT); - intent.setClassName("com.android.systemui", - "com.android.systemui.recent.RecentsPreloadReceiver"); - mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + if (mUseAlternateRecents) { + mAlternateRecents.onCancelPreloadingRecents(); + } else { + Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT); + intent.setClassName("com.android.systemui", + "com.android.systemui.recent.RecentsPreloadReceiver"); + sendBroadcastSafely(intent); - RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); + RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); + } } @Override public void closeRecents() { - if (DEBUG) Log.d(TAG, "closing recents panel"); - Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT); - intent.setPackage("com.android.systemui"); + if (mUseAlternateRecents) { + mAlternateRecents.onCloseRecents(); + } else { + Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT); + intent.setPackage("com.android.systemui"); + sendBroadcastSafely(intent); + + RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); + } + } + + /** + * Send broadcast only if BOOT_COMPLETED + */ + private void sendBroadcastSafely(Intent intent) { + if (!mBootCompleted) return; mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); } + + /** + * Start activity only if BOOT_COMPLETED + */ + private void startActivitySafely(Intent intent, Bundle opts) { + if (!mBootCompleted) return; + mContext.startActivityAsUser(intent, opts, new UserHandle(UserHandle.USER_CURRENT)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsActivity.java index 09a7a5e..7ab40b0 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/recent/RecentsActivity.java @@ -164,7 +164,8 @@ public class RecentsActivity extends Activity { final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks(2, ActivityManager.RECENT_WITH_EXCLUDED | - ActivityManager.RECENT_IGNORE_UNAVAILABLE); + ActivityManager.RECENT_IGNORE_UNAVAILABLE | + ActivityManager.RECENT_INCLUDE_PROFILES); if (recentTasks.size() > 1 && mRecentsPanel.simulateClick(recentTasks.get(1).persistentId)) { // recents panel will take care of calling show(false) through simulateClick diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java index be42bc0..35c824b 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java +++ b/packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java @@ -57,7 +57,7 @@ public class RecentsHorizontalScrollView extends HorizontalScrollView public RecentsHorizontalScrollView(Context context, AttributeSet attrs) { super(context, attrs, 0); float densityScale = getResources().getDisplayMetrics().density; - float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); + float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); mSwipeHelper = new SwipeHelper(SwipeHelper.Y, this, densityScale, pagingTouchSlop); mFadedEdgeDrawHelper = FadedEdgeDrawHelper.create(context, attrs, this, false); mRecycledViews = new HashSet<View>(); @@ -239,9 +239,9 @@ public class RecentsHorizontalScrollView extends HorizontalScrollView if (mFadedEdgeDrawHelper != null) { mFadedEdgeDrawHelper.drawCallback(canvas, - left, right, top, bottom, mScrollX, mScrollY, + left, right, top, bottom, getScrollX(), getScrollY(), 0, 0, - getLeftFadingEdgeStrength(), getRightFadingEdgeStrength(), mPaddingTop); + getLeftFadingEdgeStrength(), getRightFadingEdgeStrength(), getPaddingTop()); } } @@ -280,7 +280,7 @@ public class RecentsHorizontalScrollView extends HorizontalScrollView super.onFinishInflate(); setScrollbarFadingEnabled(true); mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout); - final int leftPadding = mContext.getResources() + final int leftPadding = getContext().getResources() .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin); setOverScrollEffectPadding(leftPadding, 0); } @@ -297,7 +297,7 @@ public class RecentsHorizontalScrollView extends HorizontalScrollView super.onConfigurationChanged(newConfig); float densityScale = getResources().getDisplayMetrics().density; mSwipeHelper.setDensityScale(densityScale); - float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); + float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); } diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java index 788e843..98bdee0 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java @@ -330,7 +330,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener } private void showImpl(boolean show) { - sendCloseSystemWindows(mContext, BaseStatusBar.SYSTEM_DIALOG_REASON_RECENT_APPS); + sendCloseSystemWindows(getContext(), BaseStatusBar.SYSTEM_DIALOG_REASON_RECENT_APPS); mShowing = show; @@ -372,11 +372,11 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener } public void dismiss() { - ((RecentsActivity) mContext).dismissAndGoHome(); + ((RecentsActivity) getContext()).dismissAndGoHome(); } public void dismissAndGoBack() { - ((RecentsActivity) mContext).dismissAndGoBack(); + ((RecentsActivity) getContext()).dismissAndGoBack(); } public void onAnimationCancel(Animator animation) { @@ -424,7 +424,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener } public void updateValuesFromResources() { - final Resources res = mContext.getResources(); + final Resources res = getContext().getResources(); mThumbnailWidth = Math.round(res.getDimension(R.dimen.status_bar_recents_thumbnail_width)); mFitThumbnailToXY = res.getBoolean(R.bool.config_recents_thumbnail_image_fits_to_xy); } @@ -440,7 +440,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener invalidate(); } }); - mListAdapter = new TaskDescriptionAdapter(mContext); + mListAdapter = new TaskDescriptionAdapter(getContext()); mRecentsContainer.setAdapter(mListAdapter); mRecentsContainer.setCallback(this); @@ -474,7 +474,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener if (show && h.iconView.getVisibility() != View.VISIBLE) { if (anim) { h.iconView.setAnimation( - AnimationUtils.loadAnimation(mContext, R.anim.recent_appear)); + AnimationUtils.loadAnimation(getContext(), R.anim.recent_appear)); } h.iconView.setVisibility(View.VISIBLE); } @@ -506,7 +506,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener if (show && h.thumbnailView.getVisibility() != View.VISIBLE) { if (anim) { h.thumbnailView.setAnimation( - AnimationUtils.loadAnimation(mContext, R.anim.recent_appear)); + AnimationUtils.loadAnimation(getContext(), R.anim.recent_appear)); } h.thumbnailView.setVisibility(View.VISIBLE); } @@ -617,7 +617,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener } else { mRecentTaskDescriptions.addAll(tasks); } - if (((RecentsActivity) mContext).isActivityShowing()) { + if (((RecentsActivity) getContext()).isActivityShowing()) { refreshViews(); } } @@ -689,7 +689,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener if (DEBUG) Log.v(TAG, "Starting activity " + intent); try { context.startActivityAsUser(intent, opts, - new UserHandle(UserHandle.USER_CURRENT)); + new UserHandle(ad.userId)); } catch (SecurityException e) { Log.e(TAG, "Recents does not have the permission to launch " + intent, e); } catch (ActivityNotFoundException e) { @@ -726,13 +726,13 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener // Currently, either direction means the same thing, so ignore direction and remove // the task. final ActivityManager am = (ActivityManager) - mContext.getSystemService(Context.ACTIVITY_SERVICE); + getContext().getSystemService(Context.ACTIVITY_SERVICE); if (am != null) { am.removeTask(ad.persistentTaskId, ActivityManager.REMOVE_TASK_KILL_PROCESS); // Accessibility feedback setContentDescription( - mContext.getString(R.string.accessibility_recents_item_dismissed, ad.getLabel())); + getContext().getString(R.string.accessibility_recents_item_dismissed, ad.getLabel())); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); setContentDescription(null); } @@ -741,7 +741,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener private void startApplicationDetailsActivity(String packageName) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", packageName, null)); - intent.setComponent(intent.resolveActivity(mContext.getPackageManager())); + intent.setComponent(intent.resolveActivity(getContext().getPackageManager())); TaskStackBuilder.create(getContext()) .addNextIntentWithParentStack(intent).startActivities(); } @@ -758,7 +758,7 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener final View selectedView, final View anchorView, final View thumbnailView) { thumbnailView.setSelected(true); final PopupMenu popup = - new PopupMenu(mContext, anchorView == null ? selectedView : anchorView); + new PopupMenu(getContext(), anchorView == null ? selectedView : anchorView); mPopup = popup; popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu()); popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @@ -793,15 +793,15 @@ public class RecentsPanelView extends FrameLayout implements OnItemClickListener protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); - int paddingLeft = mPaddingLeft; + int paddingLeft = getPaddingLeft(); final boolean offsetRequired = isPaddingOffsetRequired(); if (offsetRequired) { paddingLeft += getLeftPaddingOffset(); } - int left = mScrollX + paddingLeft; - int right = left + mRight - mLeft - mPaddingRight - paddingLeft; - int top = mScrollY + getFadeTop(offsetRequired); + int left = getScrollX() + paddingLeft; + int right = left + getRight() - getLeft() - getPaddingRight() - paddingLeft; + int top = getScrollY() + getFadeTop(offsetRequired); int bottom = top + getFadeHeight(offsetRequired); if (offsetRequired) { diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsVerticalScrollView.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsVerticalScrollView.java index 6dddc39..297fe0d 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsVerticalScrollView.java +++ b/packages/SystemUI/src/com/android/systemui/recent/RecentsVerticalScrollView.java @@ -57,7 +57,7 @@ public class RecentsVerticalScrollView extends ScrollView public RecentsVerticalScrollView(Context context, AttributeSet attrs) { super(context, attrs, 0); float densityScale = getResources().getDisplayMetrics().density; - float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); + float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); mFadedEdgeDrawHelper = FadedEdgeDrawHelper.create(context, attrs, this, true); @@ -69,7 +69,7 @@ public class RecentsVerticalScrollView extends ScrollView } private int scrollPositionOfMostRecent() { - return mLinearLayout.getHeight() - getHeight() + mPaddingTop; + return mLinearLayout.getHeight() - getHeight() + getPaddingTop(); } private void addToRecycledViews(View v) { @@ -248,9 +248,9 @@ public class RecentsVerticalScrollView extends ScrollView if (mFadedEdgeDrawHelper != null) { final boolean offsetRequired = isPaddingOffsetRequired(); mFadedEdgeDrawHelper.drawCallback(canvas, - left, right, top + getFadeTop(offsetRequired), bottom, mScrollX, mScrollY, + left, right, top + getFadeTop(offsetRequired), bottom, getScrollX(), getScrollY(), getTopFadingEdgeStrength(), getBottomFadingEdgeStrength(), - 0, 0, mPaddingTop); + 0, 0, getPaddingTop()); } } @@ -289,7 +289,7 @@ public class RecentsVerticalScrollView extends ScrollView super.onFinishInflate(); setScrollbarFadingEnabled(true); mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout); - final int leftPadding = mContext.getResources() + final int leftPadding = getContext().getResources() .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin); setOverScrollEffectPadding(leftPadding, 0); } @@ -306,7 +306,7 @@ public class RecentsVerticalScrollView extends ScrollView super.onConfigurationChanged(newConfig); float densityScale = getResources().getDisplayMetrics().density; mSwipeHelper.setDensityScale(densityScale); - float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); + float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); } diff --git a/packages/SystemUI/src/com/android/systemui/recent/TaskDescription.java b/packages/SystemUI/src/com/android/systemui/recent/TaskDescription.java index 2bc2821..5ad965f 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/TaskDescription.java +++ b/packages/SystemUI/src/com/android/systemui/recent/TaskDescription.java @@ -16,9 +16,9 @@ package com.android.systemui.recent; +import android.os.UserHandle; import android.content.Intent; import android.content.pm.ResolveInfo; -import android.graphics.Bitmap; import android.graphics.drawable.Drawable; public final class TaskDescription { @@ -28,6 +28,7 @@ public final class TaskDescription { final Intent intent; // launch intent for application final String packageName; // used to override animations (see onClick()) final CharSequence description; + final int userId; private Drawable mThumbnail; // generated by Activity.onCreateThumbnail() private Drawable mIcon; // application package icon @@ -36,7 +37,7 @@ public final class TaskDescription { public TaskDescription(int _taskId, int _persistentTaskId, ResolveInfo _resolveInfo, Intent _intent, - String _packageName, CharSequence _description) { + String _packageName, CharSequence _description, int _userId) { resolveInfo = _resolveInfo; intent = _intent; taskId = _taskId; @@ -44,6 +45,7 @@ public final class TaskDescription { description = _description; packageName = _packageName; + userId = _userId; } public TaskDescription() { @@ -54,6 +56,7 @@ public final class TaskDescription { description = null; packageName = null; + userId = UserHandle.USER_NULL; } public void setLoaded(boolean loaded) { diff --git a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java new file mode 100644 index 0000000..f2e322d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.View; +import android.view.WindowManager; +import com.android.systemui.R; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** A proxy implementation for the recents component */ +public class AlternateRecentsComponent { + + /** A handler for messages from the recents implementation */ + class RecentsMessageHandler extends Handler { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_UPDATE_FOR_CONFIGURATION) { + Resources res = mContext.getResources(); + float statusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + Bundle replyData = msg.getData().getParcelable(KEY_CONFIGURATION_DATA); + mSingleCountFirstTaskRect = replyData.getParcelable(KEY_SINGLE_TASK_STACK_RECT); + mSingleCountFirstTaskRect.offset(0, (int) statusBarHeight); + mMultipleCountFirstTaskRect = replyData.getParcelable(KEY_MULTIPLE_TASK_STACK_RECT); + mMultipleCountFirstTaskRect.offset(0, (int) statusBarHeight); + } + } + } + + /** A service connection to the recents implementation */ + class RecentsServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + Console.log(Constants.DebugFlags.App.RecentsComponent, + "[RecentsComponent|ServiceConnection|onServiceConnected]", + "toggleRecents: " + mToggleRecentsUponServiceBound); + mService = new Messenger(service); + mServiceIsBound = true; + + // Toggle recents if this service connection was triggered by hitting the recents button + if (mToggleRecentsUponServiceBound) { + startAlternateRecentsActivity(); + } + mToggleRecentsUponServiceBound = false; + } + + @Override + public void onServiceDisconnected(ComponentName className) { + Console.log(Constants.DebugFlags.App.RecentsComponent, + "[RecentsComponent|ServiceConnection|onServiceDisconnected]"); + mService = null; + mServiceIsBound = false; + } + } + + final public static int MSG_UPDATE_FOR_CONFIGURATION = 0; + final public static int MSG_UPDATE_TASK_THUMBNAIL = 1; + final public static int MSG_PRELOAD_TASKS = 2; + final public static int MSG_CANCEL_PRELOAD_TASKS = 3; + final public static int MSG_CLOSE_RECENTS = 4; + final public static int MSG_TOGGLE_RECENTS = 5; + + final public static String EXTRA_ANIMATING_WITH_THUMBNAIL = "recents.animatingWithThumbnail"; + final public static String KEY_CONFIGURATION_DATA = "recents.data.updateForConfiguration"; + final public static String KEY_WINDOW_RECT = "recents.windowRect"; + final public static String KEY_SYSTEM_INSETS = "recents.systemInsets"; + final public static String KEY_SINGLE_TASK_STACK_RECT = "recents.singleCountTaskRect"; + final public static String KEY_MULTIPLE_TASK_STACK_RECT = "recents.multipleCountTaskRect"; + + + final static int sMinToggleDelay = 425; + + final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS"; + final static String sRecentsPackage = "com.android.systemui"; + final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; + final static String sRecentsService = "com.android.systemui.recents.RecentsService"; + + Context mContext; + SystemServicesProxy mSystemServicesProxy; + + // Recents service binding + Messenger mService = null; + Messenger mMessenger; + boolean mServiceIsBound = false; + boolean mToggleRecentsUponServiceBound; + RecentsServiceConnection mConnection = new RecentsServiceConnection(); + + View mStatusBarView; + Rect mSingleCountFirstTaskRect = new Rect(); + Rect mMultipleCountFirstTaskRect = new Rect(); + long mLastToggleTime; + + public AlternateRecentsComponent(Context context) { + mContext = context; + mSystemServicesProxy = new SystemServicesProxy(context); + mMessenger = new Messenger(new RecentsMessageHandler()); + } + + public void onStart() { + Console.log(Constants.DebugFlags.App.RecentsComponent, "[RecentsComponent|start]"); + + // Try to create a long-running connection to the recents service + bindToRecentsService(false); + } + + /** Toggles the alternate recents activity */ + public void onToggleRecents(Display display, int layoutDirection, View statusBarView) { + Console.logStartTracingTime(Constants.DebugFlags.App.TimeRecentsStartup, + Constants.DebugFlags.App.TimeRecentsStartupKey); + Console.logStartTracingTime(Constants.DebugFlags.App.TimeRecentsLaunchTask, + Constants.DebugFlags.App.TimeRecentsLaunchKey); + Console.log(Constants.DebugFlags.App.RecentsComponent, "[RecentsComponent|toggleRecents]", + "serviceIsBound: " + mServiceIsBound); + mStatusBarView = statusBarView; + if (!mServiceIsBound) { + // Try to create a long-running connection to the recents service before toggling + // recents + bindToRecentsService(true); + return; + } + + try { + startAlternateRecentsActivity(); + } catch (ActivityNotFoundException e) { + Console.logRawError("Failed to launch RecentAppsIntent", e); + } + } + + public void onPreloadRecents() { + // Do nothing + } + + public void onCancelPreloadingRecents() { + // Do nothing + } + + public void onCloseRecents() { + Console.log(Constants.DebugFlags.App.RecentsComponent, "[RecentsComponent|closeRecents]"); + if (mServiceIsBound) { + // Try and update the recents configuration + try { + Bundle data = new Bundle(); + Message msg = Message.obtain(null, MSG_CLOSE_RECENTS, 0, 0); + msg.setData(data); + mService.send(msg); + } catch (RemoteException re) { + re.printStackTrace(); + } + } + } + + public void onConfigurationChanged(Configuration newConfig) { + if (mServiceIsBound) { + Resources res = mContext.getResources(); + int statusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + int navBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_height); + Rect rect = new Rect(); + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getRectSize(rect); + + // Try and update the recents configuration + try { + Bundle data = new Bundle(); + data.putParcelable(KEY_WINDOW_RECT, rect); + data.putParcelable(KEY_SYSTEM_INSETS, new Rect(0, statusBarHeight, 0, 0)); + Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0); + msg.setData(data); + msg.replyTo = mMessenger; + mService.send(msg); + } catch (RemoteException re) { + re.printStackTrace(); + } + } + } + + /** Binds to the recents implementation */ + private void bindToRecentsService(boolean toggleRecentsUponConnection) { + mToggleRecentsUponServiceBound = toggleRecentsUponConnection; + Intent intent = new Intent(); + intent.setClassName(sRecentsPackage, sRecentsService); + mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + /** Loads the first task thumbnail */ + Bitmap loadFirstTaskThumbnail() { + SystemServicesProxy ssp = mSystemServicesProxy; + List<ActivityManager.RecentTaskInfo> tasks = ssp.getRecentTasks(1, + UserHandle.CURRENT.getIdentifier()); + for (ActivityManager.RecentTaskInfo t : tasks) { + // Skip tasks in the home stack + if (ssp.isInHomeStack(t.persistentId)) { + return null; + } + + return ssp.getTaskThumbnail(t.persistentId); + } + return null; + } + + /** Returns whether there is are multiple recents tasks */ + boolean hasMultipleRecentsTask(List<ActivityManager.RecentTaskInfo> tasks) { + // NOTE: Currently there's no method to get the number of non-home tasks, so we have to + // compute this ourselves + SystemServicesProxy ssp = mSystemServicesProxy; + Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator(); + while (iter.hasNext()) { + ActivityManager.RecentTaskInfo t = iter.next(); + + // Skip tasks in the home stack + if (ssp.isInHomeStack(t.persistentId)) { + iter.remove(); + continue; + } + } + return (tasks.size() > 1); + } + + /** Returns whether the base intent of the top task stack was launched with the flag + * Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS. */ + boolean isTopTaskExcludeFromRecents(List<ActivityManager.RecentTaskInfo> tasks) { + if (tasks.size() > 0) { + ActivityManager.RecentTaskInfo t = tasks.get(0); + Console.log(t.baseIntent.toString()); + return (t.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0; + } + return false; + } + + /** Converts from the device rotation to the degree */ + float getDegreesForRotation(int value) { + switch (value) { + case Surface.ROTATION_90: + return 360f - 90f; + case Surface.ROTATION_180: + return 360f - 180f; + case Surface.ROTATION_270: + return 360f - 270f; + } + return 0f; + } + + /** Takes a screenshot of the surface */ + Bitmap takeScreenshot(Display display) { + DisplayMetrics dm = new DisplayMetrics(); + display.getRealMetrics(dm); + float[] dims = {dm.widthPixels, dm.heightPixels}; + float degrees = getDegreesForRotation(display.getRotation()); + boolean requiresRotation = (degrees > 0); + if (requiresRotation) { + // Get the dimensions of the device in its native orientation + Matrix m = new Matrix(); + m.preRotate(-degrees); + m.mapPoints(dims); + dims[0] = Math.abs(dims[0]); + dims[1] = Math.abs(dims[1]); + } + return SurfaceControl.screenshot((int) dims[0], (int) dims[1]); + } + + /** Starts the recents activity */ + void startAlternateRecentsActivity() { + // If the user has toggled it too quickly, then just eat up the event here (it's better than + // showing a janky screenshot). + // NOTE: Ideally, the screenshot mechanism would take the window transform into account + if (System.currentTimeMillis() - mLastToggleTime < sMinToggleDelay) { + return; + } + + // If Recents is the front most activity, then we should just communicate with it directly + // to launch the first task or dismiss itself + SystemServicesProxy ssp = mSystemServicesProxy; + List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1); + boolean isTopTaskHome = false; + if (!tasks.isEmpty()) { + ActivityManager.RunningTaskInfo topTask = tasks.get(0); + ComponentName topActivity = topTask.topActivity; + + // Check if the front most activity is recents + if (topActivity.getPackageName().equals(sRecentsPackage) && + topActivity.getClassName().equals(sRecentsActivity)) { + // Notify Recents to toggle itself + try { + Bundle data = new Bundle(); + Message msg = Message.obtain(null, MSG_TOGGLE_RECENTS, 0, 0); + msg.setData(data); + mService.send(msg); + + // Time this path + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup, + Constants.DebugFlags.App.TimeRecentsStartupKey, "sendToggleRecents"); + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsLaunchTask, + Constants.DebugFlags.App.TimeRecentsLaunchKey, "sendToggleRecents"); + } catch (RemoteException re) { + re.printStackTrace(); + } + mLastToggleTime = System.currentTimeMillis(); + return; + } + + // Determine whether the top task is currently home + isTopTaskHome = ssp.isInHomeStack(topTask.id); + } + + // Otherwise, Recents is not the front-most activity and we should animate into it. If + // the activity at the root of the top task stack is excluded from recents, or if that + // task stack is in the home stack, then we just do a simple transition. Otherwise, we + // animate to the rects defined by the Recents service, which can differ depending on the + // number of items in the list. + List<ActivityManager.RecentTaskInfo> recentTasks = + ssp.getRecentTasks(4, UserHandle.CURRENT.getIdentifier()); + boolean hasMultipleTasks = hasMultipleRecentsTask(recentTasks); + boolean isTaskExcludedFromRecents = isTopTaskExcludeFromRecents(recentTasks); + Rect taskRect = hasMultipleTasks ? mMultipleCountFirstTaskRect : mSingleCountFirstTaskRect; + if (!isTopTaskHome && !isTaskExcludedFromRecents && + (taskRect != null) && (taskRect.width() > 0) && (taskRect.height() > 0)) { + // Loading from thumbnail + Bitmap thumbnail; + Bitmap firstThumbnail = loadFirstTaskThumbnail(); + if (firstThumbnail != null) {// Create the thumbnail + thumbnail = Bitmap.createBitmap(taskRect.width(), taskRect.height(), + Bitmap.Config.ARGB_8888); + int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight()); + Canvas c = new Canvas(thumbnail); + c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size), + new Rect(0, 0, taskRect.width(), taskRect.height()), null); + c.setBitmap(null); + // Recycle the old thumbnail + firstThumbnail.recycle(); + } else { + // Load the thumbnail from the screenshot if can't get one from the system + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Bitmap screenshot = takeScreenshot(display); + Resources res = mContext.getResources(); + int size = Math.min(screenshot.getWidth(), screenshot.getHeight()); + int statusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + thumbnail = Bitmap.createBitmap(taskRect.width(), taskRect.height(), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(thumbnail); + c.drawBitmap(screenshot, new Rect(0, statusBarHeight, size, statusBarHeight + size), + new Rect(0, 0, taskRect.width(), taskRect.height()), null); + c.setBitmap(null); + // Recycle the temporary screenshot + screenshot.recycle(); + } + + ActivityOptions opts = ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView, + thumbnail, taskRect.left, taskRect.top, null); + startAlternateRecentsActivity(opts, true); + } else { + ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, + R.anim.recents_from_launcher_enter, + R.anim.recents_from_launcher_exit); + startAlternateRecentsActivity(opts, false); + } + + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup, + Constants.DebugFlags.App.TimeRecentsStartupKey, "startRecentsActivity"); + mLastToggleTime = System.currentTimeMillis(); + } + + /** Starts the recents activity */ + void startAlternateRecentsActivity(ActivityOptions opts, boolean animatingWithThumbnail) { + Intent intent = new Intent(sToggleRecentsAction); + intent.setClassName(sRecentsPackage, sRecentsActivity); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + intent.putExtra(EXTRA_ANIMATING_WITH_THUMBNAIL, animatingWithThumbnail); + if (opts != null) { + mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle( + UserHandle.USER_CURRENT)); + } else { + mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/BakedBezierInterpolator.java b/packages/SystemUI/src/com/android/systemui/recents/BakedBezierInterpolator.java new file mode 100644 index 0000000..95ab8e8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/BakedBezierInterpolator.java @@ -0,0 +1,64 @@ +package com.android.systemui.recents; + +import android.animation.TimeInterpolator; + +/** + * A pre-baked bezier-curved interpolator for quantum-paper transitions. + */ +public class BakedBezierInterpolator implements TimeInterpolator { + public static final BakedBezierInterpolator INSTANCE = new BakedBezierInterpolator(); + + /** + * Use the INSTANCE variable instead of instantiating. + */ + private BakedBezierInterpolator() { + super(); + } + + /** + * Lookup table values. + * Generated using a Bezier curve from (0,0) to (1,1) with control points: + * P0 (0,0) + * P1 (0.4, 0) + * P2 (0.2, 1.0) + * P3 (1.0, 1.0) + * + * Values sampled with x at regular intervals between 0 and 1. + */ + private static final float[] VALUES = new float[] { + 0.0f, 0.0002f, 0.0009f, 0.0019f, 0.0036f, 0.0059f, 0.0086f, 0.0119f, 0.0157f, 0.0209f, + 0.0257f, 0.0321f, 0.0392f, 0.0469f, 0.0566f, 0.0656f, 0.0768f, 0.0887f, 0.1033f, 0.1186f, + 0.1349f, 0.1519f, 0.1696f, 0.1928f, 0.2121f, 0.237f, 0.2627f, 0.2892f, 0.3109f, 0.3386f, + 0.3667f, 0.3952f, 0.4241f, 0.4474f, 0.4766f, 0.5f, 0.5234f, 0.5468f, 0.5701f, 0.5933f, + 0.6134f, 0.6333f, 0.6531f, 0.6698f, 0.6891f, 0.7054f, 0.7214f, 0.7346f, 0.7502f, 0.763f, + 0.7756f, 0.7879f, 0.8f, 0.8107f, 0.8212f, 0.8326f, 0.8415f, 0.8503f, 0.8588f, 0.8672f, + 0.8754f, 0.8833f, 0.8911f, 0.8977f, 0.9041f, 0.9113f, 0.9165f, 0.9232f, 0.9281f, 0.9328f, + 0.9382f, 0.9434f, 0.9476f, 0.9518f, 0.9557f, 0.9596f, 0.9632f, 0.9662f, 0.9695f, 0.9722f, + 0.9753f, 0.9777f, 0.9805f, 0.9826f, 0.9847f, 0.9866f, 0.9884f, 0.9901f, 0.9917f, 0.9931f, + 0.9944f, 0.9955f, 0.9964f, 0.9973f, 0.9981f, 0.9986f, 0.9992f, 0.9995f, 0.9998f, 1.0f, 1.0f + }; + + private static final float STEP_SIZE = 1.0f / (VALUES.length - 1); + + @Override + public float getInterpolation(float input) { + if (input >= 1.0f) { + return 1.0f; + } + + if (input <= 0f) { + return 0f; + } + + int position = Math.min( + (int)(input * (VALUES.length - 1)), + VALUES.length - 2); + + float quantized = position * STEP_SIZE; + float difference = input - quantized; + float weight = difference / STEP_SIZE; + + return VALUES[position] + weight * (VALUES[position + 1] - VALUES[position]); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/Console.java b/packages/SystemUI/src/com/android/systemui/recents/Console.java new file mode 100644 index 0000000..4b75c99 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/Console.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.recents; + + +import android.content.ComponentCallbacks2; +import android.content.Context; +import android.util.Log; +import android.view.MotionEvent; +import android.widget.Toast; + +import java.util.HashMap; +import java.util.Map; + + +public class Console { + // Timer + public static final Map<Object, Long> mTimeLogs = new HashMap<Object, Long>(); + + // Colors + public static final String AnsiReset = "\u001B[0m"; + public static final String AnsiBlack = "\u001B[30m"; + public static final String AnsiRed = "\u001B[31m"; // SystemUIHandshake + public static final String AnsiGreen = "\u001B[32m"; // MeasureAndLayout + public static final String AnsiYellow = "\u001B[33m"; // SynchronizeViewsWithModel + public static final String AnsiBlue = "\u001B[34m"; // TouchEvents + public static final String AnsiPurple = "\u001B[35m"; // Draw + public static final String AnsiCyan = "\u001B[36m"; // ClickEvents + public static final String AnsiWhite = "\u001B[37m"; + + /** Logs a key */ + public static void log(String key) { + log(true, key, "", AnsiReset); + } + + /** Logs a conditioned key */ + public static void log(boolean condition, String key) { + if (condition) { + log(condition, key, "", AnsiReset); + } + } + + /** Logs a key in a specific color */ + public static void log(boolean condition, String key, Object data) { + if (condition) { + log(condition, key, data, AnsiReset); + } + } + + /** Logs a key with data in a specific color */ + public static void log(boolean condition, String key, Object data, String color) { + if (condition) { + System.out.println(color + key + AnsiReset + " " + data.toString()); + } + } + + /** Logs an error */ + public static void logError(Context context, String msg) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + Log.e("Recents", msg); + } + + /** Logs a raw error */ + public static void logRawError(String msg, Exception e) { + Log.e("Recents", msg, e); + } + + /** Logs a divider bar */ + public static void logDivider(boolean condition) { + if (condition) { + System.out.println("==== [" + System.currentTimeMillis() + + "] ============================================================"); + } + } + + /** Starts a time trace */ + public static void logStartTracingTime(boolean condition, String key) { + if (condition) { + long curTime = System.currentTimeMillis(); + mTimeLogs.put(key, curTime); + Console.log(condition, "[Recents|" + key + "]", + "started @ " + curTime); + } + } + + /** Continues a time trace */ + public static void logTraceTime(boolean condition, String key, String desc) { + if (condition) { + long timeDiff = System.currentTimeMillis() - mTimeLogs.get(key); + Console.log(condition, "[Recents|" + key + "|" + desc + "]", + "+" + timeDiff + "ms"); + } + } + + /** Logs a stack trace */ + public static void logStackTrace() { + logStackTrace("", 99); + } + + /** Logs a stack trace to a certain depth */ + public static void logStackTrace(int depth) { + logStackTrace("", depth); + } + + /** Logs a stack trace to a certain depth with a key */ + public static void logStackTrace(String key, int depth) { + int offset = 0; + StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); + String tinyStackTrace = ""; + // Skip over the known stack trace classes + for (int i = 0; i < callStack.length; i++) { + StackTraceElement el = callStack[i]; + String className = el.getClassName(); + if (className.indexOf("dalvik.system.VMStack") == -1 && + className.indexOf("java.lang.Thread") == -1 && + className.indexOf("recents.Console") == -1) { + break; + } else { + offset++; + } + } + // Build the pretty stack trace + int start = Math.min(offset + depth, callStack.length); + int end = offset; + String indent = ""; + for (int i = start - 1; i >= end; i--) { + StackTraceElement el = callStack[i]; + tinyStackTrace += indent + " -> " + el.getClassName() + + "[" + el.getLineNumber() + "]." + el.getMethodName(); + if (i > end) { + tinyStackTrace += "\n"; + indent += " "; + } + } + log(true, key, tinyStackTrace, AnsiRed); + } + + + /** Returns the stringified MotionEvent action */ + public static String motionEventActionToString(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"; + case MotionEvent.ACTION_POINTER_DOWN: + return "Pointer Down"; + case MotionEvent.ACTION_POINTER_UP: + return "Pointer Up"; + default: + return "" + action; + } + } + + public static String trimMemoryLevelToString(int level) { + switch (level) { + case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: + return "UI Hidden"; + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: + return "Running Moderate"; + case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: + return "Background"; + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: + return "Running Low"; + case ComponentCallbacks2.TRIM_MEMORY_MODERATE: + return "Moderate"; + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: + return "Critical"; + case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: + return "Complete"; + default: + return "" + level; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java new file mode 100644 index 0000000..72d9a52 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +/** + * Constants + * XXX: We are going to move almost all of these into a resource. + */ +public class Constants { + public static class DebugFlags { + // Enable this with any other debug flag to see more info + public static final boolean Verbose = false; + + public static class App { + public static final boolean EnableTaskFiltering = true; + public static final boolean EnableTaskStackClipping = false; + public static final boolean EnableInfoPane = true; + public static final boolean EnableSearchButton = false; + + // This disables the bitmap and icon caches + public static final boolean DisableBackgroundCache = false; + // For debugging, this enables us to create mock recents tasks + public static final boolean EnableSystemServicesProxy = false; + // For debugging, this defines the number of mock recents packages to create + public static final int SystemServicesProxyMockPackageCount = 3; + // For debugging, this defines the number of mock recents tasks to create + public static final int SystemServicesProxyMockTaskCount = 75; + + // Timing certain paths + public static final String TimeRecentsStartupKey = "startup"; + public static final String TimeRecentsLaunchKey = "launchTask"; + public static final boolean TimeRecentsStartup = false; + public static final boolean TimeRecentsLaunchTask = false; + + public static final boolean RecentsComponent = false; + public static final boolean TaskDataLoader = false; + public static final boolean SystemUIHandshake = false; + public static final boolean TimeSystemCalls = false; + public static final boolean Memory = false; + } + + public static class UI { + public static final boolean Draw = false; + public static final boolean ClickEvents = false; + public static final boolean TouchEvents = false; + public static final boolean MeasureAndLayout = false; + public static final boolean HwLayers = false; + } + + public static class TaskStack { + public static final boolean SynchronizeViewsWithModel = false; + } + + public static class ViewPool { + public static final boolean PoolCallbacks = false; + } + } + + public static class Values { + public static class Window { + // The dark background dim is set behind the empty recents view + public static final float DarkBackgroundDim = 0.5f; + } + + public static class RecentsTaskLoader { + // XXX: This should be calculated on the first load + public static final int PreloadFirstTasksCount = 5; + } + + public static class TaskStackView { + public static final int TaskStackOverscrollRange = 150; + public static final int FilterStartDelay = 25; + + // The amount to inverse scale the movement if we are overscrolling + public static final float TouchOverscrollScaleFactor = 3f; + + // The padding will be applied to the smallest dimension, and then applied to all sides + public static final float StackPaddingPct = 0.15f; + // The overlap height relative to the task height + public static final float StackOverlapPct = 0.65f; + // The height of the peek space relative to the stack height + public static final float StackPeekHeightPct = 0.1f; + // The min scale of the last card in the peek area + public static final float StackPeekMinScale = 0.9f; + // The number of cards we see in the peek space + public static final int StackPeekNumCards = 3; + } + + public static class TaskView { + public static final boolean AnimateFrontTaskBarOnEnterRecents = true; + public static final boolean AnimateFrontTaskBarOnLeavingRecents = true; + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java new file mode 100644 index 0000000..71c45f2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import com.android.systemui.R; +import com.android.systemui.recents.model.SpaceNode; +import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.views.RecentsView; + +import java.util.ArrayList; + + +/* Activity */ +public class RecentsActivity extends Activity implements RecentsView.RecentsViewCallbacks { + FrameLayout mContainerView; + RecentsView mRecentsView; + View mEmptyView; + + boolean mVisible; + boolean mTaskLaunched; + + // Broadcast receiver to handle messages from our RecentsService + BroadcastReceiver mServiceBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsActivity|serviceBroadcast]", action, Console.AnsiRed); + if (action.equals(RecentsService.ACTION_TOGGLE_RECENTS_ACTIVITY)) { + // Try and unfilter and filtered stacks + if (!mRecentsView.unfilterFilteredStacks()) { + // If there are no filtered stacks, dismiss recents and launch the first task + dismissRecentsIfVisible(); + } + } + } + }; + + // Broadcast receiver to handle messages from the system + BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + finish(); + } + }; + + /** Updates the set of recent tasks */ + void updateRecentsTasks(Intent launchIntent) { + // Update the configuration based on the launch intent + RecentsConfiguration config = RecentsConfiguration.getInstance(); + config.launchedWithThumbnailAnimation = launchIntent.getBooleanExtra( + AlternateRecentsComponent.EXTRA_ANIMATING_WITH_THUMBNAIL, false); + + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + SpaceNode root = loader.reload(this, Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount); + ArrayList<TaskStack> stacks = root.getStacks(); + if (!stacks.isEmpty()) { + mRecentsView.setBSP(root); + } + + // Add the default no-recents layout + if (stacks.size() == 1 && stacks.get(0).getTaskCount() == 0) { + mEmptyView.setVisibility(View.VISIBLE); + + // Dim the background even more + WindowManager.LayoutParams wlp = getWindow().getAttributes(); + wlp.dimAmount = Constants.Values.Window.DarkBackgroundDim; + getWindow().setAttributes(wlp); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } else { + mEmptyView.setVisibility(View.GONE); + + // Un-dim the background + WindowManager.LayoutParams wlp = getWindow().getAttributes(); + wlp.dimAmount = 0f; + getWindow().setAttributes(wlp); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } + } + + /** Dismisses recents if we are already visible and the intent is to toggle the recents view */ + boolean dismissRecentsIfVisible() { + if (mVisible) { + if (!mRecentsView.launchFirstTask()) { + finish(); + } + return true; + } + return false; + } + + /** Called with the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Console.logDivider(Constants.DebugFlags.App.SystemUIHandshake); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onCreate]", + getIntent().getAction() + " visible: " + mVisible, Console.AnsiRed); + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup, + Constants.DebugFlags.App.TimeRecentsStartupKey, "onCreate"); + + // Initialize the loader and the configuration + RecentsTaskLoader.initialize(this); + RecentsConfiguration.reinitialize(this); + + // Create the view hierarchy + mRecentsView = new RecentsView(this); + mRecentsView.setCallbacks(this); + mRecentsView.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // Create the empty view + LayoutInflater inflater = LayoutInflater.from(this); + mEmptyView = inflater.inflate(R.layout.recents_empty, mContainerView, false); + + mContainerView = new FrameLayout(this); + mContainerView.addView(mRecentsView); + mContainerView.addView(mEmptyView); + setContentView(mContainerView); + + // Update the recent tasks + updateRecentsTasks(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + // Reset the task launched flag if we encounter an onNewIntent() before onStop() + mTaskLaunched = false; + + Console.logDivider(Constants.DebugFlags.App.SystemUIHandshake); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onNewIntent]", + intent.getAction() + " visible: " + mVisible, Console.AnsiRed); + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup, + Constants.DebugFlags.App.TimeRecentsStartupKey, "onNewIntent"); + + // Initialize the loader and the configuration + RecentsTaskLoader.initialize(this); + RecentsConfiguration.reinitialize(this); + + // Update the recent tasks + updateRecentsTasks(intent); + } + + @Override + protected void onStart() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onStart]", "", + Console.AnsiRed); + super.onStart(); + mVisible = true; + } + + @Override + protected void onResume() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onResume]", "", + Console.AnsiRed); + super.onResume(); + } + + @Override + public void onAttachedToWindow() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsActivity|onAttachedToWindow]", "", + Console.AnsiRed); + super.onAttachedToWindow(); + + // Register the broadcast receiver to handle messages from our service + IntentFilter filter = new IntentFilter(); + filter.addAction(RecentsService.ACTION_TOGGLE_RECENTS_ACTIVITY); + registerReceiver(mServiceBroadcastReceiver, filter); + + // Register the broadcast receiver to handle messages when the screen is turned off + filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + registerReceiver(mScreenOffReceiver, filter); + } + + @Override + public void onDetachedFromWindow() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsActivity|onDetachedFromWindow]", "", + Console.AnsiRed); + super.onDetachedFromWindow(); + + // Unregister any broadcast receivers we have registered + unregisterReceiver(mServiceBroadcastReceiver); + unregisterReceiver(mScreenOffReceiver); + } + + @Override + protected void onPause() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onPause]", "", + Console.AnsiRed); + super.onPause(); + } + + @Override + protected void onStop() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onStop]", "", + Console.AnsiRed); + super.onStop(); + + mVisible = false; + mTaskLaunched = false; + } + + @Override + protected void onDestroy() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onDestroy]", "", + Console.AnsiRed); + super.onDestroy(); + } + + @Override + public void onTrimMemory(int level) { + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + if (loader != null) { + loader.onTrimMemory(level); + } + } + + @Override + public void onBackPressed() { + boolean interceptedByInfoPanelClose = false; + + // Try and return from any open info panes + if (Constants.DebugFlags.App.EnableInfoPane) { + interceptedByInfoPanelClose = mRecentsView.closeOpenInfoPanes(); + } + + // If we haven't been intercepted already, then unfilter any stacks + if (!interceptedByInfoPanelClose) { + if (!mRecentsView.unfilterFilteredStacks()) { + super.onBackPressed(); + } + } + } + + @Override + public void onTaskLaunching() { + mTaskLaunched = true; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java new file mode 100644 index 0000000..d54df13 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.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.recents; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import com.android.systemui.R; + + +/** A static Recents configuration for the current context + * NOTE: We should not hold any references to a Context from a static instance */ +public class RecentsConfiguration { + static RecentsConfiguration sInstance; + + DisplayMetrics mDisplayMetrics; + + public Rect systemInsets = new Rect(); + public Rect displayRect = new Rect(); + + public float animationPxMovementPerSecond; + + public int filteringCurrentViewsMinAnimDuration; + public int filteringNewViewsMinAnimDuration; + public int taskBarEnterAnimDuration; + public int taskStackScrollDismissInfoPaneDistance; + public int taskStackMaxDim; + public int taskViewInfoPaneAnimDuration; + public int taskViewRoundedCornerRadiusPx; + public int searchBarSpaceHeightPx; + public int searchBarSpaceEdgeMarginsPx; + + public boolean launchedWithThumbnailAnimation; + + /** Private constructor */ + private RecentsConfiguration() {} + + /** Updates the configuration to the current context */ + public static RecentsConfiguration reinitialize(Context context) { + if (sInstance == null) { + sInstance = new RecentsConfiguration(); + } + sInstance.update(context); + return sInstance; + } + + /** Returns the current recents configuration */ + public static RecentsConfiguration getInstance() { + return sInstance; + } + + /** Updates the state, given the specified context */ + void update(Context context) { + Resources res = context.getResources(); + DisplayMetrics dm = res.getDisplayMetrics(); + mDisplayMetrics = dm; + + boolean isLandscape = res.getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE; + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, + "[RecentsConfiguration|orientation]", isLandscape ? "Landscape" : "Portrait", + Console.AnsiGreen); + + displayRect.set(0, 0, dm.widthPixels, dm.heightPixels); + animationPxMovementPerSecond = + res.getDimensionPixelSize(R.dimen.recents_animation_movement_in_dps_per_second); + filteringCurrentViewsMinAnimDuration = + res.getInteger(R.integer.recents_filter_animate_current_views_min_duration); + filteringNewViewsMinAnimDuration = + res.getInteger(R.integer.recents_filter_animate_new_views_min_duration); + taskBarEnterAnimDuration = + res.getInteger(R.integer.recents_animate_task_bar_enter_duration); + taskStackScrollDismissInfoPaneDistance = res.getDimensionPixelSize( + R.dimen.recents_task_stack_scroll_dismiss_info_pane_distance); + taskStackMaxDim = res.getInteger(R.integer.recents_max_task_stack_view_dim); + taskViewInfoPaneAnimDuration = + res.getInteger(R.integer.recents_animate_task_view_info_pane_duration); + taskViewRoundedCornerRadiusPx = + res.getDimensionPixelSize(R.dimen.recents_task_view_rounded_corners_radius); + searchBarSpaceHeightPx = res.getDimensionPixelSize(R.dimen.recents_search_bar_space_height); + searchBarSpaceEdgeMarginsPx = + res.getDimensionPixelSize(R.dimen.recents_search_bar_space_edge_margins); + } + + /** Updates the system insets */ + public void updateSystemInsets(Rect insets) { + systemInsets.set(insets); + } + + /** Returns the search bar bounds in the specified orientation */ + public void getSearchBarBounds(int width, int height, + Rect searchBarSpaceBounds, Rect searchBarBounds) { + // Return empty rects if search is not enabled + if (!Constants.DebugFlags.App.EnableSearchButton) { + searchBarSpaceBounds.set(0, 0, 0, 0); + searchBarBounds.set(0, 0, 0, 0); + return; + } + + // Calculate the search bar bounds, and account for the system insets + int edgeMarginPx = searchBarSpaceEdgeMarginsPx; + int availableWidth = width - systemInsets.left - systemInsets.right; + searchBarSpaceBounds.set(0, 0, availableWidth, 2 * edgeMarginPx + searchBarSpaceHeightPx); + + // Inset from the search bar space to get the search bar bounds + searchBarBounds.set(searchBarSpaceBounds); + searchBarBounds.inset(edgeMarginPx, edgeMarginPx); + } + + /** Converts from DPs to PXs */ + public int pxFromDp(float size) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + size, mDisplayMetrics)); + } + /** Converts from SPs to PXs */ + public int pxFromSp(float size) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + size, mDisplayMetrics)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java new file mode 100644 index 0000000..36b761e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.views.TaskStackView; +import com.android.systemui.recents.views.TaskViewTransform; + +import java.lang.ref.WeakReference; + + +/** The message handler to process Recents SysUI messages */ +class SystemUIMessageHandler extends Handler { + WeakReference<Context> mContext; + + SystemUIMessageHandler(Context context) { + // Keep a weak ref to the context instead of a strong ref + mContext = new WeakReference<Context>(context); + } + + @Override + public void handleMessage(Message msg) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsService|handleMessage]", msg); + + Context context = mContext.get(); + if (context == null) return; + + if (msg.what == AlternateRecentsComponent.MSG_UPDATE_FOR_CONFIGURATION) { + RecentsTaskLoader.initialize(context); + RecentsConfiguration.reinitialize(context); + + try { + Bundle data = msg.getData(); + Rect windowRect = data.getParcelable(AlternateRecentsComponent.KEY_WINDOW_RECT); + Rect systemInsets = data.getParcelable(AlternateRecentsComponent.KEY_SYSTEM_INSETS); + + // Create a dummy task stack & compute the rect for the thumbnail to animate to + TaskStack stack = new TaskStack(context); + TaskStackView tsv = new TaskStackView(context, stack); + Bundle replyData = new Bundle(); + TaskViewTransform transform; + + // Get the search bar bounds so that we can account for its height in the children + Rect searchBarSpaceBounds = new Rect(); + Rect searchBarBounds = new Rect(); + RecentsConfiguration config = RecentsConfiguration.getInstance(); + config.getSearchBarBounds(windowRect.width(), windowRect.height(), + searchBarSpaceBounds, searchBarBounds); + + // Calculate the target task rect for when there is one task + // NOTE: Since the nav bar height is already accounted for in the windowRect, don't + // pass in a bottom inset + stack.addTask(new Task()); + tsv.computeRects(windowRect.width(), windowRect.height() - systemInsets.top - + systemInsets.bottom - searchBarSpaceBounds.height(), 0); + tsv.boundScroll(); + transform = tsv.getStackTransform(0, tsv.getStackScroll()); + transform.rect.offset(0, searchBarSpaceBounds.height()); + replyData.putParcelable(AlternateRecentsComponent.KEY_SINGLE_TASK_STACK_RECT, + new Rect(transform.rect)); + + // Also calculate the target task rect when there are multiple tasks + stack.addTask(new Task()); + tsv.computeRects(windowRect.width(), windowRect.height() - systemInsets.top - + systemInsets.bottom - searchBarSpaceBounds.height(), 0); + tsv.setStackScrollRaw(Integer.MAX_VALUE); + tsv.boundScroll(); + transform = tsv.getStackTransform(1, tsv.getStackScroll()); + transform.rect.offset(0, searchBarSpaceBounds.height()); + replyData.putParcelable(AlternateRecentsComponent.KEY_MULTIPLE_TASK_STACK_RECT, + new Rect(transform.rect)); + + data.putParcelable(AlternateRecentsComponent.KEY_CONFIGURATION_DATA, replyData); + Message reply = Message.obtain(null, + AlternateRecentsComponent.MSG_UPDATE_FOR_CONFIGURATION, 0, 0); + reply.setData(data); + msg.replyTo.send(reply); + } catch (RemoteException re) { + re.printStackTrace(); + } + } else if (msg.what == AlternateRecentsComponent.MSG_CLOSE_RECENTS) { + // Do nothing + } else if (msg.what == AlternateRecentsComponent.MSG_TOGGLE_RECENTS) { + // Send a broadcast to toggle recents + Intent intent = new Intent(RecentsService.ACTION_TOGGLE_RECENTS_ACTIVITY); + intent.setPackage(context.getPackageName()); + context.sendBroadcast(intent); + + // Time this path + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup, + Constants.DebugFlags.App.TimeRecentsStartupKey, "receivedToggleRecents"); + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsLaunchTask, + Constants.DebugFlags.App.TimeRecentsLaunchKey, "receivedToggleRecents"); + } + } +} + +/* Service */ +public class RecentsService extends Service { + final static String ACTION_TOGGLE_RECENTS_ACTIVITY = "action_toggle_recents_activity"; + + Messenger mSystemUIMessenger = new Messenger(new SystemUIMessageHandler(this)); + + @Override + public void onCreate() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onCreate]"); + super.onCreate(); + } + + @Override + public IBinder onBind(Intent intent) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onBind]"); + return mSystemUIMessenger.getBinder(); + } + + @Override + public boolean onUnbind(Intent intent) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onUnbind]"); + return super.onUnbind(intent); + } + + @Override + public void onRebind(Intent intent) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onRebind]"); + super.onRebind(intent); + } + + @Override + public void onDestroy() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onDestroy]"); + super.onDestroy(); + } + + @Override + public void onTrimMemory(int level) { + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + if (loader != null) { + loader.onTrimMemory(level); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java new file mode 100644 index 0000000..52bba4a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java @@ -0,0 +1,593 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.app.ActivityManager; +import android.content.ComponentCallbacks2; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.UserHandle; +import android.util.LruCache; +import android.util.Pair; +import com.android.systemui.recents.model.SpaceNode; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + + +/** A bitmap load queue */ +class TaskResourceLoadQueue { + ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>(); + ConcurrentHashMap<Task.TaskKey, Boolean> mForceLoadSet = + new ConcurrentHashMap<Task.TaskKey, Boolean>(); + + static final Boolean sFalse = new Boolean(false); + + /** Adds a new task to the load queue */ + void addTask(Task t, boolean forceLoad) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|addTask]"); + if (!mQueue.contains(t)) { + mQueue.add(t); + } + if (forceLoad) { + mForceLoadSet.put(t.key, new Boolean(true)); + } + synchronized(this) { + notifyAll(); + } + } + + /** + * Retrieves the next task from the load queue, as well as whether we want that task to be + * force reloaded. + */ + Pair<Task, Boolean> nextTask() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|nextTask]"); + Task task = mQueue.poll(); + Boolean forceLoadTask = null; + if (task != null) { + forceLoadTask = mForceLoadSet.remove(task.key); + } + if (forceLoadTask == null) { + forceLoadTask = sFalse; + } + return new Pair<Task, Boolean>(task, forceLoadTask); + } + + /** Removes a task from the load queue */ + void removeTask(Task t) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|removeTask]"); + mQueue.remove(t); + mForceLoadSet.remove(t.key); + } + + /** Clears all the tasks from the load queue */ + void clearTasks() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|clearTasks]"); + mQueue.clear(); + mForceLoadSet.clear(); + } + + /** Returns whether the load queue is empty */ + boolean isEmpty() { + return mQueue.isEmpty(); + } +} + +/* Task resource loader */ +class TaskResourceLoader implements Runnable { + Context mContext; + HandlerThread mLoadThread; + Handler mLoadThreadHandler; + Handler mMainThreadHandler; + + SystemServicesProxy mSystemServicesProxy; + TaskResourceLoadQueue mLoadQueue; + DrawableLruCache mApplicationIconCache; + BitmapLruCache mThumbnailCache; + + boolean mCancelled; + boolean mWaitingOnLoadQueue; + + /** Constructor, creates a new loading thread that loads task resources in the background */ + public TaskResourceLoader(TaskResourceLoadQueue loadQueue, + DrawableLruCache applicationIconCache, + BitmapLruCache thumbnailCache) { + mLoadQueue = loadQueue; + mApplicationIconCache = applicationIconCache; + mThumbnailCache = thumbnailCache; + mMainThreadHandler = new Handler(); + mLoadThread = new HandlerThread("Recents-TaskResourceLoader"); + mLoadThread.setPriority(Thread.NORM_PRIORITY - 1); + mLoadThread.start(); + mLoadThreadHandler = new Handler(mLoadThread.getLooper()); + mLoadThreadHandler.post(this); + } + + /** Restarts the loader thread */ + void start(Context context) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|start]"); + mContext = context; + mCancelled = false; + mSystemServicesProxy = new SystemServicesProxy(context); + // Notify the load thread to start loading + synchronized(mLoadThread) { + mLoadThread.notifyAll(); + } + } + + /** Requests the loader thread to stop after the current iteration */ + void stop() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|stop]"); + // Mark as cancelled for the thread to pick up + mCancelled = true; + mSystemServicesProxy = null; + // If we are waiting for the load queue for more tasks, then we can just reset the + // Context now, since nothing is using it + if (mWaitingOnLoadQueue) { + mContext = null; + } + } + + @Override + public void run() { + while (true) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|run|" + Thread.currentThread().getId() + "]"); + if (mCancelled) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|cancel|" + Thread.currentThread().getId() + "]"); + // We have to unset the context here, since the background thread may be using it + // when we call stop() + mContext = null; + // If we are cancelled, then wait until we are started again + synchronized(mLoadThread) { + try { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|waitOnLoadThreadCancelled]"); + mLoadThread.wait(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } else { + SystemServicesProxy ssp = mSystemServicesProxy; + + // Load the next item from the queue + Pair<Task, Boolean> nextTaskData = mLoadQueue.nextTask(); + final Task t = nextTaskData.first; + final boolean forceLoadTask = nextTaskData.second; + if (t != null) { + Drawable loadIcon = mApplicationIconCache.get(t.key); + Bitmap loadThumbnail = mThumbnailCache.get(t.key); + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [TaskResourceLoader|load]", + t + " icon: " + loadIcon + " thumbnail: " + loadThumbnail + + " forceLoad: " + forceLoadTask); + // Load the application icon + if (loadIcon == null || forceLoadTask) { + ActivityInfo info = ssp.getActivityInfo(t.key.baseIntent.getComponent(), + t.userId); + Drawable icon = ssp.getActivityIcon(info, t.userId); + if (!mCancelled) { + if (icon != null) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [TaskResourceLoader|loadIcon]", + icon); + loadIcon = icon; + mApplicationIconCache.put(t.key, icon); + } + } + } + // Load the thumbnail + if (loadThumbnail == null || forceLoadTask) { + Bitmap thumbnail = ssp.getTaskThumbnail(t.key.id); + if (!mCancelled) { + if (thumbnail != null) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [TaskResourceLoader|loadThumbnail]", + thumbnail); + thumbnail.setHasAlpha(false); + loadThumbnail = thumbnail; + mThumbnailCache.put(t.key, thumbnail); + } else { + Console.logError(mContext, + "Failed to load task top thumbnail for: " + + t.key.baseIntent.getComponent().getPackageName()); + } + } + } + if (!mCancelled) { + // Notify that the task data has changed + final Drawable newIcon = loadIcon; + final Bitmap newThumbnail = loadThumbnail; + mMainThreadHandler.post(new Runnable() { + @Override + public void run() { + t.notifyTaskDataLoaded(newThumbnail, newIcon, forceLoadTask); + } + }); + } + } + + // If there are no other items in the list, then just wait until something is added + if (!mCancelled && mLoadQueue.isEmpty()) { + synchronized(mLoadQueue) { + try { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|waitOnLoadQueue]"); + mWaitingOnLoadQueue = true; + mLoadQueue.wait(); + mWaitingOnLoadQueue = false; + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + } + } + } +} + +/** + * The drawable cache. By using the Task's key, we can prevent holding onto a reference to the Task + * resource data, while keeping the cache data in memory where necessary. + */ +class DrawableLruCache extends LruCache<Task.TaskKey, Drawable> { + public DrawableLruCache(int cacheSize) { + super(cacheSize); + } + + @Override + protected int sizeOf(Task.TaskKey t, Drawable d) { + // The cache size will be measured in kilobytes rather than number of items + // NOTE: this isn't actually correct, as the icon may be smaller + int maxBytes = (d.getIntrinsicWidth() * d.getIntrinsicHeight() * 4); + return maxBytes / 1024; + } +} + +/** + * The bitmap cache. By using the Task's key, we can prevent holding onto a reference to the Task + * resource data, while keeping the cache data in memory where necessary. + */ +class BitmapLruCache extends LruCache<Task.TaskKey, Bitmap> { + public BitmapLruCache(int cacheSize) { + super(cacheSize); + } + + @Override + protected int sizeOf(Task.TaskKey t, Bitmap bitmap) { + // The cache size will be measured in kilobytes rather than number of items + return bitmap.getAllocationByteCount() / 1024; + } +} + +/* Recents task loader + * NOTE: We should not hold any references to a Context from a static instance */ +public class RecentsTaskLoader { + static RecentsTaskLoader sInstance; + + SystemServicesProxy mSystemServicesProxy; + DrawableLruCache mApplicationIconCache; + BitmapLruCache mThumbnailCache; + TaskResourceLoadQueue mLoadQueue; + TaskResourceLoader mLoader; + + int mMaxThumbnailCacheSize; + int mMaxIconCacheSize; + + BitmapDrawable mDefaultApplicationIcon; + Bitmap mDefaultThumbnail; + + /** Private Constructor */ + private RecentsTaskLoader(Context context) { + // Calculate the cache sizes, we just use a reasonable number here similar to those + // suggested in the Android docs, 1/8th for the thumbnail cache and 1/32 of the max memory + // for icons. + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + mMaxThumbnailCacheSize = maxMemory / 8; + mMaxIconCacheSize = mMaxThumbnailCacheSize / 4; + int iconCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 : + mMaxIconCacheSize; + int thumbnailCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 : + mMaxThumbnailCacheSize; + + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|init]", "thumbnailCache: " + thumbnailCacheSize + + " iconCache: " + iconCacheSize); + + // Initialize the proxy, cache and loaders + mSystemServicesProxy = new SystemServicesProxy(context); + mLoadQueue = new TaskResourceLoadQueue(); + mApplicationIconCache = new DrawableLruCache(iconCacheSize); + mThumbnailCache = new BitmapLruCache(thumbnailCacheSize); + mLoader = new TaskResourceLoader(mLoadQueue, mApplicationIconCache, mThumbnailCache); + + // Create the default assets + Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + icon.eraseColor(0x00000000); + mDefaultThumbnail = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + mDefaultThumbnail.eraseColor(0x00000000); + mDefaultApplicationIcon = new BitmapDrawable(context.getResources(), icon); + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|defaultBitmaps]", + "icon: " + mDefaultApplicationIcon + " thumbnail: " + mDefaultThumbnail, Console.AnsiRed); + } + + /** Initializes the recents task loader */ + public static RecentsTaskLoader initialize(Context context) { + if (sInstance == null) { + sInstance = new RecentsTaskLoader(context); + } + return sInstance; + } + + /** Returns the current recents task loader */ + public static RecentsTaskLoader getInstance() { + return sInstance; + } + + /** Returns the system services proxy */ + public SystemServicesProxy getSystemServicesProxy() { + return mSystemServicesProxy; + } + + private List<ActivityManager.RecentTaskInfo> getRecentTasks(Context context) { + long t1 = System.currentTimeMillis(); + + SystemServicesProxy ssp = mSystemServicesProxy; + List<ActivityManager.RecentTaskInfo> tasks = + ssp.getRecentTasks(25, UserHandle.CURRENT.getIdentifier()); + Collections.reverse(tasks); + Console.log(Constants.DebugFlags.App.TimeSystemCalls, + "[RecentsTaskLoader|getRecentTasks]", + "" + (System.currentTimeMillis() - t1) + "ms"); + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|tasks]", "" + tasks.size()); + + // Remove home/recents tasks + Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator(); + while (iter.hasNext()) { + ActivityManager.RecentTaskInfo t = iter.next(); + + // Skip tasks in the home stack + if (ssp.isInHomeStack(t.persistentId)) { + iter.remove(); + continue; + } + // Skip tasks from this Recents package + if (t.baseIntent.getComponent().getPackageName().equals(context.getPackageName())) { + iter.remove(); + continue; + } + } + + return tasks; + } + + /** Reload the set of recent tasks */ + SpaceNode reload(Context context, int preloadCount) { + long t1 = System.currentTimeMillis(); + + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|reload]"); + Resources res = context.getResources(); + ArrayList<Task> tasksToForceLoad = new ArrayList<Task>(); + TaskStack stack = new TaskStack(context); + SpaceNode root = new SpaceNode(context); + root.setStack(stack); + + // Get the recent tasks + SystemServicesProxy ssp = mSystemServicesProxy; + List<ActivityManager.RecentTaskInfo> tasks = getRecentTasks(context); + + // Add each task to the task stack + t1 = System.currentTimeMillis(); + int taskCount = tasks.size(); + for (int i = 0; i < taskCount; i++) { + ActivityManager.RecentTaskInfo t = tasks.get(i); + ActivityInfo info = ssp.getActivityInfo(t.baseIntent.getComponent(), t.userId); + if (info == null) continue; + + String activityLabel = (t.activityLabel == null ? ssp.getActivityLabel(info) : + t.activityLabel.toString()); + BitmapDrawable activityIcon = null; + if (t.activityIcon != null) { + activityIcon = new BitmapDrawable(res, t.activityIcon); + } + boolean isForemostTask = (i == (taskCount - 1)); + + // Create a new task + Task task = new Task(t.persistentId, (t.id > -1), t.baseIntent, activityLabel, + activityIcon, t.userId); + + // Preload the specified number of apps + if (i >= (taskCount - preloadCount)) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|preloadTask]", + "i: " + i + " task: " + t.baseIntent.getComponent().getPackageName()); + + // Load the icon (if possible and not the foremost task, from the cache) + if (!isForemostTask) { + task.applicationIcon = mApplicationIconCache.get(task.key); + if (task.applicationIcon != null) { + // Even though we get things from the cache, we should update them + // if they've changed in the bg + tasksToForceLoad.add(task); + } + } + if (task.applicationIcon == null) { + task.applicationIcon = ssp.getActivityIcon(info, task.userId); + if (task.applicationIcon != null) { + mApplicationIconCache.put(task.key, task.applicationIcon); + } else { + task.applicationIcon = mDefaultApplicationIcon; + } + } + + // Load the thumbnail (if possible and not the foremost task, from the cache) + if (!isForemostTask) { + task.thumbnail = mThumbnailCache.get(task.key); + if (task.thumbnail != null) { + // Even though we get things from the cache, we should update them if + // they've changed in the bg + tasksToForceLoad.add(task); + } + } + if (task.thumbnail == null) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|loadingTaskThumbnail]"); + task.thumbnail = ssp.getTaskThumbnail(task.key.id); + if (task.thumbnail != null) { + task.thumbnail.setHasAlpha(false); + mThumbnailCache.put(task.key, task.thumbnail); + } else { + task.thumbnail = mDefaultThumbnail; + } + } + } + + // Add the task to the stack + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName()); + stack.addTask(task); + } + Console.log(Constants.DebugFlags.App.TimeSystemCalls, + "[RecentsTaskLoader|getAllTaskTopThumbnail]", + "" + (System.currentTimeMillis() - t1) + "ms"); + + /* + // Get all the stacks + t1 = System.currentTimeMillis(); + List<ActivityManager.StackInfo> stackInfos = ams.getAllStackInfos(); + Console.log(Constants.DebugFlags.App.TimeSystemCalls, "[RecentsTaskLoader|getAllStackInfos]", "" + (System.currentTimeMillis() - t1) + "ms"); + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|stacks]", "" + tasks.size()); + for (ActivityManager.StackInfo s : stackInfos) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [RecentsTaskLoader|stack]", s.toString()); + if (stacks.containsKey(s.stackId)) { + stacks.get(s.stackId).setRect(s.bounds); + } + } + */ + + // Start the task loader + mLoader.start(context); + + // Add all the tasks that we are force/re-loading + for (Task t : tasksToForceLoad) { + mLoadQueue.addTask(t, true); + } + + return root; + } + + /** Acquires the task resource data from the pool. */ + public void loadTaskData(Task t) { + Drawable applicationIcon = mApplicationIconCache.get(t.key); + Bitmap thumbnail = mThumbnailCache.get(t.key); + + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|loadTask]", + t + " applicationIcon: " + applicationIcon + " thumbnail: " + thumbnail + + " thumbnailCacheSize: " + mThumbnailCache.size()); + + boolean requiresLoad = false; + if (applicationIcon == null) { + applicationIcon = mDefaultApplicationIcon; + requiresLoad = true; + } + if (thumbnail == null) { + thumbnail = mDefaultThumbnail; + requiresLoad = true; + } + if (requiresLoad) { + mLoadQueue.addTask(t, false); + } + t.notifyTaskDataLoaded(thumbnail, applicationIcon, false); + } + + /** Releases the task resource data back into the pool. */ + public void unloadTaskData(Task t) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|unloadTask]", t + + " thumbnailCacheSize: " + mThumbnailCache.size()); + + mLoadQueue.removeTask(t); + t.notifyTaskDataUnloaded(mDefaultThumbnail, mDefaultApplicationIcon); + } + + /** Completely removes the resource data from the pool. */ + public void deleteTaskData(Task t) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|deleteTask]", t); + + mLoadQueue.removeTask(t); + mThumbnailCache.remove(t.key); + mApplicationIconCache.remove(t.key); + t.notifyTaskDataUnloaded(mDefaultThumbnail, mDefaultApplicationIcon); + } + + /** Stops the task loader and clears all pending tasks */ + void stopLoader() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|stopLoader]"); + mLoader.stop(); + mLoadQueue.clearTasks(); + } + + void onTrimMemory(int level) { + Console.log(Constants.DebugFlags.App.Memory, "[RecentsTaskLoader|onTrimMemory]", + Console.trimMemoryLevelToString(level)); + + switch (level) { + case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: + // Stop the loader immediately when the UI is no longer visible + stopLoader(); + break; + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: + case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: + // We are leaving recents, so trim the data a bit + mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 2); + mApplicationIconCache.trimToSize(mMaxIconCacheSize / 2); + break; + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: + case ComponentCallbacks2.TRIM_MEMORY_MODERATE: + // We are going to be low on memory + mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 4); + mApplicationIconCache.trimToSize(mMaxIconCacheSize / 4); + break; + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: + case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: + // We are low on memory, so release everything + mThumbnailCache.evictAll(); + mApplicationIconCache.evictAll(); + break; + default: + break; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/SystemServicesProxy.java new file mode 100644 index 0000000..33ac0a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/SystemServicesProxy.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.AppGlobals; +import android.app.SearchManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Acts as a shim around the real system services that we need to access data from, and provides + * a point of injection when testing UI. + */ +public class SystemServicesProxy { + ActivityManager mAm; + PackageManager mPm; + IPackageManager mIpm; + UserManager mUm; + SearchManager mSm; + String mPackage; + + Bitmap mDummyIcon; + + /** Private constructor */ + public SystemServicesProxy(Context context) { + mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + mPm = context.getPackageManager(); + mUm = (UserManager) context.getSystemService(Context.USER_SERVICE); + mIpm = AppGlobals.getPackageManager(); + mSm = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); + mPackage = context.getPackageName(); + + if (Constants.DebugFlags.App.EnableSystemServicesProxy) { + // Create a dummy icon + mDummyIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + mDummyIcon.eraseColor(0xFF999999); + } + } + + /** Returns a list of the recents tasks */ + public List<ActivityManager.RecentTaskInfo> getRecentTasks(int numTasks, int userId) { + if (mAm == null) return null; + + // If we are mocking, then create some recent tasks + if (Constants.DebugFlags.App.EnableSystemServicesProxy) { + ArrayList<ActivityManager.RecentTaskInfo> tasks = + new ArrayList<ActivityManager.RecentTaskInfo>(); + int count = Math.min(numTasks, Constants.DebugFlags.App.SystemServicesProxyMockTaskCount); + for (int i = 0; i < count; i++) { + // Create a dummy component name + int packageIndex = i % Constants.DebugFlags.App.SystemServicesProxyMockPackageCount; + ComponentName cn = new ComponentName("com.android.test" + packageIndex, + "com.android.test" + i + ".Activity"); + // Create the recent task info + ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo(); + rti.id = rti.persistentId = i; + rti.baseIntent = new Intent(); + rti.baseIntent.setComponent(cn); + rti.description = rti.activityLabel = "" + i + " - " + + Long.toString(Math.abs(new Random().nextLong()), 36); + if (i % 2 == 0) { + rti.activityIcon = Bitmap.createBitmap(mDummyIcon); + } + tasks.add(rti); + } + return tasks; + } + + return mAm.getRecentTasksForUser(numTasks, + ActivityManager.RECENT_IGNORE_UNAVAILABLE | + ActivityManager.RECENT_INCLUDE_PROFILES, userId); + } + + /** Returns a list of the running tasks */ + public List<ActivityManager.RunningTaskInfo> getRunningTasks(int numTasks) { + if (mAm == null) return null; + return mAm.getRunningTasks(numTasks); + } + + /** Returns whether the specified task is in the home stack */ + public boolean isInHomeStack(int taskId) { + if (mAm == null) return false; + + // If we are mocking, then just return false + if (Constants.DebugFlags.App.EnableSystemServicesProxy) { + return false; + } + + return mAm.isInHomeStack(taskId); + } + + /** Returns the top task thumbnail for the given task id */ + public Bitmap getTaskThumbnail(int taskId) { + if (mAm == null) return null; + + // If we are mocking, then just return a dummy thumbnail + if (Constants.DebugFlags.App.EnableSystemServicesProxy) { + Bitmap thumbnail = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + thumbnail.eraseColor(0xff333333); + return thumbnail; + } + + return mAm.getTaskTopThumbnail(taskId); + } + + /** Moves a task to the front with the specified activity options */ + public void moveTaskToFront(int taskId, ActivityOptions opts) { + if (mAm == null) return; + if (Constants.DebugFlags.App.EnableSystemServicesProxy) return; + + if (opts != null) { + mAm.moveTaskToFront(taskId, ActivityManager.MOVE_TASK_WITH_HOME, + opts.toBundle()); + } else { + mAm.moveTaskToFront(taskId, ActivityManager.MOVE_TASK_WITH_HOME); + } + } + + /** Removes the task and kills the process */ + public void removeTask(int taskId) { + if (mAm == null) return; + if (Constants.DebugFlags.App.EnableSystemServicesProxy) return; + + mAm.removeTask(taskId, ActivityManager.REMOVE_TASK_KILL_PROCESS); + } + + /** + * Returns the activity info for a given component name. + * + * @param ComponentName The component name of the activity. + * @param userId The userId of the user that this is for. + */ + public ActivityInfo getActivityInfo(ComponentName cn, int userId) { + if (mIpm == null) return null; + if (Constants.DebugFlags.App.EnableSystemServicesProxy) return null; + + try { + return mIpm.getActivityInfo(cn, PackageManager.GET_META_DATA, userId); + } catch (RemoteException e) { + e.printStackTrace(); + return null; + } + } + + /** Returns the activity label */ + public String getActivityLabel(ActivityInfo info) { + if (mPm == null) return null; + + // If we are mocking, then return a mock label + if (Constants.DebugFlags.App.EnableSystemServicesProxy) { + return "Recent Task"; + } + + return info.loadLabel(mPm).toString(); + } + + /** + * Returns the activity icon for the ActivityInfo for a user, badging if + * necessary. + */ + public Drawable getActivityIcon(ActivityInfo info, int userId) { + if (mPm == null || mUm == null) return null; + + // If we are mocking, then return a mock label + if (Constants.DebugFlags.App.EnableSystemServicesProxy) { + return new ColorDrawable(0xFF666666); + } + + Drawable icon = info.loadIcon(mPm); + if (userId != UserHandle.myUserId()) { + icon = mUm.getBadgedDrawableForUser(icon, new UserHandle(userId)); + } + return icon; + } + + + /** + * Composes an intent to launch the global search activity. + */ + public Intent getGlobalSearchIntent(Rect sourceBounds) { + if (mSm == null) return null; + + // Try and get the global search activity + ComponentName globalSearchActivity = mSm.getGlobalSearchActivity(); + if (globalSearchActivity == null) return null; + + // Bundle the source of the search + Bundle appSearchData = new Bundle(); + appSearchData.putString("source", mPackage); + + // Compose the intent and Start the search activity + Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setComponent(globalSearchActivity); + intent.putExtra(SearchManager.APP_DATA, appSearchData); + intent.setSourceBounds(sourceBounds); + return intent; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/Utilities.java new file mode 100644 index 0000000..4a1b3b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/Utilities.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.recents; + +import android.graphics.Rect; + +/* Common code */ +public class Utilities { + /** + * Calculates a consistent animation duration (ms) for all animations depending on the movement + * of the object being animated. + */ + public static int calculateTranslationAnimationDuration(int distancePx) { + return calculateTranslationAnimationDuration(distancePx, 100); + } + public static int calculateTranslationAnimationDuration(int distancePx, int minDuration) { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + return Math.max(minDuration, (int) (1000f /* ms/s */ * + (Math.abs(distancePx) / config.animationPxMovementPerSecond))); + } + + /** Scales a rect about its centroid */ + public static void scaleRectAboutCenter(Rect r, float scale) { + if (scale != 1.0f) { + int cx = r.centerX(); + int cy = r.centerY(); + r.offset(-cx, -cy); + r.left = (int) (r.left * scale + 0.5f); + r.top = (int) (r.top * scale + 0.5f); + r.right = (int) (r.right * scale + 0.5f); + r.bottom = (int) (r.bottom * scale + 0.5f); + r.offset(cx, cy); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java new file mode 100644 index 0000000..1dd1be6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.model; + +import android.content.Context; +import android.graphics.Rect; + +import java.util.ArrayList; + + +/** + * The full recents space is partitioned using a BSP into various nodes that define where task + * stacks should be placed. + */ +public class SpaceNode { + /* BSP node callbacks */ + public interface SpaceNodeCallbacks { + /** Notifies when a node is added */ + public void onSpaceNodeAdded(SpaceNode node); + /** Notifies when a node is measured */ + public void onSpaceNodeMeasured(SpaceNode node, Rect rect); + } + + Context mContext; + + SpaceNode mStartNode; + SpaceNode mEndNode; + + TaskStack mStack; + + public SpaceNode(Context context) { + mContext = context; + } + + /** Sets the current stack for this space node */ + public void setStack(TaskStack stack) { + mStack = stack; + } + + /** Returns the task stack (not null if this is a leaf) */ + TaskStack getStack() { + return mStack; + } + + /** Returns whether this is a leaf node */ + boolean isLeafNode() { + return (mStartNode == null) && (mEndNode == null); + } + + /** Returns all the descendent task stacks */ + private void getStacksRec(ArrayList<TaskStack> stacks) { + if (isLeafNode()) { + stacks.add(mStack); + } else { + mStartNode.getStacksRec(stacks); + mEndNode.getStacksRec(stacks); + } + } + public ArrayList<TaskStack> getStacks() { + ArrayList<TaskStack> stacks = new ArrayList<TaskStack>(); + getStacksRec(stacks); + return stacks; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java new file mode 100644 index 0000000..1566a49 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.model; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + + +/** + * A task represents the top most task in the system's task stack. + */ +public class Task { + /* Task callbacks */ + public interface TaskCallbacks { + /* Notifies when a task has been bound */ + public void onTaskDataLoaded(boolean reloadingTaskData); + /* Notifies when a task has been unbound */ + public void onTaskDataUnloaded(); + } + + /* The Task Key represents the unique primary key for the task */ + public static class TaskKey { + public final int id; + public final Intent baseIntent; + public final int userId; + + public TaskKey(int id, Intent intent, int userId) { + this.id = id; + this.baseIntent = intent; + this.userId = userId; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TaskKey)) { + return false; + } + return id == ((TaskKey) o).id + && userId == ((TaskKey) o).userId; + } + + @Override + public int hashCode() { + return (id << 5) + userId; + } + + @Override + public String toString() { + return "Task.Key: " + id + ", " + + "u" + userId + ", " + + baseIntent.getComponent().getPackageName(); + } + } + + public TaskKey key; + public Drawable applicationIcon; + public Drawable activityIcon; + public String activityLabel; + public Bitmap thumbnail; + public boolean isActive; + public int userId; + + TaskCallbacks mCb; + + public Task() { + // Only used by RecentsService for task rect calculations. + } + + public Task(int id, boolean isActive, Intent intent, String activityTitle, + BitmapDrawable activityIcon, int userId) { + this.key = new TaskKey(id, intent, userId); + this.activityLabel = activityTitle; + this.activityIcon = activityIcon; + this.isActive = isActive; + this.userId = userId; + } + + /** Set the callbacks */ + public void setCallbacks(TaskCallbacks cb) { + mCb = cb; + } + + /** Notifies the callback listeners that this task has been loaded */ + public void notifyTaskDataLoaded(Bitmap thumbnail, Drawable applicationIcon, + boolean reloadingTaskData) { + this.applicationIcon = applicationIcon; + this.thumbnail = thumbnail; + if (mCb != null) { + mCb.onTaskDataLoaded(reloadingTaskData); + } + } + + /** Notifies the callback listeners that this task has been unloaded */ + public void notifyTaskDataUnloaded(Bitmap defaultThumbnail, Drawable defaultApplicationIcon) { + applicationIcon = defaultApplicationIcon; + thumbnail = defaultThumbnail; + if (mCb != null) { + mCb.onTaskDataUnloaded(); + } + } + + @Override + public boolean equals(Object o) { + // Check that the id matches + Task t = (Task) o; + return key.equals(t.key); + } + + @Override + public String toString() { + return "Task: " + key.baseIntent.getComponent().getPackageName() + " [" + super.toString() + "]"; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java new file mode 100644 index 0000000..d2de185 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.model; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.List; + + +/** + * An interface for a task filter to query whether a particular task should show in a stack. + */ +interface TaskFilter { + /** Returns whether the filter accepts the specified task */ + public boolean acceptTask(Task t, int index); +} + +/** + * A list of filtered tasks. + */ +class FilteredTaskList { + ArrayList<Task> mTasks = new ArrayList<Task>(); + ArrayList<Task> mFilteredTasks = new ArrayList<Task>(); + TaskFilter mFilter; + + /** Sets the task filter, saving the current touch state */ + boolean setFilter(TaskFilter filter) { + ArrayList<Task> prevFilteredTasks = new ArrayList<Task>(mFilteredTasks); + mFilter = filter; + updateFilteredTasks(); + if (!prevFilteredTasks.equals(mFilteredTasks)) { + return true; + } else { + // If the tasks are exactly the same pre/post filter, then just reset it + mFilter = null; + return false; + } + } + + /** Removes the task filter and returns the previous touch state */ + void removeFilter() { + mFilter = null; + updateFilteredTasks(); + } + + /** Adds a new task to the task list */ + void add(Task t) { + mTasks.add(t); + updateFilteredTasks(); + } + + /** Sets the list of tasks */ + void set(List<Task> tasks) { + mTasks.clear(); + mTasks.addAll(tasks); + updateFilteredTasks(); + } + + /** Removes a task from the base list only if it is in the filtered list */ + boolean remove(Task t) { + if (mFilteredTasks.contains(t)) { + boolean removed = mTasks.remove(t); + updateFilteredTasks(); + return removed; + } + return false; + } + + /** Returns the index of this task in the list of filtered tasks */ + int indexOf(Task t) { + return mFilteredTasks.indexOf(t); + } + + /** Returns the size of the list of filtered tasks */ + int size() { + return mFilteredTasks.size(); + } + + /** Returns whether the filtered list contains this task */ + boolean contains(Task t) { + return mFilteredTasks.contains(t); + } + + /** Updates the list of filtered tasks whenever the base task list changes */ + private void updateFilteredTasks() { + mFilteredTasks.clear(); + if (mFilter != null) { + int taskCount = mTasks.size(); + for (int i = 0; i < taskCount; i++) { + Task t = mTasks.get(i); + if (mFilter.acceptTask(t, i)) { + mFilteredTasks.add(t); + } + } + } else { + mFilteredTasks.addAll(mTasks); + } + } + + /** Returns whether this task list is filtered */ + boolean hasFilter() { + return (mFilter != null); + } + + /** Returns the list of filtered tasks */ + ArrayList<Task> getTasks() { + return mFilteredTasks; + } +} + +/** + * The task stack contains a list of multiple tasks. + */ +public class TaskStack { + /* Task stack callbacks */ + public interface TaskStackCallbacks { + /* Notifies when a task has been added to the stack */ + public void onStackTaskAdded(TaskStack stack, Task t); + /* Notifies when a task has been removed from the stack */ + public void onStackTaskRemoved(TaskStack stack, Task t); + /** Notifies when the stack was filtered */ + public void onStackFiltered(TaskStack newStack, ArrayList<Task> curTasks, Task t); + /** Notifies when the stack was un-filtered */ + public void onStackUnfiltered(TaskStack newStack, ArrayList<Task> curTasks); + } + + Context mContext; + + FilteredTaskList mTaskList = new FilteredTaskList(); + TaskStackCallbacks mCb; + + public TaskStack(Context context) { + mContext = context; + } + + /** Sets the callbacks for this task stack */ + public void setCallbacks(TaskStackCallbacks cb) { + mCb = cb; + } + + /** Adds a new task */ + public void addTask(Task t) { + mTaskList.add(t); + if (mCb != null) { + mCb.onStackTaskAdded(this, t); + } + } + + /** Removes a task */ + public void removeTask(Task t) { + if (mTaskList.contains(t)) { + mTaskList.remove(t); + if (mCb != null) { + mCb.onStackTaskRemoved(this, t); + } + } + } + + /** Sets a few tasks in one go */ + public void setTasks(List<Task> tasks) { + int taskCount = mTaskList.getTasks().size(); + for (int i = 0; i < taskCount; i++) { + Task t = mTaskList.getTasks().get(i); + if (mCb != null) { + mCb.onStackTaskRemoved(this, t); + } + } + mTaskList.set(tasks); + for (Task t : tasks) { + if (mCb != null) { + mCb.onStackTaskAdded(this, t); + } + } + } + + /** Gets the tasks */ + public ArrayList<Task> getTasks() { + return mTaskList.getTasks(); + } + + /** Gets the number of tasks */ + public int getTaskCount() { + return mTaskList.size(); + } + + /** Returns the index of this task in this current task stack */ + public int indexOfTask(Task t) { + return mTaskList.indexOf(t); + } + + /** Tests whether a task is in this current task stack */ + public boolean containsTask(Task t) { + return mTaskList.contains(t); + } + + /** Filters the stack into tasks similar to the one specified */ + public void filterTasks(final Task t) { + ArrayList<Task> oldStack = new ArrayList<Task>(mTaskList.getTasks()); + + // Set the task list filter + boolean filtered = mTaskList.setFilter(new TaskFilter() { + @Override + public boolean acceptTask(Task at, int i) { + return t.key.baseIntent.getComponent().getPackageName().equals( + at.key.baseIntent.getComponent().getPackageName()); + } + }); + if (filtered && mCb != null) { + mCb.onStackFiltered(this, oldStack, t); + } + } + + /** Unfilters the current stack */ + public void unfilterTasks() { + ArrayList<Task> oldStack = new ArrayList<Task>(mTaskList.getTasks()); + + // Unset the filter, then update the virtual scroll + mTaskList.removeFilter(); + if (mCb != null) { + mCb.onStackUnfiltered(this, oldStack); + } + } + + /** Returns whether tasks are currently filtered */ + public boolean hasFilteredTasks() { + return mTaskList.hasFilter(); + } + + @Override + public String toString() { + String str = "Tasks:\n"; + for (Task t : mTaskList.getTasks()) { + str += " " + t.toString() + "\n"; + } + return str; + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java new file mode 100644 index 0000000..a04cd3e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.app.ActivityOptions; +import android.app.TaskStackBuilder; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.net.Uri; +import android.os.UserHandle; +import android.provider.Settings; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; +import com.android.systemui.recents.Console; +import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.RecentsTaskLoader; +import com.android.systemui.recents.model.SpaceNode; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.R; + +import java.util.ArrayList; + + +/** + * This view is the the top level layout that contains TaskStacks (which are laid out according + * to their SpaceNode bounds. + */ +public class RecentsView extends FrameLayout implements TaskStackView.TaskStackViewCallbacks { + + /** The RecentsView callbacks */ + public interface RecentsViewCallbacks { + public void onTaskLaunching(); + } + + // The space partitioning root of this container + SpaceNode mBSP; + // Search bar view + View mSearchBar; + // Recents view callbacks + RecentsViewCallbacks mCb; + + LayoutInflater mInflater; + + public RecentsView(Context context) { + super(context); + mInflater = LayoutInflater.from(context); + setWillNotDraw(false); + } + + /** Sets the callbacks */ + public void setCallbacks(RecentsViewCallbacks cb) { + mCb = cb; + } + + /** Set/get the bsp root node */ + public void setBSP(SpaceNode n) { + mBSP = n; + + // Create and add all the stacks for this partition of space. + boolean hasTasks = false; + removeAllViews(); + ArrayList<TaskStack> stacks = mBSP.getStacks(); + for (TaskStack stack : stacks) { + TaskStackView stackView = new TaskStackView(getContext(), stack); + stackView.setCallbacks(this); + addView(stackView); + hasTasks |= (stack.getTaskCount() > 0); + } + + // Create the search bar (and hide it if we have no recent tasks) + if (Constants.DebugFlags.App.EnableSearchButton) { + createSearchBar(); + if (!hasTasks) { + mSearchBar.setVisibility(View.GONE); + } + } + } + + /** Launches the first task from the first stack if possible */ + public boolean launchFirstTask() { + // Get the first stack view + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child instanceof TaskStackView) { + TaskStackView stackView = (TaskStackView) child; + TaskStack stack = stackView.mStack; + ArrayList<Task> tasks = stack.getTasks(); + + // Get the first task in the stack + if (!tasks.isEmpty()) { + Task task = tasks.get(tasks.size() - 1); + TaskView tv = null; + + // Try and use the first child task view as the source of the launch animation + if (stackView.getChildCount() > 0) { + TaskView stv = (TaskView) stackView.getChildAt(stackView.getChildCount() - 1); + if (stv.getTask() == task) { + tv = stv; + } + } + onTaskLaunched(stackView, tv, stack, task); + return true; + } + } + } + return false; + } + + /** Creates and adds the search bar */ + void createSearchBar() { + // Create a temporary search bar + mSearchBar = mInflater.inflate(R.layout.recents_search_bar, this, false); + mSearchBar.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onSearchTriggered(); + } + }); + addView(mSearchBar); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|measure]", + "width: " + width + " height: " + height, Console.AnsiGreen); + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup, + Constants.DebugFlags.App.TimeRecentsStartupKey, "RecentsView.onMeasure"); + + // Get the search bar bounds so that we can account for its height in the children + Rect searchBarSpaceBounds = new Rect(); + Rect searchBarBounds = new Rect(); + RecentsConfiguration config = RecentsConfiguration.getInstance(); + config.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(), + searchBarSpaceBounds, searchBarBounds); + if (mSearchBar != null) { + mSearchBar.measure(MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.width(), widthMode), + MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.height(), heightMode)); + } + + // We measure our stack views sans the status bar. It will handle the nav bar itself. + int childWidth = width - config.systemInsets.right; + int childHeight = height - config.systemInsets.top - searchBarSpaceBounds.height(); + + // Measure each child + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child instanceof TaskStackView && child.getVisibility() != GONE) { + child.measure(MeasureSpec.makeMeasureSpec(childWidth, widthMode), + MeasureSpec.makeMeasureSpec(childHeight, heightMode)); + } + } + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|layout]", + new Rect(left, top, right, bottom) + " changed: " + changed, Console.AnsiGreen); + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup, + Constants.DebugFlags.App.TimeRecentsStartupKey, "RecentsView.onLayout"); + + // Get the search bar bounds so that we can account for its height in the children + Rect searchBarSpaceBounds = new Rect(); + Rect searchBarBounds = new Rect(); + RecentsConfiguration config = RecentsConfiguration.getInstance(); + config.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(), + searchBarSpaceBounds, searchBarBounds); + if (mSearchBar != null) { + mSearchBar.layout(config.systemInsets.left + searchBarSpaceBounds.left, + config.systemInsets.top + searchBarSpaceBounds.top, + config.systemInsets.left + mSearchBar.getMeasuredWidth(), + config.systemInsets.top + mSearchBar.getMeasuredHeight()); + } + + // We offset our stack views by the status bar height. It will handle the nav bar itself. + top += config.systemInsets.top + searchBarSpaceBounds.height(); + + // Layout each child + // XXX: Based on the space node for that task view + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child instanceof TaskStackView && child.getVisibility() != GONE) { + int width = child.getMeasuredWidth(); + int height = child.getMeasuredHeight(); + child.layout(left, top, left + width, top + height); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + Console.log(Constants.DebugFlags.UI.Draw, "[RecentsView|dispatchDraw]", "", + Console.AnsiPurple); + super.dispatchDraw(canvas); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, + "[RecentsView|fitSystemWindows]", "insets: " + insets, Console.AnsiGreen); + + // Update the configuration with the latest system insets and trigger a relayout + RecentsConfiguration config = RecentsConfiguration.getInstance(); + config.updateSystemInsets(insets); + requestLayout(); + + return true; + } + + /** Closes any open info panes */ + public boolean closeOpenInfoPanes() { + if (mBSP != null) { + // Get the first stack view + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child instanceof TaskStackView) { + TaskStackView stackView = (TaskStackView) child; + if (stackView.closeOpenInfoPanes()) { + return true; + } + } + } + } + return false; + } + + /** Unfilters any filtered stacks */ + public boolean unfilterFilteredStacks() { + if (mBSP != null) { + // Check if there are any filtered stacks and unfilter them before we back out of Recents + boolean stacksUnfiltered = false; + ArrayList<TaskStack> stacks = mBSP.getStacks(); + for (TaskStack stack : stacks) { + if (stack.hasFilteredTasks()) { + stack.unfilterTasks(); + stacksUnfiltered = true; + } + } + return stacksUnfiltered; + } + return false; + } + + /**** TaskStackView.TaskStackCallbacks Implementation ****/ + + @Override + public void onTaskLaunched(final TaskStackView stackView, final TaskView tv, + final TaskStack stack, final Task task) { + // Notify any callbacks of the launching of a new task + if (mCb != null) { + mCb.onTaskLaunching(); + } + + // Close any open info panes + closeOpenInfoPanes(); + + final Runnable launchRunnable = new Runnable() { + @Override + public void run() { + TaskViewTransform transform; + View sourceView = tv; + int offsetX = 0; + int offsetY = 0; + int stackScroll = stackView.getStackScroll(); + if (tv == null) { + // If there is no actual task view, then use the stack view as the source view + // and then offset to the expected transform rect, but bound this to just + // outside the display rect (to ensure we don't animate from too far away) + RecentsConfiguration config = RecentsConfiguration.getInstance(); + sourceView = stackView; + transform = stackView.getStackTransform(stack.indexOfTask(task), stackScroll); + offsetX = transform.rect.left; + offsetY = Math.min(transform.rect.top, config.displayRect.height()); + } else { + transform = stackView.getStackTransform(stack.indexOfTask(task), stackScroll); + } + + // Compute the thumbnail to scale up from + ActivityOptions opts = null; + int thumbnailWidth = transform.rect.width(); + int thumbnailHeight = transform.rect.height(); + if (task.thumbnail != null && thumbnailWidth > 0 && thumbnailHeight > 0 && + task.thumbnail.getWidth() > 0 && task.thumbnail.getHeight() > 0) { + // Resize the thumbnail to the size of the view that we are animating from + Bitmap b = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + c.drawBitmap(task.thumbnail, + new Rect(0, 0, task.thumbnail.getWidth(), task.thumbnail.getHeight()), + new Rect(0, 0, thumbnailWidth, thumbnailHeight), null); + c.setBitmap(null); + opts = ActivityOptions.makeThumbnailScaleUpAnimation(sourceView, + b, offsetX, offsetY); + } + + if (task.isActive) { + // Bring an active task to the foreground + RecentsTaskLoader.getInstance().getSystemServicesProxy() + .moveTaskToFront(task.key.id, opts); + } else { + // Launch the activity with the desired animation + Intent i = new Intent(task.key.baseIntent); + i.setFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY + | Intent.FLAG_ACTIVITY_TASK_ON_HOME + | Intent.FLAG_ACTIVITY_NEW_TASK); + try { + UserHandle taskUser = new UserHandle(task.userId); + if (opts != null) { + getContext().startActivityAsUser(i, opts.toBundle(), taskUser); + } else { + getContext().startActivityAsUser(i, taskUser); + } + } catch (ActivityNotFoundException anfe) { + Console.logError(getContext(), "Could not start Activity"); + } + } + + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsLaunchTask, + Constants.DebugFlags.App.TimeRecentsLaunchKey, "startActivity"); + } + }; + + Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsLaunchTask, + Constants.DebugFlags.App.TimeRecentsLaunchKey, "onTaskLaunched"); + + // Launch the app right away if there is no task view, otherwise, animate the icon out first + if (tv == null || !Constants.Values.TaskView.AnimateFrontTaskBarOnLeavingRecents) { + post(launchRunnable); + } else { + tv.animateOnLeavingRecents(launchRunnable); + } + } + + @Override + public void onTaskAppInfoLaunched(Task t) { + // Create a new task stack with the application info details activity + Intent baseIntent = t.key.baseIntent; + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", baseIntent.getComponent().getPackageName(), null)); + intent.setComponent(intent.resolveActivity(getContext().getPackageManager())); + TaskStackBuilder.create(getContext()) + .addNextIntentWithParentStack(intent).startActivities(); + } + + public void onSearchTriggered() { + // Get the search bar source bounds + Rect searchBarSpaceBounds = new Rect(); + Rect searchBarBounds = new Rect(); + RecentsConfiguration config = RecentsConfiguration.getInstance(); + config.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(), + searchBarSpaceBounds, searchBarBounds); + + // Get the search intent and start it + Intent searchIntent = RecentsTaskLoader.getInstance().getSystemServicesProxy() + .getGlobalSearchIntent(searchBarBounds); + if (searchIntent != null) { + try { + getContext().startActivity(searchIntent); + } catch (ActivityNotFoundException anfe) { + Console.logError(getContext(), "Could not start Search activity"); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java new file mode 100644 index 0000000..21ef9ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.animation.LinearInterpolator; +import com.android.systemui.recents.Console; +import com.android.systemui.recents.Constants; + +/** + * This class facilitates swipe to dismiss. It defines an interface to be implemented by the + * by the class hosting the views that need to swiped, and, using this interface, handles touch + * events and translates / fades / animates the view as it is dismissed. + */ +public class SwipeHelper { + static final String TAG = "SwipeHelper"; + private static final boolean SLOW_ANIMATIONS = false; // DEBUG; + private static final boolean CONSTRAIN_SWIPE = true; + private static final boolean FADE_OUT_DURING_SWIPE = true; + private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; + + public static final int X = 0; + public static final int Y = 1; + + private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); + + private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec + private int DEFAULT_ESCAPE_ANIMATION_DURATION = 75; // ms + private int MAX_ESCAPE_ANIMATION_DURATION = 150; // ms + private int MAX_DISMISS_VELOCITY = 2000; // dp/sec + private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms + + public static float ALPHA_FADE_START = 0.15f; // fraction of thumbnail width + // where fade starts + static final float ALPHA_FADE_END = 0.65f; // fraction of thumbnail width + // beyond which alpha->0 + private float mMinAlpha = 0f; + + private float mPagingTouchSlop; + Callback mCallback; + private int mSwipeDirection; + private VelocityTracker mVelocityTracker; + + private float mInitialTouchPos; + private boolean mDragging; + + private View mCurrView; + private boolean mCanCurrViewBeDimissed; + private float mDensityScale; + + public boolean mAllowSwipeTowardsStart = true; + public boolean mAllowSwipeTowardsEnd = true; + private boolean mRtl; + + public SwipeHelper(int swipeDirection, Callback callback, float densityScale, + float pagingTouchSlop) { + mCallback = callback; + mSwipeDirection = swipeDirection; + mVelocityTracker = VelocityTracker.obtain(); + mDensityScale = densityScale; + mPagingTouchSlop = pagingTouchSlop; + } + + public void setDensityScale(float densityScale) { + mDensityScale = densityScale; + } + + public void setPagingTouchSlop(float pagingTouchSlop) { + mPagingTouchSlop = pagingTouchSlop; + } + + public void cancelOngoingDrag() { + if (mDragging) { + if (mCurrView != null) { + mCallback.onDragCancelled(mCurrView); + setTranslation(mCurrView, 0); + mCallback.onSnapBackCompleted(mCurrView); + mCurrView = null; + } + mDragging = false; + } + } + + public void resetTranslation(View v) { + setTranslation(v, 0); + } + + private float getPos(MotionEvent ev) { + return mSwipeDirection == X ? ev.getX() : ev.getY(); + } + + private float getTranslation(View v) { + return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); + } + + private float getVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getXVelocity() : + vt.getYVelocity(); + } + + private ObjectAnimator createTranslationAnimation(View v, float newPos) { + ObjectAnimator anim = ObjectAnimator.ofFloat(v, + mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos); + return anim; + } + + private float getPerpendicularVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getYVelocity() : + vt.getXVelocity(); + } + + private void setTranslation(View v, float translate) { + if (mSwipeDirection == X) { + v.setTranslationX(translate); + } else { + v.setTranslationY(translate); + } + } + + private float getSize(View v) { + final DisplayMetrics dm = v.getContext().getResources().getDisplayMetrics(); + return mSwipeDirection == X ? dm.widthPixels : dm.heightPixels; + } + + public void setMinAlpha(float minAlpha) { + mMinAlpha = minAlpha; + } + + float getAlphaForOffset(View view) { + float viewSize = getSize(view); + final float fadeSize = ALPHA_FADE_END * viewSize; + float result = 1.0f; + float pos = getTranslation(view); + if (pos >= viewSize * ALPHA_FADE_START) { + result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize; + } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) { + result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize; + } + result = Math.min(result, 1.0f); + result = Math.max(result, 0f); + return Math.max(mMinAlpha, result); + } + + /** + * Determines whether the given view has RTL layout. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isLayoutRtl(View view) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection(); + } else { + return false; + } + } + + public boolean onInterceptTouchEvent(MotionEvent ev) { + Console.log(Constants.DebugFlags.UI.TouchEvents, + "[SwipeHelper|interceptTouchEvent]", + Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); + final int action = ev.getAction(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mDragging = false; + mCurrView = mCallback.getChildAtPosition(ev); + mVelocityTracker.clear(); + if (mCurrView != null) { + mRtl = isLayoutRtl(mCurrView); + mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); + mVelocityTracker.addMovement(ev); + mInitialTouchPos = getPos(ev); + } else { + mCanCurrViewBeDimissed = false; + } + break; + case MotionEvent.ACTION_MOVE: + if (mCurrView != null) { + mVelocityTracker.addMovement(ev); + float pos = getPos(ev); + float delta = pos - mInitialTouchPos; + if (Math.abs(delta) > mPagingTouchSlop) { + mCallback.onBeginDrag(mCurrView); + mDragging = true; + mInitialTouchPos = pos - getTranslation(mCurrView); + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mDragging = false; + mCurrView = null; + break; + } + return mDragging; + } + + /** + * @param view The view to be dismissed + * @param velocity The desired pixels/second speed at which the view should move + */ + private void dismissChild(final View view, float velocity) { + final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); + float newPos; + if (velocity < 0 + || (velocity == 0 && getTranslation(view) < 0) + // if we use the Menu to dismiss an item in landscape, animate up + || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) { + newPos = -getSize(view); + } else { + newPos = getSize(view); + } + int duration = MAX_ESCAPE_ANIMATION_DURATION; + if (velocity != 0) { + duration = Math.min(duration, + (int) (Math.abs(newPos - getTranslation(view)) * + 1000f / Math.abs(velocity))); + } else { + duration = DEFAULT_ESCAPE_ANIMATION_DURATION; + } + + ValueAnimator anim = createTranslationAnimation(view, newPos); + anim.setInterpolator(sLinearInterpolator); + anim.setDuration(duration); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCallback.onChildDismissed(view); + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(1.f); + } + } + }); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(getAlphaForOffset(view)); + } + } + }); + anim.start(); + } + + private void snapChild(final View view, float velocity) { + final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); + ValueAnimator anim = createTranslationAnimation(view, 0); + int duration = SNAP_ANIM_LEN; + anim.setDuration(duration); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(getAlphaForOffset(view)); + } + } + }); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(1.0f); + } + mCallback.onSnapBackCompleted(view); + } + }); + anim.start(); + } + + public boolean onTouchEvent(MotionEvent ev) { + Console.log(Constants.DebugFlags.UI.TouchEvents, + "[SwipeHelper|touchEvent]", + Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); + + if (!mDragging) { + if (!onInterceptTouchEvent(ev)) { + return mCanCurrViewBeDimissed; + } + } + + mVelocityTracker.addMovement(ev); + final int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_OUTSIDE: + case MotionEvent.ACTION_MOVE: + if (mCurrView != null) { + float delta = getPos(ev) - mInitialTouchPos; + setSwipeAmount(delta); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mCurrView != null) { + endSwipe(mVelocityTracker); + } + break; + } + return true; + } + + private void setSwipeAmount(float amount) { + // don't let items that can't be dismissed be dragged more than + // maxScrollDistance + if (CONSTRAIN_SWIPE + && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) { + float size = getSize(mCurrView); + float maxScrollDistance = 0.15f * size; + if (Math.abs(amount) >= size) { + amount = amount > 0 ? maxScrollDistance : -maxScrollDistance; + } else { + amount = maxScrollDistance * (float) Math.sin((amount/size)*(Math.PI/2)); + } + } + setTranslation(mCurrView, amount); + if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { + float alpha = getAlphaForOffset(mCurrView); + mCurrView.setAlpha(alpha); + } + } + + private boolean isValidSwipeDirection(float amount) { + if (mSwipeDirection == X) { + if (mRtl) { + return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart; + } else { + return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd; + } + } + + // Vertical swipes are always valid. + return true; + } + + private void endSwipe(VelocityTracker velocityTracker) { + float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; + velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); + float velocity = getVelocity(velocityTracker); + float perpendicularVelocity = getPerpendicularVelocity(velocityTracker); + float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; + float translation = getTranslation(mCurrView); + // Decide whether to dismiss the current view + boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && + Math.abs(translation) > 0.6 * getSize(mCurrView); + boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && + (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && + (velocity > 0) == (translation > 0); + + boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) + && isValidSwipeDirection(translation) + && (childSwipedFastEnough || childSwipedFarEnough); + + if (dismissChild) { + // flingadingy + dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); + } else { + // snappity + mCallback.onDragCancelled(mCurrView); + snapChild(mCurrView, velocity); + } + } + + public interface Callback { + View getChildAtPosition(MotionEvent ev); + + boolean canChildBeDismissed(View v); + + void onBeginDrag(View v); + + void onChildDismissed(View v); + + void onSnapBackCompleted(View v); + + void onDragCancelled(View v); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskBarView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskBarView.java new file mode 100644 index 0000000..124f11e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskBarView.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.systemui.R; +import com.android.systemui.recents.model.Task; + + +/* The task bar view */ +class TaskBarView extends FrameLayout { + Task mTask; + + ImageView mApplicationIcon; + TextView mActivityDescription; + + public TaskBarView(Context context) { + this(context, null); + } + + public TaskBarView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskBarView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TaskBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + // Initialize the icon and description views + mApplicationIcon = (ImageView) findViewById(R.id.application_icon); + mActivityDescription = (TextView) findViewById(R.id.activity_description); + } + + /** Binds the bar view to the task */ + void rebindToTask(Task t, boolean animate) { + mTask = t; + // If an activity icon is defined, then we use that as the primary icon to show in the bar, + // otherwise, we fall back to the application icon + if (t.activityIcon != null) { + mApplicationIcon.setImageDrawable(t.activityIcon); + } else if (t.applicationIcon != null) { + mApplicationIcon.setImageDrawable(t.applicationIcon); + } + mActivityDescription.setText(t.activityLabel); + if (animate) { + // XXX: Investigate how expensive it will be to create a second bitmap and crossfade + } + } + + /** Unbinds the bar view from the task */ + void unbindFromTask() { + mTask = null; + mApplicationIcon.setImageDrawable(null); + mActivityDescription.setText(""); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskInfoView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskInfoView.java new file mode 100644 index 0000000..a81d01c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskInfoView.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.widget.Button; +import android.widget.FrameLayout; +import com.android.systemui.R; +import com.android.systemui.recents.BakedBezierInterpolator; +import com.android.systemui.recents.Utilities; + + +/* The task info view */ +class TaskInfoView extends FrameLayout { + + Button mAppInfoButton; + + // Circular clip animation + boolean mCircularClipEnabled; + Path mClipPath = new Path(); + float mClipRadius; + float mMaxClipRadius; + Point mClipOrigin = new Point(); + ObjectAnimator mCircularClipAnimator; + + public TaskInfoView(Context context) { + this(context, null); + } + + public TaskInfoView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskInfoView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TaskInfoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + // Initialize the buttons on the info panel + mAppInfoButton = (Button) findViewById(R.id.task_view_app_info_button); + } + + /** Updates the positions of each of the items to fit in the rect specified */ + void updateContents(Rect visibleRect) { + // Offset the app info button + mAppInfoButton.setTranslationY(visibleRect.top + + (visibleRect.height() - mAppInfoButton.getMeasuredHeight()) / 2); + } + + /** Sets the circular clip radius on this panel */ + public void setCircularClipRadius(float r) { + mClipRadius = r; + invalidate(); + } + + /** Gets the circular clip radius on this panel */ + public float getCircularClipRadius() { + return mClipRadius; + } + + /** Animates the circular clip radius on the icon */ + void animateCircularClip(Point o, float fromRadius, float toRadius, + final Runnable postRunnable, boolean animateInContent) { + if (mCircularClipAnimator != null) { + mCircularClipAnimator.cancel(); + } + + // Calculate the max clip radius to each of the corners + int w = getMeasuredWidth() - o.x; + int h = getMeasuredHeight() - o.y; + // origin to tl, tr, br, bl + mMaxClipRadius = (int) Math.ceil(Math.sqrt(o.x * o.x + o.y * o.y)); + mMaxClipRadius = (int) Math.max(mMaxClipRadius, Math.ceil(Math.sqrt(w * w + o.y * o.y))); + mMaxClipRadius = (int) Math.max(mMaxClipRadius, Math.ceil(Math.sqrt(w * w + h * h))); + mMaxClipRadius = (int) Math.max(mMaxClipRadius, Math.ceil(Math.sqrt(o.x * o.x + h * h))); + + mClipOrigin.set(o.x, o.y); + mClipRadius = fromRadius; + int duration = Utilities.calculateTranslationAnimationDuration((int) mMaxClipRadius); + mCircularClipAnimator = ObjectAnimator.ofFloat(this, "circularClipRadius", toRadius); + mCircularClipAnimator.setDuration(duration); + mCircularClipAnimator.setInterpolator(BakedBezierInterpolator.INSTANCE); + mCircularClipAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCircularClipEnabled = false; + if (postRunnable != null) { + postRunnable.run(); + } + } + }); + mCircularClipAnimator.start(); + mCircularClipEnabled = true; + + if (animateInContent) { + animateAppInfoButtonIn(duration); + } + } + + /** Cancels the circular clip animation. */ + void cancelCircularClipAnimation() { + if (mCircularClipAnimator != null) { + mCircularClipAnimator.cancel(); + } + } + + void animateAppInfoButtonIn(int duration) { + mAppInfoButton.setScaleX(0.75f); + mAppInfoButton.setScaleY(0.75f); + mAppInfoButton.animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(duration) + .setInterpolator(BakedBezierInterpolator.INSTANCE) + .withLayer() + .start(); + } + + @Override + public void draw(Canvas canvas) { + int saveCount = 0; + if (mCircularClipEnabled) { + saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); + mClipPath.reset(); + mClipPath.addCircle(mClipOrigin.x, mClipOrigin.y, mClipRadius * mMaxClipRadius, + Path.Direction.CW); + canvas.clipPath(mClipPath); + } + super.draw(canvas); + if (mCircularClipEnabled) { + canvas.restoreToCount(saveCount); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java new file mode 100644 index 0000000..a77e61d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -0,0 +1,1452 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Region; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.widget.FrameLayout; +import android.widget.OverScroller; +import com.android.systemui.R; +import com.android.systemui.recents.BakedBezierInterpolator; +import com.android.systemui.recents.Console; +import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.RecentsTaskLoader; +import com.android.systemui.recents.Utilities; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; + +import java.util.ArrayList; +import java.util.HashMap; + + +/* The visual representation of a task stack view */ +public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCallbacks, + TaskView.TaskViewCallbacks, ViewPool.ViewPoolConsumer<TaskView, Task>, + View.OnClickListener, View.OnLongClickListener { + + /** The TaskView callbacks */ + interface TaskStackViewCallbacks { + public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t); + public void onTaskAppInfoLaunched(Task t); + } + + TaskStack mStack; + TaskStackViewTouchHandler mTouchHandler; + TaskStackViewCallbacks mCb; + ViewPool<TaskView, Task> mViewPool; + + // The various rects that define the stack view + Rect mRect = new Rect(); + Rect mStackRect = new Rect(); + Rect mStackRectSansPeek = new Rect(); + Rect mTaskRect = new Rect(); + + // The virtual stack scroll that we use for the card layout + int mStackScroll; + int mMinScroll; + int mMaxScroll; + int mStashedScroll; + int mLastInfoPaneStackScroll; + OverScroller mScroller; + ObjectAnimator mScrollAnimator; + + // Optimizations + int mHwLayersRefCount; + int mStackViewsAnimationDuration; + boolean mStackViewsDirty = true; + boolean mAwaitingFirstLayout = true; + int[] mTmpVisibleRange = new int[2]; + Rect mTmpRect = new Rect(); + Rect mTmpRect2 = new Rect(); + LayoutInflater mInflater; + + public TaskStackView(Context context, TaskStack stack) { + super(context); + mStack = stack; + mStack.setCallbacks(this); + mScroller = new OverScroller(context); + mTouchHandler = new TaskStackViewTouchHandler(context, this); + mViewPool = new ViewPool<TaskView, Task>(context, this); + mInflater = LayoutInflater.from(context); + } + + /** Sets the callbacks */ + void setCallbacks(TaskStackViewCallbacks cb) { + mCb = cb; + } + + /** Requests that the views be synchronized with the model */ + void requestSynchronizeStackViewsWithModel() { + requestSynchronizeStackViewsWithModel(0); + } + void requestSynchronizeStackViewsWithModel(int duration) { + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + "[TaskStackView|requestSynchronize]", "" + duration + "ms", Console.AnsiYellow); + if (!mStackViewsDirty) { + invalidate(); + } + if (mAwaitingFirstLayout) { + // Skip the animation if we are awaiting first layout + mStackViewsAnimationDuration = 0; + } else { + mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration); + } + mStackViewsDirty = true; + } + + // XXX: Optimization: Use a mapping of Task -> View + private TaskView getChildViewForTask(Task t) { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.getTask() == t) { + return tv; + } + } + return null; + } + + /** Update/get the transform */ + public TaskViewTransform getStackTransform(int indexInStack, int stackScroll) { + TaskViewTransform transform = new TaskViewTransform(); + + // Return early if we have an invalid index + if (indexInStack < 0) return transform; + + // Map the items to an continuous position relative to the specified scroll + int numPeekCards = Constants.Values.TaskStackView.StackPeekNumCards; + float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height(); + float peekHeight = Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); + float t = ((indexInStack * overlapHeight) - stackScroll) / overlapHeight; + float boundedT = Math.max(t, -(numPeekCards + 1)); + + // Set the scale relative to its position + float minScale = Constants.Values.TaskStackView.StackPeekMinScale; + float scaleRange = 1f - minScale; + float scaleInc = scaleRange / numPeekCards; + float scale = Math.max(minScale, Math.min(1f, 1f + (boundedT * scaleInc))); + float scaleYOffset = ((1f - scale) * mTaskRect.height()) / 2; + transform.scale = scale; + + // Set the translation + if (boundedT < 0f) { + transform.translationY = (int) ((Math.max(-numPeekCards, boundedT) / + numPeekCards) * peekHeight - scaleYOffset); + } else { + transform.translationY = (int) (boundedT * overlapHeight - scaleYOffset); + } + + // Update the rect and visibility + transform.rect.set(mTaskRect); + if (t < -(numPeekCards + 1)) { + transform.visible = false; + } else { + transform.rect.offset(0, transform.translationY); + Utilities.scaleRectAboutCenter(transform.rect, transform.scale); + transform.visible = Rect.intersects(mRect, transform.rect); + } + transform.t = t; + return transform; + } + + /** + * Gets the stack transforms of a list of tasks, and returns the visible range of tasks. + */ + private ArrayList<TaskViewTransform> getStackTransforms(ArrayList<Task> tasks, + int stackScroll, + int[] visibleRangeOut, + boolean boundTranslationsToRect) { + // XXX: Optimization: Use binary search to find the visible range + + ArrayList<TaskViewTransform> taskTransforms = new ArrayList<TaskViewTransform>(); + int taskCount = tasks.size(); + int firstVisibleIndex = -1; + int lastVisibleIndex = -1; + for (int i = 0; i < taskCount; i++) { + TaskViewTransform transform = getStackTransform(i, stackScroll); + taskTransforms.add(transform); + if (transform.visible) { + if (firstVisibleIndex < 0) { + firstVisibleIndex = i; + } + lastVisibleIndex = i; + } + + if (boundTranslationsToRect) { + transform.translationY = Math.min(transform.translationY, mRect.bottom); + } + } + if (visibleRangeOut != null) { + visibleRangeOut[0] = firstVisibleIndex; + visibleRangeOut[1] = lastVisibleIndex; + } + return taskTransforms; + } + + /** Synchronizes the views with the model */ + void synchronizeStackViewsWithModel() { + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + "[TaskStackView|synchronizeViewsWithModel]", + "mStackViewsDirty: " + mStackViewsDirty, Console.AnsiYellow); + if (mStackViewsDirty) { + // XXX: Consider using TaskViewTransform pool to prevent allocations + // XXX: Iterate children views, update transforms and remove all that are not visible + // For all remaining tasks, update transforms and if visible add the view + + // Get all the task transforms + int[] visibleRange = mTmpVisibleRange; + int stackScroll = getStackScroll(); + ArrayList<Task> tasks = mStack.getTasks(); + ArrayList<TaskViewTransform> taskTransforms = getStackTransforms(tasks, stackScroll, + visibleRange, false); + + // Update the visible state of all the tasks + int taskCount = tasks.size(); + for (int i = 0; i < taskCount; i++) { + Task task = tasks.get(i); + TaskViewTransform transform = taskTransforms.get(i); + TaskView tv = getChildViewForTask(task); + + if (transform.visible) { + if (tv == null) { + tv = mViewPool.pickUpViewFromPool(task, task); + // When we are picking up a new view from the view pool, prepare it for any + // following animation by putting it in a reasonable place + if (mStackViewsAnimationDuration > 0 && i != 0) { + int fromIndex = (transform.t < 0) ? (visibleRange[0] - 1) : + (visibleRange[1] + 1); + tv.updateViewPropertiesToTaskTransform(null, + getStackTransform(fromIndex, stackScroll), 0); + } + } + } else { + if (tv != null) { + mViewPool.returnViewToPool(tv); + } + } + } + + // Update all the remaining view children + // NOTE: We have to iterate in reverse where because we are removing views directly + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) getChildAt(i); + Task task = tv.getTask(); + int taskIndex = mStack.indexOfTask(task); + if (taskIndex < 0 || !taskTransforms.get(taskIndex).visible) { + mViewPool.returnViewToPool(tv); + } else { + tv.updateViewPropertiesToTaskTransform(null, taskTransforms.get(taskIndex), + mStackViewsAnimationDuration); + } + } + + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + " [TaskStackView|viewChildren]", "" + getChildCount()); + + mStackViewsAnimationDuration = 0; + mStackViewsDirty = false; + } + } + + /** Sets the current stack scroll */ + public void setStackScroll(int value) { + mStackScroll = value; + requestSynchronizeStackViewsWithModel(); + + // Close any open info panes if the user has scrolled away from them + boolean isAnimatingScroll = (mScrollAnimator != null && mScrollAnimator.isRunning()); + if (mLastInfoPaneStackScroll > -1 && !isAnimatingScroll) { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + if (Math.abs(mStackScroll - mLastInfoPaneStackScroll) > + config.taskStackScrollDismissInfoPaneDistance) { + // Close any open info panes + closeOpenInfoPanes(); + } + } + } + /** Sets the current stack scroll without synchronizing the stack view with the model */ + public void setStackScrollRaw(int value) { + mStackScroll = value; + } + + /** Gets the current stack scroll */ + public int getStackScroll() { + return mStackScroll; + } + + /** Animates the stack scroll into bounds */ + ObjectAnimator animateBoundScroll() { + int curScroll = getStackScroll(); + int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); + if (newScroll != curScroll) { + // Enable hw layers on the stack + addHwLayersRefCount("animateBoundScroll"); + + // Start a new scroll animation + animateScroll(curScroll, newScroll, new Runnable() { + @Override + public void run() { + // Disable hw layers on the stack + decHwLayersRefCount("animateBoundScroll"); + } + }); + } + return mScrollAnimator; + } + + /** Animates the stack scroll */ + void animateScroll(int curScroll, int newScroll, final Runnable postRunnable) { + // Abort any current animations + abortScroller(); + abortBoundScrollAnimation(); + + mScrollAnimator = ObjectAnimator.ofInt(this, "stackScroll", curScroll, newScroll); + mScrollAnimator.setDuration(Utilities.calculateTranslationAnimationDuration(newScroll - + curScroll, 250)); + mScrollAnimator.setInterpolator(BakedBezierInterpolator.INSTANCE); + mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setStackScroll((Integer) animation.getAnimatedValue()); + } + }); + mScrollAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (postRunnable != null) { + postRunnable.run(); + } + mScrollAnimator.removeAllListeners(); + } + }); + mScrollAnimator.start(); + } + + /** Aborts any current stack scrolls */ + void abortBoundScrollAnimation() { + if (mScrollAnimator != null) { + mScrollAnimator.cancel(); + } + } + + /** Aborts the scroller and any current fling */ + void abortScroller() { + if (!mScroller.isFinished()) { + // Abort the scroller + mScroller.abortAnimation(); + // And disable hw layers on the stack + decHwLayersRefCount("flingScroll"); + } + } + + /** Bounds the current scroll if necessary */ + public boolean boundScroll() { + int curScroll = getStackScroll(); + int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); + if (newScroll != curScroll) { + setStackScroll(newScroll); + return true; + } + return false; + } + + /** + * Bounds the current scroll if necessary, but does not synchronize the stack view with the + * model. + */ + public boolean boundScrollRaw() { + int curScroll = getStackScroll(); + int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); + if (newScroll != curScroll) { + setStackScrollRaw(newScroll); + return true; + } + return false; + } + + /** Returns whether the specified scroll is out of bounds */ + boolean isScrollOutOfBounds(int scroll) { + return (scroll < mMinScroll) || (scroll > mMaxScroll); + } + boolean isScrollOutOfBounds() { + return isScrollOutOfBounds(getStackScroll()); + } + + /** Updates the min and max virtual scroll bounds */ + void updateMinMaxScroll(boolean boundScrollToNewMinMax) { + // Compute the min and max scroll values + int numTasks = Math.max(1, mStack.getTaskCount()); + int taskHeight = mTaskRect.height(); + int stackHeight = mStackRectSansPeek.height(); + int maxScrollHeight = taskHeight + (int) ((numTasks - 1) * + Constants.Values.TaskStackView.StackOverlapPct * taskHeight); + + if (numTasks <= 1) { + // If there is only one task, then center the task in the stack rect (sans peek) + mMinScroll = mMaxScroll = -(stackHeight - taskHeight) / 2; + } else { + mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight; + mMaxScroll = maxScrollHeight - stackHeight; + } + + // Debug logging + if (Constants.DebugFlags.UI.MeasureAndLayout) { + Console.log(" [TaskStack|minScroll] " + mMinScroll); + Console.log(" [TaskStack|maxScroll] " + mMaxScroll); + } + + if (boundScrollToNewMinMax) { + boundScroll(); + } + } + + /** Closes any open info panes. */ + boolean closeOpenInfoPanes() { + if (!Constants.DebugFlags.App.EnableInfoPane) return false; + + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.isInfoPaneVisible()) { + tv.hideInfoPane(); + return true; + } + } + return false; + } + + /** Enables the hw layers and increments the hw layer requirement ref count */ + void addHwLayersRefCount(String reason) { + Console.log(Constants.DebugFlags.UI.HwLayers, + "[TaskStackView|addHwLayersRefCount] refCount: " + + mHwLayersRefCount + "->" + (mHwLayersRefCount + 1) + " " + reason); + if (mHwLayersRefCount == 0) { + // Enable hw layers on each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + tv.enableHwLayers(); + } + } + mHwLayersRefCount++; + } + + /** Decrements the hw layer requirement ref count and disables the hw layers when we don't + need them anymore. */ + void decHwLayersRefCount(String reason) { + Console.log(Constants.DebugFlags.UI.HwLayers, + "[TaskStackView|decHwLayersRefCount] refCount: " + + mHwLayersRefCount + "->" + (mHwLayersRefCount - 1) + " " + reason); + mHwLayersRefCount--; + if (mHwLayersRefCount == 0) { + // Disable hw layers on each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + tv.disableHwLayers(); + } + } else if (mHwLayersRefCount < 0) { + new Throwable("Invalid hw layers ref count").printStackTrace(); + Console.logError(getContext(), "Invalid HW layers ref count"); + } + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + setStackScroll(mScroller.getCurrY()); + invalidate(); + + // If we just finished scrolling, then disable the hw layers + if (mScroller.isFinished()) { + decHwLayersRefCount("finishedFlingScroll"); + } + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return mTouchHandler.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mTouchHandler.onTouchEvent(ev); + } + + @Override + public void dispatchDraw(Canvas canvas) { + Console.log(Constants.DebugFlags.UI.Draw, "[TaskStackView|dispatchDraw]", "", + Console.AnsiPurple); + synchronizeStackViewsWithModel(); + super.dispatchDraw(canvas); + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (Constants.DebugFlags.App.EnableTaskStackClipping) { + TaskView tv = (TaskView) child; + TaskView nextTv = null; + int curIndex = indexOfChild(tv); + if ((curIndex > -1) && (curIndex < (getChildCount() - 1))) { + // Clip against the next view (if we aren't animating its alpha) + nextTv = (TaskView) getChildAt(curIndex + 1); + if (nextTv.getAlpha() == 1f) { + Rect curRect = tv.getClippingRect(mTmpRect); + Rect nextRect = nextTv.getClippingRect(mTmpRect2); + RecentsConfiguration config = RecentsConfiguration.getInstance(); + // The hit rects are relative to the task view, which needs to be offset by the + // system bar height + curRect.offset(0, config.systemInsets.top); + nextRect.offset(0, config.systemInsets.top); + // Compute the clip region + Region clipRegion = new Region(); + clipRegion.op(curRect, Region.Op.UNION); + clipRegion.op(nextRect, Region.Op.DIFFERENCE); + // Clip the canvas + int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipRegion(clipRegion); + boolean invalidate = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(saveCount); + return invalidate; + } + } + } + return super.drawChild(canvas, child, drawingTime); + } + + /** Computes the stack and task rects */ + public void computeRects(int width, int height, int insetBottom) { + // Note: We let the stack view be the full height because we want the cards to go under the + // navigation bar if possible. However, the stack rects which we use to calculate + // max scroll, etc. need to take the nav bar into account + + // Compute the stack rects + mRect.set(0, 0, width, height); + mStackRect.set(mRect); + mStackRect.bottom -= insetBottom; + + int smallestDimension = Math.min(width, height); + int padding = (int) (Constants.Values.TaskStackView.StackPaddingPct * smallestDimension / 2f); + if (Constants.DebugFlags.App.EnableSearchButton) { + // Don't need to pad the top since we have some padding on the search bar already + mStackRect.left += padding; + mStackRect.right -= padding; + mStackRect.bottom -= padding; + } else { + mStackRect.inset(padding, padding); + } + mStackRectSansPeek.set(mStackRect); + mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); + + // Compute the task rect + int minHeight = (int) (mStackRect.height() - + (Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height())); + int size = Math.min(minHeight, Math.min(mStackRect.width(), mStackRect.height())); + int left = mStackRect.left + (mStackRect.width() - size) / 2; + mTaskRect.set(left, mStackRectSansPeek.top, + left + size, mStackRectSansPeek.top + size); + + // Update the scroll bounds + updateMinMaxScroll(false); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|measure]", + "width: " + width + " height: " + height + + " awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen); + + // Compute our stack/task rects + RecentsConfiguration config = RecentsConfiguration.getInstance(); + computeRects(width, height, config.systemInsets.bottom); + + // Debug logging + if (Constants.DebugFlags.UI.MeasureAndLayout) { + Console.log(" [TaskStack|fullRect] " + mRect); + Console.log(" [TaskStack|stackRect] " + mStackRect); + Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); + Console.log(" [TaskStack|taskRect] " + mTaskRect); + } + + // If this is the first layout, then scroll to the front of the stack and synchronize the + // stack views immediately + if (mAwaitingFirstLayout) { + setStackScroll(mMaxScroll); + requestSynchronizeStackViewsWithModel(); + synchronizeStackViewsWithModel(); + + // Animate the task bar of the first task view + if (config.launchedWithThumbnailAnimation && + Constants.Values.TaskView.AnimateFrontTaskBarOnEnterRecents) { + TaskView tv = (TaskView) getChildAt(getChildCount() - 1); + if (tv != null) { + tv.animateOnEnterRecents(); + } + } + } + + // Measure each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView t = (TaskView) getChildAt(i); + t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY)); + } + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|layout]", + "" + new Rect(left, top, right, bottom), Console.AnsiGreen); + + // Debug logging + if (Constants.DebugFlags.UI.MeasureAndLayout) { + Console.log(" [TaskStack|fullRect] " + mRect); + Console.log(" [TaskStack|stackRect] " + mStackRect); + Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); + Console.log(" [TaskStack|taskRect] " + mTaskRect); + } + + // Layout each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView t = (TaskView) getChildAt(i); + t.layout(mTaskRect.left, mStackRectSansPeek.top, + mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height()); + } + + if (mAwaitingFirstLayout) { + mAwaitingFirstLayout = false; + } + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + requestSynchronizeStackViewsWithModel(); + } + + public boolean isTransformedTouchPointInView(float x, float y, View child) { + return isTransformedTouchPointInView(x, y, child, null); + } + + /**** TaskStackCallbacks Implementation ****/ + + @Override + public void onStackTaskAdded(TaskStack stack, Task t) { + requestSynchronizeStackViewsWithModel(); + } + + @Override + public void onStackTaskRemoved(TaskStack stack, Task t) { + // Remove the view associated with this task, we can't rely on updateTransforms + // to work here because the task is no longer in the list + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.getTask() == t) { + mViewPool.returnViewToPool(tv); + break; + } + } + + updateMinMaxScroll(true); + int movement = (int) (Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height()); + requestSynchronizeStackViewsWithModel(Utilities.calculateTranslationAnimationDuration(movement)); + } + + /** + * Creates the animations for all the children views that need to be removed or to move views + * to their un/filtered position when we are un/filtering a stack, and returns the duration + * for these animations. + */ + int getExitTransformsForFilterAnimation(ArrayList<Task> curTasks, + ArrayList<TaskViewTransform> curTaskTransforms, + ArrayList<Task> tasks, ArrayList<TaskViewTransform> taskTransforms, + HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransformsOut, + ArrayList<TaskView> childrenToRemoveOut, + RecentsConfiguration config) { + // Animate all of the existing views out of view (if they are not in the visible range in + // the new stack) or to their final positions in the new stack + int movement = 0; + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + Task task = tv.getTask(); + int taskIndex = tasks.indexOf(task); + TaskViewTransform toTransform; + + // If the view is no longer visible, then we should just animate it out + boolean willBeInvisible = taskIndex < 0 || !taskTransforms.get(taskIndex).visible; + if (willBeInvisible) { + if (taskIndex < 0) { + toTransform = curTaskTransforms.get(curTasks.indexOf(task)); + } else { + toTransform = new TaskViewTransform(taskTransforms.get(taskIndex)); + } + tv.prepareTaskTransformForFilterTaskVisible(toTransform); + childrenToRemoveOut.add(tv); + } else { + toTransform = taskTransforms.get(taskIndex); + // Use the movement of the visible views to calculate the duration of the animation + movement = Math.max(movement, Math.abs(toTransform.translationY - + (int) tv.getTranslationY())); + } + childViewTransformsOut.put(tv, new Pair(0, toTransform)); + } + return Utilities.calculateTranslationAnimationDuration(movement, + config.filteringCurrentViewsMinAnimDuration); + } + + /** + * Creates the animations for all the children views that need to be animated in when we are + * un/filtering a stack, and returns the duration for these animations. + */ + int getEnterTransformsForFilterAnimation(ArrayList<Task> tasks, + ArrayList<TaskViewTransform> taskTransforms, + HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransformsOut, + RecentsConfiguration config) { + int offset = 0; + int movement = 0; + int taskCount = tasks.size(); + for (int i = taskCount - 1; i >= 0; i--) { + Task task = tasks.get(i); + TaskViewTransform toTransform = taskTransforms.get(i); + if (toTransform.visible) { + TaskView tv = getChildViewForTask(task); + if (tv == null) { + // For views that are not already visible, animate them in + tv = mViewPool.pickUpViewFromPool(task, task); + + // Compose a new transform to fade and slide the new task in + TaskViewTransform fromTransform = new TaskViewTransform(toTransform); + tv.prepareTaskTransformForFilterTaskHidden(fromTransform); + tv.updateViewPropertiesToTaskTransform(null, fromTransform, 0); + + int startDelay = offset * + Constants.Values.TaskStackView.FilterStartDelay; + childViewTransformsOut.put(tv, new Pair(startDelay, toTransform)); + + // Use the movement of the new views to calculate the duration of the animation + movement = Math.max(movement, + Math.abs(toTransform.translationY - fromTransform.translationY)); + offset++; + } + } + } + return Utilities.calculateTranslationAnimationDuration(movement, + config.filteringNewViewsMinAnimDuration); + } + + /** Orchestrates the animations of the current child views and any new views. */ + void doFilteringAnimation(ArrayList<Task> curTasks, + ArrayList<TaskViewTransform> curTaskTransforms, + final ArrayList<Task> tasks, + final ArrayList<TaskViewTransform> taskTransforms) { + final RecentsConfiguration config = RecentsConfiguration.getInstance(); + + // Calculate the transforms to animate out all the existing views if they are not in the + // new visible range (or to their final positions in the stack if they are) + final ArrayList<TaskView> childrenToRemove = new ArrayList<TaskView>(); + final HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransforms = + new HashMap<TaskView, Pair<Integer, TaskViewTransform>>(); + int duration = getExitTransformsForFilterAnimation(curTasks, curTaskTransforms, tasks, + taskTransforms, childViewTransforms, childrenToRemove, config); + + // If all the current views are in the visible range of the new stack, then don't wait for + // views to animate out and animate all the new views into their place + final boolean unifyNewViewAnimation = childrenToRemove.isEmpty(); + if (unifyNewViewAnimation) { + int inDuration = getEnterTransformsForFilterAnimation(tasks, taskTransforms, + childViewTransforms, config); + duration = Math.max(duration, inDuration); + } + + // Animate all the views to their final transforms + for (final TaskView tv : childViewTransforms.keySet()) { + Pair<Integer, TaskViewTransform> t = childViewTransforms.get(tv); + tv.animate().cancel(); + tv.animate() + .setStartDelay(t.first) + .withEndAction(new Runnable() { + @Override + public void run() { + childViewTransforms.remove(tv); + if (childViewTransforms.isEmpty()) { + // Return all the removed children to the view pool + for (TaskView tv : childrenToRemove) { + mViewPool.returnViewToPool(tv); + } + + if (!unifyNewViewAnimation) { + // For views that are not already visible, animate them in + childViewTransforms.clear(); + int duration = getEnterTransformsForFilterAnimation(tasks, + taskTransforms, childViewTransforms, config); + for (final TaskView tv : childViewTransforms.keySet()) { + Pair<Integer, TaskViewTransform> t = childViewTransforms.get(tv); + tv.animate().setStartDelay(t.first); + tv.updateViewPropertiesToTaskTransform(null, t.second, duration); + } + } + } + } + }); + tv.updateViewPropertiesToTaskTransform(null, t.second, duration); + } + } + + @Override + public void onStackFiltered(TaskStack newStack, final ArrayList<Task> curTasks, + Task filteredTask) { + // Close any open info panes + closeOpenInfoPanes(); + + // Stash the scroll and filtered task for us to restore to when we unfilter + mStashedScroll = getStackScroll(); + + // Calculate the current task transforms + ArrayList<TaskViewTransform> curTaskTransforms = + getStackTransforms(curTasks, getStackScroll(), null, true); + + // Scroll the item to the top of the stack (sans-peek) rect so that we can see it better + updateMinMaxScroll(false); + float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height(); + setStackScrollRaw((int) (newStack.indexOfTask(filteredTask) * overlapHeight)); + boundScrollRaw(); + + // Compute the transforms of the items in the new stack after setting the new scroll + final ArrayList<Task> tasks = mStack.getTasks(); + final ArrayList<TaskViewTransform> taskTransforms = + getStackTransforms(mStack.getTasks(), getStackScroll(), null, true); + + // Animate + doFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms); + } + + @Override + public void onStackUnfiltered(TaskStack newStack, final ArrayList<Task> curTasks) { + // Close any open info panes + closeOpenInfoPanes(); + + // Calculate the current task transforms + final ArrayList<TaskViewTransform> curTaskTransforms = + getStackTransforms(curTasks, getStackScroll(), null, true); + + // Restore the stashed scroll + updateMinMaxScroll(false); + setStackScrollRaw(mStashedScroll); + boundScrollRaw(); + + // Compute the transforms of the items in the new stack after restoring the stashed scroll + final ArrayList<Task> tasks = mStack.getTasks(); + final ArrayList<TaskViewTransform> taskTransforms = + getStackTransforms(tasks, getStackScroll(), null, true); + + // Animate + doFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms); + + // Clear the saved vars + mStashedScroll = 0; + } + + /**** ViewPoolConsumer Implementation ****/ + + @Override + public TaskView createView(Context context) { + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]"); + return (TaskView) mInflater.inflate(R.layout.recents_task_view, this, false); + } + + @Override + public void prepareViewToEnterPool(TaskView tv) { + Task task = tv.getTask(); + tv.resetViewProperties(); + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]", + tv.getTask() + " tv: " + tv); + + // Report that this tasks's data is no longer being used + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.unloadTaskData(task); + + // Detach the view from the hierarchy + detachViewFromParent(tv); + + // Disable hw layers on this view + tv.disableHwLayers(); + } + + @Override + public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) { + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]", + "isNewView: " + isNewView); + + // Setup and attach the view to the window + Task task = prepareData; + // We try and rebind the task (this MUST be done before the task filled) + tv.onTaskBound(task); + // Request that this tasks's data be filled + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.loadTaskData(task); + + // Find the index where this task should be placed in the children + int insertIndex = -1; + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + Task tvTask = ((TaskView) getChildAt(i)).getTask(); + if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) { + insertIndex = i; + break; + } + } + + // Add/attach the view to the hierarchy + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, " [TaskStackView|insertIndex]", + "" + insertIndex); + if (isNewView) { + addView(tv, insertIndex); + + // Set the callbacks and listeners for this new view + tv.setOnClickListener(this); + if (Constants.DebugFlags.App.EnableInfoPane) { + tv.setOnLongClickListener(this); + } + tv.setCallbacks(this); + } else { + attachViewToParent(tv, insertIndex, tv.getLayoutParams()); + } + + // Enable hw layers on this view if hw layers are enabled on the stack + if (mHwLayersRefCount > 0) { + tv.enableHwLayers(); + } + } + + @Override + public boolean hasPreferredData(TaskView tv, Task preferredData) { + return (tv.getTask() == preferredData); + } + + /**** TaskViewCallbacks Implementation ****/ + + @Override + public void onTaskIconClicked(TaskView tv) { + Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Icon]", + tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(), + Console.AnsiCyan); + if (Constants.DebugFlags.App.EnableTaskFiltering) { + if (mStack.hasFilteredTasks()) { + mStack.unfilterTasks(); + } else { + mStack.filterTasks(tv.getTask()); + } + } else { + Console.logError(getContext(), "Task Filtering TBD"); + } + } + + @Override + public void onTaskInfoPanelShown(TaskView tv) { + // Do nothing + } + + @Override + public void onTaskInfoPanelHidden(TaskView tv) { + // Unset the saved scroll + mLastInfoPaneStackScroll = -1; + } + + @Override + public void onTaskAppInfoClicked(TaskView tv) { + if (mCb != null) { + mCb.onTaskAppInfoLaunched(tv.getTask()); + } + } + + /**** View.OnClickListener Implementation ****/ + + @Override + public void onClick(View v) { + TaskView tv = (TaskView) v; + Task task = tv.getTask(); + Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]", + task + " cb: " + mCb); + + // Close any open info panes if the user taps on another task + if (closeOpenInfoPanes()) { + return; + } + + if (mCb != null) { + mCb.onTaskLaunched(this, tv, mStack, task); + } + } + + @Override + public boolean onLongClick(View v) { + if (!Constants.DebugFlags.App.EnableInfoPane) return false; + + TaskView tv = (TaskView) v; + + // Close any other task info panels if we launch another info pane + closeOpenInfoPanes(); + + // Scroll the task view so that it is maximally visible + float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height(); + int taskIndex = mStack.indexOfTask(tv.getTask()); + int curScroll = getStackScroll(); + int newScroll = (int) Math.max(mMinScroll, Math.min(mMaxScroll, taskIndex * overlapHeight)); + TaskViewTransform transform = getStackTransform(taskIndex, curScroll); + Rect nonOverlapRect = new Rect(transform.rect); + if (taskIndex < (mStack.getTaskCount() - 1)) { + nonOverlapRect.bottom = nonOverlapRect.top + (int) overlapHeight; + } + + // XXX: Use HW Layers + if (transform.t < 0f) { + animateScroll(curScroll, newScroll, null); + } else if (nonOverlapRect.bottom > mStackRectSansPeek.bottom) { + // Check if we are out of bounds, if so, just scroll it in such that the bottom of the + // task view is visible + newScroll = curScroll - (mStackRectSansPeek.bottom - nonOverlapRect.bottom); + animateScroll(curScroll, newScroll, null); + } + mLastInfoPaneStackScroll = newScroll; + + // Show the info pane for this task view + tv.showInfoPane(new Rect(0, 0, 0, (int) overlapHeight)); + return true; + } +} + +/* Handles touch events */ +class TaskStackViewTouchHandler implements SwipeHelper.Callback { + static int INACTIVE_POINTER_ID = -1; + + TaskStackView mSv; + VelocityTracker mVelocityTracker; + + boolean mIsScrolling; + + int mInitialMotionX, mInitialMotionY; + int mLastMotionX, mLastMotionY; + int mActivePointerId = INACTIVE_POINTER_ID; + TaskView mActiveTaskView = null; + + int mTotalScrollMotion; + int mMinimumVelocity; + int mMaximumVelocity; + // The scroll touch slop is used to calculate when we start scrolling + int mScrollTouchSlop; + // The page touch slop is used to calculate when we start swiping + float mPagingTouchSlop; + + SwipeHelper mSwipeHelper; + boolean mInterceptedBySwipeHelper; + + public TaskStackViewTouchHandler(Context context, TaskStackView sv) { + ViewConfiguration configuration = ViewConfiguration.get(context); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mScrollTouchSlop = configuration.getScaledTouchSlop(); + mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); + mSv = sv; + + + float densityScale = context.getResources().getDisplayMetrics().density; + mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop); + mSwipeHelper.setMinAlpha(1f); + } + + /** Velocity tracker helpers */ + void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** Returns the view at the specified coordinates */ + TaskView findViewAtPoint(int x, int y) { + int childCount = mSv.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) mSv.getChildAt(i); + if (tv.getVisibility() == View.VISIBLE) { + if (mSv.isTransformedTouchPointInView(x, y, tv)) { + return tv; + } + } + } + return null; + } + + /** Touch preprocessing for handling below */ + public boolean onInterceptTouchEvent(MotionEvent ev) { + Console.log(Constants.DebugFlags.UI.TouchEvents, + "[TaskStackViewTouchHandler|interceptTouchEvent]", + Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); + + // Return early if we have no children + boolean hasChildren = (mSv.getChildCount() > 0); + if (!hasChildren) { + return false; + } + + // Pass through to swipe helper if we are swiping + mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev); + if (mInterceptedBySwipeHelper) { + return true; + } + + boolean wasScrolling = !mSv.mScroller.isFinished() || + (mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning()); + int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + // Save the touch down info + mInitialMotionX = mLastMotionX = (int) ev.getX(); + mInitialMotionY = mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); + // Stop the current scroll if it is still flinging + mSv.abortScroller(); + mSv.abortBoundScrollAnimation(); + // Initialize the velocity tracker + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Check if the scroller is finished yet + mIsScrolling = !mSv.mScroller.isFinished(); + break; + } + case MotionEvent.ACTION_MOVE: { + if (mActivePointerId == INACTIVE_POINTER_ID) break; + + int activePointerIndex = ev.findPointerIndex(mActivePointerId); + int y = (int) ev.getY(activePointerIndex); + int x = (int) ev.getX(activePointerIndex); + if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { + // Save the touch move info + mIsScrolling = true; + // Initialize the velocity tracker if necessary + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount("stackScroll"); + } + + mLastMotionX = x; + mLastMotionY = y; + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + // Animate the scroll back if we've cancelled + mSv.animateBoundScroll(); + // Disable HW layers + if (mIsScrolling) { + mSv.decHwLayersRefCount("stackScroll"); + } + // Reset the drag state and the velocity tracker + mIsScrolling = false; + mActivePointerId = INACTIVE_POINTER_ID; + mActiveTaskView = null; + mTotalScrollMotion = 0; + recycleVelocityTracker(); + break; + } + } + + return wasScrolling || mIsScrolling; + } + + /** Handles touch events once we have intercepted them */ + public boolean onTouchEvent(MotionEvent ev) { + Console.log(Constants.DebugFlags.UI.TouchEvents, + "[TaskStackViewTouchHandler|touchEvent]", + Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); + + // Short circuit if we have no children + boolean hasChildren = (mSv.getChildCount() > 0); + if (!hasChildren) { + return false; + } + + // Pass through to swipe helper if we are swiping + if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { + return true; + } + + // Update the velocity tracker + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + + int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + // Save the touch down info + mInitialMotionX = mLastMotionX = (int) ev.getX(); + mInitialMotionY = mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); + // Stop the current scroll if it is still flinging + mSv.abortScroller(); + mSv.abortBoundScrollAnimation(); + // Initialize the velocity tracker + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + break; + } + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + mActivePointerId = ev.getPointerId(index); + mLastMotionX = (int) ev.getX(index); + mLastMotionY = (int) ev.getY(index); + break; + } + case MotionEvent.ACTION_MOVE: { + if (mActivePointerId == INACTIVE_POINTER_ID) break; + + int activePointerIndex = ev.findPointerIndex(mActivePointerId); + int x = (int) ev.getX(activePointerIndex); + int y = (int) ev.getY(activePointerIndex); + int yTotal = Math.abs(y - mInitialMotionY); + int deltaY = mLastMotionY - y; + if (!mIsScrolling) { + if (yTotal > mScrollTouchSlop) { + mIsScrolling = true; + // Initialize the velocity tracker + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount("stackScroll"); + } + } + if (mIsScrolling) { + int curStackScroll = mSv.getStackScroll(); + if (mSv.isScrollOutOfBounds(curStackScroll + deltaY)) { + // Scale the touch if we are overscrolling + deltaY /= Constants.Values.TaskStackView.TouchOverscrollScaleFactor; + } + mSv.setStackScroll(curStackScroll + deltaY); + if (mSv.isScrollOutOfBounds()) { + mVelocityTracker.clear(); + } + } + mLastMotionX = x; + mLastMotionY = y; + mTotalScrollMotion += Math.abs(deltaY); + break; + } + case MotionEvent.ACTION_UP: { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int velocity = (int) velocityTracker.getYVelocity(mActivePointerId); + + if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) { + // Enable HW layers on the stack + mSv.addHwLayersRefCount("flingScroll"); + int overscrollRange = (int) (Math.min(1f, + Math.abs((float) velocity / mMaximumVelocity)) * + Constants.Values.TaskStackView.TaskStackOverscrollRange); + + Console.log(Constants.DebugFlags.UI.TouchEvents, + "[TaskStackViewTouchHandler|fling]", + "scroll: " + mSv.getStackScroll() + " velocity: " + velocity + + " maxVelocity: " + mMaximumVelocity + + " overscrollRange: " + overscrollRange, + Console.AnsiGreen); + + // Fling scroll + mSv.mScroller.fling(0, mSv.getStackScroll(), + 0, -velocity, + 0, 0, + mSv.mMinScroll, mSv.mMaxScroll, + 0, overscrollRange); + // Invalidate to kick off computeScroll + mSv.invalidate(); + } else if (mSv.isScrollOutOfBounds()) { + // Animate the scroll back into bounds + // XXX: Make this animation a function of the velocity OR distance + mSv.animateBoundScroll(); + } + + if (mIsScrolling) { + // Disable HW layers + mSv.decHwLayersRefCount("stackScroll"); + } + mActivePointerId = INACTIVE_POINTER_ID; + mIsScrolling = false; + mTotalScrollMotion = 0; + recycleVelocityTracker(); + break; + } + case MotionEvent.ACTION_POINTER_UP: { + int pointerIndex = ev.getActionIndex(); + int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // Select a new active pointer id and reset the motion state + final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + mLastMotionX = (int) ev.getX(newPointerIndex); + mLastMotionY = (int) ev.getY(newPointerIndex); + mVelocityTracker.clear(); + } + break; + } + case MotionEvent.ACTION_CANCEL: { + if (mIsScrolling) { + // Disable HW layers + mSv.decHwLayersRefCount("stackScroll"); + } + if (mSv.isScrollOutOfBounds()) { + // Animate the scroll back into bounds + // XXX: Make this animation a function of the velocity OR distance + mSv.animateBoundScroll(); + } + mActivePointerId = INACTIVE_POINTER_ID; + mIsScrolling = false; + mTotalScrollMotion = 0; + recycleVelocityTracker(); + break; + } + } + return true; + } + + /**** SwipeHelper Implementation ****/ + + @Override + public View getChildAtPosition(MotionEvent ev) { + return findViewAtPoint((int) ev.getX(), (int) ev.getY()); + } + + @Override + public boolean canChildBeDismissed(View v) { + return true; + } + + @Override + public void onBeginDrag(View v) { + // Enable HW layers + mSv.addHwLayersRefCount("swipeBegin"); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // If the info panel is currently showing on this view, then we need to dismiss it + if (Constants.DebugFlags.App.EnableInfoPane) { + TaskView tv = (TaskView) v; + if (tv.isInfoPaneVisible()) { + tv.hideInfoPane(); + } + } + } + + @Override + public void onChildDismissed(View v) { + TaskView tv = (TaskView) v; + Task task = tv.getTask(); + Activity activity = (Activity) mSv.getContext(); + + // Remove the task from the view + mSv.mStack.removeTask(task); + + // Remove any stored data from the loader + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.deleteTaskData(task); + + // Remove the task from activity manager + RecentsTaskLoader.getInstance().getSystemServicesProxy().removeTask(tv.getTask().key.id); + + // If there are no remaining tasks, then either unfilter the current stack, or just close + // the activity if there are no filtered stacks + if (mSv.mStack.getTaskCount() == 0) { + boolean shouldFinishActivity = true; + if (mSv.mStack.hasFilteredTasks()) { + mSv.mStack.unfilterTasks(); + shouldFinishActivity = (mSv.mStack.getTaskCount() == 0); + } + if (shouldFinishActivity) { + activity.finish(); + } + } + + // Disable HW layers + mSv.decHwLayersRefCount("swipeComplete"); + } + + @Override + public void onSnapBackCompleted(View v) { + // Do Nothing + } + + @Override + public void onDragCancelled(View v) { + // Disable HW layers + mSv.decHwLayersRefCount("swipeCancelled"); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskThumbnailView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskThumbnailView.java new file mode 100644 index 0000000..8a9250a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskThumbnailView.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; +import com.android.systemui.recents.model.Task; + + +/** The task thumbnail view */ +public class TaskThumbnailView extends ImageView { + Task mTask; + + public TaskThumbnailView(Context context) { + this(context, null); + } + + public TaskThumbnailView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setScaleType(ScaleType.FIT_XY); + } + + /** Binds the thumbnail view to the task */ + void rebindToTask(Task t, boolean animate) { + mTask = t; + if (t.thumbnail != null) { + setImageBitmap(t.thumbnail); + if (animate) { + // XXX: Investigate how expensive it will be to create a second bitmap and crossfade + } + } + } + + /** Unbinds the thumbnail view from the task */ + void unbindFromTask() { + mTask = null; + setImageDrawable(null); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java new file mode 100644 index 0000000..d3b79d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.widget.FrameLayout; +import com.android.systemui.R; +import com.android.systemui.recents.BakedBezierInterpolator; +import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.Utilities; +import com.android.systemui.recents.model.Task; + + +/* A task view */ +public class TaskView extends FrameLayout implements View.OnClickListener, + Task.TaskCallbacks { + /** The TaskView callbacks */ + interface TaskViewCallbacks { + public void onTaskIconClicked(TaskView tv); + public void onTaskInfoPanelShown(TaskView tv); + public void onTaskInfoPanelHidden(TaskView tv); + public void onTaskAppInfoClicked(TaskView tv); + + // public void onTaskViewReboundToTask(TaskView tv, Task t); + } + + int mDim; + int mMaxDim; + TimeInterpolator mDimInterpolator = new AccelerateInterpolator(); + + Task mTask; + boolean mTaskDataLoaded; + boolean mTaskInfoPaneVisible; + Point mLastTouchDown = new Point(); + Path mRoundedRectClipPath = new Path(); + + TaskThumbnailView mThumbnailView; + TaskBarView mBarView; + TaskInfoView mInfoView; + TaskViewCallbacks mCb; + + + public TaskView(Context context) { + this(context, null); + } + + public TaskView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setWillNotDraw(false); + } + + @Override + protected void onFinishInflate() { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + mMaxDim = config.taskStackMaxDim; + + // Bind the views + mThumbnailView = (TaskThumbnailView) findViewById(R.id.task_view_thumbnail); + mBarView = (TaskBarView) findViewById(R.id.task_view_bar); + mInfoView = (TaskInfoView) findViewById(R.id.task_view_info_pane); + + if (mTaskDataLoaded) { + onTaskDataLoaded(false); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Update the rounded rect clip path + RecentsConfiguration config = RecentsConfiguration.getInstance(); + float radius = config.taskViewRoundedCornerRadiusPx; + mRoundedRectClipPath.reset(); + mRoundedRectClipPath.addRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()), + radius, radius, Path.Direction.CW); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mLastTouchDown.set((int) ev.getX(), (int) ev.getY()); + break; + } + return super.onInterceptTouchEvent(ev); + } + + /** Set callback */ + void setCallbacks(TaskViewCallbacks cb) { + mCb = cb; + } + + /** Gets the task */ + Task getTask() { + return mTask; + } + + /** Synchronizes this view's properties with the task's transform */ + void updateViewPropertiesToTaskTransform(TaskViewTransform animateFromTransform, + TaskViewTransform toTransform, int duration) { + if (duration > 0) { + if (animateFromTransform != null) { + setTranslationY(animateFromTransform.translationY); + setScaleX(animateFromTransform.scale); + setScaleY(animateFromTransform.scale); + setAlpha(animateFromTransform.alpha); + } + animate().translationY(toTransform.translationY) + .scaleX(toTransform.scale) + .scaleY(toTransform.scale) + .alpha(toTransform.alpha) + .setDuration(duration) + .setInterpolator(BakedBezierInterpolator.INSTANCE) + .withLayer() + .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + updateDimOverlayFromScale(); + } + }) + .start(); + } else { + setTranslationY(toTransform.translationY); + setScaleX(toTransform.scale); + setScaleY(toTransform.scale); + setAlpha(toTransform.alpha); + } + updateDimOverlayFromScale(); + invalidate(); + } + + /** Resets this view's properties */ + void resetViewProperties() { + setTranslationX(0f); + setTranslationY(0f); + setScaleX(1f); + setScaleY(1f); + setAlpha(1f); + invalidate(); + } + + /** + * When we are un/filtering, this method will set up the transform that we are animating to, + * in order to hide the task. + */ + void prepareTaskTransformForFilterTaskHidden(TaskViewTransform toTransform) { + // Fade the view out and slide it away + toTransform.alpha = 0f; + toTransform.translationY += 200; + } + + /** + * When we are un/filtering, this method will setup the transform that we are animating from, + * in order to show the task. + */ + void prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform) { + // Fade the view in + fromTransform.alpha = 0f; + } + + /** Animates this task view as it enters recents */ + public void animateOnEnterRecents() { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + int translate = config.pxFromDp(10); + mBarView.setScaleX(1.25f); + mBarView.setScaleY(1.25f); + mBarView.setAlpha(0f); + mBarView.setTranslationX(translate / 2); + mBarView.setTranslationY(-translate); + mBarView.animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .translationX(0) + .translationY(0) + .setStartDelay(235) + .setInterpolator(BakedBezierInterpolator.INSTANCE) + .setDuration(config.taskBarEnterAnimDuration) + .withLayer() + .start(); + } + + /** Animates this task view as it exits recents */ + public void animateOnLeavingRecents(final Runnable r) { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + int translate = config.pxFromDp(10); + mBarView.animate() + .alpha(0f) + .scaleX(1.1f) + .scaleY(1.1f) + .translationX(translate / 2) + .translationY(-translate) + .setStartDelay(0) + .setInterpolator(BakedBezierInterpolator.INSTANCE) + .setDuration(Utilities.calculateTranslationAnimationDuration(translate)) + .withLayer() + .withEndAction(new Runnable() { + @Override + public void run() { + post(r); + } + }) + .start(); + } + + /** Returns the rect we want to clip (it may not be the full rect) */ + Rect getClippingRect(Rect outRect) { + getHitRect(outRect); + // XXX: We should get the hit rect of the thumbnail view and intersect, but this is faster + outRect.right = outRect.left + mThumbnailView.getRight(); + outRect.bottom = outRect.top + mThumbnailView.getBottom(); + return outRect; + } + + /** Returns whether this task has an info pane visible */ + boolean isInfoPaneVisible() { + return mTaskInfoPaneVisible; + } + + /** Shows the info pane if it is not visible. */ + void showInfoPane(Rect taskVisibleRect) { + if (mTaskInfoPaneVisible) return; + + // Remove the bar view from the visible rect and update the info pane contents + taskVisibleRect.top += mBarView.getMeasuredHeight(); + mInfoView.updateContents(taskVisibleRect); + + // Show the info pane and animate it into view + mInfoView.setVisibility(View.VISIBLE); + mInfoView.animateCircularClip(mLastTouchDown, 0f, 1f, null, true); + mInfoView.setOnClickListener(this); + mTaskInfoPaneVisible = true; + + // Notify any callbacks + if (mCb != null) { + mCb.onTaskInfoPanelShown(this); + } + } + + /** Hides the info pane if it is visible. */ + void hideInfoPane() { + if (!mTaskInfoPaneVisible) return; + RecentsConfiguration config = RecentsConfiguration.getInstance(); + + // Cancel any circular clip animation + mInfoView.cancelCircularClipAnimation(); + + // Animate the info pane out + mInfoView.animate() + .alpha(0f) + .setDuration(config.taskViewInfoPaneAnimDuration) + .setInterpolator(BakedBezierInterpolator.INSTANCE) + .withLayer() + .withEndAction(new Runnable() { + @Override + public void run() { + mInfoView.setVisibility(View.INVISIBLE); + mInfoView.setOnClickListener(null); + + mInfoView.setAlpha(1f); + } + }) + .start(); + mTaskInfoPaneVisible = false; + + // Notify any callbacks + if (mCb != null) { + mCb.onTaskInfoPanelHidden(this); + } + } + + /** Enable the hw layers on this task view */ + void enableHwLayers() { + mThumbnailView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + /** Disable the hw layers on this task view */ + void disableHwLayers() { + mThumbnailView.setLayerType(View.LAYER_TYPE_NONE, null); + } + + /** Update the dim as a function of the scale of this view. */ + void updateDimOverlayFromScale() { + float minScale = Constants.Values.TaskStackView.StackPeekMinScale; + float scaleRange = 1f - minScale; + float dim = (1f - getScaleX()) / scaleRange; + dim = mDimInterpolator.getInterpolation(Math.min(dim, 1f)); + mDim = Math.max(0, Math.min(mMaxDim, (int) (dim * 255))); + invalidate(); + } + + @Override + public void draw(Canvas canvas) { + // Apply the rounded rect clip path on the whole view + canvas.clipPath(mRoundedRectClipPath); + + super.draw(canvas); + + // Apply the dim if necessary + if (mDim > 0) { + canvas.drawColor(mDim << 24); + } + } + + /**** TaskCallbacks Implementation ****/ + + /** Binds this task view to the task */ + public void onTaskBound(Task t) { + mTask = t; + mTask.setCallbacks(this); + } + + @Override + public void onTaskDataLoaded(boolean reloadingTaskData) { + if (mThumbnailView != null && mBarView != null && mInfoView != null) { + // Bind each of the views to the new task data + mThumbnailView.rebindToTask(mTask, reloadingTaskData); + mBarView.rebindToTask(mTask, reloadingTaskData); + // Rebind any listeners + mBarView.mApplicationIcon.setOnClickListener(this); + mInfoView.mAppInfoButton.setOnClickListener(this); + } + mTaskDataLoaded = true; + } + + @Override + public void onTaskDataUnloaded() { + if (mThumbnailView != null && mBarView != null && mInfoView != null) { + // Unbind each of the views from the task data and remove the task callback + mTask.setCallbacks(null); + mThumbnailView.unbindFromTask(); + mBarView.unbindFromTask(); + // Unbind any listeners + mBarView.mApplicationIcon.setOnClickListener(null); + mInfoView.mAppInfoButton.setOnClickListener(null); + } + mTaskDataLoaded = false; + } + + @Override + public void onClick(View v) { + if (v == mInfoView) { + // Do nothing + } else if (v == mBarView.mApplicationIcon) { + mCb.onTaskIconClicked(this); + } else if (v == mInfoView.mAppInfoButton) { + mCb.onTaskAppInfoClicked(this); + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java new file mode 100644 index 0000000..0748bbb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.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.recents.views; + +import android.graphics.Rect; + + +/* The transform state for a task view */ +public class TaskViewTransform { + public int translationY = 0; + public float scale = 1f; + public float alpha = 1f; + public boolean visible = false; + public Rect rect = new Rect(); + float t; + + public TaskViewTransform() { + // Do nothing + } + + public TaskViewTransform(TaskViewTransform o) { + translationY = o.translationY; + scale = o.scale; + alpha = o.alpha; + visible = o.visible; + rect.set(o.rect); + t = o.t; + } + + @Override + public String toString() { + return "TaskViewTransform y: " + translationY + " scale: " + scale + " alpha: " + alpha + + " visible: " + visible + " rect: " + rect; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java new file mode 100644 index 0000000..af0094e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.content.Context; + +import java.util.Iterator; +import java.util.LinkedList; + + +/* A view pool to manage more views than we can visibly handle */ +public class ViewPool<V, T> { + + /* An interface to the consumer of a view pool */ + public interface ViewPoolConsumer<V, T> { + public V createView(Context context); + public void prepareViewToEnterPool(V v); + public void prepareViewToLeavePool(V v, T prepareData, boolean isNewView); + public boolean hasPreferredData(V v, T preferredData); + } + + Context mContext; + ViewPoolConsumer<V, T> mViewCreator; + LinkedList<V> mPool = new LinkedList<V>(); + + /** Initializes the pool with a fixed predetermined pool size */ + public ViewPool(Context context, ViewPoolConsumer<V, T> viewCreator) { + mContext = context; + mViewCreator = viewCreator; + } + + /** Returns a view into the pool */ + void returnViewToPool(V v) { + mViewCreator.prepareViewToEnterPool(v); + mPool.push(v); + } + + /** Gets a view from the pool and prepares it */ + V pickUpViewFromPool(T preferredData, T prepareData) { + V v = null; + boolean isNewView = false; + if (mPool.isEmpty()) { + v = mViewCreator.createView(mContext); + isNewView = true; + } else { + // Try and find a preferred view + Iterator<V> iter = mPool.iterator(); + while (iter.hasNext()) { + V vpv = iter.next(); + if (mViewCreator.hasPreferredData(vpv, preferredData)) { + v = vpv; + iter.remove(); + break; + } + } + // Otherwise, just grab the first view + if (v == null) { + v = mPool.pop(); + } + } + mViewCreator.prepareViewToLeavePool(v, prepareData, isNewView); + return v; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 74d982a..5771299 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -98,7 +98,7 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi private final int mNotificationId; private final NotificationManager mNotificationManager; - private final Notification.Builder mNotificationBuilder; + private final Notification.Builder mNotificationBuilder, mPublicNotificationBuilder; private final File mScreenshotDir; private final String mImageFileName; private final String mImageFilePath; @@ -152,18 +152,31 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi mTickerAddSpace = !mTickerAddSpace; mNotificationId = nId; mNotificationManager = nManager; + final long now = System.currentTimeMillis(); + mNotificationBuilder = new Notification.Builder(context) .setTicker(r.getString(R.string.screenshot_saving_ticker) + (mTickerAddSpace ? " " : "")) .setContentTitle(r.getString(R.string.screenshot_saving_title)) .setContentText(r.getString(R.string.screenshot_saving_text)) .setSmallIcon(R.drawable.stat_notify_image) - .setWhen(System.currentTimeMillis()); + .setWhen(now); mNotificationStyle = new Notification.BigPictureStyle() .bigPicture(preview); mNotificationBuilder.setStyle(mNotificationStyle); + // For "public" situations we want to show all the same info but + // omit the actual screenshot image. + mPublicNotificationBuilder = new Notification.Builder(context) + .setContentTitle(r.getString(R.string.screenshot_saving_title)) + .setContentText(r.getString(R.string.screenshot_saving_text)) + .setSmallIcon(R.drawable.stat_notify_image) + .setCategory(Notification.CATEGORY_PROGRESS) + .setWhen(now); + + mNotificationBuilder.setPublicVersion(mPublicNotificationBuilder.build()); + Notification n = mNotificationBuilder.build(); n.flags |= Notification.FLAG_NO_CLEAR; mNotificationManager.notify(nId, n); @@ -280,13 +293,25 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi launchIntent.setDataAndType(params.imageUri, "image/png"); launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final long now = System.currentTimeMillis(); + mNotificationBuilder .setContentTitle(r.getString(R.string.screenshot_saved_title)) .setContentText(r.getString(R.string.screenshot_saved_text)) .setContentIntent(PendingIntent.getActivity(params.context, 0, launchIntent, 0)) - .setWhen(System.currentTimeMillis()) + .setWhen(now) + .setAutoCancel(true); + + // Update the text in the public version as well + mPublicNotificationBuilder + .setContentTitle(r.getString(R.string.screenshot_saved_title)) + .setContentText(r.getString(R.string.screenshot_saved_text)) + .setContentIntent(PendingIntent.getActivity(params.context, 0, launchIntent, 0)) + .setWhen(now) .setAutoCancel(true); + mNotificationBuilder.setPublicVersion(mPublicNotificationBuilder.build()); + Notification n = mNotificationBuilder.build(); n.flags &= ~Notification.FLAG_NO_CLEAR; mNotificationManager.notify(mNotificationId, n); @@ -669,6 +694,8 @@ class GlobalScreenshot { .setContentText(r.getString(R.string.screenshot_failed_text)) .setSmallIcon(R.drawable.stat_notify_image_error) .setWhen(System.currentTimeMillis()) + .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen + .setCategory(Notification.CATEGORY_ERROR) .setAutoCancel(true); Notification n = new Notification.BigTextStyle(b) diff --git a/packages/SystemUI/src/com/android/systemui/settings/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/BrightnessDialog.java index ff79f04..bd5e5e8 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/BrightnessDialog.java @@ -21,6 +21,8 @@ import android.content.Context; import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; +import android.view.Gravity; +import android.view.KeyEvent; import android.view.Window; import android.view.WindowManager; import android.widget.ImageView; @@ -67,9 +69,15 @@ public class BrightnessDialog extends Dialog implements public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Window window = getWindow(); - window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); - window.getAttributes().privateFlags |= + window.setGravity(Gravity.TOP); + WindowManager.LayoutParams lp = window.getAttributes(); + // Offset from the top + lp.y = getContext().getResources().getDimensionPixelOffset( + com.android.internal.R.dimen.volume_panel_top); + lp.type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; + lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + window.setAttributes(lp); window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); window.requestFeature(Window.FEATURE_NO_TITLE); @@ -108,4 +116,13 @@ public class BrightnessDialog extends Dialog implements mHandler.removeCallbacks(mDismissDialogRunnable); } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + keyCode == KeyEvent.KEYCODE_VOLUME_UP || + keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) { + dismiss(); + } + return super.onKeyDown(keyCode, event); + } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/ToggleSeekBar.java b/packages/SystemUI/src/com/android/systemui/settings/ToggleSeekBar.java new file mode 100644 index 0000000..a0a5561 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/ToggleSeekBar.java @@ -0,0 +1,45 @@ +/* + * 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.settings; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.SeekBar; + +public class ToggleSeekBar extends SeekBar { + public ToggleSeekBar(Context context) { + super(context); + } + + public ToggleSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ToggleSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled()) { + setEnabled(true); + } + + return super.onTouchEvent(event); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/ToggleSlider.java b/packages/SystemUI/src/com/android/systemui/settings/ToggleSlider.java index d584043..7d38058 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/ToggleSlider.java +++ b/packages/SystemUI/src/com/android/systemui/settings/ToggleSlider.java @@ -19,20 +19,18 @@ package com.android.systemui.settings; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.RelativeLayout; import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; import com.android.systemui.R; -public class ToggleSlider extends RelativeLayout - implements CompoundButton.OnCheckedChangeListener, SeekBar.OnSeekBarChangeListener { - private static final String TAG = "StatusBar.ToggleSlider"; - +public class ToggleSlider extends RelativeLayout { public interface Listener { public void onInit(ToggleSlider v); public void onChanged(ToggleSlider v, boolean tracking, boolean checked, int value); @@ -55,20 +53,21 @@ public class ToggleSlider extends RelativeLayout public ToggleSlider(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + View.inflate(context, R.layout.status_bar_toggle_slider, this); final Resources res = context.getResources(); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ToggleSlider, - defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ToggleSlider, defStyle, 0); - mToggle = (CompoundButton)findViewById(R.id.toggle); - mToggle.setOnCheckedChangeListener(this); - mToggle.setBackgroundDrawable(res.getDrawable(R.drawable.status_bar_toggle_button)); + mToggle = (CompoundButton) findViewById(R.id.toggle); + mToggle.setOnCheckedChangeListener(mCheckListener); + mToggle.setBackground(res.getDrawable(R.drawable.status_bar_toggle_button)); - mSlider = (SeekBar)findViewById(R.id.slider); - mSlider.setOnSeekBarChangeListener(this); + mSlider = (SeekBar) findViewById(R.id.slider); + mSlider.setOnSeekBarChangeListener(mSeekListener); - mLabel = (TextView)findViewById(R.id.label); + mLabel = (TextView) findViewById(R.id.label); mLabel.setText(a.getString(R.styleable.ToggleSlider_text)); a.recycle(); @@ -82,50 +81,6 @@ public class ToggleSlider extends RelativeLayout } } - public void onCheckedChanged(CompoundButton toggle, boolean checked) { - Drawable thumb; - Drawable slider; - final Resources res = getContext().getResources(); - if (checked) { - thumb = res.getDrawable( - com.android.internal.R.drawable.scrubber_control_disabled_holo); - slider = res.getDrawable( - R.drawable.status_bar_settings_slider_disabled); - } else { - thumb = res.getDrawable( - com.android.internal.R.drawable.scrubber_control_selector_holo); - slider = res.getDrawable( - com.android.internal.R.drawable.scrubber_progress_horizontal_holo_dark); - } - mSlider.setThumb(thumb); - mSlider.setProgressDrawable(slider); - - if (mListener != null) { - mListener.onChanged(this, mTracking, checked, mSlider.getProgress()); - } - } - - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (mListener != null) { - mListener.onChanged(this, mTracking, mToggle.isChecked(), progress); - } - } - - public void onStartTrackingTouch(SeekBar seekBar) { - mTracking = true; - if (mListener != null) { - mListener.onChanged(this, mTracking, mToggle.isChecked(), mSlider.getProgress()); - } - mToggle.setChecked(false); - } - - public void onStopTrackingTouch(SeekBar seekBar) { - mTracking = false; - if (mListener != null) { - mListener.onChanged(this, mTracking, mToggle.isChecked(), mSlider.getProgress()); - } - } - public void setOnChangedListener(Listener l) { mListener = l; } @@ -145,5 +100,49 @@ public class ToggleSlider extends RelativeLayout public void setValue(int value) { mSlider.setProgress(value); } + + private final OnCheckedChangeListener mCheckListener = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton toggle, boolean checked) { + mSlider.setEnabled(!checked); + + if (mListener != null) { + mListener.onChanged( + ToggleSlider.this, mTracking, checked, mSlider.getProgress()); + } + } + }; + + private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (mListener != null) { + mListener.onChanged( + ToggleSlider.this, mTracking, mToggle.isChecked(), progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mTracking = true; + + if (mListener != null) { + mListener.onChanged( + ToggleSlider.this, mTracking, mToggle.isChecked(), mSlider.getProgress()); + } + + mToggle.setChecked(false); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mTracking = false; + + if (mListener != null) { + mListener.onChanged( + ToggleSlider.this, mTracking, mToggle.isChecked(), mSlider.getProgress()); + } + } + }; } diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserSwitcherHostView.java b/packages/SystemUI/src/com/android/systemui/settings/UserSwitcherHostView.java new file mode 100644 index 0000000..d67e7cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/UserSwitcherHostView.java @@ -0,0 +1,178 @@ +/* + * 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.settings; + +import com.android.systemui.R; + +import android.app.ActivityManagerNative; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.RemoteException; +import android.os.UserManager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManagerGlobal; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +/** + * A quick and dirty view to show a user switcher. + */ +public class UserSwitcherHostView extends FrameLayout implements ListView.OnItemClickListener { + + private static final String TAG = "UserSwitcherDialog"; + + private ArrayList<UserInfo> mUserInfo = new ArrayList<UserInfo>(); + private Adapter mAdapter = new Adapter(); + private UserManager mUserManager; + private Runnable mFinishRunnable; + private ListView mListView; + + public UserSwitcherHostView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + if (isInEditMode()) { + return; + } + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + } + + public UserSwitcherHostView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.listViewStyle); + } + + public UserSwitcherHostView(Context context) { + this(context, null); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mListView = (ListView) findViewById(android.R.id.list); + mListView.setAdapter(mAdapter); + mListView.setOnItemClickListener(this); + } + + @Override + public void onItemClick(AdapterView<?> l, View v, int position, long id) { + int userId = mAdapter.getItem(position).id; + try { + WindowManagerGlobal.getWindowManagerService().lockNow(null); + ActivityManagerNative.getDefault().switchUser(userId); + finish(); + } catch (RemoteException e) { + Log.e(TAG, "Couldn't switch user.", e); + } + } + + private void finish() { + if (mFinishRunnable != null) { + mFinishRunnable.run(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + finish(); + } + return true; + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + // A gross hack to get rid of the switcher when the shade is collapsed. + if (visibility != VISIBLE) { + finish(); + } + } + + public void setFinishRunnable(Runnable finishRunnable) { + mFinishRunnable = finishRunnable; + } + + public void refreshUsers() { + mUserInfo.clear(); + List<UserInfo> users = mUserManager.getUsers(true); + for (UserInfo user : users) { + if (!user.isManagedProfile()) { + mUserInfo.add(user); + } + } + mAdapter.notifyDataSetChanged(); + } + + private class Adapter extends BaseAdapter { + + @Override + public int getCount() { + return mUserInfo.size(); + } + + @Override + public UserInfo getItem(int position) { + return mUserInfo.get(position); + } + + @Override + public long getItemId(int position) { + return getItem(position).serialNumber; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null || (!(convertView.getTag() instanceof ViewHolder))) { + convertView = createView(parent); + } + ViewHolder h = (ViewHolder) convertView.getTag(); + bindView(h, getItem(position)); + return convertView; + } + + private View createView(ViewGroup parent) { + View v = LayoutInflater.from(getContext()).inflate( + R.layout.user_switcher_item, parent, false); + ViewHolder h = new ViewHolder(); + h.name = (TextView) v.findViewById(R.id.user_name); + h.picture = (ImageView) v.findViewById(R.id.user_picture); + v.setTag(h); + return v; + } + + private void bindView(ViewHolder h, UserInfo item) { + h.name.setText(item.name); + h.picture.setImageBitmap(mUserManager.getUserIcon(item.id)); + } + + class ViewHolder { + TextView name; + ImageView picture; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java new file mode 100644 index 0000000..0f32dc0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java @@ -0,0 +1,208 @@ +/* + * 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.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.FrameLayout; + +import com.android.internal.R; + +/** + * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer} + * to implement dimming/activating on Keyguard for the double-tap gesture + */ +public abstract class ActivatableNotificationView extends ExpandableOutlineView { + + private static final long DOUBLETAP_TIMEOUT_MS = 1000; + + private boolean mDimmed; + private boolean mLocked; + + private int mBgResId = R.drawable.notification_quantum_bg; + private int mDimmedBgResId = R.drawable.notification_quantum_bg_dim; + + /** + * Flag to indicate that the notification has been touched once and the second touch will + * click it. + */ + private boolean mActivated; + + private float mDownX; + private float mDownY; + private final float mTouchSlop; + + private OnActivatedListener mOnActivatedListener; + + public ActivatableNotificationView(Context context, AttributeSet attrs) { + super(context, attrs); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + updateBackgroundResource(); + } + + + private final Runnable mTapTimeoutRunnable = new Runnable() { + @Override + public void run() { + makeInactive(); + } + }; + + @Override + public void setOnClickListener(OnClickListener l) { + super.setOnClickListener(l); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mLocked) { + return handleTouchEventLocked(event); + } else { + return super.onTouchEvent(event); + } + } + + private boolean handleTouchEventLocked(MotionEvent event) { + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mDownX = event.getX(); + mDownY = event.getY(); + if (mDownY > getActualHeight()) { + return false; + } + + // Call the listener tentatively directly, even if we don't know whether the user + // will stay within the touch slop, as the listener is implemented as a scale + // animation, which is cancellable without jarring effects when swiping away + // notifications. + if (mOnActivatedListener != null) { + mOnActivatedListener.onActivated(this); + } + break; + case MotionEvent.ACTION_MOVE: + if (!isWithinTouchSlop(event)) { + makeInactive(); + return false; + } + break; + case MotionEvent.ACTION_UP: + if (isWithinTouchSlop(event)) { + if (!mActivated) { + makeActive(event.getX(), event.getY()); + postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS); + } else { + performClick(); + makeInactive(); + } + } else { + makeInactive(); + } + break; + case MotionEvent.ACTION_CANCEL: + makeInactive(); + break; + default: + break; + } + return true; + } + + private void makeActive(float x, float y) { + mCustomBackground.setHotspot(0, x, y); + mActivated = true; + } + + /** + * Cancels the hotspot and makes the notification inactive. + */ + private void makeInactive() { + if (mActivated) { + // Make sure that we clear the hotspot from the center. + mCustomBackground.setHotspot(0, getWidth() / 2, getActualHeight() / 2); + mCustomBackground.removeHotspot(0); + mActivated = false; + } + if (mOnActivatedListener != null) { + mOnActivatedListener.onReset(this); + } + removeCallbacks(mTapTimeoutRunnable); + } + + private boolean isWithinTouchSlop(MotionEvent event) { + return Math.abs(event.getX() - mDownX) < mTouchSlop + && Math.abs(event.getY() - mDownY) < mTouchSlop; + } + + /** + * Sets the notification as dimmed, meaning that it will appear in a more gray variant. + */ + public void setDimmed(boolean dimmed) { + if (mDimmed != dimmed) { + mDimmed = dimmed; + updateBackgroundResource(); + } + } + + /** + * Sets the notification as locked. In the locked state, the first tap will produce a quantum + * ripple to make the notification brighter and only the second tap will cause a click. + */ + public void setLocked(boolean locked) { + mLocked = locked; + } + + /** + * Sets the resource id for the background of this notification. + * + * @param bgResId The background resource to use in normal state. + * @param dimmedBgResId The background resource to use in dimmed state. + */ + public void setBackgroundResourceIds(int bgResId, int dimmedBgResId) { + mBgResId = bgResId; + mDimmedBgResId = dimmedBgResId; + updateBackgroundResource(); + } + + private void updateBackgroundResource() { + setCustomBackgroundResource(mDimmed ? mDimmedBgResId : mBgResId); + } + + @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) { + super.setActualHeight(actualHeight); + setPivotY(actualHeight/2); + } + + public void setOnActivatedListener(OnActivatedListener onActivatedListener) { + mOnActivatedListener = onActivatedListener; + } + + public interface OnActivatedListener { + void onActivated(View view); + void onReset(View view); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java b/packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java index 7d3e870..9839fe9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java @@ -38,7 +38,7 @@ public class AnimatedImageView extends ImageView { } private void updateAnim() { - Drawable drawable = mAttached ? getDrawable() : null; + Drawable drawable = getDrawable(); if (mAttached && mAnim != null) { mAnim.stop(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index 9a0749d..9149e2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -26,10 +26,13 @@ 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.content.res.Configuration; import android.database.ContentObserver; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Handler; @@ -39,12 +42,15 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; import android.service.dreams.DreamService; import android.service.dreams.IDreamManager; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.Log; +import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.view.Display; import android.view.IWindowManager; import android.view.LayoutInflater; @@ -64,19 +70,19 @@ import android.widget.TextView; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.statusbar.StatusBarIconList; -import com.android.internal.widget.SizeAdaptiveLayout; +import com.android.internal.util.LegacyNotificationUtil; import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.SearchPanelView; import com.android.systemui.SystemUI; import com.android.systemui.statusbar.phone.KeyguardTouchDelegate; -import com.android.systemui.statusbar.policy.NotificationRowLayout; +import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; import java.util.ArrayList; import java.util.Locale; public abstract class BaseStatusBar extends SystemUI implements - CommandQueue.Callbacks { + CommandQueue.Callbacks, ActivatableNotificationView.OnActivatedListener { public static final String TAG = "StatusBar"; public static final boolean DEBUG = false; public static final boolean MULTIUSER_DEBUG = false; @@ -93,8 +99,8 @@ public abstract class BaseStatusBar extends SystemUI implements protected static final boolean ENABLE_HEADS_UP = true; // scores above this threshold should be displayed in heads up mode. - protected static final int INTERRUPTION_THRESHOLD = 11; - protected static final String SETTING_HEADS_UP = "heads_up_enabled"; + protected static final int INTERRUPTION_THRESHOLD = 10; + protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up"; // Should match the value in PhoneWindowManager public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; @@ -108,7 +114,7 @@ public abstract class BaseStatusBar extends SystemUI implements // all notifications protected NotificationData mNotificationData = new NotificationData(); - protected NotificationRowLayout mPile; + protected NotificationStackScrollLayout mStackScroller; protected NotificationData.Entry mInterruptingNotificationEntry; protected long mInterruptingNotificationTime; @@ -122,14 +128,24 @@ public abstract class BaseStatusBar extends SystemUI implements protected PopupMenu mNotificationBlamePopup; protected int mCurrentUserId = 0; + final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>(); protected int mLayoutDirection = -1; // invalid private Locale mLocale; protected boolean mUseHeadsUp = false; + protected boolean mHeadsUpTicker = false; protected IDreamManager mDreamManager; PowerManager mPowerManager; - protected int mRowHeight; + protected int mRowMinHeight; + protected int mRowMaxHeight; + + // public mode, private notifications, etc + private boolean mLockscreenPublicMode = false; + private final SparseBooleanArray mUsersAllowingPrivateNotifications = new SparseBooleanArray(); + private LegacyNotificationUtil mLegacyNotificationUtil = LegacyNotificationUtil.getInstance(); + + private UserManager mUserManager; // UI-specific methods @@ -149,15 +165,16 @@ public abstract class BaseStatusBar extends SystemUI implements private RecentsComponent mRecents; - public IStatusBarService getStatusBarService() { - return mBarService; - } + protected int mZenMode; + + protected boolean mOnKeyguard; + protected NotificationOverflowContainer mKeyguardIconOverflowContainer; public boolean isDeviceProvisioned() { return mDeviceProvisioned; } - private ContentObserver mProvisioningObserver = new ContentObserver(new Handler()) { + protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { final boolean provisioned = 0 != Settings.Global.getInt( @@ -166,6 +183,20 @@ public abstract class BaseStatusBar extends SystemUI implements mDeviceProvisioned = provisioned; updateNotificationIcons(); } + final int mode = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF); + setZenMode(mode); + } + }; + + 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, + // so we just dump our cache ... + mUsersAllowingPrivateNotifications.clear(); + // ... and refresh all the notifications + updateNotificationIcons(); } }; @@ -207,12 +238,26 @@ public abstract class BaseStatusBar extends SystemUI implements 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"); userSwitched(mCurrentUserId); + } else if (Intent.ACTION_USER_ADDED.equals(action)) { + updateCurrentProfilesCache(); } } }; + private void updateCurrentProfilesCache() { + synchronized (mCurrentProfiles) { + mCurrentProfiles.clear(); + if (mUserManager != null) { + for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) { + mCurrentProfiles.put(user.id, user); + } + } + } + } + public void start() { mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); @@ -222,10 +267,19 @@ public abstract class BaseStatusBar extends SystemUI implements ServiceManager.checkService(DreamService.DREAM_SERVICE)); mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - mProvisioningObserver.onChange(false); // set up + mSettingsObserver.onChange(false); // set up mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), true, - mProvisioningObserver); + mSettingsObserver); + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false, + mSettingsObserver); + + 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)); @@ -235,6 +289,8 @@ public abstract class BaseStatusBar extends SystemUI implements mLocale = mContext.getResources().getConfiguration().locale; mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(mLocale); + mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + // Connect in to the status bar manager service StatusBarIconList iconList = new StatusBarIconList(); ArrayList<IBinder> notificationKeys = new ArrayList<IBinder>(); @@ -296,22 +352,27 @@ public abstract class BaseStatusBar extends SystemUI implements IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); + filter.addAction(Intent.ACTION_USER_ADDED); mContext.registerReceiver(mBroadcastReceiver, filter); + + updateCurrentProfilesCache(); } public void userSwitched(int newUserId) { // should be overridden } - public boolean notificationIsForCurrentUser(StatusBarNotification n) { + public boolean notificationIsForCurrentProfiles(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 notificationUserId == UserHandle.USER_ALL - || thisUserId == notificationUserId; + synchronized (mCurrentProfiles) { + return notificationUserId == UserHandle.USER_ALL + || mCurrentProfiles.get(notificationUserId) != null; + } } @Override @@ -337,13 +398,14 @@ public abstract class BaseStatusBar extends SystemUI implements final String _pkg = n.getPackageName(); final String _tag = n.getTag(); final int _id = n.getId(); + final int _userId = n.getUserId(); vetoButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Accessibility feedback v.announceForAccessibility( mContext.getString(R.string.accessibility_notification_dismissed)); try { - mBarService.onNotificationClear(_pkg, _tag, _id); + mBarService.onNotificationClear(_pkg, _tag, _id, _userId); } catch (RemoteException ex) { // system process is dead if we're here. @@ -359,9 +421,9 @@ public abstract class BaseStatusBar extends SystemUI implements } - protected void applyLegacyRowBackground(StatusBarNotification sbn, View content) { - if (sbn.getNotification().contentView.getLayoutId() != - com.android.internal.R.layout.notification_template_base) { + protected void applyLegacyRowBackground(StatusBarNotification sbn, + NotificationData.Entry entry) { + if (entry.expanded.getId() != com.android.internal.R.id.status_bar_latest_event_content) { int version = 0; try { ApplicationInfo info = mContext.getPackageManager().getApplicationInfo(sbn.getPackageName(), 0); @@ -370,9 +432,11 @@ public abstract class BaseStatusBar extends SystemUI implements Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex); } if (version > 0 && version < Build.VERSION_CODES.GINGERBREAD) { - content.setBackgroundResource(R.drawable.notification_row_legacy_bg); - } else { - content.setBackgroundResource(com.android.internal.R.drawable.notification_bg); + entry.row.setBackgroundResource(R.drawable.notification_row_legacy_bg); + } else if (version < Build.VERSION_CODES.L) { + entry.row.setBackgroundResourceIds( + com.android.internal.R.drawable.notification_bg, + com.android.internal.R.drawable.notification_bg_dim); } } } @@ -524,6 +588,7 @@ public abstract class BaseStatusBar extends SystemUI implements protected void toggleRecentsActivity() { if (mRecents != null) { + sendCloseSystemWindows(mContext, SYSTEM_DIALOG_REASON_RECENT_APPS); mRecents.toggleRecents(mDisplay, mLayoutDirection, getStatusBarView()); } } @@ -548,6 +613,37 @@ public abstract class BaseStatusBar extends SystemUI implements public abstract void resetHeadsUpDecayTimer(); + /** + * Save the current "public" (locked and secure) state of the lockscreen. + */ + public void setLockscreenPublicMode(boolean publicMode) { + mLockscreenPublicMode = publicMode; + } + + public boolean isLockscreenPublicMode() { + return mLockscreenPublicMode; + } + + /** + * 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 allowed = 0 != Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle); + mUsersAllowingPrivateNotifications.append(userHandle, allowed); + return allowed; + } + + return mUsersAllowingPrivateNotifications.get(userHandle); + } + protected class H extends Handler { public void handleMessage(Message m) { Intent intent; @@ -614,6 +710,14 @@ public abstract class BaseStatusBar extends SystemUI implements } public boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) { + return inflateViews(entry, parent, false); + } + + public boolean inflateViewsForHeadsUp(NotificationData.Entry entry, ViewGroup parent) { + return inflateViews(entry, parent, true); + } + + public boolean inflateViews(NotificationData.Entry entry, ViewGroup parent, boolean isHeadsUp) { int minHeight = mContext.getResources().getDimensionPixelSize(R.dimen.notification_min_height); int maxHeight = @@ -621,10 +725,23 @@ public abstract class BaseStatusBar extends SystemUI implements StatusBarNotification sbn = entry.notification; RemoteViews contentView = sbn.getNotification().contentView; RemoteViews bigContentView = sbn.getNotification().bigContentView; + + if (isHeadsUp) { + maxHeight = + mContext.getResources().getDimensionPixelSize(R.dimen.notification_mid_height); + bigContentView = sbn.getNotification().headsUpContentView; + } + if (contentView == null) { return false; } + if (DEBUG) { + Log.v(TAG, "publicNotification: " + sbn.getNotification().publicVersion); + } + + Notification publicNotification = sbn.getNotification().publicVersion; + // create the row view LayoutInflater inflater = (LayoutInflater)mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); @@ -642,26 +759,31 @@ public abstract class BaseStatusBar extends SystemUI implements // NB: the large icon is now handled entirely by the template // bind the click event to the content area - ViewGroup content = (ViewGroup)row.findViewById(R.id.content); - ViewGroup adaptive = (ViewGroup)row.findViewById(R.id.adaptive); + NotificationContentView expanded = + (NotificationContentView) row.findViewById(R.id.expanded); + NotificationContentView expandedPublic = + (NotificationContentView) row.findViewById(R.id.expandedPublic); - content.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); PendingIntent contentIntent = sbn.getNotification().contentIntent; if (contentIntent != null) { - final View.OnClickListener listener = new NotificationClicker(contentIntent, - sbn.getPackageName(), sbn.getTag(), sbn.getId()); - content.setOnClickListener(listener); + final View.OnClickListener listener = makeClicker(contentIntent, + sbn.getPackageName(), sbn.getTag(), sbn.getId(), isHeadsUp, sbn.getUserId()); + row.setOnClickListener(listener); } else { - content.setOnClickListener(null); + row.setOnClickListener(null); } + // set up the adaptive layout View contentViewLocal = null; View bigContentViewLocal = null; try { - contentViewLocal = contentView.apply(mContext, adaptive, mOnClickHandler); + contentViewLocal = contentView.apply(mContext, expanded, + mOnClickHandler); if (bigContentView != null) { - bigContentViewLocal = bigContentView.apply(mContext, adaptive, mOnClickHandler); + bigContentViewLocal = bigContentView.apply(mContext, expanded, + mOnClickHandler); } } catch (RuntimeException e) { @@ -671,41 +793,95 @@ public abstract class BaseStatusBar extends SystemUI implements } if (contentViewLocal != null) { - SizeAdaptiveLayout.LayoutParams params = - new SizeAdaptiveLayout.LayoutParams(contentViewLocal.getLayoutParams()); - params.minHeight = minHeight; - params.maxHeight = minHeight; - adaptive.addView(contentViewLocal, params); + contentViewLocal.setIsRootNamespace(true); + expanded.setContractedChild(contentViewLocal); } if (bigContentViewLocal != null) { - SizeAdaptiveLayout.LayoutParams params = - new SizeAdaptiveLayout.LayoutParams(bigContentViewLocal.getLayoutParams()); - params.minHeight = minHeight+1; - params.maxHeight = maxHeight; - adaptive.addView(bigContentViewLocal, params); + bigContentViewLocal.setIsRootNamespace(true); + expanded.setExpandedChild(bigContentViewLocal); } - row.setDrawingCacheEnabled(true); - applyLegacyRowBackground(sbn, content); + PackageManager pm = mContext.getPackageManager(); + + // now the public version + View publicViewLocal = null; + if (publicNotification != null) { + try { + publicViewLocal = publicNotification.contentView.apply(mContext, expandedPublic, + mOnClickHandler); + + if (publicViewLocal != null) { + publicViewLocal.setIsRootNamespace(true); + expandedPublic.setContractedChild(publicViewLocal); + } + } + catch (RuntimeException e) { + final String ident = sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()); + Log.e(TAG, "couldn't inflate public view for notification " + ident, e); + publicViewLocal = null; + } + } + + if (publicViewLocal == null) { + // Add a basic notification template + publicViewLocal = LayoutInflater.from(mContext).inflate( + com.android.internal.R.layout.notification_template_quantum_base, + expandedPublic, true); + + final TextView title = (TextView) publicViewLocal.findViewById(com.android.internal.R.id.title); + try { + title.setText(pm.getApplicationLabel( + pm.getApplicationInfo(entry.notification.getPackageName(), 0))); + } catch (NameNotFoundException e) { + title.setText(entry.notification.getPackageName()); + } + + final ImageView icon = (ImageView) publicViewLocal.findViewById(com.android.internal.R.id.icon); + + final StatusBarIcon ic = new StatusBarIcon(entry.notification.getPackageName(), + entry.notification.getUser(), + entry.notification.getNotification().icon, + entry.notification.getNotification().iconLevel, + entry.notification.getNotification().number, + entry.notification.getNotification().tickerText); + + Drawable iconDrawable = StatusBarIconView.getIcon(mContext, ic); + icon.setImageDrawable(iconDrawable); + if (mLegacyNotificationUtil.isGrayscale(iconDrawable)) { + icon.setBackgroundResource( + com.android.internal.R.drawable.notification_icon_legacy_bg_inset); + } + + final TextView text = (TextView) publicViewLocal.findViewById(com.android.internal.R.id.text); + text.setText("Unlock your device to see this notification."); + + // TODO: fill out "time" as well + } + + row.setDrawingCacheEnabled(true); if (MULTIUSER_DEBUG) { TextView debug = (TextView) row.findViewById(R.id.debug_info); if (debug != null) { debug.setVisibility(View.VISIBLE); - debug.setText("U " + entry.notification.getUserId()); + debug.setText("CU " + mCurrentUserId +" NU " + entry.notification.getUserId()); } } entry.row = row; - entry.row.setRowHeight(mRowHeight); - entry.content = content; + entry.row.setHeightRange(mRowMinHeight, mRowMaxHeight); + entry.row.setOnActivatedListener(this); entry.expanded = contentViewLocal; + entry.expandedPublic = publicViewLocal; entry.setBigContentView(bigContentViewLocal); + applyLegacyRowBackground(sbn, entry); + return true; } - public NotificationClicker makeClicker(PendingIntent intent, String pkg, String tag, int id) { - return new NotificationClicker(intent, pkg, tag, id); + public NotificationClicker makeClicker(PendingIntent intent, String pkg, String tag, + int id, boolean forHun, int userId) { + return new NotificationClicker(intent, pkg, tag, id, forHun, userId); } protected class NotificationClicker implements View.OnClickListener { @@ -713,12 +889,17 @@ public abstract class BaseStatusBar extends SystemUI implements private String mPkg; private String mTag; private int mId; + private boolean mIsHeadsUp; + private int mUserId; - public NotificationClicker(PendingIntent intent, String pkg, String tag, int id) { + public NotificationClicker(PendingIntent intent, String pkg, String tag, int id, + boolean forHun, int userId) { mIntent = intent; mPkg = pkg; mTag = tag; mId = id; + mIsHeadsUp = forHun; + mUserId = userId; } public void onClick(View v) { @@ -751,7 +932,10 @@ public abstract class BaseStatusBar extends SystemUI implements } try { - mBarService.onNotificationClick(mPkg, mTag, mId); + if (mIsHeadsUp) { + mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP); + } + mBarService.onNotificationClick(mPkg, mTag, mId, mUserId); } catch (RemoteException ex) { // system process is dead if we're here. } @@ -761,6 +945,7 @@ public abstract class BaseStatusBar extends SystemUI implements visibilityChanged(false); } } + /** * The LEDs are turned o)ff when the notification panel is shown, even just a little bit. * This was added last-minute and is inconsistent with the way the rest of the notifications @@ -772,7 +957,11 @@ public abstract class BaseStatusBar extends SystemUI implements if (mPanelSlightlyVisible != visible) { mPanelSlightlyVisible = visible; try { - mBarService.onPanelRevealed(); + if (visible) { + mBarService.onPanelRevealed(); + } else { + mBarService.onPanelHidden(); + } } catch (RemoteException ex) { // Won't fail unless the world has ended. } @@ -788,7 +977,8 @@ public abstract class BaseStatusBar extends SystemUI implements void handleNotificationError(IBinder key, StatusBarNotification n, String message) { removeNotification(key); try { - mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(), n.getInitialPid(), message); + mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(), + n.getInitialPid(), message, n.getUserId()); } catch (RemoteException ex) { // The end is nigh. } @@ -803,7 +993,7 @@ public abstract class BaseStatusBar extends SystemUI implements // Remove the expanded view. ViewGroup rowParent = (ViewGroup)entry.row.getParent(); if (rowParent != null) rowParent.removeView(entry.row); - updateExpansionStates(); + updateRowStates(); updateNotificationIcons(); return entry.notification; @@ -832,7 +1022,7 @@ public abstract class BaseStatusBar extends SystemUI implements } // Construct the expanded view. NotificationData.Entry entry = new NotificationData.Entry(key, notification, iconView); - if (!inflateViews(entry, mPile)) { + if (!inflateViews(entry, mStackScroller)) { handleNotificationError(key, notification, "Couldn't expand RemoteViews for: " + notification); return null; @@ -846,7 +1036,7 @@ public abstract class BaseStatusBar extends SystemUI implements if (DEBUG) { Log.d(TAG, "addNotificationViews: added at " + pos); } - updateExpansionStates(); + updateRowStates(); updateNotificationIcons(); } @@ -854,28 +1044,102 @@ public abstract class BaseStatusBar extends SystemUI implements addNotificationViews(createNotificationViews(key, notification)); } - protected void updateExpansionStates() { - int N = mNotificationData.size(); - for (int i = 0; i < N; i++) { + /** + * @return The number of notifications we show on Keyguard. + */ + protected abstract int getMaxKeyguardNotifications(); + + /** + * Updates expanded, dimmed and locked states of notification rows. + */ + protected void updateRowStates() { + int maxKeyguardNotifications = getMaxKeyguardNotifications(); + mKeyguardIconOverflowContainer.getIconsView().removeAllViews(); + int n = mNotificationData.size(); + int visibleNotifications = 0; + for (int i = n-1; i >= 0; i--) { + NotificationData.Entry entry = mNotificationData.get(i); + if (mOnKeyguard) { + entry.row.setExpansionDisabled(true); + } else { + entry.row.setExpansionDisabled(false); + if (!entry.row.isUserLocked()) { + boolean top = (i == n-1); + entry.row.setSystemExpanded(top); + } + } + entry.row.setDimmed(mOnKeyguard); + entry.row.setLocked(mOnKeyguard); + boolean showOnKeyguard = shouldShowOnKeyguard(entry.notification); + if (mOnKeyguard && (visibleNotifications >= maxKeyguardNotifications + || !showOnKeyguard)) { + entry.row.setVisibility(View.GONE); + if (showOnKeyguard) { + mKeyguardIconOverflowContainer.getIconsView().addNotification(entry); + } + } else { + if (entry.row.getVisibility() == View.GONE) { + // notify the scroller of a child addition + mStackScroller.generateAddAnimation(entry.row); + } + entry.row.setVisibility(View.VISIBLE); + visibleNotifications++; + } + } + + if (mOnKeyguard && mKeyguardIconOverflowContainer.getIconsView().getChildCount() > 0) { + mKeyguardIconOverflowContainer.setVisibility(View.VISIBLE); + } else { + mKeyguardIconOverflowContainer.setVisibility(View.GONE); + } + } + + @Override + public void onActivated(View view) { + int n = mNotificationData.size(); + for (int i = 0; i < n; i++) { NotificationData.Entry entry = mNotificationData.get(i); - if (!entry.row.isUserLocked()) { - if (i == (N-1)) { - if (DEBUG) Log.d(TAG, "expanding top notification at " + i); - entry.row.setExpanded(true); + if (entry.row.getVisibility() != View.GONE) { + if (view == entry.row) { + entry.row.getActivator().activate(); } else { - if (!entry.row.isUserExpanded()) { - if (DEBUG) Log.d(TAG, "collapsing notification at " + i); - entry.row.setExpanded(false); - } else { - if (DEBUG) Log.d(TAG, "ignoring user-modified notification at " + i); - } + entry.row.getActivator().activateInverse(); } + } + } + if (mKeyguardIconOverflowContainer.getVisibility() != View.GONE) { + if (view == mKeyguardIconOverflowContainer) { + mKeyguardIconOverflowContainer.getActivator().activate(); } else { - if (DEBUG) Log.d(TAG, "ignoring notification being held by user at " + i); + mKeyguardIconOverflowContainer.getActivator().activateInverse(); } } } + @Override + public void onReset(View view) { + int n = mNotificationData.size(); + for (int i = 0; i < n; i++) { + NotificationData.Entry entry = mNotificationData.get(i); + if (entry.row.getVisibility() != View.GONE) { + entry.row.getActivator().reset(); + } + } + if (mKeyguardIconOverflowContainer.getVisibility() != View.GONE) { + mKeyguardIconOverflowContainer.getActivator().reset(); + } + } + + private boolean shouldShowOnKeyguard(StatusBarNotification sbn) { + return sbn.getNotification().priority >= Notification.PRIORITY_LOW; + } + + protected void setZenMode(int mode) { + if (!isDeviceProvisioned()) return; + mZenMode = mode; + updateNotificationIcons(); + } + protected abstract void haltTicker(); protected abstract void setAreThereNotifications(); protected abstract void updateNotificationIcons(); @@ -904,6 +1168,14 @@ public abstract class BaseStatusBar extends SystemUI implements final RemoteViews contentView = notification.getNotification().contentView; final RemoteViews oldBigContentView = oldNotification.getNotification().bigContentView; final RemoteViews bigContentView = notification.getNotification().bigContentView; + final RemoteViews oldHeadsUpContentView = oldNotification.getNotification().headsUpContentView; + final RemoteViews headsUpContentView = notification.getNotification().headsUpContentView; + final Notification oldPublicNotification = oldNotification.getNotification().publicVersion; + final RemoteViews oldPublicContentView = oldPublicNotification != null + ? oldPublicNotification.contentView : null; + final Notification publicNotification = notification.getNotification().publicVersion; + final RemoteViews publicContentView = publicNotification != null + ? publicNotification.contentView : null; if (DEBUG) { Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when @@ -911,11 +1183,13 @@ public abstract class BaseStatusBar extends SystemUI implements + " expanded=" + oldEntry.expanded + " contentView=" + oldContentView + " bigContentView=" + oldBigContentView + + " publicView=" + oldPublicContentView + " rowParent=" + oldEntry.row.getParent()); Log.d(TAG, "new notification: when=" + notification.getNotification().when + " ongoing=" + oldNotification.isOngoing() + " contentView=" + contentView - + " bigContentView=" + bigContentView); + + " bigContentView=" + bigContentView + + " publicView=" + publicContentView); } // Can we just reapply the RemoteViews in place? If when didn't change, the order @@ -935,8 +1209,24 @@ public abstract class BaseStatusBar extends SystemUI implements && oldBigContentView.getPackage() != null && oldBigContentView.getPackage().equals(bigContentView.getPackage()) && oldBigContentView.getLayoutId() == bigContentView.getLayoutId()); + boolean headsUpContentsUnchanged = + (oldHeadsUpContentView == null && headsUpContentView == null) + || ((oldHeadsUpContentView != null && headsUpContentView != null) + && headsUpContentView.getPackage() != null + && oldHeadsUpContentView.getPackage() != null + && oldHeadsUpContentView.getPackage().equals(headsUpContentView.getPackage()) + && oldHeadsUpContentView.getLayoutId() == headsUpContentView.getLayoutId()); + boolean publicUnchanged = + (oldPublicContentView == null && publicContentView == null) + || ((oldPublicContentView != null && publicContentView != null) + && publicContentView.getPackage() != null + && oldPublicContentView.getPackage() != null + && oldPublicContentView.getPackage().equals(publicContentView.getPackage()) + && oldPublicContentView.getLayoutId() == publicContentView.getLayoutId()); + ViewGroup rowParent = (ViewGroup) oldEntry.row.getParent(); - boolean orderUnchanged = notification.getNotification().when== oldNotification.getNotification().when + boolean orderUnchanged = + notification.getNotification().when == oldNotification.getNotification().when && notification.getScore() == oldNotification.getScore(); // score now encompasses/supersedes isOngoing() @@ -944,7 +1234,8 @@ public abstract class BaseStatusBar extends SystemUI implements && !TextUtils.equals(notification.getNotification().tickerText, oldEntry.notification.getNotification().tickerText); boolean isTopAnyway = isTopNotification(rowParent, oldEntry); - if (contentsUnchanged && bigContentsUnchanged && (orderUnchanged || isTopAnyway)) { + if (contentsUnchanged && bigContentsUnchanged && headsUpContentsUnchanged && publicUnchanged + && (orderUnchanged || isTopAnyway)) { if (DEBUG) Log.d(TAG, "reusing notification for key: " + key); oldEntry.notification = notification; try { @@ -958,7 +1249,7 @@ public abstract class BaseStatusBar extends SystemUI implements } else { if (DEBUG) Log.d(TAG, "updating the current heads up:" + notification); mInterruptingNotificationEntry.notification = notification; - updateNotificationViews(mInterruptingNotificationEntry, notification); + updateHeadsUpViews(mInterruptingNotificationEntry, notification); } } @@ -972,7 +1263,7 @@ public abstract class BaseStatusBar extends SystemUI implements handleNotificationError(key, notification, "Couldn't update icon: " + ic); return; } - updateExpansionStates(); + updateRowStates(); } catch (RuntimeException e) { // It failed to add cleanly. Log, and remove the view from the panel. @@ -985,13 +1276,14 @@ public abstract class BaseStatusBar extends SystemUI implements if (DEBUG) Log.d(TAG, "contents was " + (contentsUnchanged ? "unchanged" : "changed")); if (DEBUG) Log.d(TAG, "order was " + (orderUnchanged ? "unchanged" : "changed")); if (DEBUG) Log.d(TAG, "notification is " + (isTopAnyway ? "top" : "not top")); - final boolean wasExpanded = oldEntry.row.isUserExpanded(); removeNotificationViews(key); addNotificationViews(key, notification); // will also replace the heads up - if (wasExpanded) { - final NotificationData.Entry newEntry = mNotificationData.findByKey(key); - newEntry.row.setExpanded(true); - newEntry.row.setUserExpanded(true); + final NotificationData.Entry newEntry = mNotificationData.findByKey(key); + final boolean userChangedExpansion = oldEntry.row.hasUserChangedExpansion(); + if (userChangedExpansion) { + boolean userExpanded = oldEntry.row.isUserExpanded(); + newEntry.row.setUserExpanded(userExpanded); + newEntry.row.applyExpansionToLayout(); } } @@ -1000,7 +1292,7 @@ public abstract class BaseStatusBar extends SystemUI implements updateNotificationVetoButton(oldEntry.row, notification); // Is this for you? - boolean isForCurrentUser = notificationIsForCurrentUser(notification); + boolean isForCurrentUser = notificationIsForCurrentProfiles(notification); if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you"); // Restart the ticker if it's still running @@ -1016,22 +1308,44 @@ public abstract class BaseStatusBar extends SystemUI implements private void updateNotificationViews(NotificationData.Entry entry, StatusBarNotification notification) { + updateNotificationViews(entry, notification, false); + } + + private void updateHeadsUpViews(NotificationData.Entry entry, + StatusBarNotification notification) { + updateNotificationViews(entry, notification, true); + } + + private void updateNotificationViews(NotificationData.Entry entry, + StatusBarNotification notification, boolean isHeadsUp) { final RemoteViews contentView = notification.getNotification().contentView; - final RemoteViews bigContentView = notification.getNotification().bigContentView; + final RemoteViews bigContentView = isHeadsUp + ? notification.getNotification().headsUpContentView + : notification.getNotification().bigContentView; + final Notification publicVersion = notification.getNotification().publicVersion; + final RemoteViews publicContentView = publicVersion != null ? publicVersion.contentView + : null; + // Reapply the RemoteViews contentView.reapply(mContext, entry.expanded, mOnClickHandler); if (bigContentView != null && entry.getBigContentView() != null) { - bigContentView.reapply(mContext, entry.getBigContentView(), mOnClickHandler); + bigContentView.reapply(mContext, entry.getBigContentView(), + mOnClickHandler); + } + if (publicContentView != null && entry.getPublicContentView() != null) { + publicContentView.reapply(mContext, entry.getPublicContentView(), mOnClickHandler); } // update the contentIntent final PendingIntent contentIntent = notification.getNotification().contentIntent; if (contentIntent != null) { final View.OnClickListener listener = makeClicker(contentIntent, - notification.getPackageName(), notification.getTag(), notification.getId()); - entry.content.setOnClickListener(listener); + notification.getPackageName(), notification.getTag(), notification.getId(), + isHeadsUp, notification.getUserId()); + entry.row.setOnClickListener(listener); } else { - entry.content.setOnClickListener(null); + entry.row.setOnClickListener(null); } + entry.row.notifyContentUpdated(); } protected void notifyHeadsUpScreenOn(boolean screenOn) { @@ -1049,14 +1363,15 @@ public abstract class BaseStatusBar extends SystemUI implements || notification.vibrate != null; boolean isHighPriority = sbn.getScore() >= INTERRUPTION_THRESHOLD; boolean isFullscreen = notification.fullScreenIntent != null; + boolean hasTicker = mHeadsUpTicker && !TextUtils.isEmpty(notification.tickerText); boolean isAllowed = notification.extras.getInt(Notification.EXTRA_AS_HEADS_UP, Notification.HEADS_UP_ALLOWED) != Notification.HEADS_UP_NEVER; final KeyguardTouchDelegate keyguard = KeyguardTouchDelegate.getInstance(mContext); - boolean interrupt = (isFullscreen || (isHighPriority && isNoisy)) + boolean interrupt = (isFullscreen || (isHighPriority && (isNoisy || hasTicker))) && isAllowed && mPowerManager.isScreenOn() - && !keyguard.isShowingAndNotHidden() + && !keyguard.isShowingAndNotOccluded() && !keyguard.isInputRestricted(); try { interrupt = interrupt && !mDreamManager.isDreaming(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 39333d7..bbbe8fa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -21,6 +21,7 @@ import android.os.IBinder; import android.os.Message; import android.service.notification.StatusBarNotification; +import com.android.internal.policy.IKeyguardShowCallback; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.statusbar.StatusBarIconList; @@ -98,6 +99,7 @@ public class CommandQueue extends IStatusBar.Stub { public void hideSearchPanel(); public void cancelPreloadRecentApps(); public void setWindowState(int window, int state); + } public CommandQueue(Callbacks callbacks, StatusBarIconList list) { @@ -232,6 +234,7 @@ public class CommandQueue extends IStatusBar.Stub { } } + private final class H extends Handler { public void handleMessage(Message msg) { final int what = msg.what & MSG_MASK; @@ -295,7 +298,7 @@ public class CommandQueue extends IStatusBar.Stub { mCallbacks.topAppWindowChanged(msg.arg1 != 0); break; case MSG_SHOW_IME_BUTTON: - mCallbacks.setImeWindowStatus((IBinder)msg.obj, msg.arg1, msg.arg2); + mCallbacks.setImeWindowStatus((IBinder) msg.obj, msg.arg1, msg.arg2); break; case MSG_SET_HARD_KEYBOARD_STATUS: mCallbacks.setHardKeyboardStatus(msg.arg1 != 0, msg.arg2 != 0); @@ -312,6 +315,7 @@ public class CommandQueue extends IStatusBar.Stub { case MSG_SET_WINDOW_STATE: mCallbacks.setWindowState(msg.arg1, msg.arg2); break; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index cd6495f..61aad6f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -18,29 +18,72 @@ package com.android.systemui.statusbar; import android.content.Context; import android.util.AttributeSet; -import android.view.ViewGroup; -import android.widget.FrameLayout; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; -public class ExpandableNotificationRow extends FrameLayout { - private int mRowHeight; +import com.android.systemui.R; - /** does this row contain layouts that can adapt to row expansion */ +public class ExpandableNotificationRow extends ActivatableNotificationView { + private int mRowMinHeight; + private int mRowMaxHeight; + + /** Does this row contain layouts that can adapt to row expansion */ private boolean mExpandable; - /** has the user manually expanded this row */ + /** 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; - /** is the user touching this row */ + /** Is the user touching this row */ private boolean mUserLocked; + /** Are we showing the "public" version */ + private boolean mShowingPublic; + + /** + * Is this notification expanded by the system. The expansion state can be overridden by the + * user expansion. + */ + private boolean mIsSystemExpanded; + + /** + * Whether the notification expansion is disabled. This is the case on Keyguard. + */ + private boolean mExpansionDisabled; + + private NotificationContentView mPublicLayout; + private NotificationContentView mPrivateLayout; + private int mMaxExpandHeight; + private NotificationActivator mActivator; public ExpandableNotificationRow(Context context, AttributeSet attrs) { super(context, attrs); } - public int getRowHeight() { - return mRowHeight; + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic); + mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded); + + mActivator = new NotificationActivator(this); + } + + @Override + public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { + if (super.onRequestSendAccessibilityEvent(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; } - public void setRowHeight(int rowHeight) { - this.mRowHeight = rowHeight; + public void setHeightRange(int rowMinHeight, int rowMaxHeight) { + mRowMinHeight = rowMinHeight; + mRowMaxHeight = rowMaxHeight; } public boolean isExpandable() { @@ -51,11 +94,24 @@ public class ExpandableNotificationRow extends FrameLayout { mExpandable = expandable; } + /** + * @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) { + mHasUserChangedExpansion = true; mUserExpanded = userExpanded; } @@ -67,13 +123,138 @@ public class ExpandableNotificationRow extends FrameLayout { mUserLocked = userLocked; } - public void setExpanded(boolean expand) { - ViewGroup.LayoutParams lp = getLayoutParams(); + /** + * @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) { + mIsSystemExpanded = expand; + applyExpansionToLayout(); + } + + /** + * @param expansionDisabled whether to prevent notification expansion + */ + public void setExpansionDisabled(boolean expansionDisabled) { + mExpansionDisabled = expansionDisabled; + applyExpansionToLayout(); + } + + /** + * Apply an expansion state to the layout. + */ + public void applyExpansionToLayout() { + boolean expand = isExpanded(); if (expand && mExpandable) { - lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; + setActualHeight(mMaxExpandHeight); } else { - lp.height = mRowHeight; + setActualHeight(mRowMinHeight); + } + } + + /** + * If {@link #isExpanded()} then this is the greatest possible height this view can + * get and otherwise it is {@link #mRowMinHeight}. + * + * @return the maximum allowed expansion height of this view. + */ + public int getMaximumAllowedExpandHeight() { + if (isUserLocked()) { + return getActualHeight(); + } + boolean inExpansionState = isExpanded(); + if (!inExpansionState) { + // not expanded, so we return the collapsed size + return mRowMinHeight; + } + + return mShowingPublic ? mRowMinHeight : getMaxExpandHeight(); + } + + /** + * 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. + */ + private boolean isExpanded() { + return !mExpansionDisabled + && (!hasUserChangedExpansion() && isSystemExpanded() || isUserExpanded()); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + boolean updateExpandHeight = mMaxExpandHeight == 0; + mMaxExpandHeight = mPrivateLayout.getMaxHeight(); + if (updateExpandHeight) { + applyExpansionToLayout(); } - setLayoutParams(lp); + } + + public void setShowingPublic(boolean show) { + mShowingPublic = show; + + // bail out if no public version + if (mPublicLayout.getChildCount() == 0) return; + + // TODO: animation? + mPublicLayout.setVisibility(show ? View.VISIBLE : View.GONE); + mPrivateLayout.setVisibility(show ? View.GONE : View.VISIBLE); + } + + /** + * Sets the notification as dimmed, meaning that it will appear in a more gray variant. + */ + public void setDimmed(boolean dimmed) { + super.setDimmed(dimmed); + mActivator.setDimmed(dimmed); + } + + public int getMaxExpandHeight() { + return mMaxExpandHeight; + } + + public NotificationActivator getActivator() { + return mActivator; + } + + /** + * @return the potential height this view could expand in addition. + */ + public int getExpandPotential() { + return getMaximumAllowedExpandHeight() - getActualHeight(); + } + + @Override + public void setActualHeight(int height) { + mPrivateLayout.setActualHeight(height); + invalidate(); + super.setActualHeight(height); + } + + @Override + public int getMaxHeight() { + return mPrivateLayout.getMaxHeight(); + } + + @Override + public void setClipTopAmount(int clipTopAmount) { + super.setClipTopAmount(clipTopAmount); + mPrivateLayout.setClipTopAmount(clipTopAmount); + } + + public void notifyContentUpdated() { + mPrivateLayout.notifyContentUpdated(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java new file mode 100644 index 0000000..43eb5b5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.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; + +import android.content.Context; +import android.graphics.Outline; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * Like {@link ExpandableView}, but setting an outline for the height and clipping. + */ +public abstract class ExpandableOutlineView extends ExpandableView { + + private final Outline mOutline = new Outline(); + + public ExpandableOutlineView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void setActualHeight(int actualHeight) { + super.setActualHeight(actualHeight); + updateOutline(); + } + + @Override + public void setClipTopAmount(int clipTopAmount) { + super.setClipTopAmount(clipTopAmount); + updateOutline(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateOutline(); + } + + private void updateOutline() { + mOutline.setRect(0, + mClipTopAmount, + getWidth(), + Math.max(mActualHeight, mClipTopAmount)); + setOutline(mOutline); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java new file mode 100644 index 0000000..35913fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +/** + * An abstract view for expandable views. + */ +public abstract class ExpandableView extends FrameLayout { + + private OnHeightChangedListener mOnHeightChangedListener; + protected int mActualHeight; + protected int mClipTopAmount; + protected Drawable mCustomBackground; + private boolean mActualHeightInitialized; + + public ExpandableView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onDraw(Canvas canvas) { + if (mCustomBackground != null) { + mCustomBackground.setBounds(0, mClipTopAmount, getWidth(), mActualHeight); + mCustomBackground.draw(canvas); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mCustomBackground; + } + + @Override + protected void drawableStateChanged() { + final Drawable d = mCustomBackground; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (!mActualHeightInitialized && mActualHeight == 0) { + mActualHeight = getHeight(); + } + mActualHeightInitialized = true; + } + + /** + * 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. + */ + public void setActualHeight(int actualHeight) { + mActualHeight = actualHeight; + invalidate(); + if (mOnHeightChangedListener != null) { + mOnHeightChangedListener.onHeightChanged(this); + } + } + + /** + * See {@link #setActualHeight}. + * + * @return The actual height of this notification. + */ + public int getActualHeight() { + return mActualHeight; + } + + /** + * @return The maximum height of this notification. + */ + public abstract int getMaxHeight(); + + /** + * 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; + invalidate(); + } + + public void setOnHeightChangedListener(OnHeightChangedListener listener) { + mOnHeightChangedListener = listener; + } + + /** + * Sets a custom background drawable. As we need to change our bounds independently of layout, + * we need the notition of a custom background. + */ + public void setCustomBackground(Drawable customBackground) { + if (mCustomBackground != null) { + mCustomBackground.setCallback(null); + unscheduleDrawable(mCustomBackground); + } + mCustomBackground = customBackground; + mCustomBackground.setCallback(this); + setWillNotDraw(customBackground == null); + invalidate(); + } + + public void setCustomBackgroundResource(int drawableResId) { + setCustomBackground(getResources().getDrawable(drawableResId)); + } + + /** + * A listener notifying when {@link #getActualHeight} changes. + */ + public interface OnHeightChangedListener { + void onHeightChanged(ExpandableView view); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/InterceptedNotifications.java b/packages/SystemUI/src/com/android/systemui/statusbar/InterceptedNotifications.java new file mode 100644 index 0000000..6401695 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/InterceptedNotifications.java @@ -0,0 +1,119 @@ +/* + * 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.content.Context; +import android.os.Binder; +import android.os.IBinder; +import android.os.Process; +import android.service.notification.StatusBarNotification; +import android.util.ArrayMap; +import android.view.View; + +import com.android.systemui.R; +import com.android.systemui.statusbar.NotificationData.Entry; +import com.android.systemui.statusbar.phone.PhoneStatusBar; + +public class InterceptedNotifications { + private static final String TAG = "InterceptedNotifications"; + private static final String EXTRA_INTERCEPT = "android.intercept"; + + private final Context mContext; + private final PhoneStatusBar mBar; + private final ArrayMap<IBinder, StatusBarNotification> mIntercepted + = new ArrayMap<IBinder, StatusBarNotification>(); + + private Binder mSynKey; + + public InterceptedNotifications(Context context, PhoneStatusBar bar) { + mContext = context; + mBar = bar; + } + + public void releaseIntercepted() { + final int n = mIntercepted.size(); + for (int i = 0; i < n; i++) { + final IBinder key = mIntercepted.keyAt(i); + final StatusBarNotification sbn = mIntercepted.valueAt(i); + sbn.getNotification().extras.putBoolean(EXTRA_INTERCEPT, false); + mBar.addNotification(key, sbn); + } + mIntercepted.clear(); + updateSyntheticNotification(); + } + + public boolean tryIntercept(IBinder key, StatusBarNotification notification) { + if (!notification.getNotification().extras.getBoolean(EXTRA_INTERCEPT)) return false; + mIntercepted.put(key, notification); + updateSyntheticNotification(); + return true; + } + + public void remove(IBinder key) { + if (mIntercepted.remove(key) != null) { + updateSyntheticNotification(); + } + } + + public boolean isSyntheticEntry(Entry ent) { + return mSynKey != null && ent.key.equals(mSynKey); + } + + public void update(IBinder key, StatusBarNotification notification) { + if (mIntercepted.containsKey(key)) { + mIntercepted.put(key, notification); + } + } + + private void updateSyntheticNotification() { + if (mIntercepted.isEmpty()) { + if (mSynKey != null) { + mBar.removeNotification(mSynKey); + mSynKey = null; + } + return; + } + final Notification n = new Notification.Builder(mContext) + .setSmallIcon(R.drawable.stat_sys_zen_limited) + .setContentTitle(mContext.getResources().getQuantityString( + R.plurals.zen_mode_notification_title, + mIntercepted.size(), mIntercepted.size())) + .setContentText(mContext.getString(R.string.zen_mode_notification_text)) + .setOngoing(true) + .build(); + final StatusBarNotification sbn = new StatusBarNotification(mContext.getPackageName(), + mContext.getBasePackageName(), + TAG.hashCode(), TAG, Process.myUid(), Process.myPid(), 0, n, + mBar.getCurrentUserHandle()); + if (mSynKey == null) { + mSynKey = new Binder(); + mBar.addNotification(mSynKey, sbn); + } else { + mBar.updateNotification(mSynKey, sbn); + } + final NotificationData.Entry entry = mBar.mNotificationData.findByKey(mSynKey); + entry.row.setOnClickListener(mSynClickListener); + } + + private final View.OnClickListener mSynClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + releaseIntercepted(); + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LatestItemView.java b/packages/SystemUI/src/com/android/systemui/statusbar/LatestItemView.java deleted file mode 100644 index 6419777..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LatestItemView.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.util.AttributeSet; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.widget.FrameLayout; - -public class LatestItemView extends FrameLayout { - public LatestItemView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void setOnClickListener(OnClickListener l) { - super.setOnClickListener(l); - } - - @Override - public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { - if (super.onRequestSendAccessibilityEvent(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; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java new file mode 100644 index 0000000..620e457 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar; + +import android.content.Context; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +import com.android.systemui.R; + +/** + * A helper class used by both {@link com.android.systemui.statusbar.ExpandableNotificationRow} and + * {@link com.android.systemui.statusbar.NotificationOverflowIconsView} to make a notification look + * active after tapping it once on the Keyguard. + */ +public class NotificationActivator { + + private static final int ANIMATION_LENGTH_MS = 220; + private static final float INVERSE_ALPHA = 0.9f; + private static final float DIMMED_SCALE = 0.95f; + + private final View mTargetView; + + private final Interpolator mFastOutSlowInInterpolator; + private final Interpolator mLinearOutSlowInInterpolator; + private final int mTranslationZ; + + public NotificationActivator(View targetView) { + mTargetView = targetView; + Context ctx = targetView.getContext(); + mFastOutSlowInInterpolator = + AnimationUtils.loadInterpolator(ctx, android.R.interpolator.fast_out_slow_in); + mLinearOutSlowInInterpolator = + AnimationUtils.loadInterpolator(ctx, android.R.interpolator.linear_out_slow_in); + mTranslationZ = + ctx.getResources().getDimensionPixelSize(R.dimen.z_distance_between_notifications); + mTargetView.animate().setDuration(ANIMATION_LENGTH_MS); + } + + public void activateInverse() { + mTargetView.animate().withLayer().alpha(INVERSE_ALPHA); + } + + public void activate() { + mTargetView.animate() + .setInterpolator(mLinearOutSlowInInterpolator) + .scaleX(1) + .scaleY(1) + .translationZBy(mTranslationZ); + } + + public void reset() { + mTargetView.animate() + .setInterpolator(mFastOutSlowInInterpolator) + .scaleX(DIMMED_SCALE) + .scaleY(DIMMED_SCALE) + .translationZBy(-mTranslationZ); + if (mTargetView.getAlpha() != 1.0f) { + mTargetView.animate().withLayer().alpha(1); + } + } + + public void setDimmed(boolean dimmed) { + if (dimmed) { + mTargetView.setScaleX(DIMMED_SCALE); + mTargetView.setScaleY(DIMMED_SCALE); + } else { + mTargetView.setScaleX(1); + mTargetView.setScaleY(1); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java new file mode 100644 index 0000000..fd0cb08 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java @@ -0,0 +1,128 @@ +/* + * 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.Gravity; +import android.view.View; + +import com.android.systemui.R; + +/** + * A frame layout containing the actual payload of the notification, including the contracted and + * expanded layout. This class is responsible for clipping the content and and switching between the + * expanded and contracted view depending on its clipped size. + */ +public class NotificationContentView extends ExpandableView { + + private final Rect mClipBounds = new Rect(); + + private View mContractedChild; + private View mExpandedChild; + + private int mSmallHeight; + + public NotificationContentView(Context context, AttributeSet attrs) { + super(context, attrs); + mSmallHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); + mActualHeight = mSmallHeight; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateClipping(); + } + + public void setContractedChild(View child) { + if (mContractedChild != null) { + removeView(mContractedChild); + } + sanitizeContractedLayoutParams(child); + addView(child); + mContractedChild = child; + selectLayout(); + } + + public void setExpandedChild(View child) { + if (mExpandedChild != null) { + removeView(mExpandedChild); + } + addView(child); + mExpandedChild = child; + selectLayout(); + } + + @Override + public void setActualHeight(int actualHeight) { + super.setActualHeight(actualHeight); + selectLayout(); + updateClipping(); + } + + @Override + public int getMaxHeight() { + + // The maximum height is just the laid out height. + return getHeight(); + } + + @Override + public void setClipTopAmount(int clipTopAmount) { + super.setClipTopAmount(clipTopAmount); + updateClipping(); + } + + public int getClipTopAmount() { + return mClipTopAmount; + } + + private void updateClipping() { + mClipBounds.set(0, mClipTopAmount, getWidth(), mActualHeight); + setClipBounds(mClipBounds); + } + + private void sanitizeContractedLayoutParams(View contractedChild) { + LayoutParams lp = (LayoutParams) contractedChild.getLayoutParams(); + lp.height = mSmallHeight; + contractedChild.setLayoutParams(lp); + } + + private void selectLayout() { + if (mActualHeight <= mSmallHeight || mExpandedChild == null) { + if (mContractedChild.getVisibility() != View.VISIBLE) { + mContractedChild.setVisibility(View.VISIBLE); + } + if (mExpandedChild != null && mExpandedChild.getVisibility() != View.INVISIBLE) { + mExpandedChild.setVisibility(View.INVISIBLE); + } + } else { + if (mExpandedChild.getVisibility() != View.VISIBLE) { + mExpandedChild.setVisibility(View.VISIBLE); + } + if (mContractedChild.getVisibility() != View.INVISIBLE) { + mContractedChild.setVisibility(View.INVISIBLE); + } + } + } + + public void notifyContentUpdated() { + selectLayout(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java index 5264998..6b6f55a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java @@ -21,8 +21,6 @@ import android.service.notification.StatusBarNotification; import android.view.View; import android.widget.ImageView; -import com.android.systemui.R; - import java.util.ArrayList; import java.util.Comparator; @@ -35,8 +33,8 @@ public class NotificationData { public StatusBarNotification notification; public StatusBarIconView icon; public ExpandableNotificationRow row; // the outer expanded view - public View content; // takes the click events and sends the PendingIntent public View expanded; // the inflated RemoteViews + public View expandedPublic; // for insecure lockscreens public ImageView largeIcon; private View expandedBig; private boolean interruption; @@ -53,6 +51,7 @@ public class NotificationData { public View getBigContentView() { return expandedBig; } + public View getPublicContentView() { return expandedPublic; } /** * Set the flag indicating that this is being touched by the user. */ @@ -71,9 +70,9 @@ public class NotificationData { final StatusBarNotification na = a.notification; final StatusBarNotification nb = b.notification; int d = na.getScore() - nb.getScore(); - if (a.interruption != b.interruption) { - return a.interruption ? 1 : -1; - } else if (d != 0) { + if (a.interruption != b.interruption) { + return a.interruption ? 1 : -1; + } else if (d != 0) { return d; } else { return (int) (na.getNotification().when - nb.getNotification().when); @@ -110,19 +109,6 @@ public class NotificationData { return i; } - public int add(IBinder key, StatusBarNotification notification, ExpandableNotificationRow row, - View content, View expanded, StatusBarIconView icon) { - Entry entry = new Entry(); - entry.key = key; - entry.notification = notification; - entry.row = row; - entry.content = content; - entry.expanded = expanded; - entry.icon = icon; - entry.largeIcon = null; // TODO add support for large icons - return add(entry); - } - public Entry remove(IBinder key) { Entry e = findByKey(key); if (e != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java new file mode 100644 index 0000000..8ebd50d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.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.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; + +import com.android.systemui.R; + +/** + * Container view for overflowing notification icons on Keyguard. + */ +public class NotificationOverflowContainer extends ActivatableNotificationView { + + private NotificationOverflowIconsView mIconsView; + private NotificationActivator mActivator; + + public NotificationOverflowContainer(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void setActualHeight(int currentHeight) { + // noop + } + + @Override + public int getActualHeight() { + return getHeight(); + } + + @Override + public int getMaxHeight() { + return getHeight(); + } + + @Override + public void setClipTopAmount(int clipTopAmount) { + // noop + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mIconsView = (NotificationOverflowIconsView) findViewById(R.id.overflow_icons_view); + mIconsView.setMoreText((TextView) findViewById(R.id.more_text)); + + mActivator = new NotificationActivator(this); + mActivator.setDimmed(true); + setLocked(true); + setDimmed(true); + } + + public NotificationOverflowIconsView getIconsView() { + return mIconsView; + } + + public NotificationActivator getActivator() { + return mActivator; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowIconsView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowIconsView.java new file mode 100644 index 0000000..ce31894 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowIconsView.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; + +import android.app.Notification; +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.statusbar.StatusBarIcon; +import com.android.systemui.R; +import com.android.systemui.statusbar.phone.IconMerger; + +/** + * A view to display all the overflowing icons on Keyguard. + */ +public class NotificationOverflowIconsView extends IconMerger { + + private TextView mMoreText; + private int mTintColor; + + public NotificationOverflowIconsView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTintColor = getResources().getColor(R.color.keyguard_overflow_content_color); + } + + public void setMoreText(TextView moreText) { + mMoreText = moreText; + } + + public void addNotification(NotificationData.Entry notification) { + StatusBarIconView v = new StatusBarIconView(getContext(), "", + notification.notification.getNotification()); + v.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + v.setColorFilter(mTintColor, PorterDuff.Mode.MULTIPLY); + addView(v); + v.set(notification.icon.getStatusBarIcon()); + updateMoreText(); + } + + private void updateMoreText() { + mMoreText.setText(getResources().getQuantityString( + R.plurals.keyguard_more_overflow_text, getChildCount(), getChildCount())); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java index f1c8e01..89da08f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar; import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.util.AttributeSet; import android.util.Log; import android.view.View; @@ -35,11 +37,14 @@ public class SignalClusterView static final boolean DEBUG = false; static final String TAG = "SignalClusterView"; + static final PorterDuffColorFilter PROBLEM_FILTER + = new PorterDuffColorFilter(0xffab653b, PorterDuff.Mode.SRC_ATOP); NetworkController mNC; private boolean mWifiVisible = false; private int mWifiStrengthId = 0; + private boolean mInetProblem; private boolean mMobileVisible = false; private int mMobileStrengthId = 0, mMobileTypeId = 0; private boolean mIsAirplaneMode = false; @@ -96,19 +101,22 @@ public class SignalClusterView } @Override - public void setWifiIndicators(boolean visible, int strengthIcon, String contentDescription) { + public void setWifiIndicators(boolean visible, int strengthIcon, boolean problem, + String contentDescription) { mWifiVisible = visible; mWifiStrengthId = strengthIcon; + mInetProblem = problem; mWifiDescription = contentDescription; apply(); } @Override - public void setMobileDataIndicators(boolean visible, int strengthIcon, + public void setMobileDataIndicators(boolean visible, int strengthIcon, boolean problem, int typeIcon, String contentDescription, String typeContentDescription) { mMobileVisible = visible; mMobileStrengthId = strengthIcon; + mInetProblem = problem; mMobileTypeId = typeIcon; mMobileDescription = contentDescription; mMobileTypeDescription = typeContentDescription; @@ -158,13 +166,17 @@ public class SignalClusterView apply(); } + private void applyInetProblem(ImageView iv) { + iv.setColorFilter(mInetProblem ? PROBLEM_FILTER : null); + } + // Run after each indicator change. private void apply() { if (mWifiGroup == null) return; if (mWifiVisible) { mWifi.setImageResource(mWifiStrengthId); - + applyInetProblem(mWifi); mWifiGroup.setContentDescription(mWifiDescription); mWifiGroup.setVisibility(View.VISIBLE); } else { @@ -179,7 +191,7 @@ public class SignalClusterView if (mMobileVisible && !mIsAirplaneMode) { mMobile.setImageResource(mMobileStrengthId); mMobileType.setImageResource(mMobileTypeId); - + applyInetProblem(mMobile); mMobileGroup.setContentDescription(mMobileTypeDescription + " " + mMobileDescription); mMobileGroup.setVisibility(View.VISIBLE); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index 9f9524b..6f839bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -119,7 +119,7 @@ public class StatusBarIconView extends AnimatedImageView { } if (!numberEquals) { - if (icon.number > 0 && mContext.getResources().getBoolean( + if (icon.number > 0 && getContext().getResources().getBoolean( R.bool.config_statusBarShowNumber)) { if (mNumberBackground == null) { mNumberBackground = getContext().getResources().getDrawable( @@ -240,10 +240,10 @@ public class StatusBarIconView extends AnimatedImageView { void placeNumber() { final String str; - final int tooBig = mContext.getResources().getInteger( + final int tooBig = getContext().getResources().getInteger( android.R.integer.status_bar_notification_info_maxnum); if (mIcon.number > tooBig) { - str = mContext.getResources().getString( + str = getContext().getResources().getString( android.R.string.status_bar_notification_info_overflow); } else { NumberFormat f = NumberFormat.getIntegerInstance(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index aba7afa..a3cf0f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -143,7 +143,7 @@ public class DemoStatusIcons extends LinearLayout implements DemoMode { } } StatusBarIcon icon = new StatusBarIcon(iconPkg, UserHandle.CURRENT, iconId, 0, 0, "Demo"); - StatusBarIconView v = new StatusBarIconView(mContext, null); + StatusBarIconView v = new StatusBarIconView(getContext(), null); v.setTag(slot); v.set(icon); addView(v, 0, new LinearLayout.LayoutParams(mIconSize, mIconSize)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java new file mode 100644 index 0000000..3cc22ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -0,0 +1,232 @@ +/* + * 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.ActivityManagerNative; +import android.app.admin.DevicePolicyManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.MediaStore; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; + +import com.android.systemui.R; + +/** + * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status + * text. + */ +public class KeyguardBottomAreaView extends FrameLayout { + + final static String TAG = "PhoneStatusBar/KeyguardBottomAreaView"; + + private View mCameraButton; + private float mCameraDragDistance; + private PowerManager mPowerManager; + private int mScaledTouchSlop; + + public KeyguardBottomAreaView(Context context) { + super(context); + } + + public KeyguardBottomAreaView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public KeyguardBottomAreaView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public KeyguardBottomAreaView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mCameraButton = findViewById(R.id.camera_button); + watchForDevicePolicyChanges(); + watchForAccessibilityChanges(); + updateCameraVisibility(); + mCameraDragDistance = getResources().getDimension(R.dimen.camera_drag_distance); + mScaledTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + } + + private void updateCameraVisibility() { + boolean visible = !isCameraDisabledByDpm(); + mCameraButton.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + private boolean isCameraDisabledByDpm() { + final DevicePolicyManager dpm = + (DevicePolicyManager) getContext().getSystemService(Context.DEVICE_POLICY_SERVICE); + if (dpm != null) { + try { + final int userId = ActivityManagerNative.getDefault().getCurrentUser().id; + final int disabledFlags = dpm.getKeyguardDisabledFeatures(null, userId); + final boolean disabledBecauseKeyguardSecure = + (disabledFlags & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) != 0 + && KeyguardTouchDelegate.getInstance(getContext()).isSecure(); + return dpm.getCameraDisabled(null) || disabledBecauseKeyguardSecure; + } catch (RemoteException e) { + Log.e(TAG, "Can't get userId", e); + } + } + return false; + } + + private void watchForDevicePolicyChanges() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); + getContext().registerReceiver(new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + post(new Runnable() { + @Override + public void run() { + updateCameraVisibility(); + } + }); + } + }, filter); + } + + private void watchForAccessibilityChanges() { + final AccessibilityManager am = + (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + + // Set the initial state + enableAccessibility(am.isTouchExplorationEnabled()); + + // Watch for changes + am.addTouchExplorationStateChangeListener( + new AccessibilityManager.TouchExplorationStateChangeListener() { + @Override + public void onTouchExplorationStateChanged(boolean enabled) { + enableAccessibility(enabled); + } + }); + } + + private void enableAccessibility(boolean touchExplorationEnabled) { + + // Add a touch handler or accessibility click listener for camera button. + if (touchExplorationEnabled) { + mCameraButton.setOnTouchListener(null); + mCameraButton.setOnClickListener(mCameraClickListener); + } else { + mCameraButton.setOnTouchListener(mCameraTouchListener); + mCameraButton.setOnClickListener(null); + } + } + + private void launchCamera() { + mContext.startActivityAsUser( + new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE), + UserHandle.CURRENT); + } + + private final OnClickListener mCameraClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + launchCamera(); + } + }; + + private final OnTouchListener mCameraTouchListener = new OnTouchListener() { + private float mStartX; + private boolean mTouchSlopReached; + private boolean mSkipCancelAnimation; + + @Override + public boolean onTouch(final View cameraButtonView, MotionEvent event) { + float realX = event.getRawX(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mStartX = realX; + mTouchSlopReached = false; + mSkipCancelAnimation = false; + break; + case MotionEvent.ACTION_MOVE: + if (realX > mStartX) { + realX = mStartX; + } + if (realX < mStartX - mCameraDragDistance) { + cameraButtonView.setPressed(true); + mPowerManager.userActivity(event.getEventTime(), false); + } else { + cameraButtonView.setPressed(false); + } + if (realX < mStartX - mScaledTouchSlop) { + mTouchSlopReached = true; + } + cameraButtonView.setTranslationX(Math.max(realX - mStartX, + -mCameraDragDistance)); + break; + case MotionEvent.ACTION_UP: + if (realX < mStartX - mCameraDragDistance) { + launchCamera(); + cameraButtonView.animate().x(-cameraButtonView.getWidth()) + .setInterpolator(new AccelerateInterpolator(2f)).withEndAction( + new Runnable() { + @Override + public void run() { + cameraButtonView.setTranslationX(0); + } + }); + mSkipCancelAnimation = true; + } + if (realX < mStartX - mScaledTouchSlop) { + mTouchSlopReached = true; + } + if (!mTouchSlopReached) { + mSkipCancelAnimation = true; + cameraButtonView.animate().translationX(-mCameraDragDistance / 2). + setInterpolator(new DecelerateInterpolator()).withEndAction( + new Runnable() { + @Override + public void run() { + cameraButtonView.animate().translationX(0). + setInterpolator(new AccelerateInterpolator()); + } + }); + } + case MotionEvent.ACTION_CANCEL: + cameraButtonView.setPressed(false); + if (!mSkipCancelAnimation) { + cameraButtonView.animate().translationX(0) + .setInterpolator(new AccelerateInterpolator(2f)); + } + break; + } + return true; + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java new file mode 100644 index 0000000..1ffb4ee --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.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.phone; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardViewBase; +import com.android.keyguard.R; +import com.android.keyguard.ViewMediatorCallback; +import com.android.systemui.keyguard.KeyguardViewMediator; + +import static com.android.keyguard.KeyguardSecurityModel.*; + +/** + * A class which manages the bouncer on the lockscreen. + */ +public class KeyguardBouncer { + + private Context mContext; + private ViewMediatorCallback mCallback; + private LockPatternUtils mLockPatternUtils; + private ViewGroup mContainer; + private StatusBarWindowManager mWindowManager; + private KeyguardViewBase mKeyguardView; + private ViewGroup mRoot; + + public KeyguardBouncer(Context context, ViewMediatorCallback callback, + LockPatternUtils lockPatternUtils, StatusBarWindowManager windowManager, + ViewGroup container) { + mContext = context; + mCallback = callback; + mLockPatternUtils = lockPatternUtils; + mContainer = container; + mWindowManager = windowManager; + } + + public void prepare() { + ensureView(); + } + + public void show() { + ensureView(); + + // Try to dismiss the Keyguard. If no security pattern is set, this will dismiss the whole + // Keyguard. If we need to authenticate, show the bouncer. + if (!mKeyguardView.dismiss()) { + mRoot.setVisibility(View.VISIBLE); + mKeyguardView.requestFocus(); + mKeyguardView.onResume(); + } + } + + public void hide() { + if (mKeyguardView != null) { + mKeyguardView.cleanUp(); + } + removeView(); + } + + /** + * Reset the state of the view. + */ + public void reset() { + inflateView(); + } + + public void onScreenTurnedOff() { + if (mKeyguardView != null && mRoot != null && mRoot.getVisibility() == View.VISIBLE) { + mKeyguardView.onPause(); + } + } + + public long getUserActivityTimeout() { + if (mKeyguardView != null) { + long timeout = mKeyguardView.getUserActivityTimeout(); + if (timeout >= 0) { + return timeout; + } + } + return KeyguardViewMediator.AWAKE_INTERVAL_DEFAULT_MS; + } + + public boolean isShowing() { + return mRoot != null && mRoot.getVisibility() == View.VISIBLE; + } + + private void ensureView() { + if (mRoot == null) { + inflateView(); + } + } + + private void inflateView() { + removeView(); + mRoot = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.keyguard_bouncer, null); + mKeyguardView = (KeyguardViewBase) mRoot.findViewById(R.id.keyguard_host_view); + mKeyguardView.setLockPatternUtils(mLockPatternUtils); + mKeyguardView.setViewMediatorCallback(mCallback); + mContainer.addView(mRoot, mContainer.getChildCount()); + mRoot.setVisibility(View.INVISIBLE); + mRoot.setSystemUiVisibility(View.STATUS_BAR_DISABLE_HOME); + } + + private 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 current security method should be shown before showing + * the notifications on Keyguard, like SIM PIN/PUK. + */ + public boolean needsFullscreenBouncer() { + if (mKeyguardView != null) { + SecurityMode mode = mKeyguardView.getSecurityMode(); + return mode == SecurityMode.SimPin + || mode == SecurityMode.SimPuk; + } + return false; + } + + public boolean isSecure() { + return mKeyguardView == null || mKeyguardView.getSecurityMode() != SecurityMode.None; + } + + public boolean onMenuPressed() { + ensureView(); + if (mKeyguardView.handleMenuKey()) { + + // We need to show it in case it is secure. If not, it will get dismissed in any case. + mRoot.setVisibility(View.VISIBLE); + mKeyguardView.requestFocus(); + mKeyguardView.onResume(); + return true; + } else { + return false; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java new file mode 100644 index 0000000..769b1b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java @@ -0,0 +1,62 @@ +/* + * 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.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. + setText(text); + } + + /** + * See {@link #switchIndication}. + */ + public void switchIndication(int textResId) { + switchIndication(getResources().getText(textResId)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java index c1646ba..754075a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java @@ -26,10 +26,11 @@ import android.os.UserHandle; import android.util.Slog; import android.view.MotionEvent; -import com.android.internal.policy.IKeyguardExitCallback; -import com.android.internal.policy.IKeyguardShowCallback; import com.android.internal.policy.IKeyguardService; +import java.util.ArrayList; +import java.util.List; + /** * Facilitates event communication between navigation bar and keyguard. Currently used to @@ -38,10 +39,12 @@ import com.android.internal.policy.IKeyguardService; */ public class KeyguardTouchDelegate { // TODO: propagate changes to these to {@link KeyguardServiceDelegate} - static final String KEYGUARD_PACKAGE = "com.android.keyguard"; - static final String KEYGUARD_CLASS = "com.android.keyguard.KeyguardService"; + static final String KEYGUARD_PACKAGE = "com.android.systemui"; + static final String KEYGUARD_CLASS = "com.android.systemui.keyguard.KeyguardService"; private static KeyguardTouchDelegate sInstance; + private static final List<OnKeyguardConnectionListener> sConnectionListeners = + new ArrayList<OnKeyguardConnectionListener>(); private volatile IKeyguardService mService; @@ -54,6 +57,10 @@ public class KeyguardTouchDelegate { Slog.v(TAG, "Connected to keyguard"); mService = IKeyguardService.Stub.asInterface(service); + for (int i = 0; i < sConnectionListeners.size(); i++) { + OnKeyguardConnectionListener listener = sConnectionListeners.get(i); + listener.onKeyguardServiceConnected(KeyguardTouchDelegate.this); + } } @Override @@ -61,6 +68,11 @@ public class KeyguardTouchDelegate { Slog.v(TAG, "Disconnected from keyguard"); mService = null; sInstance = null; // force reconnection if this goes away + + for (int i = 0; i < sConnectionListeners.size(); i++) { + OnKeyguardConnectionListener listener = sConnectionListeners.get(i); + listener.onKeyguardServiceDisconnected(KeyguardTouchDelegate.this); + } } }; @@ -128,16 +140,16 @@ public class KeyguardTouchDelegate { return false; } - public boolean isShowingAndNotHidden() { + public boolean isShowingAndNotOccluded() { final IKeyguardService service = mService; if (service != null) { try { - return service.isShowingAndNotHidden(); + return service.isShowingAndNotOccluded(); } catch (RemoteException e) { Slog.w(TAG , "Remote Exception", e); } } else { - Slog.w(TAG, "isShowingAndNotHidden(): NO SERVICE!"); + Slog.w(TAG, "isShowingAndNotOccluded(): NO SERVICE!"); } return false; } @@ -184,4 +196,13 @@ public class KeyguardTouchDelegate { } } + public static void addListener(OnKeyguardConnectionListener listener) { + sConnectionListeners.add(listener); + } + + public interface OnKeyguardConnectionListener { + + void onKeyguardServiceConnected(KeyguardTouchDelegate keyguardTouchDelegate); + void onKeyguardServiceDisconnected(KeyguardTouchDelegate keyguardTouchDelegate); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java index a74230b..a0582ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java @@ -82,7 +82,6 @@ public final class NavigationBarTransitions extends BarTransitions { setKeyButtonViewQuiescentAlpha(mView.getMenuButton(), alpha, animate); setKeyButtonViewQuiescentAlpha(mView.getSearchLight(), KEYGUARD_QUIESCENT_ALPHA, animate); - setKeyButtonViewQuiescentAlpha(mView.getCameraButton(), KEYGUARD_QUIESCENT_ALPHA, animate); applyBackButtonQuiescentAlpha(mode, animate); @@ -98,7 +97,6 @@ public final class NavigationBarTransitions extends BarTransitions { public void applyBackButtonQuiescentAlpha(int mode, boolean animate) { float backAlpha = 0; backAlpha = maxVisibleQuiescentAlpha(backAlpha, mView.getSearchLight()); - backAlpha = maxVisibleQuiescentAlpha(backAlpha, mView.getCameraButton()); backAlpha = maxVisibleQuiescentAlpha(backAlpha, mView.getHomeButton()); backAlpha = maxVisibleQuiescentAlpha(backAlpha, mView.getRecentsButton()); backAlpha = maxVisibleQuiescentAlpha(backAlpha, mView.getMenuButton()); @@ -117,7 +115,7 @@ public final class NavigationBarTransitions extends BarTransitions { @Override public void setContentVisible(boolean visible) { final float alpha = visible ? 1 : 0; - fadeContent(mView.getCameraButton(), alpha); + fadeContent(mView.getBackButton(), alpha); fadeContent(mView.getSearchLight(), alpha); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 9589e8b..3fae3f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -21,20 +21,14 @@ import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; -import android.app.ActivityManagerNative; import android.app.StatusBarManager; -import android.app.admin.DevicePolicyManager; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Message; -import android.os.RemoteException; import android.util.AttributeSet; import android.util.Log; import android.view.Display; @@ -90,9 +84,6 @@ public class NavigationBarView extends LinearLayout { final static boolean WORKAROUND_INVALID_LAYOUT = true; final static int MSG_CHECK_INVALID_LAYOUT = 8686; - // used to disable the camera icon in navbar when disabled by DPM - private boolean mCameraDisabledByDpm; - // performs manual animation in sync with layout transitions private final NavTransitionListener mTransitionListener = new NavTransitionListener(); @@ -145,33 +136,12 @@ public class NavigationBarView extends LinearLayout { private final OnClickListener mAccessibilityClickListener = new OnClickListener() { @Override public void onClick(View v) { - if (v.getId() == R.id.camera_button) { - KeyguardTouchDelegate.getInstance(getContext()).launchCamera(); - } else if (v.getId() == R.id.search_light) { + if (v.getId() == R.id.search_light) { KeyguardTouchDelegate.getInstance(getContext()).showAssistant(); } } }; - private final OnTouchListener mCameraTouchListener = new OnTouchListener() { - @Override - public boolean onTouch(View cameraButtonView, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - // disable search gesture while interacting with camera - mDelegateHelper.setDisabled(true); - mBarTransitions.setContentVisible(false); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mDelegateHelper.setDisabled(false); - mBarTransitions.setContentVisible(true); - break; - } - return KeyguardTouchDelegate.getInstance(getContext()).dispatch(event); - } - }; - private class H extends Handler { public void handleMessage(Message m) { switch (m.what) { @@ -201,7 +171,7 @@ public class NavigationBarView extends LinearLayout { mDisplay = ((WindowManager)context.getSystemService( Context.WINDOW_SERVICE)).getDefaultDisplay(); - final Resources res = mContext.getResources(); + final Resources res = getContext().getResources(); mBarSize = res.getDimensionPixelSize(R.dimen.navigation_bar_size); mVertical = false; mShowMenu = false; @@ -210,24 +180,6 @@ public class NavigationBarView extends LinearLayout { getIcons(res); mBarTransitions = new NavigationBarTransitions(this); - - mCameraDisabledByDpm = isCameraDisabledByDpm(); - watchForDevicePolicyChanges(); - } - - private void watchForDevicePolicyChanges() { - final IntentFilter filter = new IntentFilter(); - filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); - mContext.registerReceiver(new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - post(new Runnable() { - @Override - public void run() { - mCameraDisabledByDpm = isCameraDisabledByDpm(); - } - }); - } - }, filter); } public BarTransitions getBarTransitions() { @@ -286,11 +238,6 @@ public class NavigationBarView extends LinearLayout { return mCurrentView.findViewById(R.id.search_light); } - // shown when keyguard is visible and camera is available - public View getCameraButton() { - return mCurrentView.findViewById(R.id.camera_button); - } - private void getIcons(Resources res) { mBackIcon = res.getDrawable(R.drawable.ic_sysbar_back); mBackLandIcon = res.getDrawable(R.drawable.ic_sysbar_back_land); @@ -302,7 +249,7 @@ public class NavigationBarView extends LinearLayout { @Override public void setLayoutDirection(int layoutDirection) { - getIcons(mContext.getResources()); + getIcons(getContext().getResources()); super.setLayoutDirection(layoutDirection); } @@ -323,7 +270,7 @@ public class NavigationBarView extends LinearLayout { mTransitionListener.onBackAltCleared(); } if (DEBUG) { - android.widget.Toast.makeText(mContext, + android.widget.Toast.makeText(getContext(), "Navigation icon hints = " + hints, 500).show(); } @@ -380,9 +327,7 @@ public class NavigationBarView extends LinearLayout { getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); final boolean showSearch = disableHome && !disableSearch; - final boolean showCamera = showSearch && !mCameraDisabledByDpm; setVisibleOrGone(getSearchLight(), showSearch); - setVisibleOrGone(getCameraButton(), showCamera); mBarTransitions.applyBackButtonQuiescentAlpha(mBarTransitions.getMode(), true /*animate*/); } @@ -393,24 +338,6 @@ public class NavigationBarView extends LinearLayout { } } - private boolean isCameraDisabledByDpm() { - final DevicePolicyManager dpm = - (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); - if (dpm != null) { - try { - final int userId = ActivityManagerNative.getDefault().getCurrentUser().id; - final int disabledFlags = dpm.getKeyguardDisabledFeatures(null, userId); - final boolean disabledBecauseKeyguardSecure = - (disabledFlags & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) != 0 - && KeyguardTouchDelegate.getInstance(getContext()).isSecure(); - return dpm.getCameraDisabled(null) || disabledBecauseKeyguardSecure; - } catch (RemoteException e) { - Log.e(TAG, "Can't get userId", e); - } - } - return false; - } - public void setSlippery(boolean newSlippery) { WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams(); if (lp != null) { @@ -457,7 +384,7 @@ public class NavigationBarView extends LinearLayout { private void watchForAccessibilityChanges() { final AccessibilityManager am = - (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); // Set the initial state enableAccessibility(am.isTouchExplorationEnabled()); @@ -477,25 +404,12 @@ public class NavigationBarView extends LinearLayout { // Add a touch handler or accessibility click listener for camera and search buttons // for all view orientations. final OnClickListener onClickListener = touchEnabled ? mAccessibilityClickListener : null; - final OnTouchListener onTouchListener = touchEnabled ? null : mCameraTouchListener; - boolean hasCamera = false; for (int i = 0; i < mRotatedViews.length; i++) { - final View cameraButton = mRotatedViews[i].findViewById(R.id.camera_button); final View searchLight = mRotatedViews[i].findViewById(R.id.search_light); - if (cameraButton != null) { - hasCamera = true; - cameraButton.setOnTouchListener(onTouchListener); - cameraButton.setOnClickListener(onClickListener); - } if (searchLight != null) { searchLight.setOnClickListener(onClickListener); } } - if (hasCamera) { - // Warm up KeyguardTouchDelegate so it's ready by the time the camera button is touched. - // This will connect to KeyguardService so that touch events are processed. - KeyguardTouchDelegate.getInstance(mContext); - } } public boolean isVertical() { @@ -575,7 +489,7 @@ public class NavigationBarView extends LinearLayout { private String getResourceName(int resId) { if (resId != 0) { - final android.content.res.Resources res = mContext.getResources(); + final android.content.res.Resources res = getContext().getResources(); try { return res.getResourceName(resId); } catch (android.content.res.Resources.NotFoundException ex) { @@ -632,7 +546,6 @@ public class NavigationBarView extends LinearLayout { dumpButton(pw, "rcnt", getRecentsButton()); dumpButton(pw, "menu", getMenuButton()); dumpButton(pw, "srch", getSearchLight()); - dumpButton(pw, "cmra", getCameraButton()); pw.println(" }"); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index 6be6d4d..712eec8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -17,45 +17,68 @@ package com.android.systemui.statusbar.phone; import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; import android.util.AttributeSet; -import android.util.EventLog; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; -import com.android.systemui.EventLogTags; import com.android.systemui.R; +import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.GestureRecorder; +import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; -public class NotificationPanelView extends PanelView { +public class NotificationPanelView extends PanelView implements + ExpandableView.OnHeightChangedListener { public static final boolean DEBUG_GESTURES = true; - Drawable mHandleBar; - int mHandleBarHeight; - View mHandleView; - int mFingers; PhoneStatusBar mStatusBar; - boolean mOkToFlip; + private View mHeader; + private View mKeyguardStatusView; + + private NotificationStackScrollLayout mNotificationStackScroller; + private boolean mTrackingSettings; + private int mNotificationTopPadding; public NotificationPanelView(Context context, AttributeSet attrs) { super(context, attrs); } public void setStatusBar(PhoneStatusBar bar) { + if (mStatusBar != null) { + mStatusBar.setOnFlipRunnable(null); + } mStatusBar = bar; + if (bar != null) { + mStatusBar.setOnFlipRunnable(new Runnable() { + @Override + public void run() { + requestPanelHeightUpdate(); + } + }); + } } @Override protected void onFinishInflate() { super.onFinishInflate(); - Resources resources = getContext().getResources(); - mHandleBar = resources.getDrawable(R.drawable.status_bar_close); - mHandleBarHeight = resources.getDimensionPixelSize(R.dimen.close_handle_height); - mHandleView = findViewById(R.id.handle); + mHeader = findViewById(R.id.header); + mKeyguardStatusView = findViewById(R.id.keyguard_status_view); + mNotificationStackScroller = (NotificationStackScrollLayout) + findViewById(R.id.notification_stack_scroller); + mNotificationStackScroller.setOnHeightChangedListener(this); + mNotificationTopPadding = getResources().getDimensionPixelSize( + R.dimen.notifications_top_padding); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + int keyguardBottomMargin = + ((MarginLayoutParams) mKeyguardStatusView.getLayoutParams()).bottomMargin; + mNotificationStackScroller.setTopPadding(mStatusBar.isOnKeyguard() + ? mKeyguardStatusView.getBottom() + keyguardBottomMargin + : mHeader.getBottom() + mNotificationTopPadding); } @Override @@ -80,61 +103,88 @@ public class NotificationPanelView extends PanelView { return super.dispatchPopulateAccessibilityEvent(event); } - // We draw the handle ourselves so that it's always glued to the bottom of the window. @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (changed) { - final int pl = getPaddingLeft(); - final int pr = getPaddingRight(); - mHandleBar.setBounds(pl, 0, getWidth() - pr, (int) mHandleBarHeight); + public boolean onInterceptTouchEvent(MotionEvent event) { + // intercept for quick settings + if (event.getAction() == MotionEvent.ACTION_DOWN) { + final View target = mStatusBar.isOnKeyguard() ? mKeyguardStatusView : mHeader; + final boolean inTarget = PhoneStatusBar.inBounds(target, event, true); + if (inTarget && !isInSettings()) { + mTrackingSettings = true; + return true; + } + if (!inTarget && isInSettings()) { + mTrackingSettings = true; + return true; + } } + return super.onInterceptTouchEvent(event); } @Override - public void draw(Canvas canvas) { - super.draw(canvas); - final int off = (int) (getHeight() - mHandleBarHeight - getPaddingBottom()); - canvas.translate(0, off); - mHandleBar.setState(mHandleView.getDrawableState()); - mHandleBar.draw(canvas); - canvas.translate(0, -off); + public boolean onTouchEvent(MotionEvent event) { + // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference + // implementation. + if (mTrackingSettings) { + mStatusBar.onSettingsEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP + || event.getAction() == MotionEvent.ACTION_CANCEL) { + mTrackingSettings = false; + } + return true; + } + if (isInSettings()) { + return true; + } + return super.onTouchEvent(event); } @Override - public boolean onTouchEvent(MotionEvent event) { - if (DEBUG_GESTURES) { - if (event.getActionMasked() != MotionEvent.ACTION_MOVE) { - EventLog.writeEvent(EventLogTags.SYSUI_NOTIFICATIONPANEL_TOUCH, - event.getActionMasked(), (int) event.getX(), (int) event.getY()); - } + protected boolean isScrolledToBottom() { + if (!isInSettings()) { + return mNotificationStackScroller.isScrolledToBottom(); } - if (PhoneStatusBar.SETTINGS_DRAG_SHORTCUT && mStatusBar.mHasFlipSettings) { - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mOkToFlip = getExpandedHeight() == 0; - break; - case MotionEvent.ACTION_POINTER_DOWN: - if (mOkToFlip) { - float miny = event.getY(0); - float maxy = miny; - for (int i=1; i<event.getPointerCount(); i++) { - final float y = event.getY(i); - if (y < miny) miny = y; - if (y > maxy) maxy = y; - } - if (maxy - miny < mHandleBarHeight) { - if (getMeasuredHeight() < mHandleBarHeight) { - mStatusBar.switchToSettings(); - } else { - mStatusBar.flipToSettings(); - } - mOkToFlip = false; - } - } - break; - } + return super.isScrolledToBottom(); + } + + @Override + protected int getMaxPanelHeight() { + if (!isInSettings()) { + int maxPanelHeight = super.getMaxPanelHeight(); + int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin(); + return maxPanelHeight - emptyBottomMargin; } - return mHandleView.dispatchTouchEvent(event); + return super.getMaxPanelHeight(); + } + + private boolean isInSettings() { + return mStatusBar != null && mStatusBar.isFlippedToSettings(); + } + + @Override + protected void onHeightUpdated(float expandedHeight) { + mNotificationStackScroller.setStackHeight(expandedHeight); + } + + @Override + protected int getDesiredMeasureHeight() { + return mMaxPanelHeight; + } + + @Override + protected void onExpandingStarted() { + super.onExpandingStarted(); + mNotificationStackScroller.onExpansionStarted(); + } + + @Override + protected void onExpandingFinished() { + super.onExpandingFinished(); + mNotificationStackScroller.onExpansionStopped(); + } + + @Override + public void onHeightChanged(ExpandableView view) { + requestPanelHeightUpdate(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java index a3e35d1..324d6f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java @@ -151,7 +151,8 @@ public class PanelBar extends FrameLayout { if (DEBUG) LOG("panelExpansionChanged: start state=%d panel=%s", mState, panel.getName()); mPanelExpandedFractionSum = 0f; for (PanelView pv : mPanels) { - final boolean visible = pv.getVisibility() == View.VISIBLE; + boolean visible = pv.getExpandedHeight() > 0; + pv.setVisibility(visible ? View.VISIBLE : View.GONE); // adjust any other panels that may be partially visible if (pv.getExpandedHeight() > 0f) { if (mState == STATE_CLOSED) { @@ -166,11 +167,6 @@ public class PanelBar extends FrameLayout { if (thisFrac == 1f) fullyOpenedPanel = panel; } } - if (pv.getExpandedHeight() > 0f) { - if (!visible) pv.setVisibility(View.VISIBLE); - } else { - if (visible) pv.setVisibility(View.GONE); - } } mPanelExpandedFractionSum /= mPanels.size(); if (fullyOpenedPanel != null && !mTracking) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java index 4b2c3e1..328a172 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java @@ -25,6 +25,7 @@ import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.widget.FrameLayout; import com.android.systemui.R; @@ -45,7 +46,6 @@ public class PanelView extends FrameLayout { } public static final boolean BRAKES = false; - private boolean mRubberbandingEnabled = true; private float mSelfExpandVelocityPx; // classic value: 2000px/s private float mSelfCollapseVelocityPx; // classic value: 2000px/s (will be negated to collapse "up") @@ -67,16 +67,15 @@ public class PanelView extends FrameLayout { private float mExpandBrakingDistancePx = 150; // XXX Resource private float mBrakingSpeedPx = 150; // XXX Resource - private View mHandleView; private float mPeekHeight; - private float mTouchOffset; + private float mInitialOffsetOnTouch; private float mExpandedFraction = 0; private float mExpandedHeight = 0; private boolean mJustPeeked; private boolean mClosing; - private boolean mRubberbanding; private boolean mTracking; private int mTrackingPointer; + private int mTouchSlop; private TimeAnimator mTimeAnimator; private ObjectAnimator mPeekAnimator; @@ -198,7 +197,6 @@ public class PanelView extends FrameLayout { } } - private int[] mAbsPos = new int[2]; PanelBar mBar; private final TimeListener mAnimationCallback = new TimeListener() { @@ -213,20 +211,23 @@ public class PanelView extends FrameLayout { public void run() { if (mTimeAnimator != null && mTimeAnimator.isStarted()) { mTimeAnimator.end(); - mRubberbanding = false; mClosing = false; + onExpandingFinished(); } } }; private float mVel, mAccel; - private int mFullHeight = 0; + protected int mMaxPanelHeight = 0; private String mViewName; protected float mInitialTouchY; + protected float mInitialTouchX; protected float mFinalTouchY; - public void setRubberbandingEnabled(boolean enable) { - mRubberbandingEnabled = enable; + protected void onExpandingFinished() { + } + + protected void onExpandingStarted() { } private void runPeekAnimation() { @@ -252,14 +253,9 @@ public class PanelView extends FrameLayout { mTimeAnimator.start(); - mRubberbanding = mRubberbandingEnabled // is it enabled at all? - && mExpandedHeight > getFullHeight() // are we past the end? - && mVel >= -mFlingGestureMinDistPx; // was this not possibly a "close" gesture? - if (mRubberbanding) { - mClosing = true; - } else if (mVel == 0) { + if (mVel == 0) { // if the panel is less than halfway open, close it - mClosing = (mFinalTouchY / getFullHeight()) < 0.5f; + mClosing = (mFinalTouchY / getMaxPanelHeight()) < 0.5f; } else { mClosing = mExpandedHeight > 0 && mVel < 0; } @@ -268,7 +264,7 @@ public class PanelView extends FrameLayout { if (DEBUG) logf("tick: v=%.2fpx/s dt=%.4fs", mVel, dt); if (DEBUG) logf("tick: before: h=%d", (int) mExpandedHeight); - final float fh = getFullHeight(); + final float fh = getMaxPanelHeight(); boolean braking = false; if (BRAKES) { if (mClosing) { @@ -300,10 +296,6 @@ public class PanelView extends FrameLayout { float h = mExpandedHeight + mVel * dt; - if (mRubberbanding && h < fh) { - h = fh; - } - if (DEBUG) logf("tick: new h=%d closing=%s", (int) h, mClosing?"true":"false"); setExpandedHeightInternal(h); @@ -312,7 +304,7 @@ public class PanelView extends FrameLayout { if (mVel == 0 || (mClosing && mExpandedHeight == 0) - || ((mRubberbanding || !mClosing) && mExpandedHeight == fh)) { + || (!mClosing && mExpandedHeight == fh)) { post(mStopAnimator); } } else { @@ -326,6 +318,7 @@ public class PanelView extends FrameLayout { mTimeAnimator = new TimeAnimator(); mTimeAnimator.setTimeListener(mAnimationCallback); + setOnHierarchyChangeListener(mHierarchyListener); } private void loadDimens() { @@ -349,8 +342,10 @@ public class PanelView extends FrameLayout { mFlingGestureMaxOutputVelocityPx = res.getDimension(R.dimen.fling_gesture_max_output_velocity); mPeekHeight = res.getDimension(R.dimen.peek_height) - + getPaddingBottom() // our window might have a dropshadow - - (mHandleView == null ? 0 : mHandleView.getPaddingTop()); // the handle might have a topshadow + + getPaddingBottom(); // our window might have a dropshadow + + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); } private void trackMovement(MotionEvent event) { @@ -363,146 +358,229 @@ public class PanelView extends FrameLayout { event.offsetLocation(-deltaX, -deltaY); } - // Pass all touches along to the handle, allowing the user to drag the panel closed from its interior @Override public boolean onTouchEvent(MotionEvent event) { - return mHandleView.dispatchTouchEvent(event); + + /* + * 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 give him 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 y = event.getY(pointerIndex); + final float x = event.getX(pointerIndex); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mTracking = true; + + mInitialTouchY = y; + mInitialTouchX = x; + initVelocityTracker(); + trackMovement(event); + mTimeAnimator.cancel(); // end any outstanding animations + onTrackingStarted(); + mInitialOffsetOnTouch = mExpandedHeight; + if (mExpandedHeight == 0) { + mJustPeeked = true; + runPeekAnimation(); + } + 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); + mInitialOffsetOnTouch = mExpandedHeight; + mInitialTouchY = newY; + mInitialTouchX = newX; + } + break; + + case MotionEvent.ACTION_MOVE: + final float h = y - mInitialTouchY + mInitialOffsetOnTouch; + if (h > mPeekHeight) { + if (mPeekAnimator != null && mPeekAnimator.isStarted()) { + mPeekAnimator.cancel(); + } + mJustPeeked = false; + } + if (!mJustPeeked) { + setExpandedHeightInternal(h); + mBar.panelExpansionChanged(PanelView.this, mExpandedFraction); + } + + trackMovement(event); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mFinalTouchY = y; + mTracking = false; + mTrackingPointer = -1; + onTrackingStopped(); + trackMovement(event); + + float vel = getCurrentVelocity(); + fling(vel, true); + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + } + return true; + } + + protected void onTrackingStopped() { + mBar.onTrackingStopped(PanelView.this); + } + + protected void onTrackingStarted() { + mBar.onTrackingStarted(PanelView.this); + onExpandingStarted(); + } + + private float getCurrentVelocity() { + float vel = 0; + float yVel = 0, xVel = 0; + boolean negative = false; + + // the velocitytracker might be null if we got a bad input stream + if (mVelocityTracker == null) { + return 0; + } + + mVelocityTracker.computeCurrentVelocity(1000); + + yVel = mVelocityTracker.getYVelocity(); + negative = yVel < 0; + + xVel = mVelocityTracker.getXVelocity(); + if (xVel < 0) { + xVel = -xVel; + } + if (xVel > mFlingGestureMaxXVelocityPx) { + xVel = mFlingGestureMaxXVelocityPx; // limit how much we care about the x axis + } + + vel = (float) Math.hypot(yVel, xVel); + if (vel > mFlingGestureMaxOutputVelocityPx) { + vel = mFlingGestureMaxOutputVelocityPx; + } + + // if you've barely moved your finger, we treat the velocity as 0 + // preventing spurious flings due to touch screen jitter + final float deltaY = Math.abs(mFinalTouchY - mInitialTouchY); + if (deltaY < mFlingGestureMinDistPx + || vel < mFlingExpandMinVelocityPx + ) { + vel = 0; + } + + if (negative) { + vel = -vel; + } + + if (DEBUG) { + logf("gesture: dy=%f vel=(%f,%f) vlinear=%f", + deltaY, + xVel, yVel, + vel); + } + return vel; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + + /* + * If the user drags anywhere inside the panel we intercept it if he moves his finger + * 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: + mInitialTouchY = y; + mInitialTouchX = x; + initVelocityTracker(); + trackMovement(event); + mTimeAnimator.cancel(); // end any outstanding animations + 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 (scrolledToBottom) { + if (h < -mTouchSlop && h < -Math.abs(x - mInitialTouchX)) { + mInitialOffsetOnTouch = mExpandedHeight; + mInitialTouchY = y; + mInitialTouchX = x; + mTracking = true; + onTrackingStarted(); + return true; + } + } + break; + } + return false; + } + + private void initVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + } + mVelocityTracker = FlingTracker.obtain(); + } + + protected boolean isScrolledToBottom() { + return false; + } + + protected float getContentHeight() { + return mExpandedHeight; } @Override protected void onFinishInflate() { super.onFinishInflate(); - mHandleView = findViewById(R.id.handle); loadDimens(); - - if (DEBUG) logf("handle view: " + mHandleView); - if (mHandleView != null) { - mHandleView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - int pointerIndex = event.findPointerIndex(mTrackingPointer); - if (pointerIndex < 0) { - pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); - } - final float y = event.getY(pointerIndex); - final float rawDelta = event.getRawY() - event.getY(); - final float rawY = y + rawDelta; - if (DEBUG) logf("handle.onTouch: a=%s p=[%d,%d] y=%.1f rawY=%.1f off=%.1f", - MotionEvent.actionToString(event.getAction()), - mTrackingPointer, pointerIndex, - y, rawY, mTouchOffset); - PanelView.this.getLocationOnScreen(mAbsPos); - - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mTracking = true; - mHandleView.setPressed(true); - postInvalidate(); // catch the press state change - mInitialTouchY = y; - mVelocityTracker = FlingTracker.obtain(); - trackMovement(event); - mTimeAnimator.cancel(); // end any outstanding animations - mBar.onTrackingStarted(PanelView.this); - mTouchOffset = (rawY - mAbsPos[1]) - mExpandedHeight; - if (mExpandedHeight == 0) { - mJustPeeked = true; - runPeekAnimation(); - } - 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 newRawY = newY + rawDelta; - mTrackingPointer = event.getPointerId(newIndex); - mTouchOffset = (newRawY - mAbsPos[1]) - mExpandedHeight; - mInitialTouchY = newY; - } - break; - - case MotionEvent.ACTION_MOVE: - final float h = rawY - mAbsPos[1] - mTouchOffset; - if (h > mPeekHeight) { - if (mPeekAnimator != null && mPeekAnimator.isStarted()) { - mPeekAnimator.cancel(); - } - mJustPeeked = false; - } - if (!mJustPeeked) { - PanelView.this.setExpandedHeightInternal(h); - mBar.panelExpansionChanged(PanelView.this, mExpandedFraction); - } - - trackMovement(event); - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mFinalTouchY = y; - mTracking = false; - mTrackingPointer = -1; - mHandleView.setPressed(false); - postInvalidate(); // catch the press state change - mBar.onTrackingStopped(PanelView.this); - trackMovement(event); - - float vel = 0, yVel = 0, xVel = 0; - boolean negative = false; - - if (mVelocityTracker != null) { - // the velocitytracker might be null if we got a bad input stream - mVelocityTracker.computeCurrentVelocity(1000); - - yVel = mVelocityTracker.getYVelocity(); - negative = yVel < 0; - - xVel = mVelocityTracker.getXVelocity(); - if (xVel < 0) { - xVel = -xVel; - } - if (xVel > mFlingGestureMaxXVelocityPx) { - xVel = mFlingGestureMaxXVelocityPx; // limit how much we care about the x axis - } - - vel = (float)Math.hypot(yVel, xVel); - if (vel > mFlingGestureMaxOutputVelocityPx) { - vel = mFlingGestureMaxOutputVelocityPx; - } - - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - - // if you've barely moved your finger, we treat the velocity as 0 - // preventing spurious flings due to touch screen jitter - final float deltaY = Math.abs(mFinalTouchY - mInitialTouchY); - if (deltaY < mFlingGestureMinDistPx - || vel < mFlingExpandMinVelocityPx - ) { - vel = 0; - } - - if (negative) { - vel = -vel; - } - - if (DEBUG) logf("gesture: dy=%f vel=(%f,%f) vlinear=%f", - deltaY, - xVel, yVel, - vel); - - fling(vel, true); - - break; - } - return true; - }}); - } } public void fling(float vel, boolean always) { @@ -511,6 +589,8 @@ public class PanelView extends FrameLayout { if (always||mVel != 0) { animationTick(0); // begin the animation + } else { + onExpandingFinished(); } } @@ -524,15 +604,6 @@ public class PanelView extends FrameLayout { return mViewName; } - @Override - protected void onViewAdded(View child) { - if (DEBUG) logf("onViewAdded: " + child); - } - - public View getHandle() { - return mHandleView; - } - // Rubberbands the panel to hold its contents. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @@ -543,23 +614,27 @@ public class PanelView extends FrameLayout { // Did one of our children change size? int newHeight = getMeasuredHeight(); - if (newHeight != mFullHeight) { - mFullHeight = newHeight; + if (newHeight != mMaxPanelHeight) { + mMaxPanelHeight = newHeight; // If the user isn't actively poking us, let's rubberband to the content - if (!mTracking && !mRubberbanding && !mTimeAnimator.isStarted() - && mExpandedHeight > 0 && mExpandedHeight != mFullHeight) { - mExpandedHeight = mFullHeight; + if (!mTracking && !mTimeAnimator.isStarted() + && mExpandedHeight > 0 && mExpandedHeight != mMaxPanelHeight + && mMaxPanelHeight > 0) { + mExpandedHeight = mMaxPanelHeight; } } heightMeasureSpec = MeasureSpec.makeMeasureSpec( - (int) mExpandedHeight, MeasureSpec.AT_MOST); // MeasureSpec.getMode(heightMeasureSpec)); + getDesiredMeasureHeight(), MeasureSpec.AT_MOST); setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); } + protected int getDesiredMeasureHeight() { + return (int) mExpandedHeight; + } + public void setExpandedHeight(float height) { if (DEBUG) logf("setExpandedHeight(%.1f)", height); - mRubberbanding = false; if (mTimeAnimator.isStarted()) { post(mStopAnimator); } @@ -569,8 +644,20 @@ public class PanelView extends FrameLayout { @Override protected void onLayout (boolean changed, int left, int top, int right, int bottom) { - if (DEBUG) logf("onLayout: changed=%s, bottom=%d eh=%d fh=%d", changed?"T":"f", bottom, (int)mExpandedHeight, mFullHeight); + if (DEBUG) logf("onLayout: changed=%s, bottom=%d eh=%d fh=%d", changed?"T":"f", bottom, + (int)mExpandedHeight, mMaxPanelHeight); super.onLayout(changed, left, top, right, bottom); + requestPanelHeightUpdate(); + } + + protected void requestPanelHeightUpdate() { + float currentMaxPanelHeight = getMaxPanelHeight(); + + // If the user isn't actively poking us, let's update the height + if (!mTracking && !mTimeAnimator.isStarted() + && mExpandedHeight > 0 && currentMaxPanelHeight != mExpandedHeight) { + setExpandedHeightInternal(currentMaxPanelHeight); + } } public void setExpandedHeightInternal(float h) { @@ -583,19 +670,23 @@ public class PanelView extends FrameLayout { h = 0; } - float fh = getFullHeight(); + float fh = getMaxPanelHeight(); if (fh == 0) { // Hmm, full height hasn't been computed yet } if (h < 0) h = 0; - if (!(mRubberbandingEnabled && (mTracking || mRubberbanding)) && h > fh) h = fh; + if (h > fh) h = fh; mExpandedHeight = h; - if (DEBUG) logf("setExpansion: height=%.1f fh=%.1f tracking=%s rubber=%s", h, fh, mTracking?"T":"f", mRubberbanding?"T":"f"); + if (DEBUG) { + logf("setExpansion: height=%.1f fh=%.1f tracking=%s", h, fh, + mTracking ? "T" : "f"); + } + + onHeightUpdated(mExpandedHeight); - requestLayout(); // FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); // lp.height = (int) mExpandedHeight; // setLayoutParams(lp); @@ -603,13 +694,23 @@ public class PanelView extends FrameLayout { mExpandedFraction = Math.min(1f, (fh == 0) ? 0 : h / fh); } - private float getFullHeight() { - if (mFullHeight <= 0) { - if (DEBUG) logf("Forcing measure() since fullHeight=" + mFullHeight); + protected void onHeightUpdated(float expandedHeight) { + requestLayout(); + } + + /** + * 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 int getMaxPanelHeight() { + if (mMaxPanelHeight <= 0) { + if (DEBUG) logf("Forcing measure() since mMaxPanelHeight=" + mMaxPanelHeight); measure(MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY)); } - return mFullHeight; + return mMaxPanelHeight; } public void setExpandedFraction(float frac) { @@ -621,7 +722,7 @@ public class PanelView extends FrameLayout { } frac = 0; } - setExpandedHeight(getFullHeight() * frac); + setExpandedHeight(getMaxPanelHeight() * frac); } public float getExpandedHeight() { @@ -633,7 +734,7 @@ public class PanelView extends FrameLayout { } public boolean isFullyExpanded() { - return mExpandedHeight >= getFullHeight(); + return mExpandedHeight >= getMaxPanelHeight(); } public boolean isFullyCollapsed() { @@ -658,8 +759,8 @@ public class PanelView extends FrameLayout { if (!isFullyCollapsed()) { mTimeAnimator.cancel(); mClosing = true; + onExpandingStarted(); // collapse() should never be a rubberband, even if an animation is already running - mRubberbanding = false; fling(-mSelfCollapseVelocityPx, /*always=*/ true); } } @@ -668,6 +769,7 @@ public class PanelView extends FrameLayout { if (DEBUG) logf("expand: " + this); if (isFullyCollapsed()) { mBar.startOpeningPanel(this); + onExpandingStarted(); fling(mSelfExpandVelocityPx, /*always=*/ true); } else if (DEBUG) { if (DEBUG) logf("skipping expansion: is expanded"); @@ -681,18 +783,28 @@ public class PanelView extends FrameLayout { } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println(String.format("[PanelView(%s): expandedHeight=%f fullHeight=%f closing=%s" - + " tracking=%s rubberbanding=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s" + pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s" + + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s" + "]", this.getClass().getSimpleName(), getExpandedHeight(), - getFullHeight(), + getMaxPanelHeight(), mClosing?"T":"f", mTracking?"T":"f", - mRubberbanding?"T":"f", mJustPeeked?"T":"f", mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""), mTimeAnimator, ((mTimeAnimator!=null && mTimeAnimator.isStarted())?" (started)":"") )); } + + private final OnHierarchyChangeListener mHierarchyListener = new OnHierarchyChangeListener() { + @Override + public void onChildViewAdded(View parent, View child) { + if (DEBUG) logf("onViewAdded: " + child); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + } + }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index bbac4ef..545352c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -16,14 +16,15 @@ package com.android.systemui.statusbar.phone; + import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.app.StatusBarManager.windowStateToString; +import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_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_LIGHTS_OUT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -49,6 +50,7 @@ import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.inputmethodservice.InputMethodService; +import android.media.AudioManager; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -58,19 +60,23 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; +import android.provider.Settings.Global; import android.service.notification.StatusBarNotification; +import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.view.Display; import android.view.Gravity; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewPropertyAnimator; -import android.view.ViewStub; +import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.animation.AccelerateInterpolator; import android.view.animation.Animation; @@ -79,18 +85,21 @@ import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.ScrollView; import android.widget.TextView; import com.android.internal.statusbar.StatusBarIcon; +import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.DemoMode; import com.android.systemui.EventLogTags; import com.android.systemui.R; +import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.statusbar.BaseStatusBar; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.GestureRecorder; +import com.android.systemui.statusbar.InterceptedNotifications; import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.NotificationData.Entry; +import com.android.systemui.statusbar.NotificationOverflowContainer; import com.android.systemui.statusbar.SignalClusterView; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.policy.BatteryController; @@ -99,13 +108,16 @@ import com.android.systemui.statusbar.policy.DateView; import com.android.systemui.statusbar.policy.HeadsUpNotificationView; import com.android.systemui.statusbar.policy.LocationController; import com.android.systemui.statusbar.policy.NetworkController; -import com.android.systemui.statusbar.policy.NotificationRowLayout; -import com.android.systemui.statusbar.policy.OnSizeChangedListener; import com.android.systemui.statusbar.policy.RotationLockController; +import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; +import com.android.systemui.statusbar.stack.NotificationStackScrollLayout.OnChildLocationsChangedListener; +import com.android.systemui.statusbar.stack.StackScrollState.ViewState; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; public class PhoneStatusBar extends BaseStatusBar implements DemoMode { static final String TAG = "PhoneStatusBar"; @@ -134,10 +146,18 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10; // see NotificationManagerService private static final int HIDE_ICONS_BELOW_SCORE = Notification.PRIORITY_LOW * NOTIFICATION_PRIORITY_MULTIPLIER; + /** + * Default value of {@link android.provider.Settings.Global#LOCK_SCREEN_SHOW_NOTIFICATIONS}. + */ + private static final boolean ALLOW_NOTIFICATIONS_DEFAULT = false; + 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; + // fling gesture tuning parameters, scaled to display density private float mSelfExpandVelocityPx; // classic value: 2000px/s private float mSelfCollapseVelocityPx; // classic value: 2000px/s (will be negated to collapse "up") @@ -168,11 +188,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { Display mDisplay; Point mCurrentDisplaySize = new Point(); private float mHeadsUpVerticalOffset; - private int[] mPilePosition = new int[2]; + private int[] mStackScrollerPosition = new int[2]; StatusBarWindowView mStatusBarWindow; PhoneStatusBarView mStatusBarView; private int mStatusBarWindowState = WINDOW_STATE_SHOWING; + private StatusBarWindowManager mStatusBarWindowManager; int mPixelFormat; Object mQueueLock = new Object(); @@ -189,10 +210,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { IconMerger mNotificationIcons; // [+> View mMoreIcon; + // mode indicator icon + ImageView mModeIcon; // expanded notifications NotificationPanelView mNotificationPanel; // the sliding/resizing panel within the notification window - ScrollView mScrollView; View mExpandedContents; int mNotificationPanelGravity; int mNotificationPanelMarginBottomPx, mNotificationPanelMarginPx; @@ -202,17 +224,22 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { // settings QuickSettings mQS; - boolean mHasSettingsPanel, mHasFlipSettings; - SettingsPanelView mSettingsPanel; + boolean mHasQuickSettings; View mFlipSettingsView; QuickSettingsContainerView mSettingsContainer; - int mSettingsPanelGravity; // top bar View mNotificationPanelHeader; + View mKeyguardStatusView; + View mKeyguardBottomArea; + KeyguardIndicationTextView mKeyguardIndicationTextView; + + // TODO: Fetch phrase from search/hotword provider. + String mKeyguardHotwordPhrase = ""; + int mKeyguardMaxNotificationCount; View mDateTimeView; View mClearButton; - ImageView mSettingsButton, mNotificationButton; + FlipperButton mHeaderFlipper, mKeyguardFlipper; // carrier/wifi label private TextView mCarrierLabel; @@ -220,6 +247,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { private int mCarrierLabelHeight; private TextView mEmergencyCallLabel; private int mNotificationHeaderHeight; + private View mKeyguardCarrierLabel; private boolean mShowCarrierInPanel = false; @@ -292,12 +320,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { if (MULTIUSER_DEBUG) Log.d(TAG, String.format("User setup changed: " + "selfChange=%s userSetup=%s mUserSetup=%s", selfChange, userSetup, mUserSetup)); - if (mSettingsButton != null && mHasFlipSettings) { - mSettingsButton.setVisibility(userSetup ? View.VISIBLE : View.INVISIBLE); - } - if (mSettingsPanel != null) { - mSettingsPanel.setEnabled(userSetup); - } + mHeaderFlipper.userSetup(userSetup); + mKeyguardFlipper.userSetup(userSetup); + if (userSetup != mUserSetup) { mUserSetup = userSetup; if (!mUserSetup && mStatusBarView != null) @@ -310,13 +335,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { @Override public void onChange(boolean selfChange) { boolean wasUsing = mUseHeadsUp; - mUseHeadsUp = ENABLE_HEADS_UP && 0 != Settings.Global.getInt( - mContext.getContentResolver(), SETTING_HEADS_UP, 0); + mUseHeadsUp = ENABLE_HEADS_UP && 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"); - mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP); + setHeadsUpVisibility(false); + mHeadsUpNotificationView.setNotification(null); removeHeadsUpView(); } else { addHeadsUpView(); @@ -331,6 +360,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { private int mNavigationBarMode; private Boolean mScreenOn; + private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private ViewMediatorCallback mKeyguardViewMediatorCallback; + private final Runnable mAutohide = new Runnable() { @Override public void run() { @@ -340,25 +372,139 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } }}; + private Runnable mOnFlipRunnable; + private InterceptedNotifications mIntercepted; + private VelocityTracker mSettingsTracker; + private float mSettingsDownY; + private boolean mSettingsStarted; + private boolean mSettingsCancelled; + private boolean mSettingsClosing; + private int mNotificationPadding; + + private final OnChildLocationsChangedListener mOnChildLocationsChangedListener = + new OnChildLocationsChangedListener() { + @Override + public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout) { + userActivity(); + } + }; + + public void setOnFlipRunnable(Runnable onFlipRunnable) { + mOnFlipRunnable = onFlipRunnable; + } + + /** Keys of notifications currently visible to the user. */ + private final ArraySet<String> mCurrentlyVisibleNotifications = new ArraySet<String>(); + private long mLastVisibilityReportUptimeMs; + + private static final int VISIBLE_LOCATIONS = ViewState.LOCATION_FIRST_CARD + | ViewState.LOCATION_TOP_STACK_PEEKING + | ViewState.LOCATION_MAIN_AREA + | ViewState.LOCATION_BOTTOM_STACK_PEEKING; + + 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. + private final Runnable mVisibilityReporter = new Runnable() { + private final ArrayList<String> mTmpNewlyVisibleNotifications = new ArrayList<String>(); + private final ArrayList<String> mTmpCurrentlyVisibleNotifications = new ArrayList<String>(); + + @Override + public void run() { + mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis(); + + // 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. + int N = mNotificationData.size(); + for (int i = 0; i < N; i++) { + Entry entry = mNotificationData.get(i); + String key = entry.notification.getKey(); + boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(key); + boolean currentlyVisible = + (mStackScroller.getChildLocation(entry.row) & VISIBLE_LOCATIONS) != 0; + if (currentlyVisible) { + // Build new set of visible notifications. + mTmpCurrentlyVisibleNotifications.add(key); + } + if (!previouslyVisible && currentlyVisible) { + mTmpNewlyVisibleNotifications.add(key); + } + } + ArraySet<String> noLongerVisibleNotifications = mCurrentlyVisibleNotifications; + noLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications); + + logNotificationVisibilityChanges( + mTmpNewlyVisibleNotifications, noLongerVisibleNotifications); + + mCurrentlyVisibleNotifications.clear(); + mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications); + + mTmpNewlyVisibleNotifications.clear(); + mTmpCurrentlyVisibleNotifications.clear(); + } + }; + + @Override + public void setZenMode(int mode) { + super.setZenMode(mode); + if (mModeIcon == null) return; + if (!isDeviceProvisioned()) return; + final boolean zen = mode != Settings.Global.ZEN_MODE_OFF; + mModeIcon.setVisibility(zen ? View.VISIBLE : View.GONE); + if (!zen) { + mIntercepted.releaseIntercepted(); + } + } + @Override public void start() { mDisplay = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); updateDisplaySize(); - + mIntercepted = new InterceptedNotifications(mContext, this); super.start(); // calls createAndAddWindows() addNavigationBar(); // Lastly, call to the icon policy to install/update all the icons. mIconPolicy = new PhoneStatusBarPolicy(mContext); + mSettingsObserver.onChange(false); // set up mHeadsUpObserver.onChange(true); // set up if (ENABLE_HEADS_UP) { mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(SETTING_HEADS_UP), true, + Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), true, + mHeadsUpObserver); + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true, mHeadsUpObserver); } + startKeyguard(); } // ================================================================================ @@ -395,7 +541,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { PanelHolder holder = (PanelHolder) mStatusBarWindow.findViewById(R.id.panel_holder); mStatusBarView.setPanelHolder(holder); - mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById(R.id.notification_panel); + mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById( + R.id.notification_panel); mNotificationPanel.setStatusBar(this); mNotificationPanelIsFullScreenWidth = (mNotificationPanel.getLayoutParams().width == ViewGroup.LayoutParams.MATCH_PARENT); @@ -421,7 +568,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mHeadsUpNotificationView.setBar(this); } if (MULTIUSER_DEBUG) { - mNotificationPanelDebugText = (TextView) mNotificationPanel.findViewById(R.id.header_debug_info); + mNotificationPanelDebugText = (TextView) mNotificationPanel.findViewById( + R.id.header_debug_info); mNotificationPanelDebugText.setVisibility(View.VISIBLE); } @@ -455,16 +603,30 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mNotificationIcons = (IconMerger)mStatusBarView.findViewById(R.id.notificationIcons); mMoreIcon = mStatusBarView.findViewById(R.id.moreIcon); mNotificationIcons.setOverflowIndicator(mMoreIcon); + mModeIcon = (ImageView)mStatusBarView.findViewById(R.id.modeIcon); + mModeIcon.setImageResource(R.drawable.stat_sys_zen_limited); mStatusBarContents = (LinearLayout)mStatusBarView.findViewById(R.id.status_bar_contents); mTickerView = mStatusBarView.findViewById(R.id.ticker); - mPile = (NotificationRowLayout)mStatusBarWindow.findViewById(R.id.latestItems); - mPile.setLayoutTransitionsEnabled(false); - mPile.setLongPressListener(getNotificationLongClicker()); - mExpandedContents = mPile; // was: expanded.findViewById(R.id.notificationLinearLayout); + mStackScroller = (NotificationStackScrollLayout) mStatusBarWindow.findViewById( + R.id.notification_stack_scroller); + mStackScroller.setLongPressListener(getNotificationLongClicker()); + mStackScroller.setChildLocationsChangedListener(mOnChildLocationsChangedListener); - mNotificationPanelHeader = mStatusBarWindow.findViewById(R.id.header); + mKeyguardIconOverflowContainer = + (NotificationOverflowContainer) LayoutInflater.from(mContext).inflate( + R.layout.status_bar_notification_keyguard_overflow, mStackScroller, false); + mKeyguardIconOverflowContainer.setOnActivatedListener(this); + mKeyguardCarrierLabel = mStatusBarWindow.findViewById(R.id.keyguard_carrier_text); + mStackScroller.addView(mKeyguardIconOverflowContainer); + + mExpandedContents = mStackScroller; + mNotificationPanelHeader = mStatusBarWindow.findViewById(R.id.header); + mKeyguardStatusView = mStatusBarWindow.findViewById(R.id.keyguard_status_view); + mKeyguardBottomArea = mStatusBarWindow.findViewById(R.id.keyguard_bottom_area); + mKeyguardIndicationTextView = (KeyguardIndicationTextView) mStatusBarWindow.findViewById( + R.id.keyguard_indication_text); mClearButton = mStatusBarWindow.findViewById(R.id.clear_all_button); mClearButton.setOnClickListener(mClearButtonListener); mClearButton.setAlpha(0f); @@ -472,8 +634,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mClearButton.setEnabled(false); mDateView = (DateView)mStatusBarWindow.findViewById(R.id.date); - mHasSettingsPanel = res.getBoolean(R.bool.config_hasSettingsPanel); - mHasFlipSettings = res.getBoolean(R.bool.config_hasFlipSettingsPanel); + mHasQuickSettings = res.getBoolean(R.bool.config_hasQuickSettings); mDateTimeView = mNotificationPanelHeader.findViewById(R.id.datetime); if (mDateTimeView != null) { @@ -481,40 +642,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mDateTimeView.setEnabled(true); } - mSettingsButton = (ImageView) mStatusBarWindow.findViewById(R.id.settings_button); - if (mSettingsButton != null) { - mSettingsButton.setOnClickListener(mSettingsButtonListener); - if (mHasSettingsPanel) { - if (mStatusBarView.hasFullWidthNotifications()) { - // the settings panel is hiding behind this button - mSettingsButton.setImageResource(R.drawable.ic_notify_quicksettings); - mSettingsButton.setVisibility(View.VISIBLE); - } else { - // there is a settings panel, but it's on the other side of the (large) screen - final View buttonHolder = mStatusBarWindow.findViewById( - R.id.settings_button_holder); - if (buttonHolder != null) { - buttonHolder.setVisibility(View.GONE); - } - } - } else { - // no settings panel, go straight to settings - mSettingsButton.setVisibility(View.VISIBLE); - mSettingsButton.setImageResource(R.drawable.ic_notify_settings); - } - } - if (mHasFlipSettings) { - mNotificationButton = (ImageView) mStatusBarWindow.findViewById(R.id.notification_button); - if (mNotificationButton != null) { - mNotificationButton.setOnClickListener(mNotificationButtonListener); - } - } + mHeaderFlipper = new FlipperButton(mStatusBarWindow.findViewById(R.id.header_flipper)); + mKeyguardFlipper =new FlipperButton(mStatusBarWindow.findViewById(R.id.keyguard_flipper)); - mScrollView = (ScrollView)mStatusBarWindow.findViewById(R.id.scroll); - mScrollView.setVerticalScrollBarEnabled(false); // less drawing during pulldowns if (!mNotificationPanelIsFullScreenWidth) { - mScrollView.setSystemUiVisibility( - View.STATUS_BAR_DISABLE_NOTIFICATION_TICKER | + mNotificationPanel.setSystemUiVisibility( View.STATUS_BAR_DISABLE_NOTIFICATION_ICONS | View.STATUS_BAR_DISABLE_CLOCK); } @@ -534,7 +666,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mBatteryController = new BatteryController(mContext); mNetworkController = new NetworkController(mContext); mBluetoothController = new BluetoothController(mContext); - mRotationLockController = new RotationLockController(mContext); + if (mContext.getResources().getBoolean(R.bool.config_showRotationLock) + || QuickSettings.DEBUG_GONE_TILES) { + mRotationLockController = new RotationLockController(mContext); + } final SignalClusterView signalCluster = (SignalClusterView)mStatusBarView.findViewById(R.id.signal_cluster); @@ -546,17 +681,18 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { if (isAPhone) { mEmergencyCallLabel = (TextView) mStatusBarWindow.findViewById(R.id.emergency_calls_only); - if (mEmergencyCallLabel != null) { - mNetworkController.addEmergencyLabelView(mEmergencyCallLabel); - mEmergencyCallLabel.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { }}); - mEmergencyCallLabel.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) { - updateCarrierLabelVisibility(false); - }}); - } + // TODO: Uncomment when correctly positioned +// if (mEmergencyCallLabel != null) { +// mNetworkController.addEmergencyLabelView(mEmergencyCallLabel); +// mEmergencyCallLabel.setOnClickListener(new View.OnClickListener() { +// public void onClick(View v) { }}); +// mEmergencyCallLabel.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) { +// updateCarrierLabelVisibility(false); +// }}); +// } } mCarrierLabel = (TextView)mStatusBarWindow.findViewById(R.id.carrier_label); @@ -574,56 +710,29 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } // set up the dynamic hide/show of the label - mPile.setOnSizeChangedListener(new OnSizeChangedListener() { - @Override - public void onSizeChanged(View view, int w, int h, int oldw, int oldh) { - updateCarrierLabelVisibility(false); - } - }); + // TODO: uncomment, handle this for the Stack scroller aswell +// ((NotificationRowLayout) mStackScroller) +// .setOnSizeChangedListener(new OnSizeChangedListener() { +// @Override +// public void onSizeChanged(View view, int w, int h, int oldw, int oldh) { +// updateCarrierLabelVisibility(false); } // Quick Settings (where available, some restrictions apply) - if (mHasSettingsPanel) { - // first, figure out where quick settings should be inflated - final View settings_stub; - if (mHasFlipSettings) { - // a version of quick settings that flips around behind the notifications - settings_stub = mStatusBarWindow.findViewById(R.id.flip_settings_stub); - if (settings_stub != null) { - mFlipSettingsView = ((ViewStub)settings_stub).inflate(); - mFlipSettingsView.setVisibility(View.GONE); - mFlipSettingsView.setVerticalScrollBarEnabled(false); - } - } else { - // full quick settings panel - settings_stub = mStatusBarWindow.findViewById(R.id.quick_settings_stub); - if (settings_stub != null) { - mSettingsPanel = (SettingsPanelView) ((ViewStub)settings_stub).inflate(); - } else { - mSettingsPanel = (SettingsPanelView) mStatusBarWindow.findViewById(R.id.settings_panel); - } - - if (mSettingsPanel != null) { - if (!ActivityManager.isHighEndGfx()) { - mSettingsPanel.setBackground(new FastColorDrawable(context.getResources().getColor( - R.color.notification_panel_solid_background))); - } - } - } - - // wherever you find it, Quick Settings needs a container to survive + mNotificationPadding = mContext.getResources() + .getDimensionPixelSize(R.dimen.notification_side_padding); + if (mHasQuickSettings) { + // Quick Settings needs a container to survive mSettingsContainer = (QuickSettingsContainerView) mStatusBarWindow.findViewById(R.id.quick_settings_container); + mFlipSettingsView = mSettingsContainer; if (mSettingsContainer != null) { mQS = new QuickSettings(mContext, mSettingsContainer); if (!mNotificationPanelIsFullScreenWidth) { mSettingsContainer.setSystemUiVisibility( - View.STATUS_BAR_DISABLE_NOTIFICATION_TICKER + View.STATUS_BAR_DISABLE_NOTIFICATION_ICONS | View.STATUS_BAR_DISABLE_SYSTEM_INFO); } - if (mSettingsPanel != null) { - mSettingsPanel.setQuickSettings(mQS); - } mQS.setService(this); mQS.setBar(mStatusBarView); mQS.setup(mNetworkController, mBluetoothController, mBatteryController, @@ -651,6 +760,106 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { return mStatusBarView; } + public boolean onSettingsEvent(MotionEvent event) { + userActivity(); + if (mSettingsClosing + && mFlipSettingsViewAnim != null && mFlipSettingsViewAnim.isRunning()) { + return true; + } + if (mSettingsTracker != null) { + mSettingsTracker.addMovement(event); + } + final int slop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mSettingsTracker = VelocityTracker.obtain(); + mSettingsDownY = event.getY(); + mSettingsCancelled = false; + mSettingsStarted = false; + mSettingsClosing = mFlipSettingsView.getVisibility() == View.VISIBLE; + if (mSettingsClosing) { + mStackScroller.setVisibility(View.VISIBLE); + } else { + mFlipSettingsView.setTranslationY(-mNotificationPanel.getMeasuredHeight()); + } + dispatchSettingsEvent(event); + } else if (mSettingsTracker != null && (event.getAction() == MotionEvent.ACTION_UP + || event.getAction() == MotionEvent.ACTION_CANCEL)) { + final float dy = event.getY() - mSettingsDownY; + final FlipperButton flipper = mOnKeyguard ? mKeyguardFlipper : mHeaderFlipper; + final boolean inButton = flipper.inHolderBounds(event); + final boolean qsTap = mSettingsClosing && Math.abs(dy) < slop; + if (!qsTap && !inButton) { + mSettingsTracker.computeCurrentVelocity(1000); + final float vy = mSettingsTracker.getYVelocity(); + final boolean animate = true; + if (dy <= slop || vy <= 0) { + flipToNotifications(animate); + } else { + flipToSettings(animate); + } + } + mSettingsTracker.recycle(); + mSettingsTracker = null; + dispatchSettingsEvent(event); + } else if (mSettingsTracker != null && event.getAction() == MotionEvent.ACTION_MOVE) { + final float dy = event.getY() - mSettingsDownY; + if (mSettingsClosing) { + positionSettings(dy); + final boolean qsTap = Math.abs(dy) < slop; + if (!mSettingsCancelled && !qsTap) { + MotionEvent cancelEvent = MotionEvent.obtainNoHistory(event); + cancelEvent.setAction(MotionEvent.ACTION_CANCEL); + dispatchSettingsEvent(cancelEvent); + mSettingsCancelled = true; + } + } else { + if (!mSettingsStarted && dy > slop) { + mSettingsStarted = true; + mFlipSettingsView.setVisibility(View.VISIBLE); + mStackScroller.setVisibility(View.VISIBLE); + } + if (mSettingsStarted) { + positionSettings(dy); + } + dispatchSettingsEvent(event); + } + } + return true; + } + + private void dispatchSettingsEvent(MotionEvent event) { + final View target = mSettingsClosing ? mFlipSettingsView : mNotificationPanelHeader; + final int[] targetLoc = new int[2]; + target.getLocationInWindow(targetLoc); + final int[] panelLoc = new int[2]; + mNotificationPanel.getLocationInWindow(panelLoc); + final int dx = targetLoc[0] - panelLoc[0]; + final int dy = targetLoc[1] - panelLoc[1]; + event.offsetLocation(-dx, -dy); + target.dispatchTouchEvent(event); + } + + private void positionSettings(float dy) { + if (mSettingsClosing) { + final int ph = mNotificationPanel.getMeasuredHeight(); + dy = Math.min(Math.max(-ph, dy), 0); + mFlipSettingsView.setTranslationY(dy); + mStackScroller.setTranslationY(ph + dy); + } else { + final int h = mFlipSettingsView.getBottom(); + dy = Math.min(Math.max(0, dy), h); + mFlipSettingsView.setTranslationY(-h + dy); + mStackScroller.setTranslationY(dy); + } + } + + private void startKeyguard() { + KeyguardViewMediator keyguardViewMediator = getComponent(KeyguardViewMediator.class); + mStatusBarKeyguardViewManager = keyguardViewMediator.registerStatusBar(this, + mStatusBarWindow, mStatusBarWindowManager); + mKeyguardViewMediatorCallback = keyguardViewMediator.getViewMediatorCallback(); + } + @Override protected void onShowSearchPanel() { if (mNavigationBarView != null) { @@ -728,10 +937,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } } - protected int getStatusBarGravity() { - return Gravity.TOP | Gravity.FILL_HORIZONTAL; - } - public int getStatusBarHeight() { if (mNaturalBarHeight < 0) { final Resources res = mContext.getResources(); @@ -853,7 +1058,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { PixelFormat.TRANSLUCENT); lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; lp.gravity = Gravity.TOP; - lp.y = getStatusBarHeight(); lp.setTitle("Heads Up"); lp.packageName = mContext.getPackageName(); lp.windowAnimations = R.style.Animation_StatusBar_HeadsUp; @@ -901,16 +1105,24 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mStatusIcons.removeViewAt(viewIndex); } + public UserHandle getCurrentUserHandle() { + return new UserHandle(mCurrentUserId); + } + public void addNotification(IBinder key, StatusBarNotification notification) { if (DEBUG) Log.d(TAG, "addNotification score=" + notification.getScore()); Entry shadeEntry = createNotificationViews(key, notification); if (shadeEntry == null) { return; } + if (mZenMode != Global.ZEN_MODE_OFF && mIntercepted.tryIntercept(key, notification)) { + return; + } if (mUseHeadsUp && shouldInterrupt(notification)) { if (DEBUG) Log.d(TAG, "launching notification in heads up mode"); Entry interruptionCandidate = new Entry(key, notification, null); - if (inflateViews(interruptionCandidate, mHeadsUpNotificationView.getHolder())) { + ViewGroup holder = mHeadsUpNotificationView.getHolder(); + if (inflateViewsForHeadsUp(interruptionCandidate, holder)) { mInterruptingNotificationTime = System.currentTimeMillis(); mInterruptingNotificationEntry = interruptionCandidate; shadeEntry.setInterruption(); @@ -951,13 +1163,19 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { @Override public void resetHeadsUpDecayTimer() { + mHandler.removeMessages(MSG_HIDE_HEADS_UP); if (mUseHeadsUp && mHeadsUpNotificationDecay > 0 && mHeadsUpNotificationView.isClearable()) { - mHandler.removeMessages(MSG_HIDE_HEADS_UP); mHandler.sendEmptyMessageDelayed(MSG_HIDE_HEADS_UP, mHeadsUpNotificationDecay); } } + @Override + public void updateNotification(IBinder key, StatusBarNotification notification) { + super.updateNotification(key, notification); + mIntercepted.update(key, notification); + } + public void removeNotification(IBinder key) { StatusBarNotification old = removeNotificationViews(key); if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old); @@ -975,11 +1193,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } if (CLOSE_PANEL_WHEN_EMPTIED && mNotificationData.size() == 0 - && !mNotificationPanel.isTracking()) { + && !mNotificationPanel.isTracking() && !mOnKeyguard) { animateCollapsePanels(); } } - + mIntercepted.remove(key); setAreThereNotifications(); } @@ -995,17 +1213,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { ((ImageView)mClearButton).setImageResource(R.drawable.ic_notify_clear); } - if (mSettingsButton != null) { - // Force asset reloading - mSettingsButton.setImageDrawable(null); - mSettingsButton.setImageResource(R.drawable.ic_notify_quicksettings); - } - - if (mNotificationButton != null) { - // Force asset reloading - mNotificationButton.setImageDrawable(null); - mNotificationButton.setImageResource(R.drawable.ic_notifications); - } + mHeaderFlipper.refreshLayout(); + mKeyguardFlipper.refreshLayout(); refreshAllStatusBarIcons(); } @@ -1016,7 +1225,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } private void loadNotificationShade() { - if (mPile == null) return; + if (mStackScroller == null) return; int N = mNotificationData.size(); @@ -1027,32 +1236,41 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { for (int i=0; i<N; i++) { Entry ent = mNotificationData.get(N-i-1); if (!(provisioned || showNotificationEvenIfUnprovisioned(ent.notification))) continue; - if (!notificationIsForCurrentUser(ent.notification)) continue; - toShow.add(ent.row); + + // TODO How do we want to badge notifcations from profiles. + if (!notificationIsForCurrentProfiles(ent.notification)) continue; + + final int vis = ent.notification.getNotification().visibility; + if (vis != Notification.VISIBILITY_SECRET) { + // when isLockscreenPublicMode() we show the public form of VISIBILITY_PRIVATE notifications + ent.row.setShowingPublic(isLockscreenPublicMode() + && vis == Notification.VISIBILITY_PRIVATE + && !userAllowsPrivateNotificationsInPublic(ent.notification.getUserId())); + toShow.add(ent.row); + } } ArrayList<View> toRemove = new ArrayList<View>(); - for (int i=0; i<mPile.getChildCount(); i++) { - View child = mPile.getChildAt(i); - if (!toShow.contains(child)) { + for (int i=0; i< mStackScroller.getChildCount(); i++) { + View child = mStackScroller.getChildAt(i); + if (!toShow.contains(child) && child != mKeyguardIconOverflowContainer) { toRemove.add(child); } } for (View remove : toRemove) { - mPile.removeView(remove); + mStackScroller.removeView(remove); } for (int i=0; i<toShow.size(); i++) { View v = toShow.get(i); if (v.getParent() == null) { - mPile.addView(v, i); + mStackScroller.addView(v, i); } } - if (mSettingsButton != null) { - mSettingsButton.setEnabled(isDeviceProvisioned()); - } + mHeaderFlipper.provisionCheck(provisioned); + mKeyguardFlipper.provisionCheck(provisioned); } @Override @@ -1078,7 +1296,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { Entry ent = mNotificationData.get(N-i-1); if (!((provisioned && ent.notification.getScore() >= HIDE_ICONS_BELOW_SCORE) || showNotificationEvenIfUnprovisioned(ent.notification))) continue; - if (!notificationIsForCurrentUser(ent.notification)) continue; + if (!notificationIsForCurrentProfiles(ent.notification)) continue; + if (isLockscreenPublicMode() + && ent.notification.getNotification().visibility + == Notification.VISIBILITY_SECRET + && !userAllowsPrivateNotificationsInPublic(ent.notification.getUserId())) { + // in "public" mode (atop a secure keyguard), secret notifs are totally hidden + continue; + } + if (mIntercepted.isSyntheticEntry(ent)) { + continue; + } toShow.add(ent.icon); } @@ -1103,19 +1331,23 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } protected void updateCarrierLabelVisibility(boolean force) { + // TODO: Handle this for the notification stack scroller as well if (!mShowCarrierInPanel) return; // The idea here is to only show the carrier label when there is enough room to see it, // i.e. when there aren't enough notifications to fill the panel. if (SPEW) { - Log.d(TAG, String.format("pileh=%d scrollh=%d carrierh=%d", - mPile.getHeight(), mScrollView.getHeight(), mCarrierLabelHeight)); + Log.d(TAG, String.format("stackScrollerh=%d scrollh=%d carrierh=%d", + mStackScroller.getHeight(), mStackScroller.getHeight(), + mCarrierLabelHeight)); } final boolean emergencyCallsShownElsewhere = mEmergencyCallLabel != null; final boolean makeVisible = !(emergencyCallsShownElsewhere && mNetworkController.isEmergencyOnly()) - && mPile.getHeight() < (mNotificationPanel.getHeight() - mCarrierLabelHeight - mNotificationHeaderHeight) - && mScrollView.getVisibility() == View.VISIBLE; + && mStackScroller.getHeight() < (mNotificationPanel.getHeight() + - mCarrierLabelHeight - mNotificationHeaderHeight) + && mStackScroller.getVisibility() == View.VISIBLE + && !mOnKeyguard; if (force || mCarrierLabelVisible != makeVisible) { mCarrierLabelVisible = makeVisible; @@ -1155,10 +1387,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { + " any=" + any + " clearable=" + clearable); } - if (mHasFlipSettings - && mFlipSettingsView != null + if (mFlipSettingsView != null && mFlipSettingsView.getVisibility() == View.VISIBLE - && mScrollView.getVisibility() != View.VISIBLE) { + && mStackScroller.getVisibility() != View.VISIBLE) { // the flip settings panel is unequivocally showing; we should not be shown mClearButton.setVisibility(View.INVISIBLE); } else if (mClearButton.isShown()) { @@ -1240,8 +1471,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { flagdbg.append(((diff & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) ? "* " : " "); flagdbg.append(((state & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) ? "ALERTS" : "alerts"); flagdbg.append(((diff & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) ? "* " : " "); - flagdbg.append(((state & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) ? "TICKER" : "ticker"); - flagdbg.append(((diff & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) ? "* " : " "); flagdbg.append(((state & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) ? "SYSTEM_INFO" : "system_info"); flagdbg.append(((diff & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) ? "* " : " "); flagdbg.append(((state & StatusBarManager.DISABLE_BACK) != 0) ? "BACK" : "back"); @@ -1326,10 +1555,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { .setDuration(175) .start(); } - } else if ((diff & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) { - if (mTicking && (state & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) { - haltTicker(); - } } } @@ -1400,14 +1625,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { return (mDisabled & StatusBarManager.DISABLE_EXPAND) == 0; } - void makeExpandedVisible() { + void makeExpandedVisible(boolean force) { if (SPEW) Log.d(TAG, "Make expanded visible: expanded visible=" + mExpandedVisible); - if (mExpandedVisible || !panelsEnabled()) { + if (!force && (mExpandedVisible || !panelsEnabled())) { return; } mExpandedVisible = true; - mPile.setLayoutTransitionsEnabled(true); if (mNavigationBarView != null) mNavigationBarView.setSlippery(true); @@ -1417,25 +1641,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { // 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! - WindowManager.LayoutParams lp = (WindowManager.LayoutParams) mStatusBarWindow.getLayoutParams(); - lp.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - lp.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - lp.height = ViewGroup.LayoutParams.MATCH_PARENT; - mWindowManager.updateViewLayout(mStatusBarWindow, lp); + mStatusBarWindowManager.setStatusBarExpanded(true); visibilityChanged(true); setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true); } - private void releaseFocus() { - WindowManager.LayoutParams lp = - (WindowManager.LayoutParams) mStatusBarWindow.getLayoutParams(); - lp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - lp.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - mWindowManager.updateViewLayout(mStatusBarWindow, lp); - } - public void animateCollapsePanels() { animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); } @@ -1447,9 +1659,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { + " flags=" + flags); } - // release focus immediately to kick off focus change transition - releaseFocus(); - if ((flags & CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL) == 0) { mHandler.removeMessages(MSG_CLOSE_RECENTS_PANEL); mHandler.sendEmptyMessage(MSG_CLOSE_RECENTS_PANEL); @@ -1460,8 +1669,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mHandler.sendEmptyMessage(MSG_CLOSE_SEARCH_PANEL); } - mStatusBarWindow.cancelExpandHelper(); - mStatusBarView.collapseAllPanels(true); + if (mStatusBarWindow != null) { + + // release focus immediately to kick off focus change transition + mStatusBarWindowManager.setStatusBarFocusable(false); + + mStatusBarWindow.cancelExpandHelper(); + mStatusBarView.collapseAllPanels(true); + if (isFlippedToSettings()) { + flipToNotifications(true /*animate*/); + } + } } public ViewPropertyAnimator setVisibilityWhenDone( @@ -1508,8 +1726,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { final int FLIP_DURATION_IN = 225; final int FLIP_DURATION = (FLIP_DURATION_IN + FLIP_DURATION_OUT); - Animator mScrollViewAnim, mFlipSettingsViewAnim, mNotificationButtonAnim, - mSettingsButtonAnim, mClearButtonAnim; + Animator mScrollViewAnim, mFlipSettingsViewAnim, mClearButtonAnim; @Override public void animateExpandNotificationsPanel() { @@ -1519,43 +1736,50 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } mNotificationPanel.expand(); - if (mHasFlipSettings && mScrollView.getVisibility() != View.VISIBLE) { - flipToNotifications(); + if (mStackScroller.getVisibility() != View.VISIBLE) { + flipToNotifications(true /*animate*/); } if (false) postStartTracing(); } - public void flipToNotifications() { - if (mFlipSettingsViewAnim != null) mFlipSettingsViewAnim.cancel(); - if (mScrollViewAnim != null) mScrollViewAnim.cancel(); - if (mSettingsButtonAnim != null) mSettingsButtonAnim.cancel(); - if (mNotificationButtonAnim != null) mNotificationButtonAnim.cancel(); - if (mClearButtonAnim != null) mClearButtonAnim.cancel(); - - mScrollView.setVisibility(View.VISIBLE); - mScrollViewAnim = start( - startDelay(FLIP_DURATION_OUT, - interpolator(mDecelerateInterpolator, - ObjectAnimator.ofFloat(mScrollView, View.SCALE_X, 0f, 1f) - .setDuration(FLIP_DURATION_IN) - ))); - mFlipSettingsViewAnim = start( - setVisibilityWhenDone( - interpolator(mAccelerateInterpolator, - ObjectAnimator.ofFloat(mFlipSettingsView, View.SCALE_X, 1f, 0f) - ) - .setDuration(FLIP_DURATION_OUT), - mFlipSettingsView, View.INVISIBLE)); - mNotificationButtonAnim = start( - setVisibilityWhenDone( - ObjectAnimator.ofFloat(mNotificationButton, View.ALPHA, 0f) - .setDuration(FLIP_DURATION), - mNotificationButton, View.INVISIBLE)); - mSettingsButton.setVisibility(View.VISIBLE); - mSettingsButtonAnim = start( - ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, 1f) - .setDuration(FLIP_DURATION)); + private static void cancelAnim(Animator anim) { + if (anim != null) { + anim.cancel(); + } + } + + public void flipToNotifications(boolean animate) { + cancelAnim(mFlipSettingsViewAnim); + cancelAnim(mScrollViewAnim); + cancelAnim(mClearButtonAnim); + mHeaderFlipper.cancel(); + mKeyguardFlipper.cancel(); + mStackScroller.setVisibility(View.VISIBLE); + final int h = mNotificationPanel.getMeasuredHeight(); + if (animate) { + final float settingsY = + mSettingsTracker != null ? mFlipSettingsView.getTranslationY() : 0; + final float scrollerY = mSettingsTracker != null ? mStackScroller.getTranslationY() : h; + mScrollViewAnim = start( + interpolator(mDecelerateInterpolator, + ObjectAnimator.ofFloat(mStackScroller, View.TRANSLATION_Y, scrollerY, 0) + .setDuration(FLIP_DURATION) + )); + mFlipSettingsViewAnim = start( + setVisibilityWhenDone( + interpolator(mDecelerateInterpolator, + ObjectAnimator.ofFloat( + mFlipSettingsView, View.TRANSLATION_Y, settingsY, -h)) + .setDuration(FLIP_DURATION), + mFlipSettingsView, View.INVISIBLE)); + } else { + mStackScroller.setTranslationY(0); + mFlipSettingsView.setTranslationY(-h); + mFlipSettingsView.setVisibility(View.INVISIBLE); + } + mHeaderFlipper.flipToNotifications(animate); + mKeyguardFlipper.flipToNotifications(animate); mClearButton.setVisibility(View.VISIBLE); mClearButton.setAlpha(0f); setAreThereNotifications(); // this will show/hide the button as necessary @@ -1563,7 +1787,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { public void run() { updateCarrierLabelVisibility(false); } - }, FLIP_DURATION - 150); + }, animate ? FLIP_DURATION - 150 : 0); + if (mOnFlipRunnable != null) { + mOnFlipRunnable.run(); + } } @Override @@ -1576,85 +1803,76 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { // Settings are not available in setup if (!mUserSetup) return; - if (mHasFlipSettings) { - mNotificationPanel.expand(); - if (mFlipSettingsView.getVisibility() != View.VISIBLE) { - flipToSettings(); - } - } else if (mSettingsPanel != null) { - mSettingsPanel.expand(); + mNotificationPanel.expand(); + if (mFlipSettingsView.getVisibility() != View.VISIBLE + || mFlipSettingsView.getTranslationY() < 0) { + flipToSettings(true /*animate*/); } if (false) postStartTracing(); } - public void switchToSettings() { - // Settings are not available in setup - if (!mUserSetup) return; - - mFlipSettingsView.setScaleX(1f); - mFlipSettingsView.setVisibility(View.VISIBLE); - mSettingsButton.setVisibility(View.GONE); - mScrollView.setVisibility(View.GONE); - mScrollView.setScaleX(0f); - mNotificationButton.setVisibility(View.VISIBLE); - mNotificationButton.setAlpha(1f); - mClearButton.setVisibility(View.GONE); + public boolean isFlippedToSettings() { + if (mFlipSettingsView != null) { + return mFlipSettingsView.getVisibility() == View.VISIBLE; + } + return false; } - public void flipToSettings() { + public void flipToSettings(boolean animate) { // Settings are not available in setup if (!mUserSetup) return; - if (mFlipSettingsViewAnim != null) mFlipSettingsViewAnim.cancel(); - if (mScrollViewAnim != null) mScrollViewAnim.cancel(); - if (mSettingsButtonAnim != null) mSettingsButtonAnim.cancel(); - if (mNotificationButtonAnim != null) mNotificationButtonAnim.cancel(); - if (mClearButtonAnim != null) mClearButtonAnim.cancel(); + cancelAnim(mFlipSettingsViewAnim); + cancelAnim(mScrollViewAnim); + mHeaderFlipper.cancel(); + mKeyguardFlipper.cancel(); + cancelAnim(mClearButtonAnim); mFlipSettingsView.setVisibility(View.VISIBLE); - mFlipSettingsView.setScaleX(0f); - mFlipSettingsViewAnim = start( - startDelay(FLIP_DURATION_OUT, - interpolator(mDecelerateInterpolator, - ObjectAnimator.ofFloat(mFlipSettingsView, View.SCALE_X, 0f, 1f) - .setDuration(FLIP_DURATION_IN) - ))); - mScrollViewAnim = start( - setVisibilityWhenDone( - interpolator(mAccelerateInterpolator, - ObjectAnimator.ofFloat(mScrollView, View.SCALE_X, 1f, 0f) - ) - .setDuration(FLIP_DURATION_OUT), - mScrollView, View.INVISIBLE)); - mSettingsButtonAnim = start( - setVisibilityWhenDone( - ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, 0f) + final int h = mNotificationPanel.getMeasuredHeight(); + if (animate) { + final float settingsY + = mSettingsTracker != null ? mFlipSettingsView.getTranslationY() : -h; + final float scrollerY = mSettingsTracker != null ? mStackScroller.getTranslationY() : 0; + mFlipSettingsViewAnim = start( + startDelay(0, + interpolator(mDecelerateInterpolator, + ObjectAnimator.ofFloat(mFlipSettingsView, View.TRANSLATION_Y, + settingsY, 0f) + .setDuration(FLIP_DURATION) + ))); + mScrollViewAnim = start( + setVisibilityWhenDone( + interpolator(mDecelerateInterpolator, + ObjectAnimator.ofFloat(mStackScroller, View.TRANSLATION_Y, scrollerY, h) + ) + .setDuration(FLIP_DURATION), + mStackScroller, View.INVISIBLE)); + } else { + mFlipSettingsView.setTranslationY(0); + mStackScroller.setTranslationY(h); + mStackScroller.setVisibility(View.INVISIBLE); + } + mHeaderFlipper.flipToSettings(animate); + mKeyguardFlipper.flipToSettings(animate); + if (animate) { + mClearButtonAnim = start( + setVisibilityWhenDone( + ObjectAnimator.ofFloat(mClearButton, View.ALPHA, 0f) .setDuration(FLIP_DURATION), - mScrollView, View.INVISIBLE)); - mNotificationButton.setVisibility(View.VISIBLE); - mNotificationButtonAnim = start( - ObjectAnimator.ofFloat(mNotificationButton, View.ALPHA, 1f) - .setDuration(FLIP_DURATION)); - mClearButtonAnim = start( - setVisibilityWhenDone( - ObjectAnimator.ofFloat(mClearButton, View.ALPHA, 0f) - .setDuration(FLIP_DURATION), - mClearButton, View.INVISIBLE)); + mClearButton, View.INVISIBLE)); + } else { + mClearButton.setAlpha(0); + mClearButton.setVisibility(View.INVISIBLE); + } mNotificationPanel.postDelayed(new Runnable() { public void run() { updateCarrierLabelVisibility(false); } - }, FLIP_DURATION - 150); - } - - public void flipPanels() { - if (mHasFlipSettings) { - if (mFlipSettingsView.getVisibility() != View.VISIBLE) { - flipToSettings(); - } else { - flipToNotifications(); - } + }, animate ? FLIP_DURATION - 150 : 0); + if (mOnFlipRunnable != null) { + mOnFlipRunnable.run(); } } @@ -1670,43 +1888,34 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible + " mExpandedVisible=" + mExpandedVisible); - if (!mExpandedVisible) { + if (!mExpandedVisible || mStatusBarWindow == null) { return; } // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868) mStatusBarView.collapseAllPanels(/*animate=*/ false); - if (mHasFlipSettings) { - // reset things to their proper state - if (mFlipSettingsViewAnim != null) mFlipSettingsViewAnim.cancel(); - if (mScrollViewAnim != null) mScrollViewAnim.cancel(); - if (mSettingsButtonAnim != null) mSettingsButtonAnim.cancel(); - if (mNotificationButtonAnim != null) mNotificationButtonAnim.cancel(); - if (mClearButtonAnim != null) mClearButtonAnim.cancel(); - - mScrollView.setScaleX(1f); - mScrollView.setVisibility(View.VISIBLE); - mSettingsButton.setAlpha(1f); - mSettingsButton.setVisibility(View.VISIBLE); - mNotificationPanel.setVisibility(View.GONE); - mFlipSettingsView.setVisibility(View.GONE); - mNotificationButton.setVisibility(View.GONE); - setAreThereNotifications(); // show the clear button - } + // reset things to their proper state + if (mFlipSettingsViewAnim != null) mFlipSettingsViewAnim.cancel(); + if (mScrollViewAnim != null) mScrollViewAnim.cancel(); + if (mClearButtonAnim != null) mClearButtonAnim.cancel(); + + mStackScroller.setVisibility(View.VISIBLE); + mNotificationPanel.setVisibility(View.GONE); + mFlipSettingsView.setVisibility(View.GONE); + + setAreThereNotifications(); // show the clear button + + mHeaderFlipper.reset(); + mKeyguardFlipper.reset(); mExpandedVisible = false; - mPile.setLayoutTransitionsEnabled(false); if (mNavigationBarView != null) mNavigationBarView.setSlippery(false); visibilityChanged(false); // Shrink the window to the size of the status bar only - WindowManager.LayoutParams lp = (WindowManager.LayoutParams) mStatusBarWindow.getLayoutParams(); - lp.height = getStatusBarHeight(); - lp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - lp.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - mWindowManager.updateViewLayout(mStatusBarWindow, lp); + mStatusBarWindowManager.setStatusBarExpanded(false); if ((mDisabled & StatusBarManager.DISABLE_NOTIFICATION_ICONS) == 0) { setNotificationIconVisibility(true, com.android.internal.R.anim.fade_in); @@ -1721,53 +1930,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false); - } - - /** - * Enables or disables layers on the children of the notifications pile. - * - * When layers are enabled, this method attempts to enable layers for the minimal - * number of children. Only children visible when the notification area is fully - * expanded will receive a layer. The technique used in this method might cause - * more children than necessary to get a layer (at most one extra child with the - * current UI.) - * - * @param layerType {@link View#LAYER_TYPE_NONE} or {@link View#LAYER_TYPE_HARDWARE} - */ - private void setPileLayers(int layerType) { - final int count = mPile.getChildCount(); - - switch (layerType) { - case View.LAYER_TYPE_NONE: - for (int i = 0; i < count; i++) { - mPile.getChildAt(i).setLayerType(layerType, null); - } - break; - case View.LAYER_TYPE_HARDWARE: - final int[] location = new int[2]; - mNotificationPanel.getLocationInWindow(location); - - final int left = location[0]; - final int top = location[1]; - final int right = left + mNotificationPanel.getWidth(); - final int bottom = top + getExpandedViewMaxHeight(); - - final Rect childBounds = new Rect(); - - for (int i = 0; i < count; i++) { - final View view = mPile.getChildAt(i); - view.getLocationInWindow(location); - childBounds.set(location[0], location[1], - location[0] + view.getWidth(), location[1] + view.getHeight()); - - if (childBounds.intersects(left, top, right, bottom)) { - view.setLayerType(layerType, null); - } - } - - break; - } + showBouncer(); } public boolean interceptTouchEvent(MotionEvent event) { @@ -1893,7 +2057,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } if (sbModeChanged || nbModeChanged) { // update transient bar autohide - if (sbMode == MODE_SEMI_TRANSPARENT || nbMode == MODE_SEMI_TRANSPARENT) { + if (mStatusBarMode == MODE_SEMI_TRANSPARENT || mNavigationBarMode == MODE_SEMI_TRANSPARENT) { scheduleAutohide(); } else { cancelAutohide(); @@ -1933,7 +2097,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { private void checkBarModes() { if (mDemoMode) return; int sbMode = mStatusBarMode; - if (panelsEnabled() && (mInteractingWindows & StatusBarManager.WINDOW_STATUS_BAR) != 0) { + if (panelsEnabled() && (mInteractingWindows & StatusBarManager.WINDOW_STATUS_BAR) != 0 + && !mOnKeyguard) { // if panels are expandable, force the status bar opaque on any interaction sbMode = MODE_OPAQUE; } @@ -2067,13 +2232,14 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { if (!isDeviceProvisioned()) return; // not for you - if (!notificationIsForCurrentUser(n)) return; + if (!notificationIsForCurrentProfiles(n)) return; // Show the ticker if one is requested. Also don't do this // until status bar window is attached to the window manager, // because... well, what's the point otherwise? And trying to // run a ticker without being attached will crash! - if (n.getNotification().tickerText != null && mStatusBarWindow.getWindowToken() != null) { + if (n.getNotification().tickerText != null && mStatusBarWindow != null + && mStatusBarWindow.getWindowToken() != null) { if (0 == (mDisabled & (StatusBarManager.DISABLE_NOTIFICATION_ICONS | StatusBarManager.DISABLE_NOTIFICATION_TICKER))) { mTicker.addEntry(n); @@ -2146,10 +2312,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { pw.println(" mTicking=" + mTicking); pw.println(" mTracking=" + mTracking); pw.println(" mDisplayMetrics=" + mDisplayMetrics); - pw.println(" mPile: " + viewInfo(mPile)); + pw.println(" mStackScroller: " + viewInfo(mStackScroller)); pw.println(" mTickerView: " + viewInfo(mTickerView)); - pw.println(" mScrollView: " + viewInfo(mScrollView) - + " scroll " + mScrollView.getScrollX() + "," + mScrollView.getScrollY()); + pw.println(" mStackScroller: " + viewInfo(mStackScroller) + + " scroll " + mStackScroller.getScrollX() + + "," + mStackScroller.getScrollY()); } pw.print(" mInteractingWindows="); pw.println(mInteractingWindows); @@ -2157,6 +2324,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { pw.println(windowStateToString(mStatusBarWindowState)); pw.print(" mStatusBarMode="); pw.println(BarTransitions.modeToString(mStatusBarMode)); + pw.print(" mZenMode="); + pw.println(Settings.Global.zenModeToString(mZenMode)); + pw.print(" mUseHeadsUp="); + pw.println(mUseHeadsUp); + pw.print(" interrupting package: "); + pw.println(hunStateToString(mInterruptingNotificationEntry)); dumpBarTransitions(pw, "mStatusBarView", mStatusBarView.getBarTransitions()); if (mNavigationBarView != null) { pw.print(" mNavigationBarWindowState="); @@ -2180,12 +2353,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { pw.print (" "); mNotificationPanel.dump(fd, pw, args); } - if (mSettingsPanel != null) { - pw.println(" mSettingsPanel=" + - mSettingsPanel + " params=" + mSettingsPanel.getLayoutParams().debug("")); - pw.print (" "); - mSettingsPanel.dump(fd, pw, args); - } if (DUMPTRUCK) { synchronized (mNotificationData) { @@ -2231,6 +2398,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mNetworkController.dump(fd, pw, args); } + private String hunStateToString(Entry entry) { + if (entry == null) return "null"; + if (entry.notification == null) return "corrupt"; + return entry.notification.getPackageName(); + } + private static void dumpBarTransitions(PrintWriter pw, String var, BarTransitions transitions) { pw.print(" "); pw.print(var); pw.print(".BarTransitions.mMode="); pw.println(BarTransitions.modeToString(transitions.getMode())); @@ -2242,30 +2415,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } private void addStatusBarWindow() { - // Put up the view - final int height = getStatusBarHeight(); - - // Now that the status bar window encompasses the sliding panel and its - // translucent backdrop, the entire thing is made TRANSLUCENT and is - // hardware-accelerated. - final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - height, - 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, - PixelFormat.TRANSLUCENT); - - lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; - - lp.gravity = getStatusBarGravity(); - lp.setTitle("StatusBar"); - lp.packageName = mContext.getPackageName(); - makeStatusBarView(); - mWindowManager.addView(mStatusBarWindow, lp); + mStatusBarWindowManager = new StatusBarWindowManager(mContext); + mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight()); } void setNotificationIconVisibility(boolean visible, int anim) { @@ -2303,17 +2455,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { lp.setMarginStart(mNotificationPanelMarginPx); mNotificationPanel.setLayoutParams(lp); - if (mSettingsPanel != null) { - lp = (FrameLayout.LayoutParams) mSettingsPanel.getLayoutParams(); - lp.gravity = mSettingsPanelGravity; - lp.setMarginEnd(mNotificationPanelMarginPx); - mSettingsPanel.setLayoutParams(lp); - } - if (ENABLE_HEADS_UP && mHeadsUpNotificationView != null) { mHeadsUpNotificationView.setMargin(mNotificationPanelMarginPx); - mPile.getLocationOnScreen(mPilePosition); - mHeadsUpVerticalOffset = mPilePosition[1] - mNaturalBarHeight; + mStackScroller.getLocationOnScreen(mStackScrollerPosition); + mHeadsUpVerticalOffset = mStackScrollerPosition[1] - mNaturalBarHeight; } updateCarrierLabelVisibility(false); @@ -2332,77 +2477,21 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { private View.OnClickListener mClearButtonListener = new View.OnClickListener() { public void onClick(View v) { synchronized (mNotificationData) { - // animate-swipe all dismissable notifications, then animate the shade closed - int numChildren = mPile.getChildCount(); - - int scrollTop = mScrollView.getScrollY(); - int scrollBottom = scrollTop + mScrollView.getHeight(); - final ArrayList<View> snapshot = new ArrayList<View>(numChildren); - for (int i=0; i<numChildren; i++) { - final View child = mPile.getChildAt(i); - if (mPile.canChildBeDismissed(child) && child.getBottom() > scrollTop && - child.getTop() < scrollBottom) { - snapshot.add(child); - } - } - if (snapshot.isEmpty()) { - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); - return; - } - new Thread(new Runnable() { + mPostCollapseCleanup = new Runnable() { @Override public void run() { - // Decrease the delay for every row we animate to give the sense of - // accelerating the swipes - final int ROW_DELAY_DECREMENT = 10; - int currentDelay = 140; - int totalDelay = 0; - - // Set the shade-animating state to avoid doing other work during - // all of these animations. In particular, avoid layout and - // redrawing when collapsing the shade. - mPile.setViewRemoval(false); - - mPostCollapseCleanup = new Runnable() { - @Override - public void run() { - if (DEBUG) { - Log.v(TAG, "running post-collapse cleanup"); - } - try { - mPile.setViewRemoval(true); - mBarService.onClearAllNotifications(); - } catch (Exception ex) { } - } - }; - - View sampleView = snapshot.get(0); - int width = sampleView.getWidth(); - final int dir = sampleView.isLayoutRtl() ? -1 : +1; - final int velocity = dir * width * 8; // 1000/8 = 125 ms duration - for (final View _v : snapshot) { - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - mPile.dismissRowAnimated(_v, velocity); - } - }, totalDelay); - currentDelay = Math.max(50, currentDelay - ROW_DELAY_DECREMENT); - totalDelay += currentDelay; + if (DEBUG) { + Log.v(TAG, "running post-collapse cleanup"); } - // Delay the collapse animation until after all swipe animations have - // finished. Provide some buffer because there may be some extra delay - // before actually starting each swipe animation. Ideally, we'd - // synchronize the end of those animations with the start of the collaps - // exactly. - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); - } - }, totalDelay + 225); + try { + mBarService.onClearAllNotifications(mCurrentUserId); + } catch (Exception ex) { } } - }).start(); + }; + + animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); + return; + // TODO: Handle this better with notification stack scroller } } }; @@ -2421,7 +2510,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { private View.OnClickListener mSettingsButtonListener = new View.OnClickListener() { public void onClick(View v) { - if (mHasSettingsPanel) { + if (mHasQuickSettings) { animateExpandSettingsPanel(); } else { startActivityDismissingKeyguard( @@ -2459,8 +2548,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { mScreenOn = false; - // no waiting! - makeExpandedInvisible(); notifyNavigationBarScreenOn(false); notifyHeadsUpScreenOn(false); finishBarAnimations(); @@ -2549,7 +2636,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mBarService.onNotificationClear( mInterruptingNotificationEntry.notification.getPackageName(), mInterruptingNotificationEntry.notification.getTag(), - mInterruptingNotificationEntry.notification.getId()); + mInterruptingNotificationEntry.notification.getId(), + mInterruptingNotificationEntry.notification.getUserId()); } catch (android.os.RemoteException ex) { // oh well } @@ -2620,11 +2708,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { if (mNotificationPanelGravity <= 0) { mNotificationPanelGravity = Gravity.START | Gravity.TOP; } - mSettingsPanelGravity = res.getInteger(R.integer.settings_panel_layout_gravity); - Log.d(TAG, "mSettingsPanelGravity = " + mSettingsPanelGravity); - if (mSettingsPanelGravity <= 0) { - mSettingsPanelGravity = Gravity.END | Gravity.TOP; - } mCarrierLabelHeight = res.getDimensionPixelSize(R.dimen.carrier_label_height); mNotificationHeaderHeight = res.getDimensionPixelSize(R.dimen.notification_panel_header_height); @@ -2635,11 +2718,49 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } mHeadsUpNotificationDecay = res.getInteger(R.integer.heads_up_notification_decay); - mRowHeight = res.getDimensionPixelSize(R.dimen.notification_row_min_height); + mRowMinHeight = res.getDimensionPixelSize(R.dimen.notification_min_height); + mRowMaxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height); + + mKeyguardMaxNotificationCount = res.getInteger(R.integer.keyguard_max_notification_count); if (false) Log.v(TAG, "updateResources"); } + // Visibility reporting + + @Override + protected void visibilityChanged(boolean visible) { + if (visible) { + mStackScroller.setChildLocationsChangedListener(mNotificationLocationsChangedListener); + } else { + // Report all notifications as invisible and turn down the + // reporter. + if (!mCurrentlyVisibleNotifications.isEmpty()) { + logNotificationVisibilityChanges( + Collections.<String>emptyList(), mCurrentlyVisibleNotifications); + mCurrentlyVisibleNotifications.clear(); + } + mHandler.removeCallbacks(mVisibilityReporter); + mStackScroller.setChildLocationsChangedListener(null); + } + super.visibilityChanged(visible); + } + + private void logNotificationVisibilityChanges( + Collection<String> newlyVisible, Collection<String> noLongerVisible) { + if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) { + return; + } + + String[] newlyVisibleAr = newlyVisible.toArray(new String[newlyVisible.size()]); + String[] noLongerVisibleAr = noLongerVisible.toArray(new String[noLongerVisible.size()]); + try { + mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr); + } catch (RemoteException e) { + // Ignore. + } + } + // // tracing // @@ -2651,7 +2772,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { void vibrate() { android.os.Vibrator vib = (android.os.Vibrator)mContext.getSystemService( Context.VIBRATOR_SERVICE); - vib.vibrate(250); + vib.vibrate(250, AudioManager.STREAM_SYSTEM); } Runnable mStartTracing = new Runnable() { @@ -2684,6 +2805,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { || (mDisabled & StatusBarManager.DISABLE_SEARCH) != 0; } + public void startSettingsActivity(String action) { + if (mQS != null) { + mQS.startSettingsActivity(action); + } + } + private static class FastColorDrawable extends Drawable { private final int mColor; @@ -2723,9 +2850,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { super.destroy(); if (mStatusBarWindow != null) { mWindowManager.removeViewImmediate(mStatusBarWindow); + mStatusBarWindow = null; } if (mNavigationBarView != null) { mWindowManager.removeViewImmediate(mNavigationBarView); + mNavigationBarView = null; } mContext.unregisterReceiver(mBroadcastReceiver); } @@ -2766,6 +2895,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { 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 : @@ -2791,4 +2929,271 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { ((DemoMode)v).dispatchDemoCommand(command, args); } } + + public boolean isOnKeyguard() { + return mOnKeyguard; + } + + public void showKeyguard() { + mOnKeyguard = true; + updateKeyguardState(); + instantExpandNotificationsPanel(); + } + + public void hideKeyguard() { + mOnKeyguard = false; + updateKeyguardState(); + instantCollapseNotificationPanel(); + } + + private void updatePublicMode() { + setLockscreenPublicMode(mOnKeyguard && mStatusBarKeyguardViewManager.isSecure()); + } + + private void updateKeyguardState() { + if (mOnKeyguard) { + if (isFlippedToSettings()) { + flipToNotifications(false /*animate*/); + } + mKeyguardStatusView.setVisibility(View.VISIBLE); + mKeyguardBottomArea.setVisibility(View.VISIBLE); + mKeyguardIndicationTextView.setVisibility(View.VISIBLE); + mKeyguardIndicationTextView.switchIndication(mKeyguardHotwordPhrase); + mKeyguardCarrierLabel.setVisibility(View.VISIBLE); + mNotificationPanelHeader.setVisibility(View.GONE); + + mKeyguardFlipper.setVisibility(View.VISIBLE); + mSettingsContainer.setKeyguardShowing(true); + } else { + mKeyguardStatusView.setVisibility(View.GONE); + mKeyguardBottomArea.setVisibility(View.GONE); + mKeyguardIndicationTextView.setVisibility(View.GONE); + mKeyguardCarrierLabel.setVisibility(View.GONE); + mNotificationPanelHeader.setVisibility(View.VISIBLE); + + mKeyguardFlipper.setVisibility(View.GONE); + mSettingsContainer.setKeyguardShowing(false); + } + + updatePublicMode(); + updateRowStates(); + checkBarModes(); + updateNotificationIcons(); + updateCarrierLabelVisibility(false); + } + + public void userActivity() { + if (mOnKeyguard) { + mKeyguardViewMediatorCallback.userActivity(); + } + } + + public boolean onMenuPressed() { + return mOnKeyguard && mStatusBarKeyguardViewManager.onMenuPressed(); + } + + public boolean onBackPressed() { + if (mOnKeyguard) { + return mStatusBarKeyguardViewManager.onBackPressed(); + } else { + animateCollapsePanels(); + return true; + } + } + + private void showBouncer() { + if (mOnKeyguard) { + mStatusBarKeyguardViewManager.dismiss(); + } + } + + private void instantExpandNotificationsPanel() { + + // Make our window larger and the panel visible. + makeExpandedVisible(true); + mNotificationPanel.setVisibility(View.VISIBLE); + + // Wait for window manager to pickup the change, so we know the maximum height of the panel + // then. + mNotificationPanel.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (mStatusBarWindow.getHeight() != getStatusBarHeight()) { + mNotificationPanel.getViewTreeObserver().removeOnGlobalLayoutListener(this); + mNotificationPanel.setExpandedFraction(1); + } + } + }); + } + + private void instantCollapseNotificationPanel() { + mNotificationPanel.setExpandedFraction(0); + } + + @Override + public void onActivated(View view) { + userActivity(); + mKeyguardIndicationTextView.switchIndication(R.string.notification_tap_again); + super.onActivated(view); + } + + @Override + public void onReset(View view) { + super.onReset(view); + mKeyguardIndicationTextView.switchIndication(mKeyguardHotwordPhrase); + } + + public void onTrackingStarted() { + if (mOnKeyguard) { + mKeyguardIndicationTextView.switchIndication(R.string.keyguard_unlock); + } + } + + public void onTrackingStopped() { + if (mOnKeyguard) { + mKeyguardIndicationTextView.switchIndication(mKeyguardHotwordPhrase); + } + } + + @Override + protected int getMaxKeyguardNotifications() { + return mKeyguardMaxNotificationCount; + } + + public NavigationBarView getNavigationBarView() { + return mNavigationBarView; + } + + /** + * @return a ViewGroup that spans the entire panel which contains the quick settings + */ + public ViewGroup getQuickSettingsOverlayParent() { + if (mHasQuickSettings) { + return mNotificationPanel; + } else { + return null; + } + } + + public static boolean inBounds(View view, MotionEvent event, boolean orAbove) { + final int[] location = new int[2]; + view.getLocationInWindow(location); + final int rx = (int) event.getRawX(); + final int ry = (int) event.getRawY(); + return rx >= location[0] && rx <= location[0] + view.getMeasuredWidth() + && (orAbove || ry >= location[1]) && ry <= location[1] + view.getMeasuredHeight(); + } + + private final class FlipperButton { + private final View mHolder; + + private ImageView mSettingsButton, mNotificationButton; + private Animator mSettingsButtonAnim, mNotificationButtonAnim; + + public FlipperButton(View holder) { + mHolder = holder; + mSettingsButton = (ImageView) holder.findViewById(R.id.settings_button); + if (mSettingsButton != null) { + mSettingsButton.setOnClickListener(mSettingsButtonListener); + if (mHasQuickSettings) { + // the settings panel is hiding behind this button + mSettingsButton.setImageResource(R.drawable.ic_notify_quicksettings); + mSettingsButton.setVisibility(View.VISIBLE); + } else { + // no settings panel, go straight to settings + mSettingsButton.setVisibility(View.VISIBLE); + mSettingsButton.setImageResource(R.drawable.ic_notify_settings); + } + } + mNotificationButton = (ImageView) holder.findViewById(R.id.notification_button); + if (mNotificationButton != null) { + mNotificationButton.setOnClickListener(mNotificationButtonListener); + } + } + + public boolean inHolderBounds(MotionEvent event) { + return inBounds(mHolder, event, false); + } + + public void provisionCheck(boolean provisioned) { + if (mSettingsButton != null) { + mSettingsButton.setEnabled(provisioned); + } + } + + public void userSetup(boolean userSetup) { + if (mSettingsButton != null) { + mSettingsButton.setVisibility(userSetup ? View.VISIBLE : View.INVISIBLE); + } + } + + public void reset() { + cancel(); + mSettingsButton.setVisibility(View.VISIBLE); + mNotificationButton.setVisibility(View.GONE); + } + + public void refreshLayout() { + if (mSettingsButton != null) { + // Force asset reloading + mSettingsButton.setImageDrawable(null); + mSettingsButton.setImageResource(R.drawable.ic_notify_quicksettings); + } + + if (mNotificationButton != null) { + // Force asset reloading + mNotificationButton.setImageDrawable(null); + mNotificationButton.setImageResource(R.drawable.ic_notifications); + } + } + + public void flipToSettings(boolean animate) { + mNotificationButton.setVisibility(View.VISIBLE); + if (animate) { + mSettingsButtonAnim = start( + setVisibilityWhenDone( + ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, 0f) + .setDuration(FLIP_DURATION_OUT), + mStackScroller, View.INVISIBLE)); + mNotificationButtonAnim = start( + startDelay(FLIP_DURATION_OUT, + ObjectAnimator.ofFloat(mNotificationButton, View.ALPHA, 1f) + .setDuration(FLIP_DURATION_IN))); + } else { + mSettingsButton.setAlpha(0f); + mSettingsButton.setVisibility(View.INVISIBLE); + mNotificationButton.setAlpha(1f); + } + } + + public void flipToNotifications(boolean animate) { + mSettingsButton.setVisibility(View.VISIBLE); + if (animate) { + mNotificationButtonAnim = start( + setVisibilityWhenDone( + ObjectAnimator.ofFloat(mNotificationButton, View.ALPHA, 0f) + .setDuration(FLIP_DURATION_OUT), + mNotificationButton, View.INVISIBLE)); + + mSettingsButtonAnim = start( + startDelay(FLIP_DURATION_OUT, + ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, 1f) + .setDuration(FLIP_DURATION_IN))); + } else { + mNotificationButton.setVisibility(View.INVISIBLE); + mNotificationButton.setAlpha(0f); + mSettingsButton.setAlpha(1f); + } + } + + public void cancel() { + cancelAnim(mSettingsButtonAnim); + cancelAnim(mNotificationButtonAnim); + } + + public void setVisibility(int vis) { + mHolder.setVisibility(vis); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index d0e9a99..79c63f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -37,13 +37,11 @@ public class PhoneStatusBarView extends PanelBar { PhoneStatusBar mBar; int mScrimColor; - float mSettingsPanelDragzoneFrac; - float mSettingsPanelDragzoneMin; + int mScrimColorKeyguard; - boolean mFullWidthNotifications; PanelView mFadingPanel = null; PanelView mLastFullyOpenedPanel = null; - PanelView mNotificationPanel, mSettingsPanel; + PanelView mNotificationPanel; private boolean mShouldFade; private final PhoneStatusBarTransitions mBarTransitions; @@ -52,13 +50,7 @@ public class PhoneStatusBarView extends PanelBar { Resources res = getContext().getResources(); mScrimColor = res.getColor(R.color.notification_panel_scrim_color); - mSettingsPanelDragzoneMin = res.getDimension(R.dimen.settings_panel_dragzone_min); - try { - mSettingsPanelDragzoneFrac = res.getFraction(R.dimen.settings_panel_dragzone_fraction, 1, 1); - } catch (NotFoundException ex) { - mSettingsPanelDragzoneFrac = 0f; - } - mFullWidthNotifications = mSettingsPanelDragzoneFrac <= 0f; + mScrimColorKeyguard = res.getColor(R.color.notification_panel_scrim_color_keyguard); mBarTransitions = new PhoneStatusBarTransitions(this); } @@ -70,15 +62,8 @@ public class PhoneStatusBarView extends PanelBar { mBar = bar; } - public boolean hasFullWidthNotifications() { - return mFullWidthNotifications; - } - @Override public void onAttachedToWindow() { - for (PanelView pv : mPanels) { - pv.setRubberbandingEnabled(!mFullWidthNotifications); - } mBarTransitions.init(); } @@ -87,10 +72,7 @@ public class PhoneStatusBarView extends PanelBar { super.addPanel(pv); if (pv.getId() == R.id.notification_panel) { mNotificationPanel = pv; - } else if (pv.getId() == R.id.settings_panel){ - mSettingsPanel = pv; } - pv.setRubberbandingEnabled(!mFullWidthNotifications); } @Override @@ -115,40 +97,16 @@ public class PhoneStatusBarView extends PanelBar { @Override public PanelView selectPanelForTouch(MotionEvent touch) { - final float x = touch.getX(); - final boolean isLayoutRtl = isLayoutRtl(); - - if (mFullWidthNotifications) { - // No double swiping. If either panel is open, nothing else can be pulled down. - return ((mSettingsPanel == null ? 0 : mSettingsPanel.getExpandedHeight()) - + mNotificationPanel.getExpandedHeight() > 0) - ? null - : mNotificationPanel; - } - - // We split the status bar into thirds: the left 2/3 are for notifications, and the - // right 1/3 for quick settings. If you pull the status bar down a second time you'll - // toggle panels no matter where you pull it down. - - final float w = getMeasuredWidth(); - float region = (w * mSettingsPanelDragzoneFrac); - - if (DEBUG) { - Log.v(TAG, String.format( - "w=%.1f frac=%.3f region=%.1f min=%.1f x=%.1f w-x=%.1f", - w, mSettingsPanelDragzoneFrac, region, mSettingsPanelDragzoneMin, x, (w-x))); - } - - if (region < mSettingsPanelDragzoneMin) region = mSettingsPanelDragzoneMin; - - final boolean showSettings = isLayoutRtl ? (x < region) : (w - region < x); - return showSettings ? mSettingsPanel : mNotificationPanel; + // No double swiping. If either panel is open, nothing else can be pulled down. + return mNotificationPanel.getExpandedHeight() > 0 + ? null + : mNotificationPanel; } @Override public void onPanelPeeked() { super.onPanelPeeked(); - mBar.makeExpandedVisible(); + mBar.makeExpandedVisible(false); } @Override @@ -170,7 +128,7 @@ public class PhoneStatusBarView extends PanelBar { mBar.makeExpandedInvisibleSoon(); mFadingPanel = null; mLastFullyOpenedPanel = null; - if (mScrimColor != 0 && ActivityManager.isHighEndGfx()) { + if (mScrimColor != 0 && ActivityManager.isHighEndGfx() && mBar.mStatusBarWindow != null) { mBar.mStatusBarWindow.setBackgroundColor(0); } } @@ -202,6 +160,18 @@ public class PhoneStatusBarView extends PanelBar { } @Override + public void onTrackingStarted(PanelView panel) { + super.onTrackingStarted(panel); + mBar.onTrackingStarted(); + } + + @Override + public void onTrackingStopped(PanelView panel) { + super.onTrackingStopped(panel); + mBar.onTrackingStopped(); + } + + @Override public boolean onInterceptTouchEvent(MotionEvent event) { return mBar.interceptTouchEvent(event) || super.onInterceptTouchEvent(event); } @@ -214,8 +184,10 @@ public class PhoneStatusBarView extends PanelBar { Log.v(TAG, "panelExpansionChanged: f=" + frac); } - if (panel == mFadingPanel && mScrimColor != 0 && ActivityManager.isHighEndGfx()) { + if (panel == mFadingPanel && mScrimColor != 0 && ActivityManager.isHighEndGfx() + && mBar.mStatusBarWindow != null) { if (mShouldFade) { + int scrimColor = mBar.isOnKeyguard() ? mScrimColorKeyguard : mScrimColor; frac = mPanelExpandedFractionSum; // don't judge me // let's start this 20% of the way down the screen frac = frac * 1.2f - 0.2f; @@ -225,7 +197,7 @@ public class PhoneStatusBarView extends PanelBar { // woo, special effects final float k = (float)(1f-0.5f*(1f-Math.cos(3.14159f * Math.pow(1f-frac, 2f)))); // attenuate background color alpha by k - final int color = (int) ((mScrimColor >>> 24) * k) << 24 | (mScrimColor & 0xFFFFFF); + final int color = (int) ((scrimColor >>> 24) * k) << 24 | (scrimColor & 0xFFFFFF); mBar.mStatusBarWindow.setBackgroundColor(color); } } @@ -248,5 +220,6 @@ public class PhoneStatusBarView extends PanelBar { mBar.animateHeadsUp(mNotificationPanel == panel, mPanelExpandedFractionSum); mBar.updateCarrierLabelVisibility(false); + mBar.userActivity(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java index e7b8fa1..f3ebf1b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java @@ -25,6 +25,7 @@ import android.app.admin.DevicePolicyManager; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; @@ -32,6 +33,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.UserInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; @@ -41,6 +43,7 @@ import android.hardware.display.DisplayManager; import android.media.MediaRouter; import android.net.wifi.WifiManager; import android.os.AsyncTask; +import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; @@ -51,18 +54,24 @@ import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Profile; import android.provider.Settings; import android.security.KeyChain; +import android.text.TextUtils.TruncateAt; import android.util.Log; import android.util.Pair; import android.view.LayoutInflater; +import android.view.Menu; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.view.WindowManager.LayoutParams; +import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import com.android.internal.app.MediaRouteDialogPresenter; import com.android.systemui.R; +import com.android.systemui.settings.UserSwitcherHostView; import com.android.systemui.statusbar.phone.QuickSettingsModel.ActivityState; import com.android.systemui.statusbar.phone.QuickSettingsModel.BluetoothState; import com.android.systemui.statusbar.phone.QuickSettingsModel.RSSIState; @@ -84,6 +93,7 @@ class QuickSettings { static final boolean DEBUG_GONE_TILES = false; private static final String TAG = "QuickSettings"; public static final boolean SHOW_IME_TILE = false; + public static final boolean SHOW_ACCESSIBILITY_TILES = true; public static final boolean LONG_PRESS_TOGGLES = true; @@ -174,7 +184,9 @@ class QuickSettings { bluetoothController.addStateChangedCallback(mModel); batteryController.addStateChangedCallback(mModel); locationController.addSettingsChangedCallback(mModel); - rotationLockController.addRotationLockControllerCallback(mModel); + if (rotationLockController != null) { + rotationLockController.addRotationLockControllerCallback(mModel); + } } private void queryForSslCaCerts() { @@ -268,13 +280,14 @@ class QuickSettings { addUserTiles(mContainerView, inflater); addSystemTiles(mContainerView, inflater); addTemporaryTiles(mContainerView, inflater); + addAccessibilityTiles(mContainerView); queryForUserInformation(); queryForSslCaCerts(); mTilesSetUp = true; } - private void startSettingsActivity(String action) { + public void startSettingsActivity(String action) { Intent intent = new Intent(action); startSettingsActivity(intent); } @@ -299,30 +312,56 @@ class QuickSettings { collapsePanels(); } - private void addUserTiles(ViewGroup parent, LayoutInflater inflater) { + private void addAccessibilityTiles(ViewGroup parent) { + if (!DEBUG_GONE_TILES && !SHOW_ACCESSIBILITY_TILES) return; + + // Color inversion tile + final SystemSettingTile inversionTile = new SystemSettingTile(mContext); + inversionTile.setUri(Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, + SystemSettingTile.TYPE_SECURE); + inversionTile.setFragment("Settings$AccessibilityInversionSettingsActivity"); + mModel.addInversionTile(inversionTile, inversionTile.getRefreshCallback()); + parent.addView(inversionTile); + + // Contrast enhancement tile + final SystemSettingTile contrastTile = new SystemSettingTile(mContext); + contrastTile.setUri(Settings.Secure.ACCESSIBILITY_DISPLAY_CONTRAST_ENABLED, + SystemSettingTile.TYPE_SECURE); + contrastTile.setFragment("Settings$AccessibilityContrastSettingsActivity"); + mModel.addContrastTile(contrastTile, contrastTile.getRefreshCallback()); + parent.addView(contrastTile); + + // Color space adjustment tile + final SystemSettingTile colorSpaceTile = new SystemSettingTile(mContext); + colorSpaceTile.setUri(Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, + SystemSettingTile.TYPE_SECURE); + colorSpaceTile.setFragment("Settings$AccessibilityDaltonizerSettingsActivity"); + mModel.addColorSpaceTile(colorSpaceTile, colorSpaceTile.getRefreshCallback()); + parent.addView(colorSpaceTile); + } + + private void addUserTiles(final ViewGroup parent, final LayoutInflater inflater) { QuickSettingsTileView userTile = (QuickSettingsTileView) inflater.inflate(R.layout.quick_settings_tile, parent, false); userTile.setContent(R.layout.quick_settings_tile_user, inflater); userTile.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - collapsePanels(); final UserManager um = UserManager.get(mContext); - if (um.getUsers(true).size() > 1) { - // Since keyguard and systemui were merged into the same process to save - // memory, they share the same Looper and graphics context. As a result, - // there's no way to allow concurrent animation while keyguard inflates. - // The workaround is to add a slight delay to allow the animation to finish. - mHandler.postDelayed(new Runnable() { + if (um.isUserSwitcherEnabled()) { + final ViewGroup switcherParent = getService().getQuickSettingsOverlayParent(); + final UserSwitcherHostView switcher = (UserSwitcherHostView) inflater.inflate( + R.layout.user_switcher_host, switcherParent, false); + switcher.setFinishRunnable(new Runnable() { + @Override public void run() { - try { - WindowManagerGlobal.getWindowManagerService().lockNow(null); - } catch (RemoteException e) { - Log.e(TAG, "Couldn't show user switcher", e); - } + switcherParent.removeView(switcher); } - }, 400); // TODO: ideally this would be tied to the collapse of the panel + }); + switcher.refreshUsers(); + switcherParent.addView(switcher); } else { + collapsePanels(); Intent intent = ContactsContract.QuickContact.composeQuickContactsIntent( mContext, v, ContactsContract.Profile.CONTENT_URI, ContactsContract.QuickContact.MODE_LARGE, null); @@ -472,8 +511,7 @@ class QuickSettings { } // Rotation Lock - if (mContext.getResources().getBoolean(R.bool.quick_settings_show_rotation_lock) - || DEBUG_GONE_TILES) { + if (mRotationLockController != null) { final QuickSettingsBasicTile rotationLockTile = new QuickSettingsBasicTile(mContext); rotationLockTile.setOnClickListener(new View.OnClickListener() { @@ -536,24 +574,6 @@ class QuickSettings { }); parent.addView(batteryTile); - // Airplane Mode - final QuickSettingsBasicTile airplaneTile - = new QuickSettingsBasicTile(mContext); - mModel.addAirplaneModeTile(airplaneTile, new QuickSettingsModel.RefreshCallback() { - @Override - public void refreshView(QuickSettingsTileView unused, State state) { - airplaneTile.setImageResource(state.iconId); - - String airplaneState = mContext.getString( - (state.enabled) ? R.string.accessibility_desc_on - : R.string.accessibility_desc_off); - airplaneTile.setContentDescription( - mContext.getString(R.string.accessibility_quick_settings_airplane, airplaneState)); - airplaneTile.setText(state.label); - } - }); - parent.addView(airplaneTile); - // Bluetooth if (mModel.deviceSupportsBluetooth() || DEBUG_GONE_TILES) { @@ -647,6 +667,50 @@ class QuickSettings { } }); parent.addView(locationTile); + + // Airplane Mode + final QuickSettingsBasicTile airplaneTile + = new QuickSettingsBasicTile(mContext); + mModel.addAirplaneModeTile(airplaneTile, new QuickSettingsModel.RefreshCallback() { + @Override + public void refreshView(QuickSettingsTileView unused, State state) { + airplaneTile.setImageResource(state.iconId); + + String airplaneState = mContext.getString( + (state.enabled) ? R.string.accessibility_desc_on + : R.string.accessibility_desc_off); + airplaneTile.setContentDescription( + mContext.getString(R.string.accessibility_quick_settings_airplane, + airplaneState)); + airplaneTile.setText(state.label); + } + }); + parent.addView(airplaneTile); + + // Zen Mode + final QuickSettingsBasicTile zenModeTile = new QuickSettingsBasicTile(mContext); + zenModeTile.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showZenModeDialog(); + } + }); + mModel.addZenModeTile(zenModeTile, new QuickSettingsModel.RefreshCallback() { + @Override + public void refreshView(QuickSettingsTileView unused, State state) { + zenModeTile.setImageResource(state.iconId); + // TODO cut new assets + zenModeTile.getImageView().setAlpha(state.enabled ? 1 : .2f); + zenModeTile.getImageView().setScaleX(1.5f); + zenModeTile.getImageView().setScaleY(1.5f); + // for landscape version + zenModeTile.getTextView().setMaxLines(2); + zenModeTile.getTextView().setEllipsize(TruncateAt.END); + // TODO content description + zenModeTile.setText(state.label); + } + }); + parent.addView(zenModeTile); } private void addTemporaryTiles(final ViewGroup parent, final LayoutInflater inflater) { @@ -832,6 +896,44 @@ class QuickSettings { dialog.show(); } + private void showZenModeDialog() { + final Dialog d = new Dialog(mContext); + d.requestWindowFeature(Window.FEATURE_NO_TITLE); + d.setCancelable(true); + d.setCanceledOnTouchOutside(true); + final ZenModeView v = new ZenModeView(mContext) { + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + WindowManager.LayoutParams lp = d.getWindow().getAttributes(); + lp.width = mContext.getResources() + .getDimensionPixelSize(R.dimen.zen_mode_dialog_width); + d.getWindow().setAttributes(lp); + } + }; + v.setAutoActivate(true); + v.setAdapter(new ZenModeViewAdapter(mContext) { + @Override + public void configure() { + if (mStatusBarService != null) { + mStatusBarService.startSettingsActivity(Settings.ACTION_ZEN_MODE_SETTINGS); + } + d.dismiss(); + } + @Override + public void close() { + d.dismiss(); + } + }); + d.setContentView(v); + d.create(); + d.getWindow().setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); + WindowManager.LayoutParams lp = d.getWindow().getAttributes(); + lp.width = mContext.getResources().getDimensionPixelSize(R.dimen.zen_mode_dialog_width); + d.getWindow().setAttributes(lp); + d.show(); + } + private void applyBluetoothStatus() { mModel.onBluetoothStateChange(mBluetoothState); } @@ -918,4 +1020,108 @@ class QuickSettings { } } } + + /** + * Quick Setting tile that represents a secure setting. This type of tile + * can toggle a URI within Settings.Secure on click and launch a Settings + * fragment on long-click. + */ + public class SystemSettingTile extends QuickSettingsBasicTile { + private static final int TYPE_GLOBAL = 0; + private static final int TYPE_SECURE = 1; + private static final int TYPE_SYSTEM = 2; + + private final QuickSettingsModel.BasicRefreshCallback mRefreshCallback; + + private String mFragment; + private String mName; + private int mType; + + public SystemSettingTile(Context context) { + super(context); + + mRefreshCallback = new QuickSettingsModel.BasicRefreshCallback(this); + mRefreshCallback.setShowWhenEnabled(true); + } + + @Override + public boolean performLongClick() { + if (mFragment != null) { + collapsePanels(); + + final Intent intent = new Intent(); + intent.setComponent(new ComponentName( + "com.android.settings", "com.android.settings." + mFragment)); + startSettingsActivity(intent); + return true; + } + return false; + } + + @Override + public boolean performClick() { + if (mName != null) { + collapsePanels(); + + final ContentResolver cr = mContext.getContentResolver(); + switch (mType) { + case TYPE_GLOBAL: { + final boolean enable = Settings.Global.getInt(cr, mName, 0) == 0; + Settings.Global.putInt(cr, mName, enable ? 1 : 0); + } break; + case TYPE_SECURE: { + final boolean enable = Settings.Secure.getIntForUser( + cr, mName, 0, UserHandle.USER_CURRENT) == 0; + Settings.Secure.putIntForUser( + cr, mName, enable ? 1 : 0, UserHandle.USER_CURRENT); + } break; + case TYPE_SYSTEM: { + final boolean enable = Settings.System.getIntForUser( + cr, mName, 0, UserHandle.USER_CURRENT) == 0; + Settings.System.putIntForUser( + cr, mName, enable ? 1 : 0, UserHandle.USER_CURRENT); + } break; + } + return true; + } + return false; + } + + /** + * Specifies the fragment within the com.android.settings package to + * launch when this tile is long-clicked. + * + * @param fragment a fragment name within the com.android.settings + * package + */ + public void setFragment(String fragment) { + mFragment = fragment; + setLongClickable(fragment != null); + } + + /** + * Specifies the setting name and type to toggle when this tile is + * clicked. + * + * @param name a setting name + * @param type the type of setting, one of: + * <ul> + * <li>{@link #TYPE_GLOBAL} + * <li>{@link #TYPE_SECURE} + * <li>{@link #TYPE_SYSTEM} + * </ul> + */ + public void setUri(String name, int type) { + mName = name; + mType = type; + setClickable(mName != null); + } + + /** + * @return the refresh callback for this tile + */ + public QuickSettingsModel.BasicRefreshCallback getRefreshCallback() { + return mRefreshCallback; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsContainerView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsContainerView.java index 17ee017..c44cb0c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsContainerView.java @@ -19,7 +19,13 @@ package com.android.systemui.statusbar.phone; import android.animation.LayoutTransition; import android.content.Context; import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.Typeface; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -31,30 +37,59 @@ import com.android.systemui.R; */ class QuickSettingsContainerView extends FrameLayout { + private static boolean sShowScrim = true; + + private final Context mContext; + // The number of columns in the QuickSettings grid private int mNumColumns; + private boolean mKeyguardShowing; + private int mMaxRows; + private int mMaxRowsOnKeyguard; + // The gap between tiles in the QuickSettings grid private float mCellGap; + private ScrimView mScrim; + public QuickSettingsContainerView(Context context, AttributeSet attrs) { super(context, attrs); - + mContext = context; updateResources(); } @Override protected void onFinishInflate() { super.onFinishInflate(); - + if (sShowScrim) { + mScrim = new ScrimView(mContext); + addView(mScrim); + } // TODO: Setup the layout transitions LayoutTransition transitions = getLayoutTransition(); } + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mScrim != null) { + sShowScrim = false; + removeView(mScrim); + } + return super.onTouchEvent(event); + } + void updateResources() { Resources r = getContext().getResources(); mCellGap = r.getDimension(R.dimen.quick_settings_cell_gap); mNumColumns = r.getInteger(R.integer.quick_settings_num_columns); + mMaxRows = r.getInteger(R.integer.quick_settings_max_rows); + mMaxRowsOnKeyguard = r.getInteger(R.integer.quick_settings_max_rows_keyguard); + requestLayout(); + } + + void setKeyguardShowing(boolean showing) { + mKeyguardShowing = showing; requestLayout(); } @@ -71,10 +106,18 @@ class QuickSettingsContainerView extends FrameLayout { final int N = getChildCount(); int cellHeight = 0; int cursor = 0; + int maxRows = mKeyguardShowing ? mMaxRowsOnKeyguard : mMaxRows; + for (int i = 0; i < N; ++i) { + if (getChildAt(i).equals(mScrim)) { + continue; + } // Update the child's width QuickSettingsTileView v = (QuickSettingsTileView) getChildAt(i); if (v.getVisibility() != View.GONE) { + int row = (int) (cursor / mNumColumns); + if (row >= maxRows) continue; + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); int colSpan = v.getColumnSpan(); lp.width = (int) ((colSpan * cellWidth) + (colSpan - 1) * mCellGap); @@ -102,6 +145,9 @@ class QuickSettingsContainerView extends FrameLayout { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (mScrim != null) { + mScrim.bringToFront(); + } final int N = getChildCount(); final boolean isLayoutRtl = isLayoutRtl(); final int width = getWidth(); @@ -109,8 +155,18 @@ class QuickSettingsContainerView extends FrameLayout { int x = getPaddingStart(); int y = getPaddingTop(); int cursor = 0; + int maxRows = mKeyguardShowing ? mMaxRowsOnKeyguard : mMaxRows; for (int i = 0; i < N; ++i) { + if (getChildAt(i).equals(mScrim)) { + int w = right - left - getPaddingLeft() - getPaddingRight(); + int h = bottom - top - getPaddingTop() - getPaddingBottom(); + mScrim.measure( + MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)); + mScrim.layout(getPaddingLeft(), getPaddingTop(), right, bottom); + continue; + } QuickSettingsTileView child = (QuickSettingsTileView) getChildAt(i); ViewGroup.LayoutParams lp = child.getLayoutParams(); if (child.getVisibility() != GONE) { @@ -121,6 +177,7 @@ class QuickSettingsContainerView extends FrameLayout { final int childHeight = lp.height; int row = (int) (cursor / mNumColumns); + if (row >= maxRows) continue; // Push the item to the next row if it can't fit on this one if ((col + colSpan) > mNumColumns) { @@ -150,4 +207,89 @@ class QuickSettingsContainerView extends FrameLayout { } } } + + private static final class ScrimView extends View { + private static final int SCRIM = 0x4f000000; + private static final int COLOR = 0xaf4285f4; + + private final Paint mLinePaint; + private final int mStrokeWidth; + private final Rect mTmp = new Rect(); + private final Paint mTextPaint; + private final int mTextSize; + + public ScrimView(Context context) { + super(context); + setFocusable(false); + final Resources res = context.getResources(); + mStrokeWidth = res.getDimensionPixelSize(R.dimen.quick_settings_tmp_scrim_stroke_width); + mTextSize = res.getDimensionPixelSize(R.dimen.quick_settings_tmp_scrim_text_size); + + mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mLinePaint.setColor(COLOR); + mLinePaint.setStrokeWidth(mStrokeWidth); + mLinePaint.setStrokeJoin(Paint.Join.ROUND); + mLinePaint.setStrokeCap(Paint.Cap.ROUND); + mLinePaint.setStyle(Paint.Style.STROKE); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setColor(COLOR); + mTextPaint.setTextSize(mTextSize); + mTextPaint.setTypeface(Typeface.create("sans-serif-condensed", Typeface.BOLD)); + } + + @Override + protected void onDraw(Canvas canvas) { + final int w = getMeasuredWidth(); + final int h = getMeasuredHeight(); + final int f = mStrokeWidth * 3 / 4; + + canvas.drawColor(SCRIM); + canvas.drawPath(line(f, h / 2, w - f, h / 2), mLinePaint); + canvas.drawPath(line(w / 2, f, w / 2, h - f), mLinePaint); + + final int s = mStrokeWidth; + mTextPaint.setTextAlign(Paint.Align.RIGHT); + canvas.drawText("FUTURE", w / 2 - s, h / 2 - s, mTextPaint); + mTextPaint.setTextAlign(Paint.Align.LEFT); + canvas.drawText("SITE OF", w / 2 + s, h / 2 - s , mTextPaint); + mTextPaint.setTextAlign(Paint.Align.RIGHT); + drawUnder(canvas, "QUANTUM", w / 2 - s, h / 2 + s); + mTextPaint.setTextAlign(Paint.Align.LEFT); + drawUnder(canvas, "SETTINGS", w / 2 + s, h / 2 + s); + } + + private void drawUnder(Canvas c, String text, float x, float y) { + if (mTmp.isEmpty()) { + mTextPaint.getTextBounds(text, 0, text.length(), mTmp); + } + c.drawText(text, x, y + mTmp.height() * .85f, mTextPaint); + } + + private Path line(float x1, float y1, float x2, float y2) { + final int a = mStrokeWidth * 2; + final Path p = new Path(); + p.moveTo(x1, y1); + p.lineTo(x2, y2); + if (y1 == y2) { + p.moveTo(x1 + a, y1 + a); + p.lineTo(x1, y1); + p.lineTo(x1 + a, y1 - a); + + p.moveTo(x2 - a, y2 - a); + p.lineTo(x2, y2); + p.lineTo(x2 - a, y2 + a); + } + if (x1 == x2) { + p.moveTo(x1 - a, y1 + a); + p.lineTo(x1, y1); + p.lineTo(x1 + a, y1 + a); + + p.moveTo(x2 - a, y2 - a); + p.lineTo(x2, y2); + p.lineTo(x2 + a, y2 - a); + } + return p; + } + } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java index 42201c5..e1ef83a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.drawable.Drawable; @@ -37,6 +38,7 @@ import android.provider.Settings.SettingNotFoundException; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; +import android.view.WindowManager; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; @@ -91,6 +93,19 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, static class BrightnessState extends State { boolean autoBrightness; } + static class InversionState extends State { + boolean toggled; + int type; + } + static class ContrastState extends State { + boolean toggled; + float contrast; + float brightness; + } + static class ColorSpaceState extends State { + boolean toggled; + int type; + } public static class BluetoothState extends State { boolean connected = false; String stateContentDescription; @@ -98,6 +113,9 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, public static class RotationLockState extends State { boolean visible = false; } + public static class ZenModeState extends State { + int zenMode = Settings.Global.ZEN_MODE_OFF; + } /** The callback to update a given tile. */ interface RefreshCallback { @@ -199,6 +217,106 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, } } + /** ContentObserver to watch display inversion */ + private class DisplayInversionObserver extends ContentObserver { + public DisplayInversionObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + onInversionChanged(); + } + + public void startObserving() { + final ContentResolver cr = mContext.getContentResolver(); + cr.unregisterContentObserver(this); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED), + false, this, mUserTracker.getCurrentUserId()); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_QUICK_SETTING_ENABLED), + false, this, mUserTracker.getCurrentUserId()); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION), + false, this, mUserTracker.getCurrentUserId()); + } + } + + /** ContentObserver to watch display contrast */ + private class DisplayContrastObserver extends ContentObserver { + public DisplayContrastObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + onContrastChanged(); + } + + public void startObserving() { + final ContentResolver cr = mContext.getContentResolver(); + cr.unregisterContentObserver(this); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_CONTRAST_ENABLED), + false, this, mUserTracker.getCurrentUserId()); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_CONTRAST_QUICK_SETTING_ENABLED), + false, this, mUserTracker.getCurrentUserId()); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_CONTRAST), + false, this, mUserTracker.getCurrentUserId()); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_BRIGHTNESS), + false, this, mUserTracker.getCurrentUserId()); + } + } + + /** ContentObserver to watch display color space adjustment */ + private class DisplayColorSpaceObserver extends ContentObserver { + public DisplayColorSpaceObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + onColorSpaceChanged(); + } + + public void startObserving() { + final ContentResolver cr = mContext.getContentResolver(); + cr.unregisterContentObserver(this); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED), + false, this, mUserTracker.getCurrentUserId()); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_QUICK_SETTING_ENABLED), + false, this, mUserTracker.getCurrentUserId()); + cr.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER), + false, this, mUserTracker.getCurrentUserId()); + } + } + + /** ContentObserver to watch display color space adjustment */ + private class ZenModeObserver extends ContentObserver { + public ZenModeObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + onZenModeChanged(); + } + + public void startObserving() { + final ContentResolver cr = mContext.getContentResolver(); + cr.unregisterContentObserver(this); + cr.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false, this); + } + } + /** Callback for changes to remote display routes. */ private class RemoteDisplayRouteCallback extends MediaRouter.SimpleCallback { @Override @@ -229,6 +347,10 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, private final NextAlarmObserver mNextAlarmObserver; private final BugreportObserver mBugreportObserver; private final BrightnessObserver mBrightnessObserver; + private final DisplayInversionObserver mInversionObserver; + private final DisplayContrastObserver mContrastObserver; + private final DisplayColorSpaceObserver mColorSpaceObserver; + private final ZenModeObserver mZenModeObserver; private final MediaRouter mMediaRouter; private final RemoteDisplayRouteCallback mRemoteDisplayRouteCallback; @@ -251,6 +373,10 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, private RefreshCallback mAirplaneModeCallback; private State mAirplaneModeState = new State(); + private QuickSettingsTileView mZenModeTile; + private RefreshCallback mZenModeCallback; + private ZenModeState mZenModeState = new ZenModeState(); + private QuickSettingsTileView mWifiTile; private RefreshCallback mWifiCallback; private WifiState mWifiState = new WifiState(); @@ -287,6 +413,18 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, private RefreshCallback mBrightnessCallback; private BrightnessState mBrightnessState = new BrightnessState(); + private QuickSettingsTileView mInversionTile; + private RefreshCallback mInversionCallback; + private InversionState mInversionState = new InversionState(); + + private QuickSettingsTileView mContrastTile; + private RefreshCallback mContrastCallback; + private ContrastState mContrastState = new ContrastState(); + + private QuickSettingsTileView mColorSpaceTile; + private RefreshCallback mColorSpaceCallback; + private ColorSpaceState mColorSpaceState = new ColorSpaceState(); + private QuickSettingsTileView mBugreportTile; private RefreshCallback mBugreportCallback; private State mBugreportState = new State(); @@ -300,6 +438,7 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, private State mSslCaCertWarningState = new State(); private RotationLockController mRotationLockController; + private int mRotationLockedLabel; public QuickSettingsModel(Context context) { mContext = context; @@ -308,8 +447,14 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, @Override public void onUserSwitched(int newUserId) { mBrightnessObserver.startObserving(); + mInversionObserver.startObserving(); + mContrastObserver.startObserving(); + mColorSpaceObserver.startObserving(); refreshRotationLockTile(); onBrightnessLevelChanged(); + onInversionChanged(); + onContrastChanged(); + onColorSpaceChanged(); onNextAlarmChanged(); onBugreportChanged(); rebindMediaRouterAsCurrentUser(); @@ -322,6 +467,14 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, mBugreportObserver.startObserving(); mBrightnessObserver = new BrightnessObserver(mHandler); mBrightnessObserver.startObserving(); + mInversionObserver = new DisplayInversionObserver(mHandler); + mInversionObserver.startObserving(); + mContrastObserver = new DisplayContrastObserver(mHandler); + mContrastObserver.startObserving(); + mColorSpaceObserver = new DisplayColorSpaceObserver(mHandler); + mColorSpaceObserver.startObserving(); + mZenModeObserver = new ZenModeObserver(mHandler); + mZenModeObserver.startObserving(); mMediaRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE); rebindMediaRouterAsCurrentUser(); @@ -444,6 +597,22 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, mAirplaneModeCallback.refreshView(mAirplaneModeTile, mAirplaneModeState); } + // Zen Mode + void addZenModeTile(QuickSettingsTileView view, RefreshCallback cb) { + mZenModeTile = view; + mZenModeCallback = cb; + onZenModeChanged(); + } + private void onZenModeChanged() { + final int mode = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF); + mZenModeState.enabled = mode != Settings.Global.ZEN_MODE_OFF; + mZenModeState.zenMode = mode; + mZenModeState.label = mContext.getString(R.string.zen_mode_title); + mZenModeState.iconId = R.drawable.stat_sys_zen_limited; + mZenModeCallback.refreshView(mZenModeTile, mZenModeState); + } + // Wifi void addWifiTile(QuickSettingsTileView view, RefreshCallback cb) { mWifiTile = view; @@ -659,7 +828,6 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, void addRemoteDisplayTile(QuickSettingsTileView view, RefreshCallback cb) { mRemoteDisplayTile = view; mRemoteDisplayCallback = cb; - final int[] count = new int[1]; mRemoteDisplayTile.setOnPrepareListener(new QuickSettingsTileView.OnPrepareListener() { @Override public void onPrepare() { @@ -798,6 +966,12 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, mRotationLockTile = view; mRotationLockCallback = cb; mRotationLockController = rotationLockController; + final int lockOrientation = mRotationLockController.getRotationLockOrientation(); + mRotationLockedLabel = lockOrientation == Configuration.ORIENTATION_PORTRAIT + ? R.string.quick_settings_rotation_locked_portrait_label + : lockOrientation == Configuration.ORIENTATION_LANDSCAPE + ? R.string.quick_settings_rotation_locked_landscape_label + : R.string.quick_settings_rotation_locked_label; onRotationLockChanged(); } void onRotationLockChanged() { @@ -812,7 +986,7 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, ? R.drawable.ic_qs_rotation_locked : R.drawable.ic_qs_auto_rotate; mRotationLockState.label = rotationLocked - ? mContext.getString(R.string.quick_settings_rotation_locked_label) + ? mContext.getString(mRotationLockedLabel) : mContext.getString(R.string.quick_settings_rotation_unlocked_label); mRotationLockCallback.refreshView(mRotationLockTile, mRotationLockState); } @@ -847,6 +1021,90 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, onBrightnessLevelChanged(); } + // Color inversion + void addInversionTile(QuickSettingsTileView view, RefreshCallback cb) { + mInversionTile = view; + mInversionCallback = cb; + onInversionChanged(); + } + public void onInversionChanged() { + final Resources res = mContext.getResources(); + final ContentResolver cr = mContext.getContentResolver(); + final int currentUserId = mUserTracker.getCurrentUserId(); + final boolean quickSettingEnabled = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_QUICK_SETTING_ENABLED, 0, + currentUserId) == 1; + final boolean enabled = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0, currentUserId) == 1; + final int type = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION, 0, currentUserId); + mInversionState.enabled = quickSettingEnabled; + mInversionState.toggled = enabled; + mInversionState.type = type; + // TODO: Add real icon assets. + mInversionState.iconId = enabled ? R.drawable.ic_qs_inversion_on + : R.drawable.ic_qs_inversion_off; + mInversionState.label = res.getString(R.string.quick_settings_inversion_label); + mInversionCallback.refreshView(mInversionTile, mInversionState); + } + + // Contrast enhancement + void addContrastTile(QuickSettingsTileView view, RefreshCallback cb) { + mContrastTile = view; + mContrastCallback = cb; + onContrastChanged(); + } + public void onContrastChanged() { + final Resources res = mContext.getResources(); + final ContentResolver cr = mContext.getContentResolver(); + final int currentUserId = mUserTracker.getCurrentUserId(); + final boolean quickSettingEnabled = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_CONTRAST_QUICK_SETTING_ENABLED, 0, + currentUserId) == 1; + final boolean enabled = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_CONTRAST_ENABLED, 0, currentUserId) == 1; + final float contrast = Settings.Secure.getFloatForUser( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_CONTRAST, 1, currentUserId); + final float brightness = Settings.Secure.getFloatForUser( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_BRIGHTNESS, 0, currentUserId); + mContrastState.enabled = quickSettingEnabled; + mContrastState.toggled = enabled; + mContrastState.contrast = contrast; + mContrastState.brightness = brightness; + // TODO: Add real icon assets. + mContrastState.iconId = enabled ? R.drawable.ic_qs_contrast_on + : R.drawable.ic_qs_contrast_off; + mContrastState.label = res.getString(R.string.quick_settings_contrast_label); + mContrastCallback.refreshView(mContrastTile, mContrastState); + } + + // Color space adjustment + void addColorSpaceTile(QuickSettingsTileView view, RefreshCallback cb) { + mColorSpaceTile = view; + mColorSpaceCallback = cb; + onColorSpaceChanged(); + } + public void onColorSpaceChanged() { + final Resources res = mContext.getResources(); + final ContentResolver cr = mContext.getContentResolver(); + final int currentUserId = mUserTracker.getCurrentUserId(); + final boolean quickSettingEnabled = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_QUICK_SETTING_ENABLED, 0, + currentUserId) == 1; + final boolean enabled = Settings.Secure.getIntForUser(cr, + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, 0, currentUserId) == 1; + final int type = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, 0, currentUserId); + mColorSpaceState.enabled = quickSettingEnabled; + mColorSpaceState.toggled = enabled; + mColorSpaceState.type = type; + // TODO: Add real icon assets. + mColorSpaceState.iconId = enabled ? R.drawable.ic_qs_color_space_on + : R.drawable.ic_qs_color_space_off; + mColorSpaceState.label = res.getString(R.string.quick_settings_color_space_label); + mColorSpaceCallback.refreshView(mColorSpaceTile, mColorSpaceState); + } + // SSL CA Cert warning. public void addSslCaCertWarningTile(QuickSettingsTileView view, RefreshCallback cb) { mSslCaCertWarningTile = view; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsScrollView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsScrollView.java index 8a2f8d6..175805a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsScrollView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsScrollView.java @@ -42,7 +42,7 @@ public class QuickSettingsScrollView extends ScrollView { if (getChildCount() > 0) { View child = getChildAt(0); scrollRange = Math.max(0, - child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop)); + child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); } return scrollRange; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SettingsPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SettingsPanelView.java deleted file mode 100644 index c10a0d4..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SettingsPanelView.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.EventLog; -import android.view.MotionEvent; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; - -import com.android.systemui.EventLogTags; -import com.android.systemui.R; -import com.android.systemui.statusbar.GestureRecorder; -import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.statusbar.policy.BluetoothController; -import com.android.systemui.statusbar.policy.LocationController; -import com.android.systemui.statusbar.policy.NetworkController; -import com.android.systemui.statusbar.policy.RotationLockController; - -public class SettingsPanelView extends PanelView { - public static final boolean DEBUG_GESTURES = true; - - private QuickSettings mQS; - private QuickSettingsContainerView mQSContainer; - - Drawable mHandleBar; - int mHandleBarHeight; - View mHandleView; - - public SettingsPanelView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - mQSContainer = (QuickSettingsContainerView) findViewById(R.id.quick_settings_container); - - Resources resources = getContext().getResources(); - mHandleBar = resources.getDrawable(R.drawable.status_bar_close); - mHandleBarHeight = resources.getDimensionPixelSize(R.dimen.close_handle_height); - mHandleView = findViewById(R.id.handle); - } - - public void setQuickSettings(QuickSettings qs) { - mQS = qs; - } - - @Override - public void setBar(PanelBar panelBar) { - super.setBar(panelBar); - - if (mQS != null) { - mQS.setBar(panelBar); - } - } - - public void setImeWindowStatus(boolean visible) { - if (mQS != null) { - mQS.setImeWindowStatus(visible); - } - } - - public void setup(NetworkController networkController, BluetoothController bluetoothController, - BatteryController batteryController, LocationController locationController, - RotationLockController rotationLockController) { - if (mQS != null) { - mQS.setup(networkController, bluetoothController, batteryController, - locationController, rotationLockController); - } - } - - void updateResources() { - if (mQS != null) { - mQS.updateResources(); - } - if (mQSContainer != null) { - mQSContainer.updateResources(); - } - requestLayout(); - } - - @Override - public void fling(float vel, boolean always) { - GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder(); - if (gr != null) { - gr.tag( - "fling " + ((vel > 0) ? "open" : "closed"), - "settings,v=" + vel); - } - super.fling(vel, always); - } - - public void setService(PhoneStatusBar phoneStatusBar) { - if (mQS != null) { - mQS.setService(phoneStatusBar); - } - } - - @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - event.getText() - .add(getContext().getString(R.string.accessibility_desc_quick_settings)); - return true; - } - - return super.dispatchPopulateAccessibilityEvent(event); - } - - // We draw the handle ourselves so that it's always glued to the bottom of the window. - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (changed) { - final int pl = getPaddingLeft(); - final int pr = getPaddingRight(); - mHandleBar.setBounds(pl, 0, getWidth() - pr, (int) mHandleBarHeight); - } - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - final int off = (int) (getHeight() - mHandleBarHeight - getPaddingBottom()); - canvas.translate(0, off); - mHandleBar.setState(mHandleView.getDrawableState()); - mHandleBar.draw(canvas); - canvas.translate(0, -off); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (DEBUG_GESTURES) { - if (event.getActionMasked() != MotionEvent.ACTION_MOVE) { - EventLog.writeEvent(EventLogTags.SYSUI_QUICKPANEL_TOUCH, - event.getActionMasked(), (int) event.getX(), (int) event.getY()); - } - } - return super.onTouchEvent(event); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java new file mode 100644 index 0000000..c2595cf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -0,0 +1,226 @@ +/* + * 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.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +import com.android.internal.policy.IKeyguardShowCallback; +import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardHostView; +import com.android.keyguard.KeyguardSimpleHostView; +import com.android.keyguard.R; +import com.android.keyguard.ViewMediatorCallback; +import com.android.systemui.keyguard.KeyguardViewMediator; + +/** + * 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 { + private static String TAG = "StatusBarKeyguardViewManager"; + + private final Context mContext; + + private LockPatternUtils mLockPatternUtils; + private ViewMediatorCallback mViewMediatorCallback; + private PhoneStatusBar mPhoneStatusBar; + + private ViewGroup mContainer; + private StatusBarWindowManager mStatusBarWindowManager; + + private boolean mScreenOn = false; + private KeyguardBouncer mBouncer; + private boolean mShowing; + private boolean mOccluded; + + public StatusBarKeyguardViewManager(Context context, ViewMediatorCallback callback, + LockPatternUtils lockPatternUtils) { + mContext = context; + mViewMediatorCallback = callback; + mLockPatternUtils = lockPatternUtils; + } + + public void registerStatusBar(PhoneStatusBar phoneStatusBar, + ViewGroup container, StatusBarWindowManager statusBarWindowManager) { + mPhoneStatusBar = phoneStatusBar; + mContainer = container; + mStatusBarWindowManager = statusBarWindowManager; + mBouncer = new KeyguardBouncer(mContext, mViewMediatorCallback, mLockPatternUtils, + mStatusBarWindowManager, container); + } + + /** + * Show the keyguard. Will handle creating and attaching to the view manager + * lazily. + */ + public void show(Bundle options) { + mShowing = true; + mStatusBarWindowManager.setKeyguardShowing(true); + showBouncerOrKeyguard(); + updateBackButtonState(); + } + + /** + * Shows the notification keyguard or the bouncer depending on + * {@link KeyguardBouncer#needsFullscreenBouncer()}. + */ + private void showBouncerOrKeyguard() { + if (mBouncer.needsFullscreenBouncer()) { + + // The keyguard might be showing (already). So we need to hide it. + mPhoneStatusBar.hideKeyguard(); + mBouncer.show(); + } else { + mPhoneStatusBar.showKeyguard(); + mBouncer.hide(); + mBouncer.prepare(); + } + } + + public void showBouncer() { + mBouncer.show(); + updateBackButtonState(); + } + + /** + * Reset the state of the view. + */ + public void reset() { + showBouncerOrKeyguard(); + updateBackButtonState(); + } + + public void onScreenTurnedOff() { + mScreenOn = false; + mBouncer.onScreenTurnedOff(); + } + + public void onScreenTurnedOn(final IKeyguardShowCallback callback) { + mScreenOn = true; + if (callback != null) { + callbackAfterDraw(callback); + } + } + + private void callbackAfterDraw(final IKeyguardShowCallback callback) { + mContainer.post(new Runnable() { + @Override + public void run() { + try { + callback.onShown(mContainer.getWindowToken()); + } catch (RemoteException e) { + Slog.w(TAG, "Exception calling onShown():", e); + } + } + }); + } + + public void verifyUnlock() { + dismiss(); + } + + public void setNeedsInput(boolean needsInput) { + mStatusBarWindowManager.setKeyguardNeedsInput(needsInput); + } + + public void updateUserActivityTimeout() { + mStatusBarWindowManager.setKeyguardUserActivityTimeout(mBouncer.getUserActivityTimeout()); + } + + public void setOccluded(boolean occluded) { + mOccluded = occluded; + mStatusBarWindowManager.setKeyguardOccluded(occluded); + updateBackButtonState(); + } + + /** + * Hides the keyguard view + */ + public void hide() { + mShowing = false; + mPhoneStatusBar.hideKeyguard(); + mStatusBarWindowManager.setKeyguardShowing(false); + mBouncer.hide(); + mViewMediatorCallback.keyguardGone(); + updateBackButtonState(); + } + + /** + * Dismisses the keyguard by going to the next screen or making it gone. + */ + public void dismiss() { + if (mScreenOn) { + showBouncer(); + } + } + + 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()) { + mBouncer.hide(); + mPhoneStatusBar.showKeyguard(); + updateBackButtonState(); + return true; + } + return false; + } + + private void updateBackButtonState() { + int vis = mContainer.getSystemUiVisibility(); + boolean bouncerDismissable = mBouncer.isShowing() && !mBouncer.needsFullscreenBouncer(); + if (bouncerDismissable || !mShowing) { + mContainer.setSystemUiVisibility(vis & ~View.STATUS_BAR_DISABLE_BACK); + } else { + mContainer.setSystemUiVisibility(vis | View.STATUS_BAR_DISABLE_BACK); + } + if (!(mShowing && !mOccluded) || mBouncer.isShowing()) { + mPhoneStatusBar.getNavigationBarView().setVisibility(View.VISIBLE); + } else { + mPhoneStatusBar.getNavigationBarView().setVisibility(View.GONE); + } + } + + public boolean onMenuPressed() { + return mBouncer.onMenuPressed(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java new file mode 100644 index 0000000..d175d7a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java @@ -0,0 +1,209 @@ +/* + * 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.ActionBar; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.os.SystemProperties; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; + +import com.android.keyguard.R; + +/** + * Encapsulates all logic for the status bar window state management. + */ +public class StatusBarWindowManager { + + private final Context mContext; + private final WindowManager mWindowManager; + private View mStatusBarView; + private WindowManager.LayoutParams mLp; + private int mBarHeight; + private final boolean mKeyguardScreenRotation; + + private final State mCurrentState = new State(); + + public StatusBarWindowManager(Context context) { + mContext = context; + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mKeyguardScreenRotation = shouldEnableKeyguardScreenRotation(); + } + + 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_TRANSLUCENT_NAVIGATION + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, + PixelFormat.TRANSLUCENT); + + mLp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + mLp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL; + mLp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + mLp.setTitle("StatusBar"); + mLp.packageName = mContext.getPackageName(); + mStatusBarView = statusBarView; + mBarHeight = barHeight; + mWindowManager.addView(mStatusBarView, mLp); + } + + private void applyKeyguardFlags(State state) { + if (state.keyguardShowing) { + mLp.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + mLp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; + } else { + mLp.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + mLp.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; + } + } + + private void adjustScreenOrientation(State state) { + if (state.isKeyguardShowingAndNotOccluded()) { + if (mKeyguardScreenRotation) { + mLp.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER; + } else { + mLp.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; + } + } else { + mLp.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + } + } + + private void applyFocusableFlag(State state) { + if (state.isKeyguardShowingAndNotOccluded() && state.keyguardNeedsInput) { + mLp.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mLp.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } else if (state.isKeyguardShowingAndNotOccluded() || state.statusBarFocusable) { + mLp.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mLp.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } else { + mLp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mLp.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } + } + + private void applyHeight(State state) { + boolean expanded = state.isKeyguardShowingAndNotOccluded() || state.statusBarExpanded; + if (expanded) { + mLp.height = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + mLp.height = mBarHeight; + } + } + + private void applyFitsSystemWindows(State state) { + mStatusBarView.setFitsSystemWindows(!state.isKeyguardShowingAndNotOccluded()); + } + + private void applyUserActivityTimeout(State state) { + if (state.isKeyguardShowingAndNotOccluded()) { + mLp.userActivityTimeout = state.keyguardUserActivityTimeout; + } else { + mLp.userActivityTimeout = -1; + } + } + + private void applyInputFeatures(State state) { + if (state.isKeyguardShowingAndNotOccluded()) { + mLp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; + } else { + mLp.inputFeatures &= ~WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; + } + } + + private void apply(State state) { + applyKeyguardFlags(state); + applyFocusableFlag(state); + adjustScreenOrientation(state); + applyHeight(state); + applyUserActivityTimeout(state); + applyInputFeatures(state); + applyFitsSystemWindows(state); + mWindowManager.updateViewLayout(mStatusBarView, mLp); + } + + 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 setStatusBarExpanded(boolean expanded) { + mCurrentState.statusBarExpanded = expanded; + mCurrentState.statusBarFocusable = expanded; + apply(mCurrentState); + } + + public void setStatusBarFocusable(boolean focusable) { + mCurrentState.statusBarFocusable = focusable; + apply(mCurrentState); + } + + public void setKeyguardUserActivityTimeout(long timeout) { + mCurrentState.keyguardUserActivityTimeout = timeout; + apply(mCurrentState); + } + + private static class State { + boolean keyguardShowing; + boolean keyguardOccluded; + boolean keyguardNeedsInput; + boolean statusBarExpanded; + boolean statusBarFocusable; + long keyguardUserActivityTimeout; + + private boolean isKeyguardShowingAndNotOccluded() { + return keyguardShowing && !keyguardOccluded; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java index 4901823..6b5ef5a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java @@ -20,18 +20,19 @@ import android.app.StatusBarManager; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Rect; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewRootImpl; import android.widget.FrameLayout; -import android.widget.ScrollView; import com.android.systemui.ExpandHelper; import com.android.systemui.R; import com.android.systemui.statusbar.BaseStatusBar; -import com.android.systemui.statusbar.policy.NotificationRowLayout; +import com.android.systemui.statusbar.policy.ScrollAdapter; +import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; public class StatusBarWindowView extends FrameLayout @@ -40,9 +41,8 @@ public class StatusBarWindowView extends FrameLayout public static final boolean DEBUG = BaseStatusBar.DEBUG; private ExpandHelper mExpandHelper; - private NotificationRowLayout latestItems; + private NotificationStackScrollLayout mStackScrollLayout; private NotificationPanelView mNotificationPanel; - private ScrollView mScrollView; PhoneStatusBar mService; @@ -53,16 +53,28 @@ public class StatusBarWindowView extends FrameLayout } @Override + protected boolean fitSystemWindows(Rect insets) { + if (getFitsSystemWindows()) { + setPadding(insets.left, insets.top, insets.right, insets.bottom); + } else { + setPadding(0, 0, 0, 0); + } + return true; + } + + @Override protected void onAttachedToWindow () { super.onAttachedToWindow(); - latestItems = (NotificationRowLayout) findViewById(R.id.latestItems); - mScrollView = (ScrollView) findViewById(R.id.scroll); + + mStackScrollLayout = (NotificationStackScrollLayout) findViewById( + R.id.notification_stack_scroller); mNotificationPanel = (NotificationPanelView) findViewById(R.id.notification_panel); - int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_min_height); - int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_max_height); - mExpandHelper = new ExpandHelper(mContext, latestItems, minHeight, maxHeight); + int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); + int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height); + mExpandHelper = new ExpandHelper(getContext(), mStackScrollLayout, + minHeight, maxHeight); mExpandHelper.setEventSource(this); - mExpandHelper.setScrollView(mScrollView); + mExpandHelper.setScrollAdapter(mStackScrollLayout); // We really need to be able to animate while window animations are going on // so that activities may be started asynchronously from panel animations @@ -76,11 +88,15 @@ public class StatusBarWindowView extends FrameLayout public boolean dispatchKeyEvent(KeyEvent event) { boolean down = event.getAction() == KeyEvent.ACTION_DOWN; switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_BACK: - if (!down) { - mService.animateCollapsePanels(); - } - return true; + case KeyEvent.KEYCODE_BACK: + if (!down) { + mService.onBackPressed(); + } + return true; + case KeyEvent.KEYCODE_MENU: + if (!down) { + return mService.onMenuPressed(); + } } return super.dispatchKeyEvent(event); } @@ -88,7 +104,9 @@ public class StatusBarWindowView extends FrameLayout @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; - if (mNotificationPanel.isFullyExpanded() && mScrollView.getVisibility() == View.VISIBLE) { + if (mNotificationPanel.isFullyExpanded() + && mStackScrollLayout.getVisibility() == View.VISIBLE + && !mService.isOnKeyguard()) { intercept = mExpandHelper.onInterceptTouchEvent(ev); } if (!intercept) { @@ -97,7 +115,7 @@ public class StatusBarWindowView extends FrameLayout if (intercept) { MotionEvent cancellation = MotionEvent.obtain(ev); cancellation.setAction(MotionEvent.ACTION_CANCEL); - latestItems.onInterceptTouchEvent(cancellation); + mStackScrollLayout.onInterceptTouchEvent(cancellation); cancellation.recycle(); } return intercept; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java new file mode 100644 index 0000000..20011ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java @@ -0,0 +1,424 @@ +/* + * 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.ValueAnimator; +import android.content.Context; +import android.graphics.PorterDuff.Mode; +import android.graphics.Typeface; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.TextPaint; +import android.text.method.LinkMovementMethod; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.systemui.R; +import com.android.systemui.statusbar.phone.ZenModeView.Adapter.ExitCondition; + +public class ZenModeView extends RelativeLayout { + private static final String TAG = ZenModeView.class.getSimpleName(); + private static final boolean DEBUG = false; + + public static final int BACKGROUND = 0xff282828; + + private static final Typeface CONDENSED = + Typeface.create("sans-serif-condensed", Typeface.NORMAL); + private static final int GRAY = 0xff999999; //TextAppearance.StatusBar.Expanded.Network + private static final int DARK_GRAY = 0xff333333; + + private static final long DURATION = new ValueAnimator().getDuration(); + private static final long PAGER_DURATION = DURATION / 2; + private static final long CLOSE_DELAY = 600; + private static final long AUTO_ACTIVATE_DELAY = 100; + + private final Context mContext; + private final TextView mModeText; + private final Switch mModeSwitch; + private final View mDivider; + private final UntilPager mUntilPager; + private final ProgressDots mProgressDots; + private final View mDivider2; + private final TextView mSettingsButton; + + private Adapter mAdapter; + private boolean mInit; + private boolean mAutoActivate; + + public ZenModeView(Context context) { + this(context, null); + } + + public ZenModeView(Context context, AttributeSet attrs) { + super(context, attrs); + if (DEBUG) log("new %s()", getClass().getSimpleName()); + mContext = context; + + final int iconSize = mContext.getResources() + .getDimensionPixelSize(com.android.internal.R.dimen.notification_large_icon_width); + final int topRowSize = iconSize * 2 / 3; + final int p = topRowSize / 3; + + LayoutParams lp = null; + + mModeText = new TextView(mContext); + mModeText.setText(R.string.zen_mode_title); + mModeText.setId(android.R.id.title); + mModeText.setTextColor(GRAY); + mModeText.setTypeface(CONDENSED); + mModeText.setAllCaps(true); + mModeText.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); + mModeText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mModeText.getTextSize() * 1.5f); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, topRowSize); + lp.leftMargin = p; + addView(mModeText, lp); + + mModeSwitch = new Switch(mContext); + mModeSwitch.setSwitchPadding(0); + mModeSwitch.setSwitchTypeface(CONDENSED); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, topRowSize); + lp.topMargin = p; + lp.rightMargin = p; + lp.addRule(ALIGN_PARENT_RIGHT); + lp.addRule(ALIGN_BASELINE, mModeText.getId()); + addView(mModeSwitch, lp); + mModeSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mAdapter.setMode(isChecked); + if (!mInit) return; + postDelayed(new Runnable(){ + @Override + public void run() { + mAdapter.close(); + } + }, CLOSE_DELAY); + } + }); + + mDivider = new View(mContext); + mDivider.setId(android.R.id.empty); + mDivider.setBackgroundColor(GRAY); + lp = new LayoutParams(LayoutParams.MATCH_PARENT, 2); + lp.addRule(BELOW, mModeText.getId()); + lp.bottomMargin = p; + addView(mDivider, lp); + + mUntilPager = new UntilPager(mContext, iconSize * 3 / 4); + mUntilPager.setId(android.R.id.tabhost); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + lp.leftMargin = lp.rightMargin = iconSize / 2; + lp.addRule(CENTER_HORIZONTAL); + lp.addRule(BELOW, mDivider.getId()); + addView(mUntilPager, lp); + + mProgressDots = new ProgressDots(mContext, iconSize / 5); + mProgressDots.setId(android.R.id.progress); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + lp.addRule(CENTER_HORIZONTAL); + lp.addRule(BELOW, mUntilPager.getId()); + addView(mProgressDots, lp); + + mDivider2 = new View(mContext); + mDivider2.setId(android.R.id.widget_frame); + mDivider2.setBackgroundColor(GRAY); + lp = new LayoutParams(LayoutParams.MATCH_PARENT, 2); + lp.addRule(BELOW, mProgressDots.getId()); + addView(mDivider2, lp); + + mSettingsButton = new TextView(mContext); + mSettingsButton.setTypeface(CONDENSED); + mSettingsButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, mSettingsButton.getTextSize() * 1.3f); + mSettingsButton.setPadding(p, p, p, p); + mSettingsButton.setText("More settings..."); + lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + lp.addRule(BELOW, mDivider2.getId()); + addView(mSettingsButton, lp); + mSettingsButton.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mSettingsButton.setBackgroundColor(DARK_GRAY); + } else if (event.getAction() == MotionEvent.ACTION_UP) { + mSettingsButton.setBackground(null); + if (mAdapter != null) { + mAdapter.configure(); + } + } + return true; + } + }); + } + + public void setAdapter(Adapter adapter) { + mAdapter = adapter; + mAdapter.setCallbacks(new Adapter.Callbacks() { + @Override + public void onChanged() { + post(new Runnable() { + @Override + public void run() { + updateState(true); + } + }); + } + }); + updateState(false); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mAutoActivate) { + mAutoActivate = false; + postDelayed(new Runnable() { + @Override + public void run() { + if (!mModeSwitch.isChecked()) { + mInit = false; + mModeSwitch.setChecked(true); + } + } + }, AUTO_ACTIVATE_DELAY); + } + } + + @Override + protected void onDetachedFromWindow() { + if (mAdapter != null) { + mAdapter.dispose(); + } + } + + public void setAutoActivate(boolean value) { + mAutoActivate = value; + } + + private void updateState(boolean animate) { + mUntilPager.updateState(); + mModeSwitch.setChecked(mAdapter.getMode()); + mInit = true; + } + + private static void log(String msg, Object... args) { + Log.d(TAG, args == null || args.length == 0 ? msg : String.format(msg, args)); + } + + private final class UntilView extends FrameLayout { + private static final boolean SUPPORT_LINKS = false; + + private final TextView mText; + public UntilView(Context context) { + super(context); + mText = new TextView(mContext); + mText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mText.getTextSize() * 1.3f); + mText.setTypeface(CONDENSED); + mText.setTextColor(GRAY); + mText.setGravity(Gravity.CENTER); + addView(mText); + } + + public void setExitCondition(final ExitCondition ec) { + SpannableStringBuilder ss = new SpannableStringBuilder(ec.summary); + if (SUPPORT_LINKS && ec.action != null) { + ss.setSpan(new CustomLinkSpan() { + @Override + public void onClick() { + // TODO wire up links + Toast.makeText(mContext, ec.action, Toast.LENGTH_SHORT).show(); + } + }, 0, ss.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + mText.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + mText.setMovementMethod(null); + } + mText.setText(ss); + } + } + + private final class ProgressDots extends LinearLayout { + private final int mDotSize; + public ProgressDots(Context context, int dotSize) { + super(context); + setOrientation(HORIZONTAL); + mDotSize = dotSize; + } + + private void updateState(int current, int count) { + while (getChildCount() < count) { + View dot = new View(mContext); + OvalShape s = new OvalShape(); + ShapeDrawable sd = new ShapeDrawable(s); + + dot.setBackground(sd); + LayoutParams lp = new LayoutParams(mDotSize, mDotSize); + lp.leftMargin = lp.rightMargin = mDotSize / 2; + lp.topMargin = lp.bottomMargin = mDotSize * 2 / 3; + addView(dot, lp); + } + while (getChildCount() > count) { + removeViewAt(getChildCount() - 1); + } + final int N = getChildCount(); + for (int i = 0; i < N; i++) { + final int color = current == i ? GRAY : DARK_GRAY; + ((ShapeDrawable)getChildAt(i).getBackground()).setColorFilter(color, Mode.ADD); + } + } + } + + private final class UntilPager extends RelativeLayout { + private final UntilView[] mViews; + private int mCurrent; + private float mDownX; + + public UntilPager(Context context, int iconSize) { + super(context); + mViews = new UntilView[3]; + for (int i = 0; i < mViews.length; i++) { + UntilView v = new UntilView(mContext); + LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, iconSize); + addView(v, lp); + mViews[i] = v; + } + updateState(); + addOnLayoutChangeListener(new OnLayoutChangeListener() { + @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 || right != oldRight) { + updateState(); + } + } + }); + setBackgroundColor(DARK_GRAY); + } + + private void updateState() { + if (mAdapter == null) { + return; + } + UntilView current = mViews[mCurrent]; + current.setExitCondition(mAdapter.getExitCondition(0)); + UntilView next = mViews[mCurrent + 1 % 3]; + next.setExitCondition(mAdapter.getExitCondition(1)); + UntilView prev = mViews[mCurrent + 2 % 3]; + prev.setExitCondition(mAdapter.getExitCondition(-1)); + position(0, false); + mProgressDots.updateState(mAdapter.getExitConditionIndex(), + mAdapter.getExitConditionCount()); + } + + private void position(float dx, boolean animate) { + int w = getWidth(); + UntilView current = mViews[mCurrent]; + UntilView next = mViews[mCurrent + 1 % 3]; + UntilView prev = mViews[mCurrent + 2 % 3]; + if (animate) { + current.animate().setDuration(PAGER_DURATION).translationX(dx).start(); + next.animate().setDuration(PAGER_DURATION).translationX(w + dx).start(); + prev.animate().setDuration(PAGER_DURATION).translationX(-w + dx).start(); + } else { + current.setTranslationX(dx); + next.setTranslationX(w + dx); + prev.setTranslationX(-w + dx); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + log("onTouchEvent " + MotionEvent.actionToString(event.getAction())); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mDownX = event.getX(); + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + float dx = event.getX() - mDownX; + position(dx, false); + } else if (event.getAction() == MotionEvent.ACTION_UP + || event.getAction() == MotionEvent.ACTION_CANCEL) { + float dx = event.getX() - mDownX; + int d = Math.abs(dx) < getWidth() / 3 ? 0 : Math.signum(dx) > 0 ? -1 : 1; + if (d != 0 && mAdapter.getExitConditionCount() > 1) { + mAdapter.select(mAdapter.getExitCondition(d)); + } else { + position(0, true); + } + } + return true; + } + } + + private abstract static class CustomLinkSpan extends URLSpan { + abstract public void onClick(); + + public CustomLinkSpan() { + super("#"); + } + + @Override + public void updateDrawState(TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + ds.bgColor = BACKGROUND; + } + + @Override + public void onClick(View widget) { + onClick(); + } + } + + public interface Adapter { + void configure(); + void close(); + boolean getMode(); + void setMode(boolean mode); + void select(ExitCondition ec); + void init(); + void dispose(); + void setCallbacks(Callbacks callbacks); + ExitCondition getExitCondition(int d); + int getExitConditionCount(); + int getExitConditionIndex(); + + public static class ExitCondition { + public String summary; + public String line1; + public String line2; + public String action; + public Object tag; + } + + public interface Callbacks { + void onChanged(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeViewAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeViewAdapter.java new file mode 100644 index 0000000..1bc97a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeViewAdapter.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.phone; + +import android.app.INotificationManager; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.Settings; +import android.service.notification.Condition; +import android.service.notification.IConditionListener; +import android.util.ArrayMap; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public abstract class ZenModeViewAdapter implements ZenModeView.Adapter { + private static final String TAG = "ZenModeViewAdapter"; + + private final Context mContext; + private final ContentResolver mResolver; + private final Handler mHandler = new Handler(); + private final SettingsObserver mObserver; + private final List<ExitCondition> mExits = new ArrayList<ExitCondition>(Arrays.asList( + newExit("Until you turn this off", "Until", "You turn this off", null))); + private final INotificationManager mNoMan; + private final ArrayMap<Uri, Condition> mConditions = new ArrayMap<Uri, Condition>(); + + private Callbacks mCallbacks; + private int mExitIndex; + private boolean mMode; + + public ZenModeViewAdapter(Context context) { + mContext = context; + mResolver = mContext.getContentResolver(); + mObserver = new SettingsObserver(mHandler); + mNoMan = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + try { + mNoMan.requestZenModeConditions(mListener, true /*requested*/); + } catch (RemoteException e) { + // noop + } + mObserver.init(); + init(); + } + + @Override + public boolean getMode() { + return mMode; + } + + @Override + public void setMode(boolean mode) { + if (mode == mMode) return; + mMode = mode; + final int v = mMode ? Settings.Global.ZEN_MODE_ON : Settings.Global.ZEN_MODE_OFF; + AsyncTask.execute(new Runnable() { + @Override + public void run() { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.ZEN_MODE, v); + } + }); + dispatchChanged(); + } + + @Override + public void init() { + if (mExitIndex != 0) { + mExitIndex = 0; + dispatchChanged(); + } + setZenModeCondition(); + } + + @Override + public void dispose() { + try { + mNoMan.requestZenModeConditions(mListener, false /*requested*/); + } catch (RemoteException e) { + // noop + } + } + + private void dispatchChanged() { + mHandler.removeCallbacks(mChanged); + mHandler.post(mChanged); + } + + @Override + public void setCallbacks(final Callbacks callbacks) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallbacks = callbacks; + } + }); + } + + @Override + public ExitCondition getExitCondition(int d) { + final int n = mExits.size(); + final int i = (n + (mExitIndex + (int)Math.signum(d))) % n; + return mExits.get(i); + } + + @Override + public int getExitConditionCount() { + return mExits.size(); + } + + @Override + public int getExitConditionIndex() { + return mExitIndex; + } + + @Override + public void select(ExitCondition ec) { + final int i = mExits.indexOf(ec); + if (i == -1 || i == mExitIndex) { + return; + } + mExitIndex = i; + dispatchChanged(); + setZenModeCondition(); + } + + private void setZenModeCondition() { + if (mExitIndex < 0 || mExitIndex >= mExits.size()) { + Log.w(TAG, "setZenModeCondition to bad index " + mExitIndex + " of " + mExits.size()); + return; + } + final Uri conditionUri = (Uri) mExits.get(mExitIndex).tag; + try { + mNoMan.setZenModeCondition(conditionUri); + } catch (RemoteException e) { + // noop + } + } + + private static ExitCondition newExit(String summary, String line1, String line2, Object tag) { + final ExitCondition rt = new ExitCondition(); + rt.summary = summary; + rt.line1 = line1; + rt.line2 = line2; + rt.tag = tag; + return rt; + } + + private final Runnable mChanged = new Runnable() { + public void run() { + if (mCallbacks == null) { + return; + } + try { + mCallbacks.onChanged(); + } catch (Throwable t) { + Log.w(TAG, "Error dispatching onChanged to " + mCallbacks, t); + } + } + }; + + private final class SettingsObserver extends ContentObserver { + public SettingsObserver(Handler handler) { + super(handler); + } + + public void init() { + loadSettings(); + mResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.ZEN_MODE), + false, this); + } + + @Override + public void onChange(boolean selfChange) { + loadSettings(); + mChanged.run(); // already on handler + } + + private void loadSettings() { + mMode = getModeFromSetting(); + } + + private boolean getModeFromSetting() { + final int v = Settings.Global.getInt(mResolver, + Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF); + return v != Settings.Global.ZEN_MODE_OFF; + } + } + + private final IConditionListener mListener = new IConditionListener.Stub() { + @Override + public void onConditionsReceived(Condition[] conditions) { + if (conditions == null || conditions.length == 0) return; + for (Condition c : conditions) { + mConditions.put(c.id, c); + } + for (int i = mExits.size() - 1; i > 0; i--) { + mExits.remove(i); + } + for (Condition c : mConditions.values()) { + mExits.add(newExit(c.caption, "", "", c.id)); + } + dispatchChanged(); + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java index b7f3cfe..cadb44a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java @@ -21,9 +21,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.view.ViewParent; import android.widget.TextView; import com.android.systemui.R; @@ -73,7 +70,7 @@ public class DateView extends TextView { filter.addAction(Intent.ACTION_TIME_CHANGED); filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); filter.addAction(Intent.ACTION_LOCALE_CHANGED); - mContext.registerReceiver(mIntentReceiver, filter, null, null); + getContext().registerReceiver(mIntentReceiver, filter, null, null); updateClock(); } @@ -83,7 +80,7 @@ public class DateView extends TextView { super.onDetachedFromWindow(); mDateFormat = null; // reload the locale next time - mContext.unregisterReceiver(mIntentReceiver); + getContext().unregisterReceiver(mIntentReceiver); } protected void updateClock() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java index f1fda78..c94c65f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.policy; -import android.app.Notification; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; @@ -27,9 +26,9 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.LinearLayout; import com.android.systemui.ExpandHelper; +import com.android.systemui.Gefingerpoken; import com.android.systemui.R; import com.android.systemui.SwipeHelper; import com.android.systemui.statusbar.BaseStatusBar; @@ -44,13 +43,13 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. private final int mTouchSensitivityDelay; private SwipeHelper mSwipeHelper; + private EdgeSwipeHelper mEdgeSwipeHelper; private BaseStatusBar mBar; private ExpandHelper mExpandHelper; - private long mStartTouchTime; + private long mStartTouchTime; private ViewGroup mContentHolder; - private ViewGroup mContentSlider; private NotificationData.Entry mHeadsUp; @@ -74,18 +73,24 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. public boolean setNotification(NotificationData.Entry headsUp) { mHeadsUp = headsUp; - mHeadsUp.row.setExpanded(false); - if (mContentHolder == null) { - // too soon! - return false; + if (mContentHolder != null) { + mContentHolder.removeAllViews(); + } + + if (mHeadsUp != null) { + mHeadsUp.row.setSystemExpanded(true); + mHeadsUp.row.setShowingPublic(false); + if (mContentHolder == null) { + // too soon! + return false; + } + mContentHolder.setX(0); + mContentHolder.setVisibility(View.VISIBLE); + mContentHolder.setAlpha(1f); + mContentHolder.addView(mHeadsUp.row); + mSwipeHelper.snapChild(mContentHolder, 1f); + mStartTouchTime = System.currentTimeMillis() + mTouchSensitivityDelay; } - mContentHolder.setX(0); - mContentHolder.setVisibility(View.VISIBLE); - mContentHolder.setAlpha(1f); - mContentHolder.removeAllViews(); - mContentHolder.addView(mHeadsUp.row); - mSwipeHelper.snapChild(mContentSlider, 1f); - mStartTouchTime = System.currentTimeMillis() + mTouchSensitivityDelay; return true; } @@ -95,10 +100,11 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. public void setMargin(int notificationPanelMarginPx) { if (SPEW) Log.v(TAG, "setMargin() " + notificationPanelMarginPx); - if (mContentSlider != null) { - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mContentSlider.getLayoutParams(); + if (mContentHolder != null && + mContentHolder.getLayoutParams() instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mContentHolder.getLayoutParams(); lp.setMarginStart(notificationPanelMarginPx); - mContentSlider.setLayoutParams(lp); + mContentHolder.setLayoutParams(lp); } } @@ -123,15 +129,17 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. @Override public void onAttachedToWindow() { float densityScale = getResources().getDisplayMetrics().density; - float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); + final ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); + float pagingTouchSlop = viewConfiguration.getScaledPagingTouchSlop(); + float touchSlop = viewConfiguration.getScaledTouchSlop(); mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); + mEdgeSwipeHelper = new EdgeSwipeHelper(touchSlop); - int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_min_height); - int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_max_height); - mExpandHelper = new ExpandHelper(mContext, this, minHeight, maxHeight); + int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); + int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height); + mExpandHelper = new ExpandHelper(getContext(), this, minHeight, maxHeight); mContentHolder = (ViewGroup) findViewById(R.id.content_holder); - mContentSlider = (ViewGroup) findViewById(R.id.content_slider); if (mHeadsUp != null) { // whoops, we're on already! @@ -145,7 +153,8 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. if (System.currentTimeMillis() < mStartTouchTime) { return true; } - return mSwipeHelper.onInterceptTouchEvent(ev) + return mEdgeSwipeHelper.onInterceptTouchEvent(ev) + || mSwipeHelper.onInterceptTouchEvent(ev) || mExpandHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); } @@ -158,7 +167,8 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. return false; } mBar.resetHeadsUpDecayTimer(); - return mSwipeHelper.onTouchEvent(ev) + return mEdgeSwipeHelper.onTouchEvent(ev) + || mSwipeHelper.onTouchEvent(ev) || mExpandHelper.onTouchEvent(ev) || super.onTouchEvent(ev); } @@ -227,11 +237,65 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper. @Override public View getChildAtPosition(MotionEvent ev) { - return mContentSlider; + return mContentHolder; } @Override public View getChildContentView(View v) { - return mContentSlider; + return mContentHolder; + } + + private class EdgeSwipeHelper implements Gefingerpoken { + private static final boolean DEBUG_EDGE_SWIPE = false; + private final float mTouchSlop; + private boolean mConsuming; + private float mFirstY; + private float mFirstX; + + public EdgeSwipeHelper(float touchSlop) { + mTouchSlop = touchSlop; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action down " + ev.getY()); + mFirstX = ev.getX(); + mFirstY = ev.getY(); + mConsuming = false; + break; + + case MotionEvent.ACTION_MOVE: + if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action move " + ev.getY()); + final float dY = ev.getY() - mFirstY; + final float daX = Math.abs(ev.getX() - mFirstX); + final float daY = Math.abs(dY); + if (!mConsuming && (4f * daX) < daY && daY > mTouchSlop) { + if (dY > 0) { + if (DEBUG_EDGE_SWIPE) Log.d(TAG, "found an open"); + mBar.animateExpandNotificationsPanel(); + } + if (dY < 0) { + if (DEBUG_EDGE_SWIPE) Log.d(TAG, "found a close"); + mBar.onHeadsUpDismissed(); + } + mConsuming = true; + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action done" ); + mConsuming = false; + break; + } + return mConsuming; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mConsuming; + } } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java index 312bba3..f5ee95b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java @@ -24,7 +24,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.database.ContentObserver; import android.location.LocationManager; import android.os.Handler; import android.os.UserHandle; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java index 09f1695..92c008e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java @@ -126,6 +126,7 @@ public class NetworkController extends BroadcastReceiver implements DemoMode { private int mConnectedNetworkType = ConnectivityManager.TYPE_NONE; private String mConnectedNetworkTypeName; private int mInetCondition = 0; + private int mLastInetCondition = 0; private static final int INET_CONDITION_THRESHOLD = 50; private boolean mAirplaneMode = false; @@ -156,9 +157,9 @@ public class NetworkController extends BroadcastReceiver implements DemoMode { boolean mDataAndWifiStacked = false; public interface SignalCluster { - void setWifiIndicators(boolean visible, int strengthIcon, + void setWifiIndicators(boolean visible, int strengthIcon, boolean problem, String contentDescription); - void setMobileDataIndicators(boolean visible, int strengthIcon, + void setMobileDataIndicators(boolean visible, int strengthIcon, boolean problem, int typeIcon, String contentDescription, String typeContentDescription); void setIsAirplaneMode(boolean is, int airplaneIcon); } @@ -288,6 +289,7 @@ public class NetworkController extends BroadcastReceiver implements DemoMode { // only show wifi in the cluster if connected or if wifi-only mWifiEnabled && (mWifiConnected || !mHasMobileDataFeature), mWifiIconId, + mInetCondition == 0, mContentDescriptionWifi); if (mIsWimaxEnabled && mWimaxConnected) { @@ -295,6 +297,7 @@ public class NetworkController extends BroadcastReceiver implements DemoMode { cluster.setMobileDataIndicators( true, mAlwaysShowCdmaRssi ? mPhoneSignalIconId : mWimaxIconId, + mInetCondition == 0, mDataTypeIconId, mContentDescriptionWimax, mContentDescriptionDataType); @@ -303,6 +306,7 @@ public class NetworkController extends BroadcastReceiver implements DemoMode { cluster.setMobileDataIndicators( mHasMobileDataFeature, mShowPhoneRSSIForData ? mPhoneSignalIconId : mDataSignalIconId, + mInetCondition == 0, mDataTypeIconId, mContentDescriptionPhoneSignal, mContentDescriptionDataType); @@ -1145,6 +1149,7 @@ public class NetworkController extends BroadcastReceiver implements DemoMode { if (mLastPhoneSignalIconId != mPhoneSignalIconId || mLastWifiIconId != mWifiIconId + || mLastInetCondition != mInetCondition || mLastWimaxIconId != mWimaxIconId || mLastDataTypeIconId != mDataTypeIconId || mLastAirplaneMode != mAirplaneMode @@ -1179,6 +1184,10 @@ public class NetworkController extends BroadcastReceiver implements DemoMode { mLastWifiIconId = mWifiIconId; } + if (mLastInetCondition != mInetCondition) { + mLastInetCondition = mInetCondition; + } + // the wimax icon on phones if (mLastWimaxIconId != mWimaxIconId) { mLastWimaxIconId = mWimaxIconId; @@ -1424,6 +1433,7 @@ public class NetworkController extends BroadcastReceiver implements DemoMode { cluster.setWifiIndicators( show, iconId, + mDemoInetCondition == 0, "Demo"); } } @@ -1456,6 +1466,7 @@ public class NetworkController extends BroadcastReceiver implements DemoMode { cluster.setMobileDataIndicators( show, iconId, + mDemoInetCondition == 0, mDemoDataTypeIconId, "Demo", "Demo"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java deleted file mode 100644 index 259422d..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.policy; - -import android.animation.LayoutTransition; -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.widget.LinearLayout; - -import com.android.systemui.ExpandHelper; -import com.android.systemui.R; -import com.android.systemui.SwipeHelper; -import com.android.systemui.statusbar.ExpandableNotificationRow; -import com.android.systemui.statusbar.NotificationData; - -import java.util.HashMap; - -public class NotificationRowLayout - extends LinearLayout - implements SwipeHelper.Callback, ExpandHelper.Callback -{ - private static final String TAG = "NotificationRowLayout"; - private static final boolean DEBUG = false; - private static final boolean SLOW_ANIMATIONS = DEBUG; - - private static final int APPEAR_ANIM_LEN = SLOW_ANIMATIONS ? 5000 : 250; - private static final int DISAPPEAR_ANIM_LEN = APPEAR_ANIM_LEN; - - boolean mAnimateBounds = true; - - Rect mTmpRect = new Rect(); - - HashMap<View, ValueAnimator> mAppearingViews = new HashMap<View, ValueAnimator>(); - HashMap<View, ValueAnimator> mDisappearingViews = new HashMap<View, ValueAnimator>(); - - private SwipeHelper mSwipeHelper; - - private OnSizeChangedListener mOnSizeChangedListener; - - // Flag set during notification removal animation to avoid causing too much work until - // animation is done - boolean mRemoveViews = true; - - private LayoutTransition mRealLayoutTransition; - - public NotificationRowLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public NotificationRowLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mRealLayoutTransition = new LayoutTransition(); - mRealLayoutTransition.setAnimateParentHierarchy(true); - setLayoutTransitionsEnabled(true); - - setOrientation(LinearLayout.VERTICAL); - - if (DEBUG) { - setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { - @Override - public void onChildViewAdded(View parent, View child) { - Log.d(TAG, "view added: " + child + "; new count: " + getChildCount()); - } - @Override - public void onChildViewRemoved(View parent, View child) { - Log.d(TAG, "view removed: " + child + "; new count: " + (getChildCount() - 1)); - } - }); - - setBackgroundColor(0x80FF8000); - } - - float densityScale = getResources().getDisplayMetrics().density; - float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); - mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); - } - - public void setLongPressListener(View.OnLongClickListener listener) { - mSwipeHelper.setLongPressListener(listener); - } - - public void setOnSizeChangedListener(OnSizeChangedListener l) { - mOnSizeChangedListener = l; - } - - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - super.onWindowFocusChanged(hasWindowFocus); - if (!hasWindowFocus) { - mSwipeHelper.removeLongPressCallback(); - } - } - - public void setAnimateBounds(boolean anim) { - mAnimateBounds = anim; - } - - private void logLayoutTransition() { - Log.v(TAG, "layout " + - (mRealLayoutTransition.isChangingLayout() ? "is " : "is not ") + - "in transition and animations " + - (mRealLayoutTransition.isRunning() ? "are " : "are not ") + - "running."); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); - if (DEBUG) logLayoutTransition(); - - return mSwipeHelper.onInterceptTouchEvent(ev) || - super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (DEBUG) Log.v(TAG, "onTouchEvent()"); - if (DEBUG) logLayoutTransition(); - - return mSwipeHelper.onTouchEvent(ev) || - super.onTouchEvent(ev); - } - - public boolean canChildBeDismissed(View v) { - final View veto = v.findViewById(R.id.veto); - return (veto != null && veto.getVisibility() != View.GONE); - } - - public boolean canChildBeExpanded(View v) { - return v instanceof ExpandableNotificationRow - && ((ExpandableNotificationRow) v).isExpandable(); - } - - public void setUserExpandedChild(View v, boolean userExpanded) { - if (v instanceof ExpandableNotificationRow) { - ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); - } - } - - public void setUserLockedChild(View v, boolean userLocked) { - if (v instanceof ExpandableNotificationRow) { - ((ExpandableNotificationRow) v).setUserLocked(userLocked); - } - } - - public void onChildDismissed(View v) { - if (DEBUG) Log.v(TAG, "onChildDismissed: " + v + " mRemoveViews=" + mRemoveViews); - final View veto = v.findViewById(R.id.veto); - if (veto != null && veto.getVisibility() != View.GONE && mRemoveViews) { - veto.performClick(); - } - } - - public void onBeginDrag(View v) { - // We need to prevent the surrounding ScrollView from intercepting us now; - // the scroll position will be locked while we swipe - requestDisallowInterceptTouchEvent(true); - } - - public void onDragCancelled(View v) { - } - - public View getChildAtPosition(MotionEvent ev) { - return getChildAtPosition(ev.getX(), ev.getY()); - } - - public View getChildAtRawPosition(float touchX, float touchY) { - int[] location = new int[2]; - getLocationOnScreen(location); - return getChildAtPosition((float) (touchX - location[0]), (float) (touchY - location[1])); - } - - public View getChildAtPosition(float touchX, float touchY) { - // find the view under the pointer, accounting for GONE views - final int count = getChildCount(); - int y = 0; - int childIdx = 0; - View slidingChild; - for (; childIdx < count; childIdx++) { - slidingChild = getChildAt(childIdx); - if (slidingChild.getVisibility() == GONE) { - continue; - } - y += slidingChild.getMeasuredHeight(); - if (touchY < y) return slidingChild; - } - return null; - } - - public View getChildContentView(View v) { - return v; - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - float densityScale = getResources().getDisplayMetrics().density; - mSwipeHelper.setDensityScale(densityScale); - float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); - mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); - } - - - /** - * Sets a flag to tell us whether to actually remove views. Removal is delayed by setting this - * to false during some animations to smooth out performance. Callers should restore the - * flag to true after the animation is done, and then they should make sure that the views - * get removed properly. - */ - public void setViewRemoval(boolean removeViews) { - if (DEBUG) Log.v(TAG, "setViewRemoval: " + removeViews); - mRemoveViews = removeViews; - } - - // Suppress layout transitions for a little while. - public void setLayoutTransitionsEnabled(boolean b) { - if (b) { - setLayoutTransition(mRealLayoutTransition); - } else { - if (mRealLayoutTransition.isRunning()) { - mRealLayoutTransition.cancel(); - } - setLayoutTransition(null); - } - } - - public void dismissRowAnimated(View child) { - dismissRowAnimated(child, 0); - } - - public void dismissRowAnimated(View child, int vel) { - mSwipeHelper.dismissChild(child, vel); - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - if (DEBUG) setWillNotDraw(false); - } - - @Override - public void onDraw(android.graphics.Canvas c) { - super.onDraw(c); - if (DEBUG) logLayoutTransition(); - if (DEBUG) { - //Log.d(TAG, "onDraw: canvas height: " + c.getHeight() + "px; measured height: " - // + getMeasuredHeight() + "px"); - c.save(); - c.clipRect(6, 6, c.getWidth() - 6, getMeasuredHeight() - 6, - android.graphics.Region.Op.DIFFERENCE); - c.drawColor(0xFFFF8000); - c.restore(); - } - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - if (mOnSizeChangedListener != null) { - mOnSizeChangedListener.onSizeChanged(this, w, h, oldw, oldh); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnSizeChangedListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnSizeChangedListener.java deleted file mode 100644 index 0377123..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnSizeChangedListener.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.view.View; - -public interface OnSizeChangedListener { - void onSizeChanged(View view, int w, int h, int oldw, int oldh); -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java index 6f61ec8..98d205a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.policy; import android.content.Context; +import android.content.res.Configuration; import android.os.UserHandle; import com.android.internal.view.RotationPolicy; @@ -42,42 +43,32 @@ public final class RotationLockController { public RotationLockController(Context context) { mContext = context; - notifyChanged(); - if (RotationPolicy.isRotationLockToggleSupported(mContext)) { - RotationPolicy.registerRotationPolicyListener(mContext, - mRotationPolicyListener, UserHandle.USER_ALL); - } + RotationPolicy.registerRotationPolicyListener(mContext, + mRotationPolicyListener, UserHandle.USER_ALL); } public void addRotationLockControllerCallback(RotationLockControllerCallback callback) { mCallbacks.add(callback); } + public int getRotationLockOrientation() { + return RotationPolicy.getRotationLockOrientation(mContext); + } + public boolean isRotationLocked() { - if (RotationPolicy.isRotationLockToggleSupported(mContext)) { - return RotationPolicy.isRotationLocked(mContext); - } - return false; + return RotationPolicy.isRotationLocked(mContext); } public void setRotationLocked(boolean locked) { - if (RotationPolicy.isRotationLockToggleSupported(mContext)) { - RotationPolicy.setRotationLock(mContext, locked); - } + RotationPolicy.setRotationLock(mContext, locked); } public boolean isRotationLockAffordanceVisible() { - if (RotationPolicy.isRotationLockToggleSupported(mContext)) { - return RotationPolicy.isRotationLockToggleVisible(mContext); - } - return false; + return RotationPolicy.isRotationLockToggleVisible(mContext); } public void release() { - if (RotationPolicy.isRotationLockToggleSupported(mContext)) { - RotationPolicy.unregisterRotationPolicyListener(mContext, - mRotationPolicyListener); - } + RotationPolicy.unregisterRotationPolicyListener(mContext, mRotationPolicyListener); } private void notifyChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ScrollAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ScrollAdapter.java new file mode 100644 index 0000000..f35e22d --- /dev/null +++ b/packages/SystemUI/src/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/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java new file mode 100644 index 0000000..9a43e37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -0,0 +1,1113 @@ +/* + * 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.Configuration; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import android.util.AttributeSet; +import android.util.Log; + +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.animation.AnimationUtils; +import android.widget.OverScroller; + +import com.android.systemui.ExpandHelper; +import com.android.systemui.R; +import com.android.systemui.SwipeHelper; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.ExpandableView; +import com.android.systemui.statusbar.stack.StackScrollState.ViewState; +import com.android.systemui.statusbar.policy.ScrollAdapter; + +import java.util.ArrayList; + +/** + * 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 { + + private static final String TAG = "NotificationStackScrollLayout"; + private static final boolean DEBUG = false; + + /** + * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + private SwipeHelper mSwipeHelper; + private boolean mSwipingInProgress; + private int mCurrentStackHeight = Integer.MAX_VALUE; + private int mOwnScrollY; + private int mMaxLayoutHeight; + + private VelocityTracker mVelocityTracker; + private OverScroller mScroller; + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mOverscrollDistance; + private int mOverflingDistance; + private boolean mIsBeingDragged; + private int mLastMotionY; + private int mActivePointerId; + + private int mSidePaddings; + private Paint mDebugPaint; + private int mContentHeight; + private int mCollapsedSize; + private int mBottomStackPeekSize; + private int mEmptyMarginBottom; + private int mPaddingBetweenElements; + private int mTopPadding; + private boolean mListenForHeightChanges = true; + + /** + * The algorithm which calculates the properties for our children + */ + private StackScrollAlgorithm mStackScrollAlgorithm; + + /** + * The current State this Layout is in + */ + private StackScrollState mCurrentStackScrollState = new StackScrollState(this); + private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>(); + private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>(); + private ArrayList<ChildHierarchyChangeEvent> mAnimationEvents + = new ArrayList<ChildHierarchyChangeEvent>(); + private ArrayList<View> mSwipedOutViews = new ArrayList<View>(); + private final StackStateAnimator mStateAnimator = new StackStateAnimator(this); + + private OnChildLocationsChangedListener mListener; + private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; + private boolean mChildHierarchyDirty; + private boolean mIsExpanded = true; + private ViewTreeObserver.OnPreDrawListener mAfterLayoutPreDrawListener + = new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + updateScrollPositionIfNecessary(); + updateChildren(); + getViewTreeObserver().removeOnPreDrawListener(this); + return true; + } + }; + + 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); + initView(context); + if (DEBUG) { + setWillNotDraw(false); + mDebugPaint = new Paint(); + mDebugPaint.setColor(0xffff0000); + mDebugPaint.setStrokeWidth(2); + mDebugPaint.setStyle(Paint.Style.STROKE); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (DEBUG) { + int y = mCollapsedSize; + canvas.drawLine(0, y, getWidth(), y, mDebugPaint); + y = (int) (getLayoutHeight() - mBottomStackPeekSize - mCollapsedSize); + canvas.drawLine(0, y, getWidth(), y, mDebugPaint); + y = (int) getLayoutHeight(); + canvas.drawLine(0, y, getWidth(), y, mDebugPaint); + } + } + + private void initView(Context context) { + mScroller = new OverScroller(getContext()); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mOverscrollDistance = configuration.getScaledOverscrollDistance(); + mOverflingDistance = configuration.getScaledOverflingDistance(); + float densityScale = getResources().getDisplayMetrics().density; + float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); + mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); + + mSidePaddings = context.getResources() + .getDimensionPixelSize(R.dimen.notification_side_padding); + mCollapsedSize = context.getResources() + .getDimensionPixelSize(R.dimen.notification_min_height); + mBottomStackPeekSize = context.getResources() + .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); + mEmptyMarginBottom = context.getResources().getDimensionPixelSize( + R.dimen.notification_stack_margin_bottom); + mPaddingBetweenElements = context.getResources() + .getDimensionPixelSize(R.dimen.notification_padding); + mStackScrollAlgorithm = new StackScrollAlgorithm(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int mode = MeasureSpec.getMode(widthMeasureSpec); + int size = MeasureSpec.getSize(widthMeasureSpec); + int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode); + measureChildren(childMeasureSpec, 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); + 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() - mEmptyMarginBottom); + updateContentHeight(); + getViewTreeObserver().addOnPreDrawListener(mAfterLayoutPreDrawListener); + } + + public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) { + mListener = listener; + } + + /** + * Returns the location the given child is currently rendered at. + * + * @param child the child to get the location for + * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants + */ + public int getChildLocation(View child) { + ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child); + if (childViewState == null) { + return ViewState.LOCATION_UNKNOWN; + } + return childViewState.location; + } + + private void setMaxLayoutHeight(int maxLayoutHeight) { + mMaxLayoutHeight = maxLayoutHeight; + updateAlgorithmHeightAndPadding(); + } + + private void updateAlgorithmHeightAndPadding() { + mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight()); + mStackScrollAlgorithm.setTopPadding(mTopPadding); + } + + /** + * 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() { + mCurrentStackScrollState.setScrollY(mOwnScrollY); + mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState); + if (!isCurrentlyAnimating() && !mChildHierarchyDirty) { + applyCurrentState(); + if (mListener != null) { + mListener.onChildLocationsChanged(this); + } + } else { + startAnimationToState(mCurrentStackScrollState); + } + } + + private boolean isCurrentlyAnimating() { + return mStateAnimator.isRunning(); + } + + private void updateScrollPositionIfNecessary() { + int scrollRange = getScrollRange(); + if (scrollRange < mOwnScrollY) { + mOwnScrollY = scrollRange; + } + } + + public int getTopPadding() { + return mTopPadding; + } + + public void setTopPadding(int topPadding) { + if (mTopPadding != topPadding) { + mTopPadding = topPadding; + updateAlgorithmHeightAndPadding(); + updateContentHeight(); + updateChildren(); + } + } + + /** + * Update the height of the stack to a new height. + * + * @param height the new height of the stack + */ + public void setStackHeight(float height) { + setIsExpanded(height > 0.0f); + int newStackHeight = (int) height; + int itemHeight = getItemHeight(); + int bottomStackPeekSize = mBottomStackPeekSize; + int minStackHeight = itemHeight + bottomStackPeekSize; + int stackHeight; + if (newStackHeight - mTopPadding >= minStackHeight) { + setTranslationY(0); + stackHeight = newStackHeight; + } else { + + // We did not reach the position yet where we actually start growing, + // so we translate the stack upwards. + int translationY = (newStackHeight - minStackHeight); + // A slight parallax effect is introduced in order for the stack to catch up with + // the top card. + float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight; + partiallyThere = Math.max(0, partiallyThere); + translationY += (1 - partiallyThere) * bottomStackPeekSize; + setTranslationY(translationY - mTopPadding); + stackHeight = (int) (height - (translationY - mTopPadding)); + } + if (stackHeight != mCurrentStackHeight) { + mCurrentStackHeight = stackHeight; + updateAlgorithmHeightAndPadding(); + updateChildren(); + } + } + + /** + * 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 getItemHeight() { + return mCollapsedSize; + } + + public int getBottomStackPeekSize() { + return mBottomStackPeekSize; + } + + public void setLongPressListener(View.OnLongClickListener listener) { + mSwipeHelper.setLongPressListener(listener); + } + + public void onChildDismissed(View v) { + if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); + final View veto = v.findViewById(R.id.veto); + if (veto != null && veto.getVisibility() != View.GONE) { + veto.performClick(); + } + setSwipingInProgress(false); + mSwipedOutViews.add(v); + } + + public void onBeginDrag(View v) { + setSwipingInProgress(true); + } + + public void onDragCancelled(View v) { + setSwipingInProgress(false); + } + + public View getChildAtPosition(MotionEvent ev) { + return getChildAtPosition(ev.getX(), ev.getY()); + } + + public View getChildAtRawPosition(float touchX, float touchY) { + int[] location = new int[2]; + getLocationOnScreen(location); + return getChildAtPosition(touchX - location[0],touchY - location[1]); + } + + public View getChildAtPosition(float touchX, float touchY) { + // 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() == GONE) { + continue; + } + float top = slidingChild.getTranslationY(); + float bottom = top + slidingChild.getActualHeight(); + int left = slidingChild.getLeft(); + int right = slidingChild.getRight(); + + if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { + return slidingChild; + } + } + return null; + } + + public boolean canChildBeExpanded(View v) { + return v instanceof ExpandableNotificationRow + && ((ExpandableNotificationRow) v).isExpandable(); + } + + public void setUserExpandedChild(View v, boolean userExpanded) { + if (v instanceof ExpandableNotificationRow) { + ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); + } + } + + public void setUserLockedChild(View v, boolean userLocked) { + if (v instanceof ExpandableNotificationRow) { + ((ExpandableNotificationRow) v).setUserLocked(userLocked); + } + } + + public View getChildContentView(View v) { + return v; + } + + public boolean canChildBeDismissed(View v) { + final View veto = v.findViewById(R.id.veto); + return (veto != null && veto.getVisibility() != View.GONE); + } + + 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 dismissRowAnimated(View child, int vel) { + mSwipeHelper.dismissChild(child, vel); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean scrollerWantsIt = false; + if (!mSwipingInProgress) { + scrollerWantsIt = onScrollTouch(ev); + } + boolean horizontalSwipeWantsIt = false; + if (!mIsBeingDragged) { + horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); + } + return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev); + } + + private boolean onScrollTouch(MotionEvent ev) { + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + if (getChildCount() == 0) { + 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.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = (int) ev.getY(); + 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); + int deltaY = mLastMotionY - y; + if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { + setIsBeingDragged(true); + if (deltaY > 0) { + deltaY -= mTouchSlop; + } else { + deltaY += mTouchSlop; + } + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + mLastMotionY = y; + + final int oldX = mScrollX; + final int oldY = mOwnScrollY; + final int range = getScrollRange(); + final int overscrollMode = getOverScrollMode(); + final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + // Calling overScrollBy will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollBy(0, deltaY, 0, mOwnScrollY, + 0, range, 0, mOverscrollDistance, true)) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + // TODO: Overscroll +// if (canOverscroll) { +// final int pulledToY = oldY + deltaY; +// if (pulledToY < 0) { +// mEdgeGlowTop.onPull((float) deltaY / getHeight()); +// if (!mEdgeGlowBottom.isFinished()) { +// mEdgeGlowBottom.onRelease(); +// } +// } else if (pulledToY > range) { +// mEdgeGlowBottom.onPull((float) deltaY / getHeight()); +// if (!mEdgeGlowTop.isFinished()) { +// mEdgeGlowTop.onRelease(); +// } +// } +// if (mEdgeGlowTop != null +// && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){ +// postInvalidateOnAnimation(); +// } +// } + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); + + if (getChildCount() > 0) { + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + fling(-initialVelocity); + } else { + if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, + getScrollRange())) { + postInvalidateOnAnimation(); + } + } + } + + mActivePointerId = INVALID_POINTER; + endDrag(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged && getChildCount() > 0) { + if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { + postInvalidateOnAnimation(); + } + mActivePointerId = INVALID_POINTER; + endDrag(); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + mLastMotionY = (int) ev.getY(index); + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); + break; + } + return true; + } + + 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(); + } + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + // This is called at drawing time by ViewGroup. + int oldX = mScrollX; + int oldY = mOwnScrollY; + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + final int range = getScrollRange(); + final int overscrollMode = getOverScrollMode(); + final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, + 0, mOverflingDistance, false); + onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); + + if (canOverscroll) { + // TODO: Overscroll +// if (y < 0 && oldY >= 0) { +// mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); +// } else if (y > range && oldY <= range) { +// mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); +// } + } + updateChildren(); + } + + // Keep on drawing until the animation has finished. + postInvalidateOnAnimation(); + } + } + + public void customScrollBy(int y) { + mOwnScrollY += y; + updateChildren(); + } + + public void customScrollTo(int y) { + mOwnScrollY = y; + updateChildren(); + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + // Treat animating scrolls differently; see #computeScroll() for why. + if (!mScroller.isFinished()) { + final int oldX = mScrollX; + final int oldY = mOwnScrollY; + mScrollX = scrollX; + mOwnScrollY = scrollY; + invalidateParentIfNeeded(); + onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); + if (clampedY) { + mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange()); + } + updateChildren(); + } else { + customScrollTo(scrollY); + scrollTo(scrollX, mScrollY); + } + } + + private int getScrollRange() { + int scrollRange = 0; + ExpandableView firstChild = (ExpandableView) getFirstChildNotGone(); + if (firstChild != null) { + int contentHeight = getContentHeight(); + int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild); + + scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize); + if (scrollRange > 0 && getChildCount() > 0) { + // We want to at least be able collapse the first item and not ending in a weird + // end state. + scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize); + } + } + return scrollRange; + } + + /** + * @return the first child which has visibility unequal to GONE + */ + private View getFirstChildNotGone() { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child.getVisibility() != View.GONE) { + return child; + } + } + return null; + } + + private int getMaxExpandHeight(View view) { + if (view instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) view; + return row.getMaximumAllowedExpandHeight(); + } + return view.getHeight(); + } + + private int getContentHeight() { + return mContentHeight; + } + + private void updateContentHeight() { + int height = 0; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child.getVisibility() != View.GONE) { + if (height != 0) { + // add the padding before this element + height += mPaddingBetweenElements; + } + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + height += row.getMaximumAllowedExpandHeight(); + } else if (child instanceof ExpandableView) { + ExpandableView expandableView = (ExpandableView) child; + height += expandableView.getActualHeight(); + } + } + } + mContentHeight = height + mTopPadding; + } + + /** + * 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. + */ + private void fling(int velocityY) { + if (getChildCount() > 0) { + int height = (int) getLayoutHeight(); + int bottom = getContentHeight(); + + mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0, + Math.max(0, bottom - height), 0, height/2); + + postInvalidateOnAnimation(); + } + } + + private void endDrag() { + setIsBeingDragged(false); + + recycleVelocityTracker(); + + // TODO: Overscroll +// if (mEdgeGlowTop != null) { +// mEdgeGlowTop.onRelease(); +// mEdgeGlowBottom.onRelease(); +// } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + boolean scrollWantsIt = false; + if (!mSwipingInProgress) { + scrollWantsIt = onInterceptTouchEventScroll(ev); + } + boolean swipeWantsIt = false; + if (!mIsBeingDragged) { + swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); + } + return swipeWantsIt || scrollWantsIt || + super.onInterceptTouchEvent(ev); + } + + @Override + protected void onViewRemoved(View child) { + super.onViewRemoved(child); + ((ExpandableView) child).setOnHeightChangedListener(null); + mCurrentStackScrollState.removeViewStateForView(child); + mStackScrollAlgorithm.notifyChildrenChanged(this); + updateScrollStateForRemovedChild(child); + if (mIsExpanded) { + + // Generate Animations + mChildrenToRemoveAnimated.add(child); + mChildHierarchyDirty = true; + } + } + + /** + * Updates the scroll position when a child was removed + * + * @param removedChild the removed child + */ + private void updateScrollStateForRemovedChild(View removedChild) { + int startingPosition = getPositionInLinearLayout(removedChild); + int childHeight = removedChild.getHeight() + mPaddingBetweenElements; + 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 + mOwnScrollY -= childHeight; + } else if (startingPosition < mOwnScrollY) { + // This child is currently being scrolled into, set the scroll position to the start of + // this child + mOwnScrollY = startingPosition; + } + } + + private int getPositionInLinearLayout(View requestedChild) { + int position = 0; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child == requestedChild) { + return position; + } + if (child.getVisibility() != View.GONE) { + position += child.getHeight(); + if (i < getChildCount()-1) { + position += mPaddingBetweenElements; + } + } + } + return 0; + } + + @Override + protected void onViewAdded(View child) { + super.onViewAdded(child); + mStackScrollAlgorithm.notifyChildrenChanged(this); + ((ExpandableView) child).setOnHeightChangedListener(this); + if (child.getVisibility() != View.GONE) { + generateAddAnimation(child); + } + } + + public void generateAddAnimation(View child) { + if (mIsExpanded) { + + // Generate Animations + mChildrenToAddAnimated.add(child); + mChildHierarchyDirty = true; + } + } + + /** + * 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) { + if (child != null && child.getParent() == this) { + // TODO: handle this + } + } + + private void startAnimationToState(StackScrollState finalState) { + if (mChildHierarchyDirty) { + generateChildHierarchyEvents(); + mChildHierarchyDirty = false; + } + mStateAnimator.startAnimationForEvents(mAnimationEvents, finalState); + } + + private void generateChildHierarchyEvents() { + generateChildAdditionEvents(); + generateChildRemovalEvents(); + mChildHierarchyDirty = false; + } + + private void generateChildRemovalEvents() { + for (View child : mChildrenToRemoveAnimated) { + boolean childWasSwipedOut = mSwipedOutViews.contains(child); + int animationType = childWasSwipedOut + ? ChildHierarchyChangeEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT + : ChildHierarchyChangeEvent.ANIMATION_TYPE_REMOVE; + mAnimationEvents.add(new ChildHierarchyChangeEvent(child, animationType)); + } + mSwipedOutViews.clear(); + mChildrenToRemoveAnimated.clear(); + } + + private void generateChildAdditionEvents() { + for (View child : mChildrenToAddAnimated) { + mAnimationEvents.add(new ChildHierarchyChangeEvent(child, + ChildHierarchyChangeEvent.ANIMATION_TYPE_ADD)); + } + mChildrenToAddAnimated.clear(); + } + + private boolean onInterceptTouchEventScroll(MotionEvent ev) { + /* + * 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 he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + + /* + * Don't try to intercept touch if we can't scroll anyway. + */ + if (mOwnScrollY == 0 && getScrollRange() == 0) { + return false; + } + + 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 his 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 yDiff = Math.abs(y - mLastMotionY); + if (yDiff > mTouchSlop) { + setIsBeingDragged(true); + mLastMotionY = y; + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + } + break; + } + + case MotionEvent.ACTION_DOWN: { + final int y = (int) ev.getY(); + if (getChildAtPosition(ev.getX(), y) == null) { + setIsBeingDragged(false); + recycleVelocityTracker(); + break; + } + + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionY = y; + 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())) { + postInvalidateOnAnimation(); + } + 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; + } + + private void setIsBeingDragged(boolean isDragged) { + mIsBeingDragged = isDragged; + if (isDragged) { + requestDisallowInterceptTouchEvent(true); + mSwipeHelper.removeLongPressCallback(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (!hasWindowFocus) { + 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(getHeight() - mContentHeight, 0); + } + + public void onExpansionStarted() { + mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); + } + + public void onExpansionStopped() { + mStackScrollAlgorithm.onExpansionStopped(); + } + + private void setIsExpanded(boolean isExpanded) { + mIsExpanded = isExpanded; + mStackScrollAlgorithm.setIsExpanded(isExpanded); + if (!isExpanded) { + mOwnScrollY = 0; + } + } + + @Override + public void onHeightChanged(ExpandableView view) { + if (mListenForHeightChanges && !isCurrentlyAnimating()) { + updateContentHeight(); + updateScrollPositionIfNecessary(); + if (mOnHeightChangedListener != null) { + mOnHeightChangedListener.onHeightChanged(view); + } + updateChildren(); + } + } + + public void setOnHeightChangedListener( + ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { + this.mOnHeightChangedListener = mOnHeightChangedListener; + } + + public void onChildAnimationFinished() { + applyCurrentState(); + mAnimationEvents.clear(); + } + + private void applyCurrentState() { + mListenForHeightChanges = false; + mCurrentStackScrollState.apply(); + mListenForHeightChanges = true; + } + + /** + * A listener that is notified when some child locations might have changed. + */ + public interface OnChildLocationsChangedListener { + public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); + } + + static class ChildHierarchyChangeEvent { + + static int ANIMATION_TYPE_ADD = 1; + static int ANIMATION_TYPE_REMOVE = 2; + static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 3; + final long eventStartTime; + final View changingView; + final int animationType; + + ChildHierarchyChangeEvent(View view, int type) { + eventStartTime = AnimationUtils.currentAnimationTimeMillis(); + changingView = view; + animationType = type; + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java new file mode 100644 index 0000000..38b544f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java @@ -0,0 +1,112 @@ +/* + * 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 java.util.ArrayList; + +/** + * A Functor which interpolates the stack distance linearly based on base values. + * The base values are based on an interpolation between a linear function and a + * quadratic function + */ +public class PiecewiseLinearIndentationFunctor extends StackIndentationFunctor { + + private final ArrayList<Float> mBaseValues; + private final float mLinearPart; + + /** + * @param maxItemsInStack The maximum number of items which should be visible at the same time, + * i.e the function returns totalTransitionDistance for the element with + * index maxItemsInStack + * @param peekSize The visual appearance of this is how far the cards in the stack peek + * out below the top card and it is measured in real pixels. + * Note that the visual appearance does not necessarily always correspond to + * the actual visual distance below the top card but is a maximum, + * achieved when the next card just starts transitioning into the stack and + * the stack is full. + * If totalTransitionDistance is equal to this, we directly start at the peek, + * otherwise the first element transitions between 0 and + * totalTransitionDistance - peekSize. + * Visualization: + * --------------------------------------------------- --- + * | | | + * | FIRST ITEM | | <- totalTransitionDistance + * | | | + * |---------------------------------------------------| | --- + * |__________________SECOND ITEM______________________| | | <- peekSize + * |===================================================| _|_ _|_ + * + * @param totalTransitionDistance The total transition distance an element has to go through + * @param linearPart The interpolation factor between the linear and the quadratic amount taken. + * This factor must be somewhere in [0 , 1] + */ + PiecewiseLinearIndentationFunctor(int maxItemsInStack, + int peekSize, + int totalTransitionDistance, + float linearPart) { + super(maxItemsInStack, peekSize, totalTransitionDistance); + mBaseValues = new ArrayList<Float>(maxItemsInStack+1); + initBaseValues(); + mLinearPart = linearPart; + } + + private void initBaseValues() { + int sumOfSquares = getSumOfSquares(mMaxItemsInStack-1); + int totalWeight = 0; + mBaseValues.add(0.0f); + for (int i = 0; i < mMaxItemsInStack - 1; i++) { + totalWeight += (mMaxItemsInStack - i - 1) * (mMaxItemsInStack - i - 1); + mBaseValues.add((float) totalWeight / sumOfSquares); + } + } + + /** + * Get the sum of squares up to and including n, i.e sum(i * i, 1, n) + * + * @param n the maximum square to include + * @return + */ + private int getSumOfSquares(int n) { + return n * (n + 1) * (2 * n + 1) / 6; + } + + @Override + public float getValue(float itemsBefore) { + if (mStackStartsAtPeek) { + // We directly start at the stack, so no initial interpolation. + itemsBefore++; + } + if (itemsBefore < 0) { + return 0; + } else if (itemsBefore >= mMaxItemsInStack) { + return mTotalTransitionDistance; + } + int below = (int) itemsBefore; + float partialIn = itemsBefore - below; + + if (below == 0) { + return mDistanceToPeekStart * partialIn; + } else { + float result = mDistanceToPeekStart; + float progress = mBaseValues.get(below - 1) * (1 - partialIn) + + mBaseValues.get(below) * partialIn; + result += (progress * (1 - mLinearPart) + + (itemsBefore - 1) / (mMaxItemsInStack - 1) * mLinearPart) * mPeekSize; + return result; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java new file mode 100644 index 0000000..f72947a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java @@ -0,0 +1,74 @@ +/* + * 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; + +/** + * A functor which can be queried for offset given the number of items before it. + */ +public abstract class StackIndentationFunctor { + + protected final int mTotalTransitionDistance; + protected final int mDistanceToPeekStart; + protected int mMaxItemsInStack; + protected int mPeekSize; + protected boolean mStackStartsAtPeek; + + /** + * @param maxItemsInStack The maximum number of items which should be visible at the same time, + * i.e the function returns totalTransitionDistance for the element with + * index maxItemsInStack + * @param peekSize The visual appearance of this is how far the cards in the stack peek + * out below the top card and it is measured in real pixels. + * Note that the visual appearance does not necessarily always correspond to + * the actual visual distance below the top card but is a maximum, + * achieved when the next card just starts transitioning into the stack and + * the stack is full. + * If totalTransitionDistance is equal to this, we directly start at the peek, + * otherwise the first element transitions between 0 and + * totalTransitionDistance - peekSize. + * Visualization: + * --------------------------------------------------- --- + * | | | + * | FIRST ITEM | | <- totalTransitionDistance + * | | | + * |---------------------------------------------------| | --- + * |__________________SECOND ITEM______________________| | | <- peekSize + * |===================================================| _|_ _|_ + * + * @param totalTransitionDistance The total transition distance an element has to go through + */ + StackIndentationFunctor(int maxItemsInStack, int peekSize, int totalTransitionDistance) { + mTotalTransitionDistance = totalTransitionDistance; + mDistanceToPeekStart = mTotalTransitionDistance - peekSize; + mStackStartsAtPeek = mDistanceToPeekStart == 0; + mMaxItemsInStack = maxItemsInStack; + mPeekSize = peekSize; + + } + + public void setPeekSize(int mPeekSize) { + this.mPeekSize = mPeekSize; + } + + /** + * Gets the offset of this Functor given a the quantity of items before it + * + * @param itemsBefore how many items are already in the stack before this element + * @return the offset + */ + public abstract float getValue(float itemsBefore); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java new file mode 100644 index 0000000..9bde673 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java @@ -0,0 +1,612 @@ +/* + * 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.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.ArrayList; + +/** + * 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 static final int MAX_ITEMS_IN_BOTTOM_STACK = 3; + private static final int MAX_ITEMS_IN_TOP_STACK = 3; + + private int mPaddingBetweenElements; + private int mCollapsedSize; + private int mTopStackPeekSize; + private int mBottomStackPeekSize; + private int mZDistanceBetweenElements; + private int mZBasicHeight; + + private StackIndentationFunctor mTopStackIndentationFunctor; + private StackIndentationFunctor mBottomStackIndentationFunctor; + + private int mLayoutHeight; + + /** mLayoutHeight - mTopPadding */ + private int mInnerHeight; + private int mTopPadding; + private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); + private boolean mIsExpansionChanging; + private int mFirstChildMaxHeight; + private boolean mIsExpanded; + private ExpandableView mFirstChildWhileExpanding; + private boolean mExpandedOnStart; + private int mTopStackTotalSize; + + public StackScrollAlgorithm(Context context) { + initConstants(context); + } + + private void initConstants(Context context) { + mPaddingBetweenElements = context.getResources() + .getDimensionPixelSize(R.dimen.notification_padding); + mCollapsedSize = context.getResources() + .getDimensionPixelSize(R.dimen.notification_min_height); + mTopStackPeekSize = context.getResources() + .getDimensionPixelSize(R.dimen.top_stack_peek_amount); + mBottomStackPeekSize = context.getResources() + .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); + mZDistanceBetweenElements = context.getResources() + .getDimensionPixelSize(R.dimen.z_distance_between_notifications); + mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; + mTopStackTotalSize = mCollapsedSize + mPaddingBetweenElements; + mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( + MAX_ITEMS_IN_TOP_STACK, + mTopStackPeekSize, + mTopStackTotalSize, + 0.5f); + mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( + MAX_ITEMS_IN_BOTTOM_STACK, + mBottomStackPeekSize, + mCollapsedSize + mBottomStackPeekSize + mPaddingBetweenElements, + 0.5f); + } + + + public void getStackScrollState(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(); + + algorithmState.itemsInTopStack = 0.0f; + algorithmState.partialInTop = 0.0f; + algorithmState.lastTopStackIndex = 0; + algorithmState.scrolledPixelsTop = 0; + algorithmState.itemsInBottomStack = 0.0f; + algorithmState.partialInBottom = 0.0f; + algorithmState.scrollY = resultState.getScrollY() + mCollapsedSize; + + updateVisibleChildren(resultState, algorithmState); + + // Phase 1: + findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState); + + // Phase 2: + updatePositionsForState(resultState, algorithmState); + + // Phase 3: + updateZValuesForState(resultState, algorithmState); + } + + /** + * Update the visible children on the state. + */ + private void updateVisibleChildren(StackScrollState resultState, + StackScrollAlgorithmState state) { + ViewGroup hostView = resultState.getHostView(); + int childCount = hostView.getChildCount(); + state.visibleChildren.clear(); + state.visibleChildren.ensureCapacity(childCount); + for (int i = 0; i < childCount; i++) { + ExpandableView v = (ExpandableView) hostView.getChildAt(i); + if (v.getVisibility() != View.GONE) { + state.visibleChildren.add(v); + } + } + } + + /** + * 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 + * and which will be updated + */ + private void updatePositionsForState(StackScrollState resultState, + StackScrollAlgorithmState algorithmState) { + + // The starting position of the bottom stack peek + float bottomPeekStart = mInnerHeight - mBottomStackPeekSize; + + // The position where the bottom stack starts. + float bottomStackStart = bottomPeekStart - mCollapsedSize; + + // The y coordinate of the current child. + float currentYPosition = 0.0f; + + // How far in is the element currently transitioning into the bottom stack. + float yPositionInScrollView = 0.0f; + + int childCount = algorithmState.visibleChildren.size(); + int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack; + for (int i = 0; i < childCount; i++) { + ExpandableView child = algorithmState.visibleChildren.get(i); + StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); + childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN; + int childHeight = getMaxAllowedChildHeight(child); + float yPositionInScrollViewAfterElement = yPositionInScrollView + + childHeight + + mPaddingBetweenElements; + float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize; + + if (i == algorithmState.lastTopStackIndex + 1) { + // Normally the position of this child is the position in the regular scrollview, + // but if the two stacks are very close to each other, + // then have have to push it even more upwards to the position of the bottom + // stack start. + currentYPosition = Math.min(scrollOffset, bottomStackStart); + } + childViewState.yTranslation = currentYPosition; + + // The y position after this element + float nextYPosition = currentYPosition + childHeight + + mPaddingBetweenElements; + + if (i <= algorithmState.lastTopStackIndex) { + // Case 1: + // We are in the top Stack + updateStateForTopStackChild(algorithmState, + numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset); + clampYTranslation(childViewState, childHeight); + // check if we are overlapping with the bottom stack + if (childViewState.yTranslation + childHeight + mPaddingBetweenElements + >= bottomStackStart && !mIsExpansionChanging) { + // TODO: handle overlapping sizes with end stack better + // we just collapse this element + childViewState.height = mCollapsedSize; + } + } else if (nextYPosition >= bottomStackStart) { + // Case 2: + // We are in the bottom stack. + if (currentYPosition >= bottomStackStart) { + // According to the regular scroll view we are fully translated out of the + // bottom of the screen so we are fully in the bottom stack + updateStateForChildFullyInBottomStack(algorithmState, + bottomStackStart, childViewState, childHeight); + } else { + // According to the regular scroll view we are currently translating out of / + // into the bottom of the screen + updateStateForChildTransitioningInBottom(algorithmState, + bottomStackStart, bottomPeekStart, currentYPosition, + childViewState, childHeight); + } + } else { + // Case 3: + // We are in the regular scroll area. + childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; + clampYTranslation(childViewState, childHeight); + } + + // The first card is always rendered. + if (i == 0) { + childViewState.alpha = 1.0f; + childViewState.yTranslation = 0; + childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD; + } + if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) { + Log.wtf(LOG_TAG, "Failed to assign location for child " + i); + } + currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements; + yPositionInScrollView = yPositionInScrollViewAfterElement; + + childViewState.yTranslation += mTopPadding; + } + } + + /** + * Clamp the yTranslation both up and down to valid positions. + * + * @param childViewState the view state of the child + * @param childHeight the height of this child + */ + private void clampYTranslation(StackScrollState.ViewState childViewState, int childHeight) { + clampPositionToBottomStackStart(childViewState, childHeight); + clampPositionToTopStackEnd(childViewState, childHeight); + } + + /** + * Clamp the yTranslation of the child down such that its end is at most on the beginning of + * the bottom stack. + * + * @param childViewState the view state of the child + * @param childHeight the height of this child + */ + private void clampPositionToBottomStackStart(StackScrollState.ViewState childViewState, + int childHeight) { + childViewState.yTranslation = Math.min(childViewState.yTranslation, + mInnerHeight - mBottomStackPeekSize - childHeight); + } + + /** + * Clamp the yTranslation of the child up such that its end is at lest on the end of the top + * stack.get + * + * @param childViewState the view state of the child + * @param childHeight the height of this child + */ + private void clampPositionToTopStackEnd(StackScrollState.ViewState childViewState, + int childHeight) { + childViewState.yTranslation = Math.max(childViewState.yTranslation, + mCollapsedSize - childHeight); + } + + private int getMaxAllowedChildHeight(View child) { + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + return row.getMaximumAllowedExpandHeight(); + } else if (child instanceof ExpandableView) { + ExpandableView expandableView = (ExpandableView) child; + return expandableView.getActualHeight(); + } + return child == null? mCollapsedSize : child.getHeight(); + } + + private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, + float transitioningPositionStart, float bottomPeakStart, float currentYPosition, + StackScrollState.ViewState childViewState, int childHeight) { + + // This is the transitioning element on top of bottom stack, calculate how far we are in. + algorithmState.partialInBottom = 1.0f - ( + (transitioningPositionStart - currentYPosition) / (childHeight + + mPaddingBetweenElements)); + + // the offset starting at the transitionPosition of the bottom stack + float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom); + algorithmState.itemsInBottomStack += algorithmState.partialInBottom; + childViewState.yTranslation = transitioningPositionStart + offset - childHeight + - mPaddingBetweenElements; + + // We want at least to be at the end of the top stack when collapsing + clampPositionToTopStackEnd(childViewState, childHeight); + childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; + } + + private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, + float transitioningPositionStart, StackScrollState.ViewState childViewState, + int childHeight) { + + float currentYPosition; + algorithmState.itemsInBottomStack += 1.0f; + if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { + // We are visually entering the bottom stack + currentYPosition = transitioningPositionStart + + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack) + - mPaddingBetweenElements; + childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING; + } else { + // we are fully inside the stack + if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { + childViewState.alpha = 0.0f; + } else if (algorithmState.itemsInBottomStack + > MAX_ITEMS_IN_BOTTOM_STACK + 1) { + childViewState.alpha = 1.0f - algorithmState.partialInBottom; + } + childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN; + currentYPosition = mInnerHeight; + } + childViewState.yTranslation = currentYPosition - childHeight; + clampPositionToTopStackEnd(childViewState, childHeight); + } + + private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, + int numberOfElementsCompletelyIn, int i, int childHeight, + StackScrollState.ViewState childViewState, float scrollOffset) { + + + // First we calculate the index relative to the current stack window of size at most + // {@link #MAX_ITEMS_IN_TOP_STACK} + int paddedIndex = i - 1 + - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); + if (paddedIndex >= 0) { + + // We are currently visually entering the top stack + float distanceToStack = childHeight - algorithmState.scrolledPixelsTop; + if (i == algorithmState.lastTopStackIndex && distanceToStack > mTopStackTotalSize) { + + // Child is currently translating into stack but not yet inside slow down zone. + // Handle it like the regular scrollview. + childViewState.yTranslation = scrollOffset; + } else { + // Apply stacking logic. + float numItemsBefore; + if (i == algorithmState.lastTopStackIndex) { + numItemsBefore = 1.0f - (distanceToStack / mTopStackTotalSize); + } else { + numItemsBefore = algorithmState.itemsInTopStack - i; + } + // The end position of the current child + float currentChildEndY = mCollapsedSize + mTopStackTotalSize - + mTopStackIndentationFunctor.getValue(numItemsBefore); + childViewState.yTranslation = currentChildEndY - childHeight; + } + childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; + } else { + if (paddedIndex == -1) { + childViewState.alpha = 1.0f - algorithmState.partialInTop; + } else { + // We are hidden behind the top card and faded out, so we can hide ourselves. + childViewState.alpha = 0.0f; + } + childViewState.yTranslation = mCollapsedSize - childHeight; + childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN; + } + + + } + + /** + * Find the number of items in the top stack and update the result state if needed. + * + * @param resultState The result state to update if a height change of an child occurs + * @param algorithmState The state in which the current pass of the algorithm is currently in + * and which will be updated + */ + private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, + StackScrollAlgorithmState algorithmState) { + + // The y Position if the element would be in a regular scrollView + float yPositionInScrollView = 0.0f; + int childCount = algorithmState.visibleChildren.size(); + + // find the number of elements in the top stack. + for (int i = 0; i < childCount; i++) { + ExpandableView child = algorithmState.visibleChildren.get(i); + StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); + int childHeight = getMaxAllowedChildHeight(child); + float yPositionInScrollViewAfterElement = yPositionInScrollView + + childHeight + + mPaddingBetweenElements; + if (yPositionInScrollView < algorithmState.scrollY) { + if (i == 0 && algorithmState.scrollY == mCollapsedSize) { + + // The starting position of the bottom stack peek + int bottomPeekStart = mInnerHeight - mBottomStackPeekSize; + // Collapse and expand the first child while the shade is being expanded + float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding + ? mFirstChildMaxHeight + : childHeight; + childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight), + mCollapsedSize); + algorithmState.itemsInTopStack = 1.0f; + + } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) { + // According to the regular scroll view we are fully off screen + algorithmState.itemsInTopStack += 1.0f; + if (i == 0) { + childViewState.height = mCollapsedSize; + } + } else { + // According to the regular scroll view we are partially off screen + // If it is expanded we have to collapse it to a new size + float newSize = yPositionInScrollViewAfterElement + - mPaddingBetweenElements + - algorithmState.scrollY; + + if (i == 0) { + newSize += mCollapsedSize; + } + + // How much did we scroll into this child + algorithmState.scrolledPixelsTop = childHeight - newSize; + algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight + + mPaddingBetweenElements); + + // Our element can be expanded, so this can get negative + algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop); + algorithmState.itemsInTopStack += algorithmState.partialInTop; + newSize = Math.max(mCollapsedSize, newSize); + if (i == 0) { + childViewState.height = (int) newSize; + } + algorithmState.lastTopStackIndex = i; + break; + } + } else { + algorithmState.lastTopStackIndex = i - 1; + // We are already past the stack so we can end the loop + break; + } + yPositionInScrollView = yPositionInScrollViewAfterElement; + } + } + + /** + * 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 + */ + private void updateZValuesForState(StackScrollState resultState, + StackScrollAlgorithmState algorithmState) { + int childCount = algorithmState.visibleChildren.size(); + for (int i = 0; i < childCount; i++) { + View child = algorithmState.visibleChildren.get(i); + StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); + if (i < algorithmState.itemsInTopStack) { + float stackIndex = algorithmState.itemsInTopStack - i; + stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2); + childViewState.zTranslation = mZBasicHeight + + stackIndex * mZDistanceBetweenElements; + } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { + float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); + float translationZ = mZBasicHeight + - numItemsAbove * mZDistanceBetweenElements; + childViewState.zTranslation = translationZ; + } else { + childViewState.zTranslation = mZBasicHeight; + } + } + } + + public void setLayoutHeight(int layoutHeight) { + this.mLayoutHeight = layoutHeight; + updateInnerHeight(); + } + + public void setTopPadding(int topPadding) { + mTopPadding = topPadding; + updateInnerHeight(); + } + + private void updateInnerHeight() { + mInnerHeight = mLayoutHeight - mTopPadding; + } + + public void onExpansionStarted(StackScrollState currentState) { + mIsExpansionChanging = true; + mExpandedOnStart = mIsExpanded; + ViewGroup hostView = currentState.getHostView(); + updateFirstChildHeightWhileExpanding(hostView); + } + + private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) { + mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView); + if (mFirstChildWhileExpanding != null) { + if (mExpandedOnStart) { + + // We are collapsing the shade, so the first child can get as most as high as the + // current height. + mFirstChildMaxHeight = mFirstChildWhileExpanding.getActualHeight(); + } else { + + // We are expanding the shade, expand it to its full height. + if (mFirstChildWhileExpanding.getWidth() == 0) { + + // This child was not layouted yet, wait for a layout pass + mFirstChildWhileExpanding + .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 (mFirstChildWhileExpanding != null) { + mFirstChildMaxHeight = getMaxAllowedChildHeight( + mFirstChildWhileExpanding); + } else { + mFirstChildMaxHeight = 0; + } + v.removeOnLayoutChangeListener(this); + } + }); + } else { + mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding); + } + } + } else { + mFirstChildMaxHeight = 0; + } + } + + private View findFirstVisibleChild(ViewGroup container) { + int childCount = container.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = container.getChildAt(i); + if (child.getVisibility() != View.GONE) { + return child; + } + } + return null; + } + + public void onExpansionStopped() { + mIsExpansionChanging = false; + mFirstChildWhileExpanding = null; + } + + public void setIsExpanded(boolean isExpanded) { + this.mIsExpanded = isExpanded; + } + + public void notifyChildrenChanged(ViewGroup hostView) { + if (mIsExpansionChanging) { + updateFirstChildHeightWhileExpanding(hostView); + } + } + + class StackScrollAlgorithmState { + + /** + * The scroll position of the algorithm + */ + public int scrollY; + + /** + * The quantity of items which are in the top stack. + */ + public float itemsInTopStack; + + /** + * how far in is the element currently transitioning into the top stack + */ + public float partialInTop; + + /** + * The number of pixels the last child in the top stack has scrolled in to the stack + */ + public float scrolledPixelsTop; + + /** + * The last item index which is in the top stack. + */ + public int lastTopStackIndex; + + /** + * The quantity of items which are in the bottom stack. + */ + public float itemsInBottomStack; + + /** + * how far in is the element currently transitioning into the bottom stack + */ + public float partialInBottom; + + /** + * The children from the host view which are not gone. + */ + public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java new file mode 100644 index 0000000..8c75adc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.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.stack; + +import android.graphics.Outline; +import android.graphics.Rect; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.statusbar.ExpandableView; + +import java.util.HashMap; +import java.util.Map; + +/** + * 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 Map<ExpandableView, ViewState> mStateMap; + private int mScrollY; + private final Rect mClipRect = new Rect(); + private int mBackgroundRoundedRectCornerRadius; + private final Outline mChildOutline = new Outline(); + + public int getScrollY() { + return mScrollY; + } + + public void setScrollY(int scrollY) { + this.mScrollY = scrollY; + } + + public StackScrollState(ViewGroup hostView) { + mHostView = hostView; + mStateMap = new HashMap<ExpandableView, ViewState>(); + mBackgroundRoundedRectCornerRadius = hostView.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.notification_quantum_rounded_rect_radius); + } + + 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); + ViewState viewState = mStateMap.get(child); + if (viewState == null) { + viewState = new ViewState(); + mStateMap.put(child, viewState); + } + // initialize with the default values of the view + viewState.height = child.getActualHeight(); + viewState.gone = child.getVisibility() == View.GONE; + viewState.alpha = 1; + } + } + + public ViewState 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(); + float previousNotificationEnd = 0; + float previousNotificationStart = 0; + for (int i = 0; i < numChildren; i++) { + ExpandableView child = (ExpandableView) mHostView.getChildAt(i); + ViewState 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) { + float alpha = child.getAlpha(); + float yTranslation = child.getTranslationY(); + float zTranslation = child.getTranslationZ(); + int height = child.getActualHeight(); + float newAlpha = state.alpha; + float newYTranslation = state.yTranslation; + float newZTranslation = state.zTranslation; + int newHeight = state.height; + boolean becomesInvisible = newAlpha == 0.0f; + if (alpha != newAlpha) { + // apply layer type + boolean becomesFullyVisible = newAlpha == 1.0f; + boolean newLayerTypeIsHardware = !becomesInvisible && !becomesFullyVisible; + int layerType = child.getLayerType(); + int newLayerType = newLayerTypeIsHardware + ? View.LAYER_TYPE_HARDWARE + : View.LAYER_TYPE_NONE; + if (layerType != newLayerType) { + child.setLayerType(newLayerType, null); + } + + // apply alpha + if (!becomesInvisible) { + child.setAlpha(newAlpha); + } + } + + // apply visibility + int oldVisibility = child.getVisibility(); + int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE; + if (newVisibility != oldVisibility) { + child.setVisibility(newVisibility); + } + + // apply yTranslation + if (yTranslation != newYTranslation) { + child.setTranslationY(newYTranslation); + } + + // apply zTranslation + if (zTranslation != newZTranslation) { + child.setTranslationZ(newZTranslation); + } + + // apply height + if (height != newHeight) { + child.setActualHeight(newHeight); + } + + // apply clipping and shadow + float newNotificationEnd = newYTranslation + newHeight; + updateChildClippingAndBackground(child, newHeight, + newNotificationEnd - (previousNotificationEnd), + (int) (newHeight - (previousNotificationStart - newYTranslation))); + + previousNotificationStart = newYTranslation; + previousNotificationEnd = newNotificationEnd; + } + } + } + + /** + * Updates the shadow outline and the clipping for a view. + * + * @param child the view to update + * @param realHeight the currently applied height of the view + * @param clipHeight the desired clip height, the rest of the view will be clipped from the top + * @param backgroundHeight the desired background height. The shadows of the view will be + * based on this height and the content will be clipped from the top + */ + private void updateChildClippingAndBackground(ExpandableView child, int realHeight, + float clipHeight, int backgroundHeight) { + if (realHeight > clipHeight) { + updateChildClip(child, realHeight, clipHeight); + } else { + child.setClipBounds(null); + } + if (realHeight > backgroundHeight) { + child.setClipTopAmount(realHeight - backgroundHeight); + } else { + child.setClipTopAmount(0); + } + } + + /** + * Updates the clipping of a view + * + * @param child the view to update + * @param height the currently applied height of the view + * @param clipHeight the desired clip height, the rest of the view will be clipped from the top + */ + private void updateChildClip(View child, int height, float clipHeight) { + int clipInset = (int) (height - clipHeight); + mClipRect.set(0, + clipInset, + child.getWidth(), + height); + child.setClipBounds(mClipRect); + } + + public static class ViewState { + + // These are flags such that we can create masks for filtering. + + public static final int LOCATION_UNKNOWN = 0x00; + public static final int LOCATION_FIRST_CARD = 0x01; + public static final int LOCATION_TOP_STACK_HIDDEN = 0x02; + public static final int LOCATION_TOP_STACK_PEEKING = 0x04; + public static final int LOCATION_MAIN_AREA = 0x08; + public static final int LOCATION_BOTTOM_STACK_PEEKING = 0x10; + public static final int LOCATION_BOTTOM_STACK_HIDDEN = 0x20; + + float alpha; + float yTranslation; + float zTranslation; + int height; + boolean gone; + + /** + * The location this view is currently rendered at. + * + * <p>See <code>LOCATION_</code> flags.</p> + */ + int location; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java new file mode 100644 index 0000000..24daa4f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java @@ -0,0 +1,159 @@ +/* + * 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.ValueAnimator; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import com.android.systemui.statusbar.ExpandableView; + +import java.util.ArrayList; + +/** + * An stack state animator which handles animations to new StackScrollStates + */ +public class StackStateAnimator { + + private static final int ANIMATION_DURATION = 360; + + private final Interpolator mFastOutSlowInInterpolator; + public NotificationStackScrollLayout mHostLayout; + private boolean mAnimationIsRunning; + private ArrayList<NotificationStackScrollLayout.ChildHierarchyChangeEvent> mHandledEvents = + new ArrayList<>(); + + public StackStateAnimator(NotificationStackScrollLayout hostLayout) { + mHostLayout = hostLayout; + mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(hostLayout.getContext(), + android.R.interpolator.fast_out_slow_in); + } + + public boolean isRunning() { + return mAnimationIsRunning; + } + + public void startAnimationForEvents( + ArrayList<NotificationStackScrollLayout.ChildHierarchyChangeEvent> mAnimationEvents, + StackScrollState finalState) { + int numEvents = mAnimationEvents.size(); + if (numEvents == 0) { + // No events, so we don't perform any animation + return; + } + long lastEventStartTime = mAnimationEvents.get(numEvents - 1).eventStartTime; + long eventEnd = lastEventStartTime + ANIMATION_DURATION; + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + long newDuration = eventEnd - currentTime; + if (newDuration <= 0) { + // last event is long before this, so we don't do anything + return; + } + initializeAddedViewStates(mAnimationEvents, finalState); + int childCount = mHostLayout.getChildCount(); + boolean isFirstAnimatingView = true; + for (int i = 0; i < childCount; i++) { + final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i); + StackScrollState.ViewState viewState = finalState.getViewStateForView(child); + if (viewState == null) { + continue; + } + int childVisibility = child.getVisibility(); + boolean wasVisible = childVisibility == View.VISIBLE; + final float alpha = viewState.alpha; + if (!wasVisible && alpha != 0 && !viewState.gone) { + child.setVisibility(View.VISIBLE); + } + + startPropertyAnimation(newDuration, isFirstAnimatingView, child, viewState, alpha); + + // TODO: animate clipBounds + child.setClipBounds(null); + int currentHeigth = child.getActualHeight(); + if (viewState.height != currentHeigth) { + startHeightAnimation(newDuration, child, viewState, currentHeigth); + } + isFirstAnimatingView = false; + } + mAnimationIsRunning = true; + } + + private void startPropertyAnimation(long newDuration, final boolean hasFinishAction, + final ExpandableView child, StackScrollState.ViewState viewState, final float alpha) { + child.animate().setInterpolator(mFastOutSlowInInterpolator) + .alpha(alpha) + .translationY(viewState.yTranslation) + .translationZ(viewState.zTranslation) + .setDuration(newDuration) + .withEndAction(new Runnable() { + @Override + public void run() { + mAnimationIsRunning = false; + if (hasFinishAction) { + mHandledEvents.clear(); + mHostLayout.onChildAnimationFinished(); + } + if (alpha == 0) { + child.setVisibility(View.INVISIBLE); + } + } + }); + } + + private void startHeightAnimation(long newDuration, final ExpandableView child, + StackScrollState.ViewState viewState, int currentHeigth) { + ValueAnimator heightAnimator = ValueAnimator.ofInt(currentHeigth, viewState.height); + heightAnimator.setInterpolator(mFastOutSlowInInterpolator); + heightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + child.setActualHeight((int) animation.getAnimatedValue()); + } + }); + heightAnimator.setDuration(newDuration); + heightAnimator.start(); + } + + /** + * Initialize the viewStates for the added children + * + * @param animationEvents the animation events who contain the added children + * @param finalState the final state to animate to + */ + private void initializeAddedViewStates( + ArrayList<NotificationStackScrollLayout.ChildHierarchyChangeEvent> animationEvents, + StackScrollState finalState) { + for (NotificationStackScrollLayout.ChildHierarchyChangeEvent event: animationEvents) { + View changingView = event.changingView; + if (event.animationType == NotificationStackScrollLayout.ChildHierarchyChangeEvent + .ANIMATION_TYPE_ADD && !mHandledEvents.contains(event)) { + + // This item is added, initialize it's properties. + StackScrollState.ViewState viewState = finalState.getViewStateForView(changingView); + if (viewState == null) { + // The position for this child was never generated, let's continue. + continue; + } + changingView.setAlpha(0); + changingView.setTranslationY(viewState.yTranslation); + changingView.setTranslationZ(viewState.zTranslation); + mHandledEvents.add(event); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java index dd13e31..d615542 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java @@ -22,6 +22,7 @@ import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; +import com.android.internal.policy.IKeyguardShowCallback; import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.statusbar.BaseStatusBar; @@ -93,10 +94,6 @@ public class TvStatusBar extends BaseStatusBar { } @Override - protected void createAndAddWindows() { - } - - @Override protected WindowManager.LayoutParams getSearchLayoutParams( LayoutParams layoutParams) { return null; @@ -141,10 +138,19 @@ public class TvStatusBar extends BaseStatusBar { } @Override + protected int getMaxKeyguardNotifications() { + return 0; + } + + @Override public void animateExpandSettingsPanel() { } @Override + protected void createAndAddWindows() { + } + + @Override protected void refreshLayout(int layoutDirection) { } diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java index 2c36ab7..481266b 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java +++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java @@ -311,6 +311,9 @@ public class StorageNotification extends SystemUI { } mUsbStorageNotification.setLatestEventInfo(mContext, title, message, pi); + mUsbStorageNotification.visibility = Notification.VISIBILITY_PUBLIC; + mUsbStorageNotification.category = Notification.CATEGORY_SYSTEM; + final boolean adbOn = 1 == Settings.Global.getInt( mContext.getContentResolver(), Settings.Global.ADB_ENABLED, @@ -401,6 +404,8 @@ public class StorageNotification extends SystemUI { mMediaStorageNotification.icon = icon; mMediaStorageNotification.setLatestEventInfo(mContext, title, message, pi); + mMediaStorageNotification.visibility = Notification.VISIBILITY_PUBLIC; + mMediaStorageNotification.category = Notification.CATEGORY_SYSTEM; } final int notificationId = mMediaStorageNotification.icon; |