summaryrefslogtreecommitdiffstats
path: root/packages/SystemUI/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/SystemUI/src')
-rwxr-xr-xpackages/SystemUI/src/com/android/systemui/BatteryMeterView.java148
-rw-r--r--packages/SystemUI/src/com/android/systemui/DemoMode.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/DessertCase.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/ExpandHelper.java153
-rw-r--r--packages/SystemUI/src/com/android/systemui/ImageWallpaper.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/LoadAverageService.java35
-rw-r--r--packages/SystemUI/src/com/android/systemui/SearchPanelView.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUI.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUIApplication.java149
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUIService.java55
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java207
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java1332
-rw-r--r--packages/SystemUI/src/com/android/systemui/power/PowerDialogWarnings.java218
-rw-r--r--packages/SystemUI/src/com/android/systemui/power/PowerUI.java194
-rw-r--r--packages/SystemUI/src/com/android/systemui/recent/RecentTasksLoader.java35
-rw-r--r--packages/SystemUI/src/com/android/systemui/recent/Recents.java102
-rw-r--r--packages/SystemUI/src/com/android/systemui/recent/RecentsActivity.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java34
-rw-r--r--packages/SystemUI/src/com/android/systemui/recent/RecentsVerticalScrollView.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/recent/TaskDescription.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java420
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/BakedBezierInterpolator.java64
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/Console.java193
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/Constants.java108
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java268
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java137
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/RecentsService.java167
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java593
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/SystemServicesProxy.java234
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/Utilities.java49
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java78
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/model/Task.java129
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java252
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java397
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java398
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/views/TaskBarView.java81
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/views/TaskInfoView.java161
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java1452
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/views/TaskThumbnailView.java62
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java386
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java49
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java78
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java33
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/BrightnessDialog.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/ToggleSeekBar.java45
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/ToggleSlider.java113
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/UserSwitcherHostView.java178
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java208
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java491
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java213
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java60
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java140
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/InterceptedNotifications.java119
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/LatestItemView.java48
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java87
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java128
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java24
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java76
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowIconsView.java69
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java20
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java232
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java163
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java62
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java35
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java99
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java172
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java486
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java1345
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java79
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java278
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsContainerView.java146
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java262
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsScrollView.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/SettingsPanelView.java161
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java226
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java209
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java52
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java424
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeViewAdapter.java230
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java118
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java285
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/OnSizeChangedListener.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/ScrollAdapter.java40
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java1113
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java112
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java74
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java612
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java230
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java159
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java5
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;