diff options
Diffstat (limited to 'packages/SystemUI/src')
228 files changed, 21273 insertions, 15085 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java index 7bdbd0a..95b58e5 100755 --- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java @@ -16,19 +16,24 @@ package com.android.systemui; +import android.animation.ArgbEvaluator; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.content.res.TypedArray; +import android.database.ContentObserver; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.graphics.Typeface; +import android.net.Uri; import android.os.BatteryManager; import android.os.Bundle; +import android.os.Handler; import android.provider.Settings; import android.util.AttributeSet; import android.view.View; @@ -39,10 +44,9 @@ public class BatteryMeterView extends View implements DemoMode, BatteryController.BatteryStateChangeCallback { public static final String TAG = BatteryMeterView.class.getSimpleName(); public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST"; + public static final String SHOW_PERCENT_SETTING = "status_bar_show_battery_percent"; - private static final boolean ENABLE_PERCENT = true; private static final boolean SINGLE_DIGIT_PERCENT = false; - private static final boolean SHOW_100_PERCENT = false; private static final int FULL = 96; @@ -50,18 +54,19 @@ public class BatteryMeterView extends View implements DemoMode, private final int[] mColors; - boolean mShowPercent = true; + private boolean mShowPercent; private float mButtonHeightFraction; private float mSubpixelSmoothingLeft; private float mSubpixelSmoothingRight; private final Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint; private float mTextHeight, mWarningTextHeight; + private int mIconTint = Color.WHITE; private int mHeight; private int mWidth; private String mWarningString; private final int mCriticalLevel; - private final int mChargeColor; + private int mChargeColor; private final float[] mBoltPoints; private final Path mBoltPath = new Path(); @@ -76,103 +81,14 @@ public class BatteryMeterView extends View implements DemoMode, private BatteryController mBatteryController; private boolean mPowerSaveEnabled; - private class BatteryTracker extends BroadcastReceiver { - public static final int UNKNOWN_LEVEL = -1; + private int mDarkModeBackgroundColor; + private int mDarkModeFillColor; - // current battery status - int level = UNKNOWN_LEVEL; - String percentStr; - int plugType; - boolean plugged; - int health; - int status; - String technology; - int voltage; - int temperature; - boolean testmode = false; + private int mLightModeBackgroundColor; + private int mLightModeFillColor; - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { - if (testmode && ! intent.getBooleanExtra("testmode", false)) return; - - level = (int)(100f - * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) - / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); - - plugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); - plugged = plugType != 0; - health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, - BatteryManager.BATTERY_HEALTH_UNKNOWN); - status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, - BatteryManager.BATTERY_STATUS_UNKNOWN); - technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY); - voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0); - temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); - - setContentDescription( - context.getString(R.string.accessibility_battery_level, level)); - postInvalidate(); - } else if (action.equals(ACTION_LEVEL_TEST)) { - testmode = true; - post(new Runnable() { - int curLevel = 0; - int incr = 1; - int saveLevel = level; - int savePlugged = plugType; - Intent dummy = new Intent(Intent.ACTION_BATTERY_CHANGED); - @Override - public void run() { - if (curLevel < 0) { - testmode = false; - dummy.putExtra("level", saveLevel); - dummy.putExtra("plugged", savePlugged); - dummy.putExtra("testmode", false); - } else { - dummy.putExtra("level", curLevel); - dummy.putExtra("plugged", incr > 0 ? BatteryManager.BATTERY_PLUGGED_AC : 0); - dummy.putExtra("testmode", true); - } - getContext().sendBroadcast(dummy); - - if (!testmode) return; - - curLevel += incr; - if (curLevel == 100) { - incr *= -1; - } - postDelayed(this, 200); - } - }); - } - } - } - - BatteryTracker mTracker = new BatteryTracker(); - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_BATTERY_CHANGED); - filter.addAction(ACTION_LEVEL_TEST); - final Intent sticky = getContext().registerReceiver(mTracker, filter); - if (sticky != null) { - // preload the battery level - mTracker.onReceive(getContext(), sticky); - } - mBatteryController.addStateChangedCallback(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - getContext().unregisterReceiver(mTracker); - mBatteryController.removeStateChangedCallback(this); - } + private BatteryTracker mTracker = new BatteryTracker(); + private final SettingObserver mSettingObserver = new SettingObserver(); public BatteryMeterView(Context context) { this(context, null, 0); @@ -189,7 +105,7 @@ public class BatteryMeterView extends View implements DemoMode, 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)); + context.getColor(R.color.batterymeter_frame_color)); TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels); TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values); @@ -202,8 +118,7 @@ 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); + updateShowPercent(); mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol); mCriticalLevel = mContext.getResources().getInteger( com.android.internal.R.integer.config_criticalBatteryWarningLevel); @@ -236,11 +151,44 @@ public class BatteryMeterView extends View implements DemoMode, mWarningTextPaint.setTypeface(font); mWarningTextPaint.setTextAlign(Paint.Align.CENTER); - mChargeColor = getResources().getColor(R.color.batterymeter_charge_color); + mChargeColor = context.getColor(R.color.batterymeter_charge_color); mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mBoltPaint.setColor(res.getColor(R.color.batterymeter_bolt_color)); + mBoltPaint.setColor(context.getColor(R.color.batterymeter_bolt_color)); mBoltPoints = loadBoltPoints(res); + + mDarkModeBackgroundColor = + context.getColor(R.color.dark_mode_icon_color_dual_tone_background); + mDarkModeFillColor = context.getColor(R.color.dark_mode_icon_color_dual_tone_fill); + mLightModeBackgroundColor = + context.getColor(R.color.light_mode_icon_color_dual_tone_background); + mLightModeFillColor = context.getColor(R.color.light_mode_icon_color_dual_tone_fill); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + filter.addAction(ACTION_LEVEL_TEST); + final Intent sticky = getContext().registerReceiver(mTracker, filter); + if (sticky != null) { + // preload the battery level + mTracker.onReceive(getContext(), sticky); + } + mBatteryController.addStateChangedCallback(this); + getContext().getContentResolver().registerContentObserver( + Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + getContext().unregisterReceiver(mTracker); + mBatteryController.removeStateChangedCallback(this); + getContext().getContentResolver().unregisterContentObserver(mSettingObserver); } public void setBatteryController(BatteryController batteryController) { @@ -282,6 +230,11 @@ public class BatteryMeterView extends View implements DemoMode, mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent; } + private void updateShowPercent() { + mShowPercent = 0 != Settings.System.getInt(getContext().getContentResolver(), + SHOW_PERCENT_SETTING, 0); + } + private int getColorForLevel(int percent) { // If we are in power save mode, always use the normal color. @@ -292,11 +245,43 @@ public class BatteryMeterView extends View implements DemoMode, for (int i=0; i<mColors.length; i+=2) { thresh = mColors[i]; color = mColors[i+1]; - if (percent <= thresh) return color; + if (percent <= thresh) { + + // Respect tinting for "normal" level + if (i == mColors.length-2) { + return mIconTint; + } else { + return color; + } + } } return color; } + public void setDarkIntensity(float darkIntensity) { + int backgroundColor = getBackgroundColor(darkIntensity); + int fillColor = getFillColor(darkIntensity); + mIconTint = fillColor; + mFramePaint.setColor(backgroundColor); + mBoltPaint.setColor(fillColor); + mChargeColor = fillColor; + invalidate(); + } + + private int getBackgroundColor(float darkIntensity) { + return getColorForDarkIntensity( + darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor); + } + + private int getFillColor(float darkIntensity) { + return getColorForDarkIntensity( + darkIntensity, mLightModeFillColor, mDarkModeFillColor); + } + + private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) { + return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor); + } + @Override public void draw(Canvas c) { BatteryTracker tracker = mDemoMode ? mDemoTracker : mTracker; @@ -397,8 +382,7 @@ public class BatteryMeterView extends View implements DemoMode, boolean pctOpaque = false; float pctX = 0, pctY = 0; String pctText = null; - if (!tracker.plugged && level > mCriticalLevel && mShowPercent - && !(tracker.level == 100 && !SHOW_100_PERCENT)) { + if (!tracker.plugged && level > mCriticalLevel && mShowPercent) { mTextPaint.setColor(getColorForLevel(level)); mTextPaint.setTextSize(height * (SINGLE_DIGIT_PERCENT ? 0.75f @@ -468,4 +452,92 @@ public class BatteryMeterView extends View implements DemoMode, postInvalidate(); } } + + private final class BatteryTracker extends BroadcastReceiver { + public static final int UNKNOWN_LEVEL = -1; + + // current battery status + int level = UNKNOWN_LEVEL; + String percentStr; + int plugType; + boolean plugged; + int health; + int status; + String technology; + int voltage; + int temperature; + boolean testmode = false; + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { + if (testmode && ! intent.getBooleanExtra("testmode", false)) return; + + level = (int)(100f + * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) + / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); + + plugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + plugged = plugType != 0; + health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, + BatteryManager.BATTERY_HEALTH_UNKNOWN); + status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN); + technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY); + voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0); + temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); + + setContentDescription( + context.getString(R.string.accessibility_battery_level, level)); + postInvalidate(); + } else if (action.equals(ACTION_LEVEL_TEST)) { + testmode = true; + post(new Runnable() { + int curLevel = 0; + int incr = 1; + int saveLevel = level; + int savePlugged = plugType; + Intent dummy = new Intent(Intent.ACTION_BATTERY_CHANGED); + @Override + public void run() { + if (curLevel < 0) { + testmode = false; + dummy.putExtra("level", saveLevel); + dummy.putExtra("plugged", savePlugged); + dummy.putExtra("testmode", false); + } else { + dummy.putExtra("level", curLevel); + dummy.putExtra("plugged", incr > 0 ? BatteryManager.BATTERY_PLUGGED_AC + : 0); + dummy.putExtra("testmode", true); + } + getContext().sendBroadcast(dummy); + + if (!testmode) return; + + curLevel += incr; + if (curLevel == 100) { + incr *= -1; + } + postDelayed(this, 200); + } + }); + } + } + } + + private final class SettingObserver extends ContentObserver { + public SettingObserver() { + super(new Handler()); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + updateShowPercent(); + postInvalidate(); + } + } + } diff --git a/packages/SystemUI/src/com/android/systemui/DemoMode.java b/packages/SystemUI/src/com/android/systemui/DemoMode.java index 9c206e2..d406f5b 100644 --- a/packages/SystemUI/src/com/android/systemui/DemoMode.java +++ b/packages/SystemUI/src/com/android/systemui/DemoMode.java @@ -24,6 +24,8 @@ public interface DemoMode { public static final String ACTION_DEMO = "com.android.systemui.demo"; + public static final String EXTRA_COMMAND = "command"; + public static final String COMMAND_ENTER = "enter"; public static final String COMMAND_EXIT = "exit"; public static final String COMMAND_CLOCK = "clock"; diff --git a/packages/SystemUI/src/com/android/systemui/EventLogConstants.java b/packages/SystemUI/src/com/android/systemui/EventLogConstants.java index c8af2d4..43a1be1 100644 --- a/packages/SystemUI/src/com/android/systemui/EventLogConstants.java +++ b/packages/SystemUI/src/com/android/systemui/EventLogConstants.java @@ -34,4 +34,10 @@ public class EventLogConstants { public static final int SYSUI_LOCKSCREEN_GESTURE_TAP_LOCK = 6; /** The user tapped a notification, needs to tap again to launch. */ public static final int SYSUI_LOCKSCREEN_GESTURE_TAP_NOTIFICATION_ACTIVATE = 7; + /** The user swiped down to open quick settings, from keyguard. */ + public static final int SYSUI_LOCKSCREEN_GESTURE_SWIPE_DOWN_QS = 8; + /** The user swiped down to open quick settings, from shade. */ + public static final int SYSUI_SHADE_GESTURE_SWIPE_DOWN_QS = 9; + /** The user tapped on the status bar to open quick settings, from shade. */ + public static final int SYSUI_TAP_TO_OPEN_QS = 10; } diff --git a/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags index d2ce94b..a584cf6 100644 --- a/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags +++ b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags @@ -5,7 +5,7 @@ option java_package com.android.systemui; # --------------------------- # PhoneStatusBar.java # --------------------------- -36000 sysui_statusbar_touch (type|1),(x|1),(y|1),(enabled|1) +36000 sysui_statusbar_touch (type|1),(x|1),(y|1),(disable1|1),(disable2|1) 36001 sysui_heads_up_status (key|3),(visible|1) 36002 sysui_fullscreen_notification (key|3) 36003 sysui_heads_up_escalation (key|3) diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java index d42ac61..b0e2afa 100644 --- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java +++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java @@ -22,10 +22,10 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.media.AudioAttributes; -import android.media.AudioManager; import android.os.Vibrator; import android.util.Log; import android.view.Gravity; +import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector.OnScaleGestureListener; @@ -65,15 +65,6 @@ public class ExpandHelper implements Gefingerpoken { // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U private static final float STRETCH_INTERVAL = 2f; - // level of glow for a touch, without overstretch - // overstretch fills the range (GLOW_BASE, 1.0] - private static final float GLOW_BASE = 0.5f; - - private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .build(); - @SuppressWarnings("unused") private Context mContext; @@ -95,13 +86,11 @@ public class ExpandHelper implements Gefingerpoken { private float mLastSpanY; private int mTouchSlop; private float mLastMotionY; - private int mPopDuration; private float mPullGestureMinXSpan; private Callback mCallback; private ScaleGestureDetector mSGD; private ViewScaler mScaler; private ObjectAnimator mScaleAnimation; - private Vibrator mVibrator; private boolean mEnabled = true; private ExpandableView mResizedView; private float mCurrentHeight; @@ -147,14 +136,14 @@ public class ExpandHelper implements Gefingerpoken { } public void setHeight(float h) { if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h); - mView.setActualHeight((int) h); + mView.setContentHeight((int) h); mCurrentHeight = h; } public float getHeight() { - return mView.getActualHeight(); + return mView.getContentHeight(); } public int getNaturalHeight(int maximum) { - return Math.min(maximum, mView.getMaxHeight()); + return Math.min(maximum, mView.getMaxContentHeight()); } } @@ -175,7 +164,6 @@ public class ExpandHelper implements Gefingerpoken { mScaler = new ViewScaler(); mGravity = Gravity.TOP; mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f); - mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms); mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min); final ViewConfiguration configuration = ViewConfiguration.get(mContext); @@ -323,6 +311,10 @@ public class ExpandHelper implements Gefingerpoken { isInside(mScrollAdapter.getHostView(), x, y) && mScrollAdapter.isScrolledToTop(); mResizedView = findView(x, y); + if (mResizedView != null && !mCallback.canChildBeExpanded(mResizedView)) { + mResizedView = null; + mWatchingForPull = false; + } mInitialTouchY = ev.getY(); break; @@ -387,7 +379,8 @@ public class ExpandHelper implements Gefingerpoken { } private boolean isFullyExpanded(ExpandableView underFocus) { - return underFocus.getIntrinsicHeight() == underFocus.getMaxHeight(); + return underFocus.areChildrenExpanded() || underFocus.getIntrinsicHeight() + - underFocus.getBottomDecorHeight() == underFocus.getMaxContentHeight(); } @Override @@ -448,7 +441,9 @@ public class ExpandHelper implements Gefingerpoken { } if (!mHasPopped) { - vibrate(mPopDuration); + if (mEventSource != null) { + mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + } mHasPopped = true; } @@ -596,16 +591,5 @@ public class ExpandHelper implements Gefingerpoken { public void onlyObserveMovements(boolean onlyMovements) { mOnlyMovements = onlyMovements; } - - /** - * Triggers haptic feedback. - */ - private synchronized void vibrate(long duration) { - if (mVibrator == null) { - mVibrator = (android.os.Vibrator) - mContext.getSystemService(Context.VIBRATOR_SERVICE); - } - mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES); - } } diff --git a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java index 7c725b3..6acd137 100644 --- a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java @@ -21,13 +21,9 @@ import static javax.microedition.khronos.egl.EGL10.*; import android.app.ActivityManager; import android.app.WallpaperManager; -import android.content.BroadcastReceiver; import android.content.ComponentCallbacks2; -import android.content.Context; -import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region.Op; @@ -37,6 +33,7 @@ import android.renderscript.Matrix4f; import android.service.wallpaper.WallpaperService; import android.util.Log; import android.view.Display; +import android.view.DisplayInfo; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.WindowManager; @@ -113,6 +110,9 @@ public class ImageWallpaper extends WallpaperService { float mYOffset = 0.5f; float mScale = 1f; + private Display mDefaultDisplay; + private final DisplayInfo mTmpDisplayInfo = new DisplayInfo(); + boolean mVisible = true; boolean mRedrawNeeded; boolean mOffsetsChanged; @@ -174,7 +174,9 @@ public class ImageWallpaper extends WallpaperService { super.onCreate(surfaceHolder); - updateSurfaceSize(surfaceHolder); + mDefaultDisplay = getSystemService(WindowManager.class).getDefaultDisplay(); + + updateSurfaceSize(surfaceHolder, getDefaultDisplayInfo()); setOffsetNotificationsEnabled(false); } @@ -186,9 +188,7 @@ public class ImageWallpaper extends WallpaperService { mWallpaperManager.forgetLoadedWallpaper(); } - void updateSurfaceSize(SurfaceHolder surfaceHolder) { - Point p = getDefaultDisplaySize(); - + void updateSurfaceSize(SurfaceHolder surfaceHolder, DisplayInfo displayInfo) { // Load background image dimensions, if we haven't saved them yet if (mBackgroundWidth <= 0 || mBackgroundHeight <= 0) { // Need to load the image to get dimensions @@ -196,14 +196,14 @@ public class ImageWallpaper extends WallpaperService { updateWallpaperLocked(); if (mBackgroundWidth <= 0 || mBackgroundHeight <= 0) { // Default to the display size if we can't find the dimensions - mBackgroundWidth = p.x; - mBackgroundHeight = p.y; + mBackgroundWidth = displayInfo.logicalWidth; + mBackgroundHeight = displayInfo.logicalHeight; } } // Force the wallpaper to cover the screen in both dimensions - int surfaceWidth = Math.max(p.x, mBackgroundWidth); - int surfaceHeight = Math.max(p.y, mBackgroundHeight); + int surfaceWidth = Math.max(displayInfo.logicalWidth, mBackgroundWidth); + int surfaceHeight = Math.max(displayInfo.logicalHeight, mBackgroundHeight); // If the surface dimensions haven't changed, then just return final Rect frame = surfaceHolder.getSurfaceFrame(); @@ -299,26 +299,22 @@ public class ImageWallpaper extends WallpaperService { drawFrame(); } - private Point getDefaultDisplaySize() { - Point p = new Point(); - Context c = ImageWallpaper.this.getApplicationContext(); - WindowManager wm = (WindowManager)c.getSystemService(Context.WINDOW_SERVICE); - Display d = wm.getDefaultDisplay(); - d.getRealSize(p); - return p; + private DisplayInfo getDefaultDisplayInfo() { + mDefaultDisplay.getDisplayInfo(mTmpDisplayInfo); + return mTmpDisplayInfo; } void drawFrame() { try { - int newRotation = ((WindowManager) getSystemService(WINDOW_SERVICE)). - getDefaultDisplay().getRotation(); + DisplayInfo displayInfo = getDefaultDisplayInfo(); + int newRotation = displayInfo.rotation; // Sometimes a wallpaper is not large enough to cover the screen in one dimension. // Call updateSurfaceSize -- it will only actually do the update if the dimensions // should change if (newRotation != mLastRotation) { // Update surface size (if necessary) - updateSurfaceSize(getSurfaceHolder()); + updateSurfaceSize(getSurfaceHolder(), displayInfo); } SurfaceHolder sh = getSurfaceHolder(); final Rect frame = sh.getSurfaceFrame(); diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java new file mode 100644 index 0000000..3657cf2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui; + +import android.annotation.StringDef; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; + +public final class Prefs { + private Prefs() {} // no instantation + + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + Key.SEARCH_APP_WIDGET_ID, + Key.DEBUG_MODE_ENABLED, + Key.HOTSPOT_TILE_LAST_USED, + Key.COLOR_INVERSION_TILE_LAST_USED, + Key.DND_TILE_VISIBLE, + Key.DND_TILE_COMBINED_ICON, + Key.DND_CONFIRMED_PRIORITY_INTRODUCTION, + Key.DND_CONFIRMED_SILENCE_INTRODUCTION, + Key.DND_FAVORITE_BUCKET_INDEX, + Key.DND_NONE_SELECTED, + Key.DND_FAVORITE_ZEN, + }) + public @interface Key { + String SEARCH_APP_WIDGET_ID = "searchAppWidgetId"; + String SEARCH_APP_WIDGET_PACKAGE = "searchAppWidgetPackage"; + String DEBUG_MODE_ENABLED = "debugModeEnabled"; + String HOTSPOT_TILE_LAST_USED = "HotspotTileLastUsed"; + String COLOR_INVERSION_TILE_LAST_USED = "ColorInversionTileLastUsed"; + String DND_TILE_VISIBLE = "DndTileVisible"; + String DND_TILE_COMBINED_ICON = "DndTileCombinedIcon"; + String DND_CONFIRMED_PRIORITY_INTRODUCTION = "DndConfirmedPriorityIntroduction"; + String DND_CONFIRMED_SILENCE_INTRODUCTION = "DndConfirmedSilenceIntroduction"; + String DND_FAVORITE_BUCKET_INDEX = "DndCountdownMinuteIndex"; + String DND_NONE_SELECTED = "DndNoneSelected"; + String DND_FAVORITE_ZEN = "DndFavoriteZen"; + } + + public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) { + return get(context).getBoolean(key, defaultValue); + } + + public static void putBoolean(Context context, @Key String key, boolean value) { + get(context).edit().putBoolean(key, value).apply(); + } + + public static int getInt(Context context, @Key String key, int defaultValue) { + return get(context).getInt(key, defaultValue); + } + + public static void putInt(Context context, @Key String key, int value) { + get(context).edit().putInt(key, value).apply(); + } + + public static long getLong(Context context, @Key String key, long defaultValue) { + return get(context).getLong(key, defaultValue); + } + + public static void putLong(Context context, @Key String key, long value) { + get(context).edit().putLong(key, value).apply(); + } + + public static String getString(Context context, @Key String key, String defaultValue) { + return get(context).getString(key, defaultValue); + } + + public static void putString(Context context, @Key String key, String value) { + get(context).edit().putString(key, value).apply(); + } + + public static Map<String, ?> getAll(Context context) { + return get(context).getAll(); + } + + public static void remove(Context context, @Key String key) { + get(context).edit().remove(key).apply(); + } + + public static void registerListener(Context context, + OnSharedPreferenceChangeListener listener) { + get(context).registerOnSharedPreferenceChangeListener(listener); + } + + public static void unregisterListener(Context context, + OnSharedPreferenceChangeListener listener) { + get(context).unregisterOnSharedPreferenceChangeListener(listener); + } + + private static SharedPreferences get(Context context) { + return context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java b/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java deleted file mode 100644 index d8fb6da..0000000 --- a/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java +++ /dev/null @@ -1,592 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Outline; -import android.graphics.Paint; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewOutlineProvider; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.view.animation.LinearInterpolator; -import android.widget.FrameLayout; -import android.widget.ImageView; -import com.android.systemui.statusbar.phone.PhoneStatusBar; - -import java.util.ArrayList; - -public class SearchPanelCircleView extends FrameLayout { - - private final int mCircleMinSize; - private final int mBaseMargin; - private final int mStaticOffset; - private final Paint mBackgroundPaint = new Paint(); - private final Paint mRipplePaint = new Paint(); - private final Rect mCircleRect = new Rect(); - private final Rect mStaticRect = new Rect(); - private final Interpolator mFastOutSlowInInterpolator; - private final Interpolator mAppearInterpolator; - private final Interpolator mDisappearInterpolator; - - private boolean mClipToOutline; - private final int mMaxElevation; - private boolean mAnimatingOut; - private float mOutlineAlpha; - private float mOffset; - private float mCircleSize; - private boolean mHorizontal; - private boolean mCircleHidden; - private ImageView mLogo; - private boolean mDraggedFarEnough; - private boolean mOffsetAnimatingIn; - private float mCircleAnimationEndValue; - private ArrayList<Ripple> mRipples = new ArrayList<Ripple>(); - - private ValueAnimator mOffsetAnimator; - private ValueAnimator mCircleAnimator; - private ValueAnimator mFadeOutAnimator; - private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener - = new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - applyCircleSize((float) animation.getAnimatedValue()); - updateElevation(); - } - }; - private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mCircleAnimator = null; - } - }; - private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener - = new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - setOffset((float) animation.getAnimatedValue()); - } - }; - - - public SearchPanelCircleView(Context context) { - this(context, null); - } - - public SearchPanelCircleView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - if (mCircleSize > 0.0f) { - outline.setOval(mCircleRect); - } else { - outline.setEmpty(); - } - outline.setAlpha(mOutlineAlpha); - } - }); - setWillNotDraw(false); - mCircleMinSize = context.getResources().getDimensionPixelSize( - R.dimen.search_panel_circle_size); - mBaseMargin = context.getResources().getDimensionPixelSize( - R.dimen.search_panel_circle_base_margin); - mStaticOffset = context.getResources().getDimensionPixelSize( - R.dimen.search_panel_circle_travel_distance); - mMaxElevation = context.getResources().getDimensionPixelSize( - R.dimen.search_panel_circle_elevation); - mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.linear_out_slow_in); - mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.fast_out_slow_in); - mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.fast_out_linear_in); - mBackgroundPaint.setAntiAlias(true); - mBackgroundPaint.setColor(getResources().getColor(R.color.search_panel_circle_color)); - mRipplePaint.setColor(getResources().getColor(R.color.search_panel_ripple_color)); - mRipplePaint.setAntiAlias(true); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - drawBackground(canvas); - drawRipples(canvas); - } - - private void drawRipples(Canvas canvas) { - for (int i = 0; i < mRipples.size(); i++) { - Ripple ripple = mRipples.get(i); - ripple.draw(canvas); - } - } - - private void drawBackground(Canvas canvas) { - canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2, - mBackgroundPaint); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mLogo = (ImageView) findViewById(R.id.search_logo); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight()); - if (changed) { - updateCircleRect(mStaticRect, mStaticOffset, true); - } - } - - public void setCircleSize(float circleSize) { - setCircleSize(circleSize, false, null, 0, null); - } - - public void setCircleSize(float circleSize, boolean animated, final Runnable endRunnable, - int startDelay, Interpolator interpolator) { - boolean isAnimating = mCircleAnimator != null; - boolean animationPending = isAnimating && !mCircleAnimator.isRunning(); - boolean animatingOut = isAnimating && mCircleAnimationEndValue == 0; - if (animated || animationPending || animatingOut) { - if (isAnimating) { - if (circleSize == mCircleAnimationEndValue) { - return; - } - mCircleAnimator.cancel(); - } - mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize); - mCircleAnimator.addUpdateListener(mCircleUpdateListener); - mCircleAnimator.addListener(mClearAnimatorListener); - mCircleAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (endRunnable != null) { - endRunnable.run(); - } - } - }); - Interpolator desiredInterpolator = interpolator != null ? interpolator - : circleSize == 0 ? mDisappearInterpolator : mAppearInterpolator; - mCircleAnimator.setInterpolator(desiredInterpolator); - mCircleAnimator.setDuration(300); - mCircleAnimator.setStartDelay(startDelay); - mCircleAnimator.start(); - mCircleAnimationEndValue = circleSize; - } else { - if (isAnimating) { - float diff = circleSize - mCircleAnimationEndValue; - PropertyValuesHolder[] values = mCircleAnimator.getValues(); - values[0].setFloatValues(diff, circleSize); - mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime()); - mCircleAnimationEndValue = circleSize; - } else { - applyCircleSize(circleSize); - updateElevation(); - } - } - } - - private void applyCircleSize(float circleSize) { - mCircleSize = circleSize; - updateLayout(); - } - - private void updateElevation() { - float t = (mStaticOffset - mOffset) / (float) mStaticOffset; - t = 1.0f - Math.max(t, 0.0f); - float offset = t * mMaxElevation; - setElevation(offset); - } - - /** - * Sets the offset to the edge of the screen. By default this not not animated. - * - * @param offset The offset to apply. - */ - public void setOffset(float offset) { - setOffset(offset, false, 0, null, null); - } - - /** - * Sets the offset to the edge of the screen. - * - * @param offset The offset to apply. - * @param animate Whether an animation should be performed. - * @param startDelay The desired start delay if animated. - * @param interpolator The desired interpolator if animated. If null, - * a default interpolator will be taken designed for appearing or - * disappearing. - * @param endRunnable The end runnable which should be executed when the animation is finished. - */ - private void setOffset(float offset, boolean animate, int startDelay, - Interpolator interpolator, final Runnable endRunnable) { - if (!animate) { - mOffset = offset; - updateLayout(); - if (endRunnable != null) { - endRunnable.run(); - } - } else { - if (mOffsetAnimator != null) { - mOffsetAnimator.removeAllListeners(); - mOffsetAnimator.cancel(); - } - mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset); - mOffsetAnimator.addUpdateListener(mOffsetUpdateListener); - mOffsetAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mOffsetAnimator = null; - if (endRunnable != null) { - endRunnable.run(); - } - } - }); - Interpolator desiredInterpolator = interpolator != null ? - interpolator : offset == 0 ? mDisappearInterpolator : mAppearInterpolator; - mOffsetAnimator.setInterpolator(desiredInterpolator); - mOffsetAnimator.setStartDelay(startDelay); - mOffsetAnimator.setDuration(300); - mOffsetAnimator.start(); - mOffsetAnimatingIn = offset != 0; - } - } - - private void updateLayout() { - updateCircleRect(); - updateLogo(); - invalidateOutline(); - invalidate(); - updateClipping(); - } - - private void updateClipping() { - boolean clip = mCircleSize < mCircleMinSize || !mRipples.isEmpty(); - if (clip != mClipToOutline) { - setClipToOutline(clip); - mClipToOutline = clip; - } - } - - private void updateLogo() { - boolean exitAnimationRunning = mFadeOutAnimator != null; - Rect rect = exitAnimationRunning ? mCircleRect : mStaticRect; - float translationX = (rect.left + rect.right) / 2.0f - mLogo.getWidth() / 2.0f; - float translationY = (rect.top + rect.bottom) / 2.0f - mLogo.getHeight() / 2.0f; - float t = (mStaticOffset - mOffset) / (float) mStaticOffset; - if (!exitAnimationRunning) { - if (mHorizontal) { - translationX += t * mStaticOffset * 0.3f; - } else { - translationY += t * mStaticOffset * 0.3f; - } - float alpha = 1.0f-t; - alpha = Math.max((alpha - 0.5f) * 2.0f, 0); - mLogo.setAlpha(alpha); - } else { - translationY += (mOffset - mStaticOffset) / 2; - } - mLogo.setTranslationX(translationX); - mLogo.setTranslationY(translationY); - } - - private void updateCircleRect() { - updateCircleRect(mCircleRect, mOffset, false); - } - - private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) { - int left, top; - float circleSize = useStaticSize ? mCircleMinSize : mCircleSize; - if (mHorizontal) { - left = (int) (getWidth() - circleSize / 2 - mBaseMargin - offset); - top = (int) ((getHeight() - circleSize) / 2); - } else { - left = (int) (getWidth() - circleSize) / 2; - top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset); - } - rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize)); - } - - public void setHorizontal(boolean horizontal) { - mHorizontal = horizontal; - updateCircleRect(mStaticRect, mStaticOffset, true); - updateLayout(); - } - - public void setDragDistance(float distance) { - if (!mAnimatingOut && (!mCircleHidden || mDraggedFarEnough)) { - float circleSize = mCircleMinSize + rubberband(distance); - setCircleSize(circleSize); - } - - } - - private float rubberband(float diff) { - return (float) Math.pow(Math.abs(diff), 0.6f); - } - - public void startAbortAnimation(Runnable endRunnable) { - if (mAnimatingOut) { - if (endRunnable != null) { - endRunnable.run(); - } - return; - } - setCircleSize(0, true, null, 0, null); - setOffset(0, true, 0, null, endRunnable); - mCircleHidden = true; - } - - public void startEnterAnimation() { - if (mAnimatingOut) { - return; - } - applyCircleSize(0); - setOffset(0); - setCircleSize(mCircleMinSize, true, null, 50, null); - setOffset(mStaticOffset, true, 50, null, null); - mCircleHidden = false; - } - - - public void startExitAnimation(final Runnable endRunnable) { - if (!mHorizontal) { - float offset = getHeight() / 2.0f; - setOffset(offset - mBaseMargin, true, 50, mFastOutSlowInInterpolator, null); - float xMax = getWidth() / 2; - float yMax = getHeight() / 2; - float maxRadius = (float) Math.ceil(Math.hypot(xMax, yMax) * 2); - setCircleSize(maxRadius, true, null, 50, mFastOutSlowInInterpolator); - performExitFadeOutAnimation(50, 300, endRunnable); - } else { - - // when in landscape, we don't wan't the animation as it interferes with the general - // rotation animation to the homescreen. - endRunnable.run(); - } - } - - private void performExitFadeOutAnimation(int startDelay, int duration, - final Runnable endRunnable) { - mFadeOutAnimator = ValueAnimator.ofFloat(mBackgroundPaint.getAlpha() / 255.0f, 0.0f); - - // Linear since we are animating multiple values - mFadeOutAnimator.setInterpolator(new LinearInterpolator()); - mFadeOutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - float animatedFraction = animation.getAnimatedFraction(); - float logoValue = animatedFraction > 0.5f ? 1.0f : animatedFraction / 0.5f; - logoValue = PhoneStatusBar.ALPHA_OUT.getInterpolation(1.0f - logoValue); - float backgroundValue = animatedFraction < 0.2f ? 0.0f : - PhoneStatusBar.ALPHA_OUT.getInterpolation((animatedFraction - 0.2f) / 0.8f); - backgroundValue = 1.0f - backgroundValue; - mBackgroundPaint.setAlpha((int) (backgroundValue * 255)); - mOutlineAlpha = backgroundValue; - mLogo.setAlpha(logoValue); - invalidateOutline(); - invalidate(); - } - }); - mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (endRunnable != null) { - endRunnable.run(); - } - mLogo.setAlpha(1.0f); - mBackgroundPaint.setAlpha(255); - mOutlineAlpha = 1.0f; - mFadeOutAnimator = null; - } - }); - mFadeOutAnimator.setStartDelay(startDelay); - mFadeOutAnimator.setDuration(duration); - mFadeOutAnimator.start(); - } - - public void setDraggedFarEnough(boolean farEnough) { - if (farEnough != mDraggedFarEnough) { - if (farEnough) { - if (mCircleHidden) { - startEnterAnimation(); - } - if (mOffsetAnimator == null) { - addRipple(); - } else { - postDelayed(new Runnable() { - @Override - public void run() { - addRipple(); - } - }, 100); - } - } else { - startAbortAnimation(null); - } - mDraggedFarEnough = farEnough; - } - - } - - private void addRipple() { - if (mRipples.size() > 1) { - // we only want 2 ripples at the time - return; - } - float xInterpolation, yInterpolation; - if (mHorizontal) { - xInterpolation = 0.75f; - yInterpolation = 0.5f; - } else { - xInterpolation = 0.5f; - yInterpolation = 0.75f; - } - float circleCenterX = mStaticRect.left * (1.0f - xInterpolation) - + mStaticRect.right * xInterpolation; - float circleCenterY = mStaticRect.top * (1.0f - yInterpolation) - + mStaticRect.bottom * yInterpolation; - float radius = Math.max(mCircleSize, mCircleMinSize * 1.25f) * 0.75f; - Ripple ripple = new Ripple(circleCenterX, circleCenterY, radius); - ripple.start(); - } - - public void reset() { - mDraggedFarEnough = false; - mAnimatingOut = false; - mCircleHidden = true; - mClipToOutline = false; - if (mFadeOutAnimator != null) { - mFadeOutAnimator.cancel(); - } - mBackgroundPaint.setAlpha(255); - mOutlineAlpha = 1.0f; - } - - /** - * Check if an animation is currently running - * - * @param enterAnimation Is the animating queried the enter animation. - */ - public boolean isAnimationRunning(boolean enterAnimation) { - return mOffsetAnimator != null && (enterAnimation == mOffsetAnimatingIn); - } - - public void performOnAnimationFinished(final Runnable runnable) { - if (mOffsetAnimator != null) { - mOffsetAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (runnable != null) { - runnable.run(); - } - } - }); - } else { - if (runnable != null) { - runnable.run(); - } - } - } - - public void setAnimatingOut(boolean animatingOut) { - mAnimatingOut = animatingOut; - } - - /** - * @return Whether the circle is currently launching to the search activity or aborting the - * interaction - */ - public boolean isAnimatingOut() { - return mAnimatingOut; - } - - @Override - public boolean hasOverlappingRendering() { - // not really true but it's ok during an animation, as it's never permanent - return false; - } - - private class Ripple { - float x; - float y; - float radius; - float endRadius; - float alpha; - - Ripple(float x, float y, float endRadius) { - this.x = x; - this.y = y; - this.endRadius = endRadius; - } - - void start() { - ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f); - - // Linear since we are animating multiple values - animator.setInterpolator(new LinearInterpolator()); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - alpha = 1.0f - animation.getAnimatedFraction(); - alpha = mDisappearInterpolator.getInterpolation(alpha); - radius = mAppearInterpolator.getInterpolation(animation.getAnimatedFraction()); - radius *= endRadius; - invalidate(); - } - }); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mRipples.remove(Ripple.this); - updateClipping(); - } - - public void onAnimationStart(Animator animation) { - mRipples.add(Ripple.this); - updateClipping(); - } - }); - animator.setDuration(400); - animator.start(); - } - - public void draw(Canvas canvas) { - mRipplePaint.setAlpha((int) (alpha * 255)); - canvas.drawCircle(x, y, radius, mRipplePaint); - } - } - -} diff --git a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java deleted file mode 100644 index 445b499..0000000 --- a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java +++ /dev/null @@ -1,345 +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; - -import android.app.ActivityOptions; -import android.app.SearchManager; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.media.AudioAttributes; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.UserHandle; -import android.os.Vibrator; -import android.provider.Settings; -import android.util.AttributeSet; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; - -import com.android.systemui.statusbar.BaseStatusBar; -import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.StatusBarPanel; -import com.android.systemui.statusbar.phone.PhoneStatusBar; - -public class SearchPanelView extends FrameLayout implements StatusBarPanel { - - private static final String TAG = "SearchPanelView"; - private static final String ASSIST_ICON_METADATA_NAME = - "com.android.systemui.action_assist_icon"; - - private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .build(); - - private final Context mContext; - private BaseStatusBar mBar; - - private SearchPanelCircleView mCircle; - private ImageView mLogo; - private View mScrim; - - private int mThreshold; - private boolean mHorizontal; - - private boolean mLaunching; - private boolean mDragging; - private boolean mDraggedFarEnough; - private float mStartTouch; - private float mStartDrag; - private boolean mLaunchPending; - - public SearchPanelView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SearchPanelView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mContext = context; - mThreshold = context.getResources().getDimensionPixelSize(R.dimen.search_panel_threshold); - } - - private void startAssistActivity() { - if (!mBar.isDeviceProvisioned()) return; - - // Close Recent Apps if needed - mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL); - - final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) - .getAssistIntent(mContext, true, UserHandle.USER_CURRENT); - if (intent == null) return; - - try { - final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, - R.anim.search_launch_enter, R.anim.search_launch_exit); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - AsyncTask.execute(new Runnable() { - @Override - public void run() { - mContext.startActivityAsUser(intent, opts.toBundle(), - new UserHandle(UserHandle.USER_CURRENT)); - } - }); - } catch (ActivityNotFoundException e) { - Log.w(TAG, "Activity not found for " + intent.getAction()); - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mCircle = (SearchPanelCircleView) findViewById(R.id.search_panel_circle); - mLogo = (ImageView) findViewById(R.id.search_logo); - mScrim = findViewById(R.id.search_panel_scrim); - } - - private void maybeSwapSearchIcon() { - Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) - .getAssistIntent(mContext, false, UserHandle.USER_CURRENT); - if (intent != null) { - ComponentName component = intent.getComponent(); - replaceDrawable(mLogo, component, ASSIST_ICON_METADATA_NAME); - } else { - mLogo.setImageDrawable(null); - } - } - - public void replaceDrawable(ImageView v, ComponentName component, String name) { - if (component != null) { - try { - PackageManager packageManager = mContext.getPackageManager(); - // Look for the search icon specified in the activity meta-data - Bundle metaData = packageManager.getActivityInfo( - component, PackageManager.GET_META_DATA).metaData; - if (metaData != null) { - int iconResId = metaData.getInt(name); - if (iconResId != 0) { - Resources res = packageManager.getResourcesForActivity(component); - v.setImageDrawable(res.getDrawable(iconResId)); - return; - } - } - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Failed to swap drawable; " - + component.flattenToShortString() + " not found", e); - } catch (Resources.NotFoundException nfe) { - Log.w(TAG, "Failed to swap drawable from " - + component.flattenToShortString(), nfe); - } - } - v.setImageDrawable(null); - } - - @Override - public boolean isInContentArea(int x, int y) { - return true; - } - - private void vibrate() { - Context context = getContext(); - if (Settings.System.getIntForUser(context.getContentResolver(), - 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), - VIBRATION_ATTRIBUTES); - } - } - - public void show(final boolean show, boolean animate) { - if (show) { - maybeSwapSearchIcon(); - if (getVisibility() != View.VISIBLE) { - setVisibility(View.VISIBLE); - vibrate(); - if (animate) { - startEnterAnimation(); - } else { - mScrim.setAlpha(1f); - } - } - setFocusable(true); - setFocusableInTouchMode(true); - requestFocus(); - } else { - if (animate) { - startAbortAnimation(); - } else { - setVisibility(View.INVISIBLE); - } - } - } - - private void startEnterAnimation() { - mCircle.startEnterAnimation(); - mScrim.setAlpha(0f); - mScrim.animate() - .alpha(1f) - .setDuration(300) - .setStartDelay(50) - .setInterpolator(PhoneStatusBar.ALPHA_IN) - .start(); - - } - - private void startAbortAnimation() { - mCircle.startAbortAnimation(new Runnable() { - @Override - public void run() { - mCircle.setAnimatingOut(false); - setVisibility(View.INVISIBLE); - } - }); - mCircle.setAnimatingOut(true); - mScrim.animate() - .alpha(0f) - .setDuration(300) - .setStartDelay(0) - .setInterpolator(PhoneStatusBar.ALPHA_OUT); - } - - public void hide(boolean animate) { - if (mBar != null) { - // This will indirectly cause show(false, ...) to get called - mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); - } else { - if (animate) { - startAbortAnimation(); - } else { - setVisibility(View.INVISIBLE); - } - } - } - - @Override - public boolean dispatchHoverEvent(MotionEvent event) { - // Ignore hover events outside of this panel bounds since such events - // generate spurious accessibility events with the panel content when - // tapping outside of it, thus confusing the user. - final int x = (int) event.getX(); - final int y = (int) event.getY(); - if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { - return super.dispatchHoverEvent(event); - } - return true; - } - - /** - * Whether the panel is showing, or, if it's animating, whether it will be - * when the animation is done. - */ - public boolean isShowing() { - return getVisibility() == View.VISIBLE && !mCircle.isAnimatingOut(); - } - - public void setBar(BaseStatusBar bar) { - mBar = bar; - } - - public boolean isAssistantAvailable() { - return ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) - .getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (mLaunching || mLaunchPending) { - return false; - } - int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - mStartTouch = mHorizontal ? event.getX() : event.getY(); - mDragging = false; - mDraggedFarEnough = false; - mCircle.reset(); - break; - case MotionEvent.ACTION_MOVE: - float currentTouch = mHorizontal ? event.getX() : event.getY(); - if (getVisibility() == View.VISIBLE && !mDragging && - (!mCircle.isAnimationRunning(true /* enterAnimation */) - || Math.abs(mStartTouch - currentTouch) > mThreshold)) { - mStartDrag = currentTouch; - mDragging = true; - } - if (mDragging) { - float offset = Math.max(mStartDrag - currentTouch, 0.0f); - mCircle.setDragDistance(offset); - mDraggedFarEnough = Math.abs(mStartTouch - currentTouch) > mThreshold; - mCircle.setDraggedFarEnough(mDraggedFarEnough); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (mDraggedFarEnough) { - if (mCircle.isAnimationRunning(true /* enterAnimation */)) { - mLaunchPending = true; - mCircle.setAnimatingOut(true); - mCircle.performOnAnimationFinished(new Runnable() { - @Override - public void run() { - startExitAnimation(); - } - }); - } else { - startExitAnimation(); - } - } else { - startAbortAnimation(); - } - break; - } - return true; - } - - private void startExitAnimation() { - mLaunchPending = false; - if (mLaunching || getVisibility() != View.VISIBLE) { - return; - } - mLaunching = true; - startAssistActivity(); - vibrate(); - mCircle.setAnimatingOut(true); - mCircle.startExitAnimation(new Runnable() { - @Override - public void run() { - mLaunching = false; - mCircle.setAnimatingOut(false); - setVisibility(View.INVISIBLE); - } - }); - mScrim.animate() - .alpha(0f) - .setDuration(300) - .setStartDelay(0) - .setInterpolator(PhoneStatusBar.ALPHA_OUT); - } - - public void setHorizontal(boolean horizontal) { - mHorizontal = horizontal; - mCircle.setHorizontal(horizontal); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index b3f90d7..33bd726 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -40,13 +40,14 @@ public class SystemUIApplication extends Application { * The classes of the stuff to start. */ private final Class<?>[] SERVICES = new Class[] { + com.android.systemui.tuner.TunerService.class, com.android.systemui.keyguard.KeyguardViewMediator.class, - com.android.systemui.recent.Recents.class, + com.android.systemui.recents.Recents.class, com.android.systemui.volume.VolumeUI.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.media.RingtonePlayer.class, }; /** diff --git a/packages/SystemUI/src/com/android/systemui/ViewInvertHelper.java b/packages/SystemUI/src/com/android/systemui/ViewInvertHelper.java index 2ff8f8a..eddf2b1 100644 --- a/packages/SystemUI/src/com/android/systemui/ViewInvertHelper.java +++ b/packages/SystemUI/src/com/android/systemui/ViewInvertHelper.java @@ -26,8 +26,6 @@ import android.view.View; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; -import com.android.systemui.statusbar.phone.NotificationPanelView; - /** * Helper to invert the colors of views and fade between the states. */ diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java b/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java new file mode 100644 index 0000000..c3a8f2e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.assist; + +import com.android.systemui.R; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.os.Handler; +import android.view.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AnimationUtils; + +/** + * Visually discloses that contextual data was provided to an assistant. + */ +public class AssistDisclosure { + private final Context mContext; + private final WindowManager mWm; + private final Handler mHandler; + + private AssistDisclosureView mView; + private boolean mViewAdded; + + public AssistDisclosure(Context context, Handler handler) { + mContext = context; + mHandler = handler; + mWm = mContext.getSystemService(WindowManager.class); + } + + public void postShow() { + mHandler.removeCallbacks(mShowRunnable); + mHandler.post(mShowRunnable); + } + + private void show() { + if (mView == null) { + mView = new AssistDisclosureView(mContext); + } + if (!mViewAdded) { + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED, + PixelFormat.TRANSLUCENT); + lp.setTitle("AssistDisclosure"); + + mWm.addView(mView, lp); + mViewAdded = true; + } + } + + private void hide() { + if (mViewAdded) { + mWm.removeView(mView); + mViewAdded = false; + } + } + + private Runnable mShowRunnable = new Runnable() { + @Override + public void run() { + show(); + } + }; + + private class AssistDisclosureView extends View + implements ValueAnimator.AnimatorUpdateListener { + + public static final int TRACING_ANIMATION_DURATION = 600; + public static final int ALPHA_IN_ANIMATION_DURATION = 450; + public static final int ALPHA_OUT_ANIMATION_DURATION = 400; + + private float mThickness; + private float mShadowThickness; + private final Paint mPaint = new Paint(); + private final Paint mShadowPaint = new Paint(); + + private final ValueAnimator mTracingAnimator; + private final ValueAnimator mAlphaOutAnimator; + private final ValueAnimator mAlphaInAnimator; + private final AnimatorSet mAnimator; + + private float mTracingProgress = 0; + private int mAlpha = 0; + + public AssistDisclosureView(Context context) { + super(context); + + mTracingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(TRACING_ANIMATION_DURATION); + mTracingAnimator.addUpdateListener(this); + mTracingAnimator.setInterpolator(AnimationUtils.loadInterpolator(mContext, + R.interpolator.assist_disclosure_trace)); + mAlphaInAnimator = ValueAnimator.ofInt(0, 255).setDuration(ALPHA_IN_ANIMATION_DURATION); + mAlphaInAnimator.addUpdateListener(this); + mAlphaInAnimator.setInterpolator(AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.fast_out_slow_in)); + mAlphaOutAnimator = ValueAnimator.ofInt(255, 0).setDuration( + ALPHA_OUT_ANIMATION_DURATION); + mAlphaOutAnimator.addUpdateListener(this); + mAlphaOutAnimator.setInterpolator(AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.fast_out_linear_in)); + mAnimator = new AnimatorSet(); + mAnimator.play(mAlphaInAnimator).with(mTracingAnimator); + mAnimator.play(mAlphaInAnimator).before(mAlphaOutAnimator); + mAnimator.addListener(new AnimatorListenerAdapter() { + boolean mCancelled; + + @Override + public void onAnimationStart(Animator animation) { + mCancelled = false; + } + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCancelled) { + hide(); + } + } + }); + + PorterDuffXfermode srcMode = new PorterDuffXfermode(PorterDuff.Mode.SRC); + mPaint.setColor(Color.WHITE); + mPaint.setXfermode(srcMode); + mShadowPaint.setColor(Color.DKGRAY); + mShadowPaint.setXfermode(srcMode); + + mThickness = getResources().getDimension(R.dimen.assist_disclosure_thickness); + mShadowThickness = getResources().getDimension( + R.dimen.assist_disclosure_shadow_thickness); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + startAnimation(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_ASSIST_READING_CONTEXT); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mAnimator.cancel(); + + mTracingProgress = 0; + mAlpha = 0; + } + + private void startAnimation() { + mAnimator.cancel(); + mAnimator.start(); + } + + @Override + protected void onDraw(Canvas canvas) { + mPaint.setAlpha(mAlpha); + mShadowPaint.setAlpha(mAlpha / 4); + + drawGeometry(canvas, mShadowPaint, mShadowThickness); + drawGeometry(canvas, mPaint, 0); + } + + private void drawGeometry(Canvas canvas, Paint paint, float padding) { + final int width = getWidth(); + final int height = getHeight(); + float thickness = mThickness; + final float pixelProgress = mTracingProgress * (width + height - 2 * thickness); + + float bottomProgress = Math.min(pixelProgress, width / 2f); + if (bottomProgress > 0) { + drawBeam(canvas, + width / 2f - bottomProgress, + height - thickness, + width / 2f + bottomProgress, + height, paint, padding); + } + + float sideProgress = Math.min(pixelProgress - bottomProgress, height - thickness); + if (sideProgress > 0) { + drawBeam(canvas, + 0, + (height - thickness) - sideProgress, + thickness, + height - thickness, paint, padding); + drawBeam(canvas, + width - thickness, + (height - thickness) - sideProgress, + width, + height - thickness, paint, padding); + } + + float topProgress = Math.min(pixelProgress - bottomProgress - sideProgress, + width / 2 - thickness); + if (sideProgress > 0 && topProgress > 0) { + drawBeam(canvas, + thickness, + 0, + thickness + topProgress, + thickness, paint, padding); + drawBeam(canvas, + (width - thickness) - topProgress, + 0, + width - thickness, + thickness, paint, padding); + } + } + + private void drawBeam(Canvas canvas, float left, float top, float right, float bottom, + Paint paint, float padding) { + canvas.drawRect(left - padding, + top - padding, + right + padding, + bottom + padding, + paint); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (animation == mAlphaOutAnimator) { + mAlpha = (int) mAlphaOutAnimator.getAnimatedValue(); + } else if (animation == mAlphaInAnimator) { + mAlpha = (int) mAlphaInAnimator.getAnimatedValue(); + } else if (animation == mTracingAnimator) { + mTracingProgress = (float) mTracingAnimator.getAnimatedValue(); + } + invalidate(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java new file mode 100644 index 0000000..51d0bf1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -0,0 +1,311 @@ +package com.android.systemui.assist; + +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.SearchManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.graphics.PixelFormat; +import android.media.AudioAttributes; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; +import android.view.Gravity; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ImageView; + +import com.android.internal.app.AssistUtils; +import com.android.internal.app.IVoiceInteractionSessionShowCallback; +import com.android.systemui.R; +import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.phone.PhoneStatusBar; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Class to manage everything related to assist in SystemUI. + */ +public class AssistManager { + + private static final String TAG = "AssistManager"; + private static final String ASSIST_ICON_METADATA_NAME = + "com.android.systemui.action_assist_icon"; + + private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) + .build(); + + private static final long TIMEOUT_SERVICE = 2500; + private static final long TIMEOUT_ACTIVITY = 1000; + + private final Context mContext; + private final WindowManager mWindowManager; + private final AssistDisclosure mAssistDisclosure; + + private AssistOrbContainer mView; + private final PhoneStatusBar mBar; + private final AssistUtils mAssistUtils; + + private ComponentName mAssistComponent; + + private IVoiceInteractionSessionShowCallback mShowCallback = + new IVoiceInteractionSessionShowCallback.Stub() { + + @Override + public void onFailed() throws RemoteException { + mView.post(mHideRunnable); + } + + @Override + public void onShown() throws RemoteException { + mView.post(mHideRunnable); + } + }; + + private Runnable mHideRunnable = new Runnable() { + @Override + public void run() { + mView.removeCallbacks(this); + mView.show(false /* show */, true /* animate */); + } + }; + + private final ContentObserver mAssistSettingsObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + updateAssistInfo(); + } + }; + + public AssistManager(PhoneStatusBar bar, Context context) { + mContext = context; + mBar = bar; + mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + mAssistUtils = new AssistUtils(context); + + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ASSISTANT), false, + mAssistSettingsObserver); + mAssistSettingsObserver.onChange(false); + mAssistDisclosure = new AssistDisclosure(context, new Handler()); + } + + public void onConfigurationChanged() { + boolean visible = false; + if (mView != null) { + visible = mView.isShowing(); + mWindowManager.removeView(mView); + } + + mView = (AssistOrbContainer) LayoutInflater.from(mContext).inflate( + R.layout.assist_orb, null); + mView.setVisibility(View.GONE); + mView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + WindowManager.LayoutParams lp = getLayoutParams(); + mWindowManager.addView(mView, lp); + if (visible) { + mView.show(true /* show */, false /* animate */); + } + } + + public void onGestureInvoked() { + if (mAssistComponent == null) { + return; + } + + final boolean isService = isAssistantService(); + if (!isService || !isVoiceSessionRunning()) { + showOrb(); + mView.postDelayed(mHideRunnable, isService + ? TIMEOUT_SERVICE + : TIMEOUT_ACTIVITY); + } + startAssist(); + } + + public void hideAssist() { + mAssistUtils.hideCurrentSession(); + } + + private WindowManager.LayoutParams getLayoutParams() { + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + mContext.getResources().getDimensionPixelSize(R.dimen.assist_orb_scrim_height), + WindowManager.LayoutParams.TYPE_VOICE_INTERACTION_STARTING, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + if (ActivityManager.isHighEndGfx()) { + lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + } + lp.gravity = Gravity.BOTTOM | Gravity.START; + lp.setTitle("AssistPreviewPanel"); + lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; + return lp; + } + + private void showOrb() { + maybeSwapSearchIcon(); + mView.show(true /* show */, true /* animate */); + } + + private void startAssist() { + if (mAssistComponent != null) { + if (isAssistantService()) { + startVoiceInteractor(); + } else { + startAssistActivity(); + } + } + } + + private void startAssistActivity() { + if (!mBar.isDeviceProvisioned()) { + return; + } + + // Close Recent Apps if needed + mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL | + CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL); + + boolean structureEnabled = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0; + + final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) + .getAssistIntent(mContext, structureEnabled, UserHandle.USER_CURRENT); + if (intent == null) { + return; + } + if (mAssistComponent != null) { + intent.setComponent(mAssistComponent); + } + + if (structureEnabled) { + showDisclosure(); + } + + try { + final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, + R.anim.search_launch_enter, R.anim.search_launch_exit); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + AsyncTask.execute(new Runnable() { + @Override + public void run() { + mContext.startActivityAsUser(intent, opts.toBundle(), + new UserHandle(UserHandle.USER_CURRENT)); + } + }); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Activity not found for " + intent.getAction()); + } + } + + private void startVoiceInteractor() { + mAssistUtils.showSessionForActiveService(mShowCallback); + } + + public void launchVoiceAssistFromKeyguard() { + mAssistUtils.launchVoiceAssistFromKeyguard(); + } + + public boolean canVoiceAssistBeLaunchedFromKeyguard() { + return mAssistUtils.activeServiceSupportsLaunchFromKeyguard(); + } + + public ComponentName getVoiceInteractorComponentName() { + return mAssistUtils.getActiveServiceComponentName(); + } + + private boolean isVoiceSessionRunning() { + return mAssistUtils.isSessionRunning(); + } + + public void destroy() { + mWindowManager.removeViewImmediate(mView); + } + + private void maybeSwapSearchIcon() { + if (mAssistComponent != null) { + replaceDrawable(mView.getOrb().getLogo(), mAssistComponent, ASSIST_ICON_METADATA_NAME, + isAssistantService()); + } else { + mView.getOrb().getLogo().setImageDrawable(null); + } + } + + public void replaceDrawable(ImageView v, ComponentName component, String name, + boolean isService) { + if (component != null) { + try { + PackageManager packageManager = mContext.getPackageManager(); + // Look for the search icon specified in the activity meta-data + Bundle metaData = isService + ? packageManager.getServiceInfo( + component, PackageManager.GET_META_DATA).metaData + : packageManager.getActivityInfo( + component, PackageManager.GET_META_DATA).metaData; + if (metaData != null) { + int iconResId = metaData.getInt(name); + if (iconResId != 0) { + Resources res = packageManager.getResourcesForApplication( + component.getPackageName()); + v.setImageDrawable(res.getDrawable(iconResId)); + return; + } + } + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Failed to swap drawable; " + + component.flattenToShortString() + " not found", e); + } catch (Resources.NotFoundException nfe) { + Log.w(TAG, "Failed to swap drawable from " + + component.flattenToShortString(), nfe); + } + } + v.setImageDrawable(null); + } + + private boolean isAssistantService() { + return mAssistComponent == null ? + false : mAssistComponent.equals(getVoiceInteractorComponentName()); + } + + private void updateAssistInfo() { + mAssistComponent = mAssistUtils.getAssistComponentForUser(UserHandle.USER_CURRENT); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("AssistManager state:"); + pw.print(" mAssistComponent="); pw.println(mAssistComponent); + } + + public void showDisclosure() { + mAssistDisclosure.postShow(); + } + + public void onUserSwitched(int newUserId) { + updateAssistInfo(); + } + + public void prepareBeforeInvocation() { + updateAssistInfo(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistOrbContainer.java b/packages/SystemUI/src/com/android/systemui/assist/AssistOrbContainer.java new file mode 100644 index 0000000..67017db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistOrbContainer.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.assist; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; + +import com.android.systemui.R; + +public class AssistOrbContainer extends FrameLayout { + + private static final long EXIT_START_DELAY = 150; + + private final Interpolator mLinearOutSlowInInterpolator; + private final Interpolator mFastOutLinearInInterpolator; + + private View mScrim; + private View mNavbarScrim; + private AssistOrbView mOrb; + + private boolean mAnimatingOut; + + public AssistOrbContainer(Context context) { + this(context, null); + } + + public AssistOrbContainer(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public AssistOrbContainer(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, + android.R.interpolator.linear_out_slow_in); + mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, + android.R.interpolator.fast_out_slow_in); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mScrim = findViewById(R.id.assist_orb_scrim); + mNavbarScrim = findViewById(R.id.assist_orb_navbar_scrim); + mOrb = (AssistOrbView) findViewById(R.id.assist_orb); + } + + public void show(final boolean show, boolean animate) { + if (show) { + if (getVisibility() != View.VISIBLE) { + setVisibility(View.VISIBLE); + if (animate) { + startEnterAnimation(); + } else { + reset(); + } + } + } else { + if (animate) { + startExitAnimation(new Runnable() { + @Override + public void run() { + mAnimatingOut = false; + setVisibility(View.GONE); + } + }); + } else { + setVisibility(View.GONE); + } + } + } + + private void reset() { + mAnimatingOut = false; + mOrb.reset(); + mScrim.setAlpha(1f); + mNavbarScrim.setAlpha(1f); + } + + private void startEnterAnimation() { + if (mAnimatingOut) { + return; + } + mOrb.startEnterAnimation(); + mScrim.setAlpha(0f); + mNavbarScrim.setAlpha(0f); + post(new Runnable() { + @Override + public void run() { + mScrim.animate() + .alpha(1f) + .setDuration(300) + .setStartDelay(0) + .setInterpolator(mLinearOutSlowInInterpolator); + mNavbarScrim.animate() + .alpha(1f) + .setDuration(300) + .setStartDelay(0) + .setInterpolator(mLinearOutSlowInInterpolator); + } + }); + } + + private void startExitAnimation(final Runnable endRunnable) { + if (mAnimatingOut) { + if (endRunnable != null) { + endRunnable.run(); + } + return; + } + mAnimatingOut = true; + mOrb.startExitAnimation(EXIT_START_DELAY); + mScrim.animate() + .alpha(0f) + .setDuration(250) + .setStartDelay(EXIT_START_DELAY) + .setInterpolator(mFastOutLinearInInterpolator); + mNavbarScrim.animate() + .alpha(0f) + .setDuration(250) + .setStartDelay(EXIT_START_DELAY) + .setInterpolator(mFastOutLinearInInterpolator) + .withEndAction(endRunnable); + } + + /** + * Whether the panel is showing, or, if it's animating, whether it will be + * when the animation is done. + */ + public boolean isShowing() { + return getVisibility() == View.VISIBLE && !mAnimatingOut; + } + + public AssistOrbView getOrb() { + return mOrb; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistOrbView.java b/packages/SystemUI/src/com/android/systemui/assist/AssistOrbView.java new file mode 100644 index 0000000..a3372a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistOrbView.java @@ -0,0 +1,285 @@ +/* + * 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.assist; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.systemui.R; + +public class AssistOrbView extends FrameLayout { + + private final int mCircleMinSize; + private final int mBaseMargin; + private final int mStaticOffset; + private final Paint mBackgroundPaint = new Paint(); + private final Rect mCircleRect = new Rect(); + private final Rect mStaticRect = new Rect(); + private final Interpolator mAppearInterpolator; + private final Interpolator mDisappearInterpolator; + private final Interpolator mOvershootInterpolator = new OvershootInterpolator(); + + private boolean mClipToOutline; + private final int mMaxElevation; + private float mOutlineAlpha; + private float mOffset; + private float mCircleSize; + private ImageView mLogo; + private float mCircleAnimationEndValue; + + private ValueAnimator mOffsetAnimator; + private ValueAnimator mCircleAnimator; + + private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + applyCircleSize((float) animation.getAnimatedValue()); + updateElevation(); + } + }; + private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCircleAnimator = null; + } + }; + private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mOffset = (float) animation.getAnimatedValue(); + updateLayout(); + } + }; + + + public AssistOrbView(Context context) { + this(context, null); + } + + public AssistOrbView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + if (mCircleSize > 0.0f) { + outline.setOval(mCircleRect); + } else { + outline.setEmpty(); + } + outline.setAlpha(mOutlineAlpha); + } + }); + setWillNotDraw(false); + mCircleMinSize = context.getResources().getDimensionPixelSize( + R.dimen.assist_orb_size); + mBaseMargin = context.getResources().getDimensionPixelSize( + R.dimen.assist_orb_base_margin); + mStaticOffset = context.getResources().getDimensionPixelSize( + R.dimen.assist_orb_travel_distance); + mMaxElevation = context.getResources().getDimensionPixelSize( + R.dimen.assist_orb_elevation); + mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.linear_out_slow_in); + mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.fast_out_linear_in); + mBackgroundPaint.setAntiAlias(true); + mBackgroundPaint.setColor(getResources().getColor(R.color.assist_orb_color)); + } + + public ImageView getLogo() { + return mLogo; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + drawBackground(canvas); + } + + private void drawBackground(Canvas canvas) { + canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2, + mBackgroundPaint); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mLogo = (ImageView) findViewById(R.id.search_logo); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight()); + if (changed) { + updateCircleRect(mStaticRect, mStaticOffset, true); + } + } + + public void animateCircleSize(float circleSize, long duration, + long startDelay, Interpolator interpolator) { + if (circleSize == mCircleAnimationEndValue) { + return; + } + if (mCircleAnimator != null) { + mCircleAnimator.cancel(); + } + mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize); + mCircleAnimator.addUpdateListener(mCircleUpdateListener); + mCircleAnimator.addListener(mClearAnimatorListener); + mCircleAnimator.setInterpolator(interpolator); + mCircleAnimator.setDuration(duration); + mCircleAnimator.setStartDelay(startDelay); + mCircleAnimator.start(); + mCircleAnimationEndValue = circleSize; + } + + private void applyCircleSize(float circleSize) { + mCircleSize = circleSize; + updateLayout(); + } + + private void updateElevation() { + float t = (mStaticOffset - mOffset) / (float) mStaticOffset; + t = 1.0f - Math.max(t, 0.0f); + float offset = t * mMaxElevation; + setElevation(offset); + } + + /** + * Animates the offset to the edge of the screen. + * + * @param offset The offset to apply. + * @param startDelay The desired start delay if animated. + * + * @param interpolator The desired interpolator if animated. If null, + * a default interpolator will be taken designed for appearing or + * disappearing. + */ + private void animateOffset(float offset, long duration, long startDelay, + Interpolator interpolator) { + if (mOffsetAnimator != null) { + mOffsetAnimator.removeAllListeners(); + mOffsetAnimator.cancel(); + } + mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset); + mOffsetAnimator.addUpdateListener(mOffsetUpdateListener); + mOffsetAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mOffsetAnimator = null; + } + }); + mOffsetAnimator.setInterpolator(interpolator); + mOffsetAnimator.setStartDelay(startDelay); + mOffsetAnimator.setDuration(duration); + mOffsetAnimator.start(); + } + + private void updateLayout() { + updateCircleRect(); + updateLogo(); + invalidateOutline(); + invalidate(); + updateClipping(); + } + + private void updateClipping() { + boolean clip = mCircleSize < mCircleMinSize; + if (clip != mClipToOutline) { + setClipToOutline(clip); + mClipToOutline = clip; + } + } + + private void updateLogo() { + float translationX = (mCircleRect.left + mCircleRect.right) / 2.0f - mLogo.getWidth() / 2.0f; + float translationY = (mCircleRect.top + mCircleRect.bottom) / 2.0f + - mLogo.getHeight() / 2.0f - mCircleMinSize / 7f; + float t = (mStaticOffset - mOffset) / (float) mStaticOffset; + translationY += t * mStaticOffset * 0.1f; + float alpha = 1.0f-t; + alpha = Math.max((alpha - 0.5f) * 2.0f, 0); + mLogo.setImageAlpha((int) (alpha * 255)); + mLogo.setTranslationX(translationX); + mLogo.setTranslationY(translationY); + } + + private void updateCircleRect() { + updateCircleRect(mCircleRect, mOffset, false); + } + + private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) { + int left, top; + float circleSize = useStaticSize ? mCircleMinSize : mCircleSize; + left = (int) (getWidth() - circleSize) / 2; + top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset); + rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize)); + } + + public void startExitAnimation(long delay) { + animateCircleSize(0, 200, delay, mDisappearInterpolator); + animateOffset(0, 200, delay, mDisappearInterpolator); + } + + public void startEnterAnimation() { + applyCircleSize(0); + post(new Runnable() { + @Override + public void run() { + animateCircleSize(mCircleMinSize, 300, 0 /* delay */, mOvershootInterpolator); + animateOffset(mStaticOffset, 400, 0 /* delay */, mAppearInterpolator); + } + }); + } + + public void reset() { + mClipToOutline = false; + mBackgroundPaint.setAlpha(255); + mOutlineAlpha = 1.0f; + } + + @Override + public boolean hasOverlappingRendering() { + // not really true but it's ok during an animation, as it's never permanent + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 1f3a830..3f72125 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -82,11 +82,9 @@ public class DozeLog { sNotificationPulseStats.append(); } - public static void traceDozing(Context context, boolean dozing) { - if (!ENABLED) return; - sPulsing = false; + private static void init(Context context) { synchronized (DozeLog.class) { - if (dozing && sMessages == null) { + if (sMessages == null) { sTimes = new long[SIZE]; sMessages = new String[SIZE]; sSince = System.currentTimeMillis(); @@ -105,6 +103,12 @@ public class DozeLog { KeyguardUpdateMonitor.getInstance(context).registerCallback(sKeyguardCallback); } } + } + + public static void traceDozing(Context context, boolean dozing) { + if (!ENABLED) return; + sPulsing = false; + init(context); log("dozing " + dozing); } @@ -146,10 +150,12 @@ public class DozeLog { } } - public static void traceProximityResult(boolean near, long millis, int pulseReason) { + public static void traceProximityResult(Context context, boolean near, long millis, + int pulseReason) { if (!ENABLED) return; log("proximityResult reason=" + pulseReasonToString(pulseReason) + " near=" + near + " millis=" + millis); + init(context); sProxStats[pulseReason][near ? 0 : 1].append(); } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index 8d27450..5d46712 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -235,7 +235,7 @@ public class DozeService extends DreamService { public void onProximityResult(int result) { final boolean isNear = result == RESULT_NEAR; final long end = SystemClock.uptimeMillis(); - DozeLog.traceProximityResult(isNear, end - start, reason); + DozeLog.traceProximityResult(mContext, isNear, end - start, reason); if (nonBlocking) { // we already continued return; diff --git a/packages/SystemUI/src/com/android/systemui/egg/LLandActivity.java b/packages/SystemUI/src/com/android/systemui/egg/LLandActivity.java index b9f8106..50221d3 100644 --- a/packages/SystemUI/src/com/android/systemui/egg/LLandActivity.java +++ b/packages/SystemUI/src/com/android/systemui/egg/LLandActivity.java @@ -18,7 +18,6 @@ package com.android.systemui.egg; import android.app.Activity; import android.os.Bundle; -import android.util.Log; import android.widget.TextView; import com.android.systemui.R; diff --git a/packages/SystemUI/src/com/android/systemui/egg/ShruggyActivity.java b/packages/SystemUI/src/com/android/systemui/egg/ShruggyActivity.java new file mode 100644 index 0000000..7459957 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/egg/ShruggyActivity.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.egg; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; +import com.android.systemui.R; + +public class ShruggyActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Toast.makeText(this, getString(R.string.regrettable_lack_of_easter_egg), + Toast.LENGTH_SHORT).show(); + Log.v("SystemUI", "Hey, it's just a preview; what did you expect?"); + finish(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 73fa2ed..98558b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -108,15 +108,21 @@ public class KeyguardService extends Service { } @Override // Binder interface - public void onScreenTurnedOff(int reason) { + public void onStartedGoingToSleep(int reason) { checkPermission(); - mKeyguardViewMediator.onScreenTurnedOff(reason); + mKeyguardViewMediator.onStartedGoingToSleep(reason); } @Override // Binder interface - public void onScreenTurnedOn(IKeyguardShowCallback callback) { + public void onFinishedGoingToSleep(int reason) { checkPermission(); - mKeyguardViewMediator.onScreenTurnedOn(callback); + mKeyguardViewMediator.onFinishedGoingToSleep(reason); + } + + @Override // Binder interface + public void onStartedWakingUp(IKeyguardShowCallback callback) { + checkPermission(); + mKeyguardViewMediator.onStartedWakingUp(callback); } @Override // Binder interface diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index fae0643..7f61fc1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -33,6 +33,7 @@ import android.content.pm.UserInfo; import android.media.AudioManager; import android.media.SoundPool; import android.os.Bundle; +import android.os.DeadObjectException; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -61,9 +62,9 @@ import com.android.internal.telephony.IccCardConstants; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardConstants; import com.android.keyguard.KeyguardDisplayManager; +import com.android.keyguard.KeyguardSecurityView; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.keyguard.MultiUserAvatarCache; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.SystemUI; import com.android.systemui.statusbar.phone.PhoneStatusBar; @@ -172,8 +173,13 @@ public class KeyguardViewMediator extends SystemUI { */ private static final String KEYGUARD_ANALYTICS_SETTING = "keyguard_analytics"; + /** + * How much faster we collapse the lockscreen when authenticating with fingerprint. + */ + private static final float FINGERPRINT_COLLAPSE_SPEEDUP_FACTOR = 1.3f; + /** The stream type that the lock sounds are tied to. */ - private int mMasterStreamType; + private int mUiSoundsStreamType; private AlarmManager mAlarmManager; private AudioManager mAudioManager; @@ -184,11 +190,6 @@ public class KeyguardViewMediator extends SystemUI { private boolean mBootCompleted; private boolean mBootSendUserPresent; - // 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; @@ -251,7 +252,7 @@ public class KeyguardViewMediator extends SystemUI { private KeyguardUpdateMonitor mUpdateMonitor; - private boolean mScreenOn; + private boolean mDeviceInteractive; // last known state of the cellular connection private String mPhoneState = TelephonyManager.EXTRA_STATE_IDLE; @@ -302,6 +303,18 @@ public class KeyguardViewMediator extends SystemUI { private final ArrayList<IKeyguardStateCallback> mKeyguardStateCallbacks = new ArrayList<>(); + /** + * When starting going to sleep, we figured out that we need to reset Keyguard state and this + * should be committed when finished going to sleep. + */ + private boolean mPendingReset; + + /** + * When starting goign to sleep, we figured out that we need to lock Keyguard and this should be + * committed when finished going to sleep. + */ + private boolean mPendingLock; + KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { @Override @@ -314,9 +327,6 @@ public class KeyguardViewMediator extends SystemUI { resetKeyguardDonePendingLocked(); 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); } } @@ -333,21 +343,14 @@ public class KeyguardViewMediator extends SystemUI { } @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 + && !mDeviceInteractive // screen off && mExternallyEnabled) { // not disabled by any app // note: this is a way to gracefully reenable the keyguard when the call @@ -369,7 +372,6 @@ public class KeyguardViewMediator extends SystemUI { @Override public void onDeviceProvisioned() { sendUserPresentBroadcast(); - updateInputRestricted(); } @Override @@ -380,14 +382,17 @@ public class KeyguardViewMediator extends SystemUI { + ",state=" + simState + ")"); } - try { - int size = mKeyguardStateCallbacks.size(); - boolean simPinSecure = mUpdateMonitor.isSimPinSecure(); - for (int i = 0; i < size; i++) { + int size = mKeyguardStateCallbacks.size(); + boolean simPinSecure = mUpdateMonitor.isSimPinSecure(); + for (int i = size - 1; i >= 0; i--) { + try { mKeyguardStateCallbacks.get(i).onSimSecureStateChanged(simPinSecure); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onSimSecureStateChanged", e); + if (e instanceof DeadObjectException) { + mKeyguardStateCallbacks.remove(i); + } } - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onSimSecureStateChanged", e); } switch (simState) { @@ -447,9 +452,13 @@ public class KeyguardViewMediator extends SystemUI { } } - public void onFingerprintRecognized(int userId) { + @Override + public void onFingerprintAuthenticated(int userId) { if (mStatusBarKeyguardViewManager.isBouncerShowing()) { - mViewMediatorCallback.keyguardDone(true); + mStatusBarKeyguardViewManager.notifyKeyguardAuthenticated(); + } else { + mStatusBarKeyguardViewManager.animateCollapsePanels( + FINGERPRINT_COLLAPSE_SPEEDUP_FACTOR); } }; @@ -477,11 +486,6 @@ public class KeyguardViewMediator extends SystemUI { } @Override - public void onUserActivityTimeoutChanged() { - mStatusBarKeyguardViewManager.updateUserActivityTimeout(); - } - - @Override public void keyguardDonePending() { mKeyguardDonePending = true; mHideAnimationRun = true; @@ -505,6 +509,11 @@ public class KeyguardViewMediator extends SystemUI { } @Override + public void resetKeyguard() { + resetStateLocked(); + } + + @Override public void playTrustedSound() { KeyguardViewMediator.this.playTrustedSound(); } @@ -513,6 +522,22 @@ public class KeyguardViewMediator extends SystemUI { public boolean isInputRestricted() { return KeyguardViewMediator.this.isInputRestricted(); } + + @Override + public boolean isScreenOn() { + return mDeviceInteractive; + } + + @Override + public int getBouncerPromptReason() { + int currentUser = ActivityManager.getCurrentUser(); + if ((mUpdateMonitor.getUserTrustIsManaged(currentUser) + || mUpdateMonitor.isUnlockWithFingerPrintPossible(currentUser)) + && !mTrustManager.hasUserAuthenticatedSinceBoot(currentUser)) { + return KeyguardSecurityView.PROMPT_REASON_RESTART; + } + return KeyguardSecurityView.PROMPT_REASON_NONE; + } }; public void userActivity() { @@ -536,17 +561,19 @@ public class KeyguardViewMediator extends SystemUI { mUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); mLockPatternUtils = new LockPatternUtils(mContext); - mLockPatternUtils.setCurrentUser(ActivityManager.getCurrentUser()); + KeyguardUpdateMonitor.setCurrentUser(ActivityManager.getCurrentUser()); // Assume keyguard is showing (unless it's disabled) until we know for sure... - setShowingLocked(!shouldWaitForProvisioning() && !mLockPatternUtils.isLockScreenDisabled()); + setShowingLocked(!shouldWaitForProvisioning() && !mLockPatternUtils.isLockScreenDisabled( + KeyguardUpdateMonitor.getCurrentUser())); + updateInputRestrictedLocked(); mTrustManager.reportKeyguardShowingChanged(); mStatusBarKeyguardViewManager = new StatusBarKeyguardViewManager(mContext, mViewMediatorCallback, mLockPatternUtils); final ContentResolver cr = mContext.getContentResolver(); - mScreenOn = mPM.isScreenOn(); + mDeviceInteractive = mPM.isInteractive(); mLockSounds = new SoundPool(1, AudioManager.STREAM_SYSTEM, 0); String soundPath = Settings.Global.getString(cr, Settings.Global.LOCK_SOUND); @@ -595,26 +622,8 @@ public class KeyguardViewMediator extends SystemUI { 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); + mUpdateMonitor.registerCallback(mUpdateCallback); } // Most services aren't available until the system reaches the ready state, so we // send it here when the device first boots. @@ -626,21 +635,18 @@ public class KeyguardViewMediator extends SystemUI { * @param why either {@link android.view.WindowManagerPolicy#OFF_BECAUSE_OF_USER} or * {@link android.view.WindowManagerPolicy#OFF_BECAUSE_OF_TIMEOUT}. */ - public void onScreenTurnedOff(int why) { + public void onStartedGoingToSleep(int why) { + if (DEBUG) Log.d(TAG, "onStartedGoingToSleep(" + why + ")"); synchronized (this) { - mScreenOn = false; - if (DEBUG) Log.d(TAG, "onScreenTurnedOff(" + why + ")"); - - resetKeyguardDonePendingLocked(); - mHideAnimationRun = false; + mDeviceInteractive = 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. + int currentUser = KeyguardUpdateMonitor.getCurrentUser(); final boolean lockImmediately = - mLockPatternUtils.getPowerButtonInstantlyLocks() || !mLockPatternUtils.isSecure(); - - notifyScreenOffLocked(); + mLockPatternUtils.getPowerButtonInstantlyLocks(currentUser) + || !mLockPatternUtils.isSecure(currentUser); if (mExitSecureCallback != null) { if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled"); @@ -654,15 +660,40 @@ public class KeyguardViewMediator extends SystemUI { hideLocked(); } } else if (mShowing) { - resetStateLocked(); + mPendingReset = true; } else if (why == WindowManagerPolicy.OFF_BECAUSE_OF_TIMEOUT - || (why == WindowManagerPolicy.OFF_BECAUSE_OF_USER && !lockImmediately)) { + || (why == WindowManagerPolicy.OFF_BECAUSE_OF_USER && !lockImmediately)) { doKeyguardLaterLocked(); - } else { + } else if (!mLockPatternUtils.isLockScreenDisabled(currentUser)) { + mPendingLock = true; + } + + if (mPendingLock) { + playSounds(true); + } + } + } + + public void onFinishedGoingToSleep(int why) { + if (DEBUG) Log.d(TAG, "onFinishedGoingToSleep(" + why + ")"); + synchronized (this) { + mDeviceInteractive = false; + + resetKeyguardDonePendingLocked(); + mHideAnimationRun = false; + + notifyScreenOffLocked(); + + if (mPendingReset) { + resetStateLocked(); + mPendingReset = false; + } + if (mPendingLock) { doKeyguardLocked(null); + mPendingLock = false; } } - KeyguardUpdateMonitor.getInstance(mContext).dispatchScreenTurndOff(why); + KeyguardUpdateMonitor.getInstance(mContext).dispatchScreenTurnedOff(why); } private void doKeyguardLaterLocked() { @@ -684,7 +715,7 @@ public class KeyguardViewMediator extends SystemUI { // From DevicePolicyAdmin final long policyTimeout = mLockPatternUtils.getDevicePolicyManager() - .getMaximumTimeToLock(null, mLockPatternUtils.getCurrentUser()); + .getMaximumTimeToLock(null, KeyguardUpdateMonitor.getCurrentUser()); long timeout; if (policyTimeout > 0) { @@ -697,7 +728,6 @@ public class KeyguardViewMediator extends SystemUI { if (timeout <= 0) { // Lock now - mSuppressNextLockSound = true; doKeyguardLocked(null); } else { // Lock in the future @@ -717,13 +747,15 @@ public class KeyguardViewMediator extends SystemUI { } /** - * Let's us know the screen was turned on. + * Let's us know when the device is waking up. */ - public void onScreenTurnedOn(IKeyguardShowCallback callback) { + public void onStartedWakingUp(IKeyguardShowCallback callback) { + + // TODO: Rename all screen off/on references to interactive/sleeping synchronized (this) { - mScreenOn = true; + mDeviceInteractive = true; cancelDoKeyguardLaterLocked(); - if (DEBUG) Log.d(TAG, "onScreenTurnedOn, seq = " + mDelayedShowingSequence); + if (DEBUG) Log.d(TAG, "onStartedWakingUp, seq = " + mDelayedShowingSequence); if (callback != null) { notifyScreenOnLocked(callback); } @@ -733,7 +765,8 @@ public class KeyguardViewMediator extends SystemUI { } private void maybeSendUserPresentBroadcast() { - if (mSystemReady && mLockPatternUtils.isLockScreenDisabled()) { + if (mSystemReady && mLockPatternUtils.isLockScreenDisabled( + KeyguardUpdateMonitor.getCurrentUser())) { // 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() @@ -747,7 +780,8 @@ public class KeyguardViewMediator extends SystemUI { */ public void onDreamingStarted() { synchronized (this) { - if (mScreenOn && mLockPatternUtils.isSecure()) { + if (mDeviceInteractive + && mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())) { doKeyguardLaterLocked(); } } @@ -758,7 +792,7 @@ public class KeyguardViewMediator extends SystemUI { */ public void onDreamingStopped() { synchronized (this) { - if (mScreenOn) { + if (mDeviceInteractive) { cancelDoKeyguardLaterLocked(); } } @@ -892,6 +926,12 @@ public class KeyguardViewMediator extends SystemUI { */ private void handleSetOccluded(boolean isOccluded) { synchronized (KeyguardViewMediator.this) { + if (mHiding && isOccluded) { + // We're in the process of going away but WindowManager wants to show a + // SHOW_WHEN_LOCKED activity instead. + startKeyguardExitAnimation(0, 0); + } + if (mOccluded != isOccluded) { mOccluded = isOccluded; mStatusBarKeyguardViewManager.setOccluded(isOccluded); @@ -917,7 +957,7 @@ public class KeyguardViewMediator extends SystemUI { * was suppressed by an app that disabled the keyguard or we haven't been provisioned yet. */ public boolean isInputRestricted() { - return mShowing || mNeedToReshowWhenReenabled || shouldWaitForProvisioning(); + return mShowing || mNeedToReshowWhenReenabled; } private void updateInputRestricted() { @@ -929,13 +969,16 @@ public class KeyguardViewMediator extends SystemUI { boolean inputRestricted = isInputRestricted(); if (mInputRestricted != inputRestricted) { mInputRestricted = inputRestricted; - try { - int size = mKeyguardStateCallbacks.size(); - for (int i = 0; i < size; i++) { + int size = mKeyguardStateCallbacks.size(); + for (int i = size - 1; i >= 0; i--) { + try { mKeyguardStateCallbacks.get(i).onInputRestrictedStateChanged(inputRestricted); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onDeviceProvisioned", e); + if (e instanceof DeadObjectException) { + mKeyguardStateCallbacks.remove(i); + } } - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onDeviceProvisioned", e); } } } @@ -982,12 +1025,13 @@ public class KeyguardViewMediator extends SystemUI { return; } - if (mLockPatternUtils.isLockScreenDisabled() && !lockedOrMissing) { + if (mLockPatternUtils.isLockScreenDisabled(KeyguardUpdateMonitor.getCurrentUser()) + && !lockedOrMissing) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off"); return; } - if (mLockPatternUtils.checkVoldPassword()) { + if (mLockPatternUtils.checkVoldPassword(KeyguardUpdateMonitor.getCurrentUser())) { if (DEBUG) Log.d(TAG, "Not showing lock screen since just decrypted"); // Without this, settings is not enabled until the lock screen first appears setShowingLocked(false); @@ -1080,7 +1124,7 @@ public class KeyguardViewMediator extends SystemUI { } public boolean isSecure() { - return mLockPatternUtils.isSecure() + return mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser()) || KeyguardUpdateMonitor.getInstance(mContext).isSimPinSecure(); } @@ -1091,7 +1135,7 @@ public class KeyguardViewMediator extends SystemUI { * @param newUserId The id of the incoming user. */ public void setCurrentUser(int newUserId) { - mLockPatternUtils.setCurrentUser(newUserId); + KeyguardUpdateMonitor.setCurrentUser(newUserId); } private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @@ -1103,8 +1147,6 @@ public class KeyguardViewMediator extends SystemUI { + 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); } } @@ -1221,7 +1263,7 @@ public class KeyguardViewMediator extends SystemUI { private void sendUserPresentBroadcast() { synchronized (this) { if (mBootCompleted) { - final UserHandle currentUser = new UserHandle(mLockPatternUtils.getCurrentUser()); + final UserHandle currentUser = new UserHandle(KeyguardUpdateMonitor.getCurrentUser()); final UserManager um = (UserManager) mContext.getSystemService( Context.USER_SERVICE); List <UserInfo> userHandles = um.getProfiles(currentUser.getIdentifier()); @@ -1255,13 +1297,6 @@ public class KeyguardViewMediator extends SystemUI { } private void playSounds(boolean locked) { - // User feedback for keyguard. - - if (mSuppressNextLockSound) { - mSuppressNextLockSound = false; - return; - } - playSound(locked ? mLockSoundId : mUnlockSoundId); } @@ -1275,10 +1310,10 @@ public class KeyguardViewMediator extends SystemUI { if (mAudioManager == null) { mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); if (mAudioManager == null) return; - mMasterStreamType = mAudioManager.getMasterStreamType(); + mUiSoundsStreamType = mAudioManager.getUiSoundsStreamType(); } // If the stream is muted, don't play the sound - if (mAudioManager.isStreamMute(mMasterStreamType)) return; + if (mAudioManager.isStreamMute(mUiSoundsStreamType)) return; mLockSoundStreamId = mLockSounds.play(soundId, mLockSoundVolume, mLockSoundVolume, 1/*priortiy*/, 0/*loop*/, 1.0f/*rate*/); @@ -1286,9 +1321,6 @@ public class KeyguardViewMediator extends SystemUI { } private void playTrustedSound() { - if (mSuppressNextLockSound) { - return; - } playSound(mTrustedSoundId); } @@ -1321,9 +1353,6 @@ public class KeyguardViewMediator extends SystemUI { adjustStatusBarLocked(); userActivity(); - // Do this at the end to not slow down display of the keyguard. - playSounds(true); - mShowKeyguardWakeLock.release(); } mKeyguardDisplayManager.show(); @@ -1333,10 +1362,12 @@ public class KeyguardViewMediator extends SystemUI { @Override public void run() { try { + mStatusBarKeyguardViewManager.keyguardGoingAway(); + // Don't actually hide the Keyguard at the moment, wait for window // manager until it tells us it's safe to do so with // startKeyguardExitAnimation. - mWM.keyguardGoingAway( + ActivityManagerNative.getDefault().keyguardGoingAway( mStatusBarKeyguardViewManager.shouldDisableWindowAnimationsForUnlock(), mStatusBarKeyguardViewManager.isGoingToNotificationShade()); } catch (RemoteException e) { @@ -1531,13 +1562,16 @@ public class KeyguardViewMediator extends SystemUI { private void setShowingLocked(boolean showing) { if (showing != mShowing) { mShowing = showing; - try { - int size = mKeyguardStateCallbacks.size(); - for (int i = 0; i < size; i++) { + int size = mKeyguardStateCallbacks.size(); + for (int i = size - 1; i >= 0; i--) { + try { mKeyguardStateCallbacks.get(i).onShowingStateChanged(showing); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onShowingStateChanged", e); + if (e instanceof DeadObjectException) { + mKeyguardStateCallbacks.remove(i); + } } - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onShowingStateChanged", e); } updateInputRestrictedLocked(); mTrustManager.reportKeyguardShowingChanged(); @@ -1550,8 +1584,9 @@ public class KeyguardViewMediator extends SystemUI { try { callback.onSimSecureStateChanged(mUpdateMonitor.isSimPinSecure()); callback.onShowingStateChanged(mShowing); + callback.onInputRestrictedStateChanged(mInputRestricted); } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onShowingStateChanged or onSimSecureStateChanged", e); + Slog.w(TAG, "Failed to call onShowingStateChanged or onSimSecureStateChanged or onInputRestrictedStateChanged", e); } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java index c23f45d..2a84362 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java @@ -18,8 +18,6 @@ package com.android.systemui.media; import android.app.Activity; import android.app.AlertDialog; -import android.app.PendingIntent; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -32,16 +30,10 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; -import android.view.LayoutInflater; import android.view.WindowManager; import android.widget.CheckBox; import android.widget.CompoundButton; -import android.widget.TextView; - -import com.android.internal.app.AlertActivity; -import com.android.internal.app.AlertController; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.SystemUIDialog; public class MediaProjectionPermissionActivity extends Activity implements DialogInterface.OnClickListener, CheckBox.OnCheckedChangeListener, @@ -104,6 +96,7 @@ public class MediaProjectionPermissionActivity extends Activity .create(); mDialog.create(); + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); ((CheckBox) mDialog.findViewById(R.id.remember)).setOnCheckedChangeListener(this); mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index 4391bfc..a12a3f1 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -50,7 +50,6 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private static final boolean DEBUG = PowerUI.DEBUG; private static final String TAG_NOTIFICATION = "low_battery"; - private static final int ID_NOTIFICATION = 100; private static final int SHOWING_NOTHING = 0; private static final int SHOWING_WARNING = 1; @@ -145,7 +144,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { showSaverNotification(); mShowing = SHOWING_SAVER; } else { - mNoMan.cancelAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, UserHandle.ALL); + mNoMan.cancelAsUser(TAG_NOTIFICATION, R.id.notification_power, UserHandle.ALL); mShowing = SHOWING_NOTHING; } } @@ -160,13 +159,13 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { .setContentText(mContext.getString(R.string.invalid_charger_text)) .setPriority(Notification.PRIORITY_MAX) .setVisibility(Notification.VISIBILITY_PUBLIC) - .setColor(mContext.getResources().getColor( + .setColor(mContext.getColor( com.android.internal.R.color.system_notification_accent_color)); final Notification n = nb.build(); if (n.headsUpContentView != null) { n.headsUpContentView.setViewVisibility(com.android.internal.R.id.right_icon, View.GONE); } - mNoMan.notifyAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, n, UserHandle.ALL); + mNoMan.notifyAsUser(TAG_NOTIFICATION, R.id.notification_power, n, UserHandle.ALL); } private void showWarningNotification() { @@ -184,7 +183,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING)) .setPriority(Notification.PRIORITY_MAX) .setVisibility(Notification.VISIBILITY_PUBLIC) - .setColor(mContext.getResources().getColor( + .setColor(mContext.getColor( com.android.internal.R.color.battery_saver_mode_color)); if (hasBatterySettings()) { nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS)); @@ -204,7 +203,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { if (n.headsUpContentView != null) { n.headsUpContentView.setViewVisibility(com.android.internal.R.id.right_icon, View.GONE); } - mNoMan.notifyAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, n, UserHandle.ALL); + mNoMan.notifyAsUser(TAG_NOTIFICATION, R.id.notification_power, n, UserHandle.ALL); } private void showSaverNotification() { @@ -215,13 +214,13 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { .setOngoing(true) .setShowWhen(false) .setVisibility(Notification.VISIBILITY_PUBLIC) - .setColor(mContext.getResources().getColor( + .setColor(mContext.getColor( com.android.internal.R.color.battery_saver_mode_color)); addStopSaverAction(nb); if (hasSaverSettings()) { nb.setContentIntent(pendingActivity(mOpenSaverSettings)); } - mNoMan.notifyAsUser(TAG_NOTIFICATION, ID_NOTIFICATION, nb.build(), UserHandle.ALL); + mNoMan.notifyAsUser(TAG_NOTIFICATION, R.id.notification_power, nb.build(), UserHandle.ALL); } private void addStopSaverAction(Notification.Builder nb) { @@ -377,7 +376,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { filter.addAction(ACTION_START_SAVER); filter.addAction(ACTION_STOP_SAVER); filter.addAction(ACTION_DISMISSED_WARNING); - mContext.registerReceiverAsUser(this, UserHandle.ALL, filter, null, mHandler); + mContext.registerReceiverAsUser(this, UserHandle.ALL, filter, + android.Manifest.permission.STATUS_BAR_SERVICE, mHandler); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/DataUsageGraph.java b/packages/SystemUI/src/com/android/systemui/qs/DataUsageGraph.java index d55ceaa..aff5d2b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/DataUsageGraph.java +++ b/packages/SystemUI/src/com/android/systemui/qs/DataUsageGraph.java @@ -44,10 +44,10 @@ public class DataUsageGraph extends View { public DataUsageGraph(Context context, AttributeSet attrs) { super(context, attrs); final Resources res = context.getResources(); - mTrackColor = res.getColor(R.color.data_usage_graph_track); - mUsageColor = res.getColor(R.color.system_accent_color); - mOverlimitColor = res.getColor(R.color.system_warning_color); - mWarningColor = res.getColor(R.color.data_usage_graph_warning); + mTrackColor = context.getColor(R.color.data_usage_graph_track); + mUsageColor = context.getColor(R.color.system_accent_color); + mOverlimitColor = context.getColor(R.color.system_warning_color); + mWarningColor = context.getColor(R.color.data_usage_graph_warning); mMarkerWidth = res.getDimensionPixelSize(R.dimen.data_usage_graph_marker_width); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java index 111484b..a318efc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java @@ -42,14 +42,21 @@ public class QSDetailClipper { } final int w = mDetail.getWidth() - x; final int h = mDetail.getHeight() - y; + int innerR = 0; + if (x < 0 || w < 0 || y < 0 || h < 0) { + innerR = Math.abs(x); + innerR = Math.min(innerR, Math.abs(y)); + innerR = Math.min(innerR, Math.abs(w)); + innerR = Math.min(innerR, Math.abs(h)); + } int r = (int) Math.ceil(Math.sqrt(x * x + y * y)); r = (int) Math.max(r, Math.ceil(Math.sqrt(w * w + y * y))); r = (int) Math.max(r, Math.ceil(Math.sqrt(w * w + h * h))); r = (int) Math.max(r, Math.ceil(Math.sqrt(x * x + h * h))); if (in) { - mAnimator = ViewAnimationUtils.createCircularReveal(mDetail, x, y, 0, r); + mAnimator = ViewAnimationUtils.createCircularReveal(mDetail, x, y, innerR, r); } else { - mAnimator = ViewAnimationUtils.createCircularReveal(mDetail, x, y, r, 0); + mAnimator = ViewAnimationUtils.createCircularReveal(mDetail, x, y, r, innerR); } mAnimator.setDuration((long)(mAnimator.getDuration() * 1.5)); if (listener != null) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java index 95ac558..25b9105 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java @@ -193,8 +193,6 @@ public class QSDetailItems extends FrameLayout { title.setMaxLines(twoLines ? 1 : 2); summary.setVisibility(twoLines ? VISIBLE : GONE); summary.setText(twoLines ? item.line2 : null); - view.setMinimumHeight(mContext.getResources() .getDimensionPixelSize( - twoLines ? R.dimen.qs_detail_item_height_twoline : R.dimen.qs_detail_item_height)); view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -239,8 +237,8 @@ public class QSDetailItems extends FrameLayout { public static class Item { public int icon; public Drawable overlay; - public String line1; - public String line2; + public CharSequence line1; + public CharSequence line2; public Object tag; public boolean canDisconnect; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java index a0b6e82..ca38528 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java @@ -18,12 +18,12 @@ package com.android.systemui.qs; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.content.res.Configuration; +import android.content.Intent; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.UserHandle; import android.util.Log; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -40,6 +40,8 @@ public class QSFooter implements OnClickListener, DialogInterface.OnClickListene protected static final String TAG = "QSFooter"; protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String ACTION_VPN_SETTINGS = "android.net.vpn.SETTINGS"; + private final View mRootView; private final TextView mFooterText; private final ImageView mFooterIcon; @@ -108,21 +110,13 @@ public class QSFooter implements OnClickListener, DialogInterface.OnClickListene } private void handleRefreshState() { + mIsIconVisible = mSecurityController.isVpnEnabled(); if (mSecurityController.hasDeviceOwner()) { mFooterTextId = R.string.device_owned_footer; mIsVisible = true; - mIsIconVisible = false; - } else if (mSecurityController.hasProfileOwner()) { - mFooterTextId = R.string.profile_owned_footer; - mIsVisible = true; - mIsIconVisible = false; - } else if (mSecurityController.isVpnEnabled()) { - mFooterTextId = R.string.vpn_footer; - mIsVisible = true; - mIsIconVisible = true; } else { - mIsVisible = false; - mIsIconVisible = false; + mFooterTextId = R.string.vpn_footer; + mIsVisible = mIsIconVisible; } mMainHandler.post(mUpdateDisplayState); } @@ -130,14 +124,21 @@ public class QSFooter implements OnClickListener, DialogInterface.OnClickListene @Override public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_NEGATIVE) { - mSecurityController.disconnectFromVpn(); + final Intent settingsIntent = new Intent(ACTION_VPN_SETTINGS); + mContext.startActivityAsUser(settingsIntent, UserHandle.CURRENT); } } private void createDialog() { + String deviceOwner = mSecurityController.getDeviceOwnerName(); + String profileOwner = mSecurityController.getProfileOwnerName(); + String primaryVpn = mSecurityController.getPrimaryVpnName(); + String profileVpn = mSecurityController.getProfileVpnName(); + boolean managed = mSecurityController.hasProfileOwner(); + mDialog = new SystemUIDialog(mContext); - mDialog.setTitle(getTitle()); - mDialog.setMessage(getMessage()); + mDialog.setTitle(getTitle(deviceOwner)); + mDialog.setMessage(getMessage(deviceOwner, profileOwner, primaryVpn, profileVpn, managed)); mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this); if (mSecurityController.isVpnEnabled()) { mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getNegativeButton(), this); @@ -146,95 +147,49 @@ public class QSFooter implements OnClickListener, DialogInterface.OnClickListene } private String getNegativeButton() { - if (mSecurityController.isLegacyVpn()) { - return mContext.getString(R.string.disconnect_vpn); - } else { - return mContext.getString(R.string.disable_vpn); - } + return mContext.getString(R.string.status_bar_settings_settings_button); } private String getPositiveButton() { return mContext.getString(R.string.quick_settings_done); } - private String getMessage() { - if (mSecurityController.hasDeviceOwner()) { - if (mSecurityController.hasProfileOwner()) { - if (mSecurityController.isVpnEnabled()) { - if (mSecurityController.isLegacyVpn()) { - return mContext.getString( - R.string.monitoring_description_legacy_vpn_device_and_profile_owned, - mSecurityController.getDeviceOwnerName(), - mSecurityController.getProfileOwnerName(), - mSecurityController.getLegacyVpnName()); - } else { - return mContext.getString( - R.string.monitoring_description_vpn_device_and_profile_owned, - mSecurityController.getDeviceOwnerName(), - mSecurityController.getProfileOwnerName(), - mSecurityController.getVpnApp()); - } - } else { - return mContext.getString( - R.string.monitoring_description_device_and_profile_owned, - mSecurityController.getDeviceOwnerName(), - mSecurityController.getProfileOwnerName()); - } + private String getMessage(String deviceOwner, String profileOwner, String primaryVpn, + String profileVpn, boolean primaryUserIsManaged) { + if (deviceOwner != null) { + if (primaryVpn != null) { + return mContext.getString(R.string.monitoring_description_vpn_app_device_owned, + deviceOwner, primaryVpn); } else { - if (mSecurityController.isVpnEnabled()) { - if (mSecurityController.isLegacyVpn()) { - return mContext.getString( - R.string.monitoring_description_legacy_vpn_device_owned, - mSecurityController.getDeviceOwnerName(), - mSecurityController.getLegacyVpnName()); - } else { - return mContext.getString(R.string.monitoring_description_vpn_device_owned, - mSecurityController.getDeviceOwnerName(), - mSecurityController.getVpnApp()); - } - } else { - return mContext.getString(R.string.monitoring_description_device_owned, - mSecurityController.getDeviceOwnerName()); - } + return mContext.getString(R.string.monitoring_description_device_owned, + deviceOwner); } - } else if (mSecurityController.hasProfileOwner()) { - if (mSecurityController.isVpnEnabled()) { - if (mSecurityController.isLegacyVpn()) { - return mContext.getString( - R.string.monitoring_description_legacy_vpn_profile_owned, - mSecurityController.getProfileOwnerName(), - mSecurityController.getLegacyVpnName()); - } else { - return mContext.getString( - R.string.monitoring_description_vpn_profile_owned, - mSecurityController.getProfileOwnerName(), - mSecurityController.getVpnApp()); - } + } else if (primaryVpn != null) { + if (profileVpn != null) { + return mContext.getString(R.string.monitoring_description_app_personal_work, + profileOwner, profileVpn, primaryVpn); } else { - return mContext.getString( - R.string.monitoring_description_profile_owned, - mSecurityController.getProfileOwnerName()); + return mContext.getString(R.string.monitoring_description_app_personal, + primaryVpn); } + } else if (profileVpn != null) { + return mContext.getString(R.string.monitoring_description_app_work, + profileOwner, profileVpn); + } else if (profileOwner != null && primaryUserIsManaged) { + return mContext.getString(R.string.monitoring_description_device_owned, + profileOwner); } else { - if (mSecurityController.isLegacyVpn()) { - return mContext.getString(R.string.monitoring_description_legacy_vpn, - mSecurityController.getLegacyVpnName()); - - } else { - return mContext.getString(R.string.monitoring_description_vpn, - mSecurityController.getVpnApp()); - } + // No device owner, no personal VPN, no work VPN, no user owner. Why are we here? + return null; } } - private int getTitle() { - if (mSecurityController.hasDeviceOwner()) { + private int getTitle(String deviceOwner) { + if (deviceOwner != null) { return R.string.monitoring_title_device_owned; + } else { + return R.string.monitoring_title; } - if (mSecurityController.hasProfileOwner()) { - return R.string.monitoring_title_profile_owned; - } - return R.string.monitoring_title; } private final Runnable mUpdateDisplayState = new Runnable() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 4dacacf..2ded919 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -26,7 +26,6 @@ import android.content.res.Resources; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -34,6 +33,7 @@ import android.view.accessibility.AccessibilityEvent; import android.widget.ImageView; import android.widget.TextView; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; import com.android.systemui.qs.QSTile.DetailAdapter; @@ -50,12 +50,12 @@ public class QSPanel extends ViewGroup { private static final float TILE_ASPECT = 1.2f; private final Context mContext; - private final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>(); + protected final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>(); private final View mDetail; private final ViewGroup mDetailContent; private final TextView mDetailSettingsButton; private final TextView mDetailDoneButton; - private final View mBrightnessView; + protected final View mBrightnessView; private final QSDetailClipper mClipper; private final H mHandler = new H(); @@ -111,6 +111,8 @@ public class QSPanel extends ViewGroup { mDetailDoneButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { + announceForAccessibility( + mContext.getString(R.string.accessibility_desc_quick_settings)); closeDetail(); } }); @@ -183,8 +185,11 @@ public class QSPanel extends ViewGroup { public void setExpanded(boolean expanded) { if (mExpanded == expanded) return; mExpanded = expanded; + MetricsLogger.visibility(mContext, MetricsLogger.QS_PANEL, mExpanded); if (!mExpanded) { closeDetail(); + } else { + logTiles(); } } @@ -212,9 +217,19 @@ public class QSPanel extends ViewGroup { mFooter.refreshState(); } - public void showDetailAdapter(boolean show, DetailAdapter adapter) { + public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) { + int xInWindow = locationInWindow[0]; + int yInWindow = locationInWindow[1]; + mDetail.getLocationInWindow(locationInWindow); + Record r = new Record(); r.detailAdapter = adapter; + r.x = xInWindow - locationInWindow[0]; + r.y = yInWindow - locationInWindow[1]; + + locationInWindow[0] = xInWindow; + locationInWindow[1] = yInWindow; + showDetail(show, r); } @@ -227,6 +242,9 @@ public class QSPanel extends ViewGroup { } private void handleSetTileVisibility(View v, int visibility) { + if (visibility == VISIBLE && !mGridContentVisible) { + visibility = INVISIBLE; + } if (visibility == v.getVisibility()) return; v.setVisibility(visibility); } @@ -244,6 +262,12 @@ public class QSPanel extends ViewGroup { } } + private void drawTile(TileRecord r, QSTile.State state) { + final int visibility = state.visible ? VISIBLE : GONE; + setTileVisibility(r.tileView, visibility); + r.tileView.onStateChanged(state); + } + private void addTile(final QSTile<?> tile) { final TileRecord r = new TileRecord(); r.tile = tile; @@ -252,15 +276,9 @@ public class QSPanel extends ViewGroup { final QSTile.Callback callback = new QSTile.Callback() { @Override public void onStateChanged(QSTile.State state) { - int visibility = state.visible ? VISIBLE : GONE; - if (state.visible && !mGridContentVisible) { - - // We don't want to show it if the content is hidden, - // then we just set it to invisible, to ensure that it gets visible again - visibility = INVISIBLE; + if (!r.openingDetail) { + drawTile(r, state); } - setTileVisibility(r.tileView, visibility); - r.tileView.onStateChanged(state); } @Override public void onShowDetail(boolean show) { @@ -334,24 +352,32 @@ public class QSPanel extends ViewGroup { if (r instanceof TileRecord) { handleShowDetailTile((TileRecord) r, show); } else { - handleShowDetailImpl(r, show, getWidth() /* x */, 0/* y */); + int x = 0; + int y = 0; + if (r != null) { + x = r.x; + y = r.y; + } + handleShowDetailImpl(r, show, x, y); } } private void handleShowDetailTile(TileRecord r, boolean show) { - if ((mDetailRecord != null) == show) return; + if ((mDetailRecord != null) == show && mDetailRecord == r) return; if (show) { r.detailAdapter = r.tile.getDetailAdapter(); if (r.detailAdapter == null) return; } + r.tile.setDetailListening(show); int x = r.tileView.getLeft() + r.tileView.getWidth() / 2; int y = r.tileView.getTop() + r.tileView.getHeight() / 2; handleShowDetailImpl(r, show, x, y); } private void handleShowDetailImpl(Record r, boolean show, int x, int y) { - if ((mDetailRecord != null) == show) return; // already in right state + boolean visibleDiff = (mDetailRecord != null) != show; + if (!visibleDiff && mDetailRecord == r) return; // already in right state DetailAdapter detailAdapter = null; AnimatorListener listener = null; if (show) { @@ -364,16 +390,26 @@ public class QSPanel extends ViewGroup { mDetailSettingsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - mHost.startSettingsActivity(settingsIntent); + mHost.startActivityDismissingKeyguard(settingsIntent); } }); mDetailContent.removeAllViews(); mDetail.bringToFront(); mDetailContent.addView(r.detailView); + MetricsLogger.visible(mContext, detailAdapter.getMetricsCategory()); + announceForAccessibility(mContext.getString( + R.string.accessibility_quick_settings_detail, + mContext.getString(detailAdapter.getTitle()))); setDetailRecord(r); listener = mHideGridContentWhenDone; + if (r instanceof TileRecord && visibleDiff) { + ((TileRecord) r).openingDetail = true; + } } else { + if (mDetailRecord != null) { + MetricsLogger.hidden(mContext, mDetailRecord.detailAdapter.getMetricsCategory()); + } mClosingDetail = true; setGridContentVisibility(true); listener = mTeardownDetailWhenDone; @@ -381,7 +417,9 @@ public class QSPanel extends ViewGroup { } sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); fireShowingDetail(show ? detailAdapter : null); - mClipper.animateCircularClip(x, y, show, listener); + if (visibleDiff) { + mClipper.animateCircularClip(x, y, show, listener); + } } private void setGridContentVisibility(boolean visible) { @@ -393,9 +431,21 @@ public class QSPanel extends ViewGroup { } } mBrightnessView.setVisibility(newVis); + if (mGridContentVisible != visible) { + MetricsLogger.visibility(mContext, MetricsLogger.QS_PANEL, newVis); + } mGridContentVisible = visible; } + private void logTiles() { + for (int i = 0; i < mRecords.size(); i++) { + TileRecord tileRecord = mRecords.get(i); + if (tileRecord.tile.getState().visible) { + MetricsLogger.visible(mContext, tileRecord.tile.getMetricsCategory()); + } + } + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int width = MeasureSpec.getSize(widthMeasureSpec); @@ -422,6 +472,7 @@ public class QSPanel extends ViewGroup { rows = r + 1; } + View previousView = mBrightnessView; for (TileRecord record : mRecords) { if (record.tileView.setDual(record.tile.supportsDualTargets())) { record.tileView.handleStateChanged(record.tile.getState()); @@ -430,6 +481,7 @@ public class QSPanel extends ViewGroup { final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth; final int ch = record.row == 0 ? mLargeCellHeight : mCellHeight; record.tileView.measure(exactly(cw), exactly(ch)); + previousView = record.tileView.updateAccessibilityOrder(previousView); } int h = rows == 0 ? brightnessHeight : (getRowTop(rows) + mPanelPaddingBottom); if (mFooter.hasFooter()) { @@ -537,14 +589,17 @@ public class QSPanel extends ViewGroup { private static class Record { View detailView; DetailAdapter detailAdapter; + int x; + int y; } - private static final class TileRecord extends Record { - QSTile<?> tile; - QSTileView tileView; - int row; - int col; - boolean scanState; + protected static final class TileRecord extends Record { + public QSTile<?> tile; + public QSTileView tileView; + public int row; + public int col; + public boolean scanState; + public boolean openingDetail; } private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() { @@ -560,6 +615,7 @@ public class QSPanel extends ViewGroup { // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get // called, this will avoid accidentally turning off the grid when we don't want to. animation.removeListener(this); + redrawTile(); }; @Override @@ -567,6 +623,15 @@ public class QSPanel extends ViewGroup { // Only hide content if still in detail state. if (mDetailRecord != null) { setGridContentVisibility(false); + redrawTile(); + } + } + + private void redrawTile() { + if (mDetailRecord instanceof TileRecord) { + final TileRecord tileRecord = (TileRecord) mDetailRecord; + tileRecord.openingDetail = false; + drawTile(tileRecord, tileRecord.tile.getState()); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java index 1790a4e..38fade2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java @@ -18,6 +18,7 @@ package com.android.systemui.qs; import android.content.Context; import android.content.Intent; +import android.graphics.drawable.Animatable; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; @@ -32,12 +33,12 @@ import com.android.systemui.qs.QSTile.State; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.FlashlightController; +import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.Listenable; import com.android.systemui.statusbar.policy.LocationController; import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.RotationLockController; -import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.ZenModeController; import java.util.Collection; @@ -68,6 +69,15 @@ public abstract class QSTile<TState extends State> implements Listenable { abstract protected void handleClick(); abstract protected void handleUpdateState(TState state, Object arg); + /** + * Declare the category of this tile. + * + * Categories are defined in {@link com.android.internal.logging.MetricsLogger} + * or if there is no relevant existing category you may define one in + * {@link com.android.systemui.qs.QSTile}. + */ + abstract public int getMetricsCategory(); + protected QSTile(Host host) { mHost = host; mContext = host.getContext(); @@ -96,6 +106,7 @@ public abstract class QSTile<TState extends State> implements Listenable { View createDetailView(Context context, View convertView, ViewGroup parent); Intent getSettingsIntent(); void setToggleState(boolean state); + int getMetricsCategory(); } // safe to call from any thread @@ -148,6 +159,10 @@ public abstract class QSTile<TState extends State> implements Listenable { return mState; } + public void setDetailListening(boolean listening) { + // optional + } + // call only on tile worker looper private void handleSetCallback(Callback callback) { @@ -291,7 +306,7 @@ public abstract class QSTile<TState extends State> implements Listenable { } public interface Host { - void startSettingsActivity(Intent intent); + void startActivityDismissingKeyguard(Intent intent); void warn(String message, Throwable t); void collapsePanels(); Looper getLooper(); @@ -325,7 +340,7 @@ public abstract class QSTile<TState extends State> implements Listenable { public static class ResourceIcon extends Icon { private static final SparseArray<Icon> ICONS = new SparseArray<Icon>(); - private final int mResId; + protected final int mResId; private ResourceIcon(int resId) { mResId = resId; @@ -342,7 +357,11 @@ public abstract class QSTile<TState extends State> implements Listenable { @Override public Drawable getDrawable(Context context) { - return context.getDrawable(mResId); + Drawable d = context.getDrawable(mResId); + if (d instanceof Animatable) { + ((Animatable) d).start(); + } + return d; } @Override @@ -370,7 +389,7 @@ public abstract class QSTile<TState extends State> implements Listenable { @Override public Drawable getDrawable(Context context) { // workaround: get a clean state for every new AVD - final AnimatedVectorDrawable d = (AnimatedVectorDrawable) super.getDrawable(context) + final AnimatedVectorDrawable d = (AnimatedVectorDrawable) context.getDrawable(mResId) .getConstantState().newDrawable(); d.start(); if (mAllowAnimation) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java index 16ae6b4..6d26a3b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java @@ -38,6 +38,7 @@ import android.widget.TextView; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.qs.QSTile.AnimationIcon; import com.android.systemui.qs.QSTile.State; import java.util.Objects; @@ -82,20 +83,21 @@ public class QSTileView extends ViewGroup { setClipChildren(false); mTopBackgroundView = new View(context); + mTopBackgroundView.setId(View.generateViewId()); addView(mTopBackgroundView); mIcon = createIcon(); addView(mIcon); mDivider = new View(mContext); - mDivider.setBackgroundColor(res.getColor(R.color.qs_tile_divider)); + mDivider.setBackgroundColor(context.getColor(R.color.qs_tile_divider)); final int dh = res.getDimensionPixelSize(R.dimen.qs_tile_divider_height); mDivider.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, dh)); addView(mDivider); setClickable(true); - updateTopPadding(); + setId(View.generateViewId()); } private void updateTopPadding() { @@ -136,10 +138,10 @@ public class QSTileView extends ViewGroup { final Resources res = mContext.getResources(); if (mDual) { mDualLabel = new QSDualTileLabel(mContext); - mDualLabel.setId(android.R.id.title); + mDualLabel.setId(View.generateViewId()); mDualLabel.setBackgroundResource(R.drawable.btn_borderless_rect); - mDualLabel.setFirstLineCaret(res.getDrawable(R.drawable.qs_dual_tile_caret)); - mDualLabel.setTextColor(res.getColor(R.color.qs_tile_text)); + mDualLabel.setFirstLineCaret(mContext.getDrawable(R.drawable.qs_dual_tile_caret)); + mDualLabel.setTextColor(mContext.getColor(R.color.qs_tile_text)); mDualLabel.setPadding(0, mDualTileVerticalPaddingPx, 0, mDualTileVerticalPaddingPx); mDualLabel.setTypeface(CONDENSED); mDualLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, @@ -154,10 +156,10 @@ public class QSTileView extends ViewGroup { mDualLabel.setContentDescription(labelDescription); } addView(mDualLabel); + mDualLabel.setAccessibilityTraversalAfter(mTopBackgroundView.getId()); } else { mLabel = new TextView(mContext); - mLabel.setId(android.R.id.title); - mLabel.setTextColor(res.getColor(R.color.qs_tile_text)); + mLabel.setTextColor(mContext.getColor(R.color.qs_tile_text)); mLabel.setGravity(Gravity.CENTER_HORIZONTAL); mLabel.setMinLines(2); mLabel.setPadding(0, 0, 0, 0); @@ -315,8 +317,9 @@ public class QSTileView extends ViewGroup { iv.setImageDrawable(d); iv.setTag(R.id.qs_icon_tag, state.icon); if (d instanceof Animatable) { - if (!iv.isShown()) { - ((Animatable) d).stop(); // skip directly to end state + Animatable a = (Animatable) d; + if (state.icon instanceof AnimationIcon && !iv.isShown()) { + a.stop(); // skip directly to end state } } } @@ -326,6 +329,26 @@ public class QSTileView extends ViewGroup { mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget(); } + /** + * Update the accessibility order for this view. + * + * @param previousView the view which should be before this one + * @return the last view in this view which is accessible + */ + public View updateAccessibilityOrder(View previousView) { + View firstView; + View lastView; + if (mDual) { + lastView = mDualLabel; + firstView = mTopBackgroundView; + } else { + firstView = this; + lastView = this; + } + firstView.setAccessibilityTraversalAfter(previousView.getId()); + return lastView; + } + private class H extends Handler { private static final int STATE_CHANGED = 1; public H() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java b/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java index e60aa53..f36019b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java +++ b/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.Listenable; @@ -32,14 +33,15 @@ public class UsageTracker implements Listenable { private final Context mContext; private final long mTimeToShowTile; - private final String mPrefKey; + @Prefs.Key private final String mPrefKey; private final String mResetAction; private boolean mRegistered; - public UsageTracker(Context context, Class<?> tile, int timeoutResource) { + public UsageTracker(Context context, @Prefs.Key String prefKey, Class<?> tile, + int timeoutResource) { mContext = context; - mPrefKey = tile.getSimpleName() + "LastUsed"; + mPrefKey = prefKey; mTimeToShowTile = MILLIS_PER_DAY * mContext.getResources().getInteger(timeoutResource); mResetAction = "com.android.systemui.qs." + tile.getSimpleName() + ".usage_reset"; } @@ -56,16 +58,16 @@ public class UsageTracker implements Listenable { } public boolean isRecentlyUsed() { - long lastUsed = getSharedPrefs().getLong(mPrefKey, 0); + long lastUsed = Prefs.getLong(mContext, mPrefKey, 0L /* defaultValue */); return (System.currentTimeMillis() - lastUsed) < mTimeToShowTile; } public void trackUsage() { - getSharedPrefs().edit().putLong(mPrefKey, System.currentTimeMillis()).commit(); + Prefs.putLong(mContext, mPrefKey, System.currentTimeMillis()); } public void reset() { - getSharedPrefs().edit().remove(mPrefKey).commit(); + Prefs.remove(mContext, mPrefKey); } public void showResetConfirmation(String title, final Runnable onConfirmed) { @@ -87,10 +89,6 @@ public class UsageTracker implements Listenable { d.show(); } - private SharedPreferences getSharedPrefs() { - return mContext.getSharedPreferences(mContext.getPackageName(), 0); - } - private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java index 2dd02a5..49f8d1c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java @@ -23,6 +23,7 @@ import android.content.IntentFilter; import android.net.ConnectivityManager; import android.provider.Settings.Global; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.qs.GlobalSetting; import com.android.systemui.qs.QSTile; @@ -55,6 +56,7 @@ public class AirplaneModeTile extends QSTile<QSTile.BooleanState> { @Override public void handleClick() { + MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); setEnabled(!mState.value); mEnable.setAllowAnimation(true); mDisable.setAllowAnimation(true); @@ -72,7 +74,7 @@ public class AirplaneModeTile extends QSTile<QSTile.BooleanState> { final boolean airplaneMode = value != 0; state.value = airplaneMode; state.visible = true; - state.label = mContext.getString(R.string.quick_settings_airplane_mode_label); + state.label = mContext.getString(R.string.airplane_mode); if (airplaneMode) { state.icon = mEnable; state.contentDescription = mContext.getString( @@ -85,6 +87,11 @@ public class AirplaneModeTile extends QSTile<QSTile.BooleanState> { } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_AIRPLANEMODE; + } + + @Override protected String composeChangeAnnouncement() { if (mState.value) { return mContext.getString(R.string.accessibility_quick_settings_airplane_changed_on); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java index c15566f..abce31f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -16,6 +16,8 @@ package com.android.systemui.qs.tiles; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.Intent; import android.provider.Settings; @@ -23,13 +25,15 @@ import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; +import com.android.internal.logging.MetricsLogger; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.systemui.R; import com.android.systemui.qs.QSDetailItems; import com.android.systemui.qs.QSDetailItems.Item; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.BluetoothController; -import com.android.systemui.statusbar.policy.BluetoothController.PairedDevice; +import java.util.Collection; import java.util.Set; /** Quick settings tile: Bluetooth **/ @@ -72,6 +76,7 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { final boolean isEnabled = (Boolean)mState.value; + MetricsLogger.action(mContext, getMetricsCategory(), !isEnabled); mController.setBluetoothEnabled(!isEnabled); } @@ -129,6 +134,11 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_BLUETOOTH; + } + + @Override protected String composeChangeAnnouncement() { if (mState.value) { return mContext.getString(R.string.accessibility_quick_settings_bluetooth_changed_on); @@ -139,11 +149,12 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { private final BluetoothController.Callback mCallback = new BluetoothController.Callback() { @Override - public void onBluetoothStateChange(boolean enabled, boolean connecting) { + public void onBluetoothStateChange(boolean enabled) { refreshState(); } + @Override - public void onBluetoothPairedDevicesChanged() { + public void onBluetoothDevicesChanged() { mUiHandler.post(new Runnable() { @Override public void run() { @@ -174,11 +185,17 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { @Override public void setToggleState(boolean state) { + MetricsLogger.action(mContext, MetricsLogger.QS_BLUETOOTH_TOGGLE, state); mController.setBluetoothEnabled(state); showDetail(false); } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_BLUETOOTH_DETAILS; + } + + @Override public View createDetailView(Context context, View convertView, ViewGroup parent) { mItems = QSDetailItems.convertOrInflate(context, convertView, parent); mItems.setTagSuffix("Bluetooth"); @@ -199,19 +216,21 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { private void updateItems() { if (mItems == null) return; Item[] items = null; - final Set<PairedDevice> devices = mController.getPairedDevices(); + final Collection<CachedBluetoothDevice> devices = mController.getDevices(); if (devices != null) { - items = new Item[devices.size()]; + items = new Item[getBondedCount(devices)]; int i = 0; - for (PairedDevice device : devices) { + for (CachedBluetoothDevice device : devices) { + if (device.getBondState() == BluetoothDevice.BOND_NONE) continue; final Item item = new Item(); item.icon = R.drawable.ic_qs_bluetooth_on; - item.line1 = device.name; - if (device.state == PairedDevice.STATE_CONNECTED) { + item.line1 = device.getName(); + int state = device.getMaxConnectionState(); + if (state == BluetoothProfile.STATE_CONNECTED) { item.icon = R.drawable.ic_qs_bluetooth_connected; item.line2 = mContext.getString(R.string.quick_settings_connected); item.canDisconnect = true; - } else if (device.state == PairedDevice.STATE_CONNECTING) { + } else if (state == BluetoothProfile.STATE_CONNECTING) { item.icon = R.drawable.ic_qs_bluetooth_connecting; item.line2 = mContext.getString(R.string.quick_settings_connecting); } @@ -222,11 +241,22 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { mItems.setItems(items); } + private int getBondedCount(Collection<CachedBluetoothDevice> devices) { + int ct = 0; + for (CachedBluetoothDevice device : devices) { + if (device.getBondState() != BluetoothDevice.BOND_NONE) { + ct++; + } + } + return ct; + } + @Override public void onDetailItemClick(Item item) { if (item == null || item.tag == null) return; - final PairedDevice device = (PairedDevice) item.tag; - if (device != null && device.state == PairedDevice.STATE_DISCONNECTED) { + final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; + if (device != null && device.getMaxConnectionState() + == BluetoothProfile.STATE_DISCONNECTED) { mController.connect(device); } } @@ -234,7 +264,7 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { @Override public void onDetailItemDisconnect(Item item) { if (item == null || item.tag == null) return; - final PairedDevice device = (PairedDevice) item.tag; + final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; if (device != null) { mController.disconnect(device); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index 5bf6fb5..937615a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -24,6 +24,7 @@ import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.qs.QSDetailItems; import com.android.systemui.qs.QSDetailItems.Item; @@ -85,12 +86,13 @@ public class CastTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { + MetricsLogger.action(mContext, getMetricsCategory()); showDetail(true); } @Override protected void handleUpdateState(BooleanState state, Object arg) { - state.visible = !(mKeyguard.isSecure() && mKeyguard.isShowing()); + state.visible = !(mKeyguard.isSecure() && mKeyguard.isShowing() && !mKeyguard.isTrusted()); state.label = mContext.getString(R.string.quick_settings_cast_title); state.value = false; state.autoMirrorDrawable = false; @@ -113,6 +115,11 @@ public class CastTile extends QSTile<QSTile.BooleanState> { } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_CAST; + } + + @Override protected String composeChangeAnnouncement() { if (!mState.value) { // We only announce when it's turned off to avoid vocal overflow. @@ -164,6 +171,11 @@ public class CastTile extends QSTile<QSTile.BooleanState> { } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_CAST_DETAILS; + } + + @Override public View createDetailView(Context context, View convertView, ViewGroup parent) { mItems = QSDetailItems.convertOrInflate(context, convertView, parent); mItems.setTagSuffix("Cast"); @@ -234,6 +246,7 @@ public class CastTile extends QSTile<QSTile.BooleanState> { @Override public void onDetailItemClick(Item item) { if (item == null || item.tag == null) return; + MetricsLogger.action(mContext, MetricsLogger.QS_CAST_SELECT); final CastDevice device = (CastDevice) item.tag; mController.startCasting(device); } @@ -241,6 +254,7 @@ public class CastTile extends QSTile<QSTile.BooleanState> { @Override public void onDetailItemDisconnect(Item item) { if (item == null || item.tag == null) return; + MetricsLogger.action(mContext, MetricsLogger.QS_CAST_DISCONNECT); final CastDevice device = (CastDevice) item.tag; mController.stopCasting(device); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java index 30f92b9..07406b9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java @@ -24,14 +24,17 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QSTileView; import com.android.systemui.qs.SignalTileView; import com.android.systemui.statusbar.policy.NetworkController; +import com.android.systemui.statusbar.policy.NetworkController.IconState; import com.android.systemui.statusbar.policy.NetworkController.MobileDataController; import com.android.systemui.statusbar.policy.NetworkController.MobileDataController.DataUsageInfo; -import com.android.systemui.statusbar.policy.NetworkController.NetworkSignalChangedCallback; +import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; +import com.android.systemui.statusbar.policy.SignalCallbackAdapter; /** Quick settings tile: Cellular **/ public class CellularTile extends QSTile<QSTile.SignalState> { @@ -62,9 +65,9 @@ public class CellularTile extends QSTile<QSTile.SignalState> { @Override public void setListening(boolean listening) { if (listening) { - mController.addNetworkSignalChangedCallback(mCallback); + mController.addSignalCallback(mSignalCallback); } else { - mController.removeNetworkSignalChangedCallback(mCallback); + mController.removeSignalCallback(mSignalCallback); } } @@ -75,10 +78,11 @@ public class CellularTile extends QSTile<QSTile.SignalState> { @Override protected void handleClick() { + MetricsLogger.action(mContext, getMetricsCategory()); if (mDataController.isMobileDataSupported()) { showDetail(true); } else { - mHost.startSettingsActivity(CELLULAR_SETTINGS); + mHost.startActivityDismissingKeyguard(CELLULAR_SETTINGS); } } @@ -118,6 +122,11 @@ public class CellularTile extends QSTile<QSTile.SignalState> { state.label); } + @Override + public int getMetricsCategory() { + return MetricsLogger.QS_CELLULAR; + } + // Remove the period from the network name public static String removeTrailingPeriod(String string) { if (string == null) return null; @@ -131,7 +140,6 @@ public class CellularTile extends QSTile<QSTile.SignalState> { private static final class CallbackInfo { boolean enabled; boolean wifiEnabled; - boolean wifiConnected; boolean airplaneModeEnabled; int mobileSignalIconId; String signalContentDescription; @@ -144,40 +152,38 @@ public class CellularTile extends QSTile<QSTile.SignalState> { boolean isDataTypeIconWide; } - private final NetworkSignalChangedCallback mCallback = new NetworkSignalChangedCallback() { + private final SignalCallback mSignalCallback = new SignalCallbackAdapter() { private final CallbackInfo mInfo = new CallbackInfo(); - @Override - public void onWifiSignalChanged(boolean enabled, boolean connected, int wifiSignalIconId, - boolean activityIn, boolean activityOut, - String wifiSignalContentDescriptionId, String description) { + public void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon, + boolean activityIn, boolean activityOut, String description) { mInfo.wifiEnabled = enabled; - mInfo.wifiConnected = connected; refreshState(mInfo); } @Override - public void onMobileDataSignalChanged(boolean enabled, - int mobileSignalIconId, - String mobileSignalContentDescriptionId, int dataTypeIconId, - boolean activityIn, boolean activityOut, - String dataTypeContentDescriptionId, String description, - boolean isDataTypeIconWide) { - mInfo.enabled = enabled; - mInfo.mobileSignalIconId = mobileSignalIconId; - mInfo.signalContentDescription = mobileSignalContentDescriptionId; - mInfo.dataTypeIconId = dataTypeIconId; - mInfo.dataContentDescription = dataTypeContentDescriptionId; + public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType, + int qsType, boolean activityIn, boolean activityOut, String typeContentDescription, + String description, boolean isWide, int subId) { + if (qsIcon == null) { + // Not data sim, don't display. + return; + } + mInfo.enabled = qsIcon.visible; + mInfo.mobileSignalIconId = qsIcon.icon; + mInfo.signalContentDescription = qsIcon.contentDescription; + mInfo.dataTypeIconId = qsType; + mInfo.dataContentDescription = typeContentDescription; mInfo.activityIn = activityIn; mInfo.activityOut = activityOut; mInfo.enabledDesc = description; - mInfo.isDataTypeIconWide = isDataTypeIconWide; + mInfo.isDataTypeIconWide = qsType != 0 && isWide; refreshState(mInfo); } @Override - public void onNoSimVisibleChanged(boolean visible) { - mInfo.noSim = visible; + public void setNoSims(boolean show) { + mInfo.noSim = show; if (mInfo.noSim) { // Make sure signal gets cleared out when no sims. mInfo.mobileSignalIconId = 0; @@ -192,12 +198,13 @@ public class CellularTile extends QSTile<QSTile.SignalState> { } @Override - public void onAirplaneModeChanged(boolean enabled) { - mInfo.airplaneModeEnabled = enabled; + public void setIsAirplaneMode(IconState icon) { + mInfo.airplaneModeEnabled = icon.visible; refreshState(mInfo); } - public void onMobileDataEnabled(boolean enabled) { + @Override + public void setMobileDataEnabled(boolean enabled) { mDetailAdapter.setMobileDataEnabled(enabled); } }; @@ -223,10 +230,16 @@ public class CellularTile extends QSTile<QSTile.SignalState> { @Override public void setToggleState(boolean state) { + MetricsLogger.action(mContext, MetricsLogger.QS_CELLULAR_TOGGLE, state); mDataController.setMobileDataEnabled(state); } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_DATAUSAGEDETAIL; + } + + @Override public View createDetailView(Context context, View convertView, ViewGroup parent) { final DataUsageDetailView v = (DataUsageDetailView) (convertView != null ? convertView diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java index 5963a45..c6fc6ff 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java @@ -18,6 +18,8 @@ package com.android.systemui.qs.tiles; import android.provider.Settings.Secure; +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.SecureSetting; @@ -50,7 +52,8 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { } } }; - mUsageTracker = new UsageTracker(host.getContext(), ColorInversionTile.class, + mUsageTracker = new UsageTracker(host.getContext(), + Prefs.Key.COLOR_INVERSION_TILE_LAST_USED, ColorInversionTile.class, R.integer.days_to_show_color_inversion_tile); if (mSetting.getValue() != 0 && !mUsageTracker.isRecentlyUsed()) { mUsageTracker.trackUsage(); @@ -84,6 +87,7 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { + MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); mSetting.setValue(mState.value ? 0 : 1); mEnable.setAllowAnimation(true); mDisable.setAllowAnimation(true); @@ -113,6 +117,11 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_COLORINVERSION; + } + + @Override protected String composeChangeAnnouncement() { if (mState.value) { return mContext.getString( diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataUsageDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataUsageDetailView.java index eb816b7..d0ae383 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataUsageDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataUsageDetailView.java @@ -96,7 +96,7 @@ public class DataUsageDetailView extends LinearLayout { title.setText(titleId); final TextView usage = (TextView) findViewById(R.id.usage_text); usage.setText(formatBytes(bytes)); - usage.setTextColor(res.getColor(usageColor)); + usage.setTextColor(mContext.getColor(usageColor)); final DataUsageGraph graph = (DataUsageGraph) findViewById(R.id.usage_graph); graph.setLevels(info.limitLevel, info.warningLevel, info.usageLevel); final TextView carrier = (TextView) findViewById(R.id.usage_carrier_text); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java new file mode 100644 index 0000000..359ed5f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.provider.Settings; +import android.provider.Settings.Global; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnAttachStateChangeListener; +import android.view.ViewGroup; +import android.widget.Toast; + +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.Prefs; +import com.android.systemui.R; +import com.android.systemui.qs.QSTile; +import com.android.systemui.statusbar.policy.ZenModeController; +import com.android.systemui.volume.ZenModePanel; + +/** Quick settings tile: Do not disturb **/ +public class DndTile extends QSTile<QSTile.BooleanState> { + + private static final Intent ZEN_SETTINGS = + new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); + + private static final Intent ZEN_PRIORITY_SETTINGS = + new Intent(Settings.ACTION_ZEN_MODE_PRIORITY_SETTINGS); + + private static final String ACTION_SET_VISIBLE = "com.android.systemui.dndtile.SET_VISIBLE"; + private static final String EXTRA_VISIBLE = "visible"; + + private static final QSTile.Icon TOTAL_SILENCE = + ResourceIcon.get(R.drawable.ic_qs_dnd_on_total_silence); + + private final AnimationIcon mDisable = + new AnimationIcon(R.drawable.ic_dnd_disable_animation); + private final AnimationIcon mDisableTotalSilence = + new AnimationIcon(R.drawable.ic_dnd_total_silence_disable_animation); + + private final ZenModeController mController; + private final DndDetailAdapter mDetailAdapter; + + private boolean mListening; + private boolean mShowingDetail; + + public DndTile(Host host) { + super(host); + mController = host.getZenModeController(); + mDetailAdapter = new DndDetailAdapter(); + mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_SET_VISIBLE)); + } + + public static void setVisible(Context context, boolean visible) { + Prefs.putBoolean(context, Prefs.Key.DND_TILE_VISIBLE, visible); + } + + public static boolean isVisible(Context context) { + return Prefs.getBoolean(context, Prefs.Key.DND_TILE_VISIBLE, false /* defaultValue */); + } + + public static void setCombinedIcon(Context context, boolean combined) { + Prefs.putBoolean(context, Prefs.Key.DND_TILE_COMBINED_ICON, combined); + } + + public static boolean isCombinedIcon(Context context) { + return Prefs.getBoolean(context, Prefs.Key.DND_TILE_COMBINED_ICON, + false /* defaultValue */); + } + + @Override + public DetailAdapter getDetailAdapter() { + return mDetailAdapter; + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleClick() { + if (mController.isVolumeRestricted()) { + // Collapse the panels, so the user can see the toast. + mHost.collapsePanels(); + Toast.makeText(mContext, mContext.getString( + com.android.internal.R.string.error_message_change_not_allowed), + Toast.LENGTH_LONG).show(); + return; + } + mDisable.setAllowAnimation(true); + mDisableTotalSilence.setAllowAnimation(true); + MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); + if (mState.value) { + mController.setZen(Global.ZEN_MODE_OFF, null, TAG); + } else { + int zen = Prefs.getInt(mContext, Prefs.Key.DND_FAVORITE_ZEN, Global.ZEN_MODE_ALARMS); + mController.setZen(zen, null, TAG); + showDetail(true); + } + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + final int zen = arg instanceof Integer ? (Integer) arg : mController.getZen(); + final boolean newValue = zen != Global.ZEN_MODE_OFF; + final boolean valueChanged = state.value != newValue; + state.value = newValue; + state.visible = isVisible(mContext); + switch (zen) { + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on); + state.label = mContext.getString(R.string.quick_settings_dnd_priority_label); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_dnd_priority_on); + break; + case Global.ZEN_MODE_NO_INTERRUPTIONS: + state.icon = TOTAL_SILENCE; + state.label = mContext.getString(R.string.quick_settings_dnd_none_label); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_dnd_none_on); + break; + case Global.ZEN_MODE_ALARMS: + state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on); + state.label = mContext.getString(R.string.quick_settings_dnd_alarms_label); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_dnd_alarms_on); + break; + default: + state.icon = TOTAL_SILENCE.equals(state.icon) ? mDisableTotalSilence : mDisable; + state.label = mContext.getString(R.string.quick_settings_dnd_label); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_dnd_off); + break; + } + if (mShowingDetail && !state.value) { + showDetail(false); + } + if (valueChanged) { + fireToggleStateChanged(state.value); + } + } + + @Override + public int getMetricsCategory() { + return MetricsLogger.QS_DND; + } + + @Override + protected String composeChangeAnnouncement() { + if (mState.value) { + return mContext.getString(R.string.accessibility_quick_settings_dnd_changed_on); + } else { + return mContext.getString(R.string.accessibility_quick_settings_dnd_changed_off); + } + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + if (mListening) { + mController.addCallback(mZenCallback); + Prefs.registerListener(mContext, mPrefListener); + } else { + mController.removeCallback(mZenCallback); + Prefs.unregisterListener(mContext, mPrefListener); + } + } + + private final OnSharedPreferenceChangeListener mPrefListener + = new OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + @Prefs.Key String key) { + if (Prefs.Key.DND_TILE_COMBINED_ICON.equals(key) || + Prefs.Key.DND_TILE_VISIBLE.equals(key)) { + refreshState(); + } + } + }; + + private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() { + public void onZenChanged(int zen) { + refreshState(zen); + } + }; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final boolean visible = intent.getBooleanExtra(EXTRA_VISIBLE, false); + setVisible(mContext, visible); + refreshState(); + } + }; + + private final class DndDetailAdapter implements DetailAdapter, OnAttachStateChangeListener { + + @Override + public int getTitle() { + return R.string.quick_settings_dnd_label; + } + + @Override + public Boolean getToggleState() { + return mState.value; + } + + @Override + public Intent getSettingsIntent() { + return ZEN_SETTINGS; + } + + @Override + public void setToggleState(boolean state) { + MetricsLogger.action(mContext, MetricsLogger.QS_DND_TOGGLE, state); + if (!state) { + mController.setZen(Global.ZEN_MODE_OFF, null, TAG); + showDetail(false); + } + } + + @Override + public int getMetricsCategory() { + return MetricsLogger.QS_DND_DETAILS; + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + final ZenModePanel zmp = convertView != null ? (ZenModePanel) convertView + : (ZenModePanel) LayoutInflater.from(context).inflate( + R.layout.zen_mode_panel, parent, false); + if (convertView == null) { + zmp.init(mController); + zmp.addOnAttachStateChangeListener(this); + zmp.setCallback(mZenModePanelCallback); + } + return zmp; + } + + @Override + public void onViewAttachedToWindow(View v) { + mShowingDetail = true; + } + + @Override + public void onViewDetachedFromWindow(View v) { + mShowingDetail = false; + } + } + + private final ZenModePanel.Callback mZenModePanelCallback = new ZenModePanel.Callback() { + @Override + public void onPrioritySettings() { + mHost.startActivityDismissingKeyguard(ZEN_PRIORITY_SETTINGS); + } + + @Override + public void onInteraction() { + // noop + } + + @Override + public void onExpanded(boolean expanded) { + // noop + } + }; + +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index 5c1a317..5d74604 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -17,8 +17,8 @@ package com.android.systemui.qs.tiles; import android.app.ActivityManager; -import android.os.SystemClock; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.FlashlightController; @@ -27,16 +27,11 @@ import com.android.systemui.statusbar.policy.FlashlightController; public class FlashlightTile extends QSTile<QSTile.BooleanState> implements FlashlightController.FlashlightListener { - /** Grace period for which we consider the flashlight - * still available because it was recently on. */ - private static final long RECENTLY_ON_DURATION_MILLIS = 500; - private final AnimationIcon mEnable = new AnimationIcon(R.drawable.ic_signal_flashlight_enable_animation); private final AnimationIcon mDisable = new AnimationIcon(R.drawable.ic_signal_flashlight_disable_animation); private final FlashlightController mFlashlightController; - private long mWasLastOn; public FlashlightTile(Host host) { super(host); @@ -68,34 +63,23 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements if (ActivityManager.isUserAMonkey()) { return; } + MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); boolean newState = !mState.value; - mFlashlightController.setFlashlight(newState); refreshState(newState ? UserBoolean.USER_TRUE : UserBoolean.USER_FALSE); + mFlashlightController.setFlashlight(newState); } @Override protected void handleUpdateState(BooleanState state, Object arg) { - if (state.value) { - mWasLastOn = SystemClock.uptimeMillis(); - } - + state.visible = mFlashlightController.isAvailable(); + state.label = mHost.getContext().getString(R.string.quick_settings_flashlight_label); if (arg instanceof UserBoolean) { - state.value = ((UserBoolean) arg).value; - } - - if (!state.value && mWasLastOn != 0) { - if (SystemClock.uptimeMillis() > mWasLastOn + RECENTLY_ON_DURATION_MILLIS) { - mWasLastOn = 0; - } else { - mHandler.removeCallbacks(mRecentlyOnTimeout); - mHandler.postAtTime(mRecentlyOnTimeout, mWasLastOn + RECENTLY_ON_DURATION_MILLIS); + boolean value = ((UserBoolean) arg).value; + if (value == state.value) { + return; } + state.value = value; } - - // Always show the tile when the flashlight is or was recently on. This is needed because - // the camera is not available while it is being used for the flashlight. - state.visible = mWasLastOn != 0 || mFlashlightController.isAvailable(); - state.label = mHost.getContext().getString(R.string.quick_settings_flashlight_label); final AnimationIcon icon = state.value ? mEnable : mDisable; icon.setAllowAnimation(arg instanceof UserBoolean && ((UserBoolean) arg).userInitiated); state.icon = icon; @@ -106,6 +90,11 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_FLASHLIGHT; + } + + @Override protected String composeChangeAnnouncement() { if (mState.value) { return mContext.getString(R.string.accessibility_quick_settings_flashlight_changed_on); @@ -115,8 +104,8 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements } @Override - public void onFlashlightOff() { - refreshState(UserBoolean.BACKGROUND_FALSE); + public void onFlashlightChanged(boolean enabled) { + refreshState(enabled ? UserBoolean.BACKGROUND_TRUE : UserBoolean.BACKGROUND_FALSE); } @Override @@ -128,11 +117,4 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements public void onFlashlightAvailabilityChanged(boolean available) { refreshState(); } - - private Runnable mRecentlyOnTimeout = new Runnable() { - @Override - public void run() { - refreshState(); - } - }; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java index fcc517f..f28a24b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java @@ -20,6 +20,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.qs.UsageTracker; import com.android.systemui.qs.QSTile; @@ -68,6 +70,7 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { final boolean isEnabled = (Boolean) mState.value; + MetricsLogger.action(mContext, getMetricsCategory(), !isEnabled); mController.setHotspotEnabled(!isEnabled); mEnable.setAllowAnimation(true); mDisable.setAllowAnimation(true); @@ -96,6 +99,11 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_HOTSPOT; + } + + @Override protected String composeChangeAnnouncement() { if (mState.value) { return mContext.getString(R.string.accessibility_quick_settings_hotspot_changed_on); @@ -105,7 +113,8 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { } private static UsageTracker newUsageTracker(Context context) { - return new UsageTracker(context, HotspotTile.class, R.integer.days_to_show_hotspot_tile); + return new UsageTracker(context, Prefs.Key.HOTSPOT_TILE_LAST_USED, HotspotTile.class, + R.integer.days_to_show_hotspot_tile); } private final class Callback implements HotspotController.Callback { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java index 2736530..f7f7acb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java @@ -29,6 +29,7 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.qs.QSTile; import java.util.Arrays; @@ -42,6 +43,7 @@ public class IntentTile extends QSTile<QSTile.State> { private PendingIntent mOnLongClick; private String mOnLongClickUri; private int mCurrentUserId; + private String mIntentPackage; private IntentTile(Host host, String action) { super(host); @@ -82,6 +84,7 @@ public class IntentTile extends QSTile<QSTile.State> { @Override protected void handleClick() { + MetricsLogger.action(mContext, getMetricsCategory(), mIntentPackage); sendIntent("click", mOnClick, mOnClickUri); } @@ -93,7 +96,11 @@ public class IntentTile extends QSTile<QSTile.State> { private void sendIntent(String type, PendingIntent pi, String uri) { try { if (pi != null) { - pi.send(); + if (pi.isActivity()) { + getHost().startActivityDismissingKeyguard(pi.getIntent()); + } else { + pi.send(); + } } else if (uri != null) { final Intent intent = Intent.parseUri(uri, Intent.URI_INTENT_SCHEME); mContext.sendBroadcastAsUser(intent, new UserHandle(mCurrentUserId)); @@ -133,6 +140,13 @@ public class IntentTile extends QSTile<QSTile.State> { mOnClickUri = intent.getStringExtra("onClickUri"); mOnLongClick = intent.getParcelableExtra("onLongClick"); mOnLongClickUri = intent.getStringExtra("onLongClickUri"); + mIntentPackage = intent.getStringExtra("package"); + mIntentPackage = mIntentPackage == null ? "" : mIntentPackage; + } + + @Override + public int getMetricsCategory() { + return MetricsLogger.QS_INTENT; } private final BroadcastReceiver mReceiver = new BroadcastReceiver() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java index 11ec722..e6fade4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.KeyguardMonitor; @@ -59,6 +60,7 @@ public class LocationTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { final boolean wasEnabled = (Boolean) mState.value; + MetricsLogger.action(mContext, getMetricsCategory(), !wasEnabled); mController.setLocationEnabled(!wasEnabled); mEnable.setAllowAnimation(true); mDisable.setAllowAnimation(true); @@ -87,6 +89,11 @@ public class LocationTile extends QSTile<QSTile.BooleanState> { } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_LOCATION; + } + + @Override protected String composeChangeAnnouncement() { if (mState.value) { return mContext.getString(R.string.accessibility_quick_settings_location_changed_on); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java index f46b9a6..6d2c8c0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java @@ -18,6 +18,7 @@ package com.android.systemui.qs.tiles; import android.content.res.Configuration; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.RotationLockController; @@ -59,6 +60,7 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { if (mController == null) return; + MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); final boolean newState = !mState.value; mController.setRotationLocked(newState); refreshState(newState ? UserBoolean.USER_TRUE : UserBoolean.USER_FALSE); @@ -71,6 +73,10 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> { : mController.isRotationLocked(); final boolean userInitiated = arg != null ? ((UserBoolean) arg).userInitiated : false; state.visible = mController.isRotationLockAffordanceVisible(); + if (state.value == rotationLocked && state.contentDescription != null) { + // No change and initialized, no need to update all the values. + return; + } state.value = rotationLocked; final boolean portrait = mContext.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE; @@ -92,6 +98,11 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> { R.string.accessibility_rotation_lock_off); } + @Override + public int getMetricsCategory() { + return MetricsLogger.QS_ROTATIONLOCK; + } + /** * Get the correct accessibility string based on the state * diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailItemView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailItemView.java index c55cbcc..21c5c96 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailItemView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailItemView.java @@ -28,7 +28,6 @@ import android.graphics.Bitmap; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.util.AttributeSet; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 6bad652..d4f54b6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.qs.PseudoGridView; import com.android.systemui.statusbar.policy.UserSwitcherController; @@ -84,6 +85,7 @@ public class UserDetailView extends PseudoGridView { public void onClick(View view) { UserSwitcherController.UserRecord tag = (UserSwitcherController.UserRecord) view.getTag(); + MetricsLogger.action(mContext, MetricsLogger.QS_SWITCH_USER); switchTo(tag); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java index e09024b..c33ef7c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java @@ -24,17 +24,21 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; +import com.android.internal.logging.MetricsLogger; +import com.android.settingslib.wifi.AccessPoint; import com.android.systemui.R; import com.android.systemui.qs.QSDetailItems; import com.android.systemui.qs.QSDetailItems.Item; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QSTileView; import com.android.systemui.qs.SignalTileView; -import com.android.systemui.statusbar.phone.QSTileHost; import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.NetworkController.AccessPointController; -import com.android.systemui.statusbar.policy.NetworkController.AccessPointController.AccessPoint; -import com.android.systemui.statusbar.policy.NetworkController.NetworkSignalChangedCallback; +import com.android.systemui.statusbar.policy.NetworkController.IconState; +import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; +import com.android.systemui.statusbar.policy.SignalCallbackAdapter; + +import java.util.List; /** Quick settings tile: Wifi **/ public class WifiTile extends QSTile<QSTile.SignalState> { @@ -65,10 +69,17 @@ public class WifiTile extends QSTile<QSTile.SignalState> { @Override public void setListening(boolean listening) { if (listening) { - mController.addNetworkSignalChangedCallback(mCallback); + mController.addSignalCallback(mSignalCallback); + } else { + mController.removeSignalCallback(mSignalCallback); + } + } + + @Override + public void setDetailListening(boolean listening) { + if (listening) { mWifiController.addAccessPointCallback(mDetailAdapter); } else { - mController.removeNetworkSignalChangedCallback(mCallback); mWifiController.removeAccessPointCallback(mDetailAdapter); } } @@ -86,13 +97,14 @@ public class WifiTile extends QSTile<QSTile.SignalState> { @Override protected void handleClick() { mState.copyTo(mStateBeforeClick); + MetricsLogger.action(mContext, getMetricsCategory(), !mState.enabled); mController.setWifiEnabled(!mState.enabled); } @Override protected void handleSecondaryClick() { if (!mWifiController.canConfigWifi()) { - mHost.startSettingsActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)); + mHost.startActivityDismissingKeyguard(new Intent(Settings.ACTION_WIFI_SETTINGS)); return; } if (!mState.enabled) { @@ -151,6 +163,11 @@ public class WifiTile extends QSTile<QSTile.SignalState> { } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_WIFI; + } + + @Override protected boolean shouldAnnouncementBeDelayed() { return mStateBeforeClick.enabled == mState.enabled; } @@ -196,46 +213,21 @@ public class WifiTile extends QSTile<QSTile.SignalState> { } } - private final NetworkSignalChangedCallback mCallback = new NetworkSignalChangedCallback() { + private final SignalCallback mSignalCallback = new SignalCallbackAdapter() { @Override - public void onWifiSignalChanged(boolean enabled, boolean connected, int wifiSignalIconId, - boolean activityIn, boolean activityOut, - String wifiSignalContentDescriptionId, String description) { + public void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon, + boolean activityIn, boolean activityOut, String description) { if (DEBUG) Log.d(TAG, "onWifiSignalChanged enabled=" + enabled); final CallbackInfo info = new CallbackInfo(); info.enabled = enabled; - info.connected = connected; - info.wifiSignalIconId = wifiSignalIconId; + info.connected = qsIcon.visible; + info.wifiSignalIconId = qsIcon.icon; info.enabledDesc = description; info.activityIn = activityIn; info.activityOut = activityOut; - info.wifiSignalContentDescription = wifiSignalContentDescriptionId; + info.wifiSignalContentDescription = qsIcon.contentDescription; refreshState(info); } - - @Override - public void onMobileDataSignalChanged(boolean enabled, - int mobileSignalIconId, - String mobileSignalContentDescriptionId, int dataTypeIconId, - boolean activityIn, boolean activityOut, - String dataTypeContentDescriptionId, String description, - boolean isDataTypeIconWide) { - // noop - } - - public void onNoSimVisibleChanged(boolean noSims) { - // noop - } - - @Override - public void onAirplaneModeChanged(boolean enabled) { - // noop - } - - @Override - public void onMobileDataEnabled(boolean enabled) { - // noop - } }; private final class WifiDetailAdapter implements DetailAdapter, @@ -261,11 +253,17 @@ public class WifiTile extends QSTile<QSTile.SignalState> { @Override public void setToggleState(boolean state) { if (DEBUG) Log.d(TAG, "setToggleState " + state); + MetricsLogger.action(mContext, MetricsLogger.QS_WIFI_TOGGLE, state); mController.setWifiEnabled(state); showDetail(false); } @Override + public int getMetricsCategory() { + return MetricsLogger.QS_WIFI_DETAILS; + } + + @Override public View createDetailView(Context context, View convertView, ViewGroup parent) { if (DEBUG) Log.d(TAG, "createDetailView convertView=" + (convertView != null)); mAccessPoints = null; @@ -282,24 +280,24 @@ public class WifiTile extends QSTile<QSTile.SignalState> { } @Override - public void onAccessPointsChanged(final AccessPoint[] accessPoints) { - mAccessPoints = accessPoints; + public void onAccessPointsChanged(final List<AccessPoint> accessPoints) { + mAccessPoints = accessPoints.toArray(new AccessPoint[accessPoints.size()]); updateItems(); - if (accessPoints != null && accessPoints.length > 0) { + if (accessPoints != null && accessPoints.size() > 0) { fireScanStateChanged(false); } } @Override public void onSettingsActivityTriggered(Intent settingsIntent) { - mHost.startSettingsActivity(settingsIntent); + mHost.startActivityDismissingKeyguard(settingsIntent); } @Override public void onDetailItemClick(Item item) { if (item == null || item.tag == null) return; final AccessPoint ap = (AccessPoint) item.tag; - if (!ap.isConnected) { + if (!ap.isActive()) { if (mWifiController.connect(ap)) { mHost.collapsePanels(); } @@ -326,16 +324,10 @@ public class WifiTile extends QSTile<QSTile.SignalState> { final AccessPoint ap = mAccessPoints[i]; final Item item = new Item(); item.tag = ap; - item.icon = ap.iconId; - item.line1 = ap.ssid; - if (ap.isConnected) { - item.line2 = mContext.getString(ap.isConfigured ? - R.string.quick_settings_connected : - R.string.quick_settings_connected_via_wfa); - } else if (ap.networkId >= 0) { - // TODO: Set line 2 to wifi saved string here. - } - item.overlay = ap.hasSecurity + item.icon = mWifiController.getIcon(ap); + item.line1 = ap.getSsid(); + item.line2 = ap.isActive() ? ap.getSummary() : null; + item.overlay = ap.getSecurity() != AccessPoint.SECURITY_NONE ? mContext.getDrawable(R.drawable.qs_ic_wifi_lock) : null; items[i] = item; diff --git a/packages/SystemUI/src/com/android/systemui/recent/ColorDrawableWithDimensions.java b/packages/SystemUI/src/com/android/systemui/recent/ColorDrawableWithDimensions.java deleted file mode 100644 index b4d3edd..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/ColorDrawableWithDimensions.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.recent; - -import android.graphics.drawable.ColorDrawable; - -public class ColorDrawableWithDimensions extends ColorDrawable { - private int mWidth; - private int mHeight; - - public ColorDrawableWithDimensions(int color, int width, int height) { - super(color); - mWidth = width; - mHeight = height; - } - - @Override - public int getIntrinsicWidth() { - return mWidth; - } - - @Override - public int getIntrinsicHeight() { - return mHeight; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recent/FadedEdgeDrawHelper.java b/packages/SystemUI/src/com/android/systemui/recent/FadedEdgeDrawHelper.java deleted file mode 100644 index 59f7a80..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/FadedEdgeDrawHelper.java +++ /dev/null @@ -1,193 +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.recent; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.LinearGradient; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Shader; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewConfiguration; -import android.widget.LinearLayout; - -import com.android.systemui.R; - -public class FadedEdgeDrawHelper { - public static final boolean OPTIMIZE_SW_RENDERED_RECENTS = true; - public static final boolean USE_DARK_FADE_IN_HW_ACCELERATED_MODE = true; - private View mScrollView; - - private int mFadingEdgeLength; - private boolean mIsVertical; - private boolean mSoftwareRendered = false; - private Paint mBlackPaint; - private Paint mFadePaint; - private Matrix mFadeMatrix; - private LinearGradient mFade; - - public static FadedEdgeDrawHelper create(Context context, - AttributeSet attrs, View scrollView, boolean isVertical) { - boolean isTablet = context.getResources(). - getBoolean(R.bool.config_recents_interface_for_tablets); - if (!isTablet && (OPTIMIZE_SW_RENDERED_RECENTS || USE_DARK_FADE_IN_HW_ACCELERATED_MODE)) { - return new FadedEdgeDrawHelper(context, attrs, scrollView, isVertical); - } else { - return null; - } - } - - public FadedEdgeDrawHelper(Context context, - AttributeSet attrs, View scrollView, boolean isVertical) { - mScrollView = scrollView; - TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View); - mFadingEdgeLength = a.getDimensionPixelSize(android.R.styleable.View_fadingEdgeLength, - ViewConfiguration.get(context).getScaledFadingEdgeLength()); - mIsVertical = isVertical; - } - - public void onAttachedToWindowCallback( - LinearLayout layout, boolean hardwareAccelerated) { - mSoftwareRendered = !hardwareAccelerated; - if ((mSoftwareRendered && OPTIMIZE_SW_RENDERED_RECENTS) - || USE_DARK_FADE_IN_HW_ACCELERATED_MODE) { - mScrollView.setVerticalFadingEdgeEnabled(false); - mScrollView.setHorizontalFadingEdgeEnabled(false); - } - } - - public void addViewCallback(View newLinearLayoutChild) { - if (mSoftwareRendered && OPTIMIZE_SW_RENDERED_RECENTS) { - final RecentsPanelView.ViewHolder holder = - (RecentsPanelView.ViewHolder) newLinearLayoutChild.getTag(); - holder.labelView.setDrawingCacheEnabled(true); - holder.labelView.buildDrawingCache(); - } - } - - public void drawCallback(Canvas canvas, - int left, int right, int top, int bottom, int scrollX, int scrollY, - float topFadingEdgeStrength, float bottomFadingEdgeStrength, - float leftFadingEdgeStrength, float rightFadingEdgeStrength, int mPaddingTop) { - - if ((mSoftwareRendered && OPTIMIZE_SW_RENDERED_RECENTS) - || USE_DARK_FADE_IN_HW_ACCELERATED_MODE) { - if (mFadePaint == null) { - mFadePaint = new Paint(); - mFadeMatrix = new Matrix(); - // use use a height of 1, and then wack the matrix each time we - // actually use it. - mFade = new LinearGradient(0, 0, 0, 1, 0xCC000000, 0, Shader.TileMode.CLAMP); - // PULL OUT THIS CONSTANT - mFadePaint.setShader(mFade); - } - - // draw the fade effect - boolean drawTop = false; - boolean drawBottom = false; - boolean drawLeft = false; - boolean drawRight = false; - - float topFadeStrength = 0.0f; - float bottomFadeStrength = 0.0f; - float leftFadeStrength = 0.0f; - float rightFadeStrength = 0.0f; - - final float fadeHeight = mFadingEdgeLength; - int length = (int) fadeHeight; - - // clip the fade length if top and bottom fades overlap - // overlapping fades produce odd-looking artifacts - if (mIsVertical && (top + length > bottom - length)) { - length = (bottom - top) / 2; - } - - // also clip horizontal fades if necessary - if (!mIsVertical && (left + length > right - length)) { - length = (right - left) / 2; - } - - if (mIsVertical) { - topFadeStrength = Math.max(0.0f, Math.min(1.0f, topFadingEdgeStrength)); - drawTop = topFadeStrength * fadeHeight > 1.0f; - bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, bottomFadingEdgeStrength)); - drawBottom = bottomFadeStrength * fadeHeight > 1.0f; - } - - if (!mIsVertical) { - leftFadeStrength = Math.max(0.0f, Math.min(1.0f, leftFadingEdgeStrength)); - drawLeft = leftFadeStrength * fadeHeight > 1.0f; - rightFadeStrength = Math.max(0.0f, Math.min(1.0f, rightFadingEdgeStrength)); - drawRight = rightFadeStrength * fadeHeight > 1.0f; - } - - if (drawTop) { - mFadeMatrix.setScale(1, fadeHeight * topFadeStrength); - mFadeMatrix.postTranslate(left, top); - mFade.setLocalMatrix(mFadeMatrix); - mFadePaint.setShader(mFade); - canvas.drawRect(left, top, right, top + length, mFadePaint); - - if (mBlackPaint == null) { - // Draw under the status bar at the top - mBlackPaint = new Paint(); - mBlackPaint.setColor(0xFF000000); - } - canvas.drawRect(left, top - mPaddingTop, right, top, mBlackPaint); - } - - if (drawBottom) { - mFadeMatrix.setScale(1, fadeHeight * bottomFadeStrength); - mFadeMatrix.postRotate(180); - mFadeMatrix.postTranslate(left, bottom); - mFade.setLocalMatrix(mFadeMatrix); - mFadePaint.setShader(mFade); - canvas.drawRect(left, bottom - length, right, bottom, mFadePaint); - } - - if (drawLeft) { - mFadeMatrix.setScale(1, fadeHeight * leftFadeStrength); - mFadeMatrix.postRotate(-90); - mFadeMatrix.postTranslate(left, top); - mFade.setLocalMatrix(mFadeMatrix); - mFadePaint.setShader(mFade); - canvas.drawRect(left, top, left + length, bottom, mFadePaint); - } - - if (drawRight) { - mFadeMatrix.setScale(1, fadeHeight * rightFadeStrength); - mFadeMatrix.postRotate(90); - mFadeMatrix.postTranslate(right, top); - mFade.setLocalMatrix(mFadeMatrix); - mFadePaint.setShader(mFade); - canvas.drawRect(right - length, top, right, bottom, mFadePaint); - } - } - } - - public int getVerticalFadingEdgeLength() { - return mFadingEdgeLength; - } - - public int getHorizontalFadingEdgeLength() { - return mFadingEdgeLength; - } - -} diff --git a/packages/SystemUI/src/com/android/systemui/recent/FirstFrameAnimatorHelper.java b/packages/SystemUI/src/com/android/systemui/recent/FirstFrameAnimatorHelper.java deleted file mode 100644 index 84d13cf..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/FirstFrameAnimatorHelper.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.recent; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.util.Log; -import android.view.View; -import android.view.ViewPropertyAnimator; -import android.view.ViewTreeObserver; - -/* - * This is a helper class that listens to updates from the corresponding animation. - * For the first two frames, it adjusts the current play time of the animation to - * prevent jank at the beginning of the animation - */ -public class FirstFrameAnimatorHelper extends AnimatorListenerAdapter - implements ValueAnimator.AnimatorUpdateListener { - private static final boolean DEBUG = false; - private static final int MAX_DELAY = 1000; - private static final int IDEAL_FRAME_DURATION = 16; - private View mTarget; - private long mStartFrame; - private long mStartTime = -1; - private boolean mHandlingOnAnimationUpdate; - private boolean mAdjustedSecondFrameTime; - - private static ViewTreeObserver.OnDrawListener sGlobalDrawListener; - private static long sGlobalFrameCounter; - - public FirstFrameAnimatorHelper(ValueAnimator animator, View target) { - mTarget = target; - animator.addUpdateListener(this); - } - - public FirstFrameAnimatorHelper(ViewPropertyAnimator vpa, View target) { - mTarget = target; - vpa.setListener(this); - } - - // only used for ViewPropertyAnimators - public void onAnimationStart(Animator animation) { - final ValueAnimator va = (ValueAnimator) animation; - va.addUpdateListener(FirstFrameAnimatorHelper.this); - onAnimationUpdate(va); - } - - public static void initializeDrawListener(View view) { - if (sGlobalDrawListener != null) { - view.getViewTreeObserver().removeOnDrawListener(sGlobalDrawListener); - } - sGlobalDrawListener = new ViewTreeObserver.OnDrawListener() { - private long mTime = System.currentTimeMillis(); - public void onDraw() { - sGlobalFrameCounter++; - if (DEBUG) { - long newTime = System.currentTimeMillis(); - Log.d("FirstFrameAnimatorHelper", "TICK " + (newTime - mTime)); - mTime = newTime; - } - } - }; - view.getViewTreeObserver().addOnDrawListener(sGlobalDrawListener); - } - - public void onAnimationUpdate(final ValueAnimator animation) { - final long currentTime = System.currentTimeMillis(); - if (mStartTime == -1) { - mStartFrame = sGlobalFrameCounter; - mStartTime = currentTime; - } - - if (!mHandlingOnAnimationUpdate && - // If the current play time exceeds the duration, the animation - // will get finished, even if we call setCurrentPlayTime -- therefore - // don't adjust the animation in that case - animation.getCurrentPlayTime() < animation.getDuration()) { - mHandlingOnAnimationUpdate = true; - long frameNum = sGlobalFrameCounter - mStartFrame; - // If we haven't drawn our first frame, reset the time to t = 0 - // (give up after MAX_DELAY ms of waiting though - might happen, for example, if we - // are no longer in the foreground and no frames are being rendered ever) - if (frameNum == 0 && currentTime < mStartTime + MAX_DELAY) { - // The first frame on animations doesn't always trigger an invalidate... - // force an invalidate here to make sure the animation continues to advance - mTarget.getRootView().invalidate(); - animation.setCurrentPlayTime(0); - - // For the second frame, if the first frame took more than 16ms, - // adjust the start time and pretend it took only 16ms anyway. This - // prevents a large jump in the animation due to an expensive first frame - } else if (frameNum == 1 && currentTime < mStartTime + MAX_DELAY && - !mAdjustedSecondFrameTime && - currentTime > mStartTime + IDEAL_FRAME_DURATION) { - animation.setCurrentPlayTime(IDEAL_FRAME_DURATION); - mAdjustedSecondFrameTime = true; - } else { - if (frameNum > 1) { - mTarget.post(new Runnable() { - public void run() { - animation.removeUpdateListener(FirstFrameAnimatorHelper.this); - } - }); - } - if (DEBUG) print(animation); - } - mHandlingOnAnimationUpdate = false; - } else { - if (DEBUG) print(animation); - } - } - - public void print(ValueAnimator animation) { - float flatFraction = animation.getCurrentPlayTime() / (float) animation.getDuration(); - Log.d("FirstFrameAnimatorHelper", sGlobalFrameCounter + - "(" + (sGlobalFrameCounter - mStartFrame) + ") " + mTarget + " dirty? " + - mTarget.isDirty() + " " + flatFraction + " " + this + " " + animation); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentTasksLoader.java b/packages/SystemUI/src/com/android/systemui/recent/RecentTasksLoader.java deleted file mode 100644 index 34430d9..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentTasksLoader.java +++ /dev/null @@ -1,586 +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.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; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -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.util.Log; -import android.view.MotionEvent; -import android.view.View; - -import com.android.systemui.R; -import com.android.systemui.recents.misc.SystemServicesProxy; -import com.android.systemui.statusbar.phone.PhoneStatusBar; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -public class RecentTasksLoader implements View.OnTouchListener { - static final String TAG = "RecentTasksLoader"; - static final boolean DEBUG = PhoneStatusBar.DEBUG || false; - - private static final int DISPLAY_TASKS = 20; - private static final int MAX_TASKS = DISPLAY_TASKS + 1; // allow extra for non-apps - - private Context mContext; - private RecentsPanelView mRecentsPanel; - - private Object mFirstTaskLock = new Object(); - private TaskDescription mFirstTask; - private boolean mFirstTaskLoaded; - - private AsyncTask<Void, ArrayList<TaskDescription>, Void> mTaskLoader; - private AsyncTask<Void, TaskDescription, Void> mThumbnailLoader; - private Handler mHandler; - - private int mIconDpi; - private ColorDrawableWithDimensions mDefaultThumbnailBackground; - private ColorDrawableWithDimensions mDefaultIconBackground; - private int mNumTasksInFirstScreenful = Integer.MAX_VALUE; - - private boolean mFirstScreenful; - private ArrayList<TaskDescription> mLoadedTasks; - - private enum State { LOADING, LOADED, CANCELLED }; - private State mState = State.CANCELLED; - - - private static RecentTasksLoader sInstance; - public static RecentTasksLoader getInstance(Context context) { - if (sInstance == null) { - sInstance = new RecentTasksLoader(context); - } - return sInstance; - } - - private RecentTasksLoader(Context context) { - mContext = context; - mHandler = new Handler(); - - final Resources res = context.getResources(); - - // get the icon size we want -- on tablets, we use bigger icons - boolean isTablet = res.getBoolean(R.bool.config_recents_interface_for_tablets); - if (isTablet) { - ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - mIconDpi = activityManager.getLauncherLargeIconDensity(); - } else { - mIconDpi = res.getDisplayMetrics().densityDpi; - } - - // Render default icon (just a blank image) - int defaultIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.app_icon_size); - int iconSize = (int) (defaultIconSize * mIconDpi / res.getDisplayMetrics().densityDpi); - mDefaultIconBackground = new ColorDrawableWithDimensions(0x00000000, iconSize, iconSize); - - // Render the default thumbnail background - int thumbnailWidth = - (int) res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width); - int thumbnailHeight = - (int) res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height); - int color = res.getColor(R.drawable.status_bar_recents_app_thumbnail_background); - - mDefaultThumbnailBackground = - new ColorDrawableWithDimensions(color, thumbnailWidth, thumbnailHeight); - } - - public void setRecentsPanel(RecentsPanelView newRecentsPanel, RecentsPanelView caller) { - // Only allow clearing mRecentsPanel if the caller is the current recentsPanel - if (newRecentsPanel != null || mRecentsPanel == caller) { - mRecentsPanel = newRecentsPanel; - if (mRecentsPanel != null) { - mNumTasksInFirstScreenful = mRecentsPanel.numItemsInOneScreenful(); - } - } - } - - public Drawable getDefaultThumbnail() { - return mDefaultThumbnailBackground; - } - - public Drawable getDefaultIcon() { - return mDefaultIconBackground; - } - - public ArrayList<TaskDescription> getLoadedTasks() { - return mLoadedTasks; - } - - public void remove(TaskDescription td) { - mLoadedTasks.remove(td); - } - - public boolean isFirstScreenful() { - return mFirstScreenful; - } - - private boolean isCurrentHomeActivity(ComponentName component, ActivityInfo homeInfo) { - if (homeInfo == null) { - final PackageManager pm = mContext.getPackageManager(); - homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) - .resolveActivityInfo(pm, 0); - } - return homeInfo != null - && homeInfo.packageName.equals(component.getPackageName()) - && homeInfo.name.equals(component.getClassName()); - } - - // Create an TaskDescription, returning null if the title or icon is null - TaskDescription createTaskDescription(int taskId, int persistentTaskId, Intent baseIntent, - 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); - 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(); - - if (title != null && title.length() > 0) { - if (DEBUG) Log.v(TAG, "creating activity desc for id=" - + persistentTaskId + ", label=" + title); - - TaskDescription item = new TaskDescription(taskId, - persistentTaskId, resolveInfo, baseIntent, info.packageName, - description, userId); - item.setLabel(title); - - return item; - } else { - if (DEBUG) Log.v(TAG, "SKIPPING item " + persistentTaskId); - } - } - return null; - } - - void loadThumbnailAndIcon(TaskDescription td) { - final ActivityManager am = (ActivityManager) - mContext.getSystemService(Context.ACTIVITY_SERVICE); - final PackageManager pm = mContext.getPackageManager(); - final Bitmap thumbnail = SystemServicesProxy.getThumbnail(am, td.persistentTaskId); - Drawable icon = getFullResIcon(td.resolveInfo, pm); - if (td.userId != UserHandle.myUserId()) { - // Need to badge the icon - icon = mContext.getPackageManager().getUserBadgedIcon(icon, new UserHandle(td.userId)); - } - if (DEBUG) Log.v(TAG, "Loaded bitmap for task " - + td + ": " + thumbnail); - synchronized (td) { - if (thumbnail != null) { - td.setThumbnail(new BitmapDrawable(mContext.getResources(), thumbnail)); - } else { - td.setThumbnail(mDefaultThumbnailBackground); - } - if (icon != null) { - td.setIcon(icon); - } - td.setLoaded(true); - } - } - - Drawable getFullResDefaultActivityIcon() { - return getFullResIcon(Resources.getSystem(), - com.android.internal.R.mipmap.sym_def_app_icon); - } - - Drawable getFullResIcon(Resources resources, int iconId) { - try { - return resources.getDrawableForDensity(iconId, mIconDpi); - } catch (Resources.NotFoundException e) { - return getFullResDefaultActivityIcon(); - } - } - - private Drawable getFullResIcon(ResolveInfo info, PackageManager packageManager) { - Resources resources; - try { - resources = packageManager.getResourcesForApplication( - info.activityInfo.applicationInfo); - } catch (PackageManager.NameNotFoundException e) { - resources = null; - } - if (resources != null) { - int iconId = info.activityInfo.getIconResource(); - if (iconId != 0) { - return getFullResIcon(resources, iconId); - } - } - return getFullResDefaultActivityIcon(); - } - - Runnable mPreloadTasksRunnable = new Runnable() { - public void run() { - loadTasksInBackground(); - } - }; - - // additional optimization when we have software system buttons - start loading the recent - // tasks on touch down - @Override - public boolean onTouch(View v, MotionEvent ev) { - int action = ev.getAction() & MotionEvent.ACTION_MASK; - if (action == MotionEvent.ACTION_DOWN) { - preloadRecentTasksList(); - } else if (action == MotionEvent.ACTION_CANCEL) { - cancelPreloadingRecentTasksList(); - } else if (action == MotionEvent.ACTION_UP) { - // Remove the preloader if we haven't called it yet - mHandler.removeCallbacks(mPreloadTasksRunnable); - if (!v.isPressed()) { - cancelLoadingThumbnailsAndIcons(); - } - - } - return false; - } - - public void preloadRecentTasksList() { - mHandler.post(mPreloadTasksRunnable); - } - - public void cancelPreloadingRecentTasksList() { - cancelLoadingThumbnailsAndIcons(); - mHandler.removeCallbacks(mPreloadTasksRunnable); - } - - public void cancelLoadingThumbnailsAndIcons(RecentsPanelView caller) { - // Only oblige this request if it comes from the current RecentsPanel - // (eg when you rotate, the old RecentsPanel request should be ignored) - if (mRecentsPanel == caller) { - cancelLoadingThumbnailsAndIcons(); - } - } - - - private void cancelLoadingThumbnailsAndIcons() { - if (mRecentsPanel != null && mRecentsPanel.isShowing()) { - return; - } - - if (mTaskLoader != null) { - mTaskLoader.cancel(false); - mTaskLoader = null; - } - if (mThumbnailLoader != null) { - mThumbnailLoader.cancel(false); - mThumbnailLoader = null; - } - mLoadedTasks = null; - if (mRecentsPanel != null) { - mRecentsPanel.onTaskLoadingCancelled(); - } - mFirstScreenful = false; - mState = State.CANCELLED; - } - - private void clearFirstTask() { - synchronized (mFirstTaskLock) { - mFirstTask = null; - mFirstTaskLoaded = false; - } - } - - public void preloadFirstTask() { - Thread bgLoad = new Thread() { - public void run() { - TaskDescription first = loadFirstTask(); - synchronized(mFirstTaskLock) { - if (mCancelPreloadingFirstTask) { - clearFirstTask(); - } else { - mFirstTask = first; - mFirstTaskLoaded = true; - } - mPreloadingFirstTask = false; - } - } - }; - synchronized(mFirstTaskLock) { - if (!mPreloadingFirstTask) { - clearFirstTask(); - mPreloadingFirstTask = true; - bgLoad.start(); - } - } - } - - public void cancelPreloadingFirstTask() { - synchronized(mFirstTaskLock) { - if (mPreloadingFirstTask) { - mCancelPreloadingFirstTask = true; - } else { - clearFirstTask(); - } - } - } - - boolean mPreloadingFirstTask; - boolean mCancelPreloadingFirstTask; - public TaskDescription getFirstTask() { - while(true) { - synchronized(mFirstTaskLock) { - if (mFirstTaskLoaded) { - return mFirstTask; - } else if (!mFirstTaskLoaded && !mPreloadingFirstTask) { - mFirstTask = loadFirstTask(); - mFirstTaskLoaded = true; - return mFirstTask; - } - } - try { - Thread.sleep(3); - } catch (InterruptedException e) { - } - } - } - - public TaskDescription loadFirstTask() { - final ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); - - 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); - - Intent intent = new Intent(recentInfo.baseIntent); - if (recentInfo.origActivity != null) { - intent.setComponent(recentInfo.origActivity); - } - - // Don't load the current home activity. - if (isCurrentHomeActivity(intent.getComponent(), null)) { - return null; - } - - // Don't load ourselves - if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) { - return null; - } - - item = createTaskDescription(recentInfo.id, - recentInfo.persistentId, recentInfo.baseIntent, - recentInfo.origActivity, recentInfo.description, - recentInfo.userId); - if (item != null) { - loadThumbnailAndIcon(item); - } - return item; - } - return null; - } - - public void loadTasksInBackground() { - loadTasksInBackground(false); - } - public void loadTasksInBackground(final boolean zeroeth) { - if (mState != State.CANCELLED) { - return; - } - mState = State.LOADING; - mFirstScreenful = true; - - final LinkedBlockingQueue<TaskDescription> tasksWaitingForThumbnails = - new LinkedBlockingQueue<TaskDescription>(); - mTaskLoader = new AsyncTask<Void, ArrayList<TaskDescription>, Void>() { - @Override - protected void onProgressUpdate(ArrayList<TaskDescription>... values) { - if (!isCancelled()) { - ArrayList<TaskDescription> newTasks = values[0]; - // do a callback to RecentsPanelView to let it know we have more values - // how do we let it know we're all done? just always call back twice - if (mRecentsPanel != null) { - mRecentsPanel.onTasksLoaded(newTasks, mFirstScreenful); - } - if (mLoadedTasks == null) { - mLoadedTasks = new ArrayList<TaskDescription>(); - } - mLoadedTasks.addAll(newTasks); - mFirstScreenful = false; - } - } - @Override - protected Void doInBackground(Void... params) { - // We load in two stages: first, we update progress with just the first screenful - // of items. Then, we update with the rest of the items - final int origPri = Process.getThreadPriority(Process.myTid()); - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - final PackageManager pm = mContext.getPackageManager(); - final ActivityManager am = (ActivityManager) - mContext.getSystemService(Context.ACTIVITY_SERVICE); - - final List<ActivityManager.RecentTaskInfo> recentTasks = - 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); - - boolean firstScreenful = true; - ArrayList<TaskDescription> tasks = new ArrayList<TaskDescription>(); - - // skip the first task - assume it's either the home screen or the current activity. - final int first = 0; - for (int i = first, index = 0; i < numTasks && (index < MAX_TASKS); ++i) { - if (isCancelled()) { - break; - } - final ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(i); - - Intent intent = new Intent(recentInfo.baseIntent); - if (recentInfo.origActivity != null) { - intent.setComponent(recentInfo.origActivity); - } - - // Don't load the current home activity. - if (isCurrentHomeActivity(intent.getComponent(), homeInfo)) { - continue; - } - - // Don't load ourselves - if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) { - continue; - } - - TaskDescription item = createTaskDescription(recentInfo.id, - recentInfo.persistentId, recentInfo.baseIntent, - recentInfo.origActivity, recentInfo.description, - recentInfo.userId); - - if (item != null) { - while (true) { - try { - tasksWaitingForThumbnails.put(item); - break; - } catch (InterruptedException e) { - } - } - tasks.add(item); - if (firstScreenful && tasks.size() == mNumTasksInFirstScreenful) { - publishProgress(tasks); - tasks = new ArrayList<TaskDescription>(); - firstScreenful = false; - //break; - } - ++index; - } - } - - if (!isCancelled()) { - publishProgress(tasks); - if (firstScreenful) { - // always should publish two updates - publishProgress(new ArrayList<TaskDescription>()); - } - } - - while (true) { - try { - tasksWaitingForThumbnails.put(new TaskDescription()); - break; - } catch (InterruptedException e) { - } - } - - Process.setThreadPriority(origPri); - return null; - } - }; - mTaskLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - loadThumbnailsAndIconsInBackground(tasksWaitingForThumbnails); - } - - private void loadThumbnailsAndIconsInBackground( - final BlockingQueue<TaskDescription> tasksWaitingForThumbnails) { - // continually read items from tasksWaitingForThumbnails and load - // thumbnails and icons for them. finish thread when cancelled or there - // is a null item in tasksWaitingForThumbnails - mThumbnailLoader = new AsyncTask<Void, TaskDescription, Void>() { - @Override - protected void onProgressUpdate(TaskDescription... values) { - if (!isCancelled()) { - TaskDescription td = values[0]; - if (td.isNull()) { // end sentinel - mState = State.LOADED; - } else { - if (mRecentsPanel != null) { - mRecentsPanel.onTaskThumbnailLoaded(td); - } - } - } - } - @Override - protected Void doInBackground(Void... params) { - final int origPri = Process.getThreadPriority(Process.myTid()); - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - - while (true) { - if (isCancelled()) { - break; - } - TaskDescription td = null; - while (td == null) { - try { - td = tasksWaitingForThumbnails.take(); - } catch (InterruptedException e) { - } - } - if (td.isNull()) { // end sentinel - publishProgress(td); - break; - } - loadThumbnailAndIcon(td); - - publishProgress(td); - } - - Process.setThreadPriority(origPri); - return null; - } - }; - mThumbnailLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recent/Recents.java b/packages/SystemUI/src/com/android/systemui/recent/Recents.java deleted file mode 100644 index e9f3cf9..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/Recents.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.recent; - -import android.app.ActivityOptions; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Bitmap; -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.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 = true; - - // Which recents to use - boolean mUseAlternateRecents = true; - boolean mBootCompleted = false; - static AlternateRecentsComponent sAlternateRecents; - - /** Returns the Recents component, creating a new one in-process if necessary. */ - public static AlternateRecentsComponent getRecentsComponent(Context context, - boolean forceInitialize) { - if (sAlternateRecents == null) { - sAlternateRecents = new AlternateRecentsComponent(context); - if (forceInitialize) { - sAlternateRecents.onStart(); - sAlternateRecents.onBootCompleted(); - } - } - return sAlternateRecents; - } - - @Override - public void start() { - if (mUseAlternateRecents) { - if (sAlternateRecents == null) { - sAlternateRecents = getRecentsComponent(mContext, false); - } - sAlternateRecents.onStart(); - } - - putComponent(RecentsComponent.class, this); - } - - @Override - protected void onBootCompleted() { - if (mUseAlternateRecents) { - if (sAlternateRecents != null) { - sAlternateRecents.onBootCompleted(); - } - } - mBootCompleted = true; - } - - @Override - public void showRecents(boolean triggeredFromAltTab, View statusBarView) { - if (mUseAlternateRecents) { - sAlternateRecents.onShowRecents(triggeredFromAltTab); - } - } - - @Override - public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { - if (mUseAlternateRecents) { - sAlternateRecents.onHideRecents(triggeredFromAltTab, triggeredFromHomeKey); - } else { - Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT); - intent.setPackage("com.android.systemui"); - sendBroadcastSafely(intent); - - RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); - } - } - - @Override - public void toggleRecents(Display display, int layoutDirection, View statusBarView) { - if (mUseAlternateRecents) { - // Launch the alternate recents if required - sAlternateRecents.onToggleRecents(); - return; - } - - if (DEBUG) Log.d(TAG, "toggle recents panel"); - try { - TaskDescription firstTask = RecentTasksLoader.getInstance(mContext).getFirstTask(); - - Intent intent = new Intent(RecentsActivity.TOGGLE_RECENTS_INTENT); - intent.setClassName("com.android.systemui", - "com.android.systemui.recent.RecentsActivity"); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - - if (firstTask == null) { - if (RecentsActivity.forceOpaqueBackground(mContext)) { - ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, - R.anim.recents_launch_from_launcher_enter, - R.anim.recents_launch_from_launcher_exit); - mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle( - UserHandle.USER_CURRENT)); - } else { - // The correct window animation will be applied via the activity's style - mContext.startActivityAsUser(intent, new UserHandle( - UserHandle.USER_CURRENT)); - } - - } else { - Bitmap first = null; - if (firstTask.getThumbnail() instanceof BitmapDrawable) { - first = ((BitmapDrawable) firstTask.getThumbnail()).getBitmap(); - } else { - first = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); - Drawable d = RecentTasksLoader.getInstance(mContext).getDefaultThumbnail(); - d.draw(new Canvas(first)); - } - final Resources res = mContext.getResources(); - - float thumbWidth = res - .getDimensionPixelSize(R.dimen.status_bar_recents_thumbnail_width); - float thumbHeight = res - .getDimensionPixelSize(R.dimen.status_bar_recents_thumbnail_height); - if (first == null) { - throw new RuntimeException("Recents thumbnail is null"); - } - if (first.getWidth() != thumbWidth || first.getHeight() != thumbHeight) { - first = Bitmap.createScaledBitmap(first, (int) thumbWidth, (int) thumbHeight, - true); - if (first == null) { - throw new RuntimeException("Recents thumbnail is null"); - } - } - - - DisplayMetrics dm = new DisplayMetrics(); - display.getMetrics(dm); - // calculate it here, but consider moving it elsewhere - // first, determine which orientation you're in. - final Configuration config = res.getConfiguration(); - int x, y; - - if (config.orientation == Configuration.ORIENTATION_PORTRAIT) { - float appLabelLeftMargin = res.getDimensionPixelSize( - R.dimen.status_bar_recents_app_label_left_margin); - float appLabelWidth = res.getDimensionPixelSize( - R.dimen.status_bar_recents_app_label_width); - float thumbLeftMargin = res.getDimensionPixelSize( - R.dimen.status_bar_recents_thumbnail_left_margin); - float thumbBgPadding = res.getDimensionPixelSize( - R.dimen.status_bar_recents_thumbnail_bg_padding); - - float width = appLabelLeftMargin + - +appLabelWidth - + thumbLeftMargin - + thumbWidth - + 2 * thumbBgPadding; - - x = (int) ((dm.widthPixels - width) / 2f + appLabelLeftMargin + appLabelWidth - + thumbBgPadding + thumbLeftMargin); - y = (int) (dm.heightPixels - - res.getDimensionPixelSize(R.dimen.status_bar_recents_thumbnail_height) - - thumbBgPadding); - if (layoutDirection == View.LAYOUT_DIRECTION_RTL) { - x = dm.widthPixels - x - res.getDimensionPixelSize( - R.dimen.status_bar_recents_thumbnail_width); - } - - } else { // if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { - float thumbTopMargin = res.getDimensionPixelSize( - R.dimen.status_bar_recents_thumbnail_top_margin); - float thumbBgPadding = res.getDimensionPixelSize( - R.dimen.status_bar_recents_thumbnail_bg_padding); - float textPadding = res.getDimensionPixelSize( - R.dimen.status_bar_recents_text_description_padding); - float labelTextSize = res.getDimensionPixelSize( - R.dimen.status_bar_recents_app_label_text_size); - Paint p = new Paint(); - p.setTextSize(labelTextSize); - float labelTextHeight = p.getFontMetricsInt().bottom - - p.getFontMetricsInt().top; - float descriptionTextSize = res.getDimensionPixelSize( - R.dimen.status_bar_recents_app_description_text_size); - p.setTextSize(descriptionTextSize); - float descriptionTextHeight = p.getFontMetricsInt().bottom - - p.getFontMetricsInt().top; - - float statusBarHeight = res.getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_height); - float recentsItemTopPadding = statusBarHeight; - - float height = thumbTopMargin - + thumbHeight - + 2 * thumbBgPadding + textPadding + labelTextHeight - + recentsItemTopPadding + textPadding + descriptionTextHeight; - float recentsItemRightPadding = res - .getDimensionPixelSize(R.dimen.status_bar_recents_item_padding); - float recentsScrollViewRightPadding = res - .getDimensionPixelSize(R.dimen.status_bar_recents_right_glow_margin); - x = (int) (dm.widthPixels - res - .getDimensionPixelSize(R.dimen.status_bar_recents_thumbnail_width) - - thumbBgPadding - recentsItemRightPadding - - recentsScrollViewRightPadding); - y = (int) ((dm.heightPixels - statusBarHeight - height) / 2f + thumbTopMargin - + recentsItemTopPadding + thumbBgPadding + statusBarHeight); - } - - ActivityOptions opts = ActivityOptions.makeThumbnailScaleDownAnimation( - statusBarView, - first, x, y, - new ActivityOptions.OnAnimationStartedListener() { - public void onAnimationStarted() { - Intent intent = - new Intent(RecentsActivity.WINDOW_ANIMATION_START_INTENT); - intent.setPackage("com.android.systemui"); - sendBroadcastSafely(intent); - } - }); - intent.putExtra(RecentsActivity.WAITING_FOR_WINDOW_ANIMATION_PARAM, true); - startActivitySafely(intent, opts.toBundle()); - } - } catch (ActivityNotFoundException e) { - Log.e(TAG, "Failed to launch RecentAppsIntent", e); - } - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - if (mUseAlternateRecents) { - sAlternateRecents.onConfigurationChanged(newConfig); - } - } - - @Override - public void preloadRecents() { - if (mUseAlternateRecents) { - sAlternateRecents.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(); - } - } - - @Override - public void cancelPreloadingRecents() { - if (mUseAlternateRecents) { - sAlternateRecents.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(); - } - } - - @Override - public void showNextAffiliatedTask() { - if (mUseAlternateRecents) { - sAlternateRecents.onShowNextAffiliatedTask(); - } - } - - @Override - public void showPrevAffiliatedTask() { - if (mUseAlternateRecents) { - sAlternateRecents.onShowPrevAffiliatedTask(); - } - } - - @Override - public void setCallback(Callbacks cb) { - if (mUseAlternateRecents) { - sAlternateRecents.setRecentsComponentCallback(cb); - } - } - - /** - * 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 deleted file mode 100644 index 7ab40b0..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsActivity.java +++ /dev/null @@ -1,248 +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.recent; - -import android.app.Activity; -import android.app.ActivityManager; -import android.app.WallpaperManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.os.UserHandle; -import android.view.MotionEvent; -import android.view.View; -import android.view.WindowManager; - -import com.android.systemui.R; -import com.android.systemui.statusbar.StatusBarPanel; - -import java.util.List; - -public class RecentsActivity extends Activity { - public static final String TOGGLE_RECENTS_INTENT = "com.android.systemui.recent.action.TOGGLE_RECENTS"; - public static final String PRELOAD_INTENT = "com.android.systemui.recent.action.PRELOAD"; - public static final String CANCEL_PRELOAD_INTENT = "com.android.systemui.recent.CANCEL_PRELOAD"; - public static final String CLOSE_RECENTS_INTENT = "com.android.systemui.recent.action.CLOSE"; - public static final String WINDOW_ANIMATION_START_INTENT = "com.android.systemui.recent.action.WINDOW_ANIMATION_START"; - public static final String PRELOAD_PERMISSION = "com.android.systemui.recent.permission.PRELOAD"; - public static final String WAITING_FOR_WINDOW_ANIMATION_PARAM = "com.android.systemui.recent.WAITING_FOR_WINDOW_ANIMATION"; - private static final String WAS_SHOWING = "was_showing"; - - private RecentsPanelView mRecentsPanel; - private IntentFilter mIntentFilter; - private boolean mShowing; - private boolean mForeground; - - private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (CLOSE_RECENTS_INTENT.equals(intent.getAction())) { - if (mRecentsPanel != null && mRecentsPanel.isShowing()) { - if (mShowing && !mForeground) { - // Captures the case right before we transition to another activity - mRecentsPanel.show(false); - } - } - } else if (WINDOW_ANIMATION_START_INTENT.equals(intent.getAction())) { - if (mRecentsPanel != null) { - mRecentsPanel.onWindowAnimationStart(); - } - } - } - }; - - public class TouchOutsideListener implements View.OnTouchListener { - private StatusBarPanel mPanel; - - public TouchOutsideListener(StatusBarPanel panel) { - mPanel = panel; - } - - public boolean onTouch(View v, MotionEvent ev) { - final int action = ev.getAction(); - if (action == MotionEvent.ACTION_OUTSIDE - || (action == MotionEvent.ACTION_DOWN - && !mPanel.isInContentArea((int) ev.getX(), (int) ev.getY()))) { - dismissAndGoHome(); - return true; - } - return false; - } - } - - @Override - public void onPause() { - overridePendingTransition( - R.anim.recents_return_to_launcher_enter, - R.anim.recents_return_to_launcher_exit); - mForeground = false; - super.onPause(); - } - - @Override - public void onStop() { - mShowing = false; - if (mRecentsPanel != null) { - mRecentsPanel.onUiHidden(); - } - super.onStop(); - } - - private void updateWallpaperVisibility(boolean visible) { - int wpflags = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0; - int curflags = getWindow().getAttributes().flags - & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; - if (wpflags != curflags) { - getWindow().setFlags(wpflags, WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER); - } - } - - public static boolean forceOpaqueBackground(Context context) { - return WallpaperManager.getInstance(context).getWallpaperInfo() != null; - } - - @Override - public void onStart() { - // Hide wallpaper if it's not a static image - if (forceOpaqueBackground(this)) { - updateWallpaperVisibility(false); - } else { - updateWallpaperVisibility(true); - } - mShowing = true; - if (mRecentsPanel != null) { - // Call and refresh the recent tasks list in case we didn't preload tasks - // or in case we don't get an onNewIntent - mRecentsPanel.refreshRecentTasksList(); - mRecentsPanel.refreshViews(); - } - super.onStart(); - } - - @Override - public void onResume() { - mForeground = true; - super.onResume(); - } - - @Override - public void onBackPressed() { - dismissAndGoBack(); - } - - public void dismissAndGoHome() { - if (mRecentsPanel != null) { - Intent homeIntent = new Intent(Intent.ACTION_MAIN, null); - homeIntent.addCategory(Intent.CATEGORY_HOME); - homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - startActivityAsUser(homeIntent, new UserHandle(UserHandle.USER_CURRENT)); - mRecentsPanel.show(false); - } - } - - public void dismissAndGoBack() { - if (mRecentsPanel != null) { - final ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - - final List<ActivityManager.RecentTaskInfo> recentTasks = - am.getRecentTasks(2, - ActivityManager.RECENT_WITH_EXCLUDED | - 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 - return; - } - mRecentsPanel.show(false); - } - finish(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - getWindow().addPrivateFlags( - WindowManager.LayoutParams.PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR); - setContentView(R.layout.status_bar_recent_panel); - mRecentsPanel = (RecentsPanelView) findViewById(R.id.recents_root); - mRecentsPanel.setOnTouchListener(new TouchOutsideListener(mRecentsPanel)); - mRecentsPanel.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - - final RecentTasksLoader recentTasksLoader = RecentTasksLoader.getInstance(this); - recentTasksLoader.setRecentsPanel(mRecentsPanel, mRecentsPanel); - mRecentsPanel.setMinSwipeAlpha( - getResources().getInteger(R.integer.config_recent_item_min_alpha) / 100f); - - if (savedInstanceState == null || - savedInstanceState.getBoolean(WAS_SHOWING)) { - handleIntent(getIntent(), (savedInstanceState == null)); - } - mIntentFilter = new IntentFilter(); - mIntentFilter.addAction(CLOSE_RECENTS_INTENT); - mIntentFilter.addAction(WINDOW_ANIMATION_START_INTENT); - registerReceiver(mIntentReceiver, mIntentFilter); - super.onCreate(savedInstanceState); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - outState.putBoolean(WAS_SHOWING, mRecentsPanel.isShowing()); - } - - @Override - protected void onDestroy() { - RecentTasksLoader.getInstance(this).setRecentsPanel(null, mRecentsPanel); - unregisterReceiver(mIntentReceiver); - super.onDestroy(); - } - - @Override - protected void onNewIntent(Intent intent) { - handleIntent(intent, true); - } - - private void handleIntent(Intent intent, boolean checkWaitingForAnimationParam) { - super.onNewIntent(intent); - - if (TOGGLE_RECENTS_INTENT.equals(intent.getAction())) { - if (mRecentsPanel != null) { - if (mRecentsPanel.isShowing()) { - dismissAndGoBack(); - } else { - final RecentTasksLoader recentTasksLoader = RecentTasksLoader.getInstance(this); - boolean waitingForWindowAnimation = checkWaitingForAnimationParam && - intent.getBooleanExtra(WAITING_FOR_WINDOW_ANIMATION_PARAM, false); - mRecentsPanel.show(true, recentTasksLoader.getLoadedTasks(), - recentTasksLoader.isFirstScreenful(), waitingForWindowAnimation); - } - } - } - } - - boolean isForeground() { - return mForeground; - } - - boolean isActivityShowing() { - return mShowing; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java deleted file mode 100644 index cf5d3a6..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java +++ /dev/null @@ -1,391 +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.recent; - -import android.animation.LayoutTransition; -import android.content.Context; -import android.content.res.Configuration; -import android.database.DataSetObserver; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewTreeObserver; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.widget.HorizontalScrollView; -import android.widget.LinearLayout; - -import com.android.systemui.R; -import com.android.systemui.SwipeHelper; -import com.android.systemui.recent.RecentsPanelView.TaskDescriptionAdapter; - -import java.util.HashSet; -import java.util.Iterator; - -public class RecentsHorizontalScrollView extends HorizontalScrollView - implements SwipeHelper.Callback, RecentsPanelView.RecentsScrollView { - private static final String TAG = RecentsPanelView.TAG; - private static final boolean DEBUG = RecentsPanelView.DEBUG; - private LinearLayout mLinearLayout; - private TaskDescriptionAdapter mAdapter; - private RecentsCallback mCallback; - protected int mLastScrollPosition; - private SwipeHelper mSwipeHelper; - private FadedEdgeDrawHelper mFadedEdgeDrawHelper; - private HashSet<View> mRecycledViews; - private int mNumItemsInOneScreenful; - private Runnable mOnScrollListener; - - public RecentsHorizontalScrollView(Context context, AttributeSet attrs) { - super(context, attrs, 0); - mSwipeHelper = new SwipeHelper(SwipeHelper.Y, this, context); - mFadedEdgeDrawHelper = FadedEdgeDrawHelper.create(context, attrs, this, false); - mRecycledViews = new HashSet<View>(); - } - - public void setMinSwipeAlpha(float minAlpha) { - mSwipeHelper.setMinSwipeProgress(minAlpha); - } - - private int scrollPositionOfMostRecent() { - return mLinearLayout.getWidth() - getWidth(); - } - - private void addToRecycledViews(View v) { - if (mRecycledViews.size() < mNumItemsInOneScreenful) { - mRecycledViews.add(v); - } - } - - public View findViewForTask(int persistentTaskId) { - for (int i = 0; i < mLinearLayout.getChildCount(); i++) { - View v = mLinearLayout.getChildAt(i); - RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) v.getTag(); - if (holder.taskDescription.persistentTaskId == persistentTaskId) { - return v; - } - } - return null; - } - - private void update() { - for (int i = 0; i < mLinearLayout.getChildCount(); i++) { - View v = mLinearLayout.getChildAt(i); - addToRecycledViews(v); - mAdapter.recycleView(v); - } - LayoutTransition transitioner = getLayoutTransition(); - setLayoutTransition(null); - - mLinearLayout.removeAllViews(); - Iterator<View> recycledViews = mRecycledViews.iterator(); - for (int i = 0; i < mAdapter.getCount(); i++) { - View old = null; - if (recycledViews.hasNext()) { - old = recycledViews.next(); - recycledViews.remove(); - old.setVisibility(VISIBLE); - } - - final View view = mAdapter.getView(i, old, mLinearLayout); - - if (mFadedEdgeDrawHelper != null) { - mFadedEdgeDrawHelper.addViewCallback(view); - } - - OnTouchListener noOpListener = new OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return true; - } - }; - - view.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - mCallback.dismiss(); - } - }); - // We don't want a click sound when we dimiss recents - view.setSoundEffectsEnabled(false); - - OnClickListener launchAppListener = new OnClickListener() { - public void onClick(View v) { - mCallback.handleOnClick(view); - } - }; - - RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) view.getTag(); - final View thumbnailView = holder.thumbnailView; - OnLongClickListener longClickListener = new OnLongClickListener() { - public boolean onLongClick(View v) { - final View anchorView = view.findViewById(R.id.app_description); - mCallback.handleLongPress(view, anchorView, thumbnailView); - return true; - } - }; - thumbnailView.setClickable(true); - thumbnailView.setOnClickListener(launchAppListener); - thumbnailView.setOnLongClickListener(longClickListener); - - // We don't want to dismiss recents if a user clicks on the app title - // (we also don't want to launch the app either, though, because the - // app title is a small target and doesn't have great click feedback) - final View appTitle = view.findViewById(R.id.app_label); - appTitle.setContentDescription(" "); - appTitle.setOnTouchListener(noOpListener); - mLinearLayout.addView(view); - } - setLayoutTransition(transitioner); - - // Scroll to end after initial layout. - - final OnGlobalLayoutListener updateScroll = new OnGlobalLayoutListener() { - public void onGlobalLayout() { - mLastScrollPosition = scrollPositionOfMostRecent(); - scrollTo(mLastScrollPosition, 0); - final ViewTreeObserver observer = getViewTreeObserver(); - if (observer.isAlive()) { - observer.removeOnGlobalLayoutListener(this); - } - } - }; - getViewTreeObserver().addOnGlobalLayoutListener(updateScroll); - } - - @Override - public void removeViewInLayout(final View view) { - dismissChild(view); - } - - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); - return mSwipeHelper.onInterceptTouchEvent(ev) || - super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - return mSwipeHelper.onTouchEvent(ev) || - super.onTouchEvent(ev); - } - - public boolean canChildBeDismissed(View v) { - return true; - } - - @Override - public boolean isAntiFalsingNeeded() { - return false; - } - - @Override - public float getFalsingThresholdFactor() { - return 1.0f; - } - - public void dismissChild(View v) { - mSwipeHelper.dismissChild(v, 0); - } - - public void onChildDismissed(View v) { - addToRecycledViews(v); - mLinearLayout.removeView(v); - mCallback.handleSwipe(v); - // Restore the alpha/translation parameters to what they were before swiping - // (for when these items are recycled) - View contentView = getChildContentView(v); - contentView.setAlpha(1f); - contentView.setTranslationY(0); - } - - public void onBeginDrag(View v) { - // We do this so the underlying ScrollView knows that it won't get - // the chance to intercept events anymore - requestDisallowInterceptTouchEvent(true); - } - - public void onDragCancelled(View v) { - } - - @Override - public void onChildSnappedBack(View animView) { - } - - @Override - public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { - return false; - } - - public View getChildAtPosition(MotionEvent ev) { - final float x = ev.getX() + getScrollX(); - final float y = ev.getY() + getScrollY(); - for (int i = 0; i < mLinearLayout.getChildCount(); i++) { - View item = mLinearLayout.getChildAt(i); - if (x >= item.getLeft() && x < item.getRight() - && y >= item.getTop() && y < item.getBottom()) { - return item; - } - } - return null; - } - - public View getChildContentView(View v) { - return v.findViewById(R.id.recent_item); - } - - @Override - public void drawFadedEdges(Canvas canvas, int left, int right, int top, int bottom) { - if (mFadedEdgeDrawHelper != null) { - - mFadedEdgeDrawHelper.drawCallback(canvas, - left, right, top, bottom, getScrollX(), getScrollY(), - 0, 0, - getLeftFadingEdgeStrength(), getRightFadingEdgeStrength(), getPaddingTop()); - } - } - - @Override - protected void onScrollChanged(int l, int t, int oldl, int oldt) { - super.onScrollChanged(l, t, oldl, oldt); - if (mOnScrollListener != null) { - mOnScrollListener.run(); - } - } - - public void setOnScrollListener(Runnable listener) { - mOnScrollListener = listener; - } - - @Override - public int getVerticalFadingEdgeLength() { - if (mFadedEdgeDrawHelper != null) { - return mFadedEdgeDrawHelper.getVerticalFadingEdgeLength(); - } else { - return super.getVerticalFadingEdgeLength(); - } - } - - @Override - public int getHorizontalFadingEdgeLength() { - if (mFadedEdgeDrawHelper != null) { - return mFadedEdgeDrawHelper.getHorizontalFadingEdgeLength(); - } else { - return super.getHorizontalFadingEdgeLength(); - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - setScrollbarFadingEnabled(true); - mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout); - final int leftPadding = getContext().getResources() - .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin); - setOverScrollEffectPadding(leftPadding, 0); - } - - @Override - public void onAttachedToWindow() { - if (mFadedEdgeDrawHelper != null) { - mFadedEdgeDrawHelper.onAttachedToWindowCallback(mLinearLayout, isHardwareAccelerated()); - } - } - - @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); - } - - private void setOverScrollEffectPadding(int leftPadding, int i) { - // TODO Add to (Vertical)ScrollView - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - // Skip this work if a transition is running; it sets the scroll values independently - // and should not have those animated values clobbered by this logic - LayoutTransition transition = mLinearLayout.getLayoutTransition(); - if (transition != null && transition.isRunning()) { - return; - } - // Keep track of the last visible item in the list so we can restore it - // to the bottom when the orientation changes. - mLastScrollPosition = scrollPositionOfMostRecent(); - - // This has to happen post-layout, so run it "in the future" - post(new Runnable() { - public void run() { - // Make sure we're still not clobbering the transition-set values, since this - // runnable launches asynchronously - LayoutTransition transition = mLinearLayout.getLayoutTransition(); - if (transition == null || !transition.isRunning()) { - scrollTo(mLastScrollPosition, 0); - } - } - }); - } - - public void setAdapter(TaskDescriptionAdapter adapter) { - mAdapter = adapter; - mAdapter.registerDataSetObserver(new DataSetObserver() { - public void onChanged() { - update(); - } - - public void onInvalidated() { - update(); - } - }); - DisplayMetrics dm = getResources().getDisplayMetrics(); - int childWidthMeasureSpec = - MeasureSpec.makeMeasureSpec(dm.widthPixels, MeasureSpec.AT_MOST); - int childheightMeasureSpec = - MeasureSpec.makeMeasureSpec(dm.heightPixels, MeasureSpec.AT_MOST); - View child = mAdapter.createView(mLinearLayout); - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - mNumItemsInOneScreenful = - (int) Math.ceil(dm.widthPixels / (double) child.getMeasuredWidth()); - addToRecycledViews(child); - - for (int i = 0; i < mNumItemsInOneScreenful - 1; i++) { - addToRecycledViews(mAdapter.createView(mLinearLayout)); - } - } - - public int numItemsInOneScreenful() { - return mNumItemsInOneScreenful; - } - - @Override - public void setLayoutTransition(LayoutTransition transition) { - // The layout transition applies to our embedded LinearLayout - mLinearLayout.setLayoutTransition(transition); - } - - public void setCallback(RecentsCallback callback) { - mCallback = callback; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java deleted file mode 100644 index 4c3460e..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java +++ /dev/null @@ -1,813 +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.recent; - -import android.animation.Animator; -import android.animation.LayoutTransition; -import android.animation.TimeInterpolator; -import android.app.ActivityManager; -import android.app.ActivityManagerNative; -import android.app.ActivityOptions; -import android.app.TaskStackBuilder; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Shader.TileMode; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.os.RemoteException; -import android.os.UserHandle; -import android.provider.Settings; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewPropertyAnimator; -import android.view.ViewRootImpl; -import android.view.accessibility.AccessibilityEvent; -import android.view.animation.AnimationUtils; -import android.view.animation.DecelerateInterpolator; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.BaseAdapter; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.ImageView.ScaleType; -import android.widget.PopupMenu; -import android.widget.TextView; - -import com.android.systemui.R; -import com.android.systemui.statusbar.BaseStatusBar; -import com.android.systemui.statusbar.StatusBarPanel; -import com.android.systemui.statusbar.phone.PhoneStatusBar; - -import java.util.ArrayList; - -public class RecentsPanelView extends FrameLayout implements OnItemClickListener, RecentsCallback, - StatusBarPanel, Animator.AnimatorListener { - static final String TAG = "RecentsPanelView"; - static final boolean DEBUG = PhoneStatusBar.DEBUG || false; - private PopupMenu mPopup; - private View mRecentsScrim; - private View mRecentsNoApps; - private RecentsScrollView mRecentsContainer; - - private boolean mShowing; - private boolean mWaitingToShow; - private ViewHolder mItemToAnimateInWhenWindowAnimationIsFinished; - private boolean mAnimateIconOfFirstTask; - private boolean mWaitingForWindowAnimation; - private long mWindowAnimationStartTime; - private boolean mCallUiHiddenBeforeNextReload; - - private RecentTasksLoader mRecentTasksLoader; - private ArrayList<TaskDescription> mRecentTaskDescriptions; - private TaskDescriptionAdapter mListAdapter; - private int mThumbnailWidth; - private boolean mFitThumbnailToXY; - private int mRecentItemLayoutId; - private boolean mHighEndGfx; - - public static interface RecentsScrollView { - public int numItemsInOneScreenful(); - public void setAdapter(TaskDescriptionAdapter adapter); - public void setCallback(RecentsCallback callback); - public void setMinSwipeAlpha(float minAlpha); - public View findViewForTask(int persistentTaskId); - public void drawFadedEdges(Canvas c, int left, int right, int top, int bottom); - public void setOnScrollListener(Runnable listener); - } - - private final class OnLongClickDelegate implements View.OnLongClickListener { - View mOtherView; - OnLongClickDelegate(View other) { - mOtherView = other; - } - public boolean onLongClick(View v) { - return mOtherView.performLongClick(); - } - } - - /* package */ final static class ViewHolder { - View thumbnailView; - ImageView thumbnailViewImage; - Drawable thumbnailViewDrawable; - ImageView iconView; - TextView labelView; - TextView descriptionView; - View calloutLine; - TaskDescription taskDescription; - boolean loadedThumbnailAndIcon; - } - - /* package */ final class TaskDescriptionAdapter extends BaseAdapter { - private LayoutInflater mInflater; - - public TaskDescriptionAdapter(Context context) { - mInflater = LayoutInflater.from(context); - } - - public int getCount() { - return mRecentTaskDescriptions != null ? mRecentTaskDescriptions.size() : 0; - } - - public Object getItem(int position) { - return position; // we only need the index - } - - public long getItemId(int position) { - return position; // we just need something unique for this position - } - - public View createView(ViewGroup parent) { - View convertView = mInflater.inflate(mRecentItemLayoutId, parent, false); - ViewHolder holder = new ViewHolder(); - holder.thumbnailView = convertView.findViewById(R.id.app_thumbnail); - holder.thumbnailViewImage = - (ImageView) convertView.findViewById(R.id.app_thumbnail_image); - // If we set the default thumbnail now, we avoid an onLayout when we update - // the thumbnail later (if they both have the same dimensions) - updateThumbnail(holder, mRecentTasksLoader.getDefaultThumbnail(), false, false); - holder.iconView = (ImageView) convertView.findViewById(R.id.app_icon); - holder.iconView.setImageDrawable(mRecentTasksLoader.getDefaultIcon()); - holder.labelView = (TextView) convertView.findViewById(R.id.app_label); - holder.calloutLine = convertView.findViewById(R.id.recents_callout_line); - holder.descriptionView = (TextView) convertView.findViewById(R.id.app_description); - - convertView.setTag(holder); - return convertView; - } - - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = createView(parent); - } - final ViewHolder holder = (ViewHolder) convertView.getTag(); - - // index is reverse since most recent appears at the bottom... - final int index = mRecentTaskDescriptions.size() - position - 1; - - final TaskDescription td = mRecentTaskDescriptions.get(index); - - holder.labelView.setText(td.getLabel()); - holder.thumbnailView.setContentDescription(td.getLabel()); - holder.loadedThumbnailAndIcon = td.isLoaded(); - if (td.isLoaded()) { - updateThumbnail(holder, td.getThumbnail(), true, false); - updateIcon(holder, td.getIcon(), true, false); - } - if (index == 0) { - if (mAnimateIconOfFirstTask) { - ViewHolder oldHolder = mItemToAnimateInWhenWindowAnimationIsFinished; - if (oldHolder != null) { - oldHolder.iconView.setAlpha(1f); - oldHolder.iconView.setTranslationX(0f); - oldHolder.iconView.setTranslationY(0f); - oldHolder.labelView.setAlpha(1f); - oldHolder.labelView.setTranslationX(0f); - oldHolder.labelView.setTranslationY(0f); - if (oldHolder.calloutLine != null) { - oldHolder.calloutLine.setAlpha(1f); - oldHolder.calloutLine.setTranslationX(0f); - oldHolder.calloutLine.setTranslationY(0f); - } - } - mItemToAnimateInWhenWindowAnimationIsFinished = holder; - int translation = -getResources().getDimensionPixelSize( - R.dimen.status_bar_recents_app_icon_translate_distance); - final Configuration config = getResources().getConfiguration(); - if (config.orientation == Configuration.ORIENTATION_PORTRAIT) { - if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { - translation = -translation; - } - holder.iconView.setAlpha(0f); - holder.iconView.setTranslationX(translation); - holder.labelView.setAlpha(0f); - holder.labelView.setTranslationX(translation); - holder.calloutLine.setAlpha(0f); - holder.calloutLine.setTranslationX(translation); - } else { - holder.iconView.setAlpha(0f); - holder.iconView.setTranslationY(translation); - } - if (!mWaitingForWindowAnimation) { - animateInIconOfFirstTask(); - } - } - } - - holder.thumbnailView.setTag(td); - holder.thumbnailView.setOnLongClickListener(new OnLongClickDelegate(convertView)); - holder.taskDescription = td; - return convertView; - } - - public void recycleView(View v) { - ViewHolder holder = (ViewHolder) v.getTag(); - updateThumbnail(holder, mRecentTasksLoader.getDefaultThumbnail(), false, false); - holder.iconView.setImageDrawable(mRecentTasksLoader.getDefaultIcon()); - holder.iconView.setVisibility(INVISIBLE); - holder.iconView.animate().cancel(); - holder.labelView.setText(null); - holder.labelView.animate().cancel(); - holder.thumbnailView.setContentDescription(null); - holder.thumbnailView.setTag(null); - holder.thumbnailView.setOnLongClickListener(null); - holder.thumbnailView.setVisibility(INVISIBLE); - holder.iconView.setAlpha(1f); - holder.iconView.setTranslationX(0f); - holder.iconView.setTranslationY(0f); - holder.labelView.setAlpha(1f); - holder.labelView.setTranslationX(0f); - holder.labelView.setTranslationY(0f); - if (holder.calloutLine != null) { - holder.calloutLine.setAlpha(1f); - holder.calloutLine.setTranslationX(0f); - holder.calloutLine.setTranslationY(0f); - holder.calloutLine.animate().cancel(); - } - holder.taskDescription = null; - holder.loadedThumbnailAndIcon = false; - } - } - - public RecentsPanelView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public RecentsPanelView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - updateValuesFromResources(); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecentsPanelView, - defStyle, 0); - - mRecentItemLayoutId = a.getResourceId(R.styleable.RecentsPanelView_recentItemLayout, 0); - mRecentTasksLoader = RecentTasksLoader.getInstance(context); - a.recycle(); - } - - public int numItemsInOneScreenful() { - return mRecentsContainer.numItemsInOneScreenful(); - } - - private boolean pointInside(int x, int y, View v) { - final int l = v.getLeft(); - final int r = v.getRight(); - final int t = v.getTop(); - final int b = v.getBottom(); - return x >= l && x < r && y >= t && y < b; - } - - public boolean isInContentArea(int x, int y) { - return pointInside(x, y, (View) mRecentsContainer); - } - - public void show(boolean show) { - show(show, null, false, false); - } - - public void show(boolean show, ArrayList<TaskDescription> recentTaskDescriptions, - boolean firstScreenful, boolean animateIconOfFirstTask) { - if (show && mCallUiHiddenBeforeNextReload) { - onUiHidden(); - recentTaskDescriptions = null; - mAnimateIconOfFirstTask = false; - mWaitingForWindowAnimation = false; - } else { - mAnimateIconOfFirstTask = animateIconOfFirstTask; - mWaitingForWindowAnimation = animateIconOfFirstTask; - } - if (show) { - mWaitingToShow = true; - refreshRecentTasksList(recentTaskDescriptions, firstScreenful); - showIfReady(); - } else { - showImpl(false); - } - } - - private void showIfReady() { - // mWaitingToShow => there was a touch up on the recents button - // mRecentTaskDescriptions != null => we've created views for the first screenful of items - if (mWaitingToShow && mRecentTaskDescriptions != null) { - showImpl(true); - } - } - - static void sendCloseSystemWindows(Context context, String reason) { - if (ActivityManagerNative.isSystemReady()) { - try { - ActivityManagerNative.getDefault().closeSystemDialogs(reason); - } catch (RemoteException e) { - } - } - } - - private void showImpl(boolean show) { - sendCloseSystemWindows(getContext(), BaseStatusBar.SYSTEM_DIALOG_REASON_RECENT_APPS); - - mShowing = show; - - if (show) { - // if there are no apps, bring up a "No recent apps" message - boolean noApps = mRecentTaskDescriptions != null - && (mRecentTaskDescriptions.size() == 0); - mRecentsNoApps.setAlpha(1f); - mRecentsNoApps.setVisibility(noApps ? View.VISIBLE : View.INVISIBLE); - - onAnimationEnd(null); - setFocusable(true); - setFocusableInTouchMode(true); - requestFocus(); - } else { - mWaitingToShow = false; - // call onAnimationEnd() and clearRecentTasksList() in onUiHidden() - mCallUiHiddenBeforeNextReload = true; - if (mPopup != null) { - mPopup.dismiss(); - } - } - } - - protected void onAttachedToWindow () { - super.onAttachedToWindow(); - final ViewRootImpl root = getViewRootImpl(); - if (root != null) { - root.setDrawDuringWindowsAnimating(true); - } - } - - public void onUiHidden() { - mCallUiHiddenBeforeNextReload = false; - if (!mShowing && mRecentTaskDescriptions != null) { - onAnimationEnd(null); - clearRecentTasksList(); - } - } - - public void dismiss() { - ((RecentsActivity) getContext()).dismissAndGoHome(); - } - - public void dismissAndGoBack() { - ((RecentsActivity) getContext()).dismissAndGoBack(); - } - - public void onAnimationCancel(Animator animation) { - } - - public void onAnimationEnd(Animator animation) { - if (mShowing) { - final LayoutTransition transitioner = new LayoutTransition(); - ((ViewGroup)mRecentsContainer).setLayoutTransition(transitioner); - createCustomAnimations(transitioner); - } else { - ((ViewGroup)mRecentsContainer).setLayoutTransition(null); - } - } - - public void onAnimationRepeat(Animator animation) { - } - - public void onAnimationStart(Animator animation) { - } - - @Override - public boolean dispatchHoverEvent(MotionEvent event) { - // Ignore hover events outside of this panel bounds since such events - // generate spurious accessibility events with the panel content when - // tapping outside of it, thus confusing the user. - final int x = (int) event.getX(); - final int y = (int) event.getY(); - if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { - return super.dispatchHoverEvent(event); - } - return true; - } - - /** - * Whether the panel is showing, or, if it's animating, whether it will be - * when the animation is done. - */ - public boolean isShowing() { - return mShowing; - } - - public void setRecentTasksLoader(RecentTasksLoader loader) { - mRecentTasksLoader = loader; - } - - public void updateValuesFromResources() { - 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); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - mRecentsContainer = (RecentsScrollView) findViewById(R.id.recents_container); - mRecentsContainer.setOnScrollListener(new Runnable() { - public void run() { - // need to redraw the faded edges - invalidate(); - } - }); - mListAdapter = new TaskDescriptionAdapter(getContext()); - mRecentsContainer.setAdapter(mListAdapter); - mRecentsContainer.setCallback(this); - - mRecentsScrim = findViewById(R.id.recents_bg_protect); - mRecentsNoApps = findViewById(R.id.recents_no_apps); - - if (mRecentsScrim != null) { - mHighEndGfx = ActivityManager.isHighEndGfx(); - if (!mHighEndGfx) { - mRecentsScrim.setBackground(null); - } else if (mRecentsScrim.getBackground() instanceof BitmapDrawable) { - // In order to save space, we make the background texture repeat in the Y direction - ((BitmapDrawable) mRecentsScrim.getBackground()).setTileModeY(TileMode.REPEAT); - } - } - } - - public void setMinSwipeAlpha(float minAlpha) { - mRecentsContainer.setMinSwipeAlpha(minAlpha); - } - - private void createCustomAnimations(LayoutTransition transitioner) { - transitioner.setDuration(200); - transitioner.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0); - transitioner.setAnimator(LayoutTransition.DISAPPEARING, null); - } - - private void updateIcon(ViewHolder h, Drawable icon, boolean show, boolean anim) { - if (icon != null) { - h.iconView.setImageDrawable(icon); - if (show && h.iconView.getVisibility() != View.VISIBLE) { - if (anim) { - h.iconView.setAnimation( - AnimationUtils.loadAnimation(getContext(), R.anim.recent_appear)); - } - h.iconView.setVisibility(View.VISIBLE); - } - } - } - - private void updateThumbnail(ViewHolder h, Drawable thumbnail, boolean show, boolean anim) { - if (thumbnail != null) { - // Should remove the default image in the frame - // that this now covers, to improve scrolling speed. - // That can't be done until the anim is complete though. - h.thumbnailViewImage.setImageDrawable(thumbnail); - - // scale the image to fill the full width of the ImageView. do this only if - // we haven't set a bitmap before, or if the bitmap size has changed - if (h.thumbnailViewDrawable == null || - h.thumbnailViewDrawable.getIntrinsicWidth() != thumbnail.getIntrinsicWidth() || - h.thumbnailViewDrawable.getIntrinsicHeight() != thumbnail.getIntrinsicHeight()) { - if (mFitThumbnailToXY) { - h.thumbnailViewImage.setScaleType(ScaleType.FIT_XY); - } else { - Matrix scaleMatrix = new Matrix(); - float scale = mThumbnailWidth / (float) thumbnail.getIntrinsicWidth(); - scaleMatrix.setScale(scale, scale); - h.thumbnailViewImage.setScaleType(ScaleType.MATRIX); - h.thumbnailViewImage.setImageMatrix(scaleMatrix); - } - } - if (show && h.thumbnailView.getVisibility() != View.VISIBLE) { - if (anim) { - h.thumbnailView.setAnimation( - AnimationUtils.loadAnimation(getContext(), R.anim.recent_appear)); - } - h.thumbnailView.setVisibility(View.VISIBLE); - } - h.thumbnailViewDrawable = thumbnail; - } - } - - void onTaskThumbnailLoaded(TaskDescription td) { - synchronized (td) { - if (mRecentsContainer != null) { - ViewGroup container = (ViewGroup) mRecentsContainer; - if (container instanceof RecentsScrollView) { - container = (ViewGroup) container.findViewById( - R.id.recents_linear_layout); - } - // Look for a view showing this thumbnail, to update. - for (int i=0; i < container.getChildCount(); i++) { - View v = container.getChildAt(i); - if (v.getTag() instanceof ViewHolder) { - ViewHolder h = (ViewHolder)v.getTag(); - if (!h.loadedThumbnailAndIcon && h.taskDescription == td) { - // only fade in the thumbnail if recents is already visible-- we - // show it immediately otherwise - //boolean animateShow = mShowing && - // mRecentsContainer.getAlpha() > ViewConfiguration.ALPHA_THRESHOLD; - boolean animateShow = false; - updateIcon(h, td.getIcon(), true, animateShow); - updateThumbnail(h, td.getThumbnail(), true, animateShow); - h.loadedThumbnailAndIcon = true; - } - } - } - } - } - showIfReady(); - } - - private void animateInIconOfFirstTask() { - if (mItemToAnimateInWhenWindowAnimationIsFinished != null && - !mRecentTasksLoader.isFirstScreenful()) { - int timeSinceWindowAnimation = - (int) (System.currentTimeMillis() - mWindowAnimationStartTime); - final int minStartDelay = 150; - final int startDelay = Math.max(0, Math.min( - minStartDelay - timeSinceWindowAnimation, minStartDelay)); - final int duration = 250; - final ViewHolder holder = mItemToAnimateInWhenWindowAnimationIsFinished; - final TimeInterpolator cubic = new DecelerateInterpolator(1.5f); - FirstFrameAnimatorHelper.initializeDrawListener(holder.iconView); - for (View v : - new View[] { holder.iconView, holder.labelView, holder.calloutLine }) { - if (v != null) { - ViewPropertyAnimator vpa = v.animate().translationX(0).translationY(0) - .alpha(1f).setStartDelay(startDelay) - .setDuration(duration).setInterpolator(cubic); - FirstFrameAnimatorHelper h = new FirstFrameAnimatorHelper(vpa, v); - } - } - mItemToAnimateInWhenWindowAnimationIsFinished = null; - mAnimateIconOfFirstTask = false; - } - } - - public void onWindowAnimationStart() { - mWaitingForWindowAnimation = false; - mWindowAnimationStartTime = System.currentTimeMillis(); - animateInIconOfFirstTask(); - } - - public void clearRecentTasksList() { - // Clear memory used by screenshots - if (mRecentTaskDescriptions != null) { - mRecentTasksLoader.cancelLoadingThumbnailsAndIcons(this); - onTaskLoadingCancelled(); - } - } - - public void onTaskLoadingCancelled() { - // Gets called by RecentTasksLoader when it's cancelled - if (mRecentTaskDescriptions != null) { - mRecentTaskDescriptions = null; - mListAdapter.notifyDataSetInvalidated(); - } - } - - public void refreshViews() { - mListAdapter.notifyDataSetInvalidated(); - updateUiElements(); - showIfReady(); - } - - public void refreshRecentTasksList() { - refreshRecentTasksList(null, false); - } - - private void refreshRecentTasksList( - ArrayList<TaskDescription> recentTasksList, boolean firstScreenful) { - if (mRecentTaskDescriptions == null && recentTasksList != null) { - onTasksLoaded(recentTasksList, firstScreenful); - } else { - mRecentTasksLoader.loadTasksInBackground(); - } - } - - public void onTasksLoaded(ArrayList<TaskDescription> tasks, boolean firstScreenful) { - if (mRecentTaskDescriptions == null) { - mRecentTaskDescriptions = new ArrayList<TaskDescription>(tasks); - } else { - mRecentTaskDescriptions.addAll(tasks); - } - if (((RecentsActivity) getContext()).isActivityShowing()) { - refreshViews(); - } - } - - private void updateUiElements() { - final int items = mRecentTaskDescriptions != null - ? mRecentTaskDescriptions.size() : 0; - - ((View) mRecentsContainer).setVisibility(items > 0 ? View.VISIBLE : View.GONE); - - // Set description for accessibility - int numRecentApps = mRecentTaskDescriptions != null - ? mRecentTaskDescriptions.size() : 0; - String recentAppsAccessibilityDescription; - if (numRecentApps == 0) { - recentAppsAccessibilityDescription = - getResources().getString(R.string.status_bar_no_recent_apps); - } else { - recentAppsAccessibilityDescription = getResources().getQuantityString( - R.plurals.status_bar_accessibility_recent_apps, numRecentApps, numRecentApps); - } - setContentDescription(recentAppsAccessibilityDescription); - } - - public boolean simulateClick(int persistentTaskId) { - View v = mRecentsContainer.findViewForTask(persistentTaskId); - if (v != null) { - handleOnClick(v); - return true; - } - return false; - } - - public void handleOnClick(View view) { - ViewHolder holder = (ViewHolder) view.getTag(); - TaskDescription ad = holder.taskDescription; - final Context context = view.getContext(); - final ActivityManager am = (ActivityManager) - context.getSystemService(Context.ACTIVITY_SERVICE); - - Bitmap bm = null; - boolean usingDrawingCache = true; - if (holder.thumbnailViewDrawable instanceof BitmapDrawable) { - bm = ((BitmapDrawable) holder.thumbnailViewDrawable).getBitmap(); - if (bm.getWidth() == holder.thumbnailViewImage.getWidth() && - bm.getHeight() == holder.thumbnailViewImage.getHeight()) { - usingDrawingCache = false; - } - } - if (usingDrawingCache) { - holder.thumbnailViewImage.setDrawingCacheEnabled(true); - bm = holder.thumbnailViewImage.getDrawingCache(); - } - Bundle opts = (bm == null) ? - null : - ActivityOptions.makeThumbnailScaleUpAnimation( - holder.thumbnailViewImage, bm, 0, 0, null).toBundle(); - - show(false); - if (ad.taskId >= 0) { - // This is an active task; it should just go to the foreground. - am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME, - opts); - } else { - Intent intent = ad.intent; - intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY - | Intent.FLAG_ACTIVITY_TASK_ON_HOME - | Intent.FLAG_ACTIVITY_NEW_TASK); - if (DEBUG) Log.v(TAG, "Starting activity " + intent); - try { - context.startActivityAsUser(intent, opts, - new UserHandle(ad.userId)); - } catch (SecurityException e) { - Log.e(TAG, "Recents does not have the permission to launch " + intent, e); - } catch (ActivityNotFoundException e) { - Log.e(TAG, "Error launching activity " + intent, e); - } - } - if (usingDrawingCache) { - holder.thumbnailViewImage.setDrawingCacheEnabled(false); - } - } - - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - handleOnClick(view); - } - - public void handleSwipe(View view) { - TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription; - if (ad == null) { - Log.v(TAG, "Not able to find activity description for swiped task; view=" + view + - " tag=" + view.getTag()); - return; - } - if (DEBUG) Log.v(TAG, "Jettison " + ad.getLabel()); - mRecentTaskDescriptions.remove(ad); - mRecentTasksLoader.remove(ad); - - // Handled by widget containers to enable LayoutTransitions properly - // mListAdapter.notifyDataSetChanged(); - - if (mRecentTaskDescriptions.size() == 0) { - dismissAndGoBack(); - } - - // Currently, either direction means the same thing, so ignore direction and remove - // the task. - final ActivityManager am = (ActivityManager) - getContext().getSystemService(Context.ACTIVITY_SERVICE); - if (am != null) { - am.removeTask(ad.persistentTaskId); - - // Accessibility feedback - setContentDescription( - getContext().getString(R.string.accessibility_recents_item_dismissed, ad.getLabel())); - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - setContentDescription(null); - } - } - - private void startApplicationDetailsActivity(String packageName, int userId) { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", packageName, null)); - intent.setComponent(intent.resolveActivity(getContext().getPackageManager())); - TaskStackBuilder.create(getContext()) - .addNextIntentWithParentStack(intent).startActivities(null, new UserHandle(userId)); - } - - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (mPopup != null) { - return true; - } else { - return super.onInterceptTouchEvent(ev); - } - } - - public void handleLongPress( - final View selectedView, final View anchorView, final View thumbnailView) { - thumbnailView.setSelected(true); - final PopupMenu popup = - new PopupMenu(getContext(), anchorView == null ? selectedView : anchorView); - mPopup = popup; - popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu()); - popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - if (item.getItemId() == R.id.recent_remove_item) { - ((ViewGroup) mRecentsContainer).removeViewInLayout(selectedView); - } else if (item.getItemId() == R.id.recent_inspect_item) { - ViewHolder viewHolder = (ViewHolder) selectedView.getTag(); - if (viewHolder != null) { - final TaskDescription ad = viewHolder.taskDescription; - startApplicationDetailsActivity(ad.packageName, ad.userId); - show(false); - } else { - throw new IllegalStateException("Oops, no tag on view " + selectedView); - } - } else { - return false; - } - return true; - } - }); - popup.setOnDismissListener(new PopupMenu.OnDismissListener() { - public void onDismiss(PopupMenu menu) { - thumbnailView.setSelected(false); - mPopup = null; - } - }); - popup.show(); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - - int paddingLeft = getPaddingLeft(); - final boolean offsetRequired = isPaddingOffsetRequired(); - if (offsetRequired) { - paddingLeft += getLeftPaddingOffset(); - } - - int left = getScrollX() + paddingLeft; - int right = left + getRight() - getLeft() - getPaddingRight() - paddingLeft; - int top = getScrollY() + getFadeTop(offsetRequired); - int bottom = top + getFadeHeight(offsetRequired); - - if (offsetRequired) { - right += getRightPaddingOffset(); - bottom += getBottomPaddingOffset(); - } - mRecentsContainer.drawFadedEdges(canvas, left, right, top, bottom); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsPreloadReceiver.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsPreloadReceiver.java deleted file mode 100644 index eb58920..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsPreloadReceiver.java +++ /dev/null @@ -1,32 +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.recent; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class RecentsPreloadReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (RecentsActivity.PRELOAD_INTENT.equals(intent.getAction())) { - RecentTasksLoader.getInstance(context).preloadRecentTasksList(); - } else if (RecentsActivity.CANCEL_PRELOAD_INTENT.equals(intent.getAction())){ - RecentTasksLoader.getInstance(context).cancelPreloadingRecentTasksList(); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsVerticalScrollView.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsVerticalScrollView.java deleted file mode 100644 index d518f74..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsVerticalScrollView.java +++ /dev/null @@ -1,401 +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.recent; - -import android.animation.LayoutTransition; -import android.content.Context; -import android.content.res.Configuration; -import android.database.DataSetObserver; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewTreeObserver; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.widget.LinearLayout; -import android.widget.ScrollView; - -import com.android.systemui.R; -import com.android.systemui.SwipeHelper; -import com.android.systemui.recent.RecentsPanelView.TaskDescriptionAdapter; - -import java.util.HashSet; -import java.util.Iterator; - -public class RecentsVerticalScrollView extends ScrollView - implements SwipeHelper.Callback, RecentsPanelView.RecentsScrollView { - private static final String TAG = RecentsPanelView.TAG; - private static final boolean DEBUG = RecentsPanelView.DEBUG; - private LinearLayout mLinearLayout; - private TaskDescriptionAdapter mAdapter; - private RecentsCallback mCallback; - protected int mLastScrollPosition; - private SwipeHelper mSwipeHelper; - private FadedEdgeDrawHelper mFadedEdgeDrawHelper; - private HashSet<View> mRecycledViews; - private int mNumItemsInOneScreenful; - private Runnable mOnScrollListener; - - public RecentsVerticalScrollView(Context context, AttributeSet attrs) { - super(context, attrs, 0); - mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, context); - - mFadedEdgeDrawHelper = FadedEdgeDrawHelper.create(context, attrs, this, true); - mRecycledViews = new HashSet<View>(); - } - - public void setMinSwipeAlpha(float minAlpha) { - mSwipeHelper.setMinSwipeProgress(minAlpha); - } - - private int scrollPositionOfMostRecent() { - return mLinearLayout.getHeight() - getHeight() + getPaddingTop(); - } - - private void addToRecycledViews(View v) { - if (mRecycledViews.size() < mNumItemsInOneScreenful) { - mRecycledViews.add(v); - } - } - - public View findViewForTask(int persistentTaskId) { - for (int i = 0; i < mLinearLayout.getChildCount(); i++) { - View v = mLinearLayout.getChildAt(i); - RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) v.getTag(); - if (holder.taskDescription.persistentTaskId == persistentTaskId) { - return v; - } - } - return null; - } - - private void update() { - for (int i = 0; i < mLinearLayout.getChildCount(); i++) { - View v = mLinearLayout.getChildAt(i); - addToRecycledViews(v); - mAdapter.recycleView(v); - } - LayoutTransition transitioner = getLayoutTransition(); - setLayoutTransition(null); - - mLinearLayout.removeAllViews(); - - // Once we can clear the data associated with individual item views, - // we can get rid of the removeAllViews() and the code below will - // recycle them. - Iterator<View> recycledViews = mRecycledViews.iterator(); - for (int i = 0; i < mAdapter.getCount(); i++) { - View old = null; - if (recycledViews.hasNext()) { - old = recycledViews.next(); - recycledViews.remove(); - old.setVisibility(VISIBLE); - } - final View view = mAdapter.getView(i, old, mLinearLayout); - - if (mFadedEdgeDrawHelper != null) { - mFadedEdgeDrawHelper.addViewCallback(view); - } - - OnTouchListener noOpListener = new OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return true; - } - }; - - view.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - mCallback.dismiss(); - } - }); - // We don't want a click sound when we dimiss recents - view.setSoundEffectsEnabled(false); - - OnClickListener launchAppListener = new OnClickListener() { - public void onClick(View v) { - mCallback.handleOnClick(view); - } - }; - - RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) view.getTag(); - final View thumbnailView = holder.thumbnailView; - OnLongClickListener longClickListener = new OnLongClickListener() { - public boolean onLongClick(View v) { - final View anchorView = view.findViewById(R.id.app_description); - mCallback.handleLongPress(view, anchorView, thumbnailView); - return true; - } - }; - thumbnailView.setClickable(true); - thumbnailView.setOnClickListener(launchAppListener); - thumbnailView.setOnLongClickListener(longClickListener); - - // We don't want to dismiss recents if a user clicks on the app title - // (we also don't want to launch the app either, though, because the - // app title is a small target and doesn't have great click feedback) - final View appTitle = view.findViewById(R.id.app_label); - appTitle.setContentDescription(" "); - appTitle.setOnTouchListener(noOpListener); - final View calloutLine = view.findViewById(R.id.recents_callout_line); - if (calloutLine != null) { - calloutLine.setOnTouchListener(noOpListener); - } - - mLinearLayout.addView(view); - } - setLayoutTransition(transitioner); - - // Scroll to end after initial layout. - final OnGlobalLayoutListener updateScroll = new OnGlobalLayoutListener() { - public void onGlobalLayout() { - mLastScrollPosition = scrollPositionOfMostRecent(); - scrollTo(0, mLastScrollPosition); - final ViewTreeObserver observer = getViewTreeObserver(); - if (observer.isAlive()) { - observer.removeOnGlobalLayoutListener(this); - } - } - }; - getViewTreeObserver().addOnGlobalLayoutListener(updateScroll); - } - - @Override - public void removeViewInLayout(final View view) { - dismissChild(view); - } - - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); - return mSwipeHelper.onInterceptTouchEvent(ev) || - super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - return mSwipeHelper.onTouchEvent(ev) || - super.onTouchEvent(ev); - } - - public boolean canChildBeDismissed(View v) { - return true; - } - - @Override - public boolean isAntiFalsingNeeded() { - return false; - } - - @Override - public float getFalsingThresholdFactor() { - return 1.0f; - } - - public void dismissChild(View v) { - mSwipeHelper.dismissChild(v, 0); - } - - public void onChildDismissed(View v) { - addToRecycledViews(v); - mLinearLayout.removeView(v); - mCallback.handleSwipe(v); - // Restore the alpha/translation parameters to what they were before swiping - // (for when these items are recycled) - View contentView = getChildContentView(v); - contentView.setAlpha(1f); - contentView.setTranslationX(0); - } - - public void onBeginDrag(View v) { - // We do this so the underlying ScrollView knows that it won't get - // the chance to intercept events anymore - requestDisallowInterceptTouchEvent(true); - } - - public void onDragCancelled(View v) { - } - - @Override - public void onChildSnappedBack(View animView) { - } - - @Override - public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { - return false; - } - - public View getChildAtPosition(MotionEvent ev) { - final float x = ev.getX() + getScrollX(); - final float y = ev.getY() + getScrollY(); - for (int i = 0; i < mLinearLayout.getChildCount(); i++) { - View item = mLinearLayout.getChildAt(i); - if (item.getVisibility() == View.VISIBLE - && x >= item.getLeft() && x < item.getRight() - && y >= item.getTop() && y < item.getBottom()) { - return item; - } - } - return null; - } - - public View getChildContentView(View v) { - return v.findViewById(R.id.recent_item); - } - - @Override - public void drawFadedEdges(Canvas canvas, int left, int right, int top, int bottom) { - if (mFadedEdgeDrawHelper != null) { - final boolean offsetRequired = isPaddingOffsetRequired(); - mFadedEdgeDrawHelper.drawCallback(canvas, - left, right, top + getFadeTop(offsetRequired), bottom, getScrollX(), getScrollY(), - getTopFadingEdgeStrength(), getBottomFadingEdgeStrength(), - 0, 0, getPaddingTop()); - } - } - - @Override - protected void onScrollChanged(int l, int t, int oldl, int oldt) { - super.onScrollChanged(l, t, oldl, oldt); - if (mOnScrollListener != null) { - mOnScrollListener.run(); - } - } - - public void setOnScrollListener(Runnable listener) { - mOnScrollListener = listener; - } - - @Override - public int getVerticalFadingEdgeLength() { - if (mFadedEdgeDrawHelper != null) { - return mFadedEdgeDrawHelper.getVerticalFadingEdgeLength(); - } else { - return super.getVerticalFadingEdgeLength(); - } - } - - @Override - public int getHorizontalFadingEdgeLength() { - if (mFadedEdgeDrawHelper != null) { - return mFadedEdgeDrawHelper.getHorizontalFadingEdgeLength(); - } else { - return super.getHorizontalFadingEdgeLength(); - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - setScrollbarFadingEnabled(true); - mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout); - final int leftPadding = getContext().getResources() - .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin); - setOverScrollEffectPadding(leftPadding, 0); - } - - @Override - public void onAttachedToWindow() { - if (mFadedEdgeDrawHelper != null) { - mFadedEdgeDrawHelper.onAttachedToWindowCallback(mLinearLayout, isHardwareAccelerated()); - } - } - - @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); - } - - private void setOverScrollEffectPadding(int leftPadding, int i) { - // TODO Add to (Vertical)ScrollView - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - // Skip this work if a transition is running; it sets the scroll values independently - // and should not have those animated values clobbered by this logic - LayoutTransition transition = mLinearLayout.getLayoutTransition(); - if (transition != null && transition.isRunning()) { - return; - } - // Keep track of the last visible item in the list so we can restore it - // to the bottom when the orientation changes. - mLastScrollPosition = scrollPositionOfMostRecent(); - - // This has to happen post-layout, so run it "in the future" - post(new Runnable() { - public void run() { - // Make sure we're still not clobbering the transition-set values, since this - // runnable launches asynchronously - LayoutTransition transition = mLinearLayout.getLayoutTransition(); - if (transition == null || !transition.isRunning()) { - scrollTo(0, mLastScrollPosition); - } - } - }); - } - - public void setAdapter(TaskDescriptionAdapter adapter) { - mAdapter = adapter; - mAdapter.registerDataSetObserver(new DataSetObserver() { - public void onChanged() { - update(); - } - - public void onInvalidated() { - update(); - } - }); - - DisplayMetrics dm = getResources().getDisplayMetrics(); - int childWidthMeasureSpec = - MeasureSpec.makeMeasureSpec(dm.widthPixels, MeasureSpec.AT_MOST); - int childheightMeasureSpec = - MeasureSpec.makeMeasureSpec(dm.heightPixels, MeasureSpec.AT_MOST); - View child = mAdapter.createView(mLinearLayout); - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - mNumItemsInOneScreenful = - (int) Math.ceil(dm.heightPixels / (double) child.getMeasuredHeight()); - addToRecycledViews(child); - - for (int i = 0; i < mNumItemsInOneScreenful - 1; i++) { - addToRecycledViews(mAdapter.createView(mLinearLayout)); - } - } - - public int numItemsInOneScreenful() { - return mNumItemsInOneScreenful; - } - - @Override - public void setLayoutTransition(LayoutTransition transition) { - // The layout transition applies to our embedded LinearLayout - mLinearLayout.setLayoutTransition(transition); - } - - public void setCallback(RecentsCallback callback) { - mCallback = callback; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recent/TaskDescription.java b/packages/SystemUI/src/com/android/systemui/recent/TaskDescription.java deleted file mode 100644 index 5ad965f..0000000 --- a/packages/SystemUI/src/com/android/systemui/recent/TaskDescription.java +++ /dev/null @@ -1,98 +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.recent; - -import android.os.UserHandle; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.graphics.drawable.Drawable; - -public final class TaskDescription { - final ResolveInfo resolveInfo; - final int taskId; // application task id for curating apps - final int persistentTaskId; // persistent id - 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 - private CharSequence mLabel; // application package label - private boolean mLoaded; - - public TaskDescription(int _taskId, int _persistentTaskId, - ResolveInfo _resolveInfo, Intent _intent, - String _packageName, CharSequence _description, int _userId) { - resolveInfo = _resolveInfo; - intent = _intent; - taskId = _taskId; - persistentTaskId = _persistentTaskId; - - description = _description; - packageName = _packageName; - userId = _userId; - } - - public TaskDescription() { - resolveInfo = null; - intent = null; - taskId = -1; - persistentTaskId = -1; - - description = null; - packageName = null; - userId = UserHandle.USER_NULL; - } - - public void setLoaded(boolean loaded) { - mLoaded = loaded; - } - - public boolean isLoaded() { - return mLoaded; - } - - public boolean isNull() { - return resolveInfo == null; - } - - // mark all these as locked? - public CharSequence getLabel() { - return mLabel; - } - - public void setLabel(CharSequence label) { - mLabel = label; - } - - public Drawable getIcon() { - return mIcon; - } - - public void setIcon(Drawable icon) { - mIcon = icon; - } - - public void setThumbnail(Drawable thumbnail) { - mThumbnail = thumbnail; - } - - public Drawable getThumbnail() { - return mThumbnail; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java index 0a1718d..18c213d 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/Constants.java +++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java @@ -29,18 +29,16 @@ public class Constants { public static final boolean EnableTransitionThumbnailDebugMode = false; // Enables the filtering of tasks according to their grouping public static final boolean EnableTaskFiltering = false; - // Enables clipping of tasks against each other - public static final boolean EnableTaskStackClipping = true; - // Enables tapping on the TaskBar to launch the task - public static final boolean EnableTaskBarTouchEvents = true; - // Enables app-info pane on long-pressing the icon - public static final boolean EnableDevAppInfoOnLongPress = true; + // Enables dismiss-all + public static final boolean EnableDismissAll = false; // Enables debug mode public static final boolean EnableDebugMode = false; // Enables the search bar layout public static final boolean EnableSearchLayout = true; // Enables the thumbnail alpha on the front-most task public static final boolean EnableThumbnailAlphaOnFrontmost = false; + // Enables all system stacks to show up in the same recents stack + public static final boolean EnableMultiStackToSingleStack = true; // This disables the bitmap and icon caches public static final boolean DisableBackgroundCache = false; // Enables the simulated task affiliations @@ -59,8 +57,6 @@ public class Constants { public static class Values { public static class App { public static int AppWidgetHostId = 1024; - public static String Key_SearchAppWidgetId = "searchAppWidgetId"; - public static String Key_DebugModeEnabled = "debugModeEnabled"; public static String DebugModeVersion = "A"; } diff --git a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java index 2ddab48..6a45369 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java +++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java @@ -20,11 +20,9 @@ import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ITaskStackListener; -import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetProviderInfo; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -33,14 +31,19 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; +import android.os.AsyncTask; import android.os.Handler; import android.os.SystemClock; import android.os.UserHandle; -import android.util.Pair; +import android.util.MutableBoolean; +import android.view.Display; import android.view.LayoutInflater; import android.view.View; +import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.RecentsComponent; +import com.android.systemui.SystemUI; +import com.android.systemui.SystemUIApplication; import com.android.systemui.recents.misc.Console; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.model.RecentsTaskLoadPlan; @@ -52,10 +55,9 @@ import com.android.systemui.recents.views.TaskStackView; import com.android.systemui.recents.views.TaskStackViewLayoutAlgorithm; import com.android.systemui.recents.views.TaskViewHeader; import com.android.systemui.recents.views.TaskViewTransform; +import com.android.systemui.statusbar.phone.PhoneStatusBar; import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; /** * Annotation for a method that is only called from the primary user's SystemUI process and will be @@ -69,7 +71,8 @@ import java.util.concurrent.atomic.AtomicBoolean; @interface ProxyFromAnyToPrimaryUser {} /** A proxy implementation for the recents component */ -public class AlternateRecentsComponent implements ActivityOptions.OnAnimationStartedListener { +public class Recents extends SystemUI + implements ActivityOptions.OnAnimationStartedListener, RecentsComponent { final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "triggeredFromAltTab"; final public static String EXTRA_TRIGGERED_FROM_HOME_KEY = "triggeredFromHomeKey"; @@ -78,6 +81,8 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta // Owner proxy events final public static String ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER = "action_notify_recents_visibility_change"; + final public static String ACTION_PROXY_SCREEN_PINNING_REQUEST_TO_OWNER = + "action_screen_pinning_request"; final public static String ACTION_START_ENTER_ANIMATION = "action_start_enter_animation"; final public static String ACTION_TOGGLE_RECENTS_ACTIVITY = "action_toggle_recents_activity"; @@ -85,7 +90,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta final static int sMinToggleDelay = 350; - final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS"; + public final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS"; public final static String sRecentsPackage = "com.android.systemui"; public final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; @@ -109,6 +114,9 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta /** Preloads the next task */ public void run() { + // Temporarily skip this if multi stack is enabled + if (mConfig.multiStackEnabled) return; + RecentsConfiguration config = RecentsConfiguration.getInstance(); if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) { RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); @@ -144,19 +152,23 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta case ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER: visibilityChanged(intent.getBooleanExtra(EXTRA_RECENTS_VISIBILITY, false)); break; + case ACTION_PROXY_SCREEN_PINNING_REQUEST_TO_OWNER: + onStartScreenPinning(context); + break; } } } static RecentsComponent.Callbacks sRecentsComponentCallbacks; static RecentsTaskLoadPlan sInstanceLoadPlan; + static Recents sInstance; - Context mContext; LayoutInflater mInflater; SystemServicesProxy mSystemServicesProxy; Handler mHandler; TaskStackListenerImpl mTaskStackListener; RecentsOwnerEventProxyReceiver mProxyBroadcastReceiver; + RecentsAppWidgetHost mAppWidgetHost; boolean mBootCompleted; boolean mStartAnimationTriggered; boolean mCanReuseTaskStackViews = true; @@ -173,19 +185,56 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta // Header (for transition) TaskViewHeader mHeaderBar; + final Object mHeaderBarLock = new Object(); TaskStackView mDummyStackView; // Variables to keep track of if we need to start recents after binding boolean mTriggeredFromAltTab; long mLastToggleTime; - public AlternateRecentsComponent(Context context) { - RecentsTaskLoader.initialize(context); - mInflater = LayoutInflater.from(context); - mContext = context; - mSystemServicesProxy = new SystemServicesProxy(context); + Bitmap mThumbnailTransitionBitmapCache; + Task mThumbnailTransitionBitmapCacheKey; + + public Recents() { + } + + /** + * Gets the singleton instance and starts it if needed. On the primary user on the device, this + * component gets started as a normal {@link SystemUI} component. On a secondary user, this + * lifecycle doesn't exist, so we need to start it manually here if needed. + */ + public static Recents getInstanceAndStartIfNeeded(Context ctx) { + if (sInstance == null) { + sInstance = new Recents(); + sInstance.mContext = ctx; + sInstance.start(); + sInstance.onBootCompleted(); + } + return sInstance; + } + + /** Creates a new broadcast intent */ + static Intent createLocalBroadcastIntent(Context context, String action) { + Intent intent = new Intent(action); + intent.setPackage(context.getPackageName()); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | + Intent.FLAG_RECEIVER_FOREGROUND); + return intent; + } + + /** Initializes the Recents. */ + @ProxyFromPrimaryToCurrentUser + @Override + public void start() { + if (sInstance == null) { + sInstance = this; + } + RecentsTaskLoader.initialize(mContext); + mInflater = LayoutInflater.from(mContext); + mSystemServicesProxy = new SystemServicesProxy(mContext); mHandler = new Handler(); mTaskStackBounds = new Rect(); + mAppWidgetHost = new RecentsAppWidgetHost(mContext, Constants.Values.App.AppWidgetHostId); // Register the task stack listener mTaskStackListener = new TaskStackListenerImpl(mHandler); @@ -197,28 +246,16 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta if (mSystemServicesProxy.isForegroundUserOwner()) { mProxyBroadcastReceiver = new RecentsOwnerEventProxyReceiver(); IntentFilter filter = new IntentFilter(); - filter.addAction(AlternateRecentsComponent.ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER); + filter.addAction(Recents.ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER); + filter.addAction(Recents.ACTION_PROXY_SCREEN_PINNING_REQUEST_TO_OWNER); mContext.registerReceiverAsUser(mProxyBroadcastReceiver, UserHandle.CURRENT, filter, null, mHandler); } - } - /** Creates a new broadcast intent */ - static Intent createLocalBroadcastIntent(Context context, String action) { - Intent intent = new Intent(action); - intent.setPackage(context.getPackageName()); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | - Intent.FLAG_RECEIVER_FOREGROUND); - return intent; - } - - /** Initializes the Recents. */ - @ProxyFromPrimaryToCurrentUser - public void onStart() { // Initialize some static datastructures TaskStackViewLayoutAlgorithm.initializeCurve(); // Load the header bar layout - reloadHeaderBarLayout(true); + reloadHeaderBarLayout(); // When we start, preload the data associated with the previous recent tasks. // We can use a new plan since the caches will be the same. @@ -230,17 +267,20 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize(); launchOpts.onlyLoadForCache = true; loader.loadTasks(mContext, plan, launchOpts); + putComponent(Recents.class, this); } + @Override public void onBootCompleted() { mBootCompleted = true; } /** Shows the Recents. */ @ProxyFromPrimaryToCurrentUser - public void onShowRecents(boolean triggeredFromAltTab) { + @Override + public void showRecents(boolean triggeredFromAltTab, View statusBarView) { if (mSystemServicesProxy.isForegroundUserOwner()) { - showRecents(triggeredFromAltTab); + showRecentsInternal(triggeredFromAltTab); } else { Intent intent = createLocalBroadcastIntent(mContext, RecentsUserEventProxyReceiver.ACTION_PROXY_SHOW_RECENTS_TO_USER); @@ -248,7 +288,8 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } - void showRecents(boolean triggeredFromAltTab) { + + void showRecentsInternal(boolean triggeredFromAltTab) { mTriggeredFromAltTab = triggeredFromAltTab; try { @@ -260,9 +301,10 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta /** Hides the Recents. */ @ProxyFromPrimaryToCurrentUser - public void onHideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { + @Override + public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { if (mSystemServicesProxy.isForegroundUserOwner()) { - hideRecents(triggeredFromAltTab, triggeredFromHomeKey); + hideRecentsInternal(triggeredFromAltTab, triggeredFromHomeKey); } else { Intent intent = createLocalBroadcastIntent(mContext, RecentsUserEventProxyReceiver.ACTION_PROXY_HIDE_RECENTS_TO_USER); @@ -271,31 +313,32 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } - void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { + + void hideRecentsInternal(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { if (mBootCompleted) { - ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); - if (topTask != null && mSystemServicesProxy.isRecentsTopMost(topTask, null)) { - // Notify recents to hide itself - Intent intent = createLocalBroadcastIntent(mContext, ACTION_HIDE_RECENTS_ACTIVITY); - intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab); - intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey); - mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); - } + // Defer to the activity to handle hiding recents, if it handles it, then it must still + // be visible + Intent intent = createLocalBroadcastIntent(mContext, ACTION_HIDE_RECENTS_ACTIVITY); + intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab); + intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } /** Toggles the Recents activity. */ @ProxyFromPrimaryToCurrentUser - public void onToggleRecents() { + @Override + public void toggleRecents(Display display, int layoutDirection, View statusBarView) { if (mSystemServicesProxy.isForegroundUserOwner()) { - toggleRecents(); + toggleRecentsInternal(); } else { Intent intent = createLocalBroadcastIntent(mContext, RecentsUserEventProxyReceiver.ACTION_PROXY_TOGGLE_RECENTS_TO_USER); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } - void toggleRecents() { + + void toggleRecentsInternal() { mTriggeredFromAltTab = false; try { @@ -307,35 +350,56 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta /** Preloads info for the Recents activity. */ @ProxyFromPrimaryToCurrentUser - public void onPreloadRecents() { + @Override + public void preloadRecents() { if (mSystemServicesProxy.isForegroundUserOwner()) { - preloadRecents(); + preloadRecentsInternal(); } else { Intent intent = createLocalBroadcastIntent(mContext, RecentsUserEventProxyReceiver.ACTION_PROXY_PRELOAD_RECENTS_TO_USER); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } - void preloadRecents() { + + void preloadRecentsInternal() { // Preload only the raw task list into a new load plan (which will be consumed by the - // RecentsActivity) + // RecentsActivity) only if there is a task to animate to. + ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); + MutableBoolean topTaskHome = new MutableBoolean(true); RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); sInstanceLoadPlan = loader.createLoadPlan(mContext); - sInstanceLoadPlan.preloadRawTasks(true); + if (topTask != null && !mSystemServicesProxy.isRecentsTopMost(topTask, topTaskHome)) { + sInstanceLoadPlan.preloadRawTasks(topTaskHome.value); + loader.preloadTasks(sInstanceLoadPlan, topTaskHome.value); + TaskStack top = sInstanceLoadPlan.getAllTaskStacks().get(0); + if (top.getTaskCount() > 0) { + preCacheThumbnailTransitionBitmapAsync(topTask, top, mDummyStackView, + topTaskHome.value); + } + } } - public void onCancelPreloadingRecents() { + @Override + public void cancelPreloadingRecents() { // Do nothing } void showRelativeAffiliatedTask(boolean showNextTask) { + // Return early if there is no focused stack + int focusedStackId = mSystemServicesProxy.getFocusedStack(); + TaskStack focusedStack = null; RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); loader.preloadTasks(plan, true /* isTopTaskHome */); - TaskStack stack = plan.getTaskStack(); + if (mConfig.multiStackEnabled) { + if (focusedStackId < 0) return; + focusedStack = plan.getTaskStack(focusedStackId); + } else { + focusedStack = plan.getAllTaskStacks().get(0); + } - // Return early if there are no tasks - if (stack.getTaskCount() == 0) return; + // Return early if there are no tasks in the focused stack + if (focusedStack == null || focusedStack.getTaskCount() == 0) return; ActivityManager.RunningTaskInfo runningTask = mSystemServicesProxy.getTopMostTask(); // Return early if there is no running task (can't determine affiliated tasks in this case) @@ -344,7 +408,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta if (mSystemServicesProxy.isInHomeStack(runningTask.id)) return; // Find the task in the recents list - ArrayList<Task> tasks = stack.getTasks(); + ArrayList<Task> tasks = focusedStack.getTasks(); Task toTask = null; ActivityOptions launchOpts = null; int taskCount = tasks.size(); @@ -366,7 +430,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta R.anim.recents_launch_prev_affiliated_task_source); } if (toTaskKey != null) { - toTask = stack.findTaskWithId(toTaskKey.id); + toTask = focusedStack.findTaskWithId(toTaskKey.id); } numAffiliatedTasks = group.getTaskCount(); break; @@ -399,11 +463,13 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } } - public void onShowNextAffiliatedTask() { + @Override + public void showNextAffiliatedTask() { showRelativeAffiliatedTask(true); } - public void onShowPrevAffiliatedTask() { + @Override + public void showPrevAffiliatedTask() { showRelativeAffiliatedTask(false); } @@ -422,11 +488,11 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta // Don't reuse task stack views if the configuration changes mCanReuseTaskStackViews = false; // Reload the header bar layout - reloadHeaderBarLayout(false); + reloadHeaderBarLayout(); } /** Prepares the header bar layout. */ - void reloadHeaderBarLayout(boolean reloadWidget) { + void reloadHeaderBarLayout() { Resources res = mContext.getResources(); mWindowRect = mSystemServicesProxy.getWindowRect(); mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); @@ -434,12 +500,17 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta mNavBarWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_width); mConfig = RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy); mConfig.updateOnConfigurationChange(); - if (reloadWidget) { - // Reload the widget id before we get the task stack bounds - reloadSearchBarAppWidget(mContext, mSystemServicesProxy); - } - mConfig.getTaskStackBounds(mWindowRect.width(), mWindowRect.height(), mStatusBarHeight, - (mConfig.hasTransposedNavBar ? mNavBarWidth : 0), mTaskStackBounds); + Rect searchBarBounds = new Rect(); + // Try and pre-emptively bind the search widget on startup to ensure that we + // have the right thumbnail bounds to animate to. + // Note: We have to reload the widget id before we get the task stack bounds below + if (mSystemServicesProxy.getOrBindSearchAppWidget(mContext, mAppWidgetHost) != null) { + mConfig.getSearchBarBounds(mWindowRect.width(), mWindowRect.height(), + mStatusBarHeight, searchBarBounds); + } + mConfig.getAvailableTaskStackBounds(mWindowRect.width(), mWindowRect.height(), + mStatusBarHeight, (mConfig.hasTransposedNavBar ? mNavBarWidth : 0), searchBarBounds, + mTaskStackBounds); if (mConfig.isLandscape && mConfig.hasTransposedNavBar) { mSystemInsets.set(0, mStatusBarHeight, mNavBarWidth, 0); } else { @@ -455,29 +526,13 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta algo.computeRects(mWindowRect.width(), mWindowRect.height(), taskStackBounds); Rect taskViewSize = algo.getUntransformedTaskViewSize(); int taskBarHeight = res.getDimensionPixelSize(R.dimen.recents_task_bar_height); - mHeaderBar = (TaskViewHeader) mInflater.inflate(R.layout.recents_task_view_header, null, - false); - mHeaderBar.measure( - View.MeasureSpec.makeMeasureSpec(taskViewSize.width(), View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(taskBarHeight, View.MeasureSpec.EXACTLY)); - mHeaderBar.layout(0, 0, taskViewSize.width(), taskBarHeight); - } - - /** Prepares the search bar app widget */ - void reloadSearchBarAppWidget(Context context, SystemServicesProxy ssp) { - // Try and pre-emptively bind the search widget on startup to ensure that we - // have the right thumbnail bounds to animate to. - if (Constants.DebugFlags.App.EnableSearchLayout) { - // If there is no id, then bind a new search app widget - if (mConfig.searchBarAppWidgetId < 0) { - AppWidgetHost host = new RecentsAppWidgetHost(context, - Constants.Values.App.AppWidgetHostId); - Pair<Integer, AppWidgetProviderInfo> widgetInfo = ssp.bindSearchAppWidget(host); - if (widgetInfo != null) { - // Save the app widget id into the settings - mConfig.updateSearchBarAppWidgetId(context, widgetInfo.first); - } - } + synchronized (mHeaderBarLock) { + mHeaderBar = (TaskViewHeader) mInflater.inflate(R.layout.recents_task_view_header, null, + false); + mHeaderBar.measure( + View.MeasureSpec.makeMeasureSpec(taskViewSize.width(), View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(taskBarHeight, View.MeasureSpec.EXACTLY)); + mHeaderBar.layout(0, 0, taskViewSize.width(), taskBarHeight); } } @@ -493,7 +548,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta // If Recents is the front most activity, then we should just communicate with it directly // to launch the first task or dismiss itself ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); - AtomicBoolean isTopTaskHome = new AtomicBoolean(true); + MutableBoolean isTopTaskHome = new MutableBoolean(true); if (topTask != null && mSystemServicesProxy.isRecentsTopMost(topTask, isTopTaskHome)) { // Notify recents to toggle itself Intent intent = createLocalBroadcastIntent(mContext, ACTION_TOGGLE_RECENTS_ACTIVITY); @@ -502,7 +557,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta return; } else { // Otherwise, start the recents activity - startRecentsActivity(topTask, isTopTaskHome.get()); + startRecentsActivity(topTask, isTopTaskHome.value); } } @@ -510,9 +565,9 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta void startRecentsActivity() { // Check if the top task is in the home stack, and start the recents activity ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); - AtomicBoolean isTopTaskHome = new AtomicBoolean(true); + MutableBoolean isTopTaskHome = new MutableBoolean(true); if (topTask == null || !mSystemServicesProxy.isRecentsTopMost(topTask, isTopTaskHome)) { - startRecentsActivity(topTask, isTopTaskHome.get()); + startRecentsActivity(topTask, isTopTaskHome.value); } } @@ -549,26 +604,24 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta */ ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask, TaskStack stack, TaskStackView stackView) { + // Update the destination rect Task toTask = new Task(); TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView, topTask.id, toTask); - if (toTransform != null && toTask.key != null) { - Rect toTaskRect = toTransform.rect; - int toHeaderWidth = (int) (mHeaderBar.getMeasuredWidth() * toTransform.scale); - int toHeaderHeight = (int) (mHeaderBar.getMeasuredHeight() * toTransform.scale); - Bitmap thumbnail = Bitmap.createBitmap(toHeaderWidth, toHeaderHeight, - Bitmap.Config.ARGB_8888); - if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) { - thumbnail.eraseColor(0xFFff0000); - } else { - Canvas c = new Canvas(thumbnail); - c.scale(toTransform.scale, toTransform.scale); - mHeaderBar.rebindToTask(toTask); - mHeaderBar.draw(c); - c.setBitmap(null); - } - + Rect toTaskRect = toTransform.rect; + Bitmap thumbnail; + if (mThumbnailTransitionBitmapCacheKey != null + && mThumbnailTransitionBitmapCacheKey.key != null + && mThumbnailTransitionBitmapCacheKey.key.equals(toTask.key)) { + thumbnail = mThumbnailTransitionBitmapCache; + mThumbnailTransitionBitmapCacheKey = null; + mThumbnailTransitionBitmapCache = null; + } else { + preloadIcon(topTask); + thumbnail = drawThumbnailTransitionBitmap(toTask, toTransform); + } + if (thumbnail != null) { mStartAnimationTriggered = false; return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView, thumbnail, toTaskRect.left, toTaskRect.top, toTaskRect.width(), @@ -579,6 +632,72 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta return getUnknownTransitionActivityOptions(); } + /** + * Preloads the icon of a task. + */ + void preloadIcon(ActivityManager.RunningTaskInfo task) { + + // Ensure that we load the running task's icon + RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); + launchOpts.runningTaskId = task.id; + launchOpts.loadThumbnails = false; + launchOpts.onlyLoadForCache = true; + RecentsTaskLoader.getInstance().loadTasks(mContext, sInstanceLoadPlan, launchOpts); + } + + /** + * Caches the header thumbnail used for a window animation asynchronously into + * {@link #mThumbnailTransitionBitmapCache}. + */ + void preCacheThumbnailTransitionBitmapAsync(ActivityManager.RunningTaskInfo topTask, + TaskStack stack, TaskStackView stackView, boolean isTopTaskHome) { + preloadIcon(topTask); + + // Update the destination rect + mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome); + final Task toTask = new Task(); + final TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView, + topTask.id, toTask); + new AsyncTask<Void, Void, Bitmap>() { + @Override + protected Bitmap doInBackground(Void... params) { + return drawThumbnailTransitionBitmap(toTask, toTransform); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + mThumbnailTransitionBitmapCache = bitmap; + mThumbnailTransitionBitmapCacheKey = toTask; + } + }.execute(); + } + + /** + * Draws the header of a task used for the window animation into a bitmap. + */ + Bitmap drawThumbnailTransitionBitmap(Task toTask, TaskViewTransform toTransform) { + if (toTransform != null && toTask.key != null) { + Bitmap thumbnail; + synchronized (mHeaderBarLock) { + int toHeaderWidth = (int) (mHeaderBar.getMeasuredWidth() * toTransform.scale); + int toHeaderHeight = (int) (mHeaderBar.getMeasuredHeight() * toTransform.scale); + thumbnail = Bitmap.createBitmap(toHeaderWidth, toHeaderHeight, + Bitmap.Config.ARGB_8888); + if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) { + thumbnail.eraseColor(0xFFff0000); + } else { + Canvas c = new Canvas(thumbnail); + c.scale(toTransform.scale, toTransform.scale); + mHeaderBar.rebindToTask(toTask); + mHeaderBar.draw(c); + c.setBitmap(null); + } + } + return thumbnail.createAshmemBitmap(); + } + return null; + } + /** Returns the transition rect for the given task id. */ TaskViewTransform getThumbnailTransitionTransform(TaskStack stack, TaskStackView stackView, int runningTaskId, Task runningTaskOut) { @@ -600,6 +719,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta if (task == null) { // If no task is specified or we can not find the task just use the front most one task = tasks.get(tasks.size() - 1); + runningTaskOut.copyFrom(task); } // Get the transform for the running task @@ -611,14 +731,34 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta /** Starts the recents activity */ void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, boolean isTopTaskHome) { + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy); + if (sInstanceLoadPlan == null) { // Create a new load plan if onPreloadRecents() was never triggered - RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); sInstanceLoadPlan = loader.createLoadPlan(mContext); } - RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); - loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome); - TaskStack stack = sInstanceLoadPlan.getTaskStack(); + + // Temporarily skip the transition (use a dummy fade) if multi stack is enabled. + // For multi-stack we need to figure out where each of the tasks are going. + if (mConfig.multiStackEnabled) { + loader.preloadTasks(sInstanceLoadPlan, true); + ArrayList<TaskStack> stacks = sInstanceLoadPlan.getAllTaskStacks(); + TaskStack stack = stacks.get(0); + mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, true); + TaskStackViewLayoutAlgorithm.VisibilityReport stackVr = + mDummyStackView.computeStackVisibilityReport(); + ActivityOptions opts = getUnknownTransitionActivityOptions(); + startAlternateRecentsActivity(topTask, opts, true /* fromHome */, + false /* fromSearchHome */, false /* fromThumbnail */, stackVr); + return; + } + + if (!sInstanceLoadPlan.hasTasks()) { + loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome); + } + ArrayList<TaskStack> stacks = sInstanceLoadPlan.getAllTaskStacks(); + TaskStack stack = stacks.get(0); // Prepare the dummy stack for the transition mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome); @@ -628,12 +768,6 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks; if (useThumbnailTransition) { - // Ensure that we load the running task's icon - RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); - launchOpts.runningTaskId = topTask.id; - launchOpts.loadThumbnails = false; - launchOpts.onlyLoadForCache = true; - loader.loadTasks(mContext, sInstanceLoadPlan, launchOpts); // Try starting with a thumbnail transition ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, stack, @@ -651,27 +785,13 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta // If there is no thumbnail transition, but is launching from home into recents, then // use a quick home transition and do the animation from home if (hasRecentTasks) { - // Get the home activity info String homeActivityPackage = mSystemServicesProxy.getHomeActivityPackageName(); - // Get the search widget info - AppWidgetProviderInfo searchWidget = null; - String searchWidgetPackage = null; - if (mConfig.hasSearchBarAppWidget()) { - searchWidget = mSystemServicesProxy.getAppWidgetInfo( - mConfig.searchBarAppWidgetId); - } else { - searchWidget = mSystemServicesProxy.resolveSearchAppWidget(); - } - if (searchWidget != null && searchWidget.provider != null) { - searchWidgetPackage = searchWidget.provider.getPackageName(); - } - // Determine whether we are coming from a search owned home activity - boolean fromSearchHome = false; - if (homeActivityPackage != null && searchWidgetPackage != null && - homeActivityPackage.equals(searchWidgetPackage)) { - fromSearchHome = true; - } + String searchWidgetPackage = + Prefs.getString(mContext, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, null); + // Determine whether we are coming from a search owned home activity + boolean fromSearchHome = (homeActivityPackage != null) && + homeActivityPackage.equals(searchWidgetPackage); ActivityOptions opts = getHomeTransitionActivityOptions(fromSearchHome); startAlternateRecentsActivity(topTask, opts, true /* fromHome */, fromSearchHome, false /* fromThumbnail */, stackVr); @@ -714,7 +834,8 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } /** Sets the RecentsComponent callbacks. */ - public void setRecentsComponentCallback(RecentsComponent.Callbacks cb) { + @Override + public void setCallback(RecentsComponent.Callbacks cb) { sRecentsComponentCallbacks = cb; } @@ -737,6 +858,27 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } } + /** Notifies the status bar to trigger screen pinning. */ + @ProxyFromAnyToPrimaryUser + public static void startScreenPinning(Context context, SystemServicesProxy ssp) { + if (ssp.isForegroundUserOwner()) { + onStartScreenPinning(context); + } else { + Intent intent = createLocalBroadcastIntent(context, + ACTION_PROXY_SCREEN_PINNING_REQUEST_TO_OWNER); + context.sendBroadcastAsUser(intent, UserHandle.OWNER); + } + } + static void onStartScreenPinning(Context context) { + // For the primary user, the context for the SystemUI component is the SystemUIApplication + SystemUIApplication app = (SystemUIApplication) + getInstanceAndStartIfNeeded(context).mContext; + PhoneStatusBar statusBar = app.getComponent(PhoneStatusBar.class); + if (statusBar != null) { + statusBar.showScreenPinningRequest(false); + } + } + /** * Returns the preloaded load plan and invalidates it. */ diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java index 7422e36..bf15c68 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -19,42 +19,36 @@ package com.android.systemui.recents; import android.app.Activity; import android.app.ActivityOptions; import android.app.SearchManager; -import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.os.Bundle; import android.os.SystemClock; import android.os.UserHandle; -import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.ViewStub; import android.widget.Toast; +import com.android.systemui.Prefs; import com.android.systemui.R; +import com.android.systemui.recents.misc.Console; import com.android.systemui.recents.misc.DebugTrigger; import com.android.systemui.recents.misc.ReferenceCountedTrigger; import com.android.systemui.recents.misc.SystemServicesProxy; -import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.RecentsTaskLoadPlan; import com.android.systemui.recents.model.RecentsTaskLoader; -import com.android.systemui.recents.model.SpaceNode; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; import com.android.systemui.recents.views.DebugOverlayView; import com.android.systemui.recents.views.RecentsView; import com.android.systemui.recents.views.SystemBarScrimViews; import com.android.systemui.recents.views.ViewAnimation; -import com.android.systemui.statusbar.phone.PhoneStatusBar; -import com.android.systemui.SystemUIApplication; import java.lang.ref.WeakReference; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; /** @@ -75,15 +69,19 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView View mEmptyView; DebugOverlayView mDebugOverlay; + // Resize task debug + RecentsResizeTaskDialog mResizeTaskDebugDialog; + // Search AppWidget + AppWidgetProviderInfo mSearchWidgetInfo; RecentsAppWidgetHost mAppWidgetHost; - AppWidgetProviderInfo mSearchAppWidgetInfo; - AppWidgetHostView mSearchAppWidgetHostView; + RecentsAppWidgetHostView mSearchWidgetHostView; // Runnables to finish the Recents activity FinishRecentsRunnable mFinishLaunchHomeRunnable; - private PhoneStatusBar mStatusBar; + // Runnable to be executed after we paused ourselves + Runnable mAfterPauseRunnable; /** * A common Runnable to finish Recents either by calling finish() (with a custom animation) or @@ -109,10 +107,15 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView public void run() { // Finish Recents if (mLaunchIntent != null) { - if (mLaunchOpts != null) { - startActivityAsUser(mLaunchIntent, mLaunchOpts.toBundle(), UserHandle.CURRENT); - } else { - startActivityAsUser(mLaunchIntent, UserHandle.CURRENT); + try { + if (mLaunchOpts != null) { + startActivityAsUser(mLaunchIntent, mLaunchOpts.toBundle(), UserHandle.CURRENT); + } else { + startActivityAsUser(mLaunchIntent, UserHandle.CURRENT); + } + } catch (Exception e) { + Console.logError(RecentsActivity.this, + getString(R.string.recents_launch_error_message, "Home")); } } else { finish(); @@ -129,20 +132,20 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (action.equals(AlternateRecentsComponent.ACTION_HIDE_RECENTS_ACTIVITY)) { - if (intent.getBooleanExtra(AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_ALT_TAB, false)) { + if (action.equals(Recents.ACTION_HIDE_RECENTS_ACTIVITY)) { + if (intent.getBooleanExtra(Recents.EXTRA_TRIGGERED_FROM_ALT_TAB, false)) { // If we are hiding from releasing Alt-Tab, dismiss Recents to the focused app dismissRecentsToFocusedTaskOrHome(false); - } else if (intent.getBooleanExtra(AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_HOME_KEY, false)) { + } else if (intent.getBooleanExtra(Recents.EXTRA_TRIGGERED_FROM_HOME_KEY, false)) { // Otherwise, dismiss Recents to Home - dismissRecentsToHome(true); + dismissRecentsToHomeRaw(true); } else { - // Do nothing, another activity is being launched on top of Recents + // Do nothing } - } else if (action.equals(AlternateRecentsComponent.ACTION_TOGGLE_RECENTS_ACTIVITY)) { + } else if (action.equals(Recents.ACTION_TOGGLE_RECENTS_ACTIVITY)) { // If we are toggling Recents, then first unfilter any filtered stacks first dismissRecentsToFocusedTaskOrHome(true); - } else if (action.equals(AlternateRecentsComponent.ACTION_START_ENTER_ANIMATION)) { + } else if (action.equals(Recents.ACTION_START_ENTER_ANIMATION)) { // Trigger the enter animation onEnterAnimationTriggered(); // Notify the fallback receiver that we have successfully got the broadcast @@ -163,8 +166,10 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView // When the screen turns off, dismiss Recents to Home dismissRecentsToHome(false); } else if (action.equals(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED)) { - // When the search activity changes, update the Search widget - refreshSearchWidget(); + // When the search activity changes, update the search widget view + SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); + mSearchWidgetInfo = ssp.getOrBindSearchAppWidget(context, mAppWidgetHost); + refreshSearchWidgetView(); } } }; @@ -180,17 +185,17 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView }); /** Updates the set of recent tasks */ - void updateRecentsTasks(Intent launchIntent) { + void updateRecentsTasks() { // If AlternateRecentsComponent has preloaded a load plan, then use that to prevent // reconstructing the task stack RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); - RecentsTaskLoadPlan plan = AlternateRecentsComponent.consumeInstanceLoadPlan(); + RecentsTaskLoadPlan plan = Recents.consumeInstanceLoadPlan(); if (plan == null) { plan = loader.createLoadPlan(this); } // Start loading tasks according to the load plan - if (plan.getTaskStack() == null) { + if (!plan.hasTasks()) { loader.preloadTasks(plan, mConfig.launchedFromHome); } RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options(); @@ -199,13 +204,11 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView loadOpts.numVisibleTaskThumbnails = mConfig.launchedNumVisibleThumbnails; loader.loadTasks(this, plan, loadOpts); - SpaceNode root = plan.getSpaceNode(); - ArrayList<TaskStack> stacks = root.getStacks(); - boolean hasTasks = root.hasTasks(); - if (hasTasks) { + ArrayList<TaskStack> stacks = plan.getAllTaskStacks(); + mConfig.launchedWithNoRecentTasks = !plan.hasTasks(); + if (!mConfig.launchedWithNoRecentTasks) { mRecentsView.setTaskStacks(stacks); } - mConfig.launchedWithNoRecentTasks = !hasTasks; // Create the home intent runnable Intent homeIntent = new Intent(Intent.ACTION_MAIN, null); @@ -247,10 +250,10 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView if (mEmptyView != null) { mEmptyView.setVisibility(View.GONE); } - if (mRecentsView.hasSearchBar()) { + if (mRecentsView.hasValidSearchBar()) { mRecentsView.setSearchBarVisibility(View.VISIBLE); } else { - addSearchBarAppWidgetView(); + refreshSearchWidgetView(); } } @@ -258,60 +261,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView mScrimViews.prepareEnterRecentsAnimation(); } - /** Attempts to allocate and bind the search bar app widget */ - void bindSearchBarAppWidget() { - if (Constants.DebugFlags.App.EnableSearchLayout) { - SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); - - // Reset the host view and widget info - mSearchAppWidgetHostView = null; - mSearchAppWidgetInfo = null; - - // Try and load the app widget id from the settings - int appWidgetId = mConfig.searchBarAppWidgetId; - if (appWidgetId >= 0) { - mSearchAppWidgetInfo = ssp.getAppWidgetInfo(appWidgetId); - if (mSearchAppWidgetInfo == null) { - // If there is no actual widget associated with that id, then delete it and - // prepare to bind another app widget in its place - ssp.unbindSearchAppWidget(mAppWidgetHost, appWidgetId); - appWidgetId = -1; - } - } - - // If there is no id, then bind a new search app widget - if (appWidgetId < 0) { - Pair<Integer, AppWidgetProviderInfo> widgetInfo = - ssp.bindSearchAppWidget(mAppWidgetHost); - if (widgetInfo != null) { - // Save the app widget id into the settings - mConfig.updateSearchBarAppWidgetId(this, widgetInfo.first); - mSearchAppWidgetInfo = widgetInfo.second; - } - } - } - } - - /** Creates the search bar app widget view */ - void addSearchBarAppWidgetView() { - if (Constants.DebugFlags.App.EnableSearchLayout) { - int appWidgetId = mConfig.searchBarAppWidgetId; - if (appWidgetId >= 0) { - mSearchAppWidgetHostView = mAppWidgetHost.createView(this, appWidgetId, - mSearchAppWidgetInfo); - Bundle opts = new Bundle(); - opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, - AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); - mSearchAppWidgetHostView.updateAppWidgetOptions(opts); - // Set the padding to 0 for this search widget - mSearchAppWidgetHostView.setPadding(0, 0, 0, 0); - mRecentsView.setSearchBar(mSearchAppWidgetHostView); - } else { - mRecentsView.setSearchBar(null); - } - } - } - /** Dismisses recents if we are already visible and the intent is to toggle the recents view */ boolean dismissRecentsToFocusedTaskOrHome(boolean checkFilteredStackState) { SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); @@ -347,6 +296,12 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView } } + /** Dismisses Recents directly to Home without transition animation. */ + void dismissRecentsToHomeWithoutTransitionAnimation() { + finish(); + overridePendingTransition(0, 0); + } + /** Dismisses Recents directly to Home if we currently aren't transitioning. */ boolean dismissRecentsToHome(boolean animated) { SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); @@ -362,12 +317,11 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // For the non-primary user, ensure that the SystemSericesProxy is initialized + // For the non-primary user, ensure that the SystemServicesProxy and configuration is + // initialized RecentsTaskLoader.initialize(this); - - // Initialize the loader and the configuration - mConfig = RecentsConfiguration.reinitialize(this, - RecentsTaskLoader.getInstance().getSystemServicesProxy()); + SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); + mConfig = RecentsConfiguration.reinitialize(this, ssp); // Initialize the widget host (the host id is static and does not change) mAppWidgetHost = new RecentsAppWidgetHost(this, Constants.Values.App.AppWidgetHostId); @@ -382,27 +336,16 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView mEmptyViewStub = (ViewStub) findViewById(R.id.empty_view_stub); mDebugOverlayStub = (ViewStub) findViewById(R.id.debug_overlay_stub); mScrimViews = new SystemBarScrimViews(this, mConfig); - mStatusBar = ((SystemUIApplication) getApplication()) - .getComponent(PhoneStatusBar.class); inflateDebugOverlay(); // Bind the search app widget when we first start up - bindSearchBarAppWidget(); + mSearchWidgetInfo = ssp.getOrBindSearchAppWidget(this, mAppWidgetHost); // Register the broadcast receiver to handle messages when the screen is turned off IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); registerReceiver(mSystemBroadcastReceiver, filter); - - // Private API calls to make the shadows look better - try { - Utilities.setShadowProperty("ambientRatio", String.valueOf(1.5f)); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } } /** Inflates the debug overlay if debug mode is enabled. */ @@ -422,9 +365,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView super.onNewIntent(intent); setIntent(intent); - // Reinitialize the configuration - RecentsConfiguration.reinitialize(this, RecentsTaskLoader.getInstance().getSystemServicesProxy()); - // Clear any debug rects if (mDebugOverlay != null) { mDebugOverlay.clear(); @@ -436,20 +376,39 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView super.onStart(); RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); - AlternateRecentsComponent.notifyVisibilityChanged(this, ssp, true); + Recents.notifyVisibilityChanged(this, ssp, true); // Register the broadcast receiver to handle messages from our service IntentFilter filter = new IntentFilter(); - filter.addAction(AlternateRecentsComponent.ACTION_HIDE_RECENTS_ACTIVITY); - filter.addAction(AlternateRecentsComponent.ACTION_TOGGLE_RECENTS_ACTIVITY); - filter.addAction(AlternateRecentsComponent.ACTION_START_ENTER_ANIMATION); + filter.addAction(Recents.ACTION_HIDE_RECENTS_ACTIVITY); + filter.addAction(Recents.ACTION_TOGGLE_RECENTS_ACTIVITY); + filter.addAction(Recents.ACTION_START_ENTER_ANIMATION); registerReceiver(mServiceBroadcastReceiver, filter); // Register any broadcast receivers for the task loader loader.registerReceivers(this, mRecentsView); // Update the recent tasks - updateRecentsTasks(getIntent()); + updateRecentsTasks(); + + // If this is a new instance from a configuration change, then we have to manually trigger + // the enter animation state + if (mConfig.launchedHasConfigurationChanged) { + onEnterAnimationTriggered(); + } + + if (!mConfig.launchedHasConfigurationChanged) { + mRecentsView.disableLayersForOneFrame(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (mAfterPauseRunnable != null) { + mRecentsView.post(mAfterPauseRunnable); + mAfterPauseRunnable = null; + } } @Override @@ -457,7 +416,7 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView super.onStop(); RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); - AlternateRecentsComponent.notifyVisibilityChanged(this, ssp, false); + Recents.notifyVisibilityChanged(this, ssp, false); // Notify the views that we are no longer visible mRecentsView.onRecentsHidden(); @@ -485,7 +444,8 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView ReferenceCountedTrigger t = new ReferenceCountedTrigger(this, null, null, null); ViewAnimation.TaskViewEnterContext ctx = new ViewAnimation.TaskViewEnterContext(t); mRecentsView.startEnterRecentsAnimation(ctx); - if (mConfig.searchBarAppWidgetId >= 0) { + + if (mSearchWidgetInfo != null) { final WeakReference<RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks> cbRef = new WeakReference<RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks>( RecentsActivity.this); @@ -565,10 +525,9 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView /** Called when debug mode is triggered */ public void onDebugModeTriggered() { if (mConfig.developerOptionsEnabled) { - SharedPreferences settings = getSharedPreferences(getPackageName(), 0); - if (settings.getBoolean(Constants.Values.App.Key_DebugModeEnabled, false)) { + if (Prefs.getBoolean(this, Prefs.Key.DEBUG_MODE_ENABLED, false /* boolean */)) { // Disable the debug mode - settings.edit().remove(Constants.Values.App.Key_DebugModeEnabled).apply(); + Prefs.remove(this, Prefs.Key.DEBUG_MODE_ENABLED); mConfig.debugModeEnabled = false; inflateDebugOverlay(); if (mDebugOverlay != null) { @@ -576,7 +535,7 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView } } else { // Enable the debug mode - settings.edit().putBoolean(Constants.Values.App.Key_DebugModeEnabled, true).apply(); + Prefs.putBoolean(this, Prefs.Key.DEBUG_MODE_ENABLED, true); mConfig.debugModeEnabled = true; inflateDebugOverlay(); if (mDebugOverlay != null) { @@ -589,6 +548,21 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView } } + + /**** RecentsResizeTaskDialog ****/ + + private RecentsResizeTaskDialog getResizeTaskDebugDialog() { + if (mResizeTaskDebugDialog == null) { + mResizeTaskDebugDialog = new RecentsResizeTaskDialog(getFragmentManager(), this); + } + return mResizeTaskDebugDialog; + } + + @Override + public void onTaskResize(Task t) { + getResizeTaskDebugDialog().showResizeTaskDialog(t, mRecentsView); + } + /**** RecentsView.RecentsViewCallbacks Implementation ****/ @Override @@ -614,17 +588,35 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView @Override public void onScreenPinningRequest() { - if (mStatusBar != null) { - mStatusBar.showScreenPinningRequest(false); - } + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + SystemServicesProxy ssp = loader.getSystemServicesProxy(); + Recents.startScreenPinning(this, ssp); + } + + @Override + public void runAfterPause(Runnable r) { + mAfterPauseRunnable = r; } /**** RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks Implementation ****/ @Override - public void refreshSearchWidget() { - bindSearchBarAppWidget(); - addSearchBarAppWidgetView(); + public void refreshSearchWidgetView() { + if (mSearchWidgetInfo != null) { + SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); + int searchWidgetId = ssp.getSearchAppWidgetId(this); + mSearchWidgetHostView = (RecentsAppWidgetHostView) mAppWidgetHost.createView( + this, searchWidgetId, mSearchWidgetInfo); + Bundle opts = new Bundle(); + opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, + AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); + mSearchWidgetHostView.updateAppWidgetOptions(opts); + // Set the padding to 0 for this search widget + mSearchWidgetHostView.setPadding(0, 0, 0, 0); + mRecentsView.setSearchBar(mSearchWidgetHostView); + } else { + mRecentsView.setSearchBar(null); + } } /**** DebugOverlayView.DebugOverlayViewCallbacks ****/ diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java index 5bae37a..0102332 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java @@ -17,28 +17,23 @@ package com.android.systemui.recents; import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; -import com.android.systemui.recents.misc.SystemServicesProxy; -import com.android.systemui.recents.model.RecentsTaskLoader; /** Our special app widget host for the Search widget */ public class RecentsAppWidgetHost extends AppWidgetHost { /* Callbacks to notify when an app package changes */ interface RecentsAppWidgetHostCallbacks { - public void refreshSearchWidget(); + void refreshSearchWidgetView(); } - Context mContext; RecentsAppWidgetHostCallbacks mCb; - RecentsConfiguration mConfig; boolean mIsListening; public RecentsAppWidgetHost(Context context, int hostId) { super(context, hostId); - mContext = context; - mConfig = RecentsConfiguration.getInstance(); } public void startListening(RecentsAppWidgetHostCallbacks cb) { @@ -56,21 +51,23 @@ public class RecentsAppWidgetHost extends AppWidgetHost { } // Ensure that we release any references to the callbacks mCb = null; - mContext = null; mIsListening = false; } @Override - protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidgetInfo) { - if (mCb == null) return; + protected AppWidgetHostView onCreateView(Context context, int appWidgetId, + AppWidgetProviderInfo appWidget) { + return new RecentsAppWidgetHostView(context); + } - SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); - if (appWidgetId > -1 && appWidgetId == mConfig.searchBarAppWidgetId) { - // The search provider may have changed, so just delete the old widget and bind it again - ssp.unbindSearchAppWidget(this, appWidgetId); - // Update the search widget - mConfig.updateSearchBarAppWidgetId(mContext, -1); - mCb.refreshSearchWidget(); + /** + * Note: this is only called for packages that have updated, not removed. + */ + @Override + protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidgetInfo) { + super.onProviderChanged(appWidgetId, appWidgetInfo); + if (mIsListening && mCb != null) { + mCb.refreshSearchWidgetView(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHostView.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHostView.java new file mode 100644 index 0000000..672d602 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHostView.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.appwidget.AppWidgetHostView; +import android.content.Context; +import android.view.View; +import android.widget.RemoteViews; + +public class RecentsAppWidgetHostView extends AppWidgetHostView { + + private Context mContext; + private int mPreviousOrientation; + + public RecentsAppWidgetHostView(Context context) { + super(context); + mContext = context; + } + + @Override + public void updateAppWidget(RemoteViews remoteViews) { + // Store the orientation in which the widget was inflated + updateLastInflationOrientation(); + super.updateAppWidget(remoteViews); + } + + @Override + protected View getErrorView() { + // Just return an empty view as the error view when failing to inflate the Recents search + // bar widget (this is mainly to catch the case where we try and inflate the widget view + // while the search provider is updating) + return new View(mContext); + } + + /** + * Updates the last orientation that this widget was inflated. + */ + private void updateLastInflationOrientation() { + mPreviousOrientation = mContext.getResources().getConfiguration().orientation; + } + + /** + * @return whether the search widget was updated while Recents was in a different orientation + * in the background. + */ + public boolean isReinflateRequired() { + // Re-inflate is required if the orientation has changed since last inflated. + int orientation = mContext.getResources().getConfiguration().orientation; + if (mPreviousOrientation != orientation) { + return true; + } + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java index 52e7e7f..dfe7e96 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java @@ -18,15 +18,15 @@ package com.android.systemui.recents; import android.app.ActivityManager; import android.content.Context; -import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.provider.Settings; import android.util.DisplayMetrics; -import android.util.TypedValue; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; + +import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.recents.misc.Console; import com.android.systemui.recents.misc.SystemServicesProxy; @@ -50,9 +50,6 @@ public class RecentsConfiguration { // Disable all thumbnail loading. public static final int SVELTE_DISABLE_LOADING = 3; - /** Animations */ - public float animationPxMovementPerSecond; - /** Interpolators */ public Interpolator fastOutSlowInInterpolator; public Interpolator fastOutLinearInInterpolator; @@ -76,13 +73,13 @@ public class RecentsConfiguration { public int maxNumTasksToLoad; /** Search bar */ - int searchBarAppWidgetId = -1; public int searchBarSpaceHeightPx; /** Task stack */ public int taskStackScrollDuration; public int taskStackMaxDim; public int taskStackTopPaddingPx; + public int dismissAllButtonSizePx; public float taskStackWidthPaddingPct; public float taskStackOverscrollPct; @@ -137,6 +134,7 @@ public class RecentsConfiguration { public boolean fakeShadows; /** Dev options and global settings */ + public boolean multiStackEnabled; public boolean lockToAppEnabled; public boolean developerOptionsEnabled; public boolean debugModeEnabled; @@ -179,12 +177,12 @@ public class RecentsConfiguration { /** Updates the state, given the specified context */ void update(Context context) { - SharedPreferences settings = context.getSharedPreferences(context.getPackageName(), 0); Resources res = context.getResources(); DisplayMetrics dm = res.getDisplayMetrics(); // Debug mode - debugModeEnabled = settings.getBoolean(Constants.Values.App.Key_DebugModeEnabled, false); + debugModeEnabled = Prefs.getBoolean(context, Prefs.Key.DEBUG_MODE_ENABLED, + false /* defaultValue */); if (debugModeEnabled) { Console.Enabled = true; } @@ -197,10 +195,6 @@ public class RecentsConfiguration { // Insets displayRect.set(0, 0, dm.widthPixels, dm.heightPixels); - // Animations - animationPxMovementPerSecond = - res.getDimensionPixelSize(R.dimen.recents_animation_movement_in_dps_per_second); - // Filtering filteringCurrentViewsAnimDuration = res.getInteger(R.integer.recents_filter_animate_current_views_duration); @@ -212,19 +206,15 @@ public class RecentsConfiguration { // Search Bar searchBarSpaceHeightPx = res.getDimensionPixelSize(R.dimen.recents_search_bar_space_height); - searchBarAppWidgetId = settings.getInt(Constants.Values.App.Key_SearchAppWidgetId, -1); // Task stack taskStackScrollDuration = res.getInteger(R.integer.recents_animate_task_stack_scroll_duration); - TypedValue widthPaddingPctValue = new TypedValue(); - res.getValue(R.dimen.recents_stack_width_padding_percentage, widthPaddingPctValue, true); - taskStackWidthPaddingPct = widthPaddingPctValue.getFloat(); - TypedValue stackOverscrollPctValue = new TypedValue(); - res.getValue(R.dimen.recents_stack_overscroll_percentage, stackOverscrollPctValue, true); - taskStackOverscrollPct = stackOverscrollPctValue.getFloat(); + taskStackWidthPaddingPct = res.getFloat(R.dimen.recents_stack_width_padding_percentage); + taskStackOverscrollPct = res.getFloat(R.dimen.recents_stack_overscroll_percentage); taskStackMaxDim = res.getInteger(R.integer.recents_max_task_stack_view_dim); taskStackTopPaddingPx = res.getDimensionPixelSize(R.dimen.recents_stack_top_padding); + dismissAllButtonSizePx = res.getDimensionPixelSize(R.dimen.recents_dismiss_all_button_size); // Transition transitionEnterFromAppDelay = @@ -254,22 +244,16 @@ public class RecentsConfiguration { taskViewTranslationZMaxPx = res.getDimensionPixelSize(R.dimen.recents_task_view_z_max); taskViewAffiliateGroupEnterOffsetPx = res.getDimensionPixelSize(R.dimen.recents_task_view_affiliate_group_enter_offset); - TypedValue thumbnailAlphaValue = new TypedValue(); - res.getValue(R.dimen.recents_task_view_thumbnail_alpha, thumbnailAlphaValue, true); - taskViewThumbnailAlpha = thumbnailAlphaValue.getFloat(); + taskViewThumbnailAlpha = res.getFloat(R.dimen.recents_task_view_thumbnail_alpha); // Task bar colors - taskBarViewDefaultBackgroundColor = - res.getColor(R.color.recents_task_bar_default_background_color); - taskBarViewLightTextColor = - res.getColor(R.color.recents_task_bar_light_text_color); - taskBarViewDarkTextColor = - res.getColor(R.color.recents_task_bar_dark_text_color); - taskBarViewHighlightColor = - res.getColor(R.color.recents_task_bar_highlight_color); - TypedValue affMinAlphaPctValue = new TypedValue(); - res.getValue(R.dimen.recents_task_affiliation_color_min_alpha_percentage, affMinAlphaPctValue, true); - taskBarViewAffiliationColorMinAlpha = affMinAlphaPctValue.getFloat(); + taskBarViewDefaultBackgroundColor = context.getColor( + R.color.recents_task_bar_default_background_color); + taskBarViewLightTextColor = context.getColor(R.color.recents_task_bar_light_text_color); + taskBarViewDarkTextColor = context.getColor(R.color.recents_task_bar_dark_text_color); + taskBarViewHighlightColor = context.getColor(R.color.recents_task_bar_highlight_color); + taskBarViewAffiliationColorMinAlpha = res.getFloat( + R.dimen.recents_task_affiliation_color_min_alpha_percentage); // Task bar size & animations taskBarHeight = res.getDimensionPixelSize(R.dimen.recents_task_bar_height); @@ -292,14 +276,6 @@ public class RecentsConfiguration { systemInsets.set(insets); } - /** Updates the search bar app widget */ - public void updateSearchBarAppWidgetId(Context context, int appWidgetId) { - searchBarAppWidgetId = appWidgetId; - SharedPreferences settings = context.getSharedPreferences(context.getPackageName(), 0); - settings.edit().putInt(Constants.Values.App.Key_SearchAppWidgetId, - appWidgetId).apply(); - } - /** Updates the states that need to be re-read whenever we re-initialize. */ void updateOnReinitialize(Context context, SystemServicesProxy ssp) { // Check if the developer options are enabled @@ -307,6 +283,7 @@ public class RecentsConfiguration { Settings.Global.DEVELOPMENT_SETTINGS_ENABLED) != 0; lockToAppEnabled = ssp.getSystemSetting(context, Settings.System.LOCK_TO_APP_ENABLED) != 0; + multiStackEnabled = "true".equals(ssp.getSystemProperty("persist.sys.debug.multi_window")); } /** Called when the configuration has changed, and we want to reset any configuration specific @@ -318,11 +295,6 @@ public class RecentsConfiguration { launchedHasConfigurationChanged = true; } - /** Returns whether the search bar app widget exists. */ - public boolean hasSearchBarAppWidget() { - return searchBarAppWidgetId >= 0; - } - /** Returns whether the status bar scrim should be animated when shown for the first time. */ public boolean shouldAnimateStatusBarScrim() { return launchedFromHome; @@ -344,19 +316,12 @@ public class RecentsConfiguration { return !launchedWithNoRecentTasks && (!hasTransposedNavBar || !isLandscape); } - /** Returns whether the current layout is horizontal. */ - public boolean hasHorizontalLayout() { - return isLandscape && hasTransposedSearchBar; - } - /** * Returns the task stack bounds in the current orientation. These bounds do not account for * the system insets. */ - public void getTaskStackBounds(int windowWidth, int windowHeight, int topInset, int rightInset, - Rect taskStackBounds) { - Rect searchBarBounds = new Rect(); - getSearchBarBounds(windowWidth, windowHeight, topInset, searchBarBounds); + public void getAvailableTaskStackBounds(int windowWidth, int windowHeight, int topInset, + int rightInset, Rect searchBarBounds, Rect taskStackBounds) { if (isLandscape && hasTransposedSearchBar) { // In landscape, the search bar appears on the left, but we overlay it on top taskStackBounds.set(0, topInset, windowWidth - rightInset, windowHeight); @@ -371,13 +336,9 @@ public class RecentsConfiguration { * the system insets. */ public void getSearchBarBounds(int windowWidth, int windowHeight, int topInset, - Rect searchBarSpaceBounds) { + Rect searchBarSpaceBounds) { // Return empty rects if search is not enabled int searchBarSize = searchBarSpaceHeightPx; - if (!Constants.DebugFlags.App.EnableSearchLayout || !hasSearchBarAppWidget()) { - searchBarSize = 0; - } - if (isLandscape && hasTransposedSearchBar) { // In landscape, the search bar appears on the left searchBarSpaceBounds.set(0, topInset, searchBarSize, windowHeight); diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsResizeTaskDialog.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsResizeTaskDialog.java new file mode 100644 index 0000000..b701e0b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsResizeTaskDialog.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Rect; +import android.os.Bundle; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import com.android.systemui.R; +import com.android.systemui.recents.misc.SystemServicesProxy; +import com.android.systemui.recents.model.RecentsTaskLoader; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.RecentsActivity; +import com.android.systemui.recents.views.RecentsView; + +import java.util.ArrayList; + +/** + * A helper for the dialogs that show when task debugging is on. + */ +public class RecentsResizeTaskDialog extends DialogFragment { + + static final String TAG = "RecentsResizeTaskDialog"; + + // The various window arrangements we can handle. + private static final int PLACE_LEFT = 1; + private static final int PLACE_RIGHT = 2; + private static final int PLACE_TOP = 3; + private static final int PLACE_BOTTOM = 4; + private static final int PLACE_TOP_LEFT = 5; + private static final int PLACE_TOP_RIGHT = 6; + private static final int PLACE_BOTTOM_LEFT = 7; + private static final int PLACE_BOTTOM_RIGHT = 8; + private static final int PLACE_FULL = 9; + + // The button resource ID combined with the arrangement command. + private static final int[][] BUTTON_DEFINITIONS = + {{R.id.place_left, PLACE_LEFT}, + {R.id.place_right, PLACE_RIGHT}, + {R.id.place_top, PLACE_TOP}, + {R.id.place_bottom, PLACE_BOTTOM}, + {R.id.place_top_left, PLACE_TOP_LEFT}, + {R.id.place_top_right, PLACE_TOP_RIGHT}, + {R.id.place_bottom_left, PLACE_BOTTOM_LEFT}, + {R.id.place_bottom_right, PLACE_BOTTOM_RIGHT}, + {R.id.place_full, PLACE_FULL}}; + + // The task we want to resize. + private FragmentManager mFragmentManager; + private View mResizeTaskDialogContent; + private RecentsActivity mRecentsActivity; + private RecentsView mRecentsView; + private SystemServicesProxy mSsp; + private Rect[] mBounds = {new Rect(), new Rect(), new Rect(), new Rect()}; + private Task[] mTasks = {null, null, null, null}; + + public RecentsResizeTaskDialog(FragmentManager mgr, RecentsActivity activity) { + mFragmentManager = mgr; + mRecentsActivity = activity; + mSsp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); + } + + /** Shows the resize-task dialog. */ + void showResizeTaskDialog(Task mainTask, RecentsView rv) { + mTasks[0] = mainTask; + mRecentsView = rv; + + show(mFragmentManager, TAG); + } + + /** Creates a new resize-task dialog. */ + private void createResizeTaskDialog(final Context context, LayoutInflater inflater, + AlertDialog.Builder builder) { + builder.setTitle(R.string.recents_caption_resize); + mResizeTaskDialogContent = + inflater.inflate(R.layout.recents_task_resize_dialog, null, false); + + for (int i = 0; i < BUTTON_DEFINITIONS.length; i++) { + Button b = (Button)mResizeTaskDialogContent.findViewById(BUTTON_DEFINITIONS[i][0]); + if (b != null) { + final int action = BUTTON_DEFINITIONS[i][1]; + b.setOnClickListener( + new View.OnClickListener() { + public void onClick(View v) { + placeTasks(action); + } + }); + } + } + + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }); + + builder.setView(mResizeTaskDialogContent); + } + + /** Helper function to place window(s) on the display according to an arrangement request. */ + private void placeTasks(int arrangement) { + Rect rect = mSsp.getWindowRect(); + for (int i = 0; i < mBounds.length; ++i) { + mBounds[i].set(rect); + if (i != 0) { + mTasks[i] = null; + } + } + int additionalTasks = 0; + switch (arrangement) { + case PLACE_LEFT: + mBounds[0].right = mBounds[0].centerX(); + mBounds[1].left = mBounds[0].right; + additionalTasks = 1; + break; + case PLACE_RIGHT: + mBounds[1].right = mBounds[1].centerX(); + mBounds[0].left = mBounds[1].right; + additionalTasks = 1; + break; + case PLACE_TOP: + mBounds[0].bottom = mBounds[0].centerY(); + mBounds[1].top = mBounds[0].bottom; + additionalTasks = 1; + break; + case PLACE_BOTTOM: + mBounds[1].bottom = mBounds[1].centerY(); + mBounds[0].top = mBounds[1].bottom; + additionalTasks = 1; + break; + case PLACE_TOP_LEFT: // TL, TR, BL, BR + mBounds[0].right = mBounds[0].centerX(); + mBounds[0].bottom = mBounds[0].centerY(); + mBounds[1].left = mBounds[0].right; + mBounds[1].bottom = mBounds[0].bottom; + mBounds[2].right = mBounds[0].right; + mBounds[2].top = mBounds[0].bottom; + mBounds[3].left = mBounds[0].right; + mBounds[3].top = mBounds[0].bottom; + additionalTasks = 3; + break; + case PLACE_TOP_RIGHT: // TR, TL, BR, BL + mBounds[0].left = mBounds[0].centerX(); + mBounds[0].bottom = mBounds[0].centerY(); + mBounds[1].right = mBounds[0].left; + mBounds[1].bottom = mBounds[0].bottom; + mBounds[2].left = mBounds[0].left; + mBounds[2].top = mBounds[0].bottom; + mBounds[3].right = mBounds[0].left; + mBounds[3].top = mBounds[0].bottom; + additionalTasks = 3; + break; + case PLACE_BOTTOM_LEFT: // BL, BR, TL, TR + mBounds[0].right = mBounds[0].centerX(); + mBounds[0].top = mBounds[0].centerY(); + mBounds[1].left = mBounds[0].right; + mBounds[1].top = mBounds[0].top; + mBounds[2].right = mBounds[0].right; + mBounds[2].bottom = mBounds[0].top; + mBounds[3].left = mBounds[0].right; + mBounds[3].bottom = mBounds[0].top; + additionalTasks = 3; + break; + case PLACE_BOTTOM_RIGHT: // BR, BL, TR, TL + mBounds[0].left = mBounds[0].centerX(); + mBounds[0].top = mBounds[0].centerY(); + mBounds[1].right = mBounds[0].left; + mBounds[1].top = mBounds[0].top; + mBounds[2].left = mBounds[0].left; + mBounds[2].bottom = mBounds[0].top; + mBounds[3].right = mBounds[0].left; + mBounds[3].bottom = mBounds[0].top; + additionalTasks = 3; + break; + case PLACE_FULL: + // Nothing to change. + break; + } + + // Get the other tasks. + for (int i = 1; i <= additionalTasks && mTasks[i - 1] != null; ++i) { + mTasks[i] = mRecentsView.getNextTaskOrTopTask(mTasks[i - 1]); + // Do stop if we circled back to the first item. + if (mTasks[i] == mTasks[0]) { + mTasks[i] = null; + } + } + + // Get rid of the dialog. + dismiss(); + mRecentsActivity.dismissRecentsToHomeWithoutTransitionAnimation(); + + // Resize all tasks beginning from the "oldest" one. + for (int i = additionalTasks; i >= 0; --i) { + if (mTasks[i] != null) { + mSsp.resizeTask(mTasks[i].key.id, mBounds[i]); + } + } + + // Show tasks as they might not be currently visible - beginning with the oldest so that + // the focus ends on the selected one. + for (int i = additionalTasks; i >= 0; --i) { + if (mTasks[i] != null) { + mRecentsView.launchTask(mTasks[i]); + } + } + } + + @Override + public Dialog onCreateDialog(Bundle args) { + final Context context = this.getActivity(); + LayoutInflater inflater = getActivity().getLayoutInflater(); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + createResizeTaskDialog(context, inflater, builder); + return builder.create(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsUserEventProxyReceiver.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsUserEventProxyReceiver.java index 236da5d..5eefbc7 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsUserEventProxyReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsUserEventProxyReceiver.java @@ -19,8 +19,6 @@ package com.android.systemui.recents; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import com.android.systemui.recent.Recents; - /** * A proxy for Recents events which happens strictly for non-owner users. @@ -39,28 +37,27 @@ public class RecentsUserEventProxyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - AlternateRecentsComponent recents = Recents.getRecentsComponent( - context.getApplicationContext(), true); + Recents recents = Recents.getInstanceAndStartIfNeeded(context); switch (intent.getAction()) { case ACTION_PROXY_SHOW_RECENTS_TO_USER: { boolean triggeredFromAltTab = intent.getBooleanExtra( - AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_ALT_TAB, false); - recents.showRecents(triggeredFromAltTab); + Recents.EXTRA_TRIGGERED_FROM_ALT_TAB, false); + recents.showRecentsInternal(triggeredFromAltTab); break; } case ACTION_PROXY_HIDE_RECENTS_TO_USER: { boolean triggeredFromAltTab = intent.getBooleanExtra( - AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_ALT_TAB, false); + Recents.EXTRA_TRIGGERED_FROM_ALT_TAB, false); boolean triggeredFromHome = intent.getBooleanExtra( - AlternateRecentsComponent.EXTRA_TRIGGERED_FROM_HOME_KEY, false); - recents.hideRecents(triggeredFromAltTab, triggeredFromHome); + Recents.EXTRA_TRIGGERED_FROM_HOME_KEY, false); + recents.hideRecentsInternal(triggeredFromAltTab, triggeredFromHome); break; } case ACTION_PROXY_TOGGLE_RECENTS_TO_USER: - recents.toggleRecents(); + recents.toggleRecentsInternal(); break; case ACTION_PROXY_PRELOAD_RECENTS_TO_USER: - recents.preloadRecents(); + recents.preloadRecentsInternal(); break; case ACTION_PROXY_CONFIG_CHANGE_TO_USER: recents.configurationChanged(); diff --git a/packages/SystemUI/src/com/android/systemui/recent/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java index 2fa0b58..cbf5c05 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/ScreenPinningRequest.java +++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.recent; +package com.android.systemui.recents; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; @@ -41,7 +41,6 @@ import android.widget.LinearLayout; import android.widget.TextView; import com.android.systemui.R; -import com.android.systemui.recents.model.RecentsTaskLoader; import java.util.ArrayList; @@ -145,7 +144,7 @@ public class ScreenPinningRequest implements View.OnClickListener { boolean isLandscape = isLandscapePhone(mContext); inflateView(isLandscape); - int bgColor = mContext.getResources().getColor( + int bgColor = mContext.getColor( R.color.screen_pinning_request_window_bg); if (ActivityManager.isHighEndGfx()) { mLayout.setAlpha(0f); diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java index 90b099c..b60c66f 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java @@ -20,6 +20,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerNative; import android.app.ActivityOptions; import android.app.AppGlobals; +import android.app.IActivityContainer; import android.app.IActivityManager; import android.app.ITaskStackListener; import android.app.SearchManager; @@ -49,25 +50,29 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; +import android.util.MutableBoolean; import android.util.Pair; +import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; import android.view.SurfaceControl; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; +import com.android.systemui.Prefs; import com.android.systemui.R; -import com.android.systemui.recents.AlternateRecentsComponent; import com.android.systemui.recents.Constants; +import com.android.systemui.recents.Recents; +import com.android.systemui.recents.RecentsAppWidgetHost; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Random; -import java.util.concurrent.atomic.AtomicBoolean; /** * Acts as a shim around the real system services that we need to access data from, and provides @@ -187,7 +192,7 @@ public class SystemServicesProxy { // Break early if we can't get a valid set of tasks if (tasks == null) { - return new ArrayList<ActivityManager.RecentTaskInfo>(); + return new ArrayList<>(); } boolean isFirstValidTask = true; @@ -214,7 +219,7 @@ public class SystemServicesProxy { } /** Returns a list of the running tasks */ - public List<ActivityManager.RunningTaskInfo> getRunningTasks(int numTasks) { + private List<ActivityManager.RunningTaskInfo> getRunningTasks(int numTasks) { if (mAm == null) return null; return mAm.getRunningTasks(numTasks); } @@ -222,7 +227,7 @@ public class SystemServicesProxy { /** Returns the top task. */ public ActivityManager.RunningTaskInfo getTopMostTask() { List<ActivityManager.RunningTaskInfo> tasks = getRunningTasks(1); - if (!tasks.isEmpty()) { + if (tasks != null && !tasks.isEmpty()) { return tasks.get(0); } return null; @@ -230,26 +235,77 @@ public class SystemServicesProxy { /** Returns whether the recents is currently running */ public boolean isRecentsTopMost(ActivityManager.RunningTaskInfo topTask, - AtomicBoolean isHomeTopMost) { + MutableBoolean isHomeTopMost) { if (topTask != null) { ComponentName topActivity = topTask.topActivity; // Check if the front most activity is recents - if (topActivity.getPackageName().equals(AlternateRecentsComponent.sRecentsPackage) && - topActivity.getClassName().equals(AlternateRecentsComponent.sRecentsActivity)) { + if (topActivity.getPackageName().equals(Recents.sRecentsPackage) && + topActivity.getClassName().equals(Recents.sRecentsActivity)) { if (isHomeTopMost != null) { - isHomeTopMost.set(false); + isHomeTopMost.value = false; } return true; } if (isHomeTopMost != null) { - isHomeTopMost.set(isInHomeStack(topTask.id)); + isHomeTopMost.value = isInHomeStack(topTask.id); } } return false; } + /** Get the bounds of a stack / task. */ + public Rect getTaskBounds(int stackId) { + ActivityManager.StackInfo info = getAllStackInfos().get(stackId); + if (info != null) + return info.bounds; + return new Rect(); + } + + /** Resize a given task. */ + public void resizeTask(int taskId, Rect bounds) { + if (mIam == null) return; + + try { + mIam.resizeTask(taskId, bounds); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + /** Returns the stack info for all stacks. */ + public SparseArray<ActivityManager.StackInfo> getAllStackInfos() { + if (mIam == null) return new SparseArray<ActivityManager.StackInfo>(); + + try { + SparseArray<ActivityManager.StackInfo> stacks = + new SparseArray<ActivityManager.StackInfo>(); + List<ActivityManager.StackInfo> infos = mIam.getAllStackInfos(); + int stackCount = infos.size(); + for (int i = 0; i < stackCount; i++) { + ActivityManager.StackInfo info = infos.get(i); + stacks.put(info.stackId, info); + } + return stacks; + } catch (RemoteException e) { + e.printStackTrace(); + return new SparseArray<ActivityManager.StackInfo>(); + } + } + + /** Returns the focused stack id. */ + public int getFocusedStack() { + if (mIam == null) return -1; + + try { + return mIam.getFocusedStackId(); + } catch (RemoteException e) { + e.printStackTrace(); + return -1; + } + } + /** Returns whether the specified task is in the home stack */ public boolean isInHomeStack(int taskId) { if (mAm == null) return false; @@ -313,7 +369,7 @@ public class SystemServicesProxy { return thumbnail; } - /** Moves a task to the front with the specified activity options */ + /** 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; @@ -382,6 +438,33 @@ public class SystemServicesProxy { return info.loadLabel(mPm).toString(); } + /** Returns the application label */ + public String getApplicationLabel(Intent baseIntent, int userId) { + if (mPm == null) return null; + + // If we are mocking, then return a mock label + if (Constants.DebugFlags.App.EnableSystemServicesProxy) { + return "Recent Task"; + } + + ResolveInfo ri = mPm.resolveActivityAsUser(baseIntent, 0, userId); + CharSequence label = (ri != null) ? ri.loadLabel(mPm) : null; + return (label != null) ? label.toString() : null; + } + + /** Returns the content description for a given task */ + public String getContentDescription(Intent baseIntent, int userId, String activityLabel, + Resources res) { + String applicationLabel = getApplicationLabel(baseIntent, userId); + if (applicationLabel == null) { + return getBadgedLabel(activityLabel, userId); + } + String badgedApplicationLabel = getBadgedLabel(applicationLabel, userId); + return applicationLabel.equals(activityLabel) ? badgedApplicationLabel + : res.getString(R.string.accessibility_recents_task_header, + badgedApplicationLabel, activityLabel); + } + /** * Returns the activity icon for the ActivityInfo for a user, badging if * necessary. @@ -408,6 +491,16 @@ public class SystemServicesProxy { return icon; } + /** + * Returns the given label for a user, badging if necessary. + */ + public String getBadgedLabel(String label, int userId) { + if (userId != UserHandle.myUserId()) { + label = mPm.getUserBadgedLabel(label, new UserHandle(userId)).toString(); + } + return label; + } + /** Returns the package name of the home activity. */ public String getHomeActivityPackageName() { if (mPm == null) return null; @@ -436,14 +529,57 @@ public class SystemServicesProxy { } /** - * Resolves and returns the first Recents widget from the same package as the global - * assist activity. + * Returns the current search widget id. */ - public AppWidgetProviderInfo resolveSearchAppWidget() { - if (mAwm == null) return null; - if (mAssistComponent == null) return null; + public int getSearchAppWidgetId(Context context) { + return Prefs.getInt(context, Prefs.Key.SEARCH_APP_WIDGET_ID, -1); + } + + /** + * Returns the current search widget info, binding a new one if necessary. + */ + public AppWidgetProviderInfo getOrBindSearchAppWidget(Context context, AppWidgetHost host) { + int searchWidgetId = Prefs.getInt(context, Prefs.Key.SEARCH_APP_WIDGET_ID, -1); + AppWidgetProviderInfo searchWidgetInfo = mAwm.getAppWidgetInfo(searchWidgetId); + AppWidgetProviderInfo resolvedSearchWidgetInfo = resolveSearchAppWidget(); + + // Return the search widget info if it hasn't changed + if (searchWidgetInfo != null && resolvedSearchWidgetInfo != null && + searchWidgetInfo.provider.equals(resolvedSearchWidgetInfo.provider)) { + if (Prefs.getString(context, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, null) == null) { + Prefs.putString(context, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, + searchWidgetInfo.provider.getPackageName()); + } + return searchWidgetInfo; + } + + // Delete the old widget + if (searchWidgetId != -1) { + host.deleteAppWidgetId(searchWidgetId); + } + + // And rebind a new search widget + if (resolvedSearchWidgetInfo != null) { + Pair<Integer, AppWidgetProviderInfo> widgetInfo = bindSearchAppWidget(host, + resolvedSearchWidgetInfo); + if (widgetInfo != null) { + Prefs.putInt(context, Prefs.Key.SEARCH_APP_WIDGET_ID, widgetInfo.first); + Prefs.putString(context, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, + widgetInfo.second.provider.getPackageName()); + return widgetInfo.second; + } + } + + // If we fall through here, then there is no resolved search widget, so clear the state + Prefs.remove(context, Prefs.Key.SEARCH_APP_WIDGET_ID); + Prefs.remove(context, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE); + return null; + } - // Find the first Recents widget from the same package as the global assist activity + /** + * Returns the first Recents widget from the same package as the global assist activity. + */ + private AppWidgetProviderInfo resolveSearchAppWidget() { List<AppWidgetProviderInfo> widgets = mAwm.getInstalledProviders( AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); for (AppWidgetProviderInfo info : widgets) { @@ -457,45 +593,21 @@ public class SystemServicesProxy { /** * Resolves and binds the search app widget that is to appear in the recents. */ - public Pair<Integer, AppWidgetProviderInfo> bindSearchAppWidget(AppWidgetHost host) { + private Pair<Integer, AppWidgetProviderInfo> bindSearchAppWidget(AppWidgetHost host, + AppWidgetProviderInfo resolvedSearchWidgetInfo) { if (mAwm == null) return null; if (mAssistComponent == null) return null; - // Find the first Recents widget from the same package as the global assist activity - AppWidgetProviderInfo searchWidgetInfo = resolveSearchAppWidget(); - - // Return early if there is no search widget - if (searchWidgetInfo == null) return null; - // Allocate a new widget id and try and bind the app widget (if that fails, then just skip) int searchWidgetId = host.allocateAppWidgetId(); Bundle opts = new Bundle(); opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); - if (!mAwm.bindAppWidgetIdIfAllowed(searchWidgetId, searchWidgetInfo.provider, opts)) { + if (!mAwm.bindAppWidgetIdIfAllowed(searchWidgetId, resolvedSearchWidgetInfo.provider, opts)) { host.deleteAppWidgetId(searchWidgetId); return null; } - return new Pair<Integer, AppWidgetProviderInfo>(searchWidgetId, searchWidgetInfo); - } - - /** - * Returns the app widget info for the specified app widget id. - */ - public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) { - if (mAwm == null) return null; - - return mAwm.getAppWidgetInfo(appWidgetId); - } - - /** - * Destroys the specified app widget. - */ - public void unbindSearchAppWidget(AppWidgetHost host, int appWidgetId) { - if (mAwm == null) return; - - // Delete the app widget - host.deleteAppWidgetId(appWidgetId); + return new Pair<>(searchWidgetId, resolvedSearchWidgetInfo); } /** @@ -524,6 +636,13 @@ public class SystemServicesProxy { } /** + * Returns a system property. + */ + public String getSystemProperty(String key) { + return SystemProperties.get(key); + } + + /** * Returns the window rect. */ public Rect getWindowRect() { diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java index e1179fa..e810410 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java @@ -21,42 +21,12 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.view.View; -import com.android.systemui.recents.RecentsConfiguration; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; /* Common code */ public class Utilities { - // Reflection methods for altering shadows - private static Method sPropertyMethod; - static { - try { - Class<?> c = Class.forName("android.view.GLES20Canvas"); - sPropertyMethod = c.getDeclaredMethod("setProperty", String.class, String.class); - if (!sPropertyMethod.isAccessible()) sPropertyMethod.setAccessible(true); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } - } - - /** - * 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) { @@ -177,12 +147,6 @@ public class Utilities { (1f - overlayAlpha) * Color.blue(overlayColor))); } - /** Sets some private shadow properties. */ - public static void setShadowProperty(String property, String value) - throws IllegalAccessException, InvocationTargetException { - sPropertyMethod.invoke(null, property, value); - } - /** * Cancels an animation ensuring that if it has listeners, onCancel and onEnd * are not called. diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java index 0e1c01a..f40c58d 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java @@ -20,9 +20,12 @@ import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.util.Log; +import android.util.SparseArray; +import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.SystemServicesProxy; @@ -34,11 +37,11 @@ import java.util.List; /** * This class stores the loading state as it goes through multiple stages of loading: - * - preloadRawTasks() will load the raw set of recents tasks from the system - * - preloadPlan() will construct a new task stack with all metadata and only icons and thumbnails - * that are currently in the cache - * - executePlan() will actually load and fill in the icons and thumbnails according to the load - * options specified, such that we can transition into the Recents activity seamlessly + * 1) preloadRawTasks() will load the raw set of recents tasks from the system + * 2) preloadPlan() will construct a new task stack with all metadata and only icons and + * thumbnails that are currently in the cache + * 3) executePlan() will actually load and fill in the icons and thumbnails according to the load + * options specified, such that we can transition into the Recents activity seamlessly */ public class RecentsTaskLoadPlan { static String TAG = "RecentsTaskLoadPlan"; @@ -60,7 +63,7 @@ public class RecentsTaskLoadPlan { SystemServicesProxy mSystemServicesProxy; List<ActivityManager.RecentTaskInfo> mRawTasks; - TaskStack mStack; + SparseArray<TaskStack> mStacks = new SparseArray<>(); HashMap<Task.ComponentNameKey, ActivityInfoHandle> mActivityInfoCache = new HashMap<Task.ComponentNameKey, ActivityInfoHandle>(); @@ -90,11 +93,14 @@ public class RecentsTaskLoadPlan { synchronized void preloadPlan(RecentsTaskLoader loader, boolean isTopTaskHome) { if (DEBUG) Log.d(TAG, "preloadPlan"); + // This activity info cache will be used for both preloadPlan() and executePlan() mActivityInfoCache.clear(); - mStack = new TaskStack(); + + // TODO (multi-display): Currently assume the primary display + Rect displayBounds = mSystemServicesProxy.getWindowRect(); Resources res = mContext.getResources(); - ArrayList<Task> loadedTasks = new ArrayList<Task>(); + SparseArray<ArrayList<Task>> stacksTasks = new SparseArray<>(); if (mRawTasks == null) { preloadRawTasks(isTopTaskHome); } @@ -103,8 +109,8 @@ public class RecentsTaskLoadPlan { ActivityManager.RecentTaskInfo t = mRawTasks.get(i); // Compose the task key - Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.baseIntent, t.userId, - t.firstActiveTime, t.lastActiveTime); + Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.stackId, t.baseIntent, + t.userId, t.firstActiveTime, t.lastActiveTime); // Get an existing activity info handle if possible Task.ComponentNameKey cnKey = taskKey.getComponentNameKey(); @@ -120,6 +126,8 @@ public class RecentsTaskLoadPlan { // Load the label, icon, and color String activityLabel = loader.getAndUpdateActivityLabel(taskKey, t.taskDescription, mSystemServicesProxy, infoHandle); + String contentDescription = loader.getAndUpdateContentDescription(taskKey, + activityLabel, mSystemServicesProxy, res); Drawable activityIcon = loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, mSystemServicesProxy, res, infoHandle, false); int activityColor = loader.getActivityPrimaryColor(t.taskDescription, mConfig); @@ -138,19 +146,48 @@ public class RecentsTaskLoadPlan { // Add the task to the stack Task task = new Task(taskKey, (t.id != RecentsTaskLoader.INVALID_TASK_ID), - t.affiliatedTaskId, t.affiliatedTaskColor, activityLabel, activityIcon, - activityColor, (i == (taskCount - 1)), mConfig.lockToAppEnabled, icon, - iconFilename); + t.affiliatedTaskId, t.affiliatedTaskColor, activityLabel, contentDescription, + activityIcon, activityColor, (i == (taskCount - 1)), mConfig.lockToAppEnabled, + icon, iconFilename); task.thumbnail = loader.getAndUpdateThumbnail(taskKey, mSystemServicesProxy, false); if (DEBUG) Log.d(TAG, "\tthumbnail: " + taskKey + ", " + task.thumbnail); - loadedTasks.add(task); + + if (!mConfig.multiStackEnabled || + Constants.DebugFlags.App.EnableMultiStackToSingleStack) { + int firstStackId = 0; + ArrayList<Task> stackTasks = stacksTasks.get(firstStackId); + if (stackTasks == null) { + stackTasks = new ArrayList<>(); + stacksTasks.put(firstStackId, stackTasks); + } + stackTasks.add(task); + } else { + ArrayList<Task> stackTasks = stacksTasks.get(t.stackId); + if (stackTasks == null) { + stackTasks = new ArrayList<>(); + stacksTasks.put(t.stackId, stackTasks); + } + stackTasks.add(task); + } } - mStack.setTasks(loadedTasks); - mStack.createAffiliatedGroupings(mConfig); - // Assertion - if (mStack.getTaskCount() != mRawTasks.size()) { - throw new RuntimeException("Loading failed"); + // Initialize the stacks + SparseArray<ActivityManager.StackInfo> stackInfos = mSystemServicesProxy.getAllStackInfos(); + mStacks.clear(); + int stackCount = stacksTasks.size(); + for (int i = 0; i < stackCount; i++) { + int stackId = stacksTasks.keyAt(i); + ActivityManager.StackInfo info = stackInfos.get(stackId); + ArrayList<Task> stackTasks = stacksTasks.valueAt(i); + TaskStack stack = new TaskStack(stackId); + if (Constants.DebugFlags.App.EnableMultiStackToSingleStack) { + stack.setBounds(displayBounds, displayBounds); + } else { + stack.setBounds(info.bounds, displayBounds); + } + stack.setTasks(stackTasks); + stack.createAffiliatedGroupings(mConfig); + mStacks.put(stackId, stack); } } @@ -166,72 +203,93 @@ public class RecentsTaskLoadPlan { Resources res = mContext.getResources(); // Iterate through each of the tasks and load them according to the load conditions. - ArrayList<Task> tasks = mStack.getTasks(); - int taskCount = tasks.size(); - for (int i = 0; i < taskCount; i++) { - ActivityManager.RecentTaskInfo t = mRawTasks.get(i); - Task task = tasks.get(i); - Task.TaskKey taskKey = task.key; + int stackCount = mStacks.size(); + for (int j = 0; j < stackCount; j++) { + ArrayList<Task> tasks = mStacks.valueAt(j).getTasks(); + int taskCount = tasks.size(); + for (int i = 0; i < taskCount; i++) { + ActivityManager.RecentTaskInfo t = mRawTasks.get(i); + Task task = tasks.get(i); + Task.TaskKey taskKey = task.key; - // Get an existing activity info handle if possible - Task.ComponentNameKey cnKey = taskKey.getComponentNameKey(); - ActivityInfoHandle infoHandle; - boolean hadCachedActivityInfo = false; - if (mActivityInfoCache.containsKey(cnKey)) { - infoHandle = mActivityInfoCache.get(cnKey); - hadCachedActivityInfo = true; - } else { - infoHandle = new ActivityInfoHandle(); - } + // Get an existing activity info handle if possible + Task.ComponentNameKey cnKey = taskKey.getComponentNameKey(); + ActivityInfoHandle infoHandle; + boolean hadCachedActivityInfo = false; + if (mActivityInfoCache.containsKey(cnKey)) { + infoHandle = mActivityInfoCache.get(cnKey); + hadCachedActivityInfo = true; + } else { + infoHandle = new ActivityInfoHandle(); + } - boolean isRunningTask = (task.key.id == opts.runningTaskId); - boolean isVisibleTask = i >= (taskCount - opts.numVisibleTasks); - boolean isVisibleThumbnail = i >= (taskCount - opts.numVisibleTaskThumbnails); + boolean isRunningTask = (task.key.id == opts.runningTaskId); + boolean isVisibleTask = i >= (taskCount - opts.numVisibleTasks); + boolean isVisibleThumbnail = i >= (taskCount - opts.numVisibleTaskThumbnails); - // If requested, skip the running task - if (opts.onlyLoadPausedActivities && isRunningTask) { - continue; - } + // If requested, skip the running task + if (opts.onlyLoadPausedActivities && isRunningTask) { + continue; + } - if (opts.loadIcons && (isRunningTask || isVisibleTask)) { - if (task.activityIcon == null) { - if (DEBUG) Log.d(TAG, "\tLoading icon: " + taskKey); - task.activityIcon = loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, - mSystemServicesProxy, res, infoHandle, true); + if (opts.loadIcons && (isRunningTask || isVisibleTask)) { + if (task.activityIcon == null) { + if (DEBUG) Log.d(TAG, "\tLoading icon: " + taskKey); + task.activityIcon = loader.getAndUpdateActivityIcon(taskKey, + t.taskDescription, mSystemServicesProxy, res, infoHandle, true); + } } - } - if (opts.loadThumbnails && (isRunningTask || isVisibleThumbnail)) { - if (task.thumbnail == null || isRunningTask) { - if (DEBUG) Log.d(TAG, "\tLoading thumbnail: " + taskKey); - if (mConfig.svelteLevel <= RecentsConfiguration.SVELTE_LIMIT_CACHE) { - task.thumbnail = loader.getAndUpdateThumbnail(taskKey, mSystemServicesProxy, - true); - } else if (mConfig.svelteLevel == RecentsConfiguration.SVELTE_DISABLE_CACHE) { - loadQueue.addTask(task); + if (opts.loadThumbnails && (isRunningTask || isVisibleThumbnail)) { + if (task.thumbnail == null || isRunningTask) { + if (DEBUG) Log.d(TAG, "\tLoading thumbnail: " + taskKey); + if (mConfig.svelteLevel <= RecentsConfiguration.SVELTE_LIMIT_CACHE) { + task.thumbnail = loader.getAndUpdateThumbnail(taskKey, + mSystemServicesProxy, true); + } else if (mConfig.svelteLevel == RecentsConfiguration.SVELTE_DISABLE_CACHE) { + loadQueue.addTask(task); + } } } - } - // Update the activity info cache - if (!hadCachedActivityInfo && infoHandle.info != null) { - mActivityInfoCache.put(cnKey, infoHandle); + // Update the activity info cache + if (!hadCachedActivityInfo && infoHandle.info != null) { + mActivityInfoCache.put(cnKey, infoHandle); + } } } } /** - * Composes and returns a TaskStack from the preloaded list of recent tasks. + * Returns all TaskStacks from the preloaded list of recent tasks. */ - public TaskStack getTaskStack() { - return mStack; + public ArrayList<TaskStack> getAllTaskStacks() { + ArrayList<TaskStack> stacks = new ArrayList<TaskStack>(); + int stackCount = mStacks.size(); + for (int i = 0; i < stackCount; i++) { + stacks.add(mStacks.valueAt(i)); + } + // Ensure that we have at least one stack + if (stacks.isEmpty()) { + stacks.add(new TaskStack()); + } + return stacks; } /** - * Composes and returns a SpaceNode from the preloaded list of recent tasks. + * Returns a specific TaskStack from the preloaded list of recent tasks. */ - public SpaceNode getSpaceNode() { - SpaceNode node = new SpaceNode(); - node.setStack(mStack); - return node; + public TaskStack getTaskStack(int stackId) { + return mStacks.get(stackId); + } + + /** Returns whether there are any tasks in any stacks. */ + public boolean hasTasks() { + int stackCount = mStacks.size(); + for (int i = 0; i < stackCount; i++) { + if (mStacks.valueAt(i).getTaskCount() > 0) { + return true; + } + } + return false; } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java index ba2903a..b2aa2b6 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java @@ -33,7 +33,6 @@ import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.SystemServicesProxy; -import java.util.Collection; import java.util.concurrent.ConcurrentLinkedQueue; @@ -47,18 +46,6 @@ class TaskResourceLoadQueue { ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>(); /** Adds a new task to the load queue */ - void addTasks(Collection<Task> tasks) { - for (Task t : tasks) { - if (!mQueue.contains(t)) { - mQueue.add(t); - } - } - synchronized(this) { - notifyAll(); - } - } - - /** Adds a new task to the load queue */ void addTask(Task t) { if (!mQueue.contains(t)) { mQueue.add(t); @@ -272,6 +259,7 @@ public class RecentsTaskLoader { DrawableLruCache mApplicationIconCache; BitmapLruCache mThumbnailCache; StringLruCache mActivityLabelCache; + StringLruCache mContentDescriptionCache; TaskResourceLoadQueue mLoadQueue; TaskResourceLoader mLoader; @@ -311,6 +299,7 @@ public class RecentsTaskLoader { mApplicationIconCache = new DrawableLruCache(iconCacheSize); mThumbnailCache = new BitmapLruCache(thumbnailCacheSize); mActivityLabelCache = new StringLruCache(100); + mContentDescriptionCache = new StringLruCache(100); mLoader = new TaskResourceLoader(mLoadQueue, mApplicationIconCache, mThumbnailCache, mDefaultThumbnail, mDefaultApplicationIcon); } @@ -361,6 +350,24 @@ public class RecentsTaskLoader { return label; } + /** Returns the content description using as many cached values as we can. */ + public String getAndUpdateContentDescription(Task.TaskKey taskKey, String activityLabel, + SystemServicesProxy ssp, Resources res) { + // Return the cached content description if it exists + String label = mContentDescriptionCache.getAndInvalidateIfModified(taskKey); + if (label != null) { + return label; + } + label = ssp.getContentDescription(taskKey.baseIntent, taskKey.userId, activityLabel, res); + if (label != null) { + mContentDescriptionCache.put(taskKey, label); + } else { + Log.w(TAG, "Missing content description for " + taskKey.baseIntent.getComponent() + + " u=" + taskKey.userId); + } + return label; + } + /** Returns the activity icon using as many cached values as we can. */ public Drawable getAndUpdateActivityIcon(Task.TaskKey taskKey, ActivityManager.TaskDescription td, SystemServicesProxy ssp, @@ -554,6 +561,7 @@ public class RecentsTaskLoader { mApplicationIconCache.evictAll(); // The cache is small, only clear the label cache when we are critical mActivityLabelCache.evictAll(); + mContentDescriptionCache.evictAll(); break; default: break; diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java deleted file mode 100644 index 831698a..0000000 --- a/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.recents.model; - -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); - } - - SpaceNode mStartNode; - SpaceNode mEndNode; - - TaskStack mStack; - - public SpaceNode() { - // Do nothing - } - - /** 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 there are any tasks in any stacks below this node. */ - public boolean hasTasks() { - return (mStack.getTaskCount() > 0) || - (mStartNode != null && mStartNode.hasTasks()) || - (mEndNode != null && mEndNode.hasTasks()); - } - - /** 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 index 55dfe45..c14adf4 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java @@ -36,6 +36,9 @@ public class Task { public void onTaskDataLoaded(); /* Notifies when a task has been unbound */ public void onTaskDataUnloaded(); + + /* Notifies when a task's stack id has changed. */ + public void onMultiStackDebugTaskStackIdChanged(); } /** The ComponentNameKey represents the unique primary key for a component @@ -68,14 +71,17 @@ public class Task { public static class TaskKey { final ComponentNameKey mComponentNameKey; public final int id; + public int stackId; public final Intent baseIntent; public final int userId; public long firstActiveTime; public long lastActiveTime; - public TaskKey(int id, Intent intent, int userId, long firstActiveTime, long lastActiveTime) { + public TaskKey(int id, int stackId, Intent intent, int userId, long firstActiveTime, + long lastActiveTime) { mComponentNameKey = new ComponentNameKey(intent.getComponent(), userId); this.id = id; + this.stackId = stackId; this.baseIntent = intent; this.userId = userId; this.firstActiveTime = firstActiveTime; @@ -92,18 +98,19 @@ public class Task { if (!(o instanceof TaskKey)) { return false; } - return id == ((TaskKey) o).id - && userId == ((TaskKey) o).userId; + TaskKey otherKey = (TaskKey) o; + return id == otherKey.id && stackId == otherKey.stackId && userId == otherKey.userId; } @Override public int hashCode() { - return (id << 5) + userId; + return Objects.hash(id, stackId, userId); } @Override public String toString() { return "Task.Key: " + id + ", " + + "s: " + stackId + ", " + "u: " + userId + ", " + "lat: " + lastActiveTime + ", " + baseIntent.getComponent().getPackageName(); @@ -117,6 +124,7 @@ public class Task { public boolean isLaunchTarget; public Drawable applicationIcon; public Drawable activityIcon; + public String contentDescription; public String activityLabel; public int colorPrimary; public boolean useLightOnPrimaryColor; @@ -133,8 +141,8 @@ public class Task { } public Task(TaskKey key, boolean isActive, int taskAffiliation, int taskAffiliationColor, - String activityTitle, Drawable activityIcon, int colorPrimary, - boolean lockToThisTask, boolean lockToTaskEnabled, Bitmap icon, + String activityTitle, String contentDescription, Drawable activityIcon, + int colorPrimary, boolean lockToThisTask, boolean lockToTaskEnabled, Bitmap icon, String iconFilename) { boolean isInAffiliationGroup = (taskAffiliation != key.id); boolean hasAffiliationGroupColor = isInAffiliationGroup && (taskAffiliationColor != 0); @@ -142,6 +150,7 @@ public class Task { this.taskAffiliation = taskAffiliation; this.taskAffiliationColor = taskAffiliationColor; this.activityLabel = activityTitle; + this.contentDescription = contentDescription; this.activityIcon = activityIcon; this.colorPrimary = hasAffiliationGroupColor ? taskAffiliationColor : colorPrimary; this.useLightOnPrimaryColor = Utilities.computeContrastBetweenColors(this.colorPrimary, @@ -159,6 +168,7 @@ public class Task { this.taskAffiliation = o.taskAffiliation; this.taskAffiliationColor = o.taskAffiliationColor; this.activityLabel = o.activityLabel; + this.contentDescription = o.contentDescription; this.activityIcon = o.activityIcon; this.colorPrimary = o.colorPrimary; this.useLightOnPrimaryColor = o.useLightOnPrimaryColor; @@ -180,6 +190,14 @@ public class Task { this.group = group; } + /** Updates the stack id of this task. */ + public void setStackId(int stackId) { + key.stackId = stackId; + if (mCb != null) { + mCb.onMultiStackDebugTaskStackIdChanged(); + } + } + /** Notifies the callback listeners that this task has been loaded */ public void notifyTaskDataLoaded(Bitmap thumbnail, Drawable applicationIcon) { this.applicationIcon = applicationIcon; diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java index 255d642..5aaea15 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java @@ -17,6 +17,7 @@ package com.android.systemui.recents.model; import android.graphics.Color; +import android.graphics.Rect; import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.NamedCounter; @@ -165,39 +166,46 @@ public class TaskStack { public void onStackTaskAdded(TaskStack stack, Task t); /* Notifies when a task has been removed from the stack */ public void onStackTaskRemoved(TaskStack stack, Task removedTask, Task newFrontMostTask); + /* Notifies when all task has been removed from the stack */ + public void onStackAllTasksRemoved(TaskStack stack, ArrayList<Task> removedTasks); /** 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); } - /** A pair of indices representing the group and task positions in the stack and group. */ - public static class GroupTaskIndex { - public int groupIndex; // Index in the stack - public int taskIndex; // Index in the group - - public GroupTaskIndex() {} - - public GroupTaskIndex(int gi, int ti) { - groupIndex = gi; - taskIndex = ti; - } - } - // The task offset to apply to a task id as a group affiliation static final int IndividualTaskIdOffset = 1 << 16; + public final int id; + public final Rect stackBounds = new Rect(); + public final Rect displayBounds = new Rect(); + FilteredTaskList mTaskList = new FilteredTaskList(); TaskStackCallbacks mCb; ArrayList<TaskGrouping> mGroups = new ArrayList<TaskGrouping>(); HashMap<Integer, TaskGrouping> mAffinitiesGroups = new HashMap<Integer, TaskGrouping>(); - /** Sets the callbacks for this task stack */ + public TaskStack() { + this(0); + } + + public TaskStack(int stackId) { + id = stackId; + } + + /** Sets the callbacks for this task stack. */ public void setCallbacks(TaskStackCallbacks cb) { mCb = cb; } + /** Sets the bounds of this stack. */ + public void setBounds(Rect stackBounds, Rect displayBounds) { + this.stackBounds.set(stackBounds); + this.displayBounds.set(displayBounds); + } + /** Resets this TaskStack. */ public void reset() { mCb = null; @@ -214,19 +222,24 @@ public class TaskStack { } } + /** Does the actual work associated with removing the task. */ + void removeTaskImpl(Task t) { + // Remove the task from the list + mTaskList.remove(t); + // Remove it from the group as well, and if it is empty, remove the group + TaskGrouping group = t.group; + group.removeTask(t); + if (group.getTaskCount() == 0) { + removeGroup(group); + } + // Update the lock-to-app state + t.lockToThisTask = false; + } + /** Removes a task */ public void removeTask(Task t) { if (mTaskList.contains(t)) { - // Remove the task from the list - mTaskList.remove(t); - // Remove it from the group as well, and if it is empty, remove the group - TaskGrouping group = t.group; - group.removeTask(t); - if (group.getTaskCount() == 0) { - removeGroup(group); - } - // Update the lock-to-app state - t.lockToThisTask = false; + removeTaskImpl(t); Task newFrontMostTask = getFrontMostTask(); if (newFrontMostTask != null && newFrontMostTask.lockToTaskEnabled) { newFrontMostTask.lockToThisTask = true; @@ -238,20 +251,27 @@ public class TaskStack { } } + /** Removes all tasks */ + public void removeAllTasks() { + ArrayList<Task> taskList = new ArrayList<Task>(mTaskList.getTasks()); + int taskCount = taskList.size(); + for (int i = taskCount - 1; i >= 0; i--) { + Task t = taskList.get(i); + removeTaskImpl(t); + } + if (mCb != null) { + // Notify that all tasks have been removed + mCb.onStackAllTasksRemoved(this, taskList); + } + } + /** Sets a few tasks in one go */ public void setTasks(List<Task> tasks) { ArrayList<Task> taskList = mTaskList.getTasks(); int taskCount = taskList.size(); - for (int i = 0; i < taskCount; i++) { + for (int i = taskCount - 1; i >= 0; i--) { Task t = taskList.get(i); - // Remove the task from the list - mTaskList.remove(t); - // Remove it from the group as well, and if it is empty, remove the group - TaskGrouping group = t.group; - group.removeTask(t); - if (group.getTaskCount() == 0) { - removeGroup(group); - } + removeTaskImpl(t); if (mCb != null) { // Notify that a task has been removed mCb.onStackTaskRemoved(this, t, null); diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/FakeShadowDrawable.java b/packages/SystemUI/src/com/android/systemui/recents/views/FakeShadowDrawable.java index 72f9001..509ad1b 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/FakeShadowDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/FakeShadowDrawable.java @@ -159,9 +159,9 @@ class FakeShadowDrawable extends Drawable { } @Override - public void setColorFilter(ColorFilter cf) { - mCornerShadowPaint.setColorFilter(cf); - mEdgeShadowPaint.setColorFilter(cf); + public void setColorFilter(ColorFilter colorFilter) { + mCornerShadowPaint.setColorFilter(colorFilter); + mEdgeShadowPaint.setColorFilter(colorFilter); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/FixedSizeImageView.java b/packages/SystemUI/src/com/android/systemui/recents/views/FixedSizeImageView.java index 4b5c0bd..3f5d0a8 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/FixedSizeImageView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/FixedSizeImageView.java @@ -73,4 +73,9 @@ public class FixedSizeImageView extends ImageView { mAllowRelayout = true; mAllowInvalidate = true; } + + @Override + public boolean hasOverlappingRendering() { + return false; + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java index ee79242..6cb11b1 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -24,15 +24,22 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.net.Uri; +import android.os.Bundle; +import android.os.IRemoteCallback; +import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.util.AttributeSet; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.WindowInsets; +import android.view.WindowManagerGlobal; import android.widget.FrameLayout; +import com.android.systemui.R; import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsAppWidgetHostView; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.model.RecentsPackageMonitor; @@ -41,6 +48,7 @@ import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; import java.util.ArrayList; +import java.util.List; /** * This view is the the top level layout that contains TaskStacks (which are laid out according @@ -49,6 +57,8 @@ import java.util.ArrayList; public class RecentsView extends FrameLayout implements TaskStackView.TaskStackViewCallbacks, RecentsPackageMonitor.PackageCallbacks { + private static final String TAG = "RecentsView"; + /** The RecentsView callbacks */ public interface RecentsViewCallbacks { public void onTaskViewClicked(); @@ -56,14 +66,18 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV public void onAllTaskViewsDismissed(); public void onExitToHomeAnimationTriggered(); public void onScreenPinningRequest(); + public void onTaskResize(Task t); + public void runAfterPause(Runnable r); } RecentsConfiguration mConfig; LayoutInflater mInflater; DebugOverlayView mDebugOverlay; + RecentsViewLayoutAlgorithm mLayoutAlgorithm; ArrayList<TaskStack> mStacks; - View mSearchBar; + List<TaskStackView> mTaskStackViews = new ArrayList<>(); + RecentsAppWidgetHostView mSearchBar; RecentsViewCallbacks mCb; public RecentsView(Context context) { @@ -82,6 +96,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV super(context, attrs, defStyleAttr, defStyleRes); mConfig = RecentsConfiguration.getInstance(); mInflater = LayoutInflater.from(context); + mLayoutAlgorithm = new RecentsViewLayoutAlgorithm(mConfig); } /** Sets the callbacks */ @@ -98,29 +113,18 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV public void setTaskStacks(ArrayList<TaskStack> stacks) { int numStacks = stacks.size(); - // Make a list of the stack view children only - ArrayList<TaskStackView> stackViews = new ArrayList<TaskStackView>(); - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - stackViews.add((TaskStackView) child); - } - } - // Remove all/extra stack views int numTaskStacksToKeep = 0; // Keep no tasks if we are recreating the layout if (mConfig.launchedReuseTaskStackViews) { - numTaskStacksToKeep = Math.min(childCount, numStacks); + numTaskStacksToKeep = Math.min(mTaskStackViews.size(), numStacks); } - for (int i = stackViews.size() - 1; i >= numTaskStacksToKeep; i--) { - removeView(stackViews.get(i)); - stackViews.remove(i); + for (int i = mTaskStackViews.size() - 1; i >= numTaskStacksToKeep; i--) { + removeView(mTaskStackViews.remove(i)); } // Update the stack views that we are keeping for (int i = 0; i < numTaskStacksToKeep; i++) { - TaskStackView tsv = stackViews.get(i); + TaskStackView tsv = mTaskStackViews.get(i); // If onRecentsHidden is not triggered, we need to the stack view again here tsv.reset(); tsv.setStack(stacks.get(i)); @@ -128,21 +132,19 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV // Add remaining/recreate stack views mStacks = stacks; - for (int i = stackViews.size(); i < numStacks; i++) { + for (int i = mTaskStackViews.size(); i < numStacks; i++) { TaskStack stack = stacks.get(i); TaskStackView stackView = new TaskStackView(getContext(), stack); stackView.setCallbacks(this); addView(stackView); + mTaskStackViews.add(stackView); } // Enable debug mode drawing on all the stacks if necessary if (mConfig.debugModeEnabled) { - for (int i = childCount - 1; i >= 0; i--) { - View v = getChildAt(i); - if (v != mSearchBar) { - TaskStackView stackView = (TaskStackView) v; - stackView.setDebugOverlay(mDebugOverlay); - } + for (int i = mTaskStackViews.size() - 1; i >= 0; i--) { + TaskStackView stackView = mTaskStackViews.get(i); + stackView.setDebugOverlay(mDebugOverlay); } } @@ -150,24 +152,75 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV requestLayout(); } + /** Gets the list of task views */ + List<TaskStackView> getTaskStackViews() { + return mTaskStackViews; + } + + /** Gets the next task in the stack - or if the last - the top task */ + public Task getNextTaskOrTopTask(Task taskToSearch) { + Task returnTask = null; + boolean found = false; + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = stackCount - 1; i >= 0; --i) { + TaskStack stack = stackViews.get(i).getStack(); + ArrayList<Task> taskList = stack.getTasks(); + // Iterate the stack views and try and find the focused task + for (int j = taskList.size() - 1; j >= 0; --j) { + Task task = taskList.get(j); + // Return the next task in the line. + if (found) + return task; + // Remember the first possible task as the top task. + if (returnTask == null) + returnTask = task; + if (task == taskToSearch) + found = true; + } + } + return returnTask; + } + /** Launches the focused task from the first stack if possible */ public boolean launchFocusedTask() { // Get the first stack view - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - TaskStackView stackView = (TaskStackView) child; - TaskStack stack = stackView.mStack; - // Iterate the stack views and try and find the focused task - int taskCount = stackView.getChildCount(); - for (int j = 0; j < taskCount; j++) { - TaskView tv = (TaskView) stackView.getChildAt(j); - Task task = tv.getTask(); - if (tv.isFocusedTask()) { - onTaskViewClicked(stackView, tv, stack, task, false); - return true; - } + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + TaskStack stack = stackView.getStack(); + // Iterate the stack views and try and find the focused task + List<TaskView> taskViews = stackView.getTaskViews(); + int taskViewCount = taskViews.size(); + for (int j = 0; j < taskViewCount; j++) { + TaskView tv = taskViews.get(j); + Task task = tv.getTask(); + if (tv.isFocusedTask()) { + onTaskViewClicked(stackView, tv, stack, task, false); + return true; + } + } + } + return false; + } + + /** Launches a given task. */ + public boolean launchTask(Task task) { + // Get the first stack view + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + TaskStack stack = stackView.getStack(); + // Iterate the stack views and try and find the given task. + List<TaskView> taskViews = stackView.getTaskViews(); + int taskViewCount = taskViews.size(); + for (int j = 0; j < taskViewCount; j++) { + TaskView tv = taskViews.get(j); + if (tv.getTask() == task) { + onTaskViewClicked(stackView, tv, stack, task, false); + return true; } } } @@ -177,24 +230,22 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV /** Launches the task that Recents was launched from, if possible */ public boolean launchPreviousTask() { // Get the first stack view - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - TaskStackView stackView = (TaskStackView) child; - TaskStack stack = stackView.mStack; - ArrayList<Task> tasks = stack.getTasks(); - - // Find the launch task in the stack - if (!tasks.isEmpty()) { - int taskCount = tasks.size(); - for (int j = 0; j < taskCount; j++) { - if (tasks.get(j).isLaunchTarget) { - Task task = tasks.get(j); - TaskView tv = stackView.getChildViewForTask(task); - onTaskViewClicked(stackView, tv, stack, task, false); - return true; - } + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + TaskStack stack = stackView.getStack(); + ArrayList<Task> tasks = stack.getTasks(); + + // Find the launch task in the stack + if (!tasks.isEmpty()) { + int taskCount = tasks.size(); + for (int j = 0; j < taskCount; j++) { + if (tasks.get(j).isLaunchTarget) { + Task task = tasks.get(j); + TaskView tv = stackView.getChildViewForTask(task); + onTaskViewClicked(stackView, tv, stack, task, false); + return true; } } } @@ -208,13 +259,11 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV // to ensure that it runs ctx.postAnimationTrigger.increment(); - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - TaskStackView stackView = (TaskStackView) child; - stackView.startEnterRecentsAnimation(ctx); - } + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + stackView.startEnterRecentsAnimation(ctx); } ctx.postAnimationTrigger.decrement(); } @@ -224,13 +273,11 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV // We have to increment/decrement the post animation trigger in case there are no children // to ensure that it runs ctx.postAnimationTrigger.increment(); - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - TaskStackView stackView = (TaskStackView) child; - stackView.startExitToHomeAnimation(ctx); - } + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + stackView.startExitToHomeAnimation(ctx); } ctx.postAnimationTrigger.decrement(); @@ -239,24 +286,21 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV } /** Adds the search bar */ - public void setSearchBar(View searchBar) { - // Create the search bar (and hide it if we have no recent tasks) - if (Constants.DebugFlags.App.EnableSearchLayout) { - // Remove the previous search bar if one exists - if (mSearchBar != null && indexOfChild(mSearchBar) > -1) { - removeView(mSearchBar); - } - // Add the new search bar - if (searchBar != null) { - mSearchBar = searchBar; - addView(mSearchBar); - } + public void setSearchBar(RecentsAppWidgetHostView searchBar) { + // Remove the previous search bar if one exists + if (mSearchBar != null && indexOfChild(mSearchBar) > -1) { + removeView(mSearchBar); + } + // Add the new search bar + if (searchBar != null) { + mSearchBar = searchBar; + addView(mSearchBar); } } /** Returns whether there is currently a search bar */ - public boolean hasSearchBar() { - return mSearchBar != null; + public boolean hasValidSearchBar() { + return mSearchBar != null && !mSearchBar.isReinflateRequired(); } /** Sets the visibility of the search bar */ @@ -277,8 +321,8 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV int height = MeasureSpec.getSize(heightMeasureSpec); // Get the search bar bounds and measure the search bar layout + Rect searchBarSpaceBounds = new Rect(); if (mSearchBar != null) { - Rect searchBarSpaceBounds = new Rect(); mConfig.getSearchBarBounds(width, height, mConfig.systemInsets.top, searchBarSpaceBounds); mSearchBar.measure( MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.width(), MeasureSpec.EXACTLY), @@ -286,19 +330,23 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV } Rect taskStackBounds = new Rect(); - mConfig.getTaskStackBounds(width, height, mConfig.systemInsets.top, - mConfig.systemInsets.right, taskStackBounds); + mConfig.getAvailableTaskStackBounds(width, height, mConfig.systemInsets.top, + mConfig.systemInsets.right, searchBarSpaceBounds, taskStackBounds); - // Measure each TaskStackView with the full width and height of the window since the + // Measure each TaskStackView with the full width and height of the window since the // transition view is a child of that stack view - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar && child.getVisibility() != GONE) { - TaskStackView tsv = (TaskStackView) child; - // Set the insets to be the top/left inset + search bounds - tsv.setStackInsetRect(taskStackBounds); - tsv.measure(widthMeasureSpec, heightMeasureSpec); + List<TaskStackView> stackViews = getTaskStackViews(); + List<Rect> stackViewsBounds = mLayoutAlgorithm.computeStackRects(stackViews, + taskStackBounds); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + if (stackView.getVisibility() != GONE) { + // We are going to measure the TaskStackView with the whole RecentsView dimensions, + // but the actual stack is going to be inset to the bounds calculated by the layout + // algorithm + stackView.setStackInsetRect(stackViewsBounds.get(i)); + stackView.measure(widthMeasureSpec, heightMeasureSpec); } } @@ -321,12 +369,13 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV // Layout each TaskStackView with the full width and height of the window since the // transition view is a child of that stack view - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar && child.getVisibility() != GONE) { - child.layout(left, top, left + child.getMeasuredWidth(), - top + child.getMeasuredHeight()); + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + if (stackView.getVisibility() != GONE) { + stackView.layout(left, top, left + stackView.getMeasuredWidth(), + top + stackView.getMeasuredHeight()); } } } @@ -342,41 +391,29 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV /** Notifies each task view of the user interaction. */ public void onUserInteraction() { // Get the first stack view - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - TaskStackView stackView = (TaskStackView) child; - stackView.onUserInteraction(); - } + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + stackView.onUserInteraction(); } } /** Focuses the next task in the first stack view */ public void focusNextTask(boolean forward) { // Get the first stack view - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - TaskStackView stackView = (TaskStackView) child; - stackView.focusNextTask(forward, true); - break; - } + List<TaskStackView> stackViews = getTaskStackViews(); + if (!stackViews.isEmpty()) { + stackViews.get(0).focusNextTask(forward, true); } } /** Dismisses the focused task. */ public void dismissFocusedTask() { // Get the first stack view - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - TaskStackView stackView = (TaskStackView) child; - stackView.dismissFocusedTask(); - break; - } + List<TaskStackView> stackViews = getTaskStackViews(); + if (!stackViews.isEmpty()) { + stackViews.get(0).dismissFocusedTask(); } } @@ -398,11 +435,75 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV return false; } + public void disableLayersForOneFrame() { + List<TaskStackView> stackViews = getTaskStackViews(); + for (int i = 0; i < stackViews.size(); i++) { + stackViews.get(i).disableLayersForOneFrame(); + } + } + + private void postDrawHeaderThumbnailTransitionRunnable(final TaskView tv, final int offsetX, + final int offsetY, final TaskViewTransform transform, + final ActivityOptions.OnAnimationStartedListener animStartedListener) { + Runnable r = new Runnable() { + @Override + public void run() { + // Disable any focused state before we draw the header + if (tv.isFocusedTask()) { + tv.unsetFocusedTask(); + } + + float scale = tv.getScaleX(); + int fromHeaderWidth = (int) (tv.mHeaderView.getMeasuredWidth() * scale); + int fromHeaderHeight = (int) (tv.mHeaderView.getMeasuredHeight() * scale); + + Bitmap b = Bitmap.createBitmap(fromHeaderWidth, fromHeaderHeight, + Bitmap.Config.ARGB_8888); + if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) { + b.eraseColor(0xFFff0000); + } else { + Canvas c = new Canvas(b); + c.scale(tv.getScaleX(), tv.getScaleY()); + tv.mHeaderView.draw(c); + c.setBitmap(null); + } + b = b.createAshmemBitmap(); + int[] pts = new int[2]; + tv.getLocationOnScreen(pts); + try { + WindowManagerGlobal.getWindowManagerService() + .overridePendingAppTransitionAspectScaledThumb(b, + pts[0] + offsetX, + pts[1] + offsetY, + transform.rect.width(), + transform.rect.height(), + new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle data) + throws RemoteException { + post(new Runnable() { + @Override + public void run() { + if (animStartedListener != null) { + animStartedListener.onAnimationStarted(); + } + } + }); + } + }, true); + } catch (RemoteException e) { + Log.w(TAG, "Error overriding app transition", e); + } + } + }; + mCb.runAfterPause(r); + } /**** TaskStackView.TaskStackCallbacks Implementation ****/ @Override public void onTaskViewClicked(final TaskStackView stackView, final TaskView tv, final TaskStack stack, final Task task, final boolean lockToTask) { + // Notify any callbacks of the launching of a new task if (mCb != null) { mCb.onTaskViewClicked(); @@ -433,30 +534,6 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV ActivityOptions opts = null; if (task.thumbnail != null && task.thumbnail.getWidth() > 0 && task.thumbnail.getHeight() > 0) { - Bitmap b; - if (tv != null) { - // Disable any focused state before we draw the header - if (tv.isFocusedTask()) { - tv.unsetFocusedTask(); - } - - float scale = tv.getScaleX(); - int fromHeaderWidth = (int) (tv.mHeaderView.getMeasuredWidth() * scale); - int fromHeaderHeight = (int) (tv.mHeaderView.getMeasuredHeight() * scale); - b = Bitmap.createBitmap(fromHeaderWidth, fromHeaderHeight, - Bitmap.Config.ARGB_8888); - if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) { - b.eraseColor(0xFFff0000); - } else { - Canvas c = new Canvas(b); - c.scale(tv.getScaleX(), tv.getScaleY()); - tv.mHeaderView.draw(c); - c.setBitmap(null); - } - } else { - // Notify the system to skip the thumbnail layer by using an ALPHA_8 bitmap - b = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8); - } ActivityOptions.OnAnimationStartedListener animStartedListener = null; if (lockToTask) { animStartedListener = new ActivityOptions.OnAnimationStartedListener() { @@ -475,9 +552,21 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV } }; } - opts = ActivityOptions.makeThumbnailAspectScaleUpAnimation(sourceView, - b, offsetX, offsetY, transform.rect.width(), transform.rect.height(), - sourceView.getHandler(), animStartedListener); + if (tv != null) { + postDrawHeaderThumbnailTransitionRunnable(tv, offsetX, offsetY, transform, + animStartedListener); + } + if (mConfig.multiStackEnabled) { + opts = ActivityOptions.makeCustomAnimation(sourceView.getContext(), + R.anim.recents_from_unknown_enter, + R.anim.recents_from_unknown_exit, + sourceView.getHandler(), animStartedListener); + } else { + opts = ActivityOptions.makeThumbnailAspectScaleUpAnimation(sourceView, + Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8).createAshmemBitmap(), + offsetX, offsetY, transform.rect.width(), transform.rect.height(), + sourceView.getHandler(), animStartedListener); + } } final ActivityOptions launchOpts = opts; @@ -509,7 +598,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV if (tv == null) { launchRunnable.run(); } else { - if (!task.group.isFrontMostTask(task)) { + if (task.group != null && !task.group.isFrontMostTask(task)) { // For affiliated tasks that are behind other tasks, we must animate the front cards // out of view before starting the task transition stackView.startLaunchTaskAnimation(tv, launchRunnable, lockToTask); @@ -542,24 +631,29 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV loader.deleteTaskData(t, false); // Remove the old task from activity manager - RecentsTaskLoader.getInstance().getSystemServicesProxy().removeTask(t.key.id); + loader.getSystemServicesProxy().removeTask(t.key.id); } @Override - public void onAllTaskViewsDismissed() { + public void onAllTaskViewsDismissed(ArrayList<Task> removedTasks) { + if (removedTasks != null) { + int taskCount = removedTasks.size(); + for (int i = 0; i < taskCount; i++) { + onTaskViewDismissed(removedTasks.get(i)); + } + } + mCb.onAllTaskViewsDismissed(); } /** Final callback after Recents is finally hidden. */ public void onRecentsHidden() { // Notify each task stack view - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - TaskStackView stackView = (TaskStackView) child; - stackView.onRecentsHidden(); - } + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + stackView.onRecentsHidden(); } } @@ -591,18 +685,23 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV } } + @Override + public void onTaskResize(Task t) { + if (mCb != null) { + mCb.onTaskResize(t); + } + } + /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/ @Override public void onPackagesChanged(RecentsPackageMonitor monitor, String packageName, int userId) { // Propagate this event down to each task stack view - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child != mSearchBar) { - TaskStackView stackView = (TaskStackView) child; - stackView.onPackagesChanged(monitor, packageName, userId); - } + List<TaskStackView> stackViews = getTaskStackViews(); + int stackCount = stackViews.size(); + for (int i = 0; i < stackCount; i++) { + TaskStackView stackView = stackViews.get(i); + stackView.onPackagesChanged(monitor, packageName, userId); } } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewLayoutAlgorithm.java new file mode 100644 index 0000000..eea273c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewLayoutAlgorithm.java @@ -0,0 +1,59 @@ +/* + * 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; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.model.TaskStack; + +import java.util.ArrayList; +import java.util.List; + +/* The layout logic for the RecentsView. */ +public class RecentsViewLayoutAlgorithm { + + RecentsConfiguration mConfig; + + public RecentsViewLayoutAlgorithm(RecentsConfiguration config) { + mConfig = config; + } + + /** Return the relative coordinate given coordinates in another size. */ + private int getRelativeCoordinate(int availableOffset, int availableSize, int otherCoord, int otherSize) { + float relPos = (float) otherCoord / otherSize; + return availableOffset + (int) (relPos * availableSize); + } + + /** + * Computes and returns the bounds that each of the stack views should take up. + */ + List<Rect> computeStackRects(List<TaskStackView> stackViews, Rect availableBounds) { + ArrayList<Rect> bounds = new ArrayList<Rect>(stackViews.size()); + int stackViewsCount = stackViews.size(); + for (int i = 0; i < stackViewsCount; i++) { + TaskStack stack = stackViews.get(i).getStack(); + Rect sb = stack.stackBounds; + Rect db = stack.displayBounds; + Rect ab = availableBounds; + bounds.add(new Rect(getRelativeCoordinate(ab.left, ab.width(), sb.left, db.width()), + getRelativeCoordinate(ab.top, ab.height(), sb.top, db.height()), + getRelativeCoordinate(ab.left, ab.width(), sb.right, db.width()), + getRelativeCoordinate(ab.top, ab.height(), sb.bottom, db.height()))); + } + return bounds; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java index 169683f..ebfc796 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -19,12 +19,15 @@ package com.android.systemui.recents.views; import android.animation.ValueAnimator; import android.content.ComponentName; import android.content.Context; +import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; +import android.os.Bundle; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import com.android.systemui.R; import com.android.systemui.recents.Constants; @@ -36,11 +39,14 @@ import com.android.systemui.recents.model.RecentsPackageMonitor; import com.android.systemui.recents.model.RecentsTaskLoader; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.statusbar.DismissView; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; /* The visual representation of a task stack view */ @@ -54,11 +60,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal boolean lockToTask); public void onTaskViewAppInfoClicked(Task t); public void onTaskViewDismissed(Task t); - public void onAllTaskViewsDismissed(); + public void onAllTaskViewsDismissed(ArrayList<Task> removedTasks); public void onTaskStackFilterTriggered(); public void onTaskStackUnfilterTriggered(); - } + public void onTaskResize(Task t); + } RecentsConfiguration mConfig; TaskStack mStack; @@ -72,9 +79,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal DozeTrigger mUIDozeTrigger; DebugOverlayView mDebugOverlay; Rect mTaskStackBounds = new Rect(); + DismissView mDismissAllButton; + boolean mDismissAllButtonAnimating; int mFocusedTaskIndex = -1; int mPrevAccessibilityFocusedIndex = -1; - // Optimizations int mStackViewsAnimationDuration; boolean mStackViewsDirty = true; @@ -89,7 +97,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal Rect mTmpRect = new Rect(); TaskViewTransform mTmpTransform = new TaskViewTransform(); HashMap<Task, TaskView> mTmpTaskViewMap = new HashMap<Task, TaskView>(); + ArrayList<TaskView> mTaskViews = new ArrayList<TaskView>(); + List<TaskView> mImmutableTaskViews = new ArrayList<TaskView>(); LayoutInflater mInflater; + boolean mLayersDisabled; // A convenience update listener to request updating clipping of tasks ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener = @@ -116,13 +127,15 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal @Override public void run() { // Show the task bar dismiss buttons - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - TaskView tv = (TaskView) getChildAt(i); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + TaskView tv = taskViews.get(i); tv.startNoUserInteractionAnimation(); } } }); + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } /** Sets the callbacks */ @@ -141,21 +154,44 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal requestLayout(); } + /** Returns the task stack. */ + TaskStack getStack() { + return mStack; + } + /** Sets the debug overlay */ public void setDebugOverlay(DebugOverlayView overlay) { mDebugOverlay = overlay; } + /** Updates the list of task views */ + void updateTaskViewsList() { + mTaskViews.clear(); + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View v = getChildAt(i); + if (v instanceof TaskView) { + mTaskViews.add((TaskView) v); + } + } + mImmutableTaskViews = Collections.unmodifiableList(mTaskViews); + } + + /** Gets the list of task views */ + List<TaskView> getTaskViews() { + return mImmutableTaskViews; + } + /** Resets this TaskStackView for reuse. */ void reset() { // Reset the focused task resetFocusedTask(); // Return all the views to the pool - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - TaskView tv = (TaskView) getChildAt(i); - mViewPool.returnViewToPool(tv); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = taskViewCount - 1; i >= 0; i--) { + mViewPool.returnViewToPool(taskViews.get(i)); } // Mark each task view for relayout @@ -209,9 +245,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal /** Finds the child view given a specific task. */ public TaskView getChildViewForTask(Task t) { - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - TaskView tv = (TaskView) getChildAt(i); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + TaskView tv = taskViews.get(i); if (tv.getTask() == t) { return tv; } @@ -299,17 +336,38 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mDebugOverlay.setText("vis[" + visibleRange[1] + "-" + visibleRange[0] + "]"); } + // Inflate and add the dismiss button if necessary + if (Constants.DebugFlags.App.EnableDismissAll && mDismissAllButton == null) { + mDismissAllButton = (DismissView) + mInflater.inflate(R.layout.recents_dismiss_button, this, false); + mDismissAllButton.setOnButtonClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mStack.removeAllTasks(); + } + }); + addView(mDismissAllButton, 0); + } + // Return all the invisible children to the pool mTmpTaskViewMap.clear(); - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - TaskView tv = (TaskView) getChildAt(i); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + boolean reaquireAccessibilityFocus = false; + for (int i = taskViewCount - 1; i >= 0; i--) { + TaskView tv = taskViews.get(i); Task task = tv.getTask(); int taskIndex = mStack.indexOfTask(task); if (visibleRange[1] <= taskIndex && taskIndex <= visibleRange[0]) { mTmpTaskViewMap.put(task, tv); } else { mViewPool.returnViewToPool(tv); + reaquireAccessibilityFocus |= (i == mPrevAccessibilityFocusedIndex); + + // Hide the dismiss button if the front most task is invisible + if (task == mStack.getFrontMostTask()) { + hideDismissAllButton(null); + } } } @@ -322,7 +380,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal if (tv == null) { tv = mViewPool.pickUpViewFromPool(task, task); - + if (mLayersDisabled) { + tv.disableLayersForOneFrame(); + } if (mStackViewsAnimationDuration > 0) { // For items in the list, put them in start animating them from the // approriate ends of the list where they are expected to appear @@ -333,6 +393,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } tv.updateViewPropertiesToTaskTransform(mTmpTransform, 0); } + + // If we show the front most task view then ensure that the dismiss button + // is visible too. + if (!mAwaitingFirstLayout && (task == mStack.getFrontMostTask())) { + showDismissAllButton(); + } } // Animate the task into place @@ -341,13 +407,17 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Request accessibility focus on the next view if we removed the task // that previously held accessibility focus - childCount = getChildCount(); - if (childCount > 0 && ssp.isTouchExplorationEnabled()) { - TaskView atv = (TaskView) getChildAt(childCount - 1); - int indexOfTask = mStack.indexOfTask(atv.getTask()); - if (mPrevAccessibilityFocusedIndex != indexOfTask) { - tv.requestAccessibilityFocus(); - mPrevAccessibilityFocusedIndex = indexOfTask; + if (reaquireAccessibilityFocus) { + taskViews = getTaskViews(); + taskViewCount = taskViews.size(); + if (taskViewCount > 0 && ssp.isTouchExplorationEnabled() && + mPrevAccessibilityFocusedIndex != -1) { + TaskView atv = taskViews.get(taskViewCount - 1); + int indexOfTask = mStack.indexOfTask(atv.getTask()); + if (mPrevAccessibilityFocusedIndex != indexOfTask) { + tv.requestAccessibilityFocus(); + mPrevAccessibilityFocusedIndex = indexOfTask; + } } } } @@ -364,44 +434,43 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal /** Updates the clip for each of the task views. */ void clipTaskViews() { // Update the clip on each task child - if (Constants.DebugFlags.App.EnableTaskStackClipping) { - int childCount = getChildCount(); - for (int i = 0; i < childCount - 1; i++) { - TaskView tv = (TaskView) getChildAt(i); - TaskView nextTv = null; - TaskView tmpTv = null; - int clipBottom = 0; - if (tv.shouldClipViewInStack()) { - // Find the next view to clip against - int nextIndex = i; - while (nextIndex < getChildCount()) { - tmpTv = (TaskView) getChildAt(++nextIndex); - if (tmpTv != null && tmpTv.shouldClipViewInStack()) { - nextTv = tmpTv; - break; - } + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount - 1; i++) { + TaskView tv = taskViews.get(i); + TaskView nextTv = null; + TaskView tmpTv = null; + int clipBottom = 0; + if (tv.shouldClipViewInStack()) { + // Find the next view to clip against + int nextIndex = i; + while (nextIndex < (taskViewCount - 1)) { + tmpTv = taskViews.get(++nextIndex); + if (tmpTv != null && tmpTv.shouldClipViewInStack()) { + nextTv = tmpTv; + break; } + } - // Clip against the next view, this is just an approximation since we are - // stacked and we can make assumptions about the visibility of the this - // task relative to the ones in front of it. - if (nextTv != null) { - // Map the top edge of next task view into the local space of the current - // task view to find the clip amount in local space - mTmpCoord[0] = mTmpCoord[1] = 0; - Utilities.mapCoordInDescendentToSelf(nextTv, this, mTmpCoord, false); - Utilities.mapCoordInSelfToDescendent(tv, this, mTmpCoord, mTmpMatrix); - clipBottom = (int) Math.floor(tv.getMeasuredHeight() - mTmpCoord[1] - - nextTv.getPaddingTop() - 1); - } + // Clip against the next view, this is just an approximation since we are + // stacked and we can make assumptions about the visibility of the this + // task relative to the ones in front of it. + if (nextTv != null) { + // Map the top edge of next task view into the local space of the current + // task view to find the clip amount in local space + mTmpCoord[0] = mTmpCoord[1] = 0; + Utilities.mapCoordInDescendentToSelf(nextTv, this, mTmpCoord, false); + Utilities.mapCoordInSelfToDescendent(tv, this, mTmpCoord, mTmpMatrix); + clipBottom = (int) Math.floor(tv.getMeasuredHeight() - mTmpCoord[1] + - nextTv.getPaddingTop() - 1); } - tv.getViewBounds().setClipBottom(clipBottom); - } - if (getChildCount() > 0) { - // The front most task should never be clipped - TaskView tv = (TaskView) getChildAt(getChildCount() - 1); - tv.getViewBounds().setClipBottom(0); } + tv.getViewBounds().setClipBottom(clipBottom); + } + if (taskViewCount > 0) { + // The front most task should never be clipped + TaskView tv = taskViews.get(taskViewCount - 1); + tv.getViewBounds().setClipBottom(0); } mStackViewsClipDirty = false; } @@ -435,25 +504,20 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal if (0 <= taskIndex && taskIndex < mStack.getTaskCount()) { mFocusedTaskIndex = taskIndex; + mPrevAccessibilityFocusedIndex = taskIndex; // Focus the view if possible, otherwise, focus the view after we scroll into position - Task t = mStack.getTasks().get(taskIndex); - TaskView tv = getChildViewForTask(t); - Runnable postScrollRunnable = null; - if (tv != null) { - tv.setFocusedTask(animateFocusedState); - } else { - postScrollRunnable = new Runnable() { - @Override - public void run() { - Task t = mStack.getTasks().get(mFocusedTaskIndex); - TaskView tv = getChildViewForTask(t); - if (tv != null) { - tv.setFocusedTask(animateFocusedState); - } + final Task t = mStack.getTasks().get(mFocusedTaskIndex); + Runnable postScrollRunnable = new Runnable() { + @Override + public void run() { + TaskView tv = getChildViewForTask(t); + if (tv != null) { + tv.setFocusedTask(animateFocusedState); + tv.requestAccessibilityFocus(); } - }; - } + } + }; // Scroll the view into position (just center it in the curve) if (scrollToNewPosition) { @@ -473,24 +537,30 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal * Ensures that there is a task focused, if nothing is focused, then we will use the task * at the center of the visible stack. */ - public boolean ensureFocusedTask() { + public boolean ensureFocusedTask(boolean findClosestToCenter) { if (mFocusedTaskIndex < 0) { - // If there is no task focused, then find the task that is closes to the center - // of the screen and use that as the currently focused task - int x = mLayoutAlgorithm.mStackVisibleRect.centerX(); - int y = mLayoutAlgorithm.mStackVisibleRect.centerY(); - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - TaskView tv = (TaskView) getChildAt(i); - tv.getHitRect(mTmpRect); - if (mTmpRect.contains(x, y)) { - mFocusedTaskIndex = mStack.indexOfTask(tv.getTask()); - break; + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + if (findClosestToCenter) { + // If there is no task focused, then find the task that is closes to the center + // of the screen and use that as the currently focused task + int x = mLayoutAlgorithm.mStackVisibleRect.centerX(); + int y = mLayoutAlgorithm.mStackVisibleRect.centerY(); + for (int i = taskViewCount - 1; i >= 0; i--) { + TaskView tv = taskViews.get(i); + tv.getHitRect(mTmpRect); + if (mTmpRect.contains(x, y)) { + mFocusedTaskIndex = mStack.indexOfTask(tv.getTask()); + mPrevAccessibilityFocusedIndex = mFocusedTaskIndex; + break; + } } } // If we can't find the center task, then use the front most index - if (mFocusedTaskIndex < 0 && childCount > 0) { - mFocusedTaskIndex = childCount - 1; + if (mFocusedTaskIndex < 0 && taskViewCount > 0) { + TaskView tv = taskViews.get(taskViewCount - 1); + mFocusedTaskIndex = mStack.indexOfTask(tv.getTask()); + mPrevAccessibilityFocusedIndex = mFocusedTaskIndex; } } return mFocusedTaskIndex >= 0; @@ -538,15 +608,17 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } } mFocusedTaskIndex = -1; + mPrevAccessibilityFocusedIndex = -1; } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); - int childCount = getChildCount(); - if (childCount > 0) { - TaskView backMostTask = (TaskView) getChildAt(0); - TaskView frontMostTask = (TaskView) getChildAt(childCount - 1); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + if (taskViewCount > 0) { + TaskView backMostTask = taskViews.get(0); + TaskView frontMostTask = taskViews.get(taskViewCount - 1); event.setFromIndex(mStack.indexOfTask(backMostTask.getTask())); event.setToIndex(mStack.indexOfTask(frontMostTask.getTask())); event.setContentDescription(frontMostTask.getTask().activityLabel); @@ -557,6 +629,53 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + if (taskViewCount > 1 && mPrevAccessibilityFocusedIndex != -1) { + info.setScrollable(true); + if (mPrevAccessibilityFocusedIndex > 0) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + } + if (mPrevAccessibilityFocusedIndex < mStack.getTaskCount() - 1) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + } + + @Override + public CharSequence getAccessibilityClassName() { + return TaskStackView.class.getName(); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (super.performAccessibilityAction(action, arguments)) { + return true; + } + if (ensureFocusedTask(false)) { + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { + if (mPrevAccessibilityFocusedIndex > 0) { + focusNextTask(true, false); + return true; + } + } + break; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { + if (mPrevAccessibilityFocusedIndex < mStack.getTaskCount() - 1) { + focusNextTask(false, false); + return true; + } + } + break; + } + } + return false; + } + + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mTouchHandler.onInterceptTouchEvent(ev); } @@ -571,12 +690,18 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal return mTouchHandler.onGenericMotionEvent(ev); } + /** Returns the region that touch gestures can be started in. */ + Rect getTouchableRegion() { + return mTaskStackBounds; + } + @Override public void computeScroll() { mStackScroller.computeScroll(); // Synchronize the views synchronizeStackViewsWithModel(); clipTaskViews(); + updateDismissButtonPosition(); // Notify accessibility sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED); } @@ -633,9 +758,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } // Measure each of the TaskViews - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - TaskView tv = (TaskView) getChildAt(i); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + TaskView tv = taskViews.get(i); if (tv.getBackground() != null) { tv.getBackground().getPadding(mTmpRect); } else { @@ -650,6 +776,14 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal MeasureSpec.EXACTLY)); } + // Measure the dismiss button + if (mDismissAllButton != null) { + int taskRectWidth = mLayoutAlgorithm.mTaskRect.width(); + mDismissAllButton.measure( + MeasureSpec.makeMeasureSpec(taskRectWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mConfig.dismissAllButtonSizePx, MeasureSpec.EXACTLY)); + } + setMeasuredDimension(width, height); } @@ -661,9 +795,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // Layout each of the children - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - TaskView tv = (TaskView) getChildAt(i); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + TaskView tv = taskViews.get(i); if (tv.getBackground() != null) { tv.getBackground().getPadding(mTmpRect); } else { @@ -675,6 +810,15 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mLayoutAlgorithm.mTaskRect.bottom + mTmpRect.bottom); } + // Layout the dismiss button at the top of the screen, and just translate it accordingly + // when synchronizing the views with the model to attach it to the bottom of the front-most + // task view + if (mDismissAllButton != null) { + mDismissAllButton.layout(mLayoutAlgorithm.mTaskRect.left, 0, + mLayoutAlgorithm.mTaskRect.left + mDismissAllButton.getMeasuredWidth(), + mDismissAllButton.getMeasuredHeight()); + } + if (mAwaitingFirstLayout) { mAwaitingFirstLayout = false; onFirstLayout(); @@ -688,9 +832,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Find the launch target task Task launchTargetTask = null; - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - TaskView tv = (TaskView) getChildAt(i); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = taskViewCount - 1; i >= 0; i--) { + TaskView tv = taskViews.get(i); Task task = tv.getTask(); if (task.isLaunchTarget) { launchTargetTask = task; @@ -699,8 +844,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } // Prepare the first view for its enter animation - for (int i = childCount - 1; i >= 0; i--) { - TaskView tv = (TaskView) getChildAt(i); + for (int i = taskViewCount - 1; i >= 0; i--) { + TaskView tv = taskViews.get(i); Task task = tv.getTask(); boolean occludesLaunchTarget = (launchTargetTask != null) && launchTargetTask.group.isTaskAboveTask(task, launchTargetTask); @@ -728,7 +873,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } // Start dozing - mUIDozeTrigger.startDozing(); + if (!mConfig.multiStackEnabled) { + mUIDozeTrigger.startDozing(); + } } /** Requests this task stacks to start it's enter-recents animation */ @@ -743,9 +890,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal if (mStack.getTaskCount() > 0) { // Find the launch target task Task launchTargetTask = null; - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - TaskView tv = (TaskView) getChildAt(i); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = taskViewCount - 1; i >= 0; i--) { + TaskView tv = taskViews.get(i); Task task = tv.getTask(); if (task.isLaunchTarget) { launchTargetTask = task; @@ -754,12 +902,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } // Animate all the task views into view - for (int i = childCount - 1; i >= 0; i--) { - TaskView tv = (TaskView) getChildAt(i); + for (int i = taskViewCount - 1; i >= 0; i--) { + TaskView tv = taskViews.get(i); Task task = tv.getTask(); ctx.currentTaskTransform = new TaskViewTransform(); ctx.currentStackViewIndex = i; - ctx.currentStackViewCount = childCount; + ctx.currentStackViewCount = taskViewCount; ctx.currentTaskRect = mLayoutAlgorithm.mTaskRect; ctx.currentTaskOccludesLaunchTarget = (launchTargetTask != null) && launchTargetTask.group.isTaskAboveTask(task, launchTargetTask); @@ -778,29 +926,35 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); - int childCount = getChildCount(); - if (childCount > 0) { + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + if (taskViewCount > 0) { // Focus the first view if accessibility is enabled if (ssp.isTouchExplorationEnabled()) { - TaskView tv = ((TaskView) getChildAt(childCount - 1)); + TaskView tv = taskViews.get(taskViewCount - 1); tv.requestAccessibilityFocus(); mPrevAccessibilityFocusedIndex = mStack.indexOfTask(tv.getTask()); } } // Start the focus animation when alt-tabbing - if (mConfig.launchedWithAltTab && !mConfig.launchedHasConfigurationChanged) { - View tv = getChildAt(mFocusedTaskIndex); + ArrayList<Task> tasks = mStack.getTasks(); + if (mConfig.launchedWithAltTab && !mConfig.launchedHasConfigurationChanged && + 0 <= mFocusedTaskIndex && mFocusedTaskIndex < tasks.size()) { + TaskView tv = getChildViewForTask(tasks.get(mFocusedTaskIndex)); if (tv != null) { - ((TaskView) tv).setFocusedTask(true); + tv.setFocusedTask(true); } } + + // Show the dismiss button + showDismissAllButton(); } }); } } - /** Requests this task stacks to start it's exit-recents animation. */ + /** Requests this task stack to start it's exit-recents animation. */ public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { // Stop any scrolling mStackScroller.stopScroller(); @@ -808,19 +962,44 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Animate all the task views out of view ctx.offscreenTranslationY = mLayoutAlgorithm.mViewRect.bottom - (mLayoutAlgorithm.mTaskRect.top - mLayoutAlgorithm.mViewRect.top); - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - TaskView tv = (TaskView) getChildAt(i); + // Animate the dismiss-all button + hideDismissAllButton(null); + + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + TaskView tv = taskViews.get(i); tv.startExitToHomeAnimation(ctx); } } + /** Requests this task stack to start it's dismiss-all animation. */ + public void startDismissAllAnimation(final Runnable postAnimationRunnable) { + // Clear the focused task + resetFocusedTask(); + // Animate the dismiss-all button + hideDismissAllButton(new Runnable() { + @Override + public void run() { + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + int count = 0; + for (int i = taskViewCount - 1; i >= 0; i--) { + TaskView tv = taskViews.get(i); + tv.startDeleteTaskAnimation(i > 0 ? null : postAnimationRunnable, count * 50); + count++; + } + } + }); + } + /** Animates a task view in this stack as it launches. */ public void startLaunchTaskAnimation(TaskView tv, Runnable r, boolean lockToTask) { Task launchTargetTask = tv.getTask(); - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - TaskView t = (TaskView) getChildAt(i); + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + TaskView t = taskViews.get(i); if (t == tv) { t.setClipViewInStack(false); t.startLaunchTaskAnimation(r, true, true, lockToTask); @@ -832,6 +1011,69 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } } + /** Shows the dismiss button */ + void showDismissAllButton() { + if (mDismissAllButton == null) return; + + if (mDismissAllButtonAnimating || mDismissAllButton.getVisibility() != View.VISIBLE || + Float.compare(mDismissAllButton.getAlpha(), 0f) == 0) { + mDismissAllButtonAnimating = true; + mDismissAllButton.setVisibility(View.VISIBLE); + mDismissAllButton.showClearButton(); + mDismissAllButton.findViewById(R.id.dismiss_text).setAlpha(1f); + mDismissAllButton.setAlpha(0f); + mDismissAllButton.animate() + .alpha(1f) + .setDuration(250) + .withEndAction(new Runnable() { + @Override + public void run() { + mDismissAllButtonAnimating = false; + } + }) + .start(); + } + } + + /** Hides the dismiss button */ + void hideDismissAllButton(final Runnable postAnimRunnable) { + if (mDismissAllButton == null) return; + + mDismissAllButtonAnimating = true; + mDismissAllButton.animate() + .alpha(0f) + .setDuration(200) + .withEndAction(new Runnable() { + @Override + public void run() { + mDismissAllButtonAnimating = false; + mDismissAllButton.setVisibility(View.GONE); + if (postAnimRunnable != null) { + postAnimRunnable.run(); + } + } + }) + .start(); + } + + /** Updates the dismiss button position */ + void updateDismissButtonPosition() { + if (mDismissAllButton == null) return; + + // Update the position of the clear-all button to hang it off the first task view + if (mStack.getTaskCount() > 0) { + mTmpCoord[0] = mTmpCoord[1] = 0; + TaskView tv = getChildViewForTask(mStack.getFrontMostTask()); + TaskViewTransform transform = mCurrentTaskTransforms.get(mStack.getTaskCount() - 1); + if (tv != null && transform.visible) { + Utilities.mapCoordInDescendentToSelf(tv, this, mTmpCoord, false); + mDismissAllButton.setTranslationY(mTmpCoord[1] + (tv.getScaleY() * tv.getHeight())); + mDismissAllButton.setTranslationX(-(mLayoutAlgorithm.mStackRect.width() - + transform.rect.width()) / 2f); + } + } + } + /** Final callback after Recents is finally hidden. */ void onRecentsHidden() { reset(); @@ -847,6 +1089,20 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mUIDozeTrigger.poke(); } + @Override + protected void dispatchDraw(Canvas canvas) { + mLayersDisabled = false; + super.dispatchDraw(canvas); + } + + public void disableLayersForOneFrame() { + mLayersDisabled = true; + List<TaskView> taskViews = getTaskViews(); + for (int i = 0; i < taskViews.size(); i++) { + taskViews.get(i).disableLayersForOneFrame(); + } + } + /**** TaskStackCallbacks Implementation ****/ @Override @@ -908,12 +1164,30 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal shouldFinishActivity = (mStack.getTaskCount() == 0); } if (shouldFinishActivity) { - mCb.onAllTaskViewsDismissed(); + mCb.onAllTaskViewsDismissed(null); } + } else { + // Fade the dismiss button back in + showDismissAllButton(); } } @Override + public void onStackAllTasksRemoved(TaskStack stack, final ArrayList<Task> removedTasks) { + // Announce for accessibility + String msg = getContext().getString(R.string.accessibility_recents_all_items_dismissed); + announceForAccessibility(msg); + + startDismissAllAnimation(new Runnable() { + @Override + public void run() { + // Notify that all tasks have been removed + mCb.onAllTaskViewsDismissed(removedTasks); + } + }); + } + + @Override public void onStackFiltered(TaskStack newStack, final ArrayList<Task> curTasks, Task filteredTask) { /* @@ -998,6 +1272,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Detach the view from the hierarchy detachViewFromParent(tv); + // Update the task views list after removing the task view + updateTaskViewsList(); // Reset the view properties tv.resetViewProperties(); @@ -1019,7 +1295,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal RecentsTaskLoader.getInstance().loadTaskData(task); // If the doze trigger has already fired, then update the state for this task view - if (mUIDozeTrigger.hasTriggered()) { + if (mConfig.multiStackEnabled || mUIDozeTrigger.hasTriggered()) { tv.setNoUserInteractionState(); } @@ -1032,11 +1308,14 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal int insertIndex = -1; int taskIndex = mStack.indexOfTask(task); if (taskIndex != -1) { - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - Task tvTask = ((TaskView) getChildAt(i)).getTask(); + + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + Task tvTask = taskViews.get(i).getTask(); if (taskIndex < mStack.indexOfTask(tvTask)) { - insertIndex = i; + // Offset by 1 if we have a dismiss-all button + insertIndex = i + (Constants.DebugFlags.App.EnableDismissAll ? 1 : 0); break; } } @@ -1051,6 +1330,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal tv.requestLayout(); } } + // Update the task views list after adding the new task view + updateTaskViewsList(); // Set the new state for this view, including the callbacks and view clipping tv.setCallbacks(this); @@ -1133,6 +1414,13 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } } + @Override + public void onTaskResize(TaskView tv) { + if (mCb != null) { + mCb.onTaskResize(tv.getTask()); + } + } + /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/ @Override @@ -1163,7 +1451,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal public void run() { mStack.removeTask(t); } - }); + }, 0); } else { // Otherwise, remove the task from the stack immediately mStack.removeTask(t); diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewFilterAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewFilterAlgorithm.java index 9cd5ae4..614ca53 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewFilterAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewFilterAlgorithm.java @@ -22,6 +22,7 @@ import com.android.systemui.recents.model.Task; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; /* The layout logic for a TaskStackView */ public class TaskStackViewFilterAlgorithm { @@ -142,9 +143,10 @@ public class TaskStackViewFilterAlgorithm { // the new stack) or to their final positions in the new stack int offset = 0; int movement = 0; - int childCount = mStackView.getChildCount(); - for (int i = 0; i < childCount; i++) { - TaskView tv = (TaskView) mStackView.getChildAt(i); + List<TaskView> taskViews = mStackView.getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + TaskView tv = taskViews.get(i); Task task = tv.getTask(); int taskIndex = tasks.indexOf(task); TaskViewTransform toTransform; diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java index 49b9129..f6df881 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java @@ -17,6 +17,7 @@ package com.android.systemui.recents.views; import android.graphics.Rect; +import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.Task; @@ -131,6 +132,11 @@ public class TaskStackViewLayoutAlgorithm { float pNavBarOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - (mStackVisibleRect.bottom - mStackRect.bottom)); + float pDismissAllButtonOffset = 0f; + if (Constants.DebugFlags.App.EnableDismissAll) { + pDismissAllButtonOffset = pAtBottomOfStackRect - + screenYToCurveProgress(mStackVisibleRect.bottom - mConfig.dismissAllButtonSizePx); + } // Update the task offsets float pAtBackMostCardTop = 0.5f; @@ -148,7 +154,8 @@ public class TaskStackViewLayoutAlgorithm { } } - mMaxScrollP = pAtFrontMostCardTop - ((1f - pTaskHeightOffset - pNavBarOffset)); + mMaxScrollP = pAtFrontMostCardTop + pDismissAllButtonOffset - + ((1f - pTaskHeightOffset - pNavBarOffset)); mMinScrollP = tasks.size() == 1 ? Math.max(mMaxScrollP, 0f) : 0f; if (launchedWithAltTab && launchedFromHome) { // Center the top most task, since that will be focused first diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java index f7067be..fabc86d 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java @@ -96,23 +96,13 @@ public class TaskStackViewScroller { } return false; } - /** Bounds the current scroll if necessary, but does not synchronize the stack view with the model. */ - public boolean boundScrollRaw() { - float curScroll = getStackScroll(); - float newScroll = getBoundedStackScroll(curScroll); - if (Float.compare(newScroll, curScroll) != 0) { - setStackScrollRaw(newScroll); - return true; - } - return false; - } /** Returns the bounded stack scroll */ float getBoundedStackScroll(float scroll) { return Math.max(mLayoutAlgorithm.mMinScrollP, Math.min(mLayoutAlgorithm.mMaxScrollP, scroll)); } - /** Returns the amount that the aboslute value of how much the scroll is out of bounds. */ + /** Returns the amount that the absolute value of how much the scroll is out of bounds. */ float getScrollAmountOutOfBounds(float scroll) { if (scroll < mLayoutAlgorithm.mMinScrollP) { return Math.abs(scroll - mLayoutAlgorithm.mMinScrollP); diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java index 59e38f4..13bdbd2 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java @@ -24,8 +24,11 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; import com.android.systemui.recents.Constants; +import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsConfiguration; +import java.util.List; + /* Handles touch events for a TaskStackView. */ class TaskStackViewTouchHandler implements SwipeHelper.Callback { static int INACTIVE_POINTER_ID = -1; @@ -51,6 +54,8 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { int mScrollTouchSlop; // The page touch slop is used to calculate when we start swiping float mPagingTouchSlop; + // Used to calculate when a tap is outside a task view rectangle. + final int mWindowTouchSlop; SwipeHelper mSwipeHelper; boolean mInterceptedBySwipeHelper; @@ -62,6 +67,7 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mScrollTouchSlop = configuration.getScaledTouchSlop(); mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); + mWindowTouchSlop = configuration.getScaledWindowTouchSlop(); mSv = sv; mScroller = scroller; mConfig = config; @@ -93,9 +99,10 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { /** 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); + List<TaskView> taskViews = mSv.getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = taskViewCount - 1; i >= 0; i--) { + TaskView tv = taskViews.get(i); if (tv.getVisibility() == View.VISIBLE) { if (mSv.isTransformedTouchPointInView(x, y, tv)) { return tv; @@ -115,11 +122,21 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { /** Touch preprocessing for handling below */ public boolean onInterceptTouchEvent(MotionEvent ev) { // Return early if we have no children - boolean hasChildren = (mSv.getChildCount() > 0); - if (!hasChildren) { + boolean hasTaskViews = (mSv.getTaskViews().size() > 0); + if (!hasTaskViews) { return false; } + int action = ev.getAction(); + if (mConfig.multiStackEnabled) { + // Check if we are within the bounds of the stack view contents + if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { + if (!mSv.getTouchableRegion().contains((int) ev.getX(), (int) ev.getY())) { + return false; + } + } + } + // Pass through to swipe helper if we are swiping mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev); if (mInterceptedBySwipeHelper) { @@ -128,7 +145,6 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { boolean wasScrolling = mScroller.isScrolling() || (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning()); - int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { // Save the touch down info @@ -190,11 +206,21 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { /** Handles touch events once we have intercepted them */ public boolean onTouchEvent(MotionEvent ev) { // Short circuit if we have no children - boolean hasChildren = (mSv.getChildCount() > 0); - if (!hasChildren) { + boolean hasTaskViews = (mSv.getTaskViews().size() > 0); + if (!hasTaskViews) { return false; } + int action = ev.getAction(); + if (mConfig.multiStackEnabled) { + // Check if we are within the bounds of the stack view contents + if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { + if (!mSv.getTouchableRegion().contains((int) ev.getX(), (int) ev.getY())) { + return false; + } + } + } + // Pass through to swipe helper if we are swiping if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { return true; @@ -203,7 +229,6 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { // Update the velocity tracker initVelocityTrackerIfNotExists(); - int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { // Save the touch down info @@ -279,7 +304,7 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity); int overscrollRange = (int) (Math.min(1f, overscrollRangePct) * (Constants.Values.TaskStackView.TaskStackMaxOverscrollRange - - Constants.Values.TaskStackView.TaskStackMinOverscrollRange)); + Constants.Values.TaskStackView.TaskStackMinOverscrollRange)); mScroller.mScroller.fling(0, mScroller.progressToScrollRange(mScroller.getStackScroll()), 0, velocity, @@ -293,6 +318,9 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { } else if (mScroller.isScrollOutOfBounds()) { // Animate the scroll back into bounds mScroller.animateBoundScroll(); + } else if (mActiveTaskView == null) { + // This tap didn't start on a task. + maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY()); } mActivePointerId = INACTIVE_POINTER_ID; @@ -330,6 +358,34 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { return true; } + /** Hides recents if the up event at (x, y) is a tap on the background area. */ + void maybeHideRecentsFromBackgroundTap(int x, int y) { + // Ignore the up event if it's too far from its start position. The user might have been + // trying to scroll or swipe. + int dx = Math.abs(mInitialMotionX - x); + int dy = Math.abs(mInitialMotionY - y); + if (dx > mScrollTouchSlop || dy > mScrollTouchSlop) { + return; + } + + // Shift the tap position toward the center of the task stack and check to see if it would + // have hit a view. The user might have tried to tap on a task and missed slightly. + int shiftedX = x; + if (x > mSv.getTouchableRegion().centerX()) { + shiftedX -= mWindowTouchSlop; + } else { + shiftedX += mWindowTouchSlop; + } + if (findViewAtPoint(shiftedX, y) != null) { + return; + } + + // The user intentionally tapped on the background, which is like a tap on the "desktop". + // Hide recents and transition to the launcher. + Recents recents = Recents.getInstanceAndStartIfNeeded(mSv.getContext()); + recents.hideRecents(false /* altTab */, true /* homeKey */); + } + /** Handles generic motion events */ public boolean onGenericMotionEvent(MotionEvent ev) { if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == @@ -340,11 +396,11 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { // Find the front most task and scroll the next task to the front float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); if (vScroll > 0) { - if (mSv.ensureFocusedTask()) { + if (mSv.ensureFocusedTask(true)) { mSv.focusNextTask(true, false); } } else { - if (mSv.ensureFocusedTask()) { + if (mSv.ensureFocusedTask(true)) { mSv.focusNextTask(false, false); } } @@ -378,6 +434,8 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } + // Fade out the dismiss button + mSv.hideDismissAllButton(null); } @Override @@ -403,6 +461,8 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { tv.setClipViewInStack(true); // Re-enable touch events from this task view tv.setTouchEnabled(true); + // Restore the dismiss button + mSv.showDismissAllButton(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java index faa728d..5906ef1 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -22,6 +22,7 @@ import android.animation.ValueAnimator; import android.content.Context; import android.graphics.*; import android.util.AttributeSet; +import android.view.accessibility.AccessibilityManager; import android.view.View; import android.view.ViewOutlineProvider; import android.view.animation.AccelerateInterpolator; @@ -45,6 +46,8 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, public void onTaskViewDismissed(TaskView tv); public void onTaskViewClipStateChanged(TaskView tv); public void onTaskViewFocusChanged(TaskView tv, boolean focused); + + public void onTaskResize(TaskView tv); } RecentsConfiguration mConfig; @@ -364,7 +367,6 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, .setStartDelay(delay) .setDuration(duration) .setInterpolator(PhoneStatusBar.ALPHA_IN) - .withLayer() .start(); } @@ -381,6 +383,11 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, ctx.postAnimationTrigger.increment(); } + /** Animates this task view away when dismissing all tasks. */ + void startDismissAllAnimation() { + dismissTask(); + } + /** Animates this task view as it exits recents */ void startLaunchTaskAnimation(final Runnable postAnimRunnable, boolean isLaunchingTask, boolean occludesLaunchTarget, boolean lockToTask) { @@ -408,7 +415,6 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, .setStartDelay(0) .setDuration(mConfig.taskViewExitToAppDuration) .setInterpolator(mConfig.fastOutLinearInInterpolator) - .withLayer() .start(); } else { // Hide the dismiss button @@ -428,25 +434,22 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, } /** Animates the deletion of this task view */ - void startDeleteTaskAnimation(final Runnable r) { + void startDeleteTaskAnimation(final Runnable r, int delay) { // Disabling clipping with the stack while the view is animating away setClipViewInStack(false); animate().translationX(mConfig.taskViewRemoveAnimTranslationXPx) .alpha(0f) - .setStartDelay(0) + .setStartDelay(delay) .setUpdateListener(null) .setInterpolator(mConfig.fastOutSlowInInterpolator) .setDuration(mConfig.taskViewRemoveAnimDuration) .withEndAction(new Runnable() { @Override public void run() { - // We just throw this into a runnable because starting a view property - // animation using layers can cause inconsisten results if we try and - // update the layers while the animation is running. In some cases, - // the runnabled passed in may start an animation which also uses layers - // so we defer all this by posting this. - r.run(); + if (r != null) { + r.run(); + } // Re-enable clipping with the stack (we will reuse this view) setClipViewInStack(true); @@ -455,6 +458,11 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, .start(); } + /** Enables/disables handling touch on this task view. */ + void setTouchEnabled(boolean enabled) { + setOnClickListener(enabled ? this : null); + } + /** Animates this task view if the user does not interact with the stack after a certain time. */ void startNoUserInteractionAnimation() { mHeaderView.startNoUserInteractionAnimation(); @@ -481,7 +489,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mCb.onTaskViewDismissed(tv); } } - }); + }, 0); } /** @@ -641,6 +649,10 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, } } + public void disableLayersForOneFrame() { + mHeaderView.disableLayersForOneFrame(); + } + /**** TaskCallbacks Implementation ****/ /** Binds this task view to the task */ @@ -663,14 +675,17 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mThumbnailView.rebindToTask(mTask); mHeaderView.rebindToTask(mTask); // Rebind any listeners - mHeaderView.mApplicationIcon.setOnClickListener(this); + AccessibilityManager am = (AccessibilityManager) getContext(). + getSystemService(Context.ACCESSIBILITY_SERVICE); + if (Constants.DebugFlags.App.EnableTaskFiltering || (am != null && am.isEnabled())) { + mHeaderView.mApplicationIcon.setOnClickListener(this); + } mHeaderView.mDismissButton.setOnClickListener(this); - mActionButtonView.setOnClickListener(this); - if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { - if (mConfig.developerOptionsEnabled) { - mHeaderView.mApplicationIcon.setOnLongClickListener(this); - } + if (mConfig.multiStackEnabled) { + mHeaderView.mMoveTaskButton.setOnClickListener(this); } + mActionButtonView.setOnClickListener(this); + mHeaderView.mApplicationIcon.setOnLongClickListener(this); } mTaskDataLoaded = true; } @@ -685,17 +700,18 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, // Unbind any listeners mHeaderView.mApplicationIcon.setOnClickListener(null); mHeaderView.mDismissButton.setOnClickListener(null); - mActionButtonView.setOnClickListener(null); - if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { - mHeaderView.mApplicationIcon.setOnLongClickListener(null); + if (mConfig.multiStackEnabled) { + mHeaderView.mMoveTaskButton.setOnClickListener(null); } + mActionButtonView.setOnClickListener(null); + mHeaderView.mApplicationIcon.setOnLongClickListener(null); } mTaskDataLoaded = false; } - /** Enables/disables handling touch on this task view. */ - void setTouchEnabled(boolean enabled) { - setOnClickListener(enabled ? this : null); + @Override + public void onMultiStackDebugTaskStackIdChanged() { + mHeaderView.rebindToTask(mTask); } /**** View.OnClickListener Implementation ****/ @@ -709,12 +725,26 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, postDelayed(new Runnable() { @Override public void run() { - if (Constants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) { - if (mCb != null) { - mCb.onTaskViewAppIconClicked(tv); + if (v == mHeaderView.mApplicationIcon) { + if (Constants.DebugFlags.App.EnableTaskFiltering) { + if (mCb != null) { + mCb.onTaskViewAppIconClicked(tv); + } + } else { + AccessibilityManager am = (AccessibilityManager) getContext(). + getSystemService(Context.ACCESSIBILITY_SERVICE); + if (am != null && am.isEnabled()) { + if (mCb != null) { + mCb.onTaskViewAppInfoClicked(tv); + } + } } } else if (v == mHeaderView.mDismissButton) { dismissTask(); + } else if (v == mHeaderView.mMoveTaskButton) { + if (mCb != null) { + mCb.onTaskResize(tv); + } } } }, 125); diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java index 05f6f40..6db4020 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java @@ -24,20 +24,19 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.ColorStateList; -import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.RippleDrawable; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffXfermode; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.RippleDrawable; +import android.graphics.Rect; import android.util.AttributeSet; -import android.view.MotionEvent; import android.view.View; import android.view.ViewOutlineProvider; import android.widget.FrameLayout; @@ -46,7 +45,9 @@ import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.Utilities; +import com.android.systemui.recents.model.RecentsTaskLoader; import com.android.systemui.recents.model.Task; @@ -54,8 +55,10 @@ import com.android.systemui.recents.model.Task; public class TaskViewHeader extends FrameLayout { RecentsConfiguration mConfig; + private SystemServicesProxy mSsp; // Header views + ImageView mMoveTaskButton; ImageView mDismissButton; ImageView mApplicationIcon; TextView mActivityDescription; @@ -78,6 +81,8 @@ public class TaskViewHeader extends FrameLayout { Paint mDimLayerPaint = new Paint(); PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); + boolean mLayersDisabled; + public TaskViewHeader(Context context) { this(context, null); } @@ -93,6 +98,7 @@ public class TaskViewHeader extends FrameLayout { public TaskViewHeader(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mConfig = RecentsConfiguration.getInstance(); + mSsp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); setWillNotDraw(false); setClipToOutline(true); setOutlineProvider(new ViewOutlineProvider() { @@ -103,11 +109,10 @@ public class TaskViewHeader extends FrameLayout { }); // Load the dismiss resources - Resources res = context.getResources(); - mLightDismissDrawable = res.getDrawable(R.drawable.recents_dismiss_light); - mDarkDismissDrawable = res.getDrawable(R.drawable.recents_dismiss_dark); + mLightDismissDrawable = context.getDrawable(R.drawable.recents_dismiss_light); + mDarkDismissDrawable = context.getDrawable(R.drawable.recents_dismiss_dark); mDismissContentDescription = - res.getString(R.string.accessibility_recents_item_will_be_dismissed); + context.getString(R.string.accessibility_recents_item_will_be_dismissed); // Configure the highlight paint if (sHighlightPaint == null) { @@ -121,19 +126,12 @@ public class TaskViewHeader extends FrameLayout { } @Override - public boolean onTouchEvent(MotionEvent event) { - // We ignore taps on the task bar except on the filter and dismiss buttons - if (!Constants.DebugFlags.App.EnableTaskBarTouchEvents) return true; - - return super.onTouchEvent(event); - } - - @Override protected void onFinishInflate() { // Initialize the icon and description views mApplicationIcon = (ImageView) findViewById(R.id.application_icon); mActivityDescription = (TextView) findViewById(R.id.activity_description); mDismissButton = (ImageView) findViewById(R.id.dismiss_task); + mMoveTaskButton = (ImageView) findViewById(R.id.move_task); // Hide the backgrounds if they are ripple drawables if (!Constants.DebugFlags.App.EnableTaskFiltering) { @@ -177,7 +175,9 @@ public class TaskViewHeader extends FrameLayout { void setDimAlpha(int alpha) { mDimColorFilter.setColor(Color.argb(alpha, 0, 0, 0)); mDimLayerPaint.setColorFilter(mDimColorFilter); - setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); + if (!mLayersDisabled) { + setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); + } } /** Returns the secondary color for a primary color. */ @@ -195,10 +195,11 @@ public class TaskViewHeader extends FrameLayout { } else if (t.applicationIcon != null) { mApplicationIcon.setImageDrawable(t.applicationIcon); } - mApplicationIcon.setContentDescription(t.activityLabel); if (!mActivityDescription.getText().toString().equals(t.activityLabel)) { mActivityDescription.setText(t.activityLabel); } + mActivityDescription.setContentDescription(t.contentDescription); + // Try and apply the system ui tint int existingBgColor = (getBackground() instanceof ColorDrawable) ? ((ColorDrawable) getBackground()).getColor() : 0; @@ -213,7 +214,44 @@ public class TaskViewHeader extends FrameLayout { mDismissButton.setImageDrawable(t.useLightOnPrimaryColor ? mLightDismissDrawable : mDarkDismissDrawable); mDismissButton.setContentDescription(String.format(mDismissContentDescription, - t.activityLabel)); + t.contentDescription)); + mMoveTaskButton.setVisibility((mConfig.multiStackEnabled) ? View.VISIBLE : View.INVISIBLE); + if (mConfig.multiStackEnabled) { + updateResizeTaskBarIcon(t); + } + } + + /** Updates the resize task bar button. */ + void updateResizeTaskBarIcon(Task t) { + Rect display = mSsp.getWindowRect(); + Rect taskRect = mSsp.getTaskBounds(t.key.stackId); + int resId = R.drawable.star; + if (display.equals(taskRect) || taskRect.isEmpty()) { + resId = R.drawable.vector_drawable_place_fullscreen; + } else { + boolean top = display.top == taskRect.top; + boolean bottom = display.bottom == taskRect.bottom; + boolean left = display.left == taskRect.left; + boolean right = display.right == taskRect.right; + if (top && bottom && left) { + resId = R.drawable.vector_drawable_place_left; + } else if (top && bottom && right) { + resId = R.drawable.vector_drawable_place_right; + } else if (top && left && right) { + resId = R.drawable.vector_drawable_place_top; + } else if (bottom && left && right) { + resId = R.drawable.vector_drawable_place_bottom; + } else if (top && right) { + resId = R.drawable.vector_drawable_place_top_right; + } else if (top && left) { + resId = R.drawable.vector_drawable_place_top_left; + } else if (bottom && right) { + resId = R.drawable.vector_drawable_place_bottom_right; + } else if (bottom && left) { + resId = R.drawable.vector_drawable_place_bottom_left; + } + } + mMoveTaskButton.setImageResource(resId); } /** Unbinds the bar view from the task */ @@ -230,7 +268,6 @@ public class TaskViewHeader extends FrameLayout { .setStartDelay(0) .setInterpolator(mConfig.fastOutSlowInInterpolator) .setDuration(mConfig.taskViewExitToAppDuration) - .withLayer() .start(); } } @@ -245,7 +282,6 @@ public class TaskViewHeader extends FrameLayout { .setStartDelay(0) .setInterpolator(mConfig.fastOutLinearInInterpolator) .setDuration(mConfig.taskViewEnterFromAppDuration) - .withLayer() .start(); } } @@ -272,6 +308,28 @@ public class TaskViewHeader extends FrameLayout { return new int[] {}; } + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (mLayersDisabled) { + mLayersDisabled = false; + postOnAnimation(new Runnable() { + @Override + public void run() { + mLayersDisabled = false; + setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); + } + }); + } + } + + public void disableLayersForOneFrame() { + mLayersDisabled = true; + + // Disable layer for a frame so we can draw our first frame faster. + setLayerType(LAYER_TYPE_NONE, null); + } + /** Notifies the associated TaskView has been focused. */ void onTaskViewFocusChanged(boolean focused, boolean animateFocusedState) { // If we are not animating the visible state, just return @@ -284,23 +342,26 @@ public class TaskViewHeader extends FrameLayout { } if (focused) { + int currentColor = mBackgroundColor; int secondaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); int[][] states = new int[][] { + new int[] {}, new int[] { android.R.attr.state_enabled }, new int[] { android.R.attr.state_pressed } }; int[] newStates = new int[]{ + 0, android.R.attr.state_enabled, android.R.attr.state_pressed }; int[] colors = new int[] { + currentColor, secondaryColor, secondaryColor }; mBackground.setColor(new ColorStateList(states, colors)); mBackground.setState(newStates); // Pulse the background color - int currentColor = mBackgroundColor; int lightPrimaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(), currentColor, lightPrimaryColor); @@ -327,7 +388,7 @@ public class TaskViewHeader extends FrameLayout { mFocusAnimator = new AnimatorSet(); mFocusAnimator.playTogether(backgroundColor, translation); - mFocusAnimator.setStartDelay(750); + mFocusAnimator.setStartDelay(150); mFocusAnimator.setDuration(750); mFocusAnimator.start(); } else { diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java index 42c0f9f..a55e026 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java @@ -21,7 +21,6 @@ import android.graphics.Rect; import android.view.View; import android.view.ViewPropertyAnimator; import android.view.animation.Interpolator; -import com.android.systemui.recents.Constants; /* The transform state for a task view */ @@ -133,6 +132,8 @@ public class TaskViewTransform { /** Reset the transform on a view. */ public static void reset(View v) { + // Cancel any running animations + v.animate().cancel(); v.setTranslationX(0f); v.setTranslationY(0f); v.setTranslationZ(0f); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index d9fea47..9e3cf37 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -25,6 +25,7 @@ import android.app.Notification; import android.app.Notification.BigPictureStyle; import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -59,6 +60,7 @@ import android.widget.ImageView; import com.android.systemui.R; import java.io.File; +import java.io.FileOutputStream; import java.io.OutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -122,7 +124,7 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi // Prepare all the output metadata mImageTime = System.currentTimeMillis(); - String imageDate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date(mImageTime)); + String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime)); mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate); mScreenshotDir = new File(Environment.getExternalStoragePublicDirectory( @@ -136,21 +138,31 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi int previewWidth = data.previewWidth; int previewHeight = data.previewheight; - final int shortSide = mImageWidth < mImageHeight ? mImageWidth : mImageHeight; - Bitmap preview = Bitmap.createBitmap(previewWidth, previewHeight, data.image.getConfig()); - Canvas c = new Canvas(preview); + Canvas c = new Canvas(); Paint paint = new Paint(); ColorMatrix desat = new ColorMatrix(); desat.setSaturation(0.25f); paint.setColorFilter(new ColorMatrixColorFilter(desat)); Matrix matrix = new Matrix(); - matrix.postTranslate((previewWidth - mImageWidth) / 2, - (previewHeight - mImageHeight) / 2); + int overlayColor = 0x40FFFFFF; + + Bitmap picture = Bitmap.createBitmap(previewWidth, previewHeight, data.image.getConfig()); + matrix.setTranslate((previewWidth - mImageWidth) / 2, (previewHeight - mImageHeight) / 2); + c.setBitmap(picture); c.drawBitmap(data.image, matrix, paint); - c.drawColor(0x40FFFFFF); + c.drawColor(overlayColor); c.setBitmap(null); - Bitmap croppedIcon = Bitmap.createScaledBitmap(preview, iconSize, iconSize, true); + // Note, we can't use the preview for the small icon, since it is non-square + float scale = (float) iconSize / Math.min(mImageWidth, mImageHeight); + Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, data.image.getConfig()); + matrix.setScale(scale, scale); + matrix.postTranslate((iconSize - (scale * mImageWidth)) / 2, + (iconSize - (scale * mImageHeight)) / 2); + c.setBitmap(icon); + c.drawBitmap(data.image, matrix, paint); + c.drawColor(overlayColor); + c.setBitmap(null); // Show the intermediate notification mTickerAddSpace = !mTickerAddSpace; @@ -168,7 +180,7 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi .setColor(r.getColor(com.android.internal.R.color.system_notification_accent_color)); mNotificationStyle = new Notification.BigPictureStyle() - .bigPicture(preview); + .bigPicture(picture.createAshmemBitmap()); mNotificationBuilder.setStyle(mNotificationStyle); // For "public" situations we want to show all the same info but @@ -191,9 +203,9 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi // On the tablet, the large icon makes the notification appear as if it is clickable (and // on small devices, the large icon is not shown) so defer showing the large icon until // we compose the final post-save notification below. - mNotificationBuilder.setLargeIcon(croppedIcon); + mNotificationBuilder.setLargeIcon(icon.createAshmemBitmap()); // But we still don't set it for the expanded view, allowing the smallIcon to show here. - mNotificationStyle.bigLargeIcon(null); + mNotificationStyle.bigLargeIcon((Bitmap) null); } @Override @@ -221,6 +233,12 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi // for DATE_TAKEN long dateSeconds = mImageTime / 1000; + // Save + OutputStream out = new FileOutputStream(mImageFilePath); + image.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + // Save the screenshot to the MediaStore ContentValues values = new ContentValues(); ContentResolver resolver = context.getContentResolver(); @@ -233,8 +251,10 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png"); values.put(MediaStore.Images.ImageColumns.WIDTH, mImageWidth); values.put(MediaStore.Images.ImageColumns.HEIGHT, mImageHeight); + values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length()); Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + // Create a share intent String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); Intent sharingIntent = new Intent(Intent.ACTION_SEND); @@ -242,24 +262,28 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi sharingIntent.putExtra(Intent.EXTRA_STREAM, uri); sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject); - Intent chooserIntent = Intent.createChooser(sharingIntent, null); + // Create a share action for the notification + final PendingIntent callback = PendingIntent.getBroadcast(context, 0, + new Intent(context, GlobalScreenshot.TargetChosenReceiver.class) + .putExtra(GlobalScreenshot.CANCEL_ID, mNotificationId), + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); + Intent chooserIntent = Intent.createChooser(sharingIntent, null, + callback.getIntentSender()); chooserIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - - mNotificationBuilder.addAction(R.drawable.ic_menu_share, - r.getString(com.android.internal.R.string.share), - PendingIntent.getActivity(context, 0, chooserIntent, - PendingIntent.FLAG_CANCEL_CURRENT)); - - OutputStream out = resolver.openOutputStream(uri); - image.compress(Bitmap.CompressFormat.PNG, 100, out); - out.flush(); - out.close(); - - // update file size in the database - values.clear(); - values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length()); - resolver.update(uri, values, null, null); + mNotificationBuilder.addAction(R.drawable.ic_screenshot_share, + r.getString(com.android.internal.R.string.share), + PendingIntent.getActivity(context, 0, chooserIntent, + PendingIntent.FLAG_CANCEL_CURRENT)); + + // Create a delete action for the notification + final PendingIntent deleteAction = PendingIntent.getBroadcast(context, 0, + new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class) + .putExtra(GlobalScreenshot.CANCEL_ID, mNotificationId) + .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()), + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); + mNotificationBuilder.addAction(R.drawable.ic_screenshot_delete, + r.getString(com.android.internal.R.string.delete), deleteAction); params[0].imageUri = uri; params[0].image = null; @@ -333,6 +357,29 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi } /** + * An AsyncTask that deletes an image from the media store in the background. + */ +class DeleteImageInBackgroundTask extends AsyncTask<Uri, Void, Void> { + private static final String TAG = "DeleteImageInBackgroundTask"; + + private Context mContext; + + DeleteImageInBackgroundTask(Context context) { + mContext = context; + } + + @Override + protected Void doInBackground(Uri... params) { + if (params.length != 1) return null; + + Uri screenshotUri = params[0]; + ContentResolver resolver = mContext.getContentResolver(); + resolver.delete(screenshotUri, null, null); + return null; + } +} + +/** * TODO: * - Performance when over gl surfaces? Ie. Gallery * - what do we say in the Toast? Which icon do we get if the user uses another @@ -341,7 +388,9 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi class GlobalScreenshot { private static final String TAG = "GlobalScreenshot"; - private static final int SCREENSHOT_NOTIFICATION_ID = 789; + static final String CANCEL_ID = "android:cancel_id"; + static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; + private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130; private static final int SCREENSHOT_DROP_IN_DURATION = 430; private static final int SCREENSHOT_DROP_OUT_DELAY = 500; @@ -464,7 +513,7 @@ class GlobalScreenshot { mSaveInBgTask.cancel(false); } mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager, - SCREENSHOT_NOTIFICATION_ID).execute(data); + R.id.notification_screenshot).execute(data); } /** @@ -725,12 +774,52 @@ class GlobalScreenshot { .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen .setCategory(Notification.CATEGORY_ERROR) .setAutoCancel(true) - .setColor(context.getResources().getColor( + .setColor(context.getColor( com.android.internal.R.color.system_notification_accent_color)); Notification n = new Notification.BigTextStyle(b) .bigText(r.getString(R.string.screenshot_failed_text)) .build(); - nManager.notify(SCREENSHOT_NOTIFICATION_ID, n); + nManager.notify(R.id.notification_screenshot, n); + } + + /** + * Removes the notification for a screenshot after a share target is chosen. + */ + public static class TargetChosenReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.hasExtra(CANCEL_ID)) { + return; + } + + // Clear the notification + final NotificationManager nm = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + final int id = intent.getIntExtra(CANCEL_ID, 0); + nm.cancel(id); + } + } + + /** + * Removes the last screenshot. + */ + public static class DeleteScreenshotReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.hasExtra(CANCEL_ID) || !intent.hasExtra(SCREENSHOT_URI_ID)) { + return; + } + + // Clear the notification + final NotificationManager nm = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + final int id = intent.getIntExtra(CANCEL_ID, 0); + final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID)); + nm.cancel(id); + + // And delete the image from the media store + new DeleteImageInBackgroundTask(context).execute(uri); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java index 9fbcd7f..77c27fa 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java @@ -30,6 +30,8 @@ import android.os.UserHandle; import android.provider.Settings; import android.widget.ImageView; +import com.android.internal.logging.MetricsLogger; + import java.util.ArrayList; public class BrightnessController implements ToggleSlider.Listener { @@ -40,7 +42,7 @@ public class BrightnessController implements ToggleSlider.Listener { * {@link android.provider.Settings.System#SCREEN_AUTO_BRIGHTNESS_ADJ} uses the range [-1, 1]. * Using this factor, it is converted to [0, BRIGHTNESS_ADJ_RESOLUTION] for the SeekBar. */ - private static final float BRIGHTNESS_ADJ_RESOLUTION = 100; + private static final float BRIGHTNESS_ADJ_RESOLUTION = 2048; private final int mMinimumBacklight; private final int mMaximumBacklight; @@ -195,12 +197,16 @@ public class BrightnessController implements ToggleSlider.Listener { } @Override - public void onChanged(ToggleSlider view, boolean tracking, boolean automatic, int value) { + public void onChanged(ToggleSlider view, boolean tracking, boolean automatic, int value, + boolean stopTracking) { updateIcon(mAutomatic); if (mExternalChange) return; if (!mAutomatic) { final int val = value + mMinimumBacklight; + if (stopTracking) { + MetricsLogger.action(mContext, MetricsLogger.ACTION_BRIGHTNESS, val); + } setBrightness(val); if (!tracking) { AsyncTask.execute(new Runnable() { @@ -213,6 +219,9 @@ public class BrightnessController implements ToggleSlider.Listener { } } else { final float adj = value / (BRIGHTNESS_ADJ_RESOLUTION / 2f) - 1; + if (stopTracking) { + MetricsLogger.action(mContext, MetricsLogger.ACTION_BRIGHTNESS_AUTO, value); + } setBrightnessAdj(adj); if (!tracking) { AsyncTask.execute(new Runnable() { diff --git a/packages/SystemUI/src/com/android/systemui/settings/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/BrightnessDialog.java index a1704ff..cef4d34 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/BrightnessDialog.java @@ -17,17 +17,14 @@ package com.android.systemui.settings; import android.app.Activity; -import android.app.Dialog; -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; +import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; /** A dialog that provides controls for adjusting the screen brightness. */ @@ -56,11 +53,13 @@ public class BrightnessDialog extends Activity { protected void onStart() { super.onStart(); mBrightnessController.registerCallbacks(); + MetricsLogger.visible(this, MetricsLogger.BRIGHTNESS_DIALOG); } @Override protected void onStop() { super.onStop(); + MetricsLogger.hidden(this, MetricsLogger.BRIGHTNESS_DIALOG); mBrightnessController.unregisterCallbacks(); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/ToggleSeekBar.java b/packages/SystemUI/src/com/android/systemui/settings/ToggleSeekBar.java index a0a5561..8829794 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/ToggleSeekBar.java +++ b/packages/SystemUI/src/com/android/systemui/settings/ToggleSeekBar.java @@ -19,9 +19,13 @@ package com.android.systemui.settings; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.SeekBar; public class ToggleSeekBar extends SeekBar { + private String mAccessibilityLabel; + public ToggleSeekBar(Context context) { super(context); } @@ -42,4 +46,16 @@ public class ToggleSeekBar extends SeekBar { return super.onTouchEvent(event); } + + public void setAccessibilityLabel(String label) { + mAccessibilityLabel = label; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if (mAccessibilityLabel != null) { + info.setText(mAccessibilityLabel); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/ToggleSlider.java b/packages/SystemUI/src/com/android/systemui/settings/ToggleSlider.java index 35b574b..b263707 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/ToggleSlider.java +++ b/packages/SystemUI/src/com/android/systemui/settings/ToggleSlider.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; @@ -34,14 +35,15 @@ import com.android.systemui.statusbar.policy.BrightnessMirrorController; public class ToggleSlider extends RelativeLayout { public interface Listener { public void onInit(ToggleSlider v); - public void onChanged(ToggleSlider v, boolean tracking, boolean checked, int value); + public void onChanged(ToggleSlider v, boolean tracking, boolean checked, int value, + boolean stopTracking); } private Listener mListener; private boolean mTracking; private CompoundButton mToggle; - private SeekBar mSlider; + private ToggleSeekBar mSlider; private TextView mLabel; private ToggleSlider mMirror; @@ -67,12 +69,14 @@ public class ToggleSlider extends RelativeLayout { mToggle = (CompoundButton) findViewById(R.id.toggle); mToggle.setOnCheckedChangeListener(mCheckListener); - mSlider = (SeekBar) findViewById(R.id.slider); + mSlider = (ToggleSeekBar) findViewById(R.id.slider); mSlider.setOnSeekBarChangeListener(mSeekListener); mLabel = (TextView) findViewById(R.id.label); mLabel.setText(a.getString(R.styleable.ToggleSlider_text)); + mSlider.setAccessibilityLabel(getContentDescription().toString()); + a.recycle(); } @@ -123,6 +127,16 @@ public class ToggleSlider extends RelativeLayout { } } + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (mMirror != null) { + MotionEvent copy = ev.copy(); + mMirror.dispatchTouchEvent(copy); + copy.recycle(); + } + return super.dispatchTouchEvent(ev); + } + private final OnCheckedChangeListener mCheckListener = new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton toggle, boolean checked) { @@ -130,7 +144,7 @@ public class ToggleSlider extends RelativeLayout { if (mListener != null) { mListener.onChanged( - ToggleSlider.this, mTracking, checked, mSlider.getProgress()); + ToggleSlider.this, mTracking, checked, mSlider.getProgress(), false); } if (mMirror != null) { @@ -144,11 +158,7 @@ public class ToggleSlider extends RelativeLayout { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (mListener != null) { mListener.onChanged( - ToggleSlider.this, mTracking, mToggle.isChecked(), progress); - } - - if (mMirror != null) { - mMirror.setValue(progress); + ToggleSlider.this, mTracking, mToggle.isChecked(), progress, false); } } @@ -157,16 +167,12 @@ public class ToggleSlider extends RelativeLayout { mTracking = true; if (mListener != null) { - mListener.onChanged( - ToggleSlider.this, mTracking, mToggle.isChecked(), mSlider.getProgress()); + mListener.onChanged(ToggleSlider.this, mTracking, mToggle.isChecked(), + mSlider.getProgress(), false); } mToggle.setChecked(false); - if (mMirror != null) { - mMirror.mSlider.setPressed(true); - } - if (mMirrorController != null) { mMirrorController.showMirror(); mMirrorController.setLocation((View) getParent()); @@ -178,12 +184,8 @@ public class ToggleSlider extends RelativeLayout { mTracking = false; if (mListener != null) { - mListener.onChanged( - ToggleSlider.this, mTracking, mToggle.isChecked(), mSlider.getProgress()); - } - - if (mMirror != null) { - mMirror.mSlider.setPressed(false); + mListener.onChanged(ToggleSlider.this, mTracking, mToggle.isChecked(), + mSlider.getProgress(), true); } if (mMirrorController != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java index 465a141..7cde44c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java @@ -21,15 +21,8 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapShader; import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.graphics.RectF; -import android.graphics.Shader; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -94,13 +87,12 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView = new PathInterpolator(0, 0, 0.5f, 1); private final int mTintedRippleColor; private final int mLowPriorityRippleColor; - private final int mNormalRippleColor; + protected final int mNormalRippleColor; private boolean mDimmed; private boolean mDark; private int mBgTint = 0; - private final int mRoundedRectCornerRadius; /** * Flag to indicate that the notification has been touched once and the second touch will @@ -115,7 +107,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private OnActivatedListener mOnActivatedListener; private final Interpolator mLinearOutSlowInInterpolator; - private final Interpolator mFastOutSlowInInterpolator; + protected final Interpolator mFastOutSlowInInterpolator; private final Interpolator mSlowOutFastInInterpolator; private final Interpolator mSlowOutLinearInInterpolator; private final Interpolator mLinearInterpolator; @@ -126,10 +118,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private NotificationBackgroundView mBackgroundDimmed; private ObjectAnimator mBackgroundAnimator; private RectF mAppearAnimationRect = new RectF(); - private PorterDuffColorFilter mAppearAnimationFilter; private float mAnimationTranslationY; private boolean mDrawingAppearAnimation; - private Paint mAppearPaint = new Paint(); private ValueAnimator mAppearAnimator; private float mAppearAnimationFraction = -1.0f; private float mAppearAnimationTranslation; @@ -151,18 +141,15 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mLinearInterpolator = new LinearInterpolator(); setClipChildren(false); setClipToPadding(false); - mAppearAnimationFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); - mRoundedRectCornerRadius = getResources().getDimensionPixelSize( - R.dimen.notification_material_rounded_rect_radius); - mLegacyColor = getResources().getColor(R.color.notification_legacy_background_color); - mNormalColor = getResources().getColor(R.color.notification_material_background_color); - mLowPriorityColor = getResources().getColor( + mLegacyColor = context.getColor(R.color.notification_legacy_background_color); + mNormalColor = context.getColor(R.color.notification_material_background_color); + mLowPriorityColor = context.getColor( R.color.notification_material_background_low_priority_color); - mTintedRippleColor = context.getResources().getColor( + mTintedRippleColor = context.getColor( R.color.notification_ripple_tinted_color); - mLowPriorityRippleColor = context.getResources().getColor( + mLowPriorityRippleColor = context.getColor( R.color.notification_ripple_color_low_priority); - mNormalRippleColor = context.getResources().getColor( + mNormalRippleColor = context.getColor( R.color.notification_ripple_untinted_color); } @@ -388,7 +375,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private void updateBackgroundTint() { - int color = getBackgroundColor(); + int color = getBgColor(); int rippleColor = getRippleColor(); if (color == mNormalColor) { // We don't need to tint a normal notification @@ -540,9 +527,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay, long duration, final Runnable onFinishedRunnable) { - if (mAppearAnimator != null) { - mAppearAnimator.cancel(); - } + cancelAppearAnimation(); mAnimationTranslationY = translationDirection * getActualHeight(); if (mAppearAnimationFraction == -1.0f) { // not initialized yet, we start anew @@ -613,6 +598,17 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mAppearAnimator.start(); } + private void cancelAppearAnimation() { + if (mAppearAnimator != null) { + mAppearAnimator.cancel(); + } + } + + public void cancelAppearDrawing() { + cancelAppearAnimation(); + enableAppearDrawing(false); + } + private void updateAppearRect() { float inverseFraction = (1.0f - mAppearAnimationFraction); float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction); @@ -652,21 +648,27 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private void updateAppearAnimationAlpha() { - int backgroundColor = getBackgroundColor(); - if (backgroundColor != -1) { - float contentAlphaProgress = mAppearAnimationFraction; - contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END); - contentAlphaProgress = Math.min(1.0f, contentAlphaProgress); - contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress); - int sourceColor = Color.argb((int) (255 * (1.0f - contentAlphaProgress)), - Color.red(backgroundColor), Color.green(backgroundColor), - Color.blue(backgroundColor)); - mAppearAnimationFilter.setColor(sourceColor); - mAppearPaint.setColorFilter(mAppearAnimationFilter); + float contentAlphaProgress = mAppearAnimationFraction; + contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END); + contentAlphaProgress = Math.min(1.0f, contentAlphaProgress); + contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress); + setContentAlpha(contentAlphaProgress); + } + + private void setContentAlpha(float contentAlpha) { + int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE + : LAYER_TYPE_HARDWARE; + View contentView = getContentView(); + int currentLayerType = contentView.getLayerType(); + if (currentLayerType != layerType) { + contentView.setLayerType(layerType, null); } + contentView.setAlpha(contentAlpha); } - private int getBackgroundColor() { + protected abstract View getContentView(); + + private int getBgColor() { if (mBgTint != 0) { return mBgTint; } else if (mShowingLegacyBackground) { @@ -678,7 +680,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } } - private int getRippleColor() { + protected int getRippleColor() { if (mBgTint != 0) { return mTintedRippleColor; } else if (mShowingLegacyBackground) { @@ -699,41 +701,24 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView */ private void enableAppearDrawing(boolean enable) { if (enable != mDrawingAppearAnimation) { - if (enable) { - if (getWidth() == 0 || getActualHeight() == 0) { - // TODO: This should not happen, but it can during expansion. Needs - // investigation - return; - } - Bitmap bitmap = Bitmap.createBitmap(getWidth(), getActualHeight(), - Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - draw(canvas); - mAppearPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, - Shader.TileMode.CLAMP)); - } else { - mAppearPaint.setShader(null); - } mDrawingAppearAnimation = enable; + if (!enable) { + setContentAlpha(1.0f); + } invalidate(); } } @Override protected void dispatchDraw(Canvas canvas) { - if (!mDrawingAppearAnimation) { - super.dispatchDraw(canvas); - } else { - drawAppearRect(canvas); + if (mDrawingAppearAnimation) { + canvas.save(); + canvas.translate(0, mAppearAnimationTranslation); + } + super.dispatchDraw(canvas); + if (mDrawingAppearAnimation) { + canvas.restore(); } - } - - private void drawAppearRect(Canvas canvas) { - canvas.save(); - canvas.translate(0, mAppearAnimationTranslation); - canvas.drawRoundRect(mAppearAnimationRect, mRoundedRectCornerRadius, - mRoundedRectCornerRadius, mAppearPaint); - canvas.restore(); } public void setOnActivatedListener(OnActivatedListener onActivatedListener) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java new file mode 100644 index 0000000..87c12c2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; + +/** + * A Button which doesn't have overlapping drawing commands + */ +public class AlphaOptimizedButton extends Button { + public AlphaOptimizedButton(Context context) { + super(context); + } + + public AlphaOptimizedButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AlphaOptimizedButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public AlphaOptimizedButton(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java index a835c0e..359272e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java @@ -19,7 +19,6 @@ package com.android.systemui.statusbar; import android.content.Context; import android.util.AttributeSet; import android.widget.FrameLayout; -import android.widget.LinearLayout; /** * A frame layout which does not have overlapping renderings commands and therefore does not need a diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedImageView.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedImageView.java index 094161d..700ea34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedImageView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedImageView.java @@ -17,35 +17,49 @@ package com.android.systemui.statusbar; import android.content.Context; +import android.content.res.TypedArray; import android.util.AttributeSet; -import android.view.View; import android.widget.ImageView; +import com.android.systemui.R; + /** - * An ImageView which does not have overlapping rendering commands and therefore does not need a - * layer when alpha is changed. + * An ImageView which supports an attribute specifying whether it has overlapping rendering + * commands and therefore does not need a layer when alpha is changed. */ -public class AlphaOptimizedImageView extends ImageView -{ +public class AlphaOptimizedImageView extends ImageView { + private final boolean mHasOverlappingRendering; + public AlphaOptimizedImageView(Context context) { - super(context); + this(context, null /* attrs */); } public AlphaOptimizedImageView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0 /* defStyleAttr */); } public AlphaOptimizedImageView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, 0 /* defStyleRes */); } public AlphaOptimizedImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, + R.styleable.AlphaOptimizedImageView, 0, 0); + + try { + // Default to true, which is what View.java defaults to + mHasOverlappingRendering = a.getBoolean( + R.styleable.AlphaOptimizedImageView_hasOverlappingRendering, true); + } finally { + a.recycle(); + } } @Override public boolean hasOverlappingRendering() { - return false; + return mHasOverlappingRendering; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java b/packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java index 9839fe9..90f7c08 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java @@ -25,10 +25,15 @@ import android.widget.ImageView; import android.widget.RemoteViews.RemoteView; @RemoteView -public class AnimatedImageView extends ImageView { +public class AnimatedImageView extends AlphaOptimizedImageView { AnimationDrawable mAnim; boolean mAttached; + // Tracks the last image that was set, so that we don't refresh the image if it is exactly + // the same as the previous one. If this is a resid, we track that. If it's a drawable, we + // track the hashcode of the drawable. + int mDrawableId; + public AnimatedImageView(Context context) { super(context); } @@ -43,7 +48,7 @@ public class AnimatedImageView extends ImageView { mAnim.stop(); } if (drawable instanceof AnimationDrawable) { - mAnim = (AnimationDrawable)drawable; + mAnim = (AnimationDrawable) drawable; if (isShown()) { mAnim.start(); } @@ -54,6 +59,13 @@ public class AnimatedImageView extends ImageView { @Override public void setImageDrawable(Drawable drawable) { + if (drawable != null) { + if (mDrawableId == drawable.hashCode()) return; + + mDrawableId = drawable.hashCode(); + } else { + mDrawableId = 0; + } super.setImageDrawable(drawable); updateAnim(); } @@ -61,6 +73,9 @@ public class AnimatedImageView extends ImageView { @Override @android.view.RemotableViewMethod public void setImageResource(int resid) { + if (mDrawableId == resid) return; + + mDrawableId = resid; super.setImageResource(resid); updateAnim(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index 8a03a2b..6ad0ef9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -41,6 +41,7 @@ import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; @@ -49,6 +50,7 @@ import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -63,39 +65,42 @@ import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.Display; +import android.view.Gravity; import android.view.IWindowManager; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; -import android.view.ViewStub; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.view.animation.AnimationUtils; import android.widget.DateTimeView; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.RemoteViews; import android.widget.TextView; +import android.widget.Toast; +import com.android.internal.logging.MetricsLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.statusbar.StatusBarIconList; import com.android.internal.util.NotificationColorUtil; import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.R; import com.android.systemui.RecentsComponent; -import com.android.systemui.SearchPanelView; import com.android.systemui.SwipeHelper; import com.android.systemui.SystemUI; +import com.android.systemui.assist.AssistManager; +import com.android.systemui.recents.Recents; import com.android.systemui.statusbar.NotificationData.Entry; import com.android.systemui.statusbar.phone.NavigationBarView; +import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.policy.HeadsUpNotificationView; +import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.PreviewInflater; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; @@ -116,6 +121,9 @@ public abstract class BaseStatusBar extends SystemUI implements // STOPSHIP disable once we resolve b/18102199 private static final boolean NOTIFICATION_CLICK_DEBUG = true; + public static final boolean ENABLE_CHILD_NOTIFICATIONS = Build.IS_DEBUGGABLE + && SystemProperties.getBoolean("debug.child_notifs", false); + protected static final int MSG_SHOW_RECENT_APPS = 1019; protected static final int MSG_HIDE_RECENT_APPS = 1020; protected static final int MSG_TOGGLE_RECENTS_APPS = 1021; @@ -123,11 +131,6 @@ public abstract class BaseStatusBar extends SystemUI implements protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023; protected static final int MSG_SHOW_NEXT_AFFILIATED_TASK = 1024; protected static final int MSG_SHOW_PREV_AFFILIATED_TASK = 1025; - protected static final int MSG_CLOSE_SEARCH_PANEL = 1027; - protected static final int MSG_SHOW_HEADS_UP = 1028; - protected static final int MSG_HIDE_HEADS_UP = 1029; - protected static final int MSG_ESCALATE_HEADS_UP = 1030; - protected static final int MSG_DECAY_HEADS_UP = 1031; protected static final boolean ENABLE_HEADS_UP = true; // scores above this threshold should be displayed in heads up mode. @@ -137,10 +140,6 @@ public abstract class BaseStatusBar extends SystemUI implements // Should match the value in PhoneWindowManager public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; - public static final int EXPANDED_LEAVE_ALONE = -10000; - public static final int EXPANDED_FULL_OPEN = -10001; - - private static final int HIDDEN_NOTIFICATION_ID = 10000; private static final String BANNER_ACTION_CANCEL = "com.android.systemui.statusbar.banner_action_cancel"; private static final String BANNER_ACTION_SETUP = @@ -154,12 +153,10 @@ public abstract class BaseStatusBar extends SystemUI implements protected NotificationData mNotificationData; protected NotificationStackScrollLayout mStackScroller; - // for heads up notifications - protected HeadsUpNotificationView mHeadsUpNotificationView; - protected int mHeadsUpNotificationDecay; + protected NotificationGroupManager mGroupManager = new NotificationGroupManager(); - // Search panel - protected SearchPanelView mSearchPanelView; + // for heads up notifications + protected HeadsUpManager mHeadsUpManager; protected int mCurrentUserId = 0; final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>(); @@ -241,6 +238,10 @@ public abstract class BaseStatusBar extends SystemUI implements protected DismissView mDismissView; protected EmptyShadeView mEmptyShadeView; + private NotificationClicker mNotificationClicker = new NotificationClicker(); + + protected AssistManager mAssistManager; + @Override // NotificationData.Environment public boolean isDeviceProvisioned() { return mDeviceProvisioned; @@ -302,6 +303,7 @@ public abstract class BaseStatusBar extends SystemUI implements try { ActivityManagerNative.getDefault() .keyguardWaitingForActivityDrawn(); + ActivityManagerNative.getDefault().resumeAppSwitches(); } catch (RemoteException e) { } } @@ -314,7 +316,9 @@ public abstract class BaseStatusBar extends SystemUI implements animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */); visibilityChanged(false); + mAssistManager.hideAssist(); } + // Wait for activity start. return handled; } @@ -379,15 +383,36 @@ public abstract class BaseStatusBar extends SystemUI implements userSwitched(mCurrentUserId); } else if (Intent.ACTION_USER_ADDED.equals(action)) { updateCurrentProfilesCache(); - } else if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals( - action)) { - mUsersAllowingPrivateNotifications.clear(); - updateLockscreenNotificationSetting(); - updateNotifications(); + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + List<ActivityManager.RecentTaskInfo> recentTask = null; + try { + recentTask = ActivityManagerNative.getDefault().getRecentTasks(1, + ActivityManager.RECENT_WITH_EXCLUDED + | ActivityManager.RECENT_INCLUDE_PROFILES, + mCurrentUserId); + } catch (RemoteException e) { + // Abandon hope activity manager not running. + } + if (recentTask != null && recentTask.size() > 0) { + UserInfo user = mUserManager.getUserInfo(recentTask.get(0).userId); + if (user != null && user.isManagedProfile()) { + Toast toast = Toast.makeText(mContext, + R.string.managed_profile_foreground_toast, + Toast.LENGTH_SHORT); + TextView text = (TextView) toast.getView().findViewById( + android.R.id.message); + text.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.stat_sys_managed_profile_status, 0, 0, 0); + int paddingPx = mContext.getResources().getDimensionPixelSize( + R.dimen.managed_profile_toast_padding); + text.setCompoundDrawablePadding(paddingPx); + toast.show(); + } + } } else if (BANNER_ACTION_CANCEL.equals(action) || BANNER_ACTION_SETUP.equals(action)) { NotificationManager noMan = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - noMan.cancel(HIDDEN_NOTIFICATION_ID); + noMan.cancel(R.id.notification_hidden); Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0); @@ -403,6 +428,19 @@ public abstract class BaseStatusBar extends SystemUI implements } }; + private final BroadcastReceiver mAllUsersReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(action) && + isCurrentProfile(getSendingUserId())) { + mUsersAllowingPrivateNotifications.clear(); + updateLockscreenNotificationSetting(); + updateNotifications(); + } + } + }; + private final NotificationListenerService mNotificationListener = new NotificationListenerService() { @Override @@ -414,7 +452,7 @@ public abstract class BaseStatusBar extends SystemUI implements @Override public void run() { for (StatusBarNotification sbn : notifications) { - addNotification(sbn, currentRanking); + addNotification(sbn, currentRanking, null /* oldEntry */); } } }); @@ -424,61 +462,68 @@ public abstract class BaseStatusBar extends SystemUI implements public void onNotificationPosted(final StatusBarNotification sbn, final RankingMap rankingMap) { if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn); - mHandler.post(new Runnable() { - @Override - public void run() { - Notification n = sbn.getNotification(); - boolean isUpdate = mNotificationData.get(sbn.getKey()) != null - || isHeadsUp(sbn.getKey()); - - // Ignore children of notifications that have a summary, since we're not - // going to show them anyway. This is true also when the summary is canceled, - // because children are automatically canceled by NoMan in that case. - if (n.isGroupChild() && - mNotificationData.isGroupWithSummary(sbn.getGroupKey())) { - if (DEBUG) { - Log.d(TAG, "Ignoring group child due to existing summary: " + sbn); - } + if (sbn != null) { + mHandler.post(new Runnable() { + @Override + public void run() { - // Remove existing notification to avoid stale data. + String key = sbn.getKey(); + boolean isUpdate = mNotificationData.get(key) != null; + + // In case we don't allow child notifications, we ignore children of + // notifications that have a summary, since we're not going to show them + // anyway. This is true also when the summary is canceled, + // because children are automatically canceled by NoMan in that case. + if (!ENABLE_CHILD_NOTIFICATIONS + && mGroupManager.isChildInGroupWithSummary(sbn)) { + if (DEBUG) { + Log.d(TAG, "Ignoring group child due to existing summary: " + sbn); + } + + // Remove existing notification to avoid stale data. + if (isUpdate) { + removeNotification(key, rankingMap); + } else { + mNotificationData.updateRanking(rankingMap); + } + return; + } if (isUpdate) { - removeNotification(sbn.getKey(), rankingMap); + updateNotification(sbn, rankingMap); } else { - mNotificationData.updateRanking(rankingMap); + addNotification(sbn, rankingMap, null /* oldEntry */); } - return; } - if (isUpdate) { - updateNotification(sbn, rankingMap); - } else { - addNotification(sbn, rankingMap); - } - } - }); + }); + } } @Override - public void onNotificationRemoved(final StatusBarNotification sbn, + public void onNotificationRemoved(StatusBarNotification sbn, final RankingMap rankingMap) { if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn); - mHandler.post(new Runnable() { - @Override - public void run() { - removeNotification(sbn.getKey(), rankingMap); - } - }); + if (sbn != null) { + final String key = sbn.getKey(); + mHandler.post(new Runnable() { + @Override + public void run() { + removeNotification(key, rankingMap); + } + }); + } } @Override public void onNotificationRankingUpdate(final RankingMap rankingMap) { if (DEBUG) Log.d(TAG, "onRankingUpdate"); + if (rankingMap != null) { mHandler.post(new Runnable() { @Override public void run() { updateNotificationRanking(rankingMap); } }); - } + } } }; @@ -511,7 +556,6 @@ public abstract class BaseStatusBar extends SystemUI implements ServiceManager.checkService(DreamService.DREAM_SERVICE)); mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - mSettingsObserver.onChange(false); // set up mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), true, mSettingsObserver); @@ -532,7 +576,7 @@ public abstract class BaseStatusBar extends SystemUI implements mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); - mRecents = getComponent(RecentsComponent.class); + mRecents = getComponent(Recents.class); mRecents.setCallback(this); final Configuration currentConfig = mContext.getResources().getConfiguration(); @@ -561,7 +605,8 @@ public abstract class BaseStatusBar extends SystemUI implements createAndAddWindows(); - disable(switches[0], false /* animate */); + mSettingsObserver.onChange(false); // set up + disable(switches[0], switches[6], false /* animate */); setSystemUiVisibility(switches[1], 0xffffffff); topAppWindowChanged(switches[2] != 0); // StatusBarManagerService has a back up of IME token and it's restored here. @@ -605,11 +650,16 @@ public abstract class BaseStatusBar extends SystemUI implements IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(Intent.ACTION_USER_ADDED); + filter.addAction(Intent.ACTION_USER_PRESENT); filter.addAction(BANNER_ACTION_CANCEL); filter.addAction(BANNER_ACTION_SETUP); - filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); mContext.registerReceiver(mBroadcastReceiver, filter); + IntentFilter allUsersFilter = new IntentFilter(); + allUsersFilter.addAction( + DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); + mContext.registerReceiverAsUser(mAllUsersReceiver, UserHandle.ALL, allUsersFilter, + null, null); updateCurrentProfilesCache(); } @@ -618,7 +668,7 @@ public abstract class BaseStatusBar extends SystemUI implements Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 1)) { Log.d(TAG, "user hasn't seen notification about hidden notifications"); final LockPatternUtils lockPatternUtils = new LockPatternUtils(mContext); - if (!lockPatternUtils.isSecure()) { + if (!lockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())) { Log.d(TAG, "insecure lockscreen, skipping notification"); Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0); @@ -647,7 +697,7 @@ public abstract class BaseStatusBar extends SystemUI implements .setContentText(mContext.getString(R.string.hidden_notifications_text)) .setPriority(Notification.PRIORITY_HIGH) .setOngoing(true) - .setColor(res.getColor(colorRes)) + .setColor(mContext.getColor(colorRes)) .setContentIntent(setupIntent) .addAction(R.drawable.ic_close, mContext.getString(R.string.hidden_notifications_cancel), @@ -658,7 +708,7 @@ public abstract class BaseStatusBar extends SystemUI implements NotificationManager noMan = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - noMan.notify(HIDDEN_NOTIFICATION_ID, note.build()); + noMan.notify(R.id.notification_hidden, note.build()); } } @@ -666,15 +716,7 @@ public abstract class BaseStatusBar extends SystemUI implements setHeadsUpUser(newUserId); } - private void setHeadsUpUser(int newUserId) { - if (mHeadsUpNotificationView != null) { - mHeadsUpNotificationView.setUser(newUserId); - } - } - - public boolean isHeadsUp(String key) { - return mHeadsUpNotificationView != null && mHeadsUpNotificationView.isShowing(key); - } + protected abstract void setHeadsUpUser(int newUserId); @Override // NotificationData.Environment public boolean isNotificationForCurrentProfiles(StatusBarNotification n) { @@ -687,6 +729,14 @@ public abstract class BaseStatusBar extends SystemUI implements return isCurrentProfile(notificationUserId); } + protected void setNotificationShown(StatusBarNotification n) { + mNotificationListener.setNotificationsShown(new String[] { n.getKey() }); + } + + protected void setNotificationsShown(String[] keys) { + mNotificationListener.setNotificationsShown(keys); + } + protected boolean isCurrentProfile(int userId) { synchronized (mCurrentProfiles) { return userId == UserHandle.USER_ALL || mCurrentProfiles.get(userId) != null; @@ -698,6 +748,11 @@ public abstract class BaseStatusBar extends SystemUI implements return null; } + @Override + public NotificationGroupManager getGroupManager() { + return mGroupManager; + } + /** * Takes the necessary steps to prepare the status bar for starting an activity, then starts it. * @param action A dismiss action that is called if it's safe to start the activity. @@ -727,8 +782,7 @@ public abstract class BaseStatusBar extends SystemUI implements protected View updateNotificationVetoButton(View row, StatusBarNotification n) { View vetoButton = row.findViewById(R.id.veto); - if (n.isClearable() || (mHeadsUpNotificationView.getEntry() != null - && mHeadsUpNotificationView.getEntry().row == row)) { + if (n.isClearable()) { final String _pkg = n.getPackageName(); final String _tag = n.getTag(); final int _id = n.getId(); @@ -758,7 +812,8 @@ public abstract class BaseStatusBar extends SystemUI implements protected void applyColorsAndBackgrounds(StatusBarNotification sbn, NotificationData.Entry entry) { - if (entry.expanded.getId() != com.android.internal.R.id.status_bar_latest_event_content) { + if (entry.getContentView().getId() + != com.android.internal.R.id.status_bar_latest_event_content) { // Using custom RemoteViews if (entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD && entry.targetSdk < Build.VERSION_CODES.LOLLIPOP) { @@ -770,25 +825,22 @@ public abstract class BaseStatusBar extends SystemUI implements final int color = sbn.getNotification().color; if (isMediaNotification(entry)) { entry.row.setTintColor(color == Notification.COLOR_DEFAULT - ? mContext.getResources().getColor( + ? mContext.getColor( R.color.notification_material_background_media_default_color) : color); } } if (entry.icon != null) { - if (entry.targetSdk >= Build.VERSION_CODES.LOLLIPOP) { - entry.icon.setColorFilter(mContext.getResources().getColor(android.R.color.white)); - } else { - entry.icon.setColorFilter(null); - } + entry.icon.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP); } } public boolean isMediaNotification(NotificationData.Entry entry) { // TODO: confirm that there's a valid media key - return entry.expandedBig != null && - entry.expandedBig.findViewById(com.android.internal.R.id.media_actions) != null; + return entry.getExpandedContentView() != null && + entry.getExpandedContentView() + .findViewById(com.android.internal.R.id.media_actions) != null; } // The gear button in the guts that links to the app's own notification settings @@ -834,16 +886,13 @@ public abstract class BaseStatusBar extends SystemUI implements }, false /* afterKeyguardGone */); } - private void inflateGuts(ExpandableNotificationRow row) { - ViewStub stub = (ViewStub) row.findViewById(R.id.notification_guts_stub); - if (stub != null) { - stub.inflate(); - } + private void bindGuts(ExpandableNotificationRow row) { + row.inflateGuts(); final StatusBarNotification sbn = row.getStatusBarNotification(); PackageManager pmUser = getPackageManagerForUser( sbn.getUser().getIdentifier()); row.setTag(sbn.getPackageName()); - final View guts = row.findViewById(R.id.notification_guts); + final View guts = row.getGuts(); final String pkg = sbn.getPackageName(); String appname = pkg; Drawable pkgicon = null; @@ -871,6 +920,7 @@ public abstract class BaseStatusBar extends SystemUI implements final int appUidF = appUid; settingsButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { + MetricsLogger.action(mContext, MetricsLogger.ACTION_NOTE_INFO); startAppNotificationSettingsActivity(pkg, appUidF); } }); @@ -891,6 +941,7 @@ public abstract class BaseStatusBar extends SystemUI implements .setClassName(pkg, infos.get(0).activityInfo.name); appSettingsButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { + MetricsLogger.action(mContext, MetricsLogger.ACTION_APP_NOTE_SETTINGS); startAppOwnNotificationSettingsActivity(appSettingsLaunchIntent, sbn.getId(), sbn.getTag(), @@ -921,11 +972,11 @@ public abstract class BaseStatusBar extends SystemUI implements return false; } - inflateGuts((ExpandableNotificationRow) v); + ExpandableNotificationRow row = (ExpandableNotificationRow) v; + bindGuts(row); // Assume we are a status_bar_notification_row - final NotificationGuts guts = (NotificationGuts) v.findViewById( - R.id.notification_guts); + final NotificationGuts guts = row.getGuts(); if (guts == null) { // This view has no guts. Examples are the more card or the dismiss all view return false; @@ -937,6 +988,7 @@ public abstract class BaseStatusBar extends SystemUI implements return false; } + MetricsLogger.action(mContext, MetricsLogger.ACTION_NOTE_CONTROLS); guts.setVisibility(View.VISIBLE); final double horz = Math.max(guts.getWidth() - x, x); final double vert = Math.max(guts.getActualHeight() - y, y); @@ -978,9 +1030,6 @@ public abstract class BaseStatusBar extends SystemUI implements } } - public void onHeadsUpDismissed() { - } - @Override public void showRecentApps(boolean triggeredFromAltTab) { int msg = MSG_SHOW_RECENT_APPS; @@ -998,9 +1047,7 @@ public abstract class BaseStatusBar extends SystemUI implements @Override public void toggleRecentApps() { - int msg = MSG_TOGGLE_RECENTS_APPS; - mHandler.removeMessages(msg); - mHandler.sendEmptyMessage(msg); + toggleRecents(); } @Override @@ -1031,50 +1078,6 @@ public abstract class BaseStatusBar extends SystemUI implements mHandler.sendEmptyMessage(msg); } - @Override - public void showSearchPanel() { - if (mSearchPanelView != null && mSearchPanelView.isAssistantAvailable()) { - mSearchPanelView.show(true, true); - } - } - - @Override - public void hideSearchPanel() { - int msg = MSG_CLOSE_SEARCH_PANEL; - mHandler.removeMessages(msg); - mHandler.sendEmptyMessage(msg); - } - - protected abstract WindowManager.LayoutParams getSearchLayoutParams( - LayoutParams layoutParams); - - protected void updateSearchPanel() { - // Search Panel - boolean visible = false; - if (mSearchPanelView != null) { - visible = mSearchPanelView.isShowing(); - mWindowManager.removeView(mSearchPanelView); - } - - // Provide SearchPanel with a temporary parent to allow layout params to work. - LinearLayout tmpRoot = new LinearLayout(mContext); - mSearchPanelView = (SearchPanelView) LayoutInflater.from(mContext).inflate( - R.layout.status_bar_search_panel, tmpRoot, false); - mSearchPanelView.setOnTouchListener( - new TouchOutsideListener(MSG_CLOSE_SEARCH_PANEL, mSearchPanelView)); - mSearchPanelView.setVisibility(View.GONE); - boolean vertical = mNavigationBarView != null && mNavigationBarView.isVertical(); - mSearchPanelView.setHorizontal(vertical); - - WindowManager.LayoutParams lp = getSearchLayoutParams(mSearchPanelView.getLayoutParams()); - - mWindowManager.addView(mSearchPanelView, lp); - mSearchPanelView.setBar(this); - if (visible) { - mSearchPanelView.show(true, false); - } - } - protected H createHandler() { return new H(); } @@ -1161,13 +1164,10 @@ public abstract class BaseStatusBar extends SystemUI implements // Do nothing } - public abstract void resetHeadsUpDecayTimer(); - - public abstract void scheduleHeadsUpOpen(); - - public abstract void scheduleHeadsUpClose(); - - public abstract void scheduleHeadsUpEscalation(); + /** + * If there is an active heads-up notification and it has a fullscreen intent, fire it now. + */ + public abstract void maybeEscalateHeadsUp(); /** * Save the current "public" (locked and secure) state of the lockscreen. @@ -1190,14 +1190,15 @@ public abstract class BaseStatusBar extends SystemUI implements } if (mUsersAllowingPrivateNotifications.indexOfKey(userHandle) < 0) { - final boolean allowed = 0 != Settings.Secure.getIntForUser( + final boolean allowedByUser = 0 != Settings.Secure.getIntForUser( mContext.getContentResolver(), Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle); final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */, userHandle); final boolean allowedByDpm = (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS) == 0; - mUsersAllowingPrivateNotifications.append(userHandle, allowed && allowedByDpm); + final boolean allowed = allowedByUser && allowedByDpm; + mUsersAllowingPrivateNotifications.append(userHandle, allowed); return allowed; } @@ -1250,50 +1251,14 @@ public abstract class BaseStatusBar extends SystemUI implements case MSG_SHOW_PREV_AFFILIATED_TASK: showRecentsPreviousAffiliatedTask(); break; - case MSG_CLOSE_SEARCH_PANEL: - if (DEBUG) Log.d(TAG, "closing search panel"); - if (mSearchPanelView != null && mSearchPanelView.isShowing()) { - mSearchPanelView.show(false, true); - } - break; - } - } - } - - public class TouchOutsideListener implements View.OnTouchListener { - private int mMsg; - private StatusBarPanel mPanel; - - public TouchOutsideListener(int msg, StatusBarPanel panel) { - mMsg = msg; - mPanel = panel; - } - - public boolean onTouch(View v, MotionEvent ev) { - final int action = ev.getAction(); - if (action == MotionEvent.ACTION_OUTSIDE - || (action == MotionEvent.ACTION_DOWN - && !mPanel.isInContentArea((int)ev.getX(), (int)ev.getY()))) { - mHandler.removeMessages(mMsg); - mHandler.sendEmptyMessage(mMsg); - return true; } - return false; } } protected void workAroundBadLayerDrawableOpacity(View v) { } - private boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) { - return inflateViews(entry, parent, false); - } - - protected boolean inflateViewsForHeadsUp(NotificationData.Entry entry, ViewGroup parent) { - return inflateViews(entry, parent, true); - } - - private boolean inflateViews(NotificationData.Entry entry, ViewGroup parent, boolean isHeadsUp) { + protected boolean inflateViews(Entry entry, ViewGroup parent) { PackageManager pmUser = getPackageManagerForUser( entry.notification.getUser().getIdentifier()); @@ -1301,12 +1266,7 @@ public abstract class BaseStatusBar extends SystemUI implements final 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; - } + RemoteViews headsUpContentView = sbn.getNotification().headsUpContentView; if (contentView == null) { return false; @@ -1342,6 +1302,7 @@ public abstract class BaseStatusBar extends SystemUI implements row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row, parent, false); row.setExpansionLogger(this, entry.notification.getKey()); + row.setGroupManager(mGroupManager); } workAroundBadLayerDrawableOpacity(row); @@ -1352,30 +1313,26 @@ 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 - NotificationContentView expanded = - (NotificationContentView) row.findViewById(R.id.expanded); - NotificationContentView expandedPublic = - (NotificationContentView) row.findViewById(R.id.expandedPublic); + NotificationContentView contentContainer = row.getPrivateLayout(); + NotificationContentView contentContainerPublic = row.getPublicLayout(); row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); - PendingIntent contentIntent = sbn.getNotification().contentIntent; - if (contentIntent != null) { - final View.OnClickListener listener = makeClicker(contentIntent, sbn.getKey(), - isHeadsUp); - row.setOnClickListener(listener); - } else { - row.setOnClickListener(null); - } + mNotificationClicker.register(row, sbn); // set up the adaptive layout View contentViewLocal = null; View bigContentViewLocal = null; + View headsUpContentViewLocal = null; try { - contentViewLocal = contentView.apply(mContext, expanded, + contentViewLocal = contentView.apply(mContext, contentContainer, mOnClickHandler); if (bigContentView != null) { - bigContentViewLocal = bigContentView.apply(mContext, expanded, + bigContentViewLocal = bigContentView.apply(mContext, contentContainer, + mOnClickHandler); + } + if (headsUpContentView != null) { + headsUpContentViewLocal = headsUpContentView.apply(mContext, contentContainer, mOnClickHandler); } } @@ -1387,23 +1344,27 @@ public abstract class BaseStatusBar extends SystemUI implements if (contentViewLocal != null) { contentViewLocal.setIsRootNamespace(true); - expanded.setContractedChild(contentViewLocal); + contentContainer.setContractedChild(contentViewLocal); } if (bigContentViewLocal != null) { bigContentViewLocal.setIsRootNamespace(true); - expanded.setExpandedChild(bigContentViewLocal); + contentContainer.setExpandedChild(bigContentViewLocal); + } + if (headsUpContentViewLocal != null) { + headsUpContentViewLocal.setIsRootNamespace(true); + contentContainer.setHeadsUpChild(headsUpContentViewLocal); } // now the public version View publicViewLocal = null; if (publicNotification != null) { try { - publicViewLocal = publicNotification.contentView.apply(mContext, expandedPublic, - mOnClickHandler); + publicViewLocal = publicNotification.contentView.apply(mContext, + contentContainerPublic, mOnClickHandler); if (publicViewLocal != null) { publicViewLocal.setIsRootNamespace(true); - expandedPublic.setContractedChild(publicViewLocal); + contentContainerPublic.setContractedChild(publicViewLocal); } } catch (RuntimeException e) { @@ -1425,9 +1386,8 @@ public abstract class BaseStatusBar extends SystemUI implements // Add a basic notification template publicViewLocal = LayoutInflater.from(mContext).inflate( R.layout.notification_public_default, - expandedPublic, false); + contentContainerPublic, false); publicViewLocal.setIsRootNamespace(true); - expandedPublic.setContractedChild(publicViewLocal); final TextView title = (TextView) publicViewLocal.findViewById(R.id.title); try { @@ -1441,9 +1401,10 @@ public abstract class BaseStatusBar extends SystemUI implements final ImageView profileBadge = (ImageView) publicViewLocal.findViewById( R.id.profile_badge_line3); - final StatusBarIcon ic = new StatusBarIcon(entry.notification.getPackageName(), + final StatusBarIcon ic = new StatusBarIcon( entry.notification.getUser(), - entry.notification.getNotification().icon, + entry.notification.getPackageName(), + entry.notification.getNotification().getSmallIcon(), entry.notification.getNotification().iconLevel, entry.notification.getNotification().number, entry.notification.getNotification().tickerText); @@ -1493,6 +1454,7 @@ public abstract class BaseStatusBar extends SystemUI implements mContext.getResources().getConfiguration().fontScale); title.setPadding(0, topPadding, 0, 0); + contentContainerPublic.setContractedChild(publicViewLocal); entry.autoRedacted = true; } @@ -1506,9 +1468,7 @@ public abstract class BaseStatusBar extends SystemUI implements entry.row = row; entry.row.setHeightRange(mRowMinHeight, maxHeight); entry.row.setOnActivatedListener(this); - entry.expanded = contentViewLocal; - entry.expandedPublic = publicViewLocal; - entry.setBigContentView(bigContentViewLocal); + entry.row.setExpandable(bigContentViewLocal != null); applyColorsAndBackgrounds(sbn, entry); @@ -1520,41 +1480,42 @@ public abstract class BaseStatusBar extends SystemUI implements } row.setUserLocked(userLocked); row.setStatusBarNotification(entry.notification); + return true; } - public NotificationClicker makeClicker(PendingIntent intent, String notificationKey, - boolean forHun) { - return new NotificationClicker(intent, notificationKey, forHun); - } + private final class NotificationClicker implements View.OnClickListener { + public void onClick(final View v) { + if (!(v instanceof ExpandableNotificationRow)) { + Log.e(TAG, "NotificationClicker called on a view that is not a notification row."); + return; + } - protected class NotificationClicker implements View.OnClickListener { - private PendingIntent mIntent; - private final String mNotificationKey; - private boolean mIsHeadsUp; + final ExpandableNotificationRow row = (ExpandableNotificationRow) v; + final StatusBarNotification sbn = row.getStatusBarNotification(); + if (sbn == null) { + Log.e(TAG, "NotificationClicker called on an unclickable notification,"); + return; + } - public NotificationClicker(PendingIntent intent, String notificationKey, boolean forHun) { - mIntent = intent; - mNotificationKey = notificationKey; - mIsHeadsUp = forHun; - } + final PendingIntent intent = sbn.getNotification().contentIntent; + final String notificationKey = sbn.getKey(); - public void onClick(final View v) { if (NOTIFICATION_CLICK_DEBUG) { - Log.d(TAG, "Clicked on content of " + mNotificationKey); + Log.d(TAG, "Clicked on content of " + notificationKey); } final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing(); - final boolean afterKeyguardGone = mIntent.isActivity() - && PreviewInflater.wouldLaunchResolverActivity(mContext, mIntent.getIntent(), + final boolean afterKeyguardGone = intent.isActivity() + && PreviewInflater.wouldLaunchResolverActivity(mContext, intent.getIntent(), mCurrentUserId); dismissKeyguardThenExecute(new OnDismissAction() { public boolean onDismiss() { - if (mIsHeadsUp) { + if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUp(notificationKey)) { // Release the HUN notification to the shade. // // In most cases, when FLAG_AUTO_CANCEL is set, the notification will // become canceled shortly by NoMan, but we can't assume that. - mHeadsUpNotificationView.releaseAndClose(); + mHeadsUpManager.releaseImmediately(notificationKey); } new Thread() { @Override @@ -1573,9 +1534,9 @@ public abstract class BaseStatusBar extends SystemUI implements } catch (RemoteException e) { } - if (mIntent != null) { + if (intent != null) { try { - mIntent.send(); + intent.send(); } catch (PendingIntent.CanceledException e) { // the stack trace isn't very helpful here. // Just log the exception message. @@ -1583,14 +1544,15 @@ public abstract class BaseStatusBar extends SystemUI implements // TODO: Dismiss Keyguard. } - if (mIntent.isActivity()) { + if (intent.isActivity()) { + mAssistManager.hideAssist(); overrideActivityPendingAppTransition(keyguardShowing && !afterKeyguardGone); } } try { - mBarService.onNotificationClick(mNotificationKey); + mBarService.onNotificationClick(notificationKey); } catch (RemoteException ex) { // system process is dead if we're here. } @@ -1599,18 +1561,30 @@ public abstract class BaseStatusBar extends SystemUI implements // close the shade if it was open animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, - true /* force */); + true /* force */, true /* delayed */); visibilityChanged(false); - return mIntent != null && mIntent.isActivity(); + return true; } }, afterKeyguardGone); } + + public void register(ExpandableNotificationRow row, StatusBarNotification sbn) { + final PendingIntent contentIntent = sbn.getNotification().contentIntent; + if (contentIntent != null) { + row.setOnClickListener(this); + } else { + row.setOnClickListener(null); + } + } } public void animateCollapsePanels(int flags, boolean force) { } + public void animateCollapsePanels(int flags, boolean force, boolean delayed) { + } + public void overrideActivityPendingAppTransition(boolean keyguardShowing) { if (keyguardShowing) { try { @@ -1642,19 +1616,20 @@ public abstract class BaseStatusBar extends SystemUI implements /** * The LEDs are turned off when the notification panel is shown, even just a little bit. - * This was added last-minute and is inconsistent with the way the rest of the notifications - * are handled, because the notification isn't really cancelled. The lights are just - * turned off. If any other notifications happen, the lights will turn back on. Steve says - * this is what he wants. (see bug 1131461) */ protected void handleVisibleToUserChanged(boolean visibleToUser) { try { if (visibleToUser) { - // Only stop blinking, vibrating, ringing when the user went into the shade - // manually (SHADE or SHADE_LOCKED). - boolean clearNotificationEffects = - (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED); - mBarService.onPanelRevealed(clearNotificationEffects); + boolean clearNotificationEffects = !isPanelFullyCollapsed() && + (mShowLockscreenNotifications || + (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED)); + int notificationLoad = mNotificationData.getActiveNotifications().size(); + if (mHeadsUpManager.hasPinnedHeadsUp() && isPanelFullyCollapsed()) { + notificationLoad = 1; + } else { + MetricsLogger.histogram(mContext, "note_load", notificationLoad); + } + mBarService.onPanelRevealed(clearNotificationEffects, notificationLoad); } else { mBarService.onPanelHidden(); } @@ -1664,6 +1639,19 @@ public abstract class BaseStatusBar extends SystemUI implements } /** + * Clear Buzz/Beep/Blink. + */ + public void clearNotificationEffects() { + try { + mBarService.clearNotificationEffects(); + } catch (RemoteException e) { + // Won't fail unless the world has ended. + } + } + + protected abstract boolean isPanelFullyCollapsed(); + + /** * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService * about the failure. * @@ -1693,29 +1681,39 @@ public abstract class BaseStatusBar extends SystemUI implements if (DEBUG) { Log.d(TAG, "createNotificationViews(notification=" + sbn); } + final StatusBarIconView iconView = createIcon(sbn); + if (iconView == null) { + return null; + } + + // Construct the expanded view. + NotificationData.Entry entry = new NotificationData.Entry(sbn, iconView); + if (!inflateViews(entry, mStackScroller)) { + handleNotificationError(sbn, "Couldn't expand RemoteViews for: " + sbn); + return null; + } + return entry; + } + + protected StatusBarIconView createIcon(StatusBarNotification sbn) { // Construct the icon. Notification n = sbn.getNotification(); final StatusBarIconView iconView = new StatusBarIconView(mContext, sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), n); iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - final StatusBarIcon ic = new StatusBarIcon(sbn.getPackageName(), + final StatusBarIcon ic = new StatusBarIcon( sbn.getUser(), - n.icon, - n.iconLevel, - n.number, - n.tickerText); + sbn.getPackageName(), + n.getSmallIcon(), + n.iconLevel, + n.number, + n.tickerText); if (!iconView.set(ic)) { handleNotificationError(sbn, "Couldn't create icon: " + ic); return null; } - // Construct the expanded view. - NotificationData.Entry entry = new NotificationData.Entry(sbn, iconView); - if (!inflateViews(entry, mStackScroller)) { - handleNotificationError(sbn, "Couldn't expand RemoteViews for: " + sbn); - return null; - } - return entry; + return iconView; } protected void addNotificationViews(Entry entry, RankingMap ranking) { @@ -1755,35 +1753,35 @@ public abstract class BaseStatusBar extends SystemUI implements entry.row.setSystemExpanded(top); } } + boolean isInvisibleChild = !mGroupManager.isVisible(entry.notification); boolean showOnKeyguard = shouldShowOnKeyguard(entry.notification); if ((isLockscreenPublicMode() && !mShowLockscreenNotifications) || (onKeyguard && (visibleNotifications >= maxKeyguardNotifications - || !showOnKeyguard))) { + || !showOnKeyguard || isInvisibleChild))) { entry.row.setVisibility(View.GONE); - if (onKeyguard && showOnKeyguard) { + if (onKeyguard && showOnKeyguard && !isInvisibleChild) { mKeyguardIconOverflowContainer.getIconsView().addNotification(entry); } } else { boolean wasGone = entry.row.getVisibility() == View.GONE; entry.row.setVisibility(View.VISIBLE); - if (wasGone) { - // notify the scroller of a child addition - mStackScroller.generateAddAnimation(entry.row, true /* fromMoreCard */); + if (!isInvisibleChild) { + if (wasGone) { + // notify the scroller of a child addition + mStackScroller.generateAddAnimation(entry.row, true /* fromMoreCard */); + } + visibleNotifications++; } - visibleNotifications++; } } - if (onKeyguard && mKeyguardIconOverflowContainer.getIconsView().getChildCount() > 0) { - mKeyguardIconOverflowContainer.setVisibility(View.VISIBLE); - } else { - mKeyguardIconOverflowContainer.setVisibility(View.GONE); - } + mStackScroller.updateOverflowContainerVisibility(onKeyguard + && mKeyguardIconOverflowContainer.getIconsView().getChildCount() > 0); + mStackScroller.changeViewPosition(mDismissView, mStackScroller.getChildCount() - 1); + mStackScroller.changeViewPosition(mEmptyShadeView, mStackScroller.getChildCount() - 2); mStackScroller.changeViewPosition(mKeyguardIconOverflowContainer, mStackScroller.getChildCount() - 3); - mStackScroller.changeViewPosition(mEmptyShadeView, mStackScroller.getChildCount() - 2); - mStackScroller.changeViewPosition(mDismissView, mStackScroller.getChildCount() - 1); } private boolean shouldShowOnKeyguard(StatusBarNotification sbn) { @@ -1813,15 +1811,12 @@ public abstract class BaseStatusBar extends SystemUI implements setShowLockscreenNotifications(show && allowedByDpm); } - protected abstract void haltTicker(); protected abstract void setAreThereNotifications(); protected abstract void updateNotifications(); - protected abstract void tick(StatusBarNotification n, boolean firstTime); - protected abstract void updateExpandedViewPos(int expandedPosition); - protected abstract boolean shouldDisableNavbarGestures(); + public abstract boolean shouldDisableNavbarGestures(); public abstract void addNotification(StatusBarNotification notification, - RankingMap ranking); + RankingMap ranking, Entry oldEntry); protected abstract void updateNotificationRanking(RankingMap ranking); public abstract void removeNotification(String key, RankingMap ranking); @@ -1829,26 +1824,111 @@ public abstract class BaseStatusBar extends SystemUI implements if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")"); final String key = notification.getKey(); - boolean wasHeadsUp = isHeadsUp(key); - Entry oldEntry; - if (wasHeadsUp) { - oldEntry = mHeadsUpNotificationView.getEntry(); - } else { - oldEntry = mNotificationData.get(key); - } - if (oldEntry == null) { + Entry entry = mNotificationData.get(key); + if (entry == null) { return; } - final StatusBarNotification oldNotification = oldEntry.notification; + Notification n = notification.getNotification(); + if (DEBUG) { + logUpdate(entry, n); + } + boolean applyInPlace = shouldApplyInPlace(entry, n); + boolean shouldInterrupt = shouldInterrupt(entry); + boolean alertAgain = alertAgain(entry, n); + + entry.notification = notification; + mGroupManager.onEntryUpdated(entry, entry.notification); + + boolean updateSuccessful = false; + if (applyInPlace) { + if (DEBUG) Log.d(TAG, "reusing notification for key: " + key); + try { + if (entry.icon != null) { + // Update the icon + final StatusBarIcon ic = new StatusBarIcon( + notification.getUser(), + notification.getPackageName(), + n.getSmallIcon(), + n.iconLevel, + n.number, + n.tickerText); + entry.icon.setNotification(n); + if (!entry.icon.set(ic)) { + handleNotificationError(notification, "Couldn't update icon: " + ic); + return; + } + } + updateNotificationViews(entry, notification); + updateSuccessful = true; + } + catch (RuntimeException e) { + // It failed to apply cleanly. + Log.w(TAG, "Couldn't reapply views for package " + n.contentView.getPackage(), e); + } + } + if (!updateSuccessful) { + if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key); + final StatusBarIcon ic = new StatusBarIcon( + notification.getUser(), + notification.getPackageName(), + n.getSmallIcon(), + n.iconLevel, + n.number, + n.tickerText); + entry.icon.setNotification(n); + entry.icon.set(ic); + inflateViews(entry, mStackScroller); + } + updateHeadsUp(key, entry, shouldInterrupt, alertAgain); + mNotificationData.updateRanking(ranking); + updateNotifications(); + + // Update the veto button accordingly (and as a result, whether this row is + // swipe-dismissable) + updateNotificationVetoButton(entry.row, notification); + + if (DEBUG) { + // Is this for you? + boolean isForCurrentUser = isNotificationForCurrentProfiles(notification); + Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you"); + } + + setAreThereNotifications(); + } + + protected abstract void updateHeadsUp(String key, Entry entry, boolean shouldInterrupt, + boolean alertAgain); + private void logUpdate(Entry oldEntry, Notification n) { + StatusBarNotification oldNotification = oldEntry.notification; + Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when + + " ongoing=" + oldNotification.isOngoing() + + " expanded=" + oldEntry.getContentView() + + " contentView=" + oldNotification.getNotification().contentView + + " bigContentView=" + oldNotification.getNotification().bigContentView + + " publicView=" + oldNotification.getNotification().publicVersion + + " rowParent=" + oldEntry.row.getParent()); + Log.d(TAG, "new notification: when=" + n.when + + " ongoing=" + oldNotification.isOngoing() + + " contentView=" + n.contentView + + " bigContentView=" + n.bigContentView + + " publicView=" + n.publicVersion); + } + + /** + * @return whether we can just reapply the RemoteViews from a notification in-place when it is + * updated + */ + private boolean shouldApplyInPlace(Entry entry, Notification n) { + StatusBarNotification oldNotification = entry.notification; // XXX: modify when we do something more intelligent with the two content views final RemoteViews oldContentView = oldNotification.getNotification().contentView; - Notification n = notification.getNotification(); final RemoteViews contentView = n.contentView; final RemoteViews oldBigContentView = oldNotification.getNotification().bigContentView; final RemoteViews bigContentView = n.bigContentView; - final RemoteViews oldHeadsUpContentView = oldNotification.getNotification().headsUpContentView; + final RemoteViews oldHeadsUpContentView + = oldNotification.getNotification().headsUpContentView; final RemoteViews headsUpContentView = n.headsUpContentView; final Notification oldPublicNotification = oldNotification.getNotification().publicVersion; final RemoteViews oldPublicContentView = oldPublicNotification != null @@ -1856,34 +1936,15 @@ public abstract class BaseStatusBar extends SystemUI implements final Notification publicNotification = n.publicVersion; final RemoteViews publicContentView = publicNotification != null ? publicNotification.contentView : null; - - if (DEBUG) { - Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when - + " ongoing=" + oldNotification.isOngoing() - + " expanded=" + oldEntry.expanded - + " contentView=" + oldContentView - + " bigContentView=" + oldBigContentView - + " publicView=" + oldPublicContentView - + " rowParent=" + oldEntry.row.getParent()); - Log.d(TAG, "new notification: when=" + n.when - + " ongoing=" + oldNotification.isOngoing() - + " contentView=" + contentView - + " bigContentView=" + bigContentView - + " publicView=" + publicContentView); - } - - // Can we just reapply the RemoteViews in place? - - // 1U is never null - boolean contentsUnchanged = oldEntry.expanded != null + boolean contentsUnchanged = entry.getContentView() != null && contentView.getPackage() != null && oldContentView.getPackage() != null && oldContentView.getPackage().equals(contentView.getPackage()) && oldContentView.getLayoutId() == contentView.getLayoutId(); // large view may be null boolean bigContentsUnchanged = - (oldEntry.getBigContentView() == null && bigContentView == null) - || ((oldEntry.getBigContentView() != null && bigContentView != null) + (entry.getExpandedContentView() == null && bigContentView == null) + || ((entry.getExpandedContentView() != null && bigContentView != null) && bigContentView.getPackage() != null && oldBigContentView.getPackage() != null && oldBigContentView.getPackage().equals(bigContentView.getPackage()) @@ -1902,172 +1963,41 @@ public abstract class BaseStatusBar extends SystemUI implements && oldPublicContentView.getPackage() != null && oldPublicContentView.getPackage().equals(publicContentView.getPackage()) && oldPublicContentView.getLayoutId() == publicContentView.getLayoutId()); - boolean updateTicker = n.tickerText != null - && !TextUtils.equals(n.tickerText, - oldEntry.notification.getNotification().tickerText); - - final boolean shouldInterrupt = shouldInterrupt(notification); - final boolean alertAgain = alertAgain(oldEntry, n); - boolean updateSuccessful = false; - if (contentsUnchanged && bigContentsUnchanged && headsUpContentsUnchanged - && publicUnchanged) { - if (DEBUG) Log.d(TAG, "reusing notification for key: " + key); - oldEntry.notification = notification; - try { - if (oldEntry.icon != null) { - // Update the icon - final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(), - notification.getUser(), - n.icon, - n.iconLevel, - n.number, - n.tickerText); - oldEntry.icon.setNotification(n); - if (!oldEntry.icon.set(ic)) { - handleNotificationError(notification, "Couldn't update icon: " + ic); - return; - } - } - - if (wasHeadsUp) { - if (shouldInterrupt) { - updateHeadsUpViews(oldEntry, notification); - if (alertAgain) { - resetHeadsUpDecayTimer(); - } - } else { - // we updated the notification above, so release to build a new shade entry - mHeadsUpNotificationView.releaseAndClose(); - return; - } - } else { - if (shouldInterrupt && alertAgain) { - removeNotificationViews(key, ranking); - addNotification(notification, ranking); //this will pop the headsup - } else { - updateNotificationViews(oldEntry, notification); - } - } - mNotificationData.updateRanking(ranking); - updateNotifications(); - updateSuccessful = true; - } - catch (RuntimeException e) { - // It failed to add cleanly. Log, and remove the view from the panel. - Log.w(TAG, "Couldn't reapply views for package " + contentView.getPackage(), e); - } - } - if (!updateSuccessful) { - if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key); - if (wasHeadsUp) { - if (shouldInterrupt) { - if (DEBUG) Log.d(TAG, "rebuilding heads up for key: " + key); - Entry newEntry = new Entry(notification, null); - ViewGroup holder = mHeadsUpNotificationView.getHolder(); - if (inflateViewsForHeadsUp(newEntry, holder)) { - mHeadsUpNotificationView.showNotification(newEntry); - if (alertAgain) { - resetHeadsUpDecayTimer(); - } - } else { - Log.w(TAG, "Couldn't create new updated headsup for package " - + contentView.getPackage()); - } - } else { - if (DEBUG) Log.d(TAG, "releasing heads up for key: " + key); - oldEntry.notification = notification; - mHeadsUpNotificationView.releaseAndClose(); - return; - } - } else { - if (shouldInterrupt && alertAgain) { - if (DEBUG) Log.d(TAG, "reposting to invoke heads up for key: " + key); - removeNotificationViews(key, ranking); - addNotification(notification, ranking); //this will pop the headsup - } else { - if (DEBUG) Log.d(TAG, "rebuilding update in place for key: " + key); - oldEntry.notification = notification; - final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(), - notification.getUser(), - n.icon, - n.iconLevel, - n.number, - n.tickerText); - oldEntry.icon.setNotification(n); - oldEntry.icon.set(ic); - inflateViews(oldEntry, mStackScroller, wasHeadsUp); - mNotificationData.updateRanking(ranking); - updateNotifications(); - } - } - } - - // Update the veto button accordingly (and as a result, whether this row is - // swipe-dismissable) - updateNotificationVetoButton(oldEntry.row, notification); - - // Is this for you? - boolean isForCurrentUser = isNotificationForCurrentProfiles(notification); - if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you"); - - // Restart the ticker if it's still running - if (updateTicker && isForCurrentUser) { - haltTicker(); - tick(notification, false); - } - - // Recalculate the position of the sliding windows and the titles. - setAreThereNotifications(); - updateExpandedViewPos(EXPANDED_LEAVE_ALONE); + return contentsUnchanged && bigContentsUnchanged && headsUpContentsUnchanged + && publicUnchanged; } - 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) { + private void updateNotificationViews(Entry entry, StatusBarNotification notification) { final RemoteViews contentView = notification.getNotification().contentView; - final RemoteViews bigContentView = isHeadsUp - ? notification.getNotification().headsUpContentView - : notification.getNotification().bigContentView; + final RemoteViews bigContentView = notification.getNotification().bigContentView; + final RemoteViews headsUpContentView = notification.getNotification().headsUpContentView; 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(), + contentView.reapply(mContext, entry.getContentView(), mOnClickHandler); + if (bigContentView != null && entry.getExpandedContentView() != null) { + bigContentView.reapply(mContext, entry.getExpandedContentView(), mOnClickHandler); } + View headsUpChild = entry.getHeadsUpContentView(); + if (headsUpContentView != null && headsUpChild != null) { + headsUpContentView.reapply(mContext, headsUpChild, 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.getKey(), - isHeadsUp); - entry.row.setOnClickListener(listener); - } else { - entry.row.setOnClickListener(null); - } + mNotificationClicker.register(entry.row, notification); + entry.row.setStatusBarNotification(notification); entry.row.notifyContentUpdated(); entry.row.resetHeight(); } - protected void notifyHeadsUpScreenOn(boolean screenOn) { - if (!screenOn) { - scheduleHeadsUpEscalation(); - } + protected void notifyHeadsUpScreenOff() { + maybeEscalateHeadsUp(); } private boolean alertAgain(Entry oldEntry, Notification newNotification) { @@ -2075,7 +2005,8 @@ public abstract class BaseStatusBar extends SystemUI implements || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; } - protected boolean shouldInterrupt(StatusBarNotification sbn) { + protected boolean shouldInterrupt(Entry entry) { + StatusBarNotification sbn = entry.notification; if (mNotificationData.shouldFilterOut(sbn)) { if (DEBUG) { Log.d(TAG, "Skipping HUN check for " + sbn.getKey() + " since it's filtered out."); @@ -2083,7 +2014,7 @@ public abstract class BaseStatusBar extends SystemUI implements return false; } - if (mHeadsUpNotificationView.isSnoozed(sbn.getPackageName())) { + if (isSnoozedPackage(sbn)) { return false; } @@ -2100,10 +2031,12 @@ public abstract class BaseStatusBar extends SystemUI implements Notification.HEADS_UP_ALLOWED) != Notification.HEADS_UP_NEVER; boolean accessibilityForcesLaunch = isFullscreen && mAccessibilityManager.isTouchExplorationEnabled(); + boolean justLaunchedFullScreenIntent = entry.hasJustLaunchedFullScreenIntent(); boolean interrupt = (isFullscreen || (isHighPriority && (isNoisy || hasTicker))) && isAllowed && !accessibilityForcesLaunch + && !justLaunchedFullScreenIntent && mPowerManager.isScreenOn() && (!mStatusBarKeyguardViewManager.isShowing() || mStatusBarKeyguardViewManager.isOccluded()) @@ -2117,6 +2050,8 @@ public abstract class BaseStatusBar extends SystemUI implements return interrupt; } + protected abstract boolean isSnoozedPackage(StatusBarNotification sbn); + public void setInteracting(int barWindow, boolean interacting) { // hook for subclasses } @@ -2133,9 +2068,6 @@ public abstract class BaseStatusBar extends SystemUI implements } public void destroy() { - if (mSearchPanelView != null) { - mWindowManager.removeViewImmediate(mSearchPanelView); - } mContext.unregisterReceiver(mBroadcastReceiver); try { mNotificationListener.unregisterAsSystemService(); @@ -2186,4 +2118,11 @@ public abstract class BaseStatusBar extends SystemUI implements } return mStatusBarKeyguardViewManager.isSecure(); } + + @Override + public void showAssistDisclosure() { + if (mAssistManager != null) { + mAssistManager.showDisclosure(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 0b1b883..0deff08 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar; import android.os.Handler; import android.os.IBinder; import android.os.Message; +import android.util.Pair; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.StatusBarIcon; @@ -57,6 +58,10 @@ public class CommandQueue extends IStatusBar.Stub { private static final int MSG_NOTIFICATION_LIGHT_OFF = 16 << MSG_SHIFT; private static final int MSG_NOTIFICATION_LIGHT_PULSE = 17 << MSG_SHIFT; private static final int MSG_SHOW_SCREEN_PIN_REQUEST = 18 << MSG_SHIFT; + private static final int MSG_APP_TRANSITION_PENDING = 19 << MSG_SHIFT; + private static final int MSG_APP_TRANSITION_CANCELLED = 20 << MSG_SHIFT; + private static final int MSG_APP_TRANSITION_STARTING = 21 << MSG_SHIFT; + private static final int MSG_ASSIST_DISCLOSURE = 22 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -79,7 +84,7 @@ public class CommandQueue extends IStatusBar.Stub { public void updateIcon(String slot, int index, int viewIndex, StatusBarIcon old, StatusBarIcon icon); public void removeIcon(String slot, int index, int viewIndex); - public void disable(int state, boolean animate); + public void disable(int state1, int state2, boolean animate); public void animateExpandNotificationsPanel(); public void animateCollapsePanels(int flags); public void animateExpandSettingsPanel(); @@ -92,13 +97,15 @@ public class CommandQueue extends IStatusBar.Stub { public void toggleRecentApps(); public void preloadRecentApps(); public void cancelPreloadRecentApps(); - public void showSearchPanel(); - public void hideSearchPanel(); public void setWindowState(int window, int state); public void buzzBeepBlinked(); public void notificationLightOff(); public void notificationLightPulse(int argb, int onMillis, int offMillis); public void showScreenPinningRequest(); + public void appTransitionPending(); + public void appTransitionCancelled(); + public void appTransitionStarting(long startTime, long duration); + public void showAssistDisclosure(); } public CommandQueue(Callbacks callbacks, StatusBarIconList list) { @@ -122,10 +129,10 @@ public class CommandQueue extends IStatusBar.Stub { } } - public void disable(int state) { + public void disable(int state1, int state2) { synchronized (mList) { mHandler.removeMessages(MSG_DISABLE); - mHandler.obtainMessage(MSG_DISABLE, state, 0, null).sendToTarget(); + mHandler.obtainMessage(MSG_DISABLE, state1, state2, null).sendToTarget(); } } @@ -152,7 +159,8 @@ public class CommandQueue extends IStatusBar.Stub { public void setSystemUiVisibility(int vis, int mask) { synchronized (mList) { - mHandler.removeMessages(MSG_SET_SYSTEMUI_VISIBILITY); + // Don't coalesce these, since it might have one time flags set such as + // STATUS_BAR_UNHIDE which might get lost. mHandler.obtainMessage(MSG_SET_SYSTEMUI_VISIBILITY, vis, mask, null).sendToTarget(); } } @@ -246,6 +254,35 @@ public class CommandQueue extends IStatusBar.Stub { } } + public void appTransitionPending() { + synchronized (mList) { + mHandler.removeMessages(MSG_APP_TRANSITION_PENDING); + mHandler.sendEmptyMessage(MSG_APP_TRANSITION_PENDING); + } + } + + public void appTransitionCancelled() { + synchronized (mList) { + mHandler.removeMessages(MSG_APP_TRANSITION_PENDING); + mHandler.sendEmptyMessage(MSG_APP_TRANSITION_PENDING); + } + } + + public void appTransitionStarting(long startTime, long duration) { + synchronized (mList) { + mHandler.removeMessages(MSG_APP_TRANSITION_STARTING); + mHandler.obtainMessage(MSG_APP_TRANSITION_STARTING, Pair.create(startTime, duration)) + .sendToTarget(); + } + } + + public void showAssistDisclosure() { + synchronized (mList) { + mHandler.removeMessages(MSG_ASSIST_DISCLOSURE); + mHandler.obtainMessage(MSG_ASSIST_DISCLOSURE).sendToTarget(); + } + } + private final class H extends Handler { public void handleMessage(Message msg) { final int what = msg.what & MSG_MASK; @@ -277,7 +314,7 @@ public class CommandQueue extends IStatusBar.Stub { break; } case MSG_DISABLE: - mCallbacks.disable(msg.arg1, true /* animate */); + mCallbacks.disable(msg.arg1, msg.arg2, true /* animate */); break; case MSG_EXPAND_NOTIFICATIONS: mCallbacks.animateExpandNotificationsPanel(); @@ -328,6 +365,19 @@ public class CommandQueue extends IStatusBar.Stub { case MSG_SHOW_SCREEN_PIN_REQUEST: mCallbacks.showScreenPinningRequest(); break; + case MSG_APP_TRANSITION_PENDING: + mCallbacks.appTransitionPending(); + break; + case MSG_APP_TRANSITION_CANCELLED: + mCallbacks.appTransitionCancelled(); + break; + case MSG_APP_TRANSITION_STARTING: + Pair<Long, Long> data = (Pair<Long, Long>) msg.obj; + mCallbacks.appTransitionStarting(data.first, data.second); + break; + case MSG_ASSIST_DISCLOSURE: + mCallbacks.showAssistDisclosure(); + break; } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DelegateViewHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/DelegateViewHelper.java deleted file mode 100644 index 7ae6764..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/DelegateViewHelper.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar; - -import android.app.StatusBarManager; -import android.content.res.Resources; -import android.graphics.RectF; -import android.view.MotionEvent; -import android.view.View; -import com.android.systemui.R; - -public class DelegateViewHelper { - private View mDelegateView; - private View mSourceView; - private BaseStatusBar mBar; - private int[] mTempPoint = new int[2]; - private float[] mDownPoint = new float[2]; - private float mTriggerThreshhold; - private boolean mPanelShowing; - - RectF mInitialTouch = new RectF(); - private boolean mStarted; - private boolean mSwapXY = false; - private boolean mDisabled; - - public DelegateViewHelper(View sourceView) { - setSourceView(sourceView); - } - - public void setDelegateView(View view) { - mDelegateView = view; - } - - public void setBar(BaseStatusBar phoneStatusBar) { - mBar = phoneStatusBar; - } - - public boolean onInterceptTouchEvent(MotionEvent event) { - if (mSourceView == null || mDelegateView == null || mBar.shouldDisableNavbarGestures()) { - return false; - } - - mSourceView.getLocationOnScreen(mTempPoint); - final float sourceX = mTempPoint[0]; - final float sourceY = mTempPoint[1]; - - final int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_DOWN: - mPanelShowing = mDelegateView.getVisibility() == View.VISIBLE; - mDownPoint[0] = event.getX(); - mDownPoint[1] = event.getY(); - mStarted = mInitialTouch.contains(mDownPoint[0] + sourceX, mDownPoint[1] + sourceY); - break; - } - - if (!mStarted) { - return false; - } - - if (!mDisabled && !mPanelShowing && action == MotionEvent.ACTION_MOVE) { - final int historySize = event.getHistorySize(); - for (int k = 0; k < historySize + 1; k++) { - float x = k < historySize ? event.getHistoricalX(k) : event.getX(); - float y = k < historySize ? event.getHistoricalY(k) : event.getY(); - final float distance = mSwapXY ? (mDownPoint[0] - x) : (mDownPoint[1] - y); - if (distance > mTriggerThreshhold) { - mBar.showSearchPanel(); - mPanelShowing = true; - break; - } - } - } - - if (action == MotionEvent.ACTION_DOWN) { - mBar.setInteracting(StatusBarManager.WINDOW_NAVIGATION_BAR, true); - } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - mBar.setInteracting(StatusBarManager.WINDOW_NAVIGATION_BAR, false); - } - - mDelegateView.getLocationOnScreen(mTempPoint); - final float delegateX = mTempPoint[0]; - final float delegateY = mTempPoint[1]; - - float deltaX = sourceX - delegateX; - float deltaY = sourceY - delegateY; - event.offsetLocation(deltaX, deltaY); - mDelegateView.dispatchTouchEvent(event); - event.offsetLocation(-deltaX, -deltaY); - return mPanelShowing; - } - - public void setSourceView(View view) { - mSourceView = view; - if (mSourceView != null) { - Resources r = mSourceView.getContext().getResources(); - mTriggerThreshhold = r.getDimensionPixelSize(R.dimen.navigation_bar_min_swipe_distance); - } - } - - /** - * Selects the initial touch region based on a list of views. This is meant to be called by - * a container widget on children over which the initial touch should be detected. Note this - * will compute a minimum bound that contains all specified views. - * - * @param views - */ - public void setInitialTouchRegion(View ... views) { - RectF bounds = new RectF(); - int p[] = new int[2]; - for (int i = 0; i < views.length; i++) { - View view = views[i]; - if (view == null) continue; - view.getLocationOnScreen(p); - if (i == 0) { - bounds.set(p[0], p[1], p[0] + view.getWidth(), p[1] + view.getHeight()); - } else { - bounds.union(p[0], p[1], p[0] + view.getWidth(), p[1] + view.getHeight()); - } - } - mInitialTouch.set(bounds); - } - - /** - * When rotation is set to NO_SENSOR, then this allows swapping x/y for gesture detection - * @param swap - */ - public void setSwapXY(boolean swap) { - mSwapXY = swap; - } - - public void setDisabled(boolean disabled) { - mDisabled = disabled; - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java index f2a5673..a323684 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java @@ -21,12 +21,9 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; -import android.os.SystemClock; import android.util.AttributeSet; -import android.view.Choreographer; import android.view.View; import android.view.ViewGroup; -import android.view.ViewRootImpl; import android.widget.Button; import com.android.systemui.R; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; @@ -51,15 +48,14 @@ public class DismissViewButton extends Button { public DismissViewButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mAnimatedDismissDrawable = (AnimatedVectorDrawable) getContext().getResources().getDrawable( + mAnimatedDismissDrawable = (AnimatedVectorDrawable) getContext().getDrawable( R.drawable.dismiss_all_shape_animation).mutate(); mAnimatedDismissDrawable.setCallback(this); mAnimatedDismissDrawable.setBounds(0, 0, mAnimatedDismissDrawable.getIntrinsicWidth(), mAnimatedDismissDrawable.getIntrinsicHeight()); - mStaticDismissDrawable = getContext().getResources().getDrawable( - R.drawable.dismiss_all_shape); + mStaticDismissDrawable = getContext().getDrawable(R.drawable.dismiss_all_shape); mStaticDismissDrawable.setBounds(0, 0, mStaticDismissDrawable.getIntrinsicWidth(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java index c9f0260..15a092c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java @@ -162,20 +162,20 @@ public class DragDownHelper implements Gefingerpoken { ? RUBBERBAND_FACTOR_EXPANDABLE : RUBBERBAND_FACTOR_STATIC; float rubberband = heightDelta * rubberbandFactor; - if (expandable && (rubberband + child.getMinHeight()) > child.getMaxHeight()) { - float overshoot = (rubberband + child.getMinHeight()) - child.getMaxHeight(); + if (expandable && (rubberband + child.getMinHeight()) > child.getMaxContentHeight()) { + float overshoot = (rubberband + child.getMinHeight()) - child.getMaxContentHeight(); overshoot *= (1 - RUBBERBAND_FACTOR_STATIC); rubberband -= overshoot; } - child.setActualHeight((int) (child.getMinHeight() + rubberband)); + child.setContentHeight((int) (child.getMinHeight() + rubberband)); } private void cancelExpansion(final ExpandableView child) { - if (child.getActualHeight() == child.getMinHeight()) { + if (child.getContentHeight() == child.getMinHeight()) { return; } - ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight", - child.getActualHeight(), child.getMinHeight()); + ObjectAnimator anim = ObjectAnimator.ofInt(child, "contentHeight", + child.getContentHeight(), child.getMinHeight()); anim.setInterpolator(mInterpolator); anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS); anim.addListener(new AnimatorListenerAdapter() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java index 0825aa3..5db0699 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java @@ -20,11 +20,9 @@ import android.content.Context; import android.content.res.Configuration; import android.util.AttributeSet; import android.view.View; -import android.view.animation.Interpolator; import android.widget.TextView; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.PhoneStatusBar; public class EmptyShadeView extends StackScrollerDecorView { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index 8ad8406..b88e5ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -16,9 +16,13 @@ package com.android.systemui.statusbar; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.content.Context; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.service.notification.StatusBarNotification; import android.util.AttributeSet; @@ -26,12 +30,25 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewStub; import android.view.accessibility.AccessibilityEvent; +import android.view.animation.LinearInterpolator; import android.widget.ImageView; + import com.android.systemui.R; +import com.android.systemui.statusbar.phone.NotificationGroupManager; +import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.statusbar.stack.NotificationChildrenContainer; +import com.android.systemui.statusbar.stack.StackScrollState; +import com.android.systemui.statusbar.stack.StackStateAnimator; +import com.android.systemui.statusbar.stack.StackViewState; + +import java.util.List; public class ExpandableNotificationRow extends ActivatableNotificationView { + + private static final int DEFAULT_DIVIDER_ALPHA = 0x29; + private static final int COLORED_DIVIDER_ALPHA = 0x7B; + private final LinearInterpolator mLinearInterpolator = new LinearInterpolator(); private int mRowMinHeight; - private int mRowMaxHeight; /** Does this row contain layouts that can adapt to row expansion */ private boolean mExpandable; @@ -45,7 +62,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { private boolean mShowingPublic; private boolean mSensitive; private boolean mShowingPublicInitialized; - private boolean mShowingPublicForIntrinsicHeight; + private boolean mHideSensitiveForIntrinsicHeight; /** * Is this notification expanded by the system. The expansion state can be overridden by the @@ -61,15 +78,45 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { private NotificationContentView mPublicLayout; private NotificationContentView mPrivateLayout; private int mMaxExpandHeight; + private int mHeadsUpHeight; private View mVetoButton; private boolean mClearable; private ExpansionLogger mLogger; private String mLoggingKey; private boolean mWasReset; private NotificationGuts mGuts; - private StatusBarNotification mStatusBarNotification; private boolean mIsHeadsUp; + private View mExpandButton; + private View mExpandButtonDivider; + private ViewStub mExpandButtonStub; + private ViewStub mChildrenContainerStub; + private NotificationGroupManager mGroupManager; + private View mExpandButtonContainer; + private boolean mChildrenExpanded; + private NotificationChildrenContainer mChildrenContainer; + private ValueAnimator mChildExpandAnimator; + private float mChildrenExpandProgress; + private float mExpandButtonStart; + private ViewStub mGutsStub; + private boolean mHasExpandAction; + private boolean mIsSystemChildExpanded; + private boolean mIsPinned; + private OnClickListener mExpandClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + mGroupManager.setGroupExpanded(mStatusBarNotification, + !mChildrenExpanded); + } + }; + + public NotificationContentView getPrivateLayout() { + return mPrivateLayout; + } + + public NotificationContentView getPublicLayout() { + return mPublicLayout; + } public void setIconAnimationRunning(boolean running) { setIconAnimationRunning(running, mPublicLayout); @@ -80,8 +127,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { if (layout != null) { View contractedChild = layout.getContractedChild(); View expandedChild = layout.getExpandedChild(); + View headsUpChild = layout.getHeadsUpChild(); setIconAnimationRunningForChild(running, contractedChild); setIconAnimationRunningForChild(running, expandedChild); + setIconAnimationRunningForChild(running, headsUpChild); } } @@ -119,14 +168,137 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { public void setStatusBarNotification(StatusBarNotification statusBarNotification) { mStatusBarNotification = statusBarNotification; updateVetoButton(); + updateExpandButton(); } public StatusBarNotification getStatusBarNotification() { return mStatusBarNotification; } + public boolean isHeadsUp() { + return mIsHeadsUp; + } + public void setHeadsUp(boolean isHeadsUp) { + int intrinsicBefore = getIntrinsicHeight(); mIsHeadsUp = isHeadsUp; + mPrivateLayout.setHeadsUp(isHeadsUp); + if (intrinsicBefore != getIntrinsicHeight()) { + notifyHeightChanged(false /* needsAnimation */); + } + } + + public void setGroupManager(NotificationGroupManager groupManager) { + mGroupManager = groupManager; + } + + public void addChildNotification(ExpandableNotificationRow row) { + addChildNotification(row, -1); + } + + /** + * Add a child notification to this view. + * + * @param row the row to add + * @param childIndex the index to add it at, if -1 it will be added at the end + */ + public void addChildNotification(ExpandableNotificationRow row, int childIndex) { + if (mChildrenContainer == null) { + mChildrenContainerStub.inflate(); + } + mChildrenContainer.addNotification(row, childIndex); + } + + public void removeChildNotification(ExpandableNotificationRow row) { + if (mChildrenContainer != null) { + mChildrenContainer.removeNotification(row); + } + } + + @Override + public boolean areChildrenExpanded() { + return mChildrenExpanded; + } + + public List<ExpandableNotificationRow> getNotificationChildren() { + return mChildrenContainer == null ? null : mChildrenContainer.getNotificationChildren(); + } + + /** + * Apply the order given in the list to the children. + * + * @param childOrder the new list order + * @return whether the list order has changed + */ + public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) { + return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder); + } + + public void getChildrenStates(StackScrollState resultState) { + if (mChildrenExpanded) { + StackViewState parentState = resultState.getViewStateForView(this); + mChildrenContainer.getState(resultState, parentState); + } + } + + public void applyChildrenState(StackScrollState state) { + if (mChildrenExpanded) { + mChildrenContainer.applyState(state); + } + } + + public void prepareExpansionChanged(StackScrollState state) { + if (mChildrenExpanded) { + mChildrenContainer.prepareExpansionChanged(state); + } + } + + public void startChildAnimation(StackScrollState finalState, + StackStateAnimator stateAnimator, boolean withDelays, long delay, long duration) { + if (mChildrenExpanded) { + mChildrenContainer.startAnimationToState(finalState, stateAnimator, withDelays, delay, + duration); + } + } + + public ExpandableNotificationRow getViewAtPosition(float y) { + if (!mChildrenExpanded) { + return this; + } else { + ExpandableNotificationRow view = mChildrenContainer.getViewAtPosition(y); + return view == null ? this : view; + } + } + + public NotificationGuts getGuts() { + return mGuts; + } + + protected int calculateContentHeightFromActualHeight(int actualHeight) { + int realActualHeight = actualHeight; + if (hasBottomDecor()) { + realActualHeight -= getBottomDecorHeight(); + } + realActualHeight = Math.max(getMinHeight(), realActualHeight); + return realActualHeight; + } + + /** + * Set this notification to be pinned to the top if {@link #isHeadsUp()} is true. By doing this + * the notification will be rendered on top of the screen. + * + * @param pinned whether it is pinned + */ + public void setPinned(boolean pinned) { + mIsPinned = pinned; + } + + public boolean isPinned() { + return mIsPinned; + } + + public int getHeadsUpHeight() { + return mHeadsUpHeight; } public interface ExpansionLogger { @@ -145,7 +317,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { super.reset(); mRowMinHeight = 0; final boolean wasExpanded = isExpanded(); - mRowMaxHeight = 0; + mMaxViewHeight = 0; mExpandable = false; mHasUserChangedExpansion = false; mUserLocked = false; @@ -165,6 +337,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { resetActualHeight(); } mMaxExpandHeight = 0; + mHeadsUpHeight = 0; mWasReset = true; onHeightReset(); requestLayout(); @@ -180,21 +353,97 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { super.onFinishInflate(); mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic); mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded); - ViewStub gutsStub = (ViewStub) findViewById(R.id.notification_guts_stub); - gutsStub.setOnInflateListener(new ViewStub.OnInflateListener() { + mGutsStub = (ViewStub) findViewById(R.id.notification_guts_stub); + mGutsStub.setOnInflateListener(new ViewStub.OnInflateListener() { @Override public void onInflate(ViewStub stub, View inflated) { mGuts = (NotificationGuts) inflated; mGuts.setClipTopAmount(getClipTopAmount()); mGuts.setActualHeight(getActualHeight()); + mGutsStub = null; + } + }); + mExpandButtonStub = (ViewStub) findViewById(R.id.more_button_stub); + mExpandButtonStub.setOnInflateListener(new ViewStub.OnInflateListener() { + + @Override + public void onInflate(ViewStub stub, View inflated) { + mExpandButtonContainer = inflated; + mExpandButton = inflated.findViewById(R.id.notification_expand_button); + mExpandButtonDivider = inflated.findViewById(R.id.notification_expand_divider); + mExpandButtonContainer.setOnClickListener(mExpandClickListener); + } + }); + mChildrenContainerStub = (ViewStub) findViewById(R.id.child_container_stub); + mChildrenContainerStub.setOnInflateListener(new ViewStub.OnInflateListener() { + + @Override + public void onInflate(ViewStub stub, View inflated) { + mChildrenContainer = (NotificationChildrenContainer) inflated; + mChildrenContainer.setCollapseClickListener(mExpandClickListener); + updateChildrenVisibility(false); } }); mVetoButton = findViewById(R.id.veto); } + public void inflateGuts() { + if (mGuts == null) { + mGutsStub.inflate(); + } + } + + private void updateChildrenVisibility(boolean animated) { + if (mChildrenContainer == null) { + return; + } + if (mChildExpandAnimator != null) { + mChildExpandAnimator.cancel(); + } + float targetProgress = mChildrenExpanded ? 1.0f : 0.0f; + if (animated) { + if (mChildrenExpanded) { + mChildrenContainer.setVisibility(VISIBLE); + } + mExpandButtonStart = mExpandButtonContainer.getTranslationY(); + mChildExpandAnimator = ValueAnimator.ofFloat(mChildrenExpandProgress, targetProgress); + mChildExpandAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setChildrenExpandProgress((float) animation.getAnimatedValue()); + } + }); + mChildExpandAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mChildExpandAnimator = null; + if (!mChildrenExpanded) { + mChildrenContainer.setVisibility(INVISIBLE); + } + } + }); + mChildExpandAnimator.setInterpolator(mLinearInterpolator); + mChildExpandAnimator.setDuration( + StackStateAnimator.ANIMATION_DURATION_EXPAND_CLICKED); + mChildExpandAnimator.start(); + } else { + setChildrenExpandProgress(targetProgress); + mChildrenContainer.setVisibility(mChildrenExpanded ? VISIBLE : INVISIBLE); + } + } + + private void setChildrenExpandProgress(float progress) { + mChildrenExpandProgress = progress; + updateExpandButtonAppearance(); + NotificationContentView showingLayout = getShowingLayout(); + float alpha = 1.0f - mChildrenExpandProgress; + alpha = PhoneStatusBar.ALPHA_OUT.getInterpolation(alpha); + showingLayout.setAlpha(alpha); + } + @Override - public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { - if (super.onRequestSendAccessibilityEvent(child, event)) { + public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) { + if (super.onRequestSendAccessibilityEventInternal(child, event)) { // Add a record for the entire layout since its content is somehow small. // The event comes from a leaf view that is interacted with. AccessibilityEvent record = AccessibilityEvent.obtain(); @@ -217,7 +466,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { public void setHeightRange(int rowMinHeight, int rowMaxHeight) { mRowMinHeight = rowMinHeight; - mRowMaxHeight = rowMaxHeight; + mMaxViewHeight = rowMaxHeight; } public boolean isExpandable() { @@ -281,7 +530,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { if (expand != mIsSystemExpanded) { final boolean wasExpanded = isExpanded(); mIsSystemExpanded = expand; - notifyHeightChanged(); + notifyHeightChanged(false /* needsAnimation */); logExpansionEvent(false, wasExpanded); } } @@ -295,7 +544,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { mExpansionDisabled = expansionDisabled; logExpansionEvent(false, wasExpanded); if (wasExpanded != isExpanded()) { - notifyHeightChanged(); + notifyHeightChanged(false /* needsAnimation */); } } } @@ -313,9 +562,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { public void applyExpansionToLayout() { boolean expand = isExpanded(); if (expand && mExpandable) { - setActualHeight(mMaxExpandHeight); + setContentHeight(mMaxExpandHeight); } else { - setActualHeight(mRowMinHeight); + setContentHeight(mRowMinHeight); } } @@ -325,12 +574,34 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { return getActualHeight(); } boolean inExpansionState = isExpanded(); - if (!inExpansionState) { - // not expanded, so we return the collapsed size + int maxContentHeight; + if (mSensitive && mHideSensitiveForIntrinsicHeight) { return mRowMinHeight; + } else if (mIsHeadsUp) { + if (inExpansionState) { + maxContentHeight = Math.max(mMaxExpandHeight, mHeadsUpHeight); + } else { + maxContentHeight = Math.max(mRowMinHeight, mHeadsUpHeight); + } + } else if ((!inExpansionState && !mChildrenExpanded)) { + maxContentHeight = mRowMinHeight; + } else if (mChildrenExpanded) { + maxContentHeight = mChildrenContainer.getIntrinsicHeight(); + } else { + maxContentHeight = getMaxExpandHeight(); } + return maxContentHeight + getBottomDecorHeight(); + } - return mShowingPublicForIntrinsicHeight ? mRowMinHeight : getMaxExpandHeight(); + @Override + protected boolean hasBottomDecor() { + return BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS + && !mIsHeadsUp && mGroupManager.hasGroupChildren(mStatusBarNotification); + } + + @Override + protected boolean canHaveBottomDecor() { + return BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS && !mIsHeadsUp; } /** @@ -343,25 +614,52 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { */ private boolean isExpanded() { return !mExpansionDisabled - && (!hasUserChangedExpansion() && isSystemExpanded() || isUserExpanded()); + && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded()) + || isUserExpanded()); + } + + private boolean isSystemChildExpanded() { + return mIsSystemChildExpanded; + } + + public void setSystemChildExpanded(boolean expanded) { + mIsSystemChildExpanded = expanded; } @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 && !mWasReset; - updateMaxExpandHeight(); + updateMaxHeights(); if (updateExpandHeight) { applyExpansionToLayout(); } mWasReset = false; } - private void updateMaxExpandHeight() { + @Override + protected boolean isChildInvisible(View child) { + + // We don't want to layout the ChildrenContainer if this is a heads-up view, otherwise the + // view will get too high and the shadows will be off. + boolean isInvisibleChildContainer = child == mChildrenContainer && mIsHeadsUp; + return super.isChildInvisible(child) || isInvisibleChildContainer; + } + + private void updateMaxHeights() { int intrinsicBefore = getIntrinsicHeight(); - mMaxExpandHeight = mPrivateLayout.getMaxHeight(); + View expandedChild = mPrivateLayout.getExpandedChild(); + if (expandedChild == null) { + expandedChild = mPrivateLayout.getContractedChild(); + } + mMaxExpandHeight = expandedChild.getHeight(); + View headsUpChild = mPrivateLayout.getHeadsUpChild(); + if (headsUpChild == null) { + headsUpChild = mPrivateLayout.getContractedChild(); + } + mHeadsUpHeight = headsUpChild.getHeight(); if (intrinsicBefore != getIntrinsicHeight()) { - notifyHeightChanged(); + notifyHeightChanged(false /* needsAnimation */); } } @@ -370,7 +668,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { } public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { - mShowingPublicForIntrinsicHeight = mSensitive && hideSensitive; + mHideSensitiveForIntrinsicHeight = hideSensitive; } public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, @@ -428,8 +726,127 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { mVetoButton.setVisibility(isClearable() && !mShowingPublic ? View.VISIBLE : View.GONE); } + public void setChildrenExpanded(boolean expanded, boolean animate) { + mChildrenExpanded = expanded; + updateChildrenVisibility(animate); + } + + public void updateExpandButton() { + boolean hasExpand = hasBottomDecor(); + if (hasExpand != mHasExpandAction) { + if (hasExpand) { + if (mExpandButtonContainer == null) { + mExpandButtonStub.inflate(); + } + mExpandButtonContainer.setVisibility(View.VISIBLE); + updateExpandButtonAppearance(); + updateExpandButtonColor(); + } else if (mExpandButtonContainer != null) { + mExpandButtonContainer.setVisibility(View.GONE); + } + notifyHeightChanged(true /* needsAnimation */); + } + mHasExpandAction = hasExpand; + } + + private void updateExpandButtonAppearance() { + if (mExpandButtonContainer == null) { + return; + } + float expandButtonAlpha = 0.0f; + float expandButtonTranslation = 0.0f; + float containerTranslation = 0.0f; + int minHeight = getMinHeight(); + if (!mChildrenExpanded || mChildExpandAnimator != null) { + int expandActionHeight = getBottomDecorHeight(); + int translationY = getActualHeight() - expandActionHeight; + if (translationY > minHeight) { + containerTranslation = translationY; + expandButtonAlpha = 1.0f; + expandButtonTranslation = 0.0f; + } else { + containerTranslation = minHeight; + float progress = expandActionHeight != 0 + ? (minHeight - translationY) / (float) expandActionHeight + : 1.0f; + expandButtonTranslation = -progress * expandActionHeight * 0.7f; + float alphaProgress = Math.min(progress / 0.7f, 1.0f); + alphaProgress = PhoneStatusBar.ALPHA_OUT.getInterpolation(alphaProgress); + expandButtonAlpha = 1.0f - alphaProgress; + } + } + if (mChildExpandAnimator != null || mChildrenExpanded) { + expandButtonAlpha = (1.0f - mChildrenExpandProgress) + * expandButtonAlpha; + expandButtonTranslation = (1.0f - mChildrenExpandProgress) + * expandButtonTranslation; + float newTranslation = -getBottomDecorHeight(); + + // We don't want to take the actual height of the view as this is already + // interpolated by a custom interpolator leading to a confusing animation. We want + // to have a stable end value to interpolate in between + float collapsedHeight = !mChildrenExpanded + ? Math.max(StackStateAnimator.getFinalActualHeight(this) + - getBottomDecorHeight(), minHeight) + : mExpandButtonStart; + float translationProgress = mFastOutSlowInInterpolator.getInterpolation( + mChildrenExpandProgress); + containerTranslation = (1.0f - translationProgress) * collapsedHeight + + translationProgress * newTranslation; + } + mExpandButton.setAlpha(expandButtonAlpha); + mExpandButtonDivider.setAlpha(expandButtonAlpha); + mExpandButton.setTranslationY(expandButtonTranslation); + mExpandButtonContainer.setTranslationY(containerTranslation); + NotificationContentView showingLayout = getShowingLayout(); + float layoutTranslation = + mExpandButtonContainer.getTranslationY() - showingLayout.getContentHeight(); + layoutTranslation = Math.min(layoutTranslation, 0); + if (!mChildrenExpanded && mChildExpandAnimator == null) { + // Needed for the DragDownHelper in order not to jump there, as the position + // can be negative for a short time. + layoutTranslation = 0; + } + showingLayout.setTranslationY(layoutTranslation); + if (mChildrenContainer != null) { + mChildrenContainer.setTranslationY( + mExpandButtonContainer.getTranslationY() + getBottomDecorHeight()); + } + } + + private void updateExpandButtonColor() { + // TODO: This needs some more baking, currently only the divider is colored according to + // the tint, but legacy black doesn't work yet perfectly for the button etc. + int color = getRippleColor(); + if (color == mNormalRippleColor) { + color = 0; + } + if (mExpandButtonDivider != null) { + applyTint(mExpandButtonDivider, color); + } + if (mChildrenContainer != null) { + mChildrenContainer.setTintColor(color); + } + } + + public static void applyTint(View v, int color) { + int alpha; + if (color != 0) { + alpha = COLORED_DIVIDER_ALPHA; + } else { + color = 0xff000000; + alpha = DEFAULT_DIVIDER_ALPHA; + } + if (v.getBackground() instanceof ColorDrawable) { + ColorDrawable background = (ColorDrawable) v.getBackground(); + background.mutate(); + background.setColor(color); + background.setAlpha(alpha); + } + } + public int getMaxExpandHeight() { - return mShowingPublicForIntrinsicHeight ? mRowMinHeight : mMaxExpandHeight; + return mMaxExpandHeight; } @Override @@ -439,18 +856,25 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { } @Override + protected View getContentView() { + return getShowingLayout(); + } + + @Override public void setActualHeight(int height, boolean notifyListeners) { - mPrivateLayout.setActualHeight(height); - mPublicLayout.setActualHeight(height); + super.setActualHeight(height, notifyListeners); + int contentHeight = calculateContentHeightFromActualHeight(height); + mPrivateLayout.setContentHeight(contentHeight); + mPublicLayout.setContentHeight(contentHeight); if (mGuts != null) { mGuts.setActualHeight(height); } invalidate(); - super.setActualHeight(height, notifyListeners); + updateExpandButtonAppearance(); } @Override - public int getMaxHeight() { + public int getMaxContentHeight() { NotificationContentView showingLayout = getShowingLayout(); return showingLayout.getMaxHeight(); } @@ -484,12 +908,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { return mShowingPublic ? mPublicLayout : mPrivateLayout; } + @Override + public void setShowingLegacyBackground(boolean showing) { + super.setShowingLegacyBackground(showing); + mPrivateLayout.setShowingLegacyBackground(showing); + mPublicLayout.setShowingLegacyBackground(showing); + } + public void setExpansionLogger(ExpansionLogger logger, String key) { mLogger = logger; mLoggingKey = key; } - private void logExpansionEvent(boolean userAction, boolean wasExpanded) { final boolean nowExpanded = isExpanded(); if (wasExpanded != nowExpanded && mLogger != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java index a18fff2..d77e050 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java @@ -24,16 +24,21 @@ import android.util.AttributeSet; import android.view.View; import android.view.ViewOutlineProvider; +import com.android.systemui.R; + /** * Like {@link ExpandableView}, but setting an outline for the height and clipping. */ public abstract class ExpandableOutlineView extends ExpandableView { private final Rect mOutlineRect = new Rect(); + protected final int mRoundedRectCornerRadius; private boolean mCustomOutline; public ExpandableOutlineView(Context context, AttributeSet attrs) { super(context, attrs); + mRoundedRectCornerRadius = getResources().getDimensionPixelSize( + R.dimen.notification_material_rounded_rect_radius); setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { @@ -43,7 +48,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { getWidth(), Math.max(getActualHeight(), mClipTopAmount)); } else { - outline.setRect(mOutlineRect); + outline.setRoundRect(mOutlineRect, mRoundedRectCornerRadius); } } }); @@ -66,12 +71,14 @@ public abstract class ExpandableOutlineView extends ExpandableView { setOutlineRect(rect.left, rect.top, rect.right, rect.bottom); } else { mCustomOutline = false; + setClipToOutline(false); invalidateOutline(); } } protected void setOutlineRect(float left, float top, float right, float bottom) { mCustomOutline = true; + setClipToOutline(true); mOutlineRect.set((int) left, (int) top, (int) right, (int) bottom); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java index ebc663c..08a6603 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java @@ -33,36 +33,47 @@ import java.util.ArrayList; */ public abstract class ExpandableView extends FrameLayout { - private final int mMaxNotificationHeight; - - private OnHeightChangedListener mOnHeightChangedListener; + private final int mBottomDecorHeight; + protected OnHeightChangedListener mOnHeightChangedListener; + protected int mMaxViewHeight; private int mActualHeight; protected int mClipTopAmount; private boolean mActualHeightInitialized; private boolean mDark; private ArrayList<View> mMatchParentViews = new ArrayList<View>(); + private int mClipTopOptimization; + private static Rect mClipRect = new Rect(); + private boolean mWillBeGone; public ExpandableView(Context context, AttributeSet attrs) { super(context, attrs); - mMaxNotificationHeight = getResources().getDimensionPixelSize( + mMaxViewHeight = getResources().getDimensionPixelSize( R.dimen.notification_max_height); + mBottomDecorHeight = resolveBottomDecorHeight(); + } + + protected int resolveBottomDecorHeight() { + return getResources().getDimensionPixelSize( + R.dimen.notification_bottom_decor_height); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int ownMaxHeight = mMaxNotificationHeight; + int ownMaxHeight = mMaxViewHeight; int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; - boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; - if (hasFixedHeight || isHeightLimited) { - int size = MeasureSpec.getSize(heightMeasureSpec); - ownMaxHeight = Math.min(ownMaxHeight, size); + if (hasFixedHeight) { + // We have a height set in our layout, so we want to be at most as big as given + ownMaxHeight = Math.min(MeasureSpec.getSize(heightMeasureSpec), ownMaxHeight); } int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST); int maxChildHeight = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); + if (child.getVisibility() == GONE || isChildInvisible(child)) { + continue; + } int childHeightSpec = newHeightSpec; ViewGroup.LayoutParams layoutParams = child.getLayoutParams(); if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) { @@ -81,7 +92,7 @@ public abstract class ExpandableView extends FrameLayout { mMatchParentViews.add(child); } } - int ownHeight = hasFixedHeight ? ownMaxHeight : maxChildHeight; + int ownHeight = hasFixedHeight ? ownMaxHeight : Math.min(ownMaxHeight, maxChildHeight); newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY); for (View child : mMatchParentViews) { child.measure(getChildMeasureSpec( @@ -90,6 +101,10 @@ public abstract class ExpandableView extends FrameLayout { } mMatchParentViews.clear(); int width = MeasureSpec.getSize(widthMeasureSpec); + if (canHaveBottomDecor()) { + // We always account for the expandAction as well. + ownHeight += mBottomDecorHeight; + } setMeasuredDimension(width, ownHeight); } @@ -99,9 +114,10 @@ public abstract class ExpandableView extends FrameLayout { if (!mActualHeightInitialized && mActualHeight == 0) { int initialHeight = getInitialHeight(); if (initialHeight != 0) { - setActualHeight(initialHeight); + setContentHeight(initialHeight); } } + updateClipping(); } /** @@ -118,6 +134,14 @@ public abstract class ExpandableView extends FrameLayout { } @Override + public boolean dispatchGenericMotionEvent(MotionEvent ev) { + if (filterMotionEvent(ev)) { + return super.dispatchGenericMotionEvent(ev); + } + return false; + } + + @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (filterMotionEvent(ev)) { return super.dispatchTouchEvent(ev); @@ -127,6 +151,8 @@ public abstract class ExpandableView extends FrameLayout { protected boolean filterMotionEvent(MotionEvent event) { return event.getActionMasked() != MotionEvent.ACTION_DOWN + && event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER + && event.getActionMasked() != MotionEvent.ACTION_HOVER_MOVE || event.getY() > mClipTopAmount && event.getY() < mActualHeight; } @@ -140,13 +166,14 @@ public abstract class ExpandableView extends FrameLayout { public void setActualHeight(int actualHeight, boolean notifyListeners) { mActualHeightInitialized = true; mActualHeight = actualHeight; + updateClipping(); if (notifyListeners) { - notifyHeightChanged(); + notifyHeightChanged(false /* needsAnimation */); } } - public void setActualHeight(int actualHeight) { - setActualHeight(actualHeight, true); + public void setContentHeight(int contentHeight) { + setActualHeight(contentHeight + getBottomDecorHeight(), true); } /** @@ -159,14 +186,39 @@ public abstract class ExpandableView extends FrameLayout { } /** + * This view may have a bottom decor which will be placed below the content. If it has one, this + * view will be layouted higher than just the content by {@link #mBottomDecorHeight}. + * @return the height of the decor if it currently has one + */ + public int getBottomDecorHeight() { + return hasBottomDecor() ? mBottomDecorHeight : 0; + } + + /** + * @return whether this view may have a bottom decor at all. This will force the view to layout + * itself higher than just it's content + */ + protected boolean canHaveBottomDecor() { + return false; + } + + /** + * @return whether this view has a decor view below it's content. This will make the intrinsic + * height from {@link #getIntrinsicHeight()} higher as well + */ + protected boolean hasBottomDecor() { + return false; + } + + /** * @return The maximum height of this notification. */ - public int getMaxHeight() { + public int getMaxContentHeight() { return getHeight(); } /** - * @return The minimum height of this notification. + * @return The minimum content height of this notification. */ public int getMinHeight() { return getHeight(); @@ -245,9 +297,9 @@ public abstract class ExpandableView extends FrameLayout { return false; } - public void notifyHeightChanged() { + public void notifyHeightChanged(boolean needsAnimation) { if (mOnHeightChangedListener != null) { - mOnHeightChangedListener.onHeightChanged(this); + mOnHeightChangedListener.onHeightChanged(this, needsAnimation); } } @@ -298,6 +350,56 @@ public abstract class ExpandableView extends FrameLayout { outRect.top += getTranslationY() + getClipTopAmount(); } + @Override + public void getBoundsOnScreen(Rect outRect, boolean clipToParent) { + super.getBoundsOnScreen(outRect, clipToParent); + outRect.bottom = outRect.top + getActualHeight(); + outRect.top += getClipTopOptimization(); + } + + public int getContentHeight() { + return mActualHeight - getBottomDecorHeight(); + } + + /** + * @return whether the given child can be ignored for layouting and measuring purposes + */ + protected boolean isChildInvisible(View child) { + return false; + } + + public boolean areChildrenExpanded() { + return false; + } + + private void updateClipping() { + mClipRect.set(0, mClipTopOptimization, getWidth(), getActualHeight()); + setClipBounds(mClipRect); + } + + public int getClipTopOptimization() { + return mClipTopOptimization; + } + + /** + * Set that the view will be clipped by a given amount from the top. Contrary to + * {@link #setClipTopAmount} this amount doesn't effect shadows and the background. + * + * @param clipTopOptimization the amount to clip from the top + */ + public void setClipTopOptimization(int clipTopOptimization) { + mClipTopOptimization = clipTopOptimization; + updateClipping(); + } + + public boolean willBeGone() { + return mWillBeGone; + } + + public void setWillBeGone(boolean willBeGone) { + mWillBeGone = willBeGone; + } + /** * A listener notifying when {@link #getActualHeight} changes. */ @@ -306,8 +408,9 @@ public abstract class ExpandableView extends FrameLayout { /** * @param view the view for which the height changed, or {@code null} if just the top * padding or the padding between the elements changed + * @param needsAnimation whether the view height needs to be animated */ - void onHeightChanged(ExpandableView view); + void onHeightChanged(ExpandableView view, boolean needsAnimation); /** * Called when the view is reset and therefore the height will change abruptly diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FlingAnimationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/FlingAnimationUtils.java index 9f0f84e..0fa088b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/FlingAnimationUtils.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/FlingAnimationUtils.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar; import android.animation.Animator; -import android.animation.ValueAnimator; import android.content.Context; import android.view.ViewPropertyAnimator; import android.view.animation.AnimationUtils; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardAffordanceView.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardAffordanceView.java index e2464c2..58fb2b1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardAffordanceView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardAffordanceView.java @@ -23,17 +23,22 @@ import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; +import android.graphics.CanvasProperty; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.util.AttributeSet; +import android.view.DisplayListCanvas; +import android.view.RenderNodeAnimator; import android.view.View; import android.view.ViewAnimationUtils; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.ImageView; import com.android.systemui.R; +import com.android.systemui.statusbar.phone.KeyguardAffordanceHelper; +import com.android.systemui.statusbar.phone.PhoneStatusBar; /** * An ImageView which does not have overlapping renderings commands and therefore does not need a @@ -55,26 +60,31 @@ public class KeyguardAffordanceView extends ImageView { private final int mNormalColor; private final ArgbEvaluator mColorInterpolator; private final FlingAnimationUtils mFlingAnimationUtils; - private final Drawable mArrowDrawable; - private final int mHintChevronPadding; private float mCircleRadius; private int mCenterX; private int mCenterY; private ValueAnimator mCircleAnimator; private ValueAnimator mAlphaAnimator; private ValueAnimator mScaleAnimator; - private ValueAnimator mArrowAnimator; private float mCircleStartValue; private boolean mCircleWillBeHidden; private int[] mTempPoint = new int[2]; private float mImageScale; private int mCircleColor; private boolean mIsLeft; - private float mArrowAlpha = 0.0f; private View mPreviewView; private float mCircleStartRadius; private float mMaxCircleSize; private Animator mPreviewClipper; + private float mRestingAlpha = KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT; + private boolean mSupportHardware; + private boolean mFinishing; + + private CanvasProperty<Float> mHwCircleRadius; + private CanvasProperty<Float> mHwCenterX; + private CanvasProperty<Float> mHwCenterY; + private CanvasProperty<Paint> mHwCirclePaint; + private AnimatorListenerAdapter mClipEndListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -99,12 +109,6 @@ public class KeyguardAffordanceView extends ImageView { mAlphaAnimator = null; } }; - private AnimatorListenerAdapter mArrowEndListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mArrowAnimator = null; - } - }; public KeyguardAffordanceView(Context context) { this(context, null); @@ -130,17 +134,12 @@ public class KeyguardAffordanceView extends ImageView { mInverseColor = 0xff000000; mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_affordance_min_background_radius); - mHintChevronPadding = mContext.getResources().getDimensionPixelSize( - R.dimen.hint_chevron_circle_padding); mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in); mColorInterpolator = new ArgbEvaluator(); mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.3f); - mArrowDrawable = context.getDrawable(R.drawable.ic_chevron_left); - mArrowDrawable.setBounds(0, 0, mArrowDrawable.getIntrinsicWidth(), - mArrowDrawable.getIntrinsicHeight()); } @Override @@ -153,8 +152,8 @@ public class KeyguardAffordanceView extends ImageView { @Override protected void onDraw(Canvas canvas) { + mSupportHardware = canvas.isHardwareAccelerated(); drawBackgroundCircle(canvas); - drawArrow(canvas); canvas.save(); canvas.scale(mImageScale, mImageScale, getWidth() / 2, getHeight() / 2); super.onDraw(canvas); @@ -168,22 +167,6 @@ public class KeyguardAffordanceView extends ImageView { } } - private void drawArrow(Canvas canvas) { - if (mArrowAlpha > 0) { - canvas.save(); - canvas.translate(mCenterX, mCenterY); - if (mIsLeft) { - canvas.scale(-1.0f, 1.0f); - } - canvas.translate(- mCircleRadius - mHintChevronPadding - - mArrowDrawable.getIntrinsicWidth() / 2, - - mArrowDrawable.getIntrinsicHeight() / 2); - mArrowDrawable.setAlpha((int) (mArrowAlpha * 255)); - mArrowDrawable.draw(canvas); - canvas.restore(); - } - } - private void updateIconColor() { Drawable drawable = getDrawable().mutate(); float alpha = mCircleRadius / mMinBackgroundRadius; @@ -194,15 +177,21 @@ public class KeyguardAffordanceView extends ImageView { private void drawBackgroundCircle(Canvas canvas) { if (mCircleRadius > 0) { - updateCircleColor(); - canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint); + if (mFinishing && mSupportHardware) { + DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas; + displayListCanvas.drawCircle(mHwCenterX, mHwCenterY, mHwCircleRadius, + mHwCirclePaint); + } else { + updateCircleColor(); + canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint); + } } } private void updateCircleColor() { float fraction = 0.5f + 0.5f * Math.max(0.0f, Math.min(1.0f, (mCircleRadius - mMinBackgroundRadius) / (0.5f * mMinBackgroundRadius))); - if (mPreviewView != null) { + if (mPreviewView != null && mPreviewView.getVisibility() == VISIBLE) { float finishingFraction = 1 - Math.max(0, mCircleRadius - mCircleStartRadius) / (mMaxCircleSize - mCircleStartRadius); fraction *= finishingFraction; @@ -216,15 +205,23 @@ public class KeyguardAffordanceView extends ImageView { public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) { cancelAnimator(mCircleAnimator); cancelAnimator(mPreviewClipper); + mFinishing = true; mCircleStartRadius = mCircleRadius; float maxCircleSize = getMaxCircleSize(); - ValueAnimator animatorToRadius = getAnimatorToRadius(maxCircleSize); + Animator animatorToRadius; + if (mSupportHardware) { + initHwProperties(); + animatorToRadius = getRtAnimatorToRadius(maxCircleSize); + } else { + animatorToRadius = getAnimatorToRadius(maxCircleSize); + } mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize, velocity, maxCircleSize); animatorToRadius.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mAnimationEndRunnable.run(); + mFinishing = false; } }); animatorToRadius.start(); @@ -238,9 +235,34 @@ public class KeyguardAffordanceView extends ImageView { velocity, maxCircleSize); mPreviewClipper.addListener(mClipEndListener); mPreviewClipper.start(); + if (mSupportHardware) { + startRtCircleFadeOut(animatorToRadius.getDuration()); + } } } + private void startRtCircleFadeOut(long duration) { + RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint, + RenderNodeAnimator.PAINT_ALPHA, 0); + animator.setDuration(duration); + animator.setInterpolator(PhoneStatusBar.ALPHA_OUT); + animator.setTarget(this); + animator.start(); + } + + private Animator getRtAnimatorToRadius(float circleRadius) { + RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius); + animator.setTarget(this); + return animator; + } + + private void initHwProperties() { + mHwCenterX = CanvasProperty.createFloat(mCenterX); + mHwCenterY = CanvasProperty.createFloat(mCenterY); + mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint); + mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius); + } + private float getMaxCircleSize() { getLocationInWindow(mTempPoint); float rootWidth = getRootView().getWidth(); @@ -394,6 +416,17 @@ public class KeyguardAffordanceView extends ImageView { } } + public void setRestingAlpha(float alpha) { + mRestingAlpha = alpha; + + // TODO: Handle the case an animation is playing. + setImageAlpha(alpha, false); + } + + public float getRestingAlpha() { + return mRestingAlpha; + } + public void setImageAlpha(float alpha, boolean animate) { setImageAlpha(alpha, animate, -1, null, null); } @@ -468,36 +501,6 @@ public class KeyguardAffordanceView extends ImageView { return mCircleRadius; } - public void showArrow(boolean show) { - cancelAnimator(mArrowAnimator); - float targetAlpha = show ? 1.0f : 0.0f; - if (mArrowAlpha == targetAlpha) { - return; - } - ValueAnimator animator = ValueAnimator.ofFloat(mArrowAlpha, targetAlpha); - mArrowAnimator = animator; - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - mArrowAlpha = (float) animation.getAnimatedValue(); - invalidate(); - } - }); - animator.addListener(mArrowEndListener); - Interpolator interpolator = show - ? mAppearInterpolator - : mDisappearInterpolator; - animator.setInterpolator(interpolator); - float durationFactor = Math.abs(mArrowAlpha - targetAlpha); - long duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor); - animator.setDuration(duration); - animator.start(); - } - - public void setIsLeft(boolean left) { - mIsLeft = left; - } - @Override public boolean performClick() { if (isClickable()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 58067c3..07a055c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -26,6 +26,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Color; import android.os.BatteryManager; import android.os.BatteryStats; import android.os.Handler; @@ -53,6 +54,7 @@ public class KeyguardIndicationController { private String mRestingIndication; private String mTransientIndication; + private int mTransientTextColor; private boolean mVisible; private boolean mPowerPluggedIn; @@ -105,7 +107,15 @@ public class KeyguardIndicationController { * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}. */ public void showTransientIndication(String transientIndication) { + showTransientIndication(transientIndication, Color.WHITE); + } + + /** + * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}. + */ + public void showTransientIndication(String transientIndication, int textColor) { mTransientIndication = transientIndication; + mTransientTextColor = textColor; mHandler.removeMessages(MSG_HIDE_TRANSIENT); updateIndication(); } @@ -124,7 +134,15 @@ public class KeyguardIndicationController { private void updateIndication() { if (mVisible) { mTextView.switchIndication(computeIndication()); + mTextView.setTextColor(computeColor()); + } + } + + private int computeColor() { + if (!TextUtils.isEmpty(mTransientIndication)) { + return mTransientTextColor; } + return Color.WHITE; } private String computeIndication() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java index 0fc46e9..01aa8d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java @@ -44,7 +44,7 @@ public class NotificationBackgroundView extends View { } private void draw(Canvas canvas, Drawable drawable) { - if (drawable != null) { + if (drawable != null && mActualHeight > mClipTopAmount) { drawable.setBounds(0, mClipTopAmount, getWidth(), mActualHeight); drawable.draw(canvas); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TickerView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigMediaNarrowViewWrapper.java index bf13751..91e5404 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TickerView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigMediaNarrowViewWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -11,32 +11,26 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License. + * limitations under the License */ -package com.android.systemui.statusbar.phone; +package com.android.systemui.statusbar; import android.content.Context; -import android.util.AttributeSet; -import android.widget.TextSwitcher; +import android.view.View; +/** + * Wraps a big media narrow notification template layout. + */ +public class NotificationBigMediaNarrowViewWrapper extends NotificationMediaViewWrapper { -public class TickerView extends TextSwitcher -{ - Ticker mTicker; - - public TickerView(Context context, AttributeSet attrs) { - super(context, attrs); + protected NotificationBigMediaNarrowViewWrapper(Context ctx, + View view) { + super(ctx, view); } @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - if (mTicker != null) mTicker.reflowText(); - } - - public void setTicker(Ticker t) { - mTicker = t; + public boolean needsRoundRectClipping() { + return true; } } - diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigPictureViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigPictureViewWrapper.java new file mode 100644 index 0000000..ffe0cd1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigPictureViewWrapper.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar; + +import android.content.Context; +import android.view.View; + +/** + * Wraps a notification view inflated from a big picture style template. + */ +public class NotificationBigPictureViewWrapper extends NotificationTemplateViewWrapper { + + protected NotificationBigPictureViewWrapper(Context ctx, View view) { + super(ctx, view); + } + + @Override + public boolean needsRoundRectClipping() { + return true; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java index 914b3d8..86d7f4f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java @@ -17,51 +17,59 @@ package com.android.systemui.statusbar; import android.content.Context; -import android.graphics.ColorFilter; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; +import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.widget.FrameLayout; -import android.widget.ImageView; + import com.android.systemui.R; /** - * 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. + * A frame layout containing the actual payload of the notification, including the contracted, + * expanded and heads up layout. This class is responsible for clipping the content and and + * switching between the expanded, contracted and the heads up view depending on its clipped size. */ public class NotificationContentView extends FrameLayout { private static final long ANIMATION_DURATION_LENGTH = 170; + private static final int VISIBLE_TYPE_CONTRACTED = 0; + private static final int VISIBLE_TYPE_EXPANDED = 1; + private static final int VISIBLE_TYPE_HEADSUP = 2; private final Rect mClipBounds = new Rect(); + private final int mSmallHeight; + private final int mHeadsUpHeight; + private final int mRoundRectRadius; + private final Interpolator mLinearInterpolator = new LinearInterpolator(); + private final boolean mRoundRectClippingEnabled; private View mContractedChild; private View mExpandedChild; + private View mHeadsUpChild; private NotificationViewWrapper mContractedWrapper; - - private int mSmallHeight; + private NotificationViewWrapper mExpandedWrapper; + private NotificationViewWrapper mHeadsUpWrapper; private int mClipTopAmount; - private int mActualHeight; - - private final Interpolator mLinearInterpolator = new LinearInterpolator(); - - private boolean mContractedVisible = true; + private int mContentHeight; + private int mUnrestrictedContentHeight; + private int mVisibleType = VISIBLE_TYPE_CONTRACTED; private boolean mDark; - private final Paint mFadePaint = new Paint(); private boolean mAnimate; - private ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener + private boolean mIsHeadsUp; + private boolean mShowingLegacyBackground; + + private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { @@ -71,16 +79,77 @@ public class NotificationContentView extends FrameLayout { } }; + private final ViewOutlineProvider mOutlineProvider = new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), mUnrestrictedContentHeight, + mRoundRectRadius); + } + }; + public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mFadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD)); + mSmallHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); + mHeadsUpHeight = getResources().getDimensionPixelSize(R.dimen.notification_mid_height); + mRoundRectRadius = getResources().getDimensionPixelSize( + R.dimen.notification_material_rounded_rect_radius); + mRoundRectClippingEnabled = getResources().getBoolean( + R.bool.config_notifications_round_rect_clipping); reset(true); + setOutlineProvider(mOutlineProvider); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; + boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; + int maxSize = Integer.MAX_VALUE; + if (hasFixedHeight || isHeightLimited) { + maxSize = MeasureSpec.getSize(heightMeasureSpec); + } + int maxChildHeight = 0; + if (mContractedChild != null) { + int size = Math.min(maxSize, mSmallHeight); + mContractedChild.measure(widthMeasureSpec, + MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)); + maxChildHeight = Math.max(maxChildHeight, mContractedChild.getMeasuredHeight()); + } + if (mExpandedChild != null) { + int size = maxSize; + ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams(); + if (layoutParams.height >= 0) { + // An actual height is set + size = Math.min(maxSize, layoutParams.height); + } + int spec = size == Integer.MAX_VALUE + ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + : MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); + mExpandedChild.measure(widthMeasureSpec, spec); + maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight()); + } + if (mHeadsUpChild != null) { + int size = Math.min(maxSize, mHeadsUpHeight); + ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams(); + if (layoutParams.height >= 0) { + // An actual height is set + size = Math.min(maxSize, layoutParams.height); + } + mHeadsUpChild.measure(widthMeasureSpec, + MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST)); + maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight()); + } + int ownHeight = Math.min(maxChildHeight, maxSize); + int width = MeasureSpec.getSize(widthMeasureSpec); + setMeasuredDimension(width, ownHeight); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); updateClipping(); + invalidateOutline(); } @Override @@ -96,13 +165,16 @@ public class NotificationContentView extends FrameLayout { if (mExpandedChild != null) { mExpandedChild.animate().cancel(); } + if (mHeadsUpChild != null) { + mHeadsUpChild.animate().cancel(); + } removeAllViews(); mContractedChild = null; mExpandedChild = null; - mSmallHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); - mContractedVisible = true; + mHeadsUpChild = null; + mVisibleType = VISIBLE_TYPE_CONTRACTED; if (resetActualHeight) { - mActualHeight = mSmallHeight; + mContentHeight = mSmallHeight; } } @@ -114,17 +186,21 @@ public class NotificationContentView extends FrameLayout { return mExpandedChild; } + public View getHeadsUpChild() { + return mHeadsUpChild; + } + public void setContractedChild(View child) { if (mContractedChild != null) { mContractedChild.animate().cancel(); removeView(mContractedChild); } - sanitizeContractedLayoutParams(child); addView(child); mContractedChild = child; mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child); selectLayout(false /* animate */, true /* force */); mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */); + updateRoundRectClipping(); } public void setExpandedChild(View child) { @@ -134,7 +210,21 @@ public class NotificationContentView extends FrameLayout { } addView(child); mExpandedChild = child; + mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child); + selectLayout(false /* animate */, true /* force */); + updateRoundRectClipping(); + } + + public void setHeadsUpChild(View child) { + if (mHeadsUpChild != null) { + mHeadsUpChild.animate().cancel(); + removeView(mHeadsUpChild); + } + addView(child); + mHeadsUpChild = child; + mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child); selectLayout(false /* animate */, true /* force */); + updateRoundRectClipping(); } @Override @@ -159,16 +249,25 @@ public class NotificationContentView extends FrameLayout { } } - public void setActualHeight(int actualHeight) { - mActualHeight = actualHeight; + public void setContentHeight(int contentHeight) { + mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());; + mUnrestrictedContentHeight = Math.max(contentHeight, getMinHeight()); selectLayout(mAnimate /* animate */, false /* force */); updateClipping(); + invalidateOutline(); } - public int getMaxHeight() { + public int getContentHeight() { + return mContentHeight; + } - // The maximum height is just the laid out height. - return getHeight(); + public int getMaxHeight() { + if (mIsHeadsUp && mHeadsUpChild != null) { + return mHeadsUpChild.getHeight(); + } else if (mExpandedChild != null) { + return mExpandedChild.getHeight(); + } + return mSmallHeight; } public int getMinHeight() { @@ -180,67 +279,129 @@ public class NotificationContentView extends FrameLayout { updateClipping(); } - private void updateClipping() { - mClipBounds.set(0, mClipTopAmount, getWidth(), mActualHeight); - setClipBounds(mClipBounds); + private void updateRoundRectClipping() { + boolean enabled = needsRoundRectClipping(); + setClipToOutline(enabled); + } + + private boolean needsRoundRectClipping() { + if (!mRoundRectClippingEnabled) { + return false; + } + boolean needsForContracted = mContractedChild != null + && mContractedChild.getVisibility() == View.VISIBLE + && mContractedWrapper.needsRoundRectClipping(); + boolean needsForExpanded = mExpandedChild != null + && mExpandedChild.getVisibility() == View.VISIBLE + && mExpandedWrapper.needsRoundRectClipping(); + boolean needsForHeadsUp = mExpandedChild != null + && mExpandedChild.getVisibility() == View.VISIBLE + && mExpandedWrapper.needsRoundRectClipping(); + return needsForContracted || needsForExpanded || needsForHeadsUp; } - private void sanitizeContractedLayoutParams(View contractedChild) { - LayoutParams lp = (LayoutParams) contractedChild.getLayoutParams(); - lp.height = mSmallHeight; - contractedChild.setLayoutParams(lp); + private void updateClipping() { + mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight); + setClipBounds(mClipBounds); } private void selectLayout(boolean animate, boolean force) { if (mContractedChild == null) { return; } - boolean showContractedChild = showContractedChild(); - if (showContractedChild != mContractedVisible || force) { - if (animate && mExpandedChild != null) { - runSwitchAnimation(showContractedChild); - } else if (mExpandedChild != null) { - mContractedChild.setVisibility(showContractedChild ? View.VISIBLE : View.INVISIBLE); - mContractedChild.setAlpha(showContractedChild ? 1f : 0f); - mExpandedChild.setVisibility(showContractedChild ? View.INVISIBLE : View.VISIBLE); - mExpandedChild.setAlpha(showContractedChild ? 0f : 1f); + int visibleType = calculateVisibleType(); + if (visibleType != mVisibleType || force) { + if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null) + || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null) + || visibleType == VISIBLE_TYPE_CONTRACTED)) { + runSwitchAnimation(visibleType); + } else { + updateViewVisibilities(visibleType); } + mVisibleType = visibleType; + } + } + + private void updateViewVisibilities(int visibleType) { + boolean contractedVisible = visibleType == VISIBLE_TYPE_CONTRACTED; + mContractedChild.setVisibility(contractedVisible ? View.VISIBLE : View.INVISIBLE); + mContractedChild.setAlpha(contractedVisible ? 1f : 0f); + mContractedChild.setLayerType(LAYER_TYPE_NONE, null); + if (mExpandedChild != null) { + boolean expandedVisible = visibleType == VISIBLE_TYPE_EXPANDED; + mExpandedChild.setVisibility(expandedVisible ? View.VISIBLE : View.INVISIBLE); + mExpandedChild.setAlpha(expandedVisible ? 1f : 0f); + mExpandedChild.setLayerType(LAYER_TYPE_NONE, null); } - mContractedVisible = showContractedChild; + if (mHeadsUpChild != null) { + boolean headsUpVisible = visibleType == VISIBLE_TYPE_HEADSUP; + mHeadsUpChild.setVisibility(headsUpVisible ? View.VISIBLE : View.INVISIBLE); + mHeadsUpChild.setAlpha(headsUpVisible ? 1f : 0f); + mHeadsUpChild.setLayerType(LAYER_TYPE_NONE, null); + } + setLayerType(LAYER_TYPE_NONE, null); + updateRoundRectClipping(); } - private void runSwitchAnimation(final boolean showContractedChild) { - mContractedChild.setVisibility(View.VISIBLE); - mExpandedChild.setVisibility(View.VISIBLE); - mContractedChild.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint); - mExpandedChild.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint); + private void runSwitchAnimation(int visibleType) { + View shownView = getViewForVisibleType(visibleType); + View hiddenView = getViewForVisibleType(mVisibleType); + shownView.setVisibility(View.VISIBLE); + hiddenView.setVisibility(View.VISIBLE); + shownView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint); + hiddenView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint); setLayerType(LAYER_TYPE_HARDWARE, null); - mContractedChild.animate() - .alpha(showContractedChild ? 1f : 0f) + hiddenView.animate() + .alpha(0f) .setDuration(ANIMATION_DURATION_LENGTH) - .setInterpolator(mLinearInterpolator); - mExpandedChild.animate() - .alpha(showContractedChild ? 0f : 1f) + .setInterpolator(mLinearInterpolator) + .withEndAction(null); // In case we have multiple changes in one frame. + shownView.animate() + .alpha(1f) .setDuration(ANIMATION_DURATION_LENGTH) .setInterpolator(mLinearInterpolator) .withEndAction(new Runnable() { @Override public void run() { - mContractedChild.setLayerType(LAYER_TYPE_NONE, null); - mExpandedChild.setLayerType(LAYER_TYPE_NONE, null); - setLayerType(LAYER_TYPE_NONE, null); - mContractedChild.setVisibility(showContractedChild - ? View.VISIBLE - : View.INVISIBLE); - mExpandedChild.setVisibility(showContractedChild - ? View.INVISIBLE - : View.VISIBLE); + updateViewVisibilities(mVisibleType); } }); + updateRoundRectClipping(); + } + + /** + * @param visibleType one of the static enum types in this view + * @return the corresponding view according to the given visible type + */ + private View getViewForVisibleType(int visibleType) { + switch (visibleType) { + case VISIBLE_TYPE_EXPANDED: + return mExpandedChild; + case VISIBLE_TYPE_HEADSUP: + return mHeadsUpChild; + default: + return mContractedChild; + } } - private boolean showContractedChild() { - return mActualHeight <= mSmallHeight || mExpandedChild == null; + /** + * @return one of the static enum types in this view, calculated form the current state + */ + private int calculateVisibleType() { + boolean noExpandedChild = mExpandedChild == null; + if (mIsHeadsUp && mHeadsUpChild != null) { + if (mContentHeight <= mHeadsUpChild.getHeight() || noExpandedChild) { + return VISIBLE_TYPE_HEADSUP; + } else { + return VISIBLE_TYPE_EXPANDED; + } + } else { + if (mContentHeight <= mSmallHeight || noExpandedChild) { + return VISIBLE_TYPE_CONTRACTED; + } else { + return VISIBLE_TYPE_EXPANDED; + } + } } public void notifyContentUpdated() { @@ -249,6 +410,10 @@ public class NotificationContentView extends FrameLayout { mContractedWrapper.notifyContentUpdated(); mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */); } + if (mExpandedChild != null) { + mExpandedWrapper.notifyContentUpdated(); + } + updateRoundRectClipping(); } public boolean isContentExpandable() { @@ -258,7 +423,12 @@ public class NotificationContentView extends FrameLayout { public void setDark(boolean dark, boolean fade, long delay) { if (mDark == dark || mContractedChild == null) return; mDark = dark; - mContractedWrapper.setDark(dark, fade, delay); + mContractedWrapper.setDark(dark && !mShowingLegacyBackground, fade, delay); + } + + public void setHeadsUp(boolean headsUp) { + mIsHeadsUp = headsUp; + selectLayout(false /* animate */, true /* force */); } @Override @@ -268,4 +438,8 @@ public class NotificationContentView extends FrameLayout { // layout, and saves us some layers. return false; } + + public void setShowingLegacyBackground(boolean showing) { + mShowingLegacyBackground = showing; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java index 0702d7e..6fd341b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java @@ -41,4 +41,9 @@ public class NotificationCustomViewWrapper extends NotificationViewWrapper { mInvertHelper.update(dark); } } + + @Override + public boolean needsRoundRectClipping() { + return true; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java index 34c458a..aedae52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java @@ -17,14 +17,17 @@ package com.android.systemui.statusbar; import android.app.Notification; +import android.os.SystemClock; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; -import android.util.ArraySet; import android.view.View; +import com.android.systemui.statusbar.phone.NotificationGroupManager; +import com.android.systemui.statusbar.policy.HeadsUpManager; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -36,33 +39,26 @@ import java.util.Comparator; public class NotificationData { private final Environment mEnvironment; + private HeadsUpManager mHeadsUpManager; public static final class Entry { + private static final long LAUNCH_COOLDOWN = 2000; + private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN; public String key; public StatusBarNotification notification; public StatusBarIconView icon; public ExpandableNotificationRow row; // the outer expanded view - public View expanded; // the inflated RemoteViews - public View expandedPublic; // for insecure lockscreens - public View expandedBig; private boolean interruption; public boolean autoRedacted; // whether the redacted notification was generated by us public boolean legacy; // whether the notification has a legacy, dark background public int targetSdk; + private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; public Entry(StatusBarNotification n, StatusBarIconView ic) { this.key = n.getKey(); this.notification = n; this.icon = ic; } - public void setBigContentView(View bigContentView) { - this.expandedBig = bigContentView; - row.setExpandable(bigContentView != null); - } - public View getBigContentView() { - return expandedBig; - } - public View getPublicContentView() { return expandedPublic; } public void setInterruption() { interruption = true; @@ -78,60 +74,94 @@ public class NotificationData { public void reset() { // NOTE: Icon needs to be preserved for now. // We should fix this at some point. - expanded = null; - expandedPublic = null; - expandedBig = null; autoRedacted = false; legacy = false; + lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; if (row != null) { row.reset(); } } + + public View getContentView() { + return row.getPrivateLayout().getContractedChild(); + } + + public View getExpandedContentView() { + return row.getPrivateLayout().getExpandedChild(); + } + + public View getHeadsUpContentView() { + return row.getPrivateLayout().getHeadsUpChild(); + } + + public View getPublicContentView() { + return row.getPublicLayout().getContractedChild(); + } + + public void notifyFullScreenIntentLaunched() { + lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime(); + } + + public boolean hasJustLaunchedFullScreenIntent() { + return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN; + } } private final ArrayMap<String, Entry> mEntries = new ArrayMap<>(); private final ArrayList<Entry> mSortedAndFiltered = new ArrayList<>(); - private ArraySet<String> mGroupsWithSummaries = new ArraySet<>(); + + private NotificationGroupManager mGroupManager; private RankingMap mRankingMap; private final Ranking mTmpRanking = new Ranking(); + + public void setHeadsUpManager(HeadsUpManager headsUpManager) { + mHeadsUpManager = headsUpManager; + } + private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() { private final Ranking mRankingA = new Ranking(); private final Ranking mRankingB = new Ranking(); @Override public int compare(Entry a, Entry b) { - // Upsort current media notification. - String mediaNotification = mEnvironment.getCurrentMediaNotificationKey(); - boolean aMedia = a.key.equals(mediaNotification); - boolean bMedia = b.key.equals(mediaNotification); - if (aMedia != bMedia) { - return aMedia ? -1 : 1; - } - final StatusBarNotification na = a.notification; final StatusBarNotification nb = b.notification; + final int aPriority = na.getNotification().priority; + final int bPriority = nb.getNotification().priority; + + String mediaNotification = mEnvironment.getCurrentMediaNotificationKey(); - // Upsort PRIORITY_MAX system notifications - boolean aSystemMax = na.getNotification().priority >= Notification.PRIORITY_MAX && + // PRIORITY_MIN media streams are allowed to drift to the bottom + final boolean aMedia = a.key.equals(mediaNotification) + && aPriority > Notification.PRIORITY_MIN; + final boolean bMedia = b.key.equals(mediaNotification) + && bPriority > Notification.PRIORITY_MIN; + + boolean aSystemMax = aPriority >= Notification.PRIORITY_MAX && isSystemNotification(na); - boolean bSystemMax = nb.getNotification().priority >= Notification.PRIORITY_MAX && + boolean bSystemMax = bPriority >= Notification.PRIORITY_MAX && isSystemNotification(nb); - if (aSystemMax != bSystemMax) { - return aSystemMax ? -1 : 1; - } + int d = nb.getScore() - na.getScore(); - // RankingMap as received from NoMan. - if (mRankingMap != null) { + boolean isHeadsUp = a.row.isHeadsUp(); + if (isHeadsUp != b.row.isHeadsUp()) { + return isHeadsUp ? -1 : 1; + } else if (isHeadsUp) { + // Provide consistent ranking with headsUpManager + return mHeadsUpManager.compare(a, b); + } else if (aMedia != bMedia) { + // Upsort current media notification. + return aMedia ? -1 : 1; + } else if (aSystemMax != bSystemMax) { + // Upsort PRIORITY_MAX system notifications + return aSystemMax ? -1 : 1; + } else if (mRankingMap != null) { + // RankingMap as received from NoMan mRankingMap.getRanking(a.key, mRankingA); mRankingMap.getRanking(b.key, mRankingB); return mRankingA.getRank() - mRankingB.getRank(); - } - - int d = nb.getScore() - na.getScore(); - if (a.interruption != b.interruption) { - return a.interruption ? -1 : 1; - } else if (d != 0) { + } if (d != 0) { return d; } else { return (int) (nb.getNotification().when - na.getNotification().when); @@ -141,6 +171,7 @@ public class NotificationData { public NotificationData(Environment environment) { mEnvironment = environment; + mGroupManager = environment.getGroupManager(); } /** @@ -163,12 +194,14 @@ public class NotificationData { public void add(Entry entry, RankingMap ranking) { mEntries.put(entry.notification.getKey(), entry); updateRankingAndSort(ranking); + mGroupManager.onEntryAdded(entry); } public Entry remove(String key, RankingMap ranking) { Entry removed = mEntries.remove(key); if (removed == null) return null; updateRankingAndSort(ranking); + mGroupManager.onEntryRemoved(removed); return removed; } @@ -203,7 +236,6 @@ public class NotificationData { // anything changed, and this class should call back the UI so it updates itself. public void filterAndSort() { mSortedAndFiltered.clear(); - mGroupsWithSummaries.clear(); final int N = mEntries.size(); for (int i = 0; i < N; i++) { @@ -214,32 +246,12 @@ public class NotificationData { continue; } - if (sbn.getNotification().isGroupSummary()) { - mGroupsWithSummaries.add(sbn.getGroupKey()); - } mSortedAndFiltered.add(entry); } - // Second pass: Filter out group children with summary. - if (!mGroupsWithSummaries.isEmpty()) { - final int M = mSortedAndFiltered.size(); - for (int i = M - 1; i >= 0; i--) { - Entry ent = mSortedAndFiltered.get(i); - StatusBarNotification sbn = ent.notification; - if (sbn.getNotification().isGroupChild() && - mGroupsWithSummaries.contains(sbn.getGroupKey())) { - mSortedAndFiltered.remove(i); - } - } - } - Collections.sort(mSortedAndFiltered, mRankingComparator); } - public boolean isGroupWithSummary(String groupKey) { - return mGroupsWithSummaries.contains(groupKey); - } - boolean shouldFilterOut(StatusBarNotification sbn) { if (!(mEnvironment.isDeviceProvisioned() || showNotificationEvenIfUnprovisioned(sbn))) { @@ -254,6 +266,11 @@ public class NotificationData { mEnvironment.shouldHideSensitiveContents(sbn.getUserId())) { return true; } + + if (!BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS + && mGroupManager.isChildInGroupWithSummary(sbn)) { + return true; + } return false; } @@ -262,7 +279,7 @@ public class NotificationData { */ public boolean hasActiveClearableNotifications() { for (Entry e : mSortedAndFiltered) { - if (e.expanded != null) { // the view successfully inflated + if (e.getContentView() != null) { // the view successfully inflated if (e.notification.isClearable()) { return true; } @@ -328,5 +345,6 @@ public class NotificationData { public boolean isDeviceProvisioned(); public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn); public String getCurrentMediaNotificationKey(); + public NotificationGroupManager getGroupManager(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java index bfa3aa5..9653b67 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar; import android.content.Context; import android.util.AttributeSet; +import android.view.View; import android.widget.TextView; import com.android.systemui.R; @@ -32,6 +33,7 @@ public class NotificationOverflowContainer extends ActivatableNotificationView { private NotificationOverflowIconsView mIconsView; private ViewInvertHelper mViewInvertHelper; private boolean mDark; + private View mContent; public NotificationOverflowContainer(Context context, AttributeSet attrs) { super(context, attrs); @@ -43,7 +45,8 @@ public class NotificationOverflowContainer extends ActivatableNotificationView { mIconsView = (NotificationOverflowIconsView) findViewById(R.id.overflow_icons_view); mIconsView.setMoreText((TextView) findViewById(R.id.more_text)); mIconsView.setOverflowIndicator(findViewById(R.id.more_icon_overflow)); - mViewInvertHelper = new ViewInvertHelper(findViewById(R.id.content), + mContent = findViewById(R.id.content); + mViewInvertHelper = new ViewInvertHelper(mContent, NotificationPanelView.DOZE_ANIMATION_DURATION); } @@ -59,7 +62,21 @@ public class NotificationOverflowContainer extends ActivatableNotificationView { } } + @Override + protected View getContentView() { + return mContent; + } + public NotificationOverflowIconsView getIconsView() { return mIconsView; } + + protected int getContentHeightFromActualHeight(int actualHeight) { + int realActualHeight = actualHeight; + if (hasBottomDecor()) { + realActualHeight -= getBottomDecorHeight(); + } + realActualHeight = Math.max(getMinHeight(), realActualHeight); + return realActualHeight; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowIconsView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowIconsView.java index c4c9dac..88bb714 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowIconsView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowIconsView.java @@ -18,7 +18,6 @@ package com.android.systemui.statusbar; import android.app.Notification; import android.content.Context; -import android.content.res.Configuration; import android.graphics.PorterDuff; import android.util.AttributeSet; import android.widget.ImageView; @@ -46,7 +45,7 @@ public class NotificationOverflowIconsView extends IconMerger { protected void onFinishInflate() { super.onFinishInflate(); mNotificationColorUtil = NotificationColorUtil.getInstance(getContext()); - mTintColor = getResources().getColor(R.color.keyguard_overflow_content_color); + mTintColor = getContext().getColor(R.color.keyguard_overflow_content_color); mIconSize = getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java index 57d162b..958b8b5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java @@ -60,7 +60,7 @@ public class NotificationTemplateViewWrapper extends NotificationViewWrapper { super(view); mIconDarkAlpha = ctx.getResources().getInteger(R.integer.doze_small_icon_alpha); mIconBackgroundDarkColor = - ctx.getResources().getColor(R.color.doze_small_icon_background_color); + ctx.getColor(R.color.doze_small_icon_background_color); mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(ctx, android.R.interpolator.linear_out_slow_in); resolveViews(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java index 78b9739..b362a29 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java @@ -19,23 +19,29 @@ package com.android.systemui.statusbar; import android.content.Context; import android.view.View; -import com.android.internal.R; - /** * Wraps the actual notification content view; used to implement behaviors which are different for * the individual templates and custom views. */ public abstract class NotificationViewWrapper { + private static final String TAG_BIG_MEDIA_NARROW = "bigMediaNarrow"; + private static final String TAG_MEDIA = "media"; + private static final String TAG_BIG_PICTURE = "bigPicture"; + protected final View mView; public static NotificationViewWrapper wrap(Context ctx, View v) { - - // TODO: Figure out a better way to find out which template the view is. - if (v.findViewById(com.android.internal.R.id.media_actions) != null) { - return new NotificationMediaViewWrapper(ctx, v); - } else if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) { - return new NotificationTemplateViewWrapper(ctx, v); + if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) { + if (TAG_BIG_MEDIA_NARROW.equals(v.getTag())) { + return new NotificationBigMediaNarrowViewWrapper(ctx, v); + } else if (TAG_MEDIA.equals(v.getTag())) { + return new NotificationMediaViewWrapper(ctx, v); + } else if (TAG_BIG_PICTURE.equals(v.getTag())) { + return new NotificationBigMediaNarrowViewWrapper(ctx, v); + } else { + return new NotificationTemplateViewWrapper(ctx, v); + } } else { return new NotificationCustomViewWrapper(v); } @@ -58,4 +64,12 @@ public abstract class NotificationViewWrapper { * Notifies this wrapper that the content of the view might have changed. */ public void notifyContentUpdated() {} + + /** + * @return true if this template might need to be clipped with a round rect to make it look + * nice, false otherwise + */ + public boolean needsRoundRectClipping() { + return false; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java b/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java index aea9ec6..602989a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java @@ -177,6 +177,7 @@ public class ServiceMonitor { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_PACKAGE_ADDED); filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addDataScheme("package"); mContext.registerReceiver(mBroadcastReceiver, filter); @@ -196,13 +197,14 @@ public class ServiceMonitor { + " extras=" + bundleToString(intent.getExtras())); if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) { mHandler.sendEmptyMessage(MSG_START_SERVICE); - } else if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) { - PackageManager pm = mContext.getPackageManager(); - boolean serviceEnabled = - pm.getApplicationEnabledSetting(mServiceName.getPackageName()) - != PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } else if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction()) + || Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { + final PackageManager pm = mContext.getPackageManager(); + final boolean serviceEnabled = isPackageAvailable() + && pm.getApplicationEnabledSetting(mServiceName.getPackageName()) + != PackageManager.COMPONENT_ENABLED_STATE_DISABLED && pm.getComponentEnabledSetting(mServiceName) - != PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + != PackageManager.COMPONENT_ENABLED_STATE_DISABLED; if (mBound && !serviceEnabled) { stopService(); scheduleCheckBound(); @@ -279,4 +281,25 @@ public class ServiceMonitor { } return sb.append('}').toString(); } + + public ComponentName getComponent() { + return getComponentNameFromSetting(); + } + + public void setComponent(ComponentName component) { + final String setting = component == null ? null : component.flattenToShortString(); + Settings.Secure.putStringForUser(mContext.getContentResolver(), + mSettingKey, setting, UserHandle.USER_CURRENT); + } + + public boolean isPackageAvailable() { + final ComponentName component = getComponent(); + if (component == null) return false; + try { + return mContext.getPackageManager().isPackageAvailable(component.getPackageName()); + } catch (RuntimeException e) { + Log.w(mTag, "Error checking package availability", e); + return false; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java index 8e35ee9..da1f03e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java @@ -17,7 +17,13 @@ package com.android.systemui.statusbar; import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; import android.telephony.SubscriptionInfo; +import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; @@ -28,8 +34,12 @@ import android.widget.ImageView; import android.widget.LinearLayout; import com.android.systemui.R; +import com.android.systemui.statusbar.phone.StatusBarIconController; +import com.android.systemui.statusbar.policy.NetworkController.IconState; import com.android.systemui.statusbar.policy.NetworkControllerImpl; import com.android.systemui.statusbar.policy.SecurityController; +import com.android.systemui.tuner.TunerService; +import com.android.systemui.tuner.TunerService.Tunable; import java.util.ArrayList; import java.util.List; @@ -37,27 +47,41 @@ import java.util.List; // Intimately tied to the design of res/layout/signal_cluster_view.xml public class SignalClusterView extends LinearLayout - implements NetworkControllerImpl.SignalCluster, - SecurityController.SecurityControllerCallback { + implements NetworkControllerImpl.SignalCallback, + SecurityController.SecurityControllerCallback, Tunable { static final String TAG = "SignalClusterView"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String SLOT_AIRPLANE = "airplane"; + private static final String SLOT_MOBILE = "mobile"; + private static final String SLOT_WIFI = "wifi"; + private static final String SLOT_ETHERNET = "ethernet"; + NetworkControllerImpl mNC; SecurityController mSC; private boolean mNoSimsVisible = false; private boolean mVpnVisible = false; + private boolean mEthernetVisible = false; + private int mEthernetIconId = 0; + private int mLastEthernetIconId = -1; private boolean mWifiVisible = false; private int mWifiStrengthId = 0; + private int mLastWifiStrengthId = -1; private boolean mIsAirplaneMode = false; private int mAirplaneIconId = 0; - private int mAirplaneContentDescription; + private int mLastAirplaneIconId = -1; + private String mAirplaneContentDescription; private String mWifiDescription; + private String mEthernetDescription; private ArrayList<PhoneState> mPhoneStates = new ArrayList<PhoneState>(); + private int mIconTint = Color.WHITE; + private float mDarkIntensity; - ViewGroup mWifiGroup; - ImageView mVpn, mWifi, mAirplane, mNoSims; + ViewGroup mEthernetGroup, mWifiGroup; + View mNoSimsCombo; + ImageView mVpn, mEthernet, mWifi, mAirplane, mNoSims, mEthernetDark, mWifiDark, mNoSimsDark; View mWifiAirplaneSpacer; View mWifiSignalSpacer; LinearLayout mMobileSignalGroup; @@ -67,6 +91,11 @@ public class SignalClusterView private int mEndPadding; private int mEndPaddingNothingVisible; + private boolean mBlockAirplane; + private boolean mBlockMobile; + private boolean mBlockWifi; + private boolean mBlockEthernet; + public SignalClusterView(Context context) { this(context, null); } @@ -79,6 +108,29 @@ public class SignalClusterView super(context, attrs, defStyle); } + @Override + public void onTuningChanged(String key, String newValue) { + if (!StatusBarIconController.ICON_BLACKLIST.equals(key)) { + return; + } + ArraySet<String> blockList = StatusBarIconController.getIconBlacklist(newValue); + boolean blockAirplane = blockList.contains(SLOT_AIRPLANE); + boolean blockMobile = blockList.contains(SLOT_MOBILE); + boolean blockWifi = blockList.contains(SLOT_WIFI); + boolean blockEthernet = blockList.contains(SLOT_ETHERNET); + + if (blockAirplane != mBlockAirplane || blockMobile != mBlockMobile + || blockEthernet != mBlockEthernet || blockWifi != mBlockWifi) { + mBlockAirplane = blockAirplane; + mBlockMobile = blockMobile; + mBlockEthernet = blockEthernet; + mBlockWifi = blockWifi; + // Re-register to get new callbacks. + mNC.removeSignalCallback(this); + mNC.addSignalCallback(this); + } + } + public void setNetworkController(NetworkControllerImpl nc) { if (DEBUG) Log.d(TAG, "NetworkController=" + nc); mNC = nc; @@ -109,28 +161,39 @@ public class SignalClusterView super.onAttachedToWindow(); mVpn = (ImageView) findViewById(R.id.vpn); + mEthernetGroup = (ViewGroup) findViewById(R.id.ethernet_combo); + mEthernet = (ImageView) findViewById(R.id.ethernet); + mEthernetDark = (ImageView) findViewById(R.id.ethernet_dark); mWifiGroup = (ViewGroup) findViewById(R.id.wifi_combo); mWifi = (ImageView) findViewById(R.id.wifi_signal); + mWifiDark = (ImageView) findViewById(R.id.wifi_signal_dark); mAirplane = (ImageView) findViewById(R.id.airplane); mNoSims = (ImageView) findViewById(R.id.no_sims); + mNoSimsDark = (ImageView) findViewById(R.id.no_sims_dark); + mNoSimsCombo = findViewById(R.id.no_sims_combo); mWifiAirplaneSpacer = findViewById(R.id.wifi_airplane_spacer); mWifiSignalSpacer = findViewById(R.id.wifi_signal_spacer); mMobileSignalGroup = (LinearLayout) findViewById(R.id.mobile_signal_group); for (PhoneState state : mPhoneStates) { mMobileSignalGroup.addView(state.mMobileGroup); } + TunerService.get(mContext).addTunable(this, StatusBarIconController.ICON_BLACKLIST); apply(); + applyIconTint(); } @Override protected void onDetachedFromWindow() { mVpn = null; + mEthernetGroup = null; + mEthernet = null; mWifiGroup = null; mWifi = null; mAirplane = null; mMobileSignalGroup.removeAllViews(); mMobileSignalGroup = null; + TunerService.get(mContext).removeTunable(this); super.onDetachedFromWindow(); } @@ -148,36 +211,52 @@ public class SignalClusterView } @Override - public void setWifiIndicators(boolean visible, int strengthIcon, String contentDescription) { - mWifiVisible = visible; - mWifiStrengthId = strengthIcon; - mWifiDescription = contentDescription; + public void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon, + boolean activityIn, boolean activityOut, String description) { + mWifiVisible = statusIcon.visible && !mBlockWifi; + mWifiStrengthId = statusIcon.icon; + mWifiDescription = statusIcon.contentDescription; apply(); } @Override - public void setMobileDataIndicators(boolean visible, int strengthIcon, int typeIcon, - String contentDescription, String typeContentDescription, boolean isTypeIconWide, - int subId) { - PhoneState state = getOrInflateState(subId); - state.mMobileVisible = visible; - state.mMobileStrengthId = strengthIcon; - state.mMobileTypeId = typeIcon; - state.mMobileDescription = contentDescription; + public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType, + int qsType, boolean activityIn, boolean activityOut, String typeContentDescription, + String description, boolean isWide, int subId) { + PhoneState state = getState(subId); + if (state == null) { + return; + } + state.mMobileVisible = statusIcon.visible && !mBlockMobile; + state.mMobileStrengthId = statusIcon.icon; + state.mMobileTypeId = statusType; + state.mMobileDescription = statusIcon.contentDescription; state.mMobileTypeDescription = typeContentDescription; - state.mIsMobileTypeIconWide = isTypeIconWide; + state.mIsMobileTypeIconWide = statusType != 0 && isWide; + + apply(); + } + + @Override + public void setEthernetIndicators(IconState state) { + mEthernetVisible = state.visible && !mBlockEthernet; + mEthernetIconId = state.icon; + mEthernetDescription = state.contentDescription; apply(); } @Override public void setNoSims(boolean show) { - mNoSimsVisible = show; + mNoSimsVisible = show && !mBlockMobile; } @Override public void setSubs(List<SubscriptionInfo> subs) { + if (hasCorrectSubs(subs)) { + return; + } // Clear out all old subIds. mPhoneStates.clear(); if (mMobileSignalGroup != null) { @@ -187,15 +266,32 @@ public class SignalClusterView for (int i = 0; i < n; i++) { inflatePhoneState(subs.get(i).getSubscriptionId()); } + if (isAttachedToWindow()) { + applyIconTint(); + } + } + + private boolean hasCorrectSubs(List<SubscriptionInfo> subs) { + final int N = subs.size(); + if (N != mPhoneStates.size()) { + return false; + } + for (int i = 0; i < N; i++) { + if (mPhoneStates.get(i).mSubId != subs.get(i).getSubscriptionId()) { + return false; + } + } + return true; } - private PhoneState getOrInflateState(int subId) { + private PhoneState getState(int subId) { for (PhoneState state : mPhoneStates) { if (state.mSubId == subId) { return state; } } - return inflatePhoneState(subId); + Log.e(TAG, "Unexpected subscription " + subId); + return null; } private PhoneState inflatePhoneState(int subId) { @@ -208,32 +304,48 @@ public class SignalClusterView } @Override - public void setIsAirplaneMode(boolean is, int airplaneIconId, int contentDescription) { - mIsAirplaneMode = is; - mAirplaneIconId = airplaneIconId; - mAirplaneContentDescription = contentDescription; + public void setIsAirplaneMode(IconState icon) { + mIsAirplaneMode = icon.visible && !mBlockAirplane; + mAirplaneIconId = icon.icon; + mAirplaneContentDescription = icon.contentDescription; apply(); } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public void setMobileDataEnabled(boolean enabled) { + // Don't care. + } + + @Override + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { // Standard group layout onPopulateAccessibilityEvent() implementations // ignore content description, so populate manually + if (mEthernetVisible && mEthernetGroup != null && + mEthernetGroup.getContentDescription() != null) + event.getText().add(mEthernetGroup.getContentDescription()); if (mWifiVisible && mWifiGroup != null && mWifiGroup.getContentDescription() != null) event.getText().add(mWifiGroup.getContentDescription()); for (PhoneState state : mPhoneStates) { state.populateAccessibilityEvent(event); } - return super.dispatchPopulateAccessibilityEvent(event); + return super.dispatchPopulateAccessibilityEventInternal(event); } @Override public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); + if (mEthernet != null) { + mEthernet.setImageDrawable(null); + mEthernetDark.setImageDrawable(null); + mLastEthernetIconId = -1; + } + if (mWifi != null) { mWifi.setImageDrawable(null); + mWifiDark.setImageDrawable(null); + mLastWifiStrengthId = -1; } for (PhoneState state : mPhoneStates) { @@ -245,8 +357,9 @@ public class SignalClusterView } } - if(mAirplane != null) { + if (mAirplane != null) { mAirplane.setImageDrawable(null); + mLastAirplaneIconId = -1; } apply(); @@ -263,8 +376,30 @@ public class SignalClusterView mVpn.setVisibility(mVpnVisible ? View.VISIBLE : View.GONE); if (DEBUG) Log.d(TAG, String.format("vpn: %s", mVpnVisible ? "VISIBLE" : "GONE")); + + if (mEthernetVisible) { + if (mLastEthernetIconId != mEthernetIconId) { + mEthernet.setImageResource(mEthernetIconId); + mEthernetDark.setImageResource(mEthernetIconId); + mLastEthernetIconId = mEthernetIconId; + } + mEthernetGroup.setContentDescription(mEthernetDescription); + mEthernetGroup.setVisibility(View.VISIBLE); + } else { + mEthernetGroup.setVisibility(View.GONE); + } + + if (DEBUG) Log.d(TAG, + String.format("ethernet: %s", + (mEthernetVisible ? "VISIBLE" : "GONE"))); + + if (mWifiVisible) { - mWifi.setImageResource(mWifiStrengthId); + if (mWifiStrengthId != mLastWifiStrengthId) { + mWifi.setImageResource(mWifiStrengthId); + mWifiDark.setImageResource(mWifiStrengthId); + mLastWifiStrengthId = mWifiStrengthId; + } mWifiGroup.setContentDescription(mWifiDescription); mWifiGroup.setVisibility(View.VISIBLE); } else { @@ -288,9 +423,11 @@ public class SignalClusterView } if (mIsAirplaneMode) { - mAirplane.setImageResource(mAirplaneIconId); - mAirplane.setContentDescription(mAirplaneContentDescription != 0 ? - mContext.getString(mAirplaneContentDescription) : null); + if (mLastAirplaneIconId != mAirplaneIconId) { + mAirplane.setImageResource(mAirplaneIconId); + mLastAirplaneIconId = mAirplaneIconId; + } + mAirplane.setContentDescription(mAirplaneContentDescription); mAirplane.setVisibility(View.VISIBLE); } else { mAirplane.setVisibility(View.GONE); @@ -308,13 +445,43 @@ public class SignalClusterView mWifiSignalSpacer.setVisibility(View.GONE); } - mNoSims.setVisibility(mNoSimsVisible ? View.VISIBLE : View.GONE); + mNoSimsCombo.setVisibility(mNoSimsVisible ? View.VISIBLE : View.GONE); boolean anythingVisible = mNoSimsVisible || mWifiVisible || mIsAirplaneMode - || anyMobileVisible || mVpnVisible; + || anyMobileVisible || mVpnVisible || mEthernetVisible; setPaddingRelative(0, 0, anythingVisible ? mEndPadding : mEndPaddingNothingVisible, 0); } + public void setIconTint(int tint, float darkIntensity) { + boolean changed = tint != mIconTint || darkIntensity != mDarkIntensity; + mIconTint = tint; + mDarkIntensity = darkIntensity; + if (changed && isAttachedToWindow()) { + applyIconTint(); + } + } + + private void applyIconTint() { + setTint(mVpn, mIconTint); + setTint(mAirplane, mIconTint); + applyDarkIntensity(mDarkIntensity, mNoSims, mNoSimsDark); + applyDarkIntensity(mDarkIntensity, mWifi, mWifiDark); + applyDarkIntensity(mDarkIntensity, mEthernet, mEthernetDark); + for (int i = 0; i < mPhoneStates.size(); i++) { + mPhoneStates.get(i).setIconTint(mIconTint, mDarkIntensity); + } + } + + private void applyDarkIntensity(float darkIntensity, View lightIcon, View darkIcon) { + lightIcon.setAlpha(1 - darkIntensity); + darkIcon.setAlpha(darkIntensity); + } + + private void setTint(ImageView v, int tint) { + v.setImageTintMode(PorterDuff.Mode.SRC_ATOP); + v.setImageTintList(ColorStateList.valueOf(tint)); + } + private class PhoneState { private final int mSubId; private boolean mMobileVisible = false; @@ -323,7 +490,7 @@ public class SignalClusterView private String mMobileDescription, mMobileTypeDescription; private ViewGroup mMobileGroup; - private ImageView mMobile, mMobileType; + private ImageView mMobile, mMobileDark, mMobileType; public PhoneState(int subId, Context context) { ViewGroup root = (ViewGroup) LayoutInflater.from(context) @@ -335,12 +502,30 @@ public class SignalClusterView public void setViews(ViewGroup root) { mMobileGroup = root; mMobile = (ImageView) root.findViewById(R.id.mobile_signal); + mMobileDark = (ImageView) root.findViewById(R.id.mobile_signal_dark); mMobileType = (ImageView) root.findViewById(R.id.mobile_type); } public boolean apply(boolean isSecondaryIcon) { if (mMobileVisible && !mIsAirplaneMode) { mMobile.setImageResource(mMobileStrengthId); + Drawable mobileDrawable = mMobile.getDrawable(); + if (mobileDrawable instanceof Animatable) { + Animatable ad = (Animatable) mobileDrawable; + if (!ad.isRunning()) { + ad.start(); + } + } + + mMobileDark.setImageResource(mMobileStrengthId); + Drawable mobileDarkDrawable = mMobileDark.getDrawable(); + if (mobileDarkDrawable instanceof Animatable) { + Animatable ad = (Animatable) mobileDarkDrawable; + if (!ad.isRunning()) { + ad.start(); + } + } + mMobileType.setImageResource(mMobileTypeId); mMobileGroup.setContentDescription(mMobileTypeDescription + " " + mMobileDescription); @@ -354,6 +539,8 @@ public class SignalClusterView 0, 0, 0); mMobile.setPaddingRelative(mIsMobileTypeIconWide ? mWideTypeIconStartPadding : 0, 0, 0, 0); + mMobileDark.setPaddingRelative(mIsMobileTypeIconWide ? mWideTypeIconStartPadding : 0, + 0, 0, 0); if (DEBUG) Log.d(TAG, String.format("mobile: %s sig=%d typ=%d", (mMobileVisible ? "VISIBLE" : "GONE"), mMobileStrengthId, mMobileTypeId)); @@ -369,6 +556,11 @@ public class SignalClusterView event.getText().add(mMobileGroup.getContentDescription()); } } + + public void setIconTint(int tint, float darkIntensity) { + applyDarkIntensity(darkIntensity, mMobile, mMobileDark); + setTint(mMobileType, tint); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StackScrollerDecorView.java index 64d80cc..2f66c41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StackScrollerDecorView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StackScrollerDecorView.java @@ -32,7 +32,6 @@ public abstract class StackScrollerDecorView extends ExpandableView { protected View mContent; private boolean mIsVisible; private boolean mAnimating; - private boolean mWillBeGone; public StackScrollerDecorView(Context context, AttributeSet attrs) { super(context, attrs); @@ -134,13 +133,5 @@ public abstract class StackScrollerDecorView extends ExpandableView { mContent.animate().cancel(); } - public boolean willBeGone() { - return mWillBeGone; - } - - public void setWillBeGone(boolean willBeGone) { - mWillBeGone = willBeGone; - } - protected abstract View findContentView(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index 20dd3e7..baac8ac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -18,12 +18,12 @@ package com.android.systemui.statusbar; import android.app.Notification; import android.content.Context; -import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; import android.os.UserHandle; import android.text.TextUtils; import android.util.AttributeSet; @@ -48,14 +48,21 @@ public class StatusBarIconView extends AnimatedImageView { private int mNumberY; private String mNumberText; private Notification mNotification; + private final boolean mBlocked; public StatusBarIconView(Context context, String slot, Notification notification) { + this(context, slot, notification, false); + } + + public StatusBarIconView(Context context, String slot, Notification notification, + boolean blocked) { super(context); final Resources res = context.getResources(); + mBlocked = blocked; mSlot = slot; mNumberPain = new Paint(); mNumberPain.setTextAlign(Paint.Align.CENTER); - mNumberPain.setColor(res.getColor(R.drawable.notification_number_text_color)); + mNumberPain.setColor(context.getColor(R.drawable.notification_number_text_color)); mNumberPain.setAntiAlias(true); setNotification(notification); @@ -79,6 +86,7 @@ public class StatusBarIconView extends AnimatedImageView { public StatusBarIconView(Context context, AttributeSet attrs) { super(context, attrs); + mBlocked = false; final Resources res = context.getResources(); final int outerBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_size); final int imageBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size); @@ -100,13 +108,23 @@ public class StatusBarIconView extends AnimatedImageView { return a.equals(b); } + public boolean equalIcons(Icon a, Icon b) { + if (a == b) return true; + if (a.getType() != b.getType()) return false; + switch (a.getType()) { + case Icon.TYPE_RESOURCE: + return a.getResPackage().equals(b.getResPackage()) && a.getResId() == b.getResId(); + case Icon.TYPE_URI: + return a.getUriString().equals(b.getUriString()); + default: + return false; + } + } /** * Returns whether the set succeeded. */ public boolean set(StatusBarIcon icon) { - final boolean iconEquals = mIcon != null - && streq(mIcon.iconPackage, icon.iconPackage) - && mIcon.iconId == icon.iconId; + final boolean iconEquals = mIcon != null && equalIcons(mIcon.icon, icon.icon); final boolean levelEquals = iconEquals && mIcon.iconLevel == icon.iconLevel; final boolean visibilityEquals = mIcon != null @@ -137,7 +155,7 @@ public class StatusBarIconView extends AnimatedImageView { invalidate(); } if (!visibilityEquals) { - setVisibility(icon.visible ? VISIBLE : GONE); + setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE); } return true; } @@ -147,6 +165,9 @@ public class StatusBarIconView extends AnimatedImageView { } private boolean updateDrawable(boolean withClear) { + if (mIcon == null) { + return false; + } Drawable drawable = getIcon(mIcon); if (drawable == null) { Log.w(TAG, "No icon for slot " + mSlot); @@ -164,45 +185,18 @@ public class StatusBarIconView extends AnimatedImageView { } /** - * Returns the right icon to use for this item, respecting the iconId and - * iconPackage (if set) + * Returns the right icon to use for this item * - * @param context Context to use to get resources if iconPackage is not set + * @param context Context to use to get resources * @return Drawable for this item, or null if the package or item could not * be found */ public static Drawable getIcon(Context context, StatusBarIcon icon) { - Resources r = null; - - if (icon.iconPackage != null) { - try { - int userId = icon.user.getIdentifier(); - if (userId == UserHandle.USER_ALL) { - userId = UserHandle.USER_OWNER; - } - r = context.getPackageManager() - .getResourcesForApplicationAsUser(icon.iconPackage, userId); - } catch (PackageManager.NameNotFoundException ex) { - Log.e(TAG, "Icon package not found: " + icon.iconPackage); - return null; - } - } else { - r = context.getResources(); + int userId = icon.user.getIdentifier(); + if (userId == UserHandle.USER_ALL) { + userId = UserHandle.USER_OWNER; } - - if (icon.iconId == 0) { - return null; - } - - try { - return r.getDrawable(icon.iconId); - } catch (RuntimeException e) { - Log.w(TAG, "Icon not found in " - + (icon.iconPackage != null ? icon.iconId : "<system>") - + ": " + Integer.toHexString(icon.iconId)); - } - - return null; + return icon.icon.loadDrawableAsUser(context, userId); } public StatusBarIcon getStatusBarIcon() { @@ -226,6 +220,12 @@ public class StatusBarIconView extends AnimatedImageView { } @Override + public void onRtlPropertiesChanged(int layoutDirection) { + super.onRtlPropertiesChanged(layoutDirection); + updateDrawable(); + } + + @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); @@ -288,4 +288,8 @@ public class StatusBarIconView extends AnimatedImageView { return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon + " notification=" + mNotification + ")"; } + + public String getSlot() { + return mSlot; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java index 23810f9..9ef320b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java @@ -24,5 +24,11 @@ import android.content.Intent; * Keyguard. */ public interface ActivityStarter { - public void startActivity(Intent intent, boolean dismissShade); + void startActivity(Intent intent, boolean dismissShade); + void startActivity(Intent intent, boolean dismissShade, Callback callback); + void preventNextAnimation(); + + interface Callback { + void onActivityStarted(int resultCode); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java index e89e15d..1601b83 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java @@ -145,12 +145,12 @@ public class BarTransitions { mTransparent = 0x2f0000ff; mWarning = 0xffff0000; } else { - mOpaque = res.getColor(R.color.system_bar_background_opaque); - mSemiTransparent = res.getColor(R.color.system_bar_background_semi_transparent); - mTransparent = res.getColor(R.color.system_bar_background_transparent); - mWarning = res.getColor(com.android.internal.R.color.battery_saver_mode_color); + mOpaque = context.getColor(R.color.system_bar_background_opaque); + mSemiTransparent = context.getColor(R.color.system_bar_background_semi_transparent); + mTransparent = context.getColor(R.color.system_bar_background_transparent); + mWarning = context.getColor(com.android.internal.R.color.battery_saver_mode_color); } - mGradient = res.getDrawable(gradientResourceId); + mGradient = context.getDrawable(gradientResourceId); mInterpolator = new LinearInterpolator(); } @@ -160,7 +160,7 @@ public class BarTransitions { } @Override - public void setColorFilter(ColorFilter cf) { + public void setColorFilter(ColorFilter colorFilter) { // noop } 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 6cb5bcc..fcdd4b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.UserHandle; import android.view.Gravity; @@ -90,24 +91,12 @@ public class DemoStatusIcons extends LinearLayout implements DemoMode { : 0; updateSlot("alarm_clock", null, iconId); } - String sync = args.getString("sync"); - if (sync != null) { - int iconId = sync.equals("show") ? R.drawable.stat_sys_sync - : 0; - updateSlot("sync_active", null, iconId); - } String tty = args.getString("tty"); if (tty != null) { int iconId = tty.equals("show") ? R.drawable.stat_sys_tty_mode : 0; updateSlot("tty", null, iconId); } - String eri = args.getString("eri"); - if (eri != null) { - int iconId = eri.equals("show") ? R.drawable.stat_sys_roaming_cdma_0 - : 0; - updateSlot("cdma_eri", null, iconId); - } String mute = args.getString("mute"); if (mute != null) { int iconId = mute.equals("show") ? android.R.drawable.stat_notify_call_mute @@ -135,6 +124,9 @@ public class DemoStatusIcons extends LinearLayout implements DemoMode { private void updateSlot(String slot, String iconPkg, int iconId) { if (!mDemoMode) return; + if (iconPkg == null) { + iconPkg = mContext.getPackageName(); + } int removeIndex = -1; for (int i = 0; i < getChildCount(); i++) { StatusBarIconView v = (StatusBarIconView) getChildAt(i); @@ -144,8 +136,7 @@ public class DemoStatusIcons extends LinearLayout implements DemoMode { break; } else { StatusBarIcon icon = v.getStatusBarIcon(); - icon.iconPackage = iconPkg; - icon.iconId = iconId; + icon.icon = Icon.createWithResource(icon.icon.getResPackage(), iconId); v.set(icon); v.updateDrawable(); return; @@ -155,13 +146,13 @@ public class DemoStatusIcons extends LinearLayout implements DemoMode { if (iconId == 0) { if (removeIndex != -1) { removeViewAt(removeIndex); - return; } + return; } - StatusBarIcon icon = new StatusBarIcon(iconPkg, UserHandle.CURRENT, iconId, 0, 0, "Demo"); + StatusBarIcon icon = new StatusBarIcon(iconPkg, UserHandle.OWNER, iconId, 0, 0, "Demo"); StatusBarIconView v = new StatusBarIconView(getContext(), null, null); v.setTag(slot); v.set(icon); addView(v, 0, new LinearLayout.LayoutParams(mIconSize, mIconSize)); } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java new file mode 100644 index 0000000..df8c7fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.phone; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.systemui.Gefingerpoken; +import com.android.systemui.R; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.ExpandableView; +import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; + +/** + * A helper class to handle touches on the heads-up views. + */ +public class HeadsUpTouchHelper implements Gefingerpoken { + + private HeadsUpManager mHeadsUpManager; + private NotificationStackScrollLayout mStackScroller; + private int mTrackingPointer; + private float mTouchSlop; + private float mInitialTouchX; + private float mInitialTouchY; + private boolean mTouchingHeadsUpView; + private boolean mTrackingHeadsUp; + private boolean mCollapseSnoozes; + private NotificationPanelView mPanel; + private ExpandableNotificationRow mPickedChild; + private final int mNotificationsTopPadding; + + public HeadsUpTouchHelper(HeadsUpManager headsUpManager, + NotificationStackScrollLayout stackScroller, + NotificationPanelView notificationPanelView) { + mHeadsUpManager = headsUpManager; + mStackScroller = stackScroller; + mPanel = notificationPanelView; + Context context = stackScroller.getContext(); + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = configuration.getScaledTouchSlop(); + mNotificationsTopPadding = context.getResources() + .getDimensionPixelSize(R.dimen.notifications_top_padding); + } + + public boolean isTrackingHeadsUp() { + return mTrackingHeadsUp; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (!mTouchingHeadsUpView && event.getActionMasked() != MotionEvent.ACTION_DOWN) { + return false; + } + int pointerIndex = event.findPointerIndex(mTrackingPointer); + if (pointerIndex < 0) { + pointerIndex = 0; + mTrackingPointer = event.getPointerId(pointerIndex); + } + final float x = event.getX(pointerIndex); + final float y = event.getY(pointerIndex); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mInitialTouchY = y; + mInitialTouchX = x; + setTrackingHeadsUp(false); + ExpandableView child = mStackScroller.getChildAtRawPosition(x, y); + if (child == null && y < mNotificationsTopPadding) { + // We should also allow drags from the margin above the heads up + child = mStackScroller.getChildAtRawPosition(x, y + mNotificationsTopPadding); + } + mTouchingHeadsUpView = false; + if (child instanceof ExpandableNotificationRow) { + mPickedChild = (ExpandableNotificationRow) child; + mTouchingHeadsUpView = mPickedChild.isHeadsUp() && mPickedChild.isPinned(); + } + break; + case MotionEvent.ACTION_POINTER_UP: + final int upPointer = event.getPointerId(event.getActionIndex()); + if (mTrackingPointer == upPointer) { + // gesture is ongoing, find a new pointer to track + final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; + mTrackingPointer = event.getPointerId(newIndex); + mInitialTouchX = event.getX(newIndex); + mInitialTouchY = event.getY(newIndex); + } + break; + + case MotionEvent.ACTION_MOVE: + final float h = y - mInitialTouchY; + if (mTouchingHeadsUpView && Math.abs(h) > mTouchSlop + && Math.abs(h) > Math.abs(x - mInitialTouchX)) { + setTrackingHeadsUp(true); + mCollapseSnoozes = h < 0; + mInitialTouchX = x; + mInitialTouchY = y; + int expandedHeight = mPickedChild.getActualHeight(); + mPanel.setPanelScrimMinFraction((float) expandedHeight + / mPanel.getMaxPanelHeight()); + mPanel.startExpandMotion(x, y, true /* startTracking */, expandedHeight + + mNotificationsTopPadding); + mPanel.clearNotificattonEffects(); + mHeadsUpManager.unpinAll(); + return true; + } + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (mPickedChild != null && mTouchingHeadsUpView) { + // We may swallow this click if the heads up just came in. + if (mHeadsUpManager.shouldSwallowClick( + mPickedChild.getStatusBarNotification().getKey())) { + endMotion(); + return true; + } + } + endMotion(); + break; + } + return false; + } + + private void setTrackingHeadsUp(boolean tracking) { + mTrackingHeadsUp = tracking; + mHeadsUpManager.setTrackingHeadsUp(tracking); + mPanel.setTrackingHeadsUp(tracking); + } + + public void notifyFling(boolean collapse) { + if (collapse && mCollapseSnoozes) { + mHeadsUpManager.snooze(); + } + mCollapseSnoozes = false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!mTrackingHeadsUp) { + return false; + } + switch (event.getActionMasked()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + endMotion(); + setTrackingHeadsUp(false); + break; + } + return true; + } + + private void endMotion() { + mTrackingPointer = -1; + mPickedChild = null; + mTouchingHeadsUpView = false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java index 3b8fccc..0877ff9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java @@ -39,7 +39,7 @@ public class KeyguardAffordanceHelper { public static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.5f; public static final long HINT_PHASE1_DURATION = 200; private static final long HINT_PHASE2_DURATION = 350; - private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.15f; + private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f; private static final int HINT_CIRCLE_OPEN_DURATION = 500; private final Context mContext; @@ -63,13 +63,30 @@ public class KeyguardAffordanceHelper { private Interpolator mDisappearInterpolator; private Animator mSwipeAnimator; private int mMinBackgroundRadius; - private boolean mMotionPerformedByUser; private boolean mMotionCancelled; + private int mTouchTargetSize; + private View mTargetedView; + private boolean mTouchSlopExeeded; private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() { + public boolean mCancelled; + @Override public void onAnimationEnd(Animator animation) { mSwipeAnimator = null; - setSwipingInProgress(false); + mSwipingInProgress = false; + if (!mCancelled) { + mTargetedView = null; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationStart(Animator animation) { + mCancelled = false; } }; private Runnable mAnimationEndRunnable = new Runnable() { @@ -83,9 +100,9 @@ public class KeyguardAffordanceHelper { mContext = context; mCallback = callback; initIcons(); - updateIcon(mLeftIcon, 0.0f, SWIPE_RESTING_ALPHA_AMOUNT, false, false); - updateIcon(mCenterIcon, 0.0f, SWIPE_RESTING_ALPHA_AMOUNT, false, false); - updateIcon(mRightIcon, 0.0f, SWIPE_RESTING_ALPHA_AMOUNT, false, false); + updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false); + updateIcon(mCenterIcon, 0.0f, mCenterIcon.getRestingAlpha(), false, false); + updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false); initDimens(); } @@ -97,6 +114,8 @@ public class KeyguardAffordanceHelper { R.dimen.keyguard_min_swipe_amount); mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_affordance_min_background_radius); + mTouchTargetSize = mContext.getResources().getDimensionPixelSize( + R.dimen.keyguard_affordance_touch_target_size); mHintGrowAmount = mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways); mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f); @@ -108,70 +127,111 @@ public class KeyguardAffordanceHelper { private void initIcons() { mLeftIcon = mCallback.getLeftIcon(); - mLeftIcon.setIsLeft(true); mCenterIcon = mCallback.getCenterIcon(); mRightIcon = mCallback.getRightIcon(); - mRightIcon.setIsLeft(false); + updatePreviews(); + } + + public void updatePreviews() { mLeftIcon.setPreviewView(mCallback.getLeftPreview()); mRightIcon.setPreviewView(mCallback.getRightPreview()); } public boolean onTouchEvent(MotionEvent event) { - if (mMotionCancelled && event.getActionMasked() != MotionEvent.ACTION_DOWN) { + int action = event.getActionMasked(); + if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) { return false; } final float y = event.getY(); final float x = event.getX(); boolean isUp = false; - switch (event.getActionMasked()) { + switch (action) { case MotionEvent.ACTION_DOWN: - if (mSwipingInProgress) { + View targetView = getIconAtPosition(x, y); + if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) { + mMotionCancelled = true; + return false; + } + if (mTargetedView != null) { cancelAnimation(); + } else { + mTouchSlopExeeded = false; } - mInitialTouchY = y; + mCallback.onSwipingStarted(targetView == mRightIcon); + mSwipingInProgress = true; + mTargetedView = targetView; mInitialTouchX = x; + mInitialTouchY = y; mTranslationOnDown = mTranslation; initVelocityTracker(); trackMovement(event); - mMotionPerformedByUser = false; mMotionCancelled = false; break; case MotionEvent.ACTION_POINTER_DOWN: mMotionCancelled = true; - endMotion(event, true /* forceSnapBack */); + endMotion(true /* forceSnapBack */, x, y); break; case MotionEvent.ACTION_MOVE: - final float w = x - mInitialTouchX; trackMovement(event); - if (((leftSwipePossible() && w > mTouchSlop) - || (rightSwipePossible() && w < -mTouchSlop)) - && Math.abs(w) > Math.abs(y - mInitialTouchY) - && !mSwipingInProgress) { - cancelAnimation(); - mInitialTouchY = y; - mInitialTouchX = x; - mTranslationOnDown = mTranslation; - setSwipingInProgress(true); + float xDist = x - mInitialTouchX; + float yDist = y - mInitialTouchY; + float distance = (float) Math.hypot(xDist, yDist); + if (!mTouchSlopExeeded && distance > mTouchSlop) { + mTouchSlopExeeded = true; } if (mSwipingInProgress) { - setTranslation(mTranslationOnDown + x - mInitialTouchX, false, false); + if (mTargetedView == mRightIcon) { + distance = mTranslationOnDown - distance; + distance = Math.min(0, distance); + } else { + distance = mTranslationOnDown + distance; + distance = Math.max(0, distance); + } + setTranslation(distance, false /* isReset */, false /* animateReset */); } break; case MotionEvent.ACTION_UP: isUp = true; case MotionEvent.ACTION_CANCEL: + boolean hintOnTheRight = mTargetedView == mRightIcon; trackMovement(event); - endMotion(event, !isUp); + endMotion(!isUp, x, y); + if (!mTouchSlopExeeded && isUp) { + mCallback.onIconClicked(hintOnTheRight); + } break; } return true; } - private void endMotion(MotionEvent event, boolean forceSnapBack) { + private View getIconAtPosition(float x, float y) { + if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) { + return mLeftIcon; + } + if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) { + return mRightIcon; + } + return null; + } + + public boolean isOnAffordanceIcon(float x, float y) { + return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y); + } + + private boolean isOnIcon(View icon, float x, float y) { + float iconX = icon.getX() + icon.getWidth() / 2.0f; + float iconY = icon.getY() + icon.getHeight() / 2.0f; + double distance = Math.hypot(x - iconX, y - iconY); + return distance <= mTouchTargetSize / 2; + } + + private void endMotion(boolean forceSnapBack, float lastX, float lastY) { if (mSwipingInProgress) { - flingWithCurrentVelocity(forceSnapBack); + flingWithCurrentVelocity(forceSnapBack, lastX, lastY); + } else { + mTargetedView = null; } if (mVelocityTracker != null) { mVelocityTracker.recycle(); @@ -179,13 +239,6 @@ public class KeyguardAffordanceHelper { } } - private void setSwipingInProgress(boolean inProgress) { - mSwipingInProgress = inProgress; - if (inProgress) { - mCallback.onSwipingStarted(); - } - } - private boolean rightSwipePossible() { return mRightIcon.getVisibility() == View.VISIBLE; } @@ -198,14 +251,14 @@ public class KeyguardAffordanceHelper { return false; } - public void startHintAnimation(boolean right, Runnable onFinishedListener) { - + public void startHintAnimation(boolean right, + Runnable onFinishedListener) { + cancelAnimation(); startHintAnimationPhase1(right, onFinishedListener); } private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) { final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; - targetView.showArrow(true); ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount); animator.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; @@ -219,8 +272,8 @@ public class KeyguardAffordanceHelper { public void onAnimationEnd(Animator animation) { if (mCancelled) { mSwipeAnimator = null; + mTargetedView = null; onFinishedListener.run(); - targetView.showArrow(false); } else { startUnlockHintAnimationPhase2(right, onFinishedListener); } @@ -230,6 +283,7 @@ public class KeyguardAffordanceHelper { animator.setDuration(HINT_PHASE1_DURATION); animator.start(); mSwipeAnimator = animator; + mTargetedView = targetView; } /** @@ -242,14 +296,9 @@ public class KeyguardAffordanceHelper { @Override public void onAnimationEnd(Animator animation) { mSwipeAnimator = null; - targetView.showArrow(false); + mTargetedView = null; onFinishedListener.run(); } - - @Override - public void onAnimationStart(Animator animation) { - targetView.showArrow(false); - } }); animator.setInterpolator(mDisappearInterpolator); animator.setDuration(HINT_PHASE2_DURATION); @@ -268,7 +317,7 @@ public class KeyguardAffordanceHelper { targetView.setCircleRadiusWithoutAnimation(newRadius); float translation = getTranslationFromRadius(newRadius); mTranslation = right ? -translation : translation; - updateIconsFromRadius(targetView, newRadius); + updateIconsFromTranslation(targetView); } }); return animator; @@ -280,8 +329,8 @@ public class KeyguardAffordanceHelper { } } - private void flingWithCurrentVelocity(boolean forceSnapBack) { - float vel = getCurrentVelocity(); + private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) { + float vel = getCurrentVelocity(lastX, lastY); // We snap back if the current translation is not far enough boolean snapBack = isBelowFalsingThreshold(); @@ -303,7 +352,9 @@ public class KeyguardAffordanceHelper { } private void fling(float vel, final boolean snapBack) { - float target = mTranslation < 0 ? -mCallback.getPageWidth() : mCallback.getPageWidth(); + float target = mTranslation < 0 + ? -mCallback.getMaxTranslationDistance() + : mCallback.getMaxTranslationDistance(); target = snapBack ? 0 : target; ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target); @@ -323,6 +374,9 @@ public class KeyguardAffordanceHelper { } animator.start(); mSwipeAnimator = animator; + if (snapBack) { + mCallback.onSwipingAborted(); + } } private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable) { @@ -334,62 +388,64 @@ public class KeyguardAffordanceHelper { translation = rightSwipePossible() ? translation : Math.max(0, translation); translation = leftSwipePossible() ? translation : Math.min(0, translation); float absTranslation = Math.abs(translation); - if (absTranslation > Math.abs(mTranslationOnDown) + getMinTranslationAmount() || - mMotionPerformedByUser) { - mMotionPerformedByUser = true; - } if (translation != mTranslation || isReset) { KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon; KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon; float alpha = absTranslation / getMinTranslationAmount(); // We interpolate the alpha of the other icons to 0 - float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha); - fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); - - // We interpolate the alpha of the targetView to 1 - alpha = fadeOutAlpha + alpha; + float fadeOutAlpha = 1.0f - alpha; + fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f); boolean animateIcons = isReset && animateReset; float radius = getRadiusFromTranslation(absTranslation); boolean slowAnimation = isReset && isBelowFalsingThreshold(); if (!isReset) { - updateIcon(targetView, radius, alpha, false, false); + updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(), + false, false); } else { - updateIcon(targetView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation); + updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(), + animateIcons, slowAnimation); } - updateIcon(otherView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation); - updateIcon(mCenterIcon, 0.0f, fadeOutAlpha, animateIcons, slowAnimation); + updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(), + animateIcons, slowAnimation); + updateIcon(mCenterIcon, 0.0f, fadeOutAlpha * mCenterIcon.getRestingAlpha(), + animateIcons, slowAnimation); mTranslation = translation; } } - private void updateIconsFromRadius(KeyguardAffordanceView targetView, float newRadius) { - float alpha = newRadius / mMinBackgroundRadius; + private void updateIconsFromTranslation(KeyguardAffordanceView targetView) { + float absTranslation = Math.abs(mTranslation); + float alpha = absTranslation / getMinTranslationAmount(); // We interpolate the alpha of the other icons to 0 - float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha); + float fadeOutAlpha = 1.0f - alpha; fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); // We interpolate the alpha of the targetView to 1 - alpha = fadeOutAlpha + alpha; KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon; - updateIconAlpha(targetView, alpha, false); - updateIconAlpha(otherView, fadeOutAlpha, false); - updateIconAlpha(mCenterIcon, fadeOutAlpha, false); + updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false); + updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false); + updateIconAlpha(mCenterIcon, fadeOutAlpha * mCenterIcon.getRestingAlpha(), false); } private float getTranslationFromRadius(float circleSize) { - float translation = (circleSize - mMinBackgroundRadius) / BACKGROUND_RADIUS_SCALE_FACTOR; - return Math.max(0, translation); + float translation = (circleSize - mMinBackgroundRadius) + / BACKGROUND_RADIUS_SCALE_FACTOR; + return translation > 0.0f ? translation + mTouchSlop : 0.0f; } private float getRadiusFromTranslation(float translation) { - return translation * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius; + if (translation <= mTouchSlop) { + return 0.0f; + } + return (translation - mTouchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius; } public void animateHideLeftRightIcon() { + cancelAnimation(); updateIcon(mRightIcon, 0f, 0f, true, false); updateIcon(mLeftIcon, 0f, 0f, true, false); } @@ -404,14 +460,14 @@ public class KeyguardAffordanceHelper { } private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) { - float scale = getScale(alpha); + float scale = getScale(alpha, view); alpha = Math.min(1.0f, alpha); view.setImageAlpha(alpha, animate); view.setImageScale(scale, animate); } - private float getScale(float alpha) { - float scale = alpha / SWIPE_RESTING_ALPHA_AMOUNT * 0.2f + + private float getScale(float alpha, KeyguardAffordanceView icon) { + float scale = alpha / icon.getRestingAlpha() * 0.2f + KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT; return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT); } @@ -429,12 +485,22 @@ public class KeyguardAffordanceHelper { mVelocityTracker = VelocityTracker.obtain(); } - private float getCurrentVelocity() { + private float getCurrentVelocity(float lastX, float lastY) { if (mVelocityTracker == null) { return 0; } mVelocityTracker.computeCurrentVelocity(1000); - return mVelocityTracker.getXVelocity(); + float aX = mVelocityTracker.getXVelocity(); + float aY = mVelocityTracker.getYVelocity(); + float bX = lastX - mInitialTouchX; + float bY = lastY - mInitialTouchY; + float bLen = (float) Math.hypot(bX, bY); + // Project the velocity onto the distance vector: a * b / |b| + float projectedVelocity = (aX * bX + aY * bY) / bLen; + if (mTargetedView == mRightIcon) { + projectedVelocity = -projectedVelocity; + } + return projectedVelocity; } public void onConfigurationChanged() { @@ -451,7 +517,11 @@ public class KeyguardAffordanceHelper { mSwipeAnimator.cancel(); } setTranslation(0.0f, true, animate); - setSwipingInProgress(false); + mMotionCancelled = true; + if (mSwipingInProgress) { + mCallback.onSwipingAborted(); + } + mSwipingInProgress = false; } public interface Callback { @@ -468,9 +538,13 @@ public class KeyguardAffordanceHelper { */ void onAnimationToSideEnded(); - float getPageWidth(); + float getMaxTranslationDistance(); + + void onSwipingStarted(boolean rightIcon); + + void onSwipingAborted(); - void onSwipingStarted(); + void onIconClicked(boolean rightIcon); KeyguardAffordanceView getLeftIcon(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index 0c21b20..815e123 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -16,12 +16,17 @@ package com.android.systemui.statusbar.phone; +import android.app.ActivityManager; import android.app.ActivityManagerNative; +import android.app.Application; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; @@ -29,9 +34,13 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; import android.os.AsyncTask; import android.os.Bundle; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; import android.os.RemoteException; import android.os.UserHandle; import android.provider.MediaStore; +import android.service.media.CameraPrewarmService; import android.telecom.TelecomManager; import android.util.AttributeSet; import android.util.Log; @@ -50,6 +59,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; import com.android.systemui.R; +import com.android.systemui.assist.AssistManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.KeyguardAffordanceView; import com.android.systemui.statusbar.KeyguardIndicationController; @@ -78,14 +88,15 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private static final Intent PHONE_INTENT = new Intent(Intent.ACTION_DIAL); private static final int DOZE_ANIMATION_STAGGER_DELAY = 48; private static final int DOZE_ANIMATION_ELEMENT_DURATION = 250; + private static final long TRANSIENT_FP_ERROR_TIMEOUT = 1300; private KeyguardAffordanceView mCameraImageView; - private KeyguardAffordanceView mPhoneImageView; - private KeyguardAffordanceView mLockIcon; + private KeyguardAffordanceView mLeftAffordanceView; + private LockIcon mLockIcon; private TextView mIndicationText; private ViewGroup mPreviewContainer; - private View mPhonePreview; + private View mLeftPreview; private View mCameraPreview; private ActivityStarter mActivityStarter; @@ -97,9 +108,26 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private AccessibilityController mAccessibilityController; private PhoneStatusBar mPhoneStatusBar; - private final TrustDrawable mTrustDrawable; private final Interpolator mLinearOutSlowInInterpolator; - private int mLastUnlockIconRes = 0; + private boolean mPrewarmBound; + private Messenger mPrewarmMessenger; + private final ServiceConnection mPrewarmConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mPrewarmMessenger = new Messenger(service); + mPrewarmBound = true; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mPrewarmBound = false; + mPrewarmMessenger = null; + } + }; + + private boolean mLeftIsVoiceAssist; + private AssistManager mAssistManager; public KeyguardBottomAreaView(Context context) { this(context, null); @@ -116,7 +144,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL public KeyguardBottomAreaView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mTrustDrawable = new TrustDrawable(mContext); mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); } @@ -130,8 +157,12 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL label = getResources().getString(R.string.unlock_label); } else if (host == mCameraImageView) { label = getResources().getString(R.string.camera_label); - } else if (host == mPhoneImageView) { - label = getResources().getString(R.string.phone_label); + } else if (host == mLeftAffordanceView) { + if (mLeftIsVoiceAssist) { + label = getResources().getString(R.string.voice_assist_label); + } else { + label = getResources().getString(R.string.phone_label); + } } info.addAction(new AccessibilityAction(ACTION_CLICK, label)); } @@ -146,8 +177,8 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } else if (host == mCameraImageView) { launchCamera(); return true; - } else if (host == mPhoneImageView) { - launchPhone(); + } else if (host == mLeftAffordanceView) { + launchLeftAffordance(); return true; } } @@ -161,30 +192,28 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL mLockPatternUtils = new LockPatternUtils(mContext); mPreviewContainer = (ViewGroup) findViewById(R.id.preview_container); mCameraImageView = (KeyguardAffordanceView) findViewById(R.id.camera_button); - mPhoneImageView = (KeyguardAffordanceView) findViewById(R.id.phone_button); - mLockIcon = (KeyguardAffordanceView) findViewById(R.id.lock_icon); + mLeftAffordanceView = (KeyguardAffordanceView) findViewById(R.id.left_button); + mLockIcon = (LockIcon) findViewById(R.id.lock_icon); mIndicationText = (TextView) findViewById(R.id.keyguard_indication_text); watchForCameraPolicyChanges(); updateCameraVisibility(); - updatePhoneVisibility(); mUnlockMethodCache = UnlockMethodCache.getInstance(getContext()); mUnlockMethodCache.addListener(this); - updateLockIcon(); + mLockIcon.update(); setClipChildren(false); setClipToPadding(false); mPreviewInflater = new PreviewInflater(mContext, new LockPatternUtils(mContext)); - inflatePreviews(); + inflateCameraPreview(); mLockIcon.setOnClickListener(this); - mLockIcon.setBackground(mTrustDrawable); mLockIcon.setOnLongClickListener(this); mCameraImageView.setOnClickListener(this); - mPhoneImageView.setOnClickListener(this); + mLeftAffordanceView.setOnClickListener(this); initAccessibility(); } private void initAccessibility() { mLockIcon.setAccessibilityDelegate(mAccessibilityDelegate); - mPhoneImageView.setAccessibilityDelegate(mAccessibilityDelegate); + mLeftAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate); mCameraImageView.setAccessibilityDelegate(mAccessibilityDelegate); } @@ -215,6 +244,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL public void setAccessibilityController(AccessibilityController accessibilityController) { mAccessibilityController = accessibilityController; + mLockIcon.setAccessibilityController(accessibilityController); accessibilityController.addStateChangedCallback(this); } @@ -226,9 +256,9 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private Intent getCameraIntent() { KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext); boolean currentUserHasTrust = updateMonitor.getUserHasTrust( - mLockPatternUtils.getCurrentUser()); - return mLockPatternUtils.isSecure() && !currentUserHasTrust - ? SECURE_CAMERA_INTENT : INSECURE_CAMERA_INTENT; + KeyguardUpdateMonitor.getCurrentUser()); + boolean secure = mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser()); + return (secure && !currentUserHasTrust) ? SECURE_CAMERA_INTENT : INSECURE_CAMERA_INTENT; } private void updateCameraVisibility() { @@ -238,15 +268,32 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } ResolveInfo resolved = mContext.getPackageManager().resolveActivityAsUser(getCameraIntent(), PackageManager.MATCH_DEFAULT_ONLY, - mLockPatternUtils.getCurrentUser()); + KeyguardUpdateMonitor.getCurrentUser()); boolean visible = !isCameraDisabledByDpm() && resolved != null && getResources().getBoolean(R.bool.config_keyguardShowCameraAffordance); mCameraImageView.setVisibility(visible ? View.VISIBLE : View.GONE); } - private void updatePhoneVisibility() { - boolean visible = isPhoneVisible(); - mPhoneImageView.setVisibility(visible ? View.VISIBLE : View.GONE); + private void updateLeftAffordanceIcon() { + mLeftIsVoiceAssist = canLaunchVoiceAssist(); + int drawableId; + int contentDescription; + if (mLeftIsVoiceAssist) { + mLeftAffordanceView.setVisibility(View.VISIBLE); + drawableId = R.drawable.ic_mic_26dp; + contentDescription = R.string.accessibility_voice_assist_button; + } else { + boolean visible = isPhoneVisible(); + mLeftAffordanceView.setVisibility(visible ? View.VISIBLE : View.GONE); + drawableId = R.drawable.ic_phone_24dp; + contentDescription = R.string.accessibility_phone_button; + } + mLeftAffordanceView.setImageDrawable(mContext.getDrawable(drawableId)); + mLeftAffordanceView.setContentDescription(mContext.getString(contentDescription)); + } + + public boolean isLeftVoiceAssist() { + return mLeftIsVoiceAssist; } private boolean isPhoneVisible() { @@ -284,32 +331,18 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL @Override public void onStateChanged(boolean accessibilityEnabled, boolean touchExplorationEnabled) { mCameraImageView.setClickable(touchExplorationEnabled); - mPhoneImageView.setClickable(touchExplorationEnabled); + mLeftAffordanceView.setClickable(touchExplorationEnabled); mCameraImageView.setFocusable(accessibilityEnabled); - mPhoneImageView.setFocusable(accessibilityEnabled); - updateLockIconClickability(); - } - - private void updateLockIconClickability() { - if (mAccessibilityController == null) { - return; - } - boolean clickToUnlock = mAccessibilityController.isTouchExplorationEnabled(); - boolean clickToForceLock = mUnlockMethodCache.isTrustManaged() - && !mAccessibilityController.isAccessibilityEnabled(); - boolean longClickToForceLock = mUnlockMethodCache.isTrustManaged() - && !clickToForceLock; - mLockIcon.setClickable(clickToForceLock || clickToUnlock); - mLockIcon.setLongClickable(longClickToForceLock); - mLockIcon.setFocusable(mAccessibilityController.isAccessibilityEnabled()); + mLeftAffordanceView.setFocusable(accessibilityEnabled); + mLockIcon.update(); } @Override public void onClick(View v) { if (v == mCameraImageView) { launchCamera(); - } else if (v == mPhoneImageView) { - launchPhone(); + } else if (v == mLeftAffordanceView) { + launchLeftAffordance(); } if (v == mLockIcon) { if (!mAccessibilityController.isAccessibilityEnabled()) { handleTrustCircleClick(); @@ -332,25 +365,124 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL 0 /* velocityDp - N/A */); mIndicationController.showTransientIndication( R.string.keyguard_indication_trust_disabled); - mLockPatternUtils.requireCredentialEntry(mLockPatternUtils.getCurrentUser()); + mLockPatternUtils.requireCredentialEntry(KeyguardUpdateMonitor.getCurrentUser()); } - public void launchCamera() { - mFlashlightController.killFlashlight(); + public void bindCameraPrewarmService() { Intent intent = getCameraIntent(); + ActivityInfo targetInfo = PreviewInflater.getTargetActivityInfo(mContext, intent, + KeyguardUpdateMonitor.getCurrentUser()); + if (targetInfo != null) { + String clazz = targetInfo.metaData.getString( + MediaStore.META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE); + if (clazz != null) { + Intent serviceIntent = new Intent(); + serviceIntent.setClassName(targetInfo.packageName, clazz); + serviceIntent.setAction(CameraPrewarmService.ACTION_PREWARM); + try { + getContext().bindServiceAsUser(serviceIntent, mPrewarmConnection, + Context.BIND_AUTO_CREATE, new UserHandle(UserHandle.USER_CURRENT)); + } catch (SecurityException e) { + Log.w(TAG, "Unable to bind to prewarm service package=" + targetInfo.packageName + + " class=" + clazz, e); + } + } + } + } + + public void unbindCameraPrewarmService(boolean launched) { + if (mPrewarmBound) { + if (launched) { + try { + mPrewarmMessenger.send(Message.obtain(null /* handler */, + CameraPrewarmService.MSG_CAMERA_FIRED)); + } catch (RemoteException e) { + Log.w(TAG, "Error sending camera fired message", e); + } + } + mContext.unbindService(mPrewarmConnection); + mPrewarmBound = false; + } + } + + public void launchCamera() { + final Intent intent = getCameraIntent(); boolean wouldLaunchResolverActivity = PreviewInflater.wouldLaunchResolverActivity( - mContext, intent, mLockPatternUtils.getCurrentUser()); + mContext, intent, KeyguardUpdateMonitor.getCurrentUser()); if (intent == SECURE_CAMERA_INTENT && !wouldLaunchResolverActivity) { - mContext.startActivityAsUser(intent, UserHandle.CURRENT); + AsyncTask.execute(new Runnable() { + @Override + public void run() { + int result = ActivityManager.START_CANCELED; + try { + result = ActivityManagerNative.getDefault().startActivityAsUser( + null, getContext().getBasePackageName(), + intent, + intent.resolveTypeIfNeeded(getContext().getContentResolver()), + null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, null, + UserHandle.CURRENT.getIdentifier()); + } catch (RemoteException e) { + Log.w(TAG, "Unable to start camera activity", e); + } + mActivityStarter.preventNextAnimation(); + final boolean launched = isSuccessfulLaunch(result); + post(new Runnable() { + @Override + public void run() { + unbindCameraPrewarmService(launched); + } + }); + } + }); } else { // We need to delay starting the activity because ResolverActivity finishes itself if // launched behind lockscreen. - mActivityStarter.startActivity(intent, false /* dismissShade */); + mActivityStarter.startActivity(intent, false /* dismissShade */, + new ActivityStarter.Callback() { + @Override + public void onActivityStarted(int resultCode) { + unbindCameraPrewarmService(isSuccessfulLaunch(resultCode)); + } + }); } } - public void launchPhone() { + private static boolean isSuccessfulLaunch(int result) { + return result == ActivityManager.START_SUCCESS + || result == ActivityManager.START_DELIVERED_TO_TOP + || result == ActivityManager.START_TASK_TO_FRONT; + } + + public void launchLeftAffordance() { + if (mLeftIsVoiceAssist) { + launchVoiceAssist(); + } else { + launchPhone(); + } + } + + private void launchVoiceAssist() { + Runnable runnable = new Runnable() { + @Override + public void run() { + mAssistManager.launchVoiceAssistFromKeyguard(); + mActivityStarter.preventNextAnimation(); + } + }; + if (mPhoneStatusBar.isKeyguardCurrentlySecure()) { + AsyncTask.execute(runnable); + } else { + mPhoneStatusBar.executeRunnableDismissingKeyguard(runnable, null /* cancelAction */, + false /* dismissShade */, false /* afterKeyguardGone */); + } + } + + private boolean canLaunchVoiceAssist() { + return mAssistManager.canVoiceAssistBeLaunchedFromKeyguard(); + } + + private void launchPhone() { final TelecomManager tm = TelecomManager.from(mContext); if (tm.isInCall()) { AsyncTask.execute(new Runnable() { @@ -368,69 +500,25 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); - if (isShown()) { - mTrustDrawable.start(); - } else { - mTrustDrawable.stop(); - } if (changedView == this && visibility == VISIBLE) { - updateLockIcon(); + mLockIcon.update(); updateCameraVisibility(); } } - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mTrustDrawable.stop(); + public KeyguardAffordanceView getLeftView() { + return mLeftAffordanceView; } - private void updateLockIcon() { - boolean visible = isShown() && KeyguardUpdateMonitor.getInstance(mContext).isScreenOn(); - if (visible) { - mTrustDrawable.start(); - } else { - mTrustDrawable.stop(); - } - if (!visible) { - return; - } - // TODO: Real icon for facelock. - int iconRes = mUnlockMethodCache.isFaceUnlockRunning() - ? com.android.internal.R.drawable.ic_account_circle - : mUnlockMethodCache.isCurrentlyInsecure() ? R.drawable.ic_lock_open_24dp - : R.drawable.ic_lock_24dp; - if (mLastUnlockIconRes != iconRes) { - Drawable icon = mContext.getDrawable(iconRes); - int iconHeight = getResources().getDimensionPixelSize( - R.dimen.keyguard_affordance_icon_height); - int iconWidth = getResources().getDimensionPixelSize( - R.dimen.keyguard_affordance_icon_width); - if (icon.getIntrinsicHeight() != iconHeight || icon.getIntrinsicWidth() != iconWidth) { - icon = new IntrinsicSizeDrawable(icon, iconWidth, iconHeight); - } - mLockIcon.setImageDrawable(icon); - } - boolean trustManaged = mUnlockMethodCache.isTrustManaged(); - mTrustDrawable.setTrustManaged(trustManaged); - updateLockIconClickability(); - } - - - - public KeyguardAffordanceView getPhoneView() { - return mPhoneImageView; - } - - public KeyguardAffordanceView getCameraView() { + public KeyguardAffordanceView getRightView() { return mCameraImageView; } - public View getPhonePreview() { - return mPhonePreview; + public View getLeftPreview() { + return mLeftPreview; } - public View getCameraPreview() { + public View getRightPreview() { return mCameraPreview; } @@ -449,27 +537,39 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL @Override public void onUnlockMethodStateChanged() { - updateLockIcon(); + mLockIcon.update(); updateCameraVisibility(); } - private void inflatePreviews() { - mPhonePreview = mPreviewInflater.inflatePreview(PHONE_INTENT); + private void inflateCameraPreview() { mCameraPreview = mPreviewInflater.inflatePreview(getCameraIntent()); - if (mPhonePreview != null) { - mPreviewContainer.addView(mPhonePreview); - mPhonePreview.setVisibility(View.INVISIBLE); - } if (mCameraPreview != null) { mPreviewContainer.addView(mCameraPreview); mCameraPreview.setVisibility(View.INVISIBLE); } } + private void updateLeftPreview() { + View previewBefore = mLeftPreview; + if (previewBefore != null) { + mPreviewContainer.removeView(previewBefore); + } + if (mLeftIsVoiceAssist) { + mLeftPreview = mPreviewInflater.inflatePreviewFromService( + mAssistManager.getVoiceInteractorComponentName()); + } else { + mLeftPreview = mPreviewInflater.inflatePreview(PHONE_INTENT); + } + if (mLeftPreview != null) { + mPreviewContainer.addView(mLeftPreview); + mLeftPreview.setVisibility(View.INVISIBLE); + } + } + public void startFinishDozeAnimation() { long delay = 0; - if (mPhoneImageView.getVisibility() == View.VISIBLE) { - startFinishDozeAnimationElement(mPhoneImageView, delay); + if (mLeftAffordanceView.getVisibility() == View.VISIBLE) { + startFinishDozeAnimationElement(mLeftAffordanceView, delay); delay += DOZE_ANIMATION_STAGGER_DELAY; } startFinishDozeAnimationElement(mLockIcon, delay); @@ -506,6 +606,21 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } }; + private final Runnable mTransientFpErrorClearRunnable = new Runnable() { + @Override + public void run() { + mLockIcon.setTransientFpError(false); + mIndicationController.hideTransientIndication(); + } + }; + + private final Runnable mHideTransientIndicationRunnable = new Runnable() { + @Override + public void run() { + mIndicationController.hideTransientIndication(); + } + }; + private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { @Override @@ -515,48 +630,59 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL @Override public void onScreenTurnedOn() { - updateLockIcon(); + mLockIcon.setScreenOn(true); } @Override public void onScreenTurnedOff(int why) { - updateLockIcon(); + mLockIcon.setScreenOn(false); } @Override public void onKeyguardVisibilityChanged(boolean showing) { - updateLockIcon(); + mLockIcon.update(); } - }; - - public void setKeyguardIndicationController( - KeyguardIndicationController keyguardIndicationController) { - mIndicationController = keyguardIndicationController; - } - - - /** - * A wrapper around another Drawable that overrides the intrinsic size. - */ - private static class IntrinsicSizeDrawable extends InsetDrawable { - private final int mIntrinsicWidth; - private final int mIntrinsicHeight; + @Override + public void onFingerprintAuthenticated(int userId) { + } - public IntrinsicSizeDrawable(Drawable drawable, int intrinsicWidth, int intrinsicHeight) { - super(drawable, 0); - mIntrinsicWidth = intrinsicWidth; - mIntrinsicHeight = intrinsicHeight; + @Override + public void onFingerprintRunningStateChanged(boolean running) { + mLockIcon.update(); } @Override - public int getIntrinsicWidth() { - return mIntrinsicWidth; + public void onFingerprintHelp(int msgId, String helpString) { + mLockIcon.setTransientFpError(true); + mIndicationController.showTransientIndication(helpString, + getResources().getColor(R.color.system_warning_color, null)); + removeCallbacks(mTransientFpErrorClearRunnable); + postDelayed(mTransientFpErrorClearRunnable, TRANSIENT_FP_ERROR_TIMEOUT); } @Override - public int getIntrinsicHeight() { - return mIntrinsicHeight; + public void onFingerprintError(int msgId, String errString) { + // TODO: Go to bouncer if this is "too many attempts" (lockout) error. + mIndicationController.showTransientIndication(errString, + getResources().getColor(R.color.system_warning_color, null)); + removeCallbacks(mHideTransientIndicationRunnable); + postDelayed(mHideTransientIndicationRunnable, 5000); } + }; + + public void setKeyguardIndicationController( + KeyguardIndicationController keyguardIndicationController) { + mIndicationController = keyguardIndicationController; + } + + public void setAssistManager(AssistManager assistManager) { + mAssistManager = assistManager; + updateLeftAffordance(); + } + + public void updateLeftAffordance() { + updateLeftAffordanceIcon(); + updateLeftPreview(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index d0fe32e..a7afec4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -25,10 +25,10 @@ import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import com.android.internal.widget.LockPatternUtils; -import com.android.keyguard.KeyguardViewBase; +import com.android.keyguard.KeyguardHostView; +import com.android.keyguard.KeyguardSecurityView; import com.android.keyguard.R; import com.android.keyguard.ViewMediatorCallback; -import com.android.systemui.keyguard.KeyguardViewMediator; import static com.android.keyguard.KeyguardHostView.OnDismissAction; import static com.android.keyguard.KeyguardSecurityModel.SecurityMode; @@ -43,10 +43,11 @@ public class KeyguardBouncer { private LockPatternUtils mLockPatternUtils; private ViewGroup mContainer; private StatusBarWindowManager mWindowManager; - private KeyguardViewBase mKeyguardView; + private KeyguardHostView mKeyguardView; private ViewGroup mRoot; private boolean mShowingSoon; private Choreographer mChoreographer = Choreographer.getInstance(); + private int mBouncerPromptReason; public KeyguardBouncer(Context context, ViewMediatorCallback callback, LockPatternUtils lockPatternUtils, StatusBarWindowManager windowManager, @@ -69,6 +70,8 @@ public class KeyguardBouncer { return; } + mBouncerPromptReason = mCallback.getBouncerPromptReason(); + // 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()) { @@ -85,27 +88,39 @@ public class KeyguardBouncer { public void run() { mRoot.setVisibility(View.VISIBLE); mKeyguardView.onResume(); + showPromptReason(mBouncerPromptReason); mKeyguardView.startAppearAnimation(); mShowingSoon = false; mKeyguardView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } }; + /** + * Show a string explaining why the security view needs to be solved. + * + * @param reason a flag indicating which string should be shown, see + * {@link KeyguardSecurityView#PROMPT_REASON_NONE} + * and {@link KeyguardSecurityView#PROMPT_REASON_RESTART} + */ + public void showPromptReason(int reason) { + mKeyguardView.showPromptReason(reason); + } + private void cancelShowRunnable() { mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, mShowRunnable, null); mShowingSoon = false; } - public void showWithDismissAction(OnDismissAction r) { + public void showWithDismissAction(OnDismissAction r, Runnable cancelAction) { ensureView(); - mKeyguardView.setOnDismissAction(r); + mKeyguardView.setOnDismissAction(r, cancelAction); show(false /* resetSecuritySelection */); } public void hide(boolean destroyView) { cancelShowRunnable(); if (mKeyguardView != null) { - mKeyguardView.setOnDismissAction(null); + mKeyguardView.cancelDismissAction(); mKeyguardView.cleanUp(); } if (destroyView) { @@ -140,16 +155,6 @@ public class KeyguardBouncer { } } - 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 mShowingSoon || (mRoot != null && mRoot.getVisibility() == View.VISIBLE); } @@ -171,7 +176,7 @@ public class KeyguardBouncer { 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 = (KeyguardHostView) mRoot.findViewById(R.id.keyguard_host_view); mKeyguardView.setLockPatternUtils(mLockPatternUtils); mKeyguardView.setViewMediatorCallback(mCallback); mContainer.addView(mRoot, mContainer.getChildCount()); @@ -195,6 +200,7 @@ public class KeyguardBouncer { * notifications on Keyguard, like SIM PIN/PUK. */ public boolean needsFullscreenBouncer() { + ensureView(); if (mKeyguardView != null) { SecurityMode mode = mKeyguardView.getSecurityMode(); return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk; @@ -239,4 +245,9 @@ public class KeyguardBouncer { ensureView(); return mKeyguardView.interceptMediaKey(event); } + + public void notifyKeyguardAuthenticated() { + ensureView(); + mKeyguardView.finish(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java index 7579039..076e5f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java @@ -20,7 +20,6 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; -import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.WindowInsets; @@ -47,7 +46,7 @@ public class KeyguardPreviewContainer extends FrameLayout { } @Override - public void setColorFilter(ColorFilter cf) { + public void setColorFilter(ColorFilter colorFilter) { // noop } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 40c9134..b93fc76 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.phone; -import android.animation.LayoutTransition; import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.Drawable; @@ -35,6 +34,7 @@ import com.android.systemui.R; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.statusbar.policy.UserSwitcherController; import java.text.NumberFormat; @@ -141,6 +141,10 @@ public class KeyguardStatusBarView extends RelativeLayout ((BatteryMeterView) findViewById(R.id.battery)).setBatteryController(batteryController); } + public void setUserSwitcherController(UserSwitcherController controller) { + mMultiUserSwitch.setUserSwitcherController(controller); + } + public void setUserInfoController(UserInfoController userInfoController) { userInfoController.addListener(new UserInfoController.OnUserInfoChangedListener() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java new file mode 100644 index 0000000..6bcb766 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.phone; + +import android.content.Context; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.R; +import com.android.systemui.statusbar.KeyguardAffordanceView; +import com.android.systemui.statusbar.policy.AccessibilityController; + +/** + * Manages the different states and animations of the unlock icon. + */ +public class LockIcon extends KeyguardAffordanceView { + + /** + * Delay animations a bit when the screen just turned on as a heuristic to start them after + * the screen has actually turned on. + */ + private static final long ANIM_DELAY_AFTER_SCREEN_ON = 250; + + private static final int STATE_LOCKED = 0; + private static final int STATE_LOCK_OPEN = 1; + private static final int STATE_FACE_UNLOCK = 2; + private static final int STATE_FINGERPRINT = 3; + private static final int STATE_FINGERPRINT_ERROR = 4; + + private int mLastState = 0; + private boolean mLastScreenOn; + private boolean mTransientFpError; + private boolean mScreenOn; + private final TrustDrawable mTrustDrawable; + private final UnlockMethodCache mUnlockMethodCache; + private AccessibilityController mAccessibilityController; + private boolean mHasFingerPrintIcon; + + public LockIcon(Context context, AttributeSet attrs) { + super(context, attrs); + mTrustDrawable = new TrustDrawable(context); + setBackground(mTrustDrawable); + mUnlockMethodCache = UnlockMethodCache.getInstance(context); + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (isShown()) { + mTrustDrawable.start(); + } else { + mTrustDrawable.stop(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mTrustDrawable.stop(); + } + + public void setTransientFpError(boolean transientFpError) { + mTransientFpError = transientFpError; + update(); + } + + public void setScreenOn(boolean screenOn) { + mScreenOn = screenOn; + update(); + } + + public void update() { + boolean visible = isShown() && KeyguardUpdateMonitor.getInstance(mContext).isScreenOn(); + if (visible) { + mTrustDrawable.start(); + } else { + mTrustDrawable.stop(); + } + if (!visible) { + return; + } + // TODO: Real icon for facelock. + int state = getState(); + boolean anyFingerprintIcon = state == STATE_FINGERPRINT || state == STATE_FINGERPRINT_ERROR; + if (state != mLastState || mScreenOn != mLastScreenOn) { + int iconRes = getAnimationResForTransition(mLastState, state, mLastScreenOn, mScreenOn); + if (iconRes == R.drawable.lockscreen_fingerprint_draw_off_animation) { + anyFingerprintIcon = true; + } + if (iconRes == -1) { + iconRes = getIconForState(state); + } + Drawable icon = mContext.getDrawable(iconRes); + final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable + ? (AnimatedVectorDrawable) icon + : null; + int iconHeight = getResources().getDimensionPixelSize( + R.dimen.keyguard_affordance_icon_height); + int iconWidth = getResources().getDimensionPixelSize( + R.dimen.keyguard_affordance_icon_width); + if (!anyFingerprintIcon && (icon.getIntrinsicHeight() != iconHeight + || icon.getIntrinsicWidth() != iconWidth)) { + icon = new IntrinsicSizeDrawable(icon, iconWidth, iconHeight); + } + setPaddingRelative(0, 0, 0, anyFingerprintIcon + ? getResources().getDimensionPixelSize( + R.dimen.fingerprint_icon_additional_padding) + : 0); + setRestingAlpha( + anyFingerprintIcon ? 1f : KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT); + setImageDrawable(icon); + String contentDescription = getResources().getString(anyFingerprintIcon + ? R.string.accessibility_unlock_button_fingerprint + : R.string.accessibility_unlock_button); + setContentDescription(contentDescription); + mHasFingerPrintIcon = anyFingerprintIcon; + if (animation != null) { + + // If we play the draw on animation, delay it by one frame when the screen is + // actually turned on. + if (iconRes == R.drawable.lockscreen_fingerprint_draw_on_animation) { + postOnAnimationDelayed(new Runnable() { + @Override + public void run() { + animation.start(); + } + }, ANIM_DELAY_AFTER_SCREEN_ON); + } else { + animation.start(); + } + } + mLastState = state; + mLastScreenOn = mScreenOn; + } + + // Hide trust circle when fingerprint is running. + boolean trustManaged = mUnlockMethodCache.isTrustManaged() && !anyFingerprintIcon; + mTrustDrawable.setTrustManaged(trustManaged); + updateClickability(); + } + + private void updateClickability() { + if (mAccessibilityController == null) { + return; + } + boolean clickToUnlock = mAccessibilityController.isTouchExplorationEnabled(); + boolean clickToForceLock = mUnlockMethodCache.isTrustManaged() + && !mAccessibilityController.isAccessibilityEnabled(); + boolean longClickToForceLock = mUnlockMethodCache.isTrustManaged() + && !clickToForceLock; + setClickable(clickToForceLock || clickToUnlock); + setLongClickable(longClickToForceLock); + setFocusable(mAccessibilityController.isAccessibilityEnabled()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if (mHasFingerPrintIcon) { + // Avoid that the button description is also spoken + info.setClassName(LockIcon.class.getName()); + AccessibilityNodeInfo.AccessibilityAction unlock + = new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, + getContext().getString(R.string.accessibility_unlock_without_fingerprint)); + info.addAction(unlock); + } + } + + public void setAccessibilityController(AccessibilityController accessibilityController) { + mAccessibilityController = accessibilityController; + } + + private int getIconForState(int state) { + switch (state) { + case STATE_LOCKED: + return R.drawable.ic_lock_24dp; + case STATE_LOCK_OPEN: + return R.drawable.ic_lock_open_24dp; + case STATE_FACE_UNLOCK: + return com.android.internal.R.drawable.ic_account_circle; + case STATE_FINGERPRINT: + return R.drawable.ic_fingerprint; + case STATE_FINGERPRINT_ERROR: + return R.drawable.ic_fingerprint_error; + default: + throw new IllegalArgumentException(); + } + } + + private int getAnimationResForTransition(int oldState, int newState, boolean oldScreenOn, + boolean screenOn) { + if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) { + return R.drawable.lockscreen_fingerprint_fp_to_error_state_animation; + } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) { + return R.drawable.lockscreen_fingerprint_error_state_to_fp_animation; + } else if (oldState == STATE_FINGERPRINT && newState == STATE_LOCK_OPEN + && !mUnlockMethodCache.isCurrentlyInsecure()) { + return R.drawable.lockscreen_fingerprint_draw_off_animation; + } else if (newState == STATE_FINGERPRINT && !oldScreenOn && screenOn) { + return R.drawable.lockscreen_fingerprint_draw_on_animation; + } else { + return -1; + } + } + + private int getState() { + boolean fingerprintRunning = + KeyguardUpdateMonitor.getInstance(mContext).isFingerprintDetectionRunning(); + if (mUnlockMethodCache.isCurrentlyInsecure()) { + return STATE_LOCK_OPEN; + } else if (mTransientFpError) { + return STATE_FINGERPRINT_ERROR; + } else if (fingerprintRunning) { + return STATE_FINGERPRINT; + } else if (mUnlockMethodCache.isFaceUnlockRunning()) { + return STATE_FACE_UNLOCK; + } else { + return STATE_LOCKED; + } + } + + /** + * A wrapper around another Drawable that overrides the intrinsic size. + */ + private static class IntrinsicSizeDrawable extends InsetDrawable { + + private final int mIntrinsicWidth; + private final int mIntrinsicHeight; + + public IntrinsicSizeDrawable(Drawable drawable, int intrinsicWidth, int intrinsicHeight) { + super(drawable, 0); + mIntrinsicWidth = intrinsicWidth; + mIntrinsicHeight = intrinsicHeight; + } + + @Override + public int getIntrinsicWidth() { + return mIntrinsicWidth; + } + + @Override + public int getIntrinsicHeight() { + return mIntrinsicHeight; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java index 82f5a9e..e70d146 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java @@ -24,7 +24,7 @@ import android.provider.ContactsContract; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; -import android.view.accessibility.AccessibilityEvent; +import android.view.ViewGroup; import android.widget.FrameLayout; import com.android.systemui.R; @@ -40,8 +40,14 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener private QSPanel mQsPanel; private KeyguardUserSwitcher mKeyguardUserSwitcher; private boolean mKeyguardMode; + private UserSwitcherController.BaseUserAdapter mUserListener; + final UserManager mUserManager; + private final int[] mTmpInt2 = new int[2]; + + private UserSwitcherController mUserSwitcherController; + public MultiUserSwitch(Context context, AttributeSet attrs) { super(context, attrs); mUserManager = UserManager.get(getContext()); @@ -51,10 +57,18 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener protected void onFinishInflate() { super.onFinishInflate(); setOnClickListener(this); + refreshContentDescription(); } public void setQsPanel(QSPanel qsPanel) { mQsPanel = qsPanel; + setUserSwitcherController(qsPanel.getHost().getUserSwitcherController()); + } + + public void setUserSwitcherController(UserSwitcherController userSwitcherController) { + mUserSwitcherController = userSwitcherController; + registerListener(); + refreshContentDescription(); } public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) { @@ -63,6 +77,28 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener public void setKeyguardMode(boolean keyguardShowing) { mKeyguardMode = keyguardShowing; + registerListener(); + } + + private void registerListener() { + if (UserSwitcherController.isUserSwitcherAvailable(mUserManager) && mUserListener == null) { + + final UserSwitcherController controller = mUserSwitcherController; + if (controller != null) { + mUserListener = new UserSwitcherController.BaseUserAdapter(controller) { + @Override + public void notifyDataSetChanged() { + refreshContentDescription(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return null; + } + }; + refreshContentDescription(); + } + } } @Override @@ -72,14 +108,16 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener if (mKeyguardUserSwitcher != null) { mKeyguardUserSwitcher.show(true /* animate */); } - } else { - if (mQsPanel != null) { - UserSwitcherController userSwitcherController = - mQsPanel.getHost().getUserSwitcherController(); - if (userSwitcherController != null) { - mQsPanel.showDetailAdapter(true, userSwitcherController.userDetailAdapter); - } - } + } else if (mQsPanel != null && mUserSwitcherController != null) { + View center = getChildCount() > 0 ? getChildAt(0) : this; + + center.getLocationInWindow(mTmpInt2); + mTmpInt2[0] += center.getWidth() / 2; + mTmpInt2[1] += center.getHeight() / 2; + + mQsPanel.showDetailAdapter(true, + mUserSwitcherController.userDetailAdapter, + mTmpInt2); } } else { Intent intent = ContactsContract.QuickContact.composeQuickContactsIntent( @@ -90,20 +128,21 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener } @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); + public void setClickable(boolean clickable) { + super.setClickable(clickable); + refreshContentDescription(); + } + private void refreshContentDescription() { + String currentUser = null; + if (UserSwitcherController.isUserSwitcherAvailable(mUserManager) + && mUserSwitcherController != null) { + currentUser = mUserSwitcherController.getCurrentUserName(mContext); + } + + String text = null; if (isClickable()) { - String text; if (UserSwitcherController.isUserSwitcherAvailable(mUserManager)) { - String currentUser = null; - if (mQsPanel != null) { - UserSwitcherController controller = mQsPanel.getHost() - .getUserSwitcherController(); - if (controller != null) { - currentUser = controller.getCurrentUserName(mContext); - } - } if (TextUtils.isEmpty(currentUser)) { text = mContext.getString(R.string.accessibility_multi_user_switch_switcher); } else { @@ -114,11 +153,17 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener } else { text = mContext.getString(R.string.accessibility_multi_user_switch_quick_contact); } - if (!TextUtils.isEmpty(text)) { - event.getText().add(text); + } else { + if (!TextUtils.isEmpty(currentUser)) { + text = mContext.getString( + R.string.accessibility_multi_user_switch_inactive, + currentUser); } } + if (!TextUtils.equals(getContentDescription(), text)) { + setContentDescription(text); + } } @Override 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 7ec84da..134c579 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java @@ -26,18 +26,13 @@ import android.view.animation.AccelerateInterpolator; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.R; -import com.android.systemui.statusbar.policy.KeyButtonView; public final class NavigationBarTransitions extends BarTransitions { - private static final int CONTENT_FADE_DURATION = 200; - private final NavigationBarView mView; private final IStatusBarService mBarService; private boolean mLightsOut; - private boolean mVertical; - private int mRequestedMode; public NavigationBarTransitions(NavigationBarView view) { super(view, R.drawable.nav_background); @@ -46,31 +41,11 @@ public final class NavigationBarTransitions extends BarTransitions { ServiceManager.getService(Context.STATUS_BAR_SERVICE)); } - public void init(boolean isVertical) { - setVertical(isVertical); + public void init() { applyModeBackground(-1, getMode(), false /*animate*/); applyMode(getMode(), false /*animate*/, true /*force*/); } - public void setVertical(boolean isVertical) { - mVertical = isVertical; - transitionTo(mRequestedMode, false /*animate*/); - } - - @Override - public void transitionTo(int mode, boolean animate) { - mRequestedMode = mode; - if (mVertical) { - // translucent mode not allowed when vertical - if (mode == MODE_TRANSLUCENT || mode == MODE_TRANSPARENT) { - mode = MODE_OPAQUE; - } else if (mode == MODE_LIGHTS_OUT_TRANSPARENT) { - mode = MODE_LIGHTS_OUT; - } - } - super.transitionTo(mode, animate); - } - @Override protected void onTransition(int oldMode, int newMode, boolean animate) { super.onTransition(oldMode, newMode, animate); @@ -78,48 +53,11 @@ public final class NavigationBarTransitions extends BarTransitions { } private void applyMode(int mode, boolean animate, boolean force) { - // apply to key buttons - final float alpha = alphaForMode(mode); - setKeyButtonViewQuiescentAlpha(mView.getHomeButton(), alpha, animate); - setKeyButtonViewQuiescentAlpha(mView.getRecentsButton(), alpha, animate); - setKeyButtonViewQuiescentAlpha(mView.getMenuButton(), alpha, animate); - setKeyButtonViewQuiescentAlpha(mView.getImeSwitchButton(), alpha, animate); - - applyBackButtonQuiescentAlpha(mode, animate); // apply to lights out applyLightsOut(isLightsOut(mode), animate, force); } - private float alphaForMode(int mode) { - final boolean isOpaque = mode == MODE_OPAQUE || mode == MODE_LIGHTS_OUT; - return isOpaque ? KeyButtonView.DEFAULT_QUIESCENT_ALPHA : 1f; - } - - public void applyBackButtonQuiescentAlpha(int mode, boolean animate) { - float backAlpha = 0; - backAlpha = maxVisibleQuiescentAlpha(backAlpha, mView.getHomeButton()); - backAlpha = maxVisibleQuiescentAlpha(backAlpha, mView.getRecentsButton()); - backAlpha = maxVisibleQuiescentAlpha(backAlpha, mView.getMenuButton()); - backAlpha = maxVisibleQuiescentAlpha(backAlpha, mView.getImeSwitchButton()); - if (backAlpha > 0) { - setKeyButtonViewQuiescentAlpha(mView.getBackButton(), backAlpha, animate); - } - } - - private static float maxVisibleQuiescentAlpha(float max, View v) { - if ((v instanceof KeyButtonView) && v.isShown()) { - return Math.max(max, ((KeyButtonView)v).getQuiescentAlpha()); - } - return max; - } - - private void setKeyButtonViewQuiescentAlpha(View button, float alpha, boolean animate) { - if (button instanceof KeyButtonView) { - ((KeyButtonView) button).setQuiescentAlpha(alpha, animate); - } - } - private void applyLightsOut(boolean lightsOut, boolean animate, boolean force) { if (!force && lightsOut == mLightsOut) return; 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 1e4dfb4..f40f501 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -47,8 +47,6 @@ import android.widget.ImageView; import android.widget.LinearLayout; import com.android.systemui.R; -import com.android.systemui.statusbar.BaseStatusBar; -import com.android.systemui.statusbar.DelegateViewHelper; import com.android.systemui.statusbar.policy.DeadZone; import com.android.systemui.statusbar.policy.KeyButtonView; @@ -80,7 +78,6 @@ public class NavigationBarView extends LinearLayout { private Drawable mRecentLandIcon; private NavigationBarViewTaskSwitchHelper mTaskSwitchHelper; - private DelegateViewHelper mDelegateHelper; private DeadZone mDeadZone; private final NavigationBarTransitions mBarTransitions; @@ -93,7 +90,6 @@ public class NavigationBarView extends LinearLayout { private OnVerticalChangedListener mOnVerticalChangedListener; private boolean mIsLayoutRtl; - private boolean mDelegateIntercepted; private class NavTransitionListener implements TransitionListener { private boolean mBackTransitioning; @@ -144,7 +140,7 @@ public class NavigationBarView extends LinearLayout { @Override public void onClick(View view) { ((InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE)) - .showInputMethodPicker(); + .showInputMethodPicker(true /* showAuxiliarySubtypes */); } }; @@ -181,7 +177,6 @@ public class NavigationBarView extends LinearLayout { mBarSize = res.getDimensionPixelSize(R.dimen.navigation_bar_size); mVertical = false; mShowMenu = false; - mDelegateHelper = new DelegateViewHelper(this); mTaskSwitchHelper = new NavigationBarViewTaskSwitchHelper(context); getIcons(res); @@ -193,13 +188,8 @@ public class NavigationBarView extends LinearLayout { return mBarTransitions; } - public void setDelegateView(View view) { - mDelegateHelper.setDelegateView(view); - } - - public void setBar(BaseStatusBar phoneStatusBar) { + public void setBar(PhoneStatusBar phoneStatusBar) { mTaskSwitchHelper.setBar(phoneStatusBar); - mDelegateHelper.setBar(phoneStatusBar); } public void setOnVerticalChangedListener(OnVerticalChangedListener onVerticalChangedListener) { @@ -209,40 +199,22 @@ public class NavigationBarView extends LinearLayout { @Override public boolean onTouchEvent(MotionEvent event) { - initDownStates(event); - if (!mDelegateIntercepted && mTaskSwitchHelper.onTouchEvent(event)) { + if (mTaskSwitchHelper.onTouchEvent(event)) { return true; } if (mDeadZone != null && event.getAction() == MotionEvent.ACTION_OUTSIDE) { mDeadZone.poke(event); } - if (mDelegateHelper != null && mDelegateIntercepted) { - boolean ret = mDelegateHelper.onInterceptTouchEvent(event); - if (ret) return true; - } return super.onTouchEvent(event); } - private void initDownStates(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mDelegateIntercepted = false; - } - } - @Override public boolean onInterceptTouchEvent(MotionEvent event) { - initDownStates(event); - boolean intercept = mTaskSwitchHelper.onInterceptTouchEvent(event); - if (!intercept) { - mDelegateIntercepted = mDelegateHelper.onInterceptTouchEvent(event); - intercept = mDelegateIntercepted; - } else { - MotionEvent cancelEvent = MotionEvent.obtain(event); - cancelEvent.setAction(MotionEvent.ACTION_CANCEL); - mDelegateHelper.onInterceptTouchEvent(cancelEvent); - cancelEvent.recycle(); - } - return intercept; + return mTaskSwitchHelper.onInterceptTouchEvent(event); + } + + public void abortCurrentGesture() { + getHomeButton().abortCurrentGesture(); } private H mHandler = new H(); @@ -263,8 +235,8 @@ public class NavigationBarView extends LinearLayout { return mCurrentView.findViewById(R.id.back); } - public View getHomeButton() { - return mCurrentView.findViewById(R.id.home); + public KeyButtonView getHomeButton() { + return (KeyButtonView) mCurrentView.findViewById(R.id.home); } public View getImeSwitchButton() { @@ -369,8 +341,6 @@ public class NavigationBarView extends LinearLayout { getBackButton() .setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE); getHomeButton() .setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); - - mBarTransitions.applyBackButtonQuiescentAlpha(mBarTransitions.getMode(), true /*animate*/); } private boolean inLockTask() { @@ -451,7 +421,7 @@ public class NavigationBarView extends LinearLayout { mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone); // force the low profile & disabled states into compliance - mBarTransitions.init(mVertical); + mBarTransitions.init(); setDisabledFlags(mDisabledFlags, true /* force */); setMenuVisibility(mShowMenu, true /* force */); @@ -459,10 +429,6 @@ public class NavigationBarView extends LinearLayout { Log.d(TAG, "reorient(): rot=" + mDisplay.getRotation()); } - // swap to x coordinate if orientation is not in vertical - if (mDelegateHelper != null) { - mDelegateHelper.setSwapXY(mVertical); - } updateTaskSwitchHelper(); setNavigationIconHints(mNavigationIconHints, true); @@ -474,12 +440,6 @@ public class NavigationBarView extends LinearLayout { } @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - mDelegateHelper.setInitialTouchRegion(getHomeButton(), getBackButton(), getRecentsButton()); - } - - @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if (DEBUG) Log.d(TAG, String.format( "onSizeChanged: (%dx%d) old: (%dx%d)", w, h, oldw, oldh)); @@ -662,10 +622,6 @@ public class NavigationBarView extends LinearLayout { + " " + visibilityToString(button.getVisibility()) + " alpha=" + button.getAlpha() ); - if (button instanceof KeyButtonView) { - pw.print(" drawingAlpha=" + ((KeyButtonView)button).getDrawingAlpha()); - pw.print(" quiescentAlpha=" + ((KeyButtonView)button).getQuiescentAlpha()); - } } pw.println(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java new file mode 100644 index 0000000..7072dcb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.phone; + +import android.app.Notification; +import android.service.notification.StatusBarNotification; + +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.NotificationData; +import com.android.systemui.statusbar.StatusBarState; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A class to handle notifications and their corresponding groups. + */ +public class NotificationGroupManager { + + private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>(); + private OnGroupChangeListener mListener; + private int mBarState = -1; + + public void setOnGroupChangeListener(OnGroupChangeListener listener) { + mListener = listener; + } + + public boolean isGroupExpanded(StatusBarNotification sbn) { + NotificationGroup group = mGroupMap.get(sbn.getGroupKey()); + if (group == null) { + return false; + } + return group.expanded; + } + + public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) { + NotificationGroup group = mGroupMap.get(sbn.getGroupKey()); + if (group == null) { + return; + } + setGroupExpanded(group, expanded); + } + + private void setGroupExpanded(NotificationGroup group, boolean expanded) { + group.expanded = expanded; + if (group.summary != null) { + mListener.onGroupExpansionChanged(group.summary.row, expanded); + } + } + + public void onEntryRemoved(NotificationData.Entry removed) { + onEntryRemovedInternal(removed, removed.notification); + } + + /** + * An entry was removed. + * + * @param removed the removed entry + * @param sbn the notification the entry has, which doesn't need to be the same as it's internal + * notification + */ + private void onEntryRemovedInternal(NotificationData.Entry removed, + final StatusBarNotification sbn) { + Notification notif = sbn.getNotification(); + String groupKey = sbn.getGroupKey(); + final NotificationGroup group = mGroupMap.get(groupKey); + if (notif.isGroupSummary()) { + group.summary = null; + } else { + group.children.remove(removed); + } + if (group.children.isEmpty()) { + if (group.summary == null) { + mGroupMap.remove(groupKey); + } else { + if (group.expanded) { + // only the summary is left. Change it to unexpanded in a few ms. We do this to + // avoid raceconditions + removed.row.post(new Runnable() { + @Override + public void run() { + if (group.children.isEmpty()) { + setGroupExpanded(sbn, false); + } + } + }); + } else { + group.summary.row.updateExpandButton(); + } + } + } + } + + public void onEntryAdded(NotificationData.Entry added) { + StatusBarNotification sbn = added.notification; + Notification notif = sbn.getNotification(); + String groupKey = sbn.getGroupKey(); + NotificationGroup group = mGroupMap.get(groupKey); + if (group == null) { + group = new NotificationGroup(); + mGroupMap.put(groupKey, group); + } + if (notif.isGroupSummary()) { + group.summary = added; + group.expanded = added.row.areChildrenExpanded(); + if (!group.children.isEmpty()) { + mListener.onGroupCreatedFromChildren(group); + } + } else { + group.children.add(added); + if (group.summary != null && group.children.size() == 1 && !group.expanded) { + group.summary.row.updateExpandButton(); + } + } + } + + public void onEntryUpdated(NotificationData.Entry entry, + StatusBarNotification oldNotification) { + if (mGroupMap.get(oldNotification.getGroupKey()) != null) { + onEntryRemovedInternal(entry, oldNotification); + } + onEntryAdded(entry); + } + + public boolean isVisible(StatusBarNotification sbn) { + if (!sbn.getNotification().isGroupChild()) { + return true; + } + NotificationGroup group = mGroupMap.get(sbn.getGroupKey()); + if (group != null && group.expanded) { + return true; + } + return false; + } + + public boolean hasGroupChildren(StatusBarNotification sbn) { + if (areGroupsProhibited()) { + return false; + } + if (!sbn.getNotification().isGroupSummary()) { + return false; + } + NotificationGroup group = mGroupMap.get(sbn.getGroupKey()); + if (group == null) { + return false; + } + return !group.children.isEmpty(); + } + + public void setStatusBarState(int newState) { + if (mBarState == newState) { + return; + } + boolean prohibitedBefore = areGroupsProhibited(); + mBarState = newState; + boolean nowProhibited = areGroupsProhibited(); + if (nowProhibited != prohibitedBefore) { + if (nowProhibited) { + for (NotificationGroup group : mGroupMap.values()) { + if (group.expanded) { + setGroupExpanded(group, false); + } + } + } + mListener.onGroupsProhibitedChanged(); + } + } + + private boolean areGroupsProhibited() { + return mBarState == StatusBarState.KEYGUARD; + } + + /** + * @return whether a given notification is a child in a group which has a summary + */ + public boolean isChildInGroupWithSummary(StatusBarNotification sbn) { + if (!sbn.getNotification().isGroupChild()) { + return false; + } + NotificationGroup group = mGroupMap.get(sbn.getGroupKey()); + if (group == null || group.summary == null) { + return false; + } + return true; + } + + public ExpandableNotificationRow getGroupSummary(StatusBarNotification sbn) { + NotificationGroup group = mGroupMap.get(sbn.getGroupKey()); + return group == null ? null + : group.summary == null ? null + : group.summary.row; + } + + public static class NotificationGroup { + public final HashSet<NotificationData.Entry> children = new HashSet<>(); + public NotificationData.Entry summary; + public boolean expanded; + } + + public interface OnGroupChangeListener { + /** + * The expansion of a group has changed. + * + * @param changedRow the row for which the expansion has changed, which is also the summary + * @param expanded a boolean indicating the new expanded state + */ + void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded); + + /** + * Children group policy has changed and children may no be prohibited or allowed. + */ + void onGroupsProhibitedChanged(); + + /** + * A group of children just received a summary notification and should therefore become + * children of it. + * + * @param group the group created + */ + void onGroupCreatedFromChildren(NotificationGroup group); + } +} 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 0ae34bb..094b9b5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -32,23 +32,29 @@ import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewTreeObserver; +import android.view.WindowInsets; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; import android.widget.FrameLayout; import android.widget.TextView; +import com.android.internal.logging.MetricsLogger; import com.android.keyguard.KeyguardStatusView; -import com.android.systemui.EventLogTags; import com.android.systemui.EventLogConstants; +import com.android.systemui.EventLogTags; import com.android.systemui.R; import com.android.systemui.qs.QSContainer; import com.android.systemui.qs.QSPanel; +import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.FlingAnimationUtils; import com.android.systemui.statusbar.GestureRecorder; import com.android.systemui.statusbar.KeyguardAffordanceView; +import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.stack.StackStateAnimator; @@ -56,7 +62,8 @@ import com.android.systemui.statusbar.stack.StackStateAnimator; public class NotificationPanelView extends PanelView implements ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener, View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener, - KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener { + KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener, + HeadsUpManager.OnHeadsUpChangedListener { private static final boolean DEBUG = false; @@ -68,6 +75,10 @@ public class NotificationPanelView extends PanelView implements private static final float HEADER_RUBBERBAND_FACTOR = 2.05f; private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f; + private static final String COUNTER_PANEL_OPEN = "panel_open"; + private static final String COUNTER_PANEL_OPEN_QS = "panel_open_qs"; + private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek"; + public static final long DOZE_ANIMATION_DURATION = 700; private KeyguardAffordanceHelper mAfforanceHelper; @@ -81,7 +92,7 @@ public class NotificationPanelView extends PanelView implements private TextView mClockView; private View mReserveNotificationSpace; private View mQsNavbarScrim; - private View mNotificationContainerParent; + private NotificationsQuickSettingsContainer mNotificationContainerParent; private NotificationStackScrollLayout mNotificationStackScroller; private int mNotificationTopPadding; private boolean mAnimateNextTopPaddingChange; @@ -107,11 +118,13 @@ public class NotificationPanelView extends PanelView implements * intercepted yet. */ private boolean mIntercepting; + private boolean mPanelExpanded; private boolean mQsExpanded; private boolean mQsExpandedWhenExpandingStarted; private boolean mQsFullyExpanded; private boolean mKeyguardShowing; private boolean mDozing; + private boolean mDozingOnDown; private int mStatusBarState; private float mInitialHeightOnTouch; private float mInitialTouchX; @@ -163,7 +176,7 @@ public class NotificationPanelView extends PanelView implements private Runnable mLaunchAnimationEndRunnable; private boolean mOnlyAffordanceInThisMotion; private boolean mKeyguardStatusViewAnimating; - private boolean mHeaderAnimatingIn; + private boolean mHeaderAnimating; private ObjectAnimator mQsContainerAnimator; private ValueAnimator mQsSizeChangeAnimator; @@ -176,6 +189,26 @@ public class NotificationPanelView extends PanelView implements private float mKeyguardStatusBarAnimateAlpha = 1f; private int mOldLayoutDirection; + private HeadsUpTouchHelper mHeadsUpTouchHelper; + private boolean mIsExpansionFromHeadsUp; + private boolean mListenForHeadsUp; + private int mNavigationBarBottomHeight; + private boolean mExpandingFromHeadsUp; + private boolean mCollapsedOnDown; + private int mPositionMinSideMargin; + private int mLastOrientation = -1; + private boolean mClosingWithAlphaFadeOut; + + private Runnable mHeadsUpExistenceChangedRunnable = new Runnable() { + @Override + public void run() { + notifyBarPanelExpansionChanged(); + } + }; + + /** Interpolator to be used for animations that respond directly to a touch */ + private final Interpolator mTouchResponseInterpolator = + new PathInterpolator(0.3f, 0f, 0.1f, 1f); public NotificationPanelView(Context context, AttributeSet attrs) { super(context, attrs); @@ -200,7 +233,8 @@ public class NotificationPanelView extends PanelView implements mScrollView.setListener(this); mScrollView.setFocusable(false); mReserveNotificationSpace = findViewById(R.id.reserve_notification_space); - mNotificationContainerParent = findViewById(R.id.notification_container_parent); + mNotificationContainerParent = (NotificationsQuickSettingsContainer) + findViewById(R.id.notification_container_parent); mNotificationStackScroller = (NotificationStackScrollLayout) findViewById(R.id.notification_stack_scroller); mNotificationStackScroller.setOnHeightChangedListener(this); @@ -218,13 +252,13 @@ public class NotificationPanelView extends PanelView implements mAfforanceHelper = new KeyguardAffordanceHelper(this, getContext()); mSecureCameraLaunchManager = new SecureCameraLaunchManager(getContext(), mKeyguardBottomArea); + mLastOrientation = getResources().getConfiguration().orientation; // recompute internal state when qspanel height changes mQsContainer.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) { + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { final int height = bottom - top; final int oldHeight = oldBottom - oldTop; if (height != oldHeight) { @@ -251,6 +285,8 @@ public class NotificationPanelView extends PanelView implements getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance); mQsFalsingThreshold = getResources().getDimensionPixelSize( R.dimen.qs_falsing_threshold); + mPositionMinSideMargin = getResources().getDimensionPixelSize( + R.dimen.notification_panel_min_side_margin); } public void updateResources() { @@ -304,7 +340,7 @@ public class NotificationPanelView extends PanelView implements } else if (!mQsExpanded) { setQsExpansion(mQsMinExpansionHeight + mLastOverscroll); } - mNotificationStackScroller.setStackHeight(getExpandedHeight()); + updateStackHeight(getExpandedHeight()); updateHeader(); mNotificationStackScroller.updateIsSmallScreen( mHeader.getCollapsedHeight() + mQsPeekHeight); @@ -317,6 +353,7 @@ public class NotificationPanelView extends PanelView implements if (mQsSizeChangeAnimator == null) { mQsContainer.setHeightOverride(mQsContainer.getDesiredHeight()); } + updateMaxHeadsUpTranslation(); } @Override @@ -450,10 +487,11 @@ public class NotificationPanelView extends PanelView implements mStatusBar.dismissPopups(); mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */, true /* cancelAnimators */); + mNotificationStackScroller.resetScrollPosition(); } public void closeQs() { - cancelAnimation(); + cancelQsAnimation(); setQsExpansion(mQsMinExpansionHeight); } @@ -470,7 +508,7 @@ public class NotificationPanelView extends PanelView implements } public void openQs() { - cancelAnimation(); + cancelQsAnimation(); if (mQsExpansionEnabled) { setQsExpansion(mQsMaxExpansionHeight); } @@ -493,14 +531,21 @@ public class NotificationPanelView extends PanelView implements } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + protected void flingToHeight(float vel, boolean expand, float target, + float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) { + mHeadsUpTouchHelper.notifyFling(!expand); + setClosingWithAlphaFadeout(!expand && getFadeoutAlpha() == 1.0f); + super.flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); + } + + @Override + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { event.getText().add(getKeyguardOrLockScreenString()); mLastAnnouncementWasQuickSettings = false; return true; } - - return super.dispatchPopulateAccessibilityEvent(event); + return super.dispatchPopulateAccessibilityEventInternal(event); } @Override @@ -508,7 +553,20 @@ public class NotificationPanelView extends PanelView implements if (mBlockTouches) { return false; } - resetDownStates(event); + initDownStates(event); + if (mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { + mIsExpansionFromHeadsUp = true; + MetricsLogger.count(mContext, COUNTER_PANEL_OPEN, 1); + MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_PEEK, 1); + return true; + } + if (!isFullyCollapsed() && onQsIntercept(event)) { + return true; + } + return super.onInterceptTouchEvent(event); + } + + private boolean onQsIntercept(MotionEvent event) { int pointerIndex = event.findPointerIndex(mTrackingPointer); if (pointerIndex < 0) { pointerIndex = 0; @@ -576,28 +634,30 @@ public class NotificationPanelView extends PanelView implements case MotionEvent.ACTION_UP: trackMovement(event); if (mQsTracking) { - flingQsWithCurrentVelocity( + flingQsWithCurrentVelocity(y, event.getActionMasked() == MotionEvent.ACTION_CANCEL); mQsTracking = false; } mIntercepting = false; break; } - return super.onInterceptTouchEvent(event); + return false; } @Override protected boolean isInContentBounds(float x, float y) { - float yTransformed = y - mNotificationStackScroller.getY(); float stackScrollerX = mNotificationStackScroller.getX(); - return mNotificationStackScroller.isInContentBounds(yTransformed) && stackScrollerX < x - && x < stackScrollerX + mNotificationStackScroller.getWidth(); + return !mNotificationStackScroller.isBelowLastNotification(x - stackScrollerX, y) + && stackScrollerX < x && x < stackScrollerX + mNotificationStackScroller.getWidth(); } - private void resetDownStates(MotionEvent event) { + private void initDownStates(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mOnlyAffordanceInThisMotion = false; mQsTouchAboveFalsingThreshold = mQsFullyExpanded; + mDozingOnDown = isDozing(); + mCollapsedOnDown = isFullyCollapsed(); + mListenForHeadsUp = mCollapsedOnDown && mHeadsUpManager.hasPinnedHeadsUp(); } } @@ -612,9 +672,24 @@ public class NotificationPanelView extends PanelView implements super.requestDisallowInterceptTouchEvent(disallowIntercept); } - private void flingQsWithCurrentVelocity(boolean isCancelMotionEvent) { + private void flingQsWithCurrentVelocity(float y, boolean isCancelMotionEvent) { float vel = getCurrentVelocity(); - flingSettings(vel, flingExpandsQs(vel) && !isCancelMotionEvent); + final boolean expandsQs = flingExpandsQs(vel); + if (expandsQs) { + logQsSwipeDown(y); + } + flingSettings(vel, expandsQs && !isCancelMotionEvent); + } + + private void logQsSwipeDown(float y) { + float vel = getCurrentVelocity(); + final int gesture = mStatusBarState == StatusBarState.KEYGUARD + ? EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_DOWN_QS + : EventLogConstants.SYSUI_SHADE_GESTURE_SWIPE_DOWN_QS; + EventLogTags.writeSysuiLockscreenGesture( + gesture, + (int) ((y - mInitialTouchY) / mStatusBar.getDisplayDensity()), + (int) (vel / mStatusBar.getDisplayDensity())); } private boolean flingExpandsQs(float vel) { @@ -642,7 +717,12 @@ public class NotificationPanelView extends PanelView implements if (mBlockTouches) { return false; } - resetDownStates(event); + initDownStates(event); + if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp() + && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { + mIsExpansionFromHeadsUp = true; + MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_PEEK, 1); + } if ((!mIsExpanding || mHintAnimationRunning) && !mQsExpanded && mStatusBar.getBarState() != StatusBarState.SHADE) { @@ -651,7 +731,21 @@ public class NotificationPanelView extends PanelView implements if (mOnlyAffordanceInThisMotion) { return true; } - if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f + mHeadsUpTouchHelper.onTouchEvent(event); + if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) { + return true; + } + if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) { + MetricsLogger.count(mContext, COUNTER_PANEL_OPEN, 1); + updateVerticalPanelPosition(event.getX()); + } + super.onTouchEvent(event); + return true; + } + + private boolean handleQsTouch(MotionEvent event) { + final int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f && mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded && mQsExpansionEnabled) { @@ -663,7 +757,7 @@ public class NotificationPanelView extends PanelView implements mInitialTouchY = event.getX(); mInitialTouchX = event.getY(); } - if (mExpandedHeight != 0) { + if (!isFullyCollapsed()) { handleQsDown(event); } if (!mQsExpandImmediate && mQsTracking) { @@ -672,17 +766,23 @@ public class NotificationPanelView extends PanelView implements return true; } } - if (event.getActionMasked() == MotionEvent.ACTION_CANCEL - || event.getActionMasked() == MotionEvent.ACTION_UP) { + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mConflictingQsExpansionGesture = false; } - if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mExpandedHeight == 0 + if (action == MotionEvent.ACTION_DOWN && isFullyCollapsed() && mQsExpansionEnabled) { mTwoFingerQsExpandPossible = true; } - if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN - && event.getPointerCount() == 2 + final int pointerCount = event.getPointerCount(); + final boolean twoFingerDrag = action == MotionEvent.ACTION_POINTER_DOWN + && pointerCount == 2; + final boolean stylusClickDrag = action == MotionEvent.ACTION_DOWN + && pointerCount == 1 && event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS + && (event.isButtonPressed(MotionEvent.BUTTON_SECONDARY) + || event.isButtonPressed(MotionEvent.BUTTON_TERTIARY)); + if (mTwoFingerQsExpandPossible && (twoFingerDrag || stylusClickDrag) && event.getY(event.getActionIndex()) < mStatusBarMinHeight) { + MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_QS, 1); mQsExpandImmediate = true; requestPanelHeightUpdate(); @@ -690,12 +790,11 @@ public class NotificationPanelView extends PanelView implements // earlier so the state is already up to date when dragging down. setListening(true); } - super.onTouchEvent(event); - return true; + return false; } private boolean isInQsArea(float x, float y) { - return (x >= mScrollView.getLeft() && x <= mScrollView.getRight()) && + return (x >= mScrollView.getX() && x <= mScrollView.getX() + mScrollView.getWidth()) && (y <= mNotificationStackScroller.getBottomMostNotificationBottom() || y <= mQsContainer.getY() + mQsContainer.getHeight()); } @@ -717,8 +816,8 @@ public class NotificationPanelView extends PanelView implements } @Override - protected boolean flingExpands(float vel, float vectorVel) { - boolean expands = super.flingExpands(vel, vectorVel); + protected boolean flingExpands(float vel, float vectorVel, float x, float y) { + boolean expands = super.flingExpands(vel, vectorVel, x, y); // If we are already running a QS expansion, make sure that we keep the panel open. if (mQsExpansionAnimator != null) { @@ -732,6 +831,11 @@ public class NotificationPanelView extends PanelView implements return mStatusBar.getBarState() != StatusBarState.SHADE; } + @Override + protected boolean shouldGestureIgnoreXTouchSlop(float x, float y) { + return !mAfforanceHelper.isOnAffordanceIcon(x, y); + } + private void onQsTouch(MotionEvent event) { int pointerIndex = event.findPointerIndex(mTrackingPointer); if (pointerIndex < 0) { @@ -740,6 +844,7 @@ public class NotificationPanelView extends PanelView implements } final float y = event.getY(pointerIndex); final float x = event.getX(pointerIndex); + final float h = y - mInitialTouchY; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: @@ -767,7 +872,6 @@ public class NotificationPanelView extends PanelView implements break; case MotionEvent.ACTION_MOVE: - final float h = y - mInitialTouchY; setQsExpansion(h + mInitialHeightOnTouch); if (h >= getFalsingThreshold()) { mQsTouchAboveFalsingThreshold = true; @@ -783,9 +887,10 @@ public class NotificationPanelView extends PanelView implements float fraction = getQsExpansionFraction(); if ((fraction != 0f || y >= mInitialTouchY) && (fraction != 1f || y <= mInitialTouchY)) { - flingQsWithCurrentVelocity( + flingQsWithCurrentVelocity(y, event.getActionMasked() == MotionEvent.ACTION_CANCEL); } else { + logQsSwipeDown(y); mScrollYOverride = -1; } if (mVelocityTracker != null) { @@ -815,7 +920,7 @@ public class NotificationPanelView extends PanelView implements @Override public void onOverscrollTopChanged(float amount, boolean isRubberbanded) { - cancelAnimation(); + cancelQsAnimation(); if (!mQsExpansionEnabled) { amount = 0f; } @@ -833,13 +938,13 @@ public class NotificationPanelView extends PanelView implements setQsExpansion(mQsExpansionHeight); flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, open && mQsExpansionEnabled, new Runnable() { - @Override - public void run() { - mStackScrollerOverscrolling = false; - mQsExpansionFromOverscroll = false; - updateQsState(); - } - }); + @Override + public void run() { + mStackScrollerOverscrolling = false; + mQsExpansionFromOverscroll = false; + updateQsState(); + } + }, false /* isClick */); } private void onQsExpansionStarted() { @@ -847,7 +952,8 @@ public class NotificationPanelView extends PanelView implements } private void onQsExpansionStarted(int overscrollAmount) { - cancelAnimation(); + cancelQsAnimation(); + cancelHeightAnimator(); // Reset scroll position and apply that position to the expanded height. float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount; @@ -868,31 +974,41 @@ public class NotificationPanelView extends PanelView implements mNotificationStackScroller.setInterceptDelegateEnabled(expanded); mStatusBar.setQsExpanded(expanded); mQsPanel.setExpanded(expanded); + mNotificationContainerParent.setQsExpanded(expanded); } } public void setBarState(int statusBarState, boolean keyguardFadingAway, boolean goingToFullShade) { - boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD - || statusBarState == StatusBarState.SHADE_LOCKED; - if (!mKeyguardShowing && keyguardShowing) { - setQsTranslation(mQsExpansionHeight); - mHeader.setTranslationY(0f); - } + int oldState = mStatusBarState; + boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD; setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade); setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); - if (goingToFullShade) { + + mStatusBarState = statusBarState; + mKeyguardShowing = keyguardShowing; + + if (goingToFullShade || (oldState == StatusBarState.KEYGUARD + && statusBarState == StatusBarState.SHADE_LOCKED)) { animateKeyguardStatusBarOut(); + animateHeaderSlidingIn(); + } else if (oldState == StatusBarState.SHADE_LOCKED + && statusBarState == StatusBarState.KEYGUARD) { + animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD); + animateHeaderSlidingOut(); } else { mKeyguardStatusBar.setAlpha(1f); mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE); + if (keyguardShowing && oldState != mStatusBarState) { + mKeyguardBottomArea.updateLeftAffordance(); + mAfforanceHelper.updatePreviews(); + } } - mStatusBarState = statusBarState; - mKeyguardShowing = keyguardShowing; - updateQsState(); - if (goingToFullShade) { - animateHeaderSlidingIn(); + if (keyguardShowing) { + updateDozingVisibilities(false /* animate */); } + resetVerticalPanelPosition(); + updateQsState(); } private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() { @@ -914,7 +1030,7 @@ public class NotificationPanelView extends PanelView implements = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mHeaderAnimatingIn = false; + mHeaderAnimating = false; mQsContainerAnimator = null; mQsContainer.removeOnLayoutChangeListener(mQsContainerAnimatorUpdater); } @@ -942,10 +1058,13 @@ public class NotificationPanelView extends PanelView implements @Override public boolean onPreDraw() { getViewTreeObserver().removeOnPreDrawListener(this); + long delay = mStatusBarState == StatusBarState.SHADE_LOCKED + ? 0 + : mStatusBar.calculateGoingToFullShadeDelay(); mHeader.setTranslationY(-mHeader.getCollapsedHeight() - mQsPeekHeight); mHeader.animate() .translationY(0f) - .setStartDelay(mStatusBar.calculateGoingToFullShadeDelay()) + .setStartDelay(delay) .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE) .setInterpolator(mFastOutSlowInInterpolator) .start(); @@ -954,7 +1073,7 @@ public class NotificationPanelView extends PanelView implements mQsContainer.getTranslationY(), mHeader.getCollapsedHeight() + mQsPeekHeight - mQsContainer.getHeight() - mQsContainer.getTop()); - mQsContainerAnimator.setStartDelay(mStatusBar.calculateGoingToFullShadeDelay()); + mQsContainerAnimator.setStartDelay(delay); mQsContainerAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE); mQsContainerAnimator.setInterpolator(mFastOutSlowInInterpolator); mQsContainerAnimator.addListener(mAnimateHeaderSlidingInListener); @@ -963,11 +1082,37 @@ public class NotificationPanelView extends PanelView implements return true; } }; - + private void animateHeaderSlidingIn() { - mHeaderAnimatingIn = true; - getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn); + // If the QS is already expanded we don't need to slide in the header as it's already + // visible. + if (!mQsExpanded) { + mHeaderAnimating = true; + getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn); + } + } + private void animateHeaderSlidingOut() { + mHeaderAnimating = true; + mHeader.animate().y(-mHeader.getHeight()) + .setStartDelay(0) + .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD) + .setInterpolator(mFastOutSlowInInterpolator) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mHeader.animate().setListener(null); + mHeaderAnimating = false; + updateQsState(); + } + }) + .start(); + mQsContainer.animate() + .y(-mQsContainer.getHeight()) + .setStartDelay(0) + .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD) + .setInterpolator(mFastOutSlowInInterpolator) + .start(); } private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() { @@ -980,34 +1125,41 @@ public class NotificationPanelView extends PanelView implements }; private void animateKeyguardStatusBarOut() { - mKeyguardStatusBar.animate() - .alpha(0f) - .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) - .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) - .setInterpolator(PhoneStatusBar.ALPHA_OUT) - .setUpdateListener(mStatusBarAnimateAlphaListener) - .withEndAction(mAnimateKeyguardStatusBarInvisibleEndRunnable) - .start(); + ValueAnimator anim = ValueAnimator.ofFloat(mKeyguardStatusBar.getAlpha(), 0f); + anim.addUpdateListener(mStatusBarAnimateAlphaListener); + anim.setStartDelay(mStatusBar.isKeyguardFadingAway() + ? mStatusBar.getKeyguardFadingAwayDelay() + : 0); + anim.setDuration(mStatusBar.isKeyguardFadingAway() + ? mStatusBar.getKeyguardFadingAwayDuration() / 2 + : StackStateAnimator.ANIMATION_DURATION_STANDARD); + anim.setInterpolator(mDozeAnimationInterpolator); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimateKeyguardStatusBarInvisibleEndRunnable.run(); + } + }); + anim.start(); } private final ValueAnimator.AnimatorUpdateListener mStatusBarAnimateAlphaListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { - mKeyguardStatusBarAnimateAlpha = mKeyguardStatusBar.getAlpha(); + mKeyguardStatusBarAnimateAlpha = (float) animation.getAnimatedValue(); + updateHeaderKeyguardAlpha(); } }; - private void animateKeyguardStatusBarIn() { + private void animateKeyguardStatusBarIn(long duration) { mKeyguardStatusBar.setVisibility(View.VISIBLE); mKeyguardStatusBar.setAlpha(0f); - mKeyguardStatusBar.animate() - .alpha(1f) - .setStartDelay(0) - .setDuration(DOZE_ANIMATION_DURATION) - .setInterpolator(mDozeAnimationInterpolator) - .setUpdateListener(mStatusBarAnimateAlphaListener) - .start(); + ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); + anim.addUpdateListener(mStatusBarAnimateAlphaListener); + anim.setDuration(duration); + anim.setInterpolator(mDozeAnimationInterpolator); + anim.start(); } private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() { @@ -1024,14 +1176,16 @@ public class NotificationPanelView extends PanelView implements mKeyguardBottomArea.animate() .alpha(0f) .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) - .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) + .setDuration(mStatusBar.getKeyguardFadingAwayDuration() / 2) .setInterpolator(PhoneStatusBar.ALPHA_OUT) .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable) .start(); } else if (statusBarState == StatusBarState.KEYGUARD || statusBarState == StatusBarState.SHADE_LOCKED) { mKeyguardBottomArea.animate().cancel(); - mKeyguardBottomArea.setVisibility(View.VISIBLE); + if (!mDozing) { + mKeyguardBottomArea.setVisibility(View.VISIBLE); + } mKeyguardBottomArea.setAlpha(1f); } else { mKeyguardBottomArea.animate().cancel(); @@ -1084,9 +1238,12 @@ public class NotificationPanelView extends PanelView implements } private void updateQsState() { - boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling; - mHeader.setVisibility((mQsExpanded || !mKeyguardShowing) ? View.VISIBLE : View.INVISIBLE); - mHeader.setExpanded(mKeyguardShowing || (mQsExpanded && !mStackScrollerOverscrolling)); + boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling || mHeaderAnimating; + mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating) + ? View.VISIBLE + : View.INVISIBLE); + mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating) + || (mQsExpanded && !mStackScrollerOverscrolling)); mNotificationStackScroller.setScrollingEnabled( mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded || mQsExpansionFromOverscroll)); @@ -1111,7 +1268,7 @@ public class NotificationPanelView extends PanelView implements setQsExpanded(true); } else if (height <= mQsMinExpansionHeight && mQsExpanded) { setQsExpanded(false); - if (mLastAnnouncementWasQuickSettings && !mTracking) { + if (mLastAnnouncementWasQuickSettings && !mTracking && !isCollapsing()) { announceForAccessibility(getKeyguardOrLockScreenString()); mLastAnnouncementWasQuickSettings = false; } @@ -1124,6 +1281,10 @@ public class NotificationPanelView extends PanelView implements if (mKeyguardShowing) { updateHeaderKeyguard(); } + if (mStatusBarState == StatusBarState.SHADE_LOCKED + || mStatusBarState == StatusBarState.KEYGUARD) { + updateKeyguardBottomAreaAlpha(); + } if (mStatusBarState == StatusBarState.SHADE && mQsExpanded && !mStackScrollerOverscrolling && mQsScrimEnabled) { mQsNavbarScrim.setAlpha(getQsExpansionFraction()); @@ -1164,10 +1325,10 @@ public class NotificationPanelView extends PanelView implements } private void setQsTranslation(float height) { - if (!mHeaderAnimatingIn) { + if (!mHeaderAnimating) { mQsContainer.setY(height - mQsContainer.getDesiredHeight() + getHeaderTranslation()); } - if (mKeyguardShowing) { + if (mKeyguardShowing && !mHeaderAnimating) { mHeader.setY(interpolate(getQsExpansionFraction(), -mHeader.getHeight(), 0)); } } @@ -1233,17 +1394,18 @@ public class NotificationPanelView extends PanelView implements return mVelocityTracker.getYVelocity(); } - private void cancelAnimation() { + private void cancelQsAnimation() { if (mQsExpansionAnimator != null) { mQsExpansionAnimator.cancel(); } } private void flingSettings(float vel, boolean expand) { - flingSettings(vel, expand, null); + flingSettings(vel, expand, null, false /* isClick */); } - private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) { + private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable, + boolean isClick) { float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight; if (target == mQsExpansionHeight) { mScrollYOverride = -1; @@ -1258,7 +1420,12 @@ public class NotificationPanelView extends PanelView implements } mScrollView.setBlockFlinging(true); ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target); - mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); + if (isClick) { + animator.setInterpolator(mTouchResponseInterpolator); + animator.setDuration(368); + } else { + mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); + } if (belowFalsingThreshold) { animator.setDuration(350); } @@ -1288,11 +1455,11 @@ public class NotificationPanelView extends PanelView implements * @return Whether we should intercept a gesture to open Quick Settings. */ private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { - if (!mQsExpansionEnabled) { + if (!mQsExpansionEnabled || mCollapsedOnDown) { return false; } View header = mKeyguardShowing ? mKeyguardStatusBar : mHeader; - boolean onHeader = x >= header.getLeft() && x <= header.getRight() + boolean onHeader = x >= header.getX() && x <= header.getX() + header.getWidth() && y >= header.getTop() && y <= header.getBottom(); if (mQsExpanded) { return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0) && isInQsArea(x, y); @@ -1359,15 +1526,26 @@ public class NotificationPanelView extends PanelView implements setQsExpansion(mQsMinExpansionHeight + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight)); } - mNotificationStackScroller.setStackHeight(expandedHeight); + updateStackHeight(expandedHeight); updateHeader(); updateUnlockIcon(); updateNotificationTranslucency(); + updatePanelExpanded(); + mNotificationStackScroller.setShadeExpanded(!isFullyCollapsed()); if (DEBUG) { invalidate(); } } + private void updatePanelExpanded() { + boolean isExpanded = !isFullyCollapsed(); + if (mPanelExpanded != isExpanded) { + mHeadsUpManager.setIsExpanded(isExpanded); + mStatusBar.setPanelExpanded(isExpanded); + mPanelExpanded = isExpanded; + } + } + /** * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when * collapsing QS / the panel when QS was scrolled @@ -1432,18 +1610,20 @@ public class NotificationPanelView extends PanelView implements } } private void updateNotificationTranslucency() { + float alpha = 1f; + if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp && !mHeadsUpManager.hasPinnedHeadsUp()) { + alpha = getFadeoutAlpha(); + } + mNotificationStackScroller.setAlpha(alpha); + } + + private float getFadeoutAlpha() { float alpha = (getNotificationsTopY() + mNotificationStackScroller.getItemHeight()) / (mQsMinExpansionHeight + mNotificationStackScroller.getBottomStackPeekSize() - - mNotificationStackScroller.getCollapseSecondCardPadding()); + - mNotificationStackScroller.getCollapseSecondCardPadding()); alpha = Math.max(0, Math.min(alpha, 1)); alpha = (float) Math.pow(alpha, 0.75); - if (alpha != 1f && mNotificationStackScroller.getLayerType() != LAYER_TYPE_HARDWARE) { - mNotificationStackScroller.setLayerType(LAYER_TYPE_HARDWARE, null); - } else if (alpha == 1f - && mNotificationStackScroller.getLayerType() == LAYER_TYPE_HARDWARE) { - mNotificationStackScroller.setLayerType(LAYER_TYPE_NONE, null); - } - mNotificationStackScroller.setAlpha(alpha); + return alpha; } @Override @@ -1466,7 +1646,7 @@ public class NotificationPanelView extends PanelView implements lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150, mFastOutLinearInterpolator); } else if (!active && mUnlockIconActive && mTracking) { - lockIcon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, true, + lockIcon.setImageAlpha(lockIcon.getRestingAlpha(), true /* animate */, 150, mFastOutLinearInterpolator, null); lockIcon.setImageScale(1.0f, true, 150, mFastOutLinearInterpolator); @@ -1479,8 +1659,7 @@ public class NotificationPanelView extends PanelView implements * Hides the header when notifications are colliding with it. */ private void updateHeader() { - if (mStatusBar.getBarState() == StatusBarState.KEYGUARD - || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { + if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { updateHeaderKeyguard(); } else { updateHeaderShade(); @@ -1489,15 +1668,14 @@ public class NotificationPanelView extends PanelView implements } private void updateHeaderShade() { - if (!mHeaderAnimatingIn) { + if (!mHeaderAnimating) { mHeader.setTranslationY(getHeaderTranslation()); } setQsTranslation(mQsExpansionHeight); } private float getHeaderTranslation() { - if (mStatusBar.getBarState() == StatusBarState.KEYGUARD - || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { + if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { return 0; } if (mNotificationStackScroller.getNotGoneChildCount() == 0) { @@ -1507,33 +1685,60 @@ public class NotificationPanelView extends PanelView implements return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight; } } - return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR; + float stackTranslation = mNotificationStackScroller.getStackTranslation(); + float translation = stackTranslation / HEADER_RUBBERBAND_FACTOR; + if (mHeadsUpManager.hasPinnedHeadsUp() || mIsExpansionFromHeadsUp) { + translation = mNotificationStackScroller.getTopPadding() + stackTranslation + - mNotificationTopPadding - mQsMinExpansionHeight; + } + return Math.min(0, translation); } - private void updateHeaderKeyguard() { - float alphaNotifications; + /** + * @return the alpha to be used to fade out the contents on Keyguard (status bar, bottom area) + * during swiping up + */ + private float getKeyguardContentsAlpha() { + float alpha; if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { // When on Keyguard, we hide the header as soon as the top card of the notification // stack scroller is close enough (collision distance) to the bottom of the header. - alphaNotifications = getNotificationsTopY() + alpha = getNotificationsTopY() / (mKeyguardStatusBar.getHeight() + mNotificationsHeaderCollideDistance); } else { // In SHADE_LOCKED, the top card is already really close to the header. Hide it as // soon as we start translating the stack. - alphaNotifications = getNotificationsTopY() / mKeyguardStatusBar.getHeight(); + alpha = getNotificationsTopY() / mKeyguardStatusBar.getHeight(); } - alphaNotifications = MathUtils.constrain(alphaNotifications, 0, 1); - alphaNotifications = (float) Math.pow(alphaNotifications, 0.75); + alpha = MathUtils.constrain(alpha, 0, 1); + alpha = (float) Math.pow(alpha, 0.75); + return alpha; + } + + private void updateHeaderKeyguardAlpha() { float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2); - mKeyguardStatusBar.setAlpha(Math.min(alphaNotifications, alphaQsExpansion) + mKeyguardStatusBar.setAlpha(Math.min(getKeyguardContentsAlpha(), alphaQsExpansion) * mKeyguardStatusBarAnimateAlpha); - mKeyguardBottomArea.setAlpha(Math.min(1 - getQsExpansionFraction(), alphaNotifications)); + mKeyguardStatusBar.setVisibility(mKeyguardStatusBar.getAlpha() != 0f + && !mDozing ? VISIBLE : INVISIBLE); + } + + private void updateHeaderKeyguard() { + updateHeaderKeyguardAlpha(); setQsTranslation(mQsExpansionHeight); } + private void updateKeyguardBottomAreaAlpha() { + float alpha = Math.min(getKeyguardContentsAlpha(), 1 - getQsExpansionFraction()); + mKeyguardBottomArea.setAlpha(alpha); + mKeyguardBottomArea.setImportantForAccessibility(alpha == 0f + ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : IMPORTANT_FOR_ACCESSIBILITY_AUTO); + } + private float getNotificationsTopY() { if (mNotificationStackScroller.getNotGoneChildCount() == 0) { return getExpandedHeight(); @@ -1556,15 +1761,20 @@ public class NotificationPanelView extends PanelView implements protected void onExpandingFinished() { super.onExpandingFinished(); mNotificationStackScroller.onExpansionStopped(); + mHeadsUpManager.onExpandingFinished(); mIsExpanding = false; mScrollYOverride = -1; - if (mExpandedHeight == 0f) { + if (isFullyCollapsed()) { setListening(false); } else { setListening(true); } mQsExpandImmediate = false; mTwoFingerQsExpandPossible = false; + mIsExpansionFromHeadsUp = false; + mNotificationStackScroller.setTrackingHeadsUp(false); + mExpandingFromHeadsUp = false; + setPanelScrimMinFraction(0.0f); } private void setListening(boolean listening) { @@ -1607,6 +1817,7 @@ public class NotificationPanelView extends PanelView implements || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { mAfforanceHelper.animateHideLeftRightIcon(); } + mNotificationStackScroller.onPanelTrackingStarted(); } @Override @@ -1616,6 +1827,7 @@ public class NotificationPanelView extends PanelView implements mNotificationStackScroller.setOverScrolledPixels( 0.0f, true /* onTop */, true /* animate */); } + mNotificationStackScroller.onPanelTrackingStopped(); if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { if (!mHintAnimationRunning) { @@ -1631,7 +1843,7 @@ public class NotificationPanelView extends PanelView implements } @Override - public void onHeightChanged(ExpandableView view) { + public void onHeightChanged(ExpandableView view, boolean needsAnimation) { // Block update if we are in quick settings and just the top padding changed // (i.e. view == null). @@ -1657,6 +1869,21 @@ public class NotificationPanelView extends PanelView implements protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mAfforanceHelper.onConfigurationChanged(); + if (newConfig.orientation != mLastOrientation) { + resetVerticalPanelPosition(); + } + mLastOrientation = newConfig.orientation; + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + mNavigationBarBottomHeight = insets.getSystemWindowInsetBottom(); + updateMaxHeadsUpTranslation(); + return insets; + } + + private void updateMaxHeadsUpTranslation() { + mNotificationStackScroller.setHeadsUpBoundaries(getHeight(), mNavigationBarBottomHeight); } @Override @@ -1672,9 +1899,12 @@ public class NotificationPanelView extends PanelView implements if (v == mHeader) { onQsExpansionStarted(); if (mQsExpanded) { - flingSettings(0 /* vel */, false /* expand */); + flingSettings(0 /* vel */, false /* expand */, null, true /* isClick */); } else if (mQsExpansionEnabled) { - flingSettings(0 /* vel */, true /* expand */); + EventLogTags.writeSysuiLockscreenGesture( + EventLogConstants.SYSUI_TAP_TO_OPEN_QS, + 0, 0); + flingSettings(0 /* vel */, true /* expand */, null, true /* isClick */); } } } @@ -1690,7 +1920,7 @@ public class NotificationPanelView extends PanelView implements if (start) { EventLogTags.writeSysuiLockscreenGesture( EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_DIALER, lengthDp, velocityDp); - mKeyguardBottomArea.launchPhone(); + mKeyguardBottomArea.launchLeftAffordance(); } else { EventLogTags.writeSysuiLockscreenGesture( EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_CAMERA, lengthDp, velocityDp); @@ -1711,29 +1941,6 @@ public class NotificationPanelView extends PanelView implements } @Override - protected void onEdgeClicked(boolean right) { - if ((right && getRightIcon().getVisibility() != View.VISIBLE) - || (!right && getLeftIcon().getVisibility() != View.VISIBLE) - || isDozing()) { - return; - } - mHintAnimationRunning = true; - mAfforanceHelper.startHintAnimation(right, new Runnable() { - @Override - public void run() { - mHintAnimationRunning = false; - mStatusBar.onHintFinished(); - } - }); - boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right; - if (start) { - mStatusBar.onPhoneHintStarted(); - } else { - mStatusBar.onCameraHintStarted(); - } - } - - @Override protected void startUnlockHintAnimation() { super.startUnlockHintAnimation(); startHighlightIconAnimation(getCenterIcon()); @@ -1747,30 +1954,66 @@ public class NotificationPanelView extends PanelView implements mFastOutSlowInInterpolator, new Runnable() { @Override public void run() { - icon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, - true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION, + icon.setImageAlpha(icon.getRestingAlpha(), + true /* animate */, KeyguardAffordanceHelper.HINT_PHASE1_DURATION, mFastOutSlowInInterpolator, null); } }); } @Override - public float getPageWidth() { - return getWidth(); + public float getMaxTranslationDistance() { + return (float) Math.hypot(getWidth(), getHeight()); } @Override - public void onSwipingStarted() { - mSecureCameraLaunchManager.onSwipingStarted(); + public void onSwipingStarted(boolean rightIcon) { + boolean camera = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? !rightIcon + : rightIcon; + if (camera) { + mSecureCameraLaunchManager.onSwipingStarted(); + mKeyguardBottomArea.bindCameraPrewarmService(); + } requestDisallowInterceptTouchEvent(true); mOnlyAffordanceInThisMotion = true; + mQsTracking = false; + } + + @Override + public void onSwipingAborted() { + mKeyguardBottomArea.unbindCameraPrewarmService(false /* launched */); + } + + @Override + public void onIconClicked(boolean rightIcon) { + if (mHintAnimationRunning) { + return; + } + mHintAnimationRunning = true; + mAfforanceHelper.startHintAnimation(rightIcon, new Runnable() { + @Override + public void run() { + mHintAnimationRunning = false; + mStatusBar.onHintFinished(); + } + }); + rightIcon = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? !rightIcon : rightIcon; + if (rightIcon) { + mStatusBar.onCameraHintStarted(); + } else { + if (mKeyguardBottomArea.isLeftVoiceAssist()) { + mStatusBar.onVoiceAssistHintStarted(); + } else { + mStatusBar.onPhoneHintStarted(); + } + } } @Override public KeyguardAffordanceView getLeftIcon() { return getLayoutDirection() == LAYOUT_DIRECTION_RTL - ? mKeyguardBottomArea.getCameraView() - : mKeyguardBottomArea.getPhoneView(); + ? mKeyguardBottomArea.getRightView() + : mKeyguardBottomArea.getLeftView(); } @Override @@ -1781,22 +2024,22 @@ public class NotificationPanelView extends PanelView implements @Override public KeyguardAffordanceView getRightIcon() { return getLayoutDirection() == LAYOUT_DIRECTION_RTL - ? mKeyguardBottomArea.getPhoneView() - : mKeyguardBottomArea.getCameraView(); + ? mKeyguardBottomArea.getLeftView() + : mKeyguardBottomArea.getRightView(); } @Override public View getLeftPreview() { return getLayoutDirection() == LAYOUT_DIRECTION_RTL - ? mKeyguardBottomArea.getCameraPreview() - : mKeyguardBottomArea.getPhonePreview(); + ? mKeyguardBottomArea.getRightPreview() + : mKeyguardBottomArea.getLeftPreview(); } @Override public View getRightPreview() { return getLayoutDirection() == LAYOUT_DIRECTION_RTL - ? mKeyguardBottomArea.getPhonePreview() - : mKeyguardBottomArea.getCameraPreview(); + ? mKeyguardBottomArea.getLeftPreview() + : mKeyguardBottomArea.getRightPreview(); } @Override @@ -1898,6 +2141,12 @@ public class NotificationPanelView extends PanelView implements public void setDozing(boolean dozing, boolean animate) { if (dozing == mDozing) return; mDozing = dozing; + if (mStatusBarState == StatusBarState.KEYGUARD) { + updateDozingVisibilities(animate); + } + } + + private void updateDozingVisibilities(boolean animate) { if (mDozing) { mKeyguardStatusBar.setVisibility(View.INVISIBLE); mKeyguardBottomArea.setVisibility(View.INVISIBLE); @@ -1905,7 +2154,7 @@ public class NotificationPanelView extends PanelView implements mKeyguardBottomArea.setVisibility(View.VISIBLE); mKeyguardStatusBar.setVisibility(View.VISIBLE); if (animate) { - animateKeyguardStatusBarIn(); + animateKeyguardStatusBarIn(DOZE_ANIMATION_DURATION); mKeyguardBottomArea.startFinishDozeAnimation(); } } @@ -1955,6 +2204,32 @@ public class NotificationPanelView extends PanelView implements onEmptySpaceClick(x); } + protected boolean onMiddleClicked() { + switch (mStatusBar.getBarState()) { + case StatusBarState.KEYGUARD: + if (!mDozingOnDown) { + EventLogTags.writeSysuiLockscreenGesture( + EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_TAP_UNLOCK_HINT, + 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); + startUnlockHintAnimation(); + } + return true; + case StatusBarState.SHADE_LOCKED: + if (!mQsExpanded) { + mStatusBar.goToKeyguard(); + } + return true; + case StatusBarState.SHADE: + + // This gets called in the middle of the touch handling, where the state is still + // that we are tracking the panel. Collapse the panel after this is done. + post(mPostCollapseRunnable); + return false; + default: + return true; + } + } + @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); @@ -1980,4 +2255,101 @@ public class NotificationPanelView extends PanelView implements mNotificationStackScroller.getTopPadding(), p); } } + + @Override + public void onHeadsUpPinnedModeChanged(final boolean inPinnedMode) { + if (inPinnedMode) { + mHeadsUpExistenceChangedRunnable.run(); + updateNotificationTranslucency(); + } else { + mNotificationStackScroller.runAfterAnimationFinished( + mHeadsUpExistenceChangedRunnable); + } + } + + @Override + public void onHeadsUpPinned(ExpandableNotificationRow headsUp) { + mNotificationStackScroller.generateHeadsUpAnimation(headsUp, true); + } + + @Override + public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) { + } + + @Override + public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) { + mNotificationStackScroller.generateHeadsUpAnimation(entry.row, isHeadsUp); + } + + @Override + public void setHeadsUpManager(HeadsUpManager headsUpManager) { + super.setHeadsUpManager(headsUpManager); + mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager, mNotificationStackScroller, + this); + } + + public void setTrackingHeadsUp(boolean tracking) { + if (tracking) { + mNotificationStackScroller.setTrackingHeadsUp(true); + mExpandingFromHeadsUp = true; + } + // otherwise we update the state when the expansion is finished + } + + @Override + protected void onClosingFinished() { + super.onClosingFinished(); + resetVerticalPanelPosition(); + setClosingWithAlphaFadeout(false); + } + + private void setClosingWithAlphaFadeout(boolean closing) { + mClosingWithAlphaFadeOut = closing; + mNotificationStackScroller.forceNoOverlappingRendering(closing); + } + + /** + * Updates the vertical position of the panel so it is positioned closer to the touch + * responsible for opening the panel. + * + * @param x the x-coordinate the touch event + */ + private void updateVerticalPanelPosition(float x) { + if (mNotificationStackScroller.getWidth() * 1.75f > getWidth()) { + resetVerticalPanelPosition(); + return; + } + float leftMost = mPositionMinSideMargin + mNotificationStackScroller.getWidth() / 2; + float rightMost = getWidth() - mPositionMinSideMargin + - mNotificationStackScroller.getWidth() / 2; + if (Math.abs(x - getWidth() / 2) < mNotificationStackScroller.getWidth() / 4) { + x = getWidth() / 2; + } + x = Math.min(rightMost, Math.max(leftMost, x)); + setVerticalPanelTranslation(x - + (mNotificationStackScroller.getLeft() + mNotificationStackScroller.getWidth() / 2)); + } + + private void resetVerticalPanelPosition() { + setVerticalPanelTranslation(0f); + } + + private void setVerticalPanelTranslation(float translation) { + mNotificationStackScroller.setTranslationX(translation); + mScrollView.setTranslationX(translation); + mHeader.setTranslationX(translation); + } + + private void updateStackHeight(float stackHeight) { + mNotificationStackScroller.setStackHeight(stackHeight); + updateKeyguardBottomAreaAlpha(); + } + + public void setPanelScrimMinFraction(float minFraction) { + mBar.panelScrimMinFractionChanged(minFraction); + } + + public void clearNotificattonEffects() { + mStatusBar.clearNotificationEffects(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java index e7b0c4c..cbb71c5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.phone; import android.content.Context; import android.graphics.Canvas; -import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.ViewStub; @@ -38,6 +37,7 @@ public class NotificationsQuickSettingsContainer extends FrameLayout private View mStackScroller; private View mKeyguardStatusBar; private boolean mInflated; + private boolean mQsExpanded; public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) { super(context, attrs); @@ -65,26 +65,29 @@ public class NotificationsQuickSettingsContainer extends FrameLayout boolean userSwitcherVisible = mInflated && mUserSwitcher.getVisibility() == View.VISIBLE; boolean statusBarVisible = mKeyguardStatusBar.getVisibility() == View.VISIBLE; + View stackQsTop = mQsExpanded ? mStackScroller : mScrollView; + View stackQsBottom = !mQsExpanded ? mStackScroller : mScrollView; // Invert the order of the scroll view and user switcher such that the notifications receive // touches first but the panel gets drawn above. if (child == mScrollView) { - return super.drawChild(canvas, mStackScroller, drawingTime); - } else if (child == mStackScroller) { - return super.drawChild(canvas, - userSwitcherVisible && statusBarVisible ? mUserSwitcher + return super.drawChild(canvas, userSwitcherVisible && statusBarVisible ? mUserSwitcher : statusBarVisible ? mKeyguardStatusBar : userSwitcherVisible ? mUserSwitcher - : mScrollView, + : stackQsBottom, drawingTime); + } else if (child == mStackScroller) { + return super.drawChild(canvas, + userSwitcherVisible && statusBarVisible ? mKeyguardStatusBar + : statusBarVisible || userSwitcherVisible ? stackQsBottom + : stackQsTop, drawingTime); } else if (child == mUserSwitcher) { return super.drawChild(canvas, - userSwitcherVisible && statusBarVisible ? mKeyguardStatusBar - : mScrollView, + userSwitcherVisible && statusBarVisible ? stackQsBottom + : stackQsTop, drawingTime); } else if (child == mKeyguardStatusBar) { return super.drawChild(canvas, - userSwitcherVisible && statusBarVisible ? mScrollView - : mScrollView, + stackQsTop, drawingTime); }else { return super.drawChild(canvas, child, drawingTime); @@ -98,4 +101,11 @@ public class NotificationsQuickSettingsContainer extends FrameLayout mInflated = true; } } + + public void setQsExpanded(boolean expanded) { + if (mQsExpanded != expanded) { + mQsExpanded = expanded; + invalidate(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ObservableScrollView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ObservableScrollView.java index 1186a33..9e5cefd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ObservableScrollView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ObservableScrollView.java @@ -99,7 +99,7 @@ public class ObservableScrollView extends ScrollView { } else if (!mTouchEnabled) { MotionEvent cancel = MotionEvent.obtain(ev); cancel.setAction(MotionEvent.ACTION_CANCEL); - super.dispatchTouchEvent(ev); + super.dispatchTouchEvent(cancel); cancel.recycle(); mTouchCancelled = true; return false; 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 3efaaff..e1a400d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java @@ -25,9 +25,11 @@ import android.widget.FrameLayout; import java.util.ArrayList; -public class PanelBar extends FrameLayout { +public abstract class PanelBar extends FrameLayout { public static final boolean DEBUG = false; public static final String TAG = PanelBar.class.getSimpleName(); + private static final boolean SPEW = false; + public static final void LOG(String fmt, Object... args) { if (!DEBUG) return; Log.v(TAG, String.format(fmt, args)); @@ -80,6 +82,17 @@ public class PanelBar extends FrameLayout { } } + public void setBouncerShowing(boolean showing) { + int important = showing ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : IMPORTANT_FOR_ACCESSIBILITY_AUTO; + + setImportantForAccessibility(important); + + if (mPanelHolder != null) { + mPanelHolder.setImportantForAccessibility(important); + } + } + public float getBarHeight() { return getMeasuredHeight(); } @@ -140,11 +153,13 @@ public class PanelBar extends FrameLayout { mPanelHolder.setSelectedPanel(mTouchingPanel); for (PanelView pv : mPanels) { if (pv != panel) { - pv.collapse(false /* delayed */); + pv.collapse(false /* delayed */, 1.0f /* speedUpFactor */); } } } + public abstract void panelScrimMinFractionChanged(float minFraction); + /** * @param panel the panel which changed its expansion state * @param frac the fraction from the expansion in [0, 1] @@ -154,11 +169,10 @@ public class PanelBar extends FrameLayout { public void panelExpansionChanged(PanelView panel, float frac, boolean expanded) { boolean fullyClosed = true; PanelView fullyOpenedPanel = null; - if (DEBUG) LOG("panelExpansionChanged: start state=%d panel=%s", mState, panel.getName()); + if (SPEW) LOG("panelExpansionChanged: start state=%d panel=%s", mState, panel.getName()); mPanelExpandedFractionSum = 0f; for (PanelView pv : mPanels) { - boolean visible = pv.getExpandedHeight() > 0; - pv.setVisibility(visible ? View.VISIBLE : View.GONE); + pv.setVisibility(expanded ? View.VISIBLE : View.INVISIBLE); // adjust any other panels that may be partially visible if (expanded) { if (mState == STATE_CLOSED) { @@ -167,8 +181,8 @@ public class PanelBar extends FrameLayout { } fullyClosed = false; final float thisFrac = pv.getExpandedFraction(); - mPanelExpandedFractionSum += (visible ? thisFrac : 0); - if (DEBUG) LOG("panelExpansionChanged: -> %s: f=%.1f", pv.getName(), thisFrac); + mPanelExpandedFractionSum += thisFrac; + if (SPEW) LOG("panelExpansionChanged: -> %s: f=%.1f", pv.getName(), thisFrac); if (panel == pv) { if (thisFrac == 1f) fullyOpenedPanel = panel; } @@ -183,20 +197,19 @@ public class PanelBar extends FrameLayout { onAllPanelsCollapsed(); } - if (DEBUG) LOG("panelExpansionChanged: end state=%d [%s%s ]", mState, + if (SPEW) LOG("panelExpansionChanged: end state=%d [%s%s ]", mState, (fullyOpenedPanel!=null)?" fullyOpened":"", fullyClosed?" fullyClosed":""); } - public void collapseAllPanels(boolean animate) { + public void collapseAllPanels(boolean animate, boolean delayed, float speedUpFactor) { boolean waiting = false; for (PanelView pv : mPanels) { if (animate && !pv.isFullyCollapsed()) { - pv.collapse(true /* delayed */); + pv.collapse(delayed, speedUpFactor); waiting = true; } else { pv.resetViews(); pv.setExpandedFraction(0); // just in case - pv.setVisibility(View.GONE); pv.cancelPeek(); } } @@ -223,10 +236,6 @@ public class PanelBar extends FrameLayout { public void onTrackingStarted(PanelView panel) { mTracking = true; - if (DEBUG && panel != mTouchingPanel) { - LOG("shouldn't happen: onTrackingStarted(%s) != mTouchingPanel(%s)", - panel, mTouchingPanel); - } } public void onTrackingStopped(PanelView panel, boolean expand) { @@ -234,7 +243,7 @@ public class PanelBar extends FrameLayout { } public void onExpandingFinished() { - + if (DEBUG) LOG("onExpandingFinished"); } public void onClosingFinished() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java index d86ccee..9343172 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java @@ -38,6 +38,7 @@ import com.android.systemui.R; import com.android.systemui.doze.DozeLog; import com.android.systemui.statusbar.FlingAnimationUtils; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.policy.HeadsUpManager; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -51,10 +52,13 @@ public abstract class PanelView extends FrameLayout { } protected PhoneStatusBar mStatusBar; + protected HeadsUpManager mHeadsUpManager; + private float mPeekHeight; private float mHintDistance; private int mEdgeTapAreaWidth; private float mInitialOffsetOnTouch; + private boolean mCollapsedAndHeadsUpOnDown; private float mExpandedFraction = 0; protected float mExpandedHeight = 0; private boolean mPanelClosedOnDown; @@ -73,6 +77,9 @@ public abstract class PanelView extends FrameLayout { private boolean mTouchAboveFalsingThreshold; private int mUnlockFalsingThreshold; private boolean mTouchStartedInEmptyArea; + private boolean mMotionAborted; + private boolean mUpwardsWhenTresholdReached; + private boolean mAnimatingOnDown; private ValueAnimator mHeightAnimator; private ObjectAnimator mPeekAnimator; @@ -98,9 +105,15 @@ public abstract class PanelView extends FrameLayout { private boolean mPeekPending; private boolean mCollapseAfterPeek; + + /** + * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. + */ + private float mNextCollapseSpeedUpFactor = 1.0f; + private boolean mExpanding; private boolean mGestureWaitForTouchSlop; - private boolean mDozingOnDown; + private boolean mIgnoreXTouchSlop; private Runnable mPeekRunnable = new Runnable() { @Override public void run() { @@ -159,12 +172,7 @@ public abstract class PanelView extends FrameLayout { public void onAnimationEnd(Animator animation) { mPeekAnimator = null; if (mCollapseAfterPeek && !mCancelled) { - postOnAnimation(new Runnable() { - @Override - public void run() { - collapse(false /* delayed */); - } - }); + postOnAnimation(mPostCollapseRunnable); } mCollapseAfterPeek = false; } @@ -209,7 +217,8 @@ public abstract class PanelView extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent event) { - if (mInstantExpanding || mTouchDisabled) { + if (mInstantExpanding || mTouchDisabled + || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) { return false; } @@ -227,32 +236,31 @@ public abstract class PanelView extends FrameLayout { pointerIndex = 0; mTrackingPointer = event.getPointerId(pointerIndex); } - final float y = event.getY(pointerIndex); final float x = event.getX(pointerIndex); + final float y = event.getY(pointerIndex); if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - mGestureWaitForTouchSlop = mExpandedHeight == 0f; + mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures(); + mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y); } - boolean waitForTouchSlop = hasConflictingGestures() || mGestureWaitForTouchSlop; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: - mInitialTouchY = y; - mInitialTouchX = x; - mInitialOffsetOnTouch = mExpandedHeight; - mTouchSlopExceeded = false; + startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); mJustPeeked = false; - mPanelClosedOnDown = mExpandedHeight == 0.0f; + mPanelClosedOnDown = isFullyCollapsed(); mHasLayoutedSinceDown = false; mUpdateFlingOnLayout = false; + mMotionAborted = false; mPeekTouching = mPanelClosedOnDown; mTouchAboveFalsingThreshold = false; - mDozingOnDown = isDozing(); + mCollapsedAndHeadsUpOnDown = isFullyCollapsed() + && mHeadsUpManager.hasPinnedHeadsUp(); if (mVelocityTracker == null) { initVelocityTracker(); } trackMovement(event); - if (!waitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) || + if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) || mPeekPending || mPeekAnimator != null) { cancelHeightAnimator(); cancelPeek(); @@ -260,7 +268,7 @@ public abstract class PanelView extends FrameLayout { || mPeekPending || mPeekAnimator != null; onTrackingStarted(); } - if (mExpandedHeight == 0) { + if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()) { schedulePeek(); } break; @@ -273,12 +281,16 @@ public abstract class PanelView extends FrameLayout { final float newY = event.getY(newIndex); final float newX = event.getX(newIndex); mTrackingPointer = event.getPointerId(newIndex); - mInitialOffsetOnTouch = mExpandedHeight; - mInitialTouchY = newY; - mInitialTouchX = newX; + startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: + if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { + mMotionAborted = true; + endMotionEvent(event, x, y, true /* forceCancel */); + return false; } break; - case MotionEvent.ACTION_MOVE: float h = y - mInitialTouchY; @@ -286,13 +298,11 @@ public abstract class PanelView extends FrameLayout { // y-component of the gesture, as we have no conflicting horizontal gesture. if (Math.abs(h) > mTouchSlop && (Math.abs(h) > Math.abs(x - mInitialTouchX) - || mInitialOffsetOnTouch == 0f)) { + || mIgnoreXTouchSlop)) { mTouchSlopExceeded = true; - if (waitForTouchSlop && !mTracking) { + if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) { if (!mJustPeeked && mInitialOffsetOnTouch != 0f) { - mInitialOffsetOnTouch = mExpandedHeight; - mInitialTouchX = x; - mInitialTouchY = y; + startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); h = 0; } cancelHeightAnimator(); @@ -310,8 +320,9 @@ public abstract class PanelView extends FrameLayout { } if (-h >= getFalsingThreshold()) { mTouchAboveFalsingThreshold = true; + mUpwardsWhenTresholdReached = isDirectionUpwards(x, y); } - if (!mJustPeeked && (!waitForTouchSlop || mTracking) && !isTrackingBlocked()) { + if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) { setExpandedHeightInternal(newHeight); } @@ -320,26 +331,58 @@ public abstract class PanelView extends FrameLayout { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: - mTrackingPointer = -1; trackMovement(event); - if ((mTracking && mTouchSlopExceeded) - || Math.abs(x - mInitialTouchX) > mTouchSlop - || Math.abs(y - mInitialTouchY) > mTouchSlop - || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { - float vel = 0f; - float vectorVel = 0f; - if (mVelocityTracker != null) { - mVelocityTracker.computeCurrentVelocity(1000); - vel = mVelocityTracker.getYVelocity(); - vectorVel = (float) Math.hypot( - mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); - } - boolean expand = flingExpands(vel, vectorVel) - || event.getActionMasked() == MotionEvent.ACTION_CANCEL; - onTrackingStopped(expand); - DozeLog.traceFling(expand, mTouchAboveFalsingThreshold, - mStatusBar.isFalsingThresholdNeeded(), - mStatusBar.isScreenOnComingFromTouch()); + endMotionEvent(event, x, y, false /* forceCancel */); + break; + } + return !mGestureWaitForTouchSlop || mTracking; + } + + /** + * @return whether the swiping direction is upwards and above a 45 degree angle compared to the + * horizontal direction + */ + private boolean isDirectionUpwards(float x, float y) { + float xDiff = x - mInitialTouchX; + float yDiff = y - mInitialTouchY; + if (yDiff >= 0) { + return false; + } + return Math.abs(yDiff) >= Math.abs(xDiff); + } + + protected void startExpandMotion(float newX, float newY, boolean startTracking, + float expandedHeight) { + mInitialOffsetOnTouch = expandedHeight; + mInitialTouchY = newY; + mInitialTouchX = newX; + if (startTracking) { + mTouchSlopExceeded = true; + onTrackingStarted(); + } + } + + private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) { + mTrackingPointer = -1; + if ((mTracking && mTouchSlopExceeded) + || Math.abs(x - mInitialTouchX) > mTouchSlop + || Math.abs(y - mInitialTouchY) > mTouchSlop + || event.getActionMasked() == MotionEvent.ACTION_CANCEL + || forceCancel) { + float vel = 0f; + float vectorVel = 0f; + if (mVelocityTracker != null) { + mVelocityTracker.computeCurrentVelocity(1000); + vel = mVelocityTracker.getYVelocity(); + vectorVel = (float) Math.hypot( + mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + } + boolean expand = flingExpands(vel, vectorVel, x, y) + || event.getActionMasked() == MotionEvent.ACTION_CANCEL + || forceCancel; + DozeLog.traceFling(expand, mTouchAboveFalsingThreshold, + mStatusBar.isFalsingThresholdNeeded(), + mStatusBar.isScreenOnComingFromTouch()); // Log collapse gesture if on lock screen. if (!expand && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { float displayDensity = mStatusBar.getDisplayDensity(); @@ -349,24 +392,22 @@ public abstract class PanelView extends FrameLayout { EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_UP_UNLOCK, heightDp, velocityDp); } - fling(vel, expand); - mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown; - if (mUpdateFlingOnLayout) { - mUpdateFlingVelocity = vel; - } - } else { - boolean expands = onEmptySpaceClick(mInitialTouchX); - onTrackingStopped(expands); - } + fling(vel, expand, isFalseTouch(x, y)); + onTrackingStopped(expand); + mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown; + if (mUpdateFlingOnLayout) { + mUpdateFlingVelocity = vel; + } + } else { + boolean expands = onEmptySpaceClick(mInitialTouchX); + onTrackingStopped(expands); + } - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - mPeekTouching = false; - break; + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; } - return !waitForTouchSlop || mTracking; + mPeekTouching = false; } private int getFalsingThreshold() { @@ -376,9 +417,12 @@ public abstract class PanelView extends FrameLayout { protected abstract boolean hasConflictingGestures(); + protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y); + protected void onTrackingStopped(boolean expand) { mTracking = false; mBar.onTrackingStopped(PanelView.this, expand); + notifyBarPanelExpansionChanged(); } protected void onTrackingStarted() { @@ -387,11 +431,13 @@ public abstract class PanelView extends FrameLayout { mCollapseAfterPeek = false; mBar.onTrackingStarted(PanelView.this); notifyExpandingStarted(); + notifyBarPanelExpansionChanged(); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { - if (mInstantExpanding) { + if (mInstantExpanding + || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) { return false; } @@ -415,8 +461,8 @@ public abstract class PanelView extends FrameLayout { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: mStatusBar.userActivity(); - if (mHeightAnimator != null && !mHintAnimationRunning || - mPeekPending || mPeekAnimator != null) { + mAnimatingOnDown = mHeightAnimator != null; + if (mAnimatingOnDown && mClosing && !mHintAnimationRunning || mPeekPending || mPeekAnimator != null) { cancelHeightAnimator(); cancelPeek(); mTouchSlopExceeded = true; @@ -427,11 +473,12 @@ public abstract class PanelView extends FrameLayout { mTouchStartedInEmptyArea = !isInContentBounds(x, y); mTouchSlopExceeded = false; mJustPeeked = false; - mPanelClosedOnDown = mExpandedHeight == 0.0f; + mMotionAborted = false; + mPanelClosedOnDown = isFullyCollapsed(); + mCollapsedAndHeadsUpOnDown = false; mHasLayoutedSinceDown = false; mUpdateFlingOnLayout = false; mTouchAboveFalsingThreshold = false; - mDozingOnDown = isDozing(); initVelocityTracker(); trackMovement(event); break; @@ -445,25 +492,34 @@ public abstract class PanelView extends FrameLayout { mInitialTouchY = event.getY(newIndex); } break; - + case MotionEvent.ACTION_POINTER_DOWN: + if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { + mMotionAborted = true; + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + break; case MotionEvent.ACTION_MOVE: final float h = y - mInitialTouchY; trackMovement(event); - if (scrolledToBottom || mTouchStartedInEmptyArea) { - if (h < -mTouchSlop && h < -Math.abs(x - mInitialTouchX)) { + if (scrolledToBottom || mTouchStartedInEmptyArea || mAnimatingOnDown) { + float hAbs = Math.abs(h); + if ((h < -mTouchSlop || (mAnimatingOnDown && hAbs > mTouchSlop)) + && hAbs > Math.abs(x - mInitialTouchX)) { cancelHeightAnimator(); - mInitialOffsetOnTouch = mExpandedHeight; - mInitialTouchY = y; - mInitialTouchX = x; - mTracking = true; - mTouchSlopExceeded = true; - onTrackingStarted(); + startExpandMotion(x, y, true /* startTracking */, mExpandedHeight); return true; } } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } break; } return false; @@ -474,7 +530,7 @@ public abstract class PanelView extends FrameLayout { */ protected abstract boolean isInContentBounds(float x, float y); - private void cancelHeightAnimator() { + protected void cancelHeightAnimator() { if (mHeightAnimator != null) { mHeightAnimator.cancel(); } @@ -520,8 +576,8 @@ public abstract class PanelView extends FrameLayout { * @param vectorVel the length of the vectorial velocity * @return whether a fling should expands the panel; contracts otherwise */ - protected boolean flingExpands(float vel, float vectorVel) { - if (isBelowFalsingThreshold()) { + protected boolean flingExpands(float vel, float vectorVel, float x, float y) { + if (isFalseTouch(x, y)) { return true; } if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) { @@ -531,14 +587,44 @@ public abstract class PanelView extends FrameLayout { } } - private boolean isBelowFalsingThreshold() { - return !mTouchAboveFalsingThreshold && mStatusBar.isFalsingThresholdNeeded(); + /** + * @param x the final x-coordinate when the finger was lifted + * @param y the final y-coordinate when the finger was lifted + * @return whether this motion should be regarded as a false touch + */ + private boolean isFalseTouch(float x, float y) { + if (!mStatusBar.isFalsingThresholdNeeded()) { + return false; + } + if (!mTouchAboveFalsingThreshold) { + return true; + } + if (mUpwardsWhenTresholdReached) { + return false; + } + return !isDirectionUpwards(x, y); } protected void fling(float vel, boolean expand) { + fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false); + } + + protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) { + fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing); + } + + protected void fling(float vel, boolean expand, float collapseSpeedUpFactor, + boolean expandBecauseOfFalsing) { cancelPeek(); float target = expand ? getMaxPanelHeight() : 0.0f; + if (!expand) { + mClosing = true; + } + flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); + } + protected void flingToHeight(float vel, boolean expand, float target, + float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) { // Hack to make the expand transition look nice when clear all button is visible - we make // the animation only to the last notification, and then jump to the maximum panel height so // clear all just fades in and the decelerating motion is towards the last notification. @@ -555,12 +641,11 @@ public abstract class PanelView extends FrameLayout { mOverExpandedBeforeFling = getOverExpansionAmount() > 0f; ValueAnimator animator = createHeightAnimator(target); if (expand) { - boolean belowFalsingThreshold = isBelowFalsingThreshold(); - if (belowFalsingThreshold) { + if (expandBecauseOfFalsing) { vel = 0; } mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight()); - if (belowFalsingThreshold) { + if (expandBecauseOfFalsing) { animator.setDuration(350); } } else { @@ -570,7 +655,13 @@ public abstract class PanelView extends FrameLayout { // Make it shorter if we run a canned animation if (vel == 0) { animator.setDuration((long) - (animator.getDuration() * getCannedFlingDurationFactor())); + (animator.getDuration() * getCannedFlingDurationFactor() + / collapseSpeedUpFactor)); + } + if (PhoneStatusBar.DEBUG_EMPTY_KEYGUARD + && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { + Log.i(PhoneStatusBar.TAG, "Panel collapsed! Stacktrace: " + + Log.getStackTraceString(new Throwable())); } } animator.addListener(new AnimatorListenerAdapter() { @@ -590,6 +681,7 @@ public abstract class PanelView extends FrameLayout { if (!mCancelled) { notifyExpandingFinished(); } + notifyBarPanelExpansionChanged(); } }); mHeightAnimator = animator; @@ -618,7 +710,7 @@ public abstract class PanelView extends FrameLayout { mHasLayoutedSinceDown = true; if (mUpdateFlingOnLayout) { abortAnimations(); - fling(mUpdateFlingVelocity, true); + fling(mUpdateFlingVelocity, true /* expands */); mUpdateFlingOnLayout = false; } } @@ -629,7 +721,7 @@ public abstract class PanelView extends FrameLayout { // If the user isn't actively poking us, let's update the height if ((!mTracking || isTrackingBlocked()) && mHeightAnimator == null - && mExpandedHeight > 0 + && !isFullyCollapsed() && currentMaxPanelHeight != mExpandedHeight && !mPeekPending && mPeekAnimator == null @@ -685,6 +777,15 @@ public abstract class PanelView extends FrameLayout { public void setExpandedFraction(float frac) { setExpandedHeight(getMaxPanelHeight() * frac); + if (PhoneStatusBar.DEBUG_EMPTY_KEYGUARD + && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { + if (frac == 0.0f) { + Log.i(PhoneStatusBar.TAG, "Panel collapsed! Stacktrace: " + + Log.getStackTraceString(new Throwable())); + } else if (frac == 1.0f) { + mStatusBar.endWindowManagerLogging(); + } + } } public float getExpandedHeight() { @@ -715,7 +816,12 @@ public abstract class PanelView extends FrameLayout { mBar = panelBar; } - public void collapse(boolean delayed) { + public void collapse(boolean delayed, float speedUpFactor) { + if (PhoneStatusBar.DEBUG_EMPTY_KEYGUARD + && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { + Log.i(PhoneStatusBar.TAG, "Panel collapsed! Stacktrace: " + + Log.getStackTraceString(new Throwable())); + } if (DEBUG) logf("collapse: " + this); if (mPeekPending || mPeekAnimator != null) { mCollapseAfterPeek = true; @@ -728,12 +834,15 @@ public abstract class PanelView extends FrameLayout { } } else if (!isFullyCollapsed() && !mTracking && !mClosing) { cancelHeightAnimator(); - mClosing = true; notifyExpandingStarted(); + + // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state. + mClosing = true; if (delayed) { + mNextCollapseSpeedUpFactor = speedUpFactor; postDelayed(mFlingCollapseRunnable, 120); } else { - fling(0, false /* expand */); + fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */); } } } @@ -741,7 +850,8 @@ public abstract class PanelView extends FrameLayout { private final Runnable mFlingCollapseRunnable = new Runnable() { @Override public void run() { - fling(0, false /* expand */); + fling(0, false /* expand */, mNextCollapseSpeedUpFactor, + false /* expandBecauseOfFalsing */); } }; @@ -779,7 +889,7 @@ public abstract class PanelView extends FrameLayout { if (mExpanding) { notifyExpandingFinished(); } - setVisibility(VISIBLE); + notifyBarPanelExpansionChanged(); // Wait for window manager to pickup the change, so we know the maximum height of the panel // then. @@ -898,6 +1008,7 @@ public abstract class PanelView extends FrameLayout { public void onAnimationEnd(Animator animation) { mHeightAnimator = null; onAnimationFinished.run(); + notifyBarPanelExpansionChanged(); } }); animator.start(); @@ -915,9 +1026,10 @@ public abstract class PanelView extends FrameLayout { return animator; } - private void notifyBarPanelExpansionChanged() { + protected void notifyBarPanelExpansionChanged() { mBar.panelExpansionChanged(this, mExpandedFraction, mExpandedFraction > 0f || mPeekPending - || mPeekAnimator != null); + || mPeekAnimator != null || mInstantExpanding || mHeadsUpManager.hasPinnedHeadsUp() + || mTracking || mHeightAnimator != null); } /** @@ -929,50 +1041,17 @@ public abstract class PanelView extends FrameLayout { if (mHintAnimationRunning) { return true; } - if (x < mEdgeTapAreaWidth - && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { - onEdgeClicked(false /* right */); - return true; - } else if (x > getWidth() - mEdgeTapAreaWidth - && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { - onEdgeClicked(true /* right */); - return true; - } else { - return onMiddleClicked(); - } + return onMiddleClicked(); } - private final Runnable mPostCollapseRunnable = new Runnable() { + protected final Runnable mPostCollapseRunnable = new Runnable() { @Override public void run() { - collapse(false /* delayed */); + collapse(false /* delayed */, 1.0f /* speedUpFactor */); } }; - private boolean onMiddleClicked() { - switch (mStatusBar.getBarState()) { - case StatusBarState.KEYGUARD: - if (!mDozingOnDown) { - EventLogTags.writeSysuiLockscreenGesture( - EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_TAP_UNLOCK_HINT, - 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); - startUnlockHintAnimation(); - } - return true; - case StatusBarState.SHADE_LOCKED: - mStatusBar.goToKeyguard(); - return true; - case StatusBarState.SHADE: - // This gets called in the middle of the touch handling, where the state is still - // that we are tracking the panel. Collapse the panel after this is done. - post(mPostCollapseRunnable); - return false; - default: - return true; - } - } - - protected abstract void onEdgeClicked(boolean right); + protected abstract boolean onMiddleClicked(); protected abstract boolean isDozing(); @@ -1009,4 +1088,8 @@ public abstract class PanelView extends FrameLayout { * @return the height of the clear all button, in pixels */ protected abstract int getClearAllHeight(); + + public void setHeadsUpManager(HeadsUpManager headsUpManager) { + mHeadsUpManager = headsUpManager; + } } 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 f227107..a637e24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -17,22 +17,8 @@ package com.android.systemui.statusbar.phone; -import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; -import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; -import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; -import static android.app.StatusBarManager.windowStateToString; -import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT; -import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT; -import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE; -import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT; -import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT; -import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT; -import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING; - import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.TimeInterpolator; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManagerNative; @@ -45,6 +31,7 @@ import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.IPackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; @@ -75,6 +62,7 @@ import android.os.Message; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; @@ -82,40 +70,33 @@ import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; -import android.text.TextUtils; import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.view.Display; -import android.view.Gravity; -import android.view.HardwareCanvas; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; +import android.view.ThreadedRenderer; import android.view.VelocityTracker; import android.view.View; -import android.view.ViewGroup; +import android.view.ViewConfiguration; import android.view.ViewGroup.LayoutParams; -import android.view.ViewPropertyAnimator; import android.view.ViewStub; -import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateInterpolator; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; -import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.statusbar.StatusBarIcon; import com.android.keyguard.KeyguardHostView.OnDismissAction; import com.android.keyguard.ViewMediatorCallback; @@ -123,13 +104,15 @@ import com.android.systemui.BatteryMeterView; import com.android.systemui.DemoMode; import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; -import com.android.systemui.FontSizeUtils; +import com.android.systemui.Prefs; import com.android.systemui.R; +import com.android.systemui.SwipeHelper; +import com.android.systemui.assist.AssistManager; import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.qs.QSPanel; -import com.android.systemui.recent.ScreenPinningRequest; +import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.statusbar.ActivatableNotificationView; import com.android.systemui.statusbar.BackDropView; import com.android.systemui.statusbar.BaseStatusBar; @@ -146,7 +129,6 @@ import com.android.systemui.statusbar.NotificationOverflowContainer; import com.android.systemui.statusbar.ScrimView; import com.android.systemui.statusbar.SignalClusterView; import com.android.systemui.statusbar.SpeedBumpView; -import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener; import com.android.systemui.statusbar.policy.AccessibilityController; @@ -156,7 +138,7 @@ import com.android.systemui.statusbar.policy.BluetoothControllerImpl; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.CastControllerImpl; import com.android.systemui.statusbar.policy.FlashlightController; -import com.android.systemui.statusbar.policy.HeadsUpNotificationView; +import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.HotspotControllerImpl; import com.android.systemui.statusbar.policy.KeyButtonView; import com.android.systemui.statusbar.policy.KeyguardMonitor; @@ -172,8 +154,7 @@ import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout.OnChildLocationsChangedListener; -import com.android.systemui.statusbar.stack.StackScrollAlgorithm; -import com.android.systemui.statusbar.stack.StackScrollState.ViewState; +import com.android.systemui.statusbar.stack.StackViewState; import com.android.systemui.volume.VolumeComponent; import java.io.FileDescriptor; @@ -181,13 +162,31 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.TreeSet; + +import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; +import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; +import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; +import static android.app.StatusBarManager.windowStateToString; +import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT; +import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT; +import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE; +import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT; +import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT; +import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT; +import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING; public class PhoneStatusBar extends BaseStatusBar implements DemoMode, - DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener { + DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener, + HeadsUpManager.OnHeadsUpChangedListener { static final String TAG = "PhoneStatusBar"; public static final boolean DEBUG = BaseStatusBar.DEBUG; + public static final boolean DEBUG_EMPTY_KEYGUARD = true; public static final boolean SPEW = false; public static final boolean DUMPTRUCK = true; // extra dumpsys info public static final boolean DEBUG_GESTURES = false; @@ -199,9 +198,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // additional instrumentation for testing purposes; intended to be left on during development public static final boolean CHATTY = DEBUG; - public static final String ACTION_STATUSBAR_START - = "com.android.internal.policy.statusbar.START"; - public static final boolean SHOW_LOCKSCREEN_MEDIA_ARTWORK = true; private static final int MSG_OPEN_NOTIFICATION_PANEL = 1000; @@ -215,9 +211,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private static final boolean CLOSE_PANEL_WHEN_EMPTIED = true; - private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10; // see NotificationManagerService - private static final int HIDE_ICONS_BELOW_SCORE = Notification.PRIORITY_LOW * NOTIFICATION_PRIORITY_MULTIPLIER; - private static final int STATUS_OR_NAV_TRANSIENT = View.STATUS_BAR_TRANSIENT | View.NAVIGATION_BAR_TRANSIENT; private static final long AUTOHIDE_TIMEOUT_MS = 3000; @@ -241,6 +234,21 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, /** Allow some time inbetween the long press for back and recents. */ private static final int LOCK_TO_APP_GESTURE_TOLERENCE = 200; + /** If true, the system is in the half-boot-to-decryption-screen state. + * Prudently disable QS and notifications. */ + private static final boolean ONLY_CORE_APPS; + + static { + boolean onlyCoreApps; + try { + onlyCoreApps = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) + .isOnlyCoreApps(); + } catch (RemoteException e) { + onlyCoreApps = false; + } + ONLY_CORE_APPS = onlyCoreApps; + } + PhoneStatusBarPolicy mIconPolicy; // These are no longer handled by the policy, because we need custom strategies for them @@ -264,8 +272,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, AccessibilityController mAccessibilityController; int mNaturalBarHeight = -1; - int mIconSize = -1; - int mIconHPadding = -1; + Display mDisplay; Point mCurrentDisplaySize = new Point(); @@ -281,34 +288,14 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, int mPixelFormat; Object mQueueLock = new Object(); - // viewgroup containing the normal contents of the statusbar - LinearLayout mStatusBarContents; - - // right-hand icons - LinearLayout mSystemIconArea; - LinearLayout mSystemIcons; - - // left-hand icons - LinearLayout mStatusIcons; - LinearLayout mStatusIconsKeyguard; - - // the icons themselves - IconMerger mNotificationIcons; - View mNotificationIconArea; - - // [+> - View mMoreIcon; + StatusBarIconController mIconController; // expanded notifications NotificationPanelView mNotificationPanel; // the sliding/resizing panel within the notification window View mExpandedContents; - int mNotificationPanelGravity; - int mNotificationPanelMarginBottomPx; - float mNotificationPanelMinHeightFrac; TextView mNotificationPanelDebugText; // settings - View mFlipSettingsView; private QSPanel mQSPanel; // top bar @@ -325,16 +312,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, int mKeyguardMaxNotificationCount; - // carrier/wifi label - private TextView mCarrierLabel; - private boolean mCarrierLabelVisible = false; - private int mCarrierLabelHeight; - private int mStatusBarHeaderHeight; - - private boolean mShowCarrierInPanel = false; - - // position - int[] mPositionTmp = new int[2]; boolean mExpandedVisible; private int mNavigationBarWindowState = WINDOW_STATE_SHOWING; @@ -342,14 +319,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // the tracker view int mTrackingPosition; // the position of the top of the tracking view. - // ticker - private boolean mTickerEnabled; - private Ticker mTicker; - private View mTickerView; - private boolean mTicking; - // Tracking finger for opening/closing. - int mEdgeBorder; // corresponds to R.dimen.status_bar_edge_ignore boolean mTracking; VelocityTracker mVelocityTracker; @@ -357,11 +327,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>(); // for disabling the status bar - int mDisabled = 0; + int mDisabled1 = 0; + int mDisabled2 = 0; // tracking calls to View.setSystemUiVisibility() int mSystemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE; + // last value sent to window manager + private int mLastDispatchedSystemUiVisibility = ~View.SYSTEM_UI_FLAG_VISIBLE; + DisplayMetrics mDisplayMetrics = new DisplayMetrics(); // XXX: gesture research @@ -393,6 +367,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (!mUserSetup && mStatusBarView != null) animateCollapseQuickSettings(); } + if (mIconPolicy != null) { + mIconPolicy.setCurrentUserSetup(mUserSetup); + } } }; @@ -410,11 +387,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (wasUsing != mUseHeadsUp) { if (!mUseHeadsUp) { Log.d(TAG, "dismissing any existing heads up notification on disable event"); - setHeadsUpVisibility(false); - mHeadsUpNotificationView.release(); - removeHeadsUpView(); - } else { - addHeadsUpView(); + mHeadsUpManager.releaseAllImmediately(); } } } @@ -440,9 +413,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private boolean mWaitingForKeyguardExit; private boolean mDozing; + private boolean mDozingRequested; private boolean mScrimSrcModeEnabled; - private Interpolator mLinearOutSlowIn; private Interpolator mLinearInterpolator = new LinearInterpolator(); private Interpolator mBackdropInterpolator = new AccelerateDecelerateInterpolator(); public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); @@ -482,10 +455,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } }; - private int mDisabledUnmodified; + private int mDisabledUnmodified1; + private int mDisabledUnmodified2; /** Keys of notifications currently visible to the user. */ - private final ArraySet<String> mCurrentlyVisibleNotifications = new ArraySet<String>(); + private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications = + new ArraySet<>(); private long mLastVisibilityReportUptimeMs; private final ShadeUpdates mShadeUpdates = new ShadeUpdates(); @@ -498,10 +473,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // Fingerprint (as computed by getLoggingFingerprint() of the last logged state. private int mLastLoggedStateFingerprint; - private static final int VISIBLE_LOCATIONS = ViewState.LOCATION_FIRST_CARD - | ViewState.LOCATION_TOP_STACK_PEEKING - | ViewState.LOCATION_MAIN_AREA - | ViewState.LOCATION_BOTTOM_STACK_PEEKING; + private static final int VISIBLE_LOCATIONS = StackViewState.LOCATION_FIRST_CARD + | StackViewState.LOCATION_MAIN_AREA; private final OnChildLocationsChangedListener mNotificationLocationsChangedListener = new OnChildLocationsChangedListener() { @@ -526,12 +499,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // 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>(); + private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications = + new ArraySet<>(); + private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications = + new ArraySet<>(); + private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications = + new ArraySet<>(); @Override public void run() { mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis(); + final String mediaKey = getCurrentMediaNotificationKey(); // 1. Loop over mNotificationData entries: // A. Keep list of visible notifications. @@ -546,37 +524,56 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, for (int i = 0; i < N; i++) { Entry entry = activeNotifications.get(i); String key = entry.notification.getKey(); - boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(key); - boolean currentlyVisible = + boolean isVisible = (mStackScroller.getChildLocation(entry.row) & VISIBLE_LOCATIONS) != 0; - if (currentlyVisible) { + NotificationVisibility visObj = NotificationVisibility.obtain(key, i, isVisible); + boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj); + if (isVisible) { // Build new set of visible notifications. - mTmpCurrentlyVisibleNotifications.add(key); - } - if (!previouslyVisible && currentlyVisible) { - mTmpNewlyVisibleNotifications.add(key); + mTmpCurrentlyVisibleNotifications.add(visObj); + if (!previouslyVisible) { + mTmpNewlyVisibleNotifications.add(visObj); + } + } else { + // release object + visObj.recycle(); } } - ArraySet<String> noLongerVisibleNotifications = mCurrentlyVisibleNotifications; - noLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications); + mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications); + mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications); logNotificationVisibilityChanges( - mTmpNewlyVisibleNotifications, noLongerVisibleNotifications); + mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications); - mCurrentlyVisibleNotifications.clear(); + recycleAllVisibilityObjects(mCurrentlyVisibleNotifications); mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications); - mTmpNewlyVisibleNotifications.clear(); + recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications); mTmpCurrentlyVisibleNotifications.clear(); + mTmpNewlyVisibleNotifications.clear(); + mTmpNoLongerVisibleNotifications.clear(); } }; + private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) { + final int N = array.size(); + for (int i = 0 ; i < N; i++) { + array.valueAt(i).recycle(); + } + array.clear(); + } + private final View.OnClickListener mOverflowClickListener = new View.OnClickListener() { @Override public void onClick(View v) { goToLockedShade(null); } }; + private HashMap<ExpandableNotificationRow, List<ExpandableNotificationRow>> mTmpChildOrderMap + = new HashMap<>(); + private HashSet<Entry> mHeadsUpEntriesToRemoveOnSwitch = new HashSet<>(); + private RankingMap mLatestRankingMap; + private boolean mNoAnimationOnNextBarModeChange; @Override public void start() { @@ -585,6 +582,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateDisplaySize(); mScrimSrcModeEnabled = mContext.getResources().getBoolean( R.bool.config_status_bar_scrim_behind_use_src); + super.start(); // calls createAndAddWindows() mMediaSessionManager @@ -596,6 +594,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // Lastly, call to the icon policy to install/update all the icons. mIconPolicy = new PhoneStatusBarPolicy(mContext, mCastController, mHotspotController); + mIconPolicy.setCurrentUserSetup(mUserSetup); mSettingsObserver.onChange(false); // set up mHeadsUpObserver.onChange(true); // set up @@ -633,11 +632,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateDisplaySize(); // populates mDisplayMetrics updateResources(); - mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size); - mStatusBarWindow = (StatusBarWindowView) View.inflate(context, R.layout.super_status_bar, null); - mStatusBarWindow.mService = this; + mStatusBarWindow.setService(this); mStatusBarWindow.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { @@ -648,7 +645,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } return mStatusBarWindow.onTouchEvent(event); - }}); + } + }); mStatusBarView = (PhoneStatusBarView) mStatusBarWindow.findViewById(R.id.status_bar); mStatusBarView.setBar(this); @@ -662,23 +660,23 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (!ActivityManager.isHighEndGfx()) { mStatusBarWindow.setBackground(null); - mNotificationPanel.setBackground(new FastColorDrawable(context.getResources().getColor( + mNotificationPanel.setBackground(new FastColorDrawable(context.getColor( R.color.notification_panel_solid_background))); } - if (ENABLE_HEADS_UP) { - mHeadsUpNotificationView = - (HeadsUpNotificationView) View.inflate(context, R.layout.heads_up, null); - mHeadsUpNotificationView.setVisibility(View.GONE); - mHeadsUpNotificationView.setBar(this); - } + + mHeadsUpManager = new HeadsUpManager(context, mStatusBarWindow); + mHeadsUpManager.setBar(this); + mHeadsUpManager.addListener(this); + mHeadsUpManager.addListener(mNotificationPanel); + mNotificationPanel.setHeadsUpManager(mHeadsUpManager); + mNotificationData.setHeadsUpManager(mHeadsUpManager); + if (MULTIUSER_DEBUG) { mNotificationPanelDebugText = (TextView) mNotificationPanel.findViewById( R.id.header_debug_info); mNotificationPanelDebugText.setVisibility(View.VISIBLE); } - updateShowSearchHoldoff(); - try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav); @@ -686,14 +684,14 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mNavigationBarView = (NavigationBarView) View.inflate(context, R.layout.navigation_bar, null); - mNavigationBarView.setDisabledFlags(mDisabled); + mNavigationBarView.setDisabledFlags(mDisabled1); mNavigationBarView.setBar(this); mNavigationBarView.setOnVerticalChangedListener( new NavigationBarView.OnVerticalChangedListener() { @Override public void onVerticalChanged(boolean isVertical) { - if (mSearchPanelView != null) { - mSearchPanelView.setHorizontal(isVertical); + if (mAssistManager != null) { + mAssistManager.onConfigurationChanged(); } mNotificationPanel.setQsScrimEnabled(!isVertical); } @@ -709,29 +707,25 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // no window manager? good luck with that } + mAssistManager = new AssistManager(this, context); + // figure out which pixel-format to use for the status bar. mPixelFormat = PixelFormat.OPAQUE; - mSystemIconArea = (LinearLayout) mStatusBarView.findViewById(R.id.system_icon_area); - mSystemIcons = (LinearLayout) mStatusBarView.findViewById(R.id.system_icons); - mStatusIcons = (LinearLayout)mStatusBarView.findViewById(R.id.statusIcons); - mNotificationIconArea = mStatusBarView.findViewById(R.id.notification_icon_area_inner); - mNotificationIcons = (IconMerger)mStatusBarView.findViewById(R.id.notificationIcons); - mMoreIcon = mStatusBarView.findViewById(R.id.moreIcon); - mNotificationIcons.setOverflowIndicator(mMoreIcon); - mStatusBarContents = (LinearLayout)mStatusBarView.findViewById(R.id.status_bar_contents); - mStackScroller = (NotificationStackScrollLayout) mStatusBarWindow.findViewById( R.id.notification_stack_scroller); mStackScroller.setLongPressListener(getNotificationLongClicker()); mStackScroller.setPhoneStatusBar(this); + mStackScroller.setGroupManager(mGroupManager); + mStackScroller.setHeadsUpManager(mHeadsUpManager); + mGroupManager.setOnGroupChangeListener(mStackScroller); mKeyguardIconOverflowContainer = (NotificationOverflowContainer) LayoutInflater.from(mContext).inflate( R.layout.status_bar_notification_keyguard_overflow, mStackScroller, false); mKeyguardIconOverflowContainer.setOnActivatedListener(this); mKeyguardIconOverflowContainer.setOnClickListener(mOverflowClickListener); - mStackScroller.addView(mKeyguardIconOverflowContainer); + mStackScroller.setOverflowContainer(mKeyguardIconOverflowContainer); SpeedBumpView speedBump = (SpeedBumpView) LayoutInflater.from(mContext).inflate( R.layout.status_bar_notification_speed_bump, mStackScroller, false); @@ -744,6 +738,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mDismissView.setOnButtonClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + MetricsLogger.action(mContext, MetricsLogger.ACTION_DISMISS_ALL_NOTES); clearAllNotifications(); } }); @@ -756,7 +751,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, ScrimView scrimBehind = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_behind); ScrimView scrimInFront = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_in_front); - mScrimController = new ScrimController(scrimBehind, scrimInFront, mScrimSrcModeEnabled); + View headsUpScrim = mStatusBarWindow.findViewById(R.id.heads_up_scrim); + mScrimController = new ScrimController(scrimBehind, scrimInFront, headsUpScrim, + mScrimSrcModeEnabled); + mHeadsUpManager.addListener(mScrimController); + mStackScroller.setScrimController(mScrimController); mScrimController.setBackDropView(mBackdrop); mStatusBarView.setScrimController(mScrimController); mDozeScrimController = new DozeScrimController(mScrimController, context); @@ -764,39 +763,29 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mHeader = (StatusBarHeaderView) mStatusBarWindow.findViewById(R.id.header); mHeader.setActivityStarter(this); mKeyguardStatusBar = (KeyguardStatusBarView) mStatusBarWindow.findViewById(R.id.keyguard_header); - mStatusIconsKeyguard = (LinearLayout) mKeyguardStatusBar.findViewById(R.id.statusIcons); mKeyguardStatusView = mStatusBarWindow.findViewById(R.id.keyguard_status_view); mKeyguardBottomArea = (KeyguardBottomAreaView) mStatusBarWindow.findViewById(R.id.keyguard_bottom_area); mKeyguardBottomArea.setActivityStarter(this); + mKeyguardBottomArea.setAssistManager(mAssistManager); mKeyguardIndicationController = new KeyguardIndicationController(mContext, (KeyguardIndicationTextView) mStatusBarWindow.findViewById( R.id.keyguard_indication_text)); mKeyguardBottomArea.setKeyguardIndicationController(mKeyguardIndicationController); - mTickerEnabled = res.getBoolean(R.bool.enable_ticker); - if (mTickerEnabled) { - final ViewStub tickerStub = (ViewStub) mStatusBarView.findViewById(R.id.ticker_stub); - if (tickerStub != null) { - mTickerView = tickerStub.inflate(); - mTicker = new MyTicker(context, mStatusBarView); - - TickerView tickerView = (TickerView) mStatusBarView.findViewById(R.id.tickerText); - tickerView.mTicker = mTicker; - } - } - - mEdgeBorder = res.getDimensionPixelSize(R.dimen.status_bar_edge_ignore); - // set the inital view visibility setAreThereNotifications(); + mIconController = new StatusBarIconController( + mContext, mStatusBarView, mKeyguardStatusBar, this); + // Background thread for any controllers that need it. mHandlerThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); mHandlerThread.start(); // Other icons - mLocationController = new LocationControllerImpl(mContext); // will post a notification + mLocationController = new LocationControllerImpl(mContext, + mHandlerThread.getLooper()); // will post a notification mBatteryController = new BatteryController(mContext); mBatteryController.addStateChangedCallback(new BatteryStateChangeCallback() { @Override @@ -811,7 +800,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // noop } }); - mNetworkController = new NetworkControllerImpl(mContext); + mNetworkController = new NetworkControllerImpl(mContext, mHandlerThread.getLooper()); mHotspotController = new HotspotControllerImpl(mContext); mBluetoothController = new BluetoothControllerImpl(mContext, mHandlerThread.getLooper()); mSecurityController = new SecurityControllerImpl(mContext); @@ -830,9 +819,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, (SignalClusterView) mKeyguardStatusBar.findViewById(R.id.signal_cluster); final SignalClusterView signalClusterQs = (SignalClusterView) mHeader.findViewById(R.id.signal_cluster); - mNetworkController.addSignalCluster(signalCluster); - mNetworkController.addSignalCluster(signalClusterKeyguard); - mNetworkController.addSignalCluster(signalClusterQs); + mNetworkController.addSignalCallback(signalCluster); + mNetworkController.addSignalCallback(signalClusterKeyguard); + mNetworkController.addSignalCallback(signalClusterQs); signalCluster.setSecurityController(mSecurityController); signalCluster.setNetworkController(mNetworkController); signalClusterKeyguard.setSecurityController(mSecurityController); @@ -841,33 +830,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, signalClusterQs.setNetworkController(mNetworkController); final boolean isAPhone = mNetworkController.hasVoiceCallingFeature(); if (isAPhone) { - mNetworkController.addEmergencyListener(new NetworkControllerImpl.EmergencyListener() { - @Override - public void setEmergencyCallsOnly(boolean emergencyOnly) { - mHeader.setShowEmergencyCallsOnly(emergencyOnly); - } - }); - } - - mCarrierLabel = (TextView)mStatusBarWindow.findViewById(R.id.carrier_label); - mShowCarrierInPanel = (mCarrierLabel != null); - if (DEBUG) Log.v(TAG, "carrierlabel=" + mCarrierLabel + " show=" + mShowCarrierInPanel); - if (mShowCarrierInPanel) { - mCarrierLabel.setVisibility(mCarrierLabelVisible ? View.VISIBLE : View.INVISIBLE); - - mNetworkController.addCarrierLabel(new NetworkControllerImpl.CarrierLabelListener() { - @Override - public void setCarrierLabel(String label) { - mCarrierLabel.setText(label); - if (mNetworkController.hasMobileDataFeature()) { - if (TextUtils.isEmpty(label)) { - mCarrierLabel.setVisibility(View.GONE); - } else { - mCarrierLabel.setVisibility(View.VISIBLE); - } - } - } - }); + mNetworkController.addEmergencyListener(mHeader); } mFlashlightController = new FlashlightController(mContext); @@ -876,7 +839,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mAccessibilityController = new AccessibilityController(mContext); mKeyguardBottomArea.setAccessibilityController(mAccessibilityController); mNextAlarmController = new NextAlarmController(mContext); - mKeyguardMonitor = new KeyguardMonitor(); + mKeyguardMonitor = new KeyguardMonitor(mContext); if (UserSwitcherController.isUserSwitcherAvailable(UserManager.get(mContext))) { mUserSwitcherController = new UserSwitcherController(mContext, mKeyguardMonitor); } @@ -910,6 +873,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // User info. Trigger first load. mHeader.setUserInfoController(mUserInfoController); mKeyguardStatusBar.setUserInfoController(mUserInfoController); + mKeyguardStatusBar.setUserSwitcherController(mUserSwitcherController); mUserInfoController.reloadUserInfo(); mHeader.setBatteryController(mBatteryController); @@ -922,6 +886,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mBroadcastReceiver.onReceive(mContext, new Intent(pm.isScreenOn() ? Intent.ACTION_SCREEN_ON : Intent.ACTION_SCREEN_OFF)); + // receive broadcasts IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); @@ -936,7 +901,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // listen for USER_SETUP_COMPLETE setting (per-user) resetUserSetupObserver(); - startGlyphRasterizeHack(); + // disable profiling bars, since they overlap and clutter the output on app windows + ThreadedRenderer.overrideProperty("disableProfileBars", "true"); + + // Private API call to make the shadows look better for Recents + ThreadedRenderer.overrideProperty("ambientRatio", String.valueOf(1.5f)); + return mStatusBarView; } @@ -948,9 +918,20 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, final ArrayList<View> viewsToHide = new ArrayList<View>(numChildren); for (int i = 0; i < numChildren; i++) { final View child = mStackScroller.getChildAt(i); - if (mStackScroller.canChildBeDismissed(child)) { - if (child.getVisibility() == View.VISIBLE) { - viewsToHide.add(child); + if (child instanceof ExpandableNotificationRow) { + if (mStackScroller.canChildBeDismissed(child)) { + if (child.getVisibility() == View.VISIBLE) { + viewsToHide.add(child); + } + } + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + List<ExpandableNotificationRow> children = row.getNotificationChildren(); + if (row.areChildrenExpanded() && children != null) { + for (ExpandableNotificationRow childRow : children) { + if (childRow.getVisibility() == View.VISIBLE) { + viewsToHide.add(childRow); + } + } } } } @@ -1007,30 +988,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } - /** - * Hack to improve glyph rasterization for scaled text views. - */ - private void startGlyphRasterizeHack() { - mStatusBarView.getViewTreeObserver().addOnPreDrawListener( - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - if (mDrawCount == 1) { - mStatusBarView.getViewTreeObserver().removeOnPreDrawListener(this); - HardwareCanvas.setProperty("extraRasterBucket", - Float.toString(StackScrollAlgorithm.DIMMED_SCALE)); - HardwareCanvas.setProperty("extraRasterBucket", Float.toString( - mContext.getResources().getDimensionPixelSize( - R.dimen.qs_time_collapsed_size) - / mContext.getResources().getDimensionPixelSize( - R.dimen.qs_time_expanded_size))); - } - mDrawCount++; - return true; - } - }); - } - @Override protected void setZenMode(int mode) { super.setZenMode(mode); @@ -1055,62 +1012,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mStatusBarWindow; } - @Override - protected WindowManager.LayoutParams getSearchLayoutParams(LayoutParams layoutParams) { - boolean opaque = false; - WindowManager.LayoutParams lp = new WindowManager.LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, - WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM - | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, - (opaque ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT)); - if (ActivityManager.isHighEndGfx()) { - lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; - } - lp.gravity = Gravity.BOTTOM | Gravity.START; - lp.setTitle("SearchPanel"); - lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED - | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; - return lp; - } - - @Override - protected void updateSearchPanel() { - super.updateSearchPanel(); - if (mNavigationBarView != null) { - mNavigationBarView.setDelegateView(mSearchPanelView); - } - } - - @Override - public void showSearchPanel() { - super.showSearchPanel(); - mHandler.removeCallbacks(mShowSearchPanel); - - // we want to freeze the sysui state wherever it is - mSearchPanelView.setSystemUiVisibility(mSystemUiVisibility); - - if (mNavigationBarView != null) { - WindowManager.LayoutParams lp = - (android.view.WindowManager.LayoutParams) mNavigationBarView.getLayoutParams(); - lp.flags &= ~WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; - mWindowManager.updateViewLayout(mNavigationBarView, lp); - } - } - - @Override - public void hideSearchPanel() { - super.hideSearchPanel(); - if (mNavigationBarView != null) { - WindowManager.LayoutParams lp = - (android.view.WindowManager.LayoutParams) mNavigationBarView.getLayoutParams(); - lp.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; - mWindowManager.updateViewLayout(mNavigationBarView, lp); - } - } - public int getStatusBarHeight() { if (mNaturalBarHeight < 0) { final Resources res = mContext.getResources(); @@ -1137,31 +1038,32 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } }; - private int mShowSearchHoldoff = 0; - private Runnable mShowSearchPanel = new Runnable() { - public void run() { - showSearchPanel(); + private final View.OnLongClickListener mLongPressHomeListener + = new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (shouldDisableNavbarGestures()) { + return false; + } + mAssistManager.prepareBeforeInvocation(); + mAssistManager.onGestureInvoked(); awakenDreams(); + if (mNavigationBarView != null) { + mNavigationBarView.abortCurrentGesture(); + } + return true; } }; - View.OnTouchListener mHomeActionListener = new View.OnTouchListener() { + private final View.OnTouchListener mHomeActionListener = new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { - switch(event.getAction()) { - case MotionEvent.ACTION_DOWN: - if (!shouldDisableNavbarGestures()) { - mHandler.removeCallbacks(mShowSearchPanel); - mHandler.postDelayed(mShowSearchPanel, mShowSearchHoldoff); - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mHandler.removeCallbacks(mShowSearchPanel); - awakenDreams(); - break; - } - return false; + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + awakenDreams(); + break; + } + return false; } }; @@ -1185,7 +1087,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mNavigationBarView.getBackButton().setLongClickable(true); mNavigationBarView.getBackButton().setOnLongClickListener(mLongPressBackRecentsListener); mNavigationBarView.getHomeButton().setOnTouchListener(mHomeActionListener); - updateSearchPanel(); + mNavigationBarView.getHomeButton().setOnLongClickListener(mLongPressHomeListener); + mAssistManager.onConfigurationChanged(); } // For small-screen devices (read: phones) that lack hardware navigation buttons @@ -1232,75 +1135,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return lp; } - private void addHeadsUpView() { - int headsUpHeight = mContext.getResources() - .getDimensionPixelSize(R.dimen.heads_up_window_height); - WindowManager.LayoutParams lp = new WindowManager.LayoutParams( - LayoutParams.MATCH_PARENT, headsUpHeight, - WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL, // above the status bar! - WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM - | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, - PixelFormat.TRANSLUCENT); - lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; - lp.gravity = Gravity.TOP; - lp.setTitle("Heads Up"); - lp.packageName = mContext.getPackageName(); - lp.windowAnimations = R.style.Animation_StatusBar_HeadsUp; - - mWindowManager.addView(mHeadsUpNotificationView, lp); - } - - private void removeHeadsUpView() { - mWindowManager.removeView(mHeadsUpNotificationView); - } - - public void refreshAllStatusBarIcons() { - refreshAllIconsForLayout(mStatusIcons); - refreshAllIconsForLayout(mStatusIconsKeyguard); - refreshAllIconsForLayout(mNotificationIcons); - } - - private void refreshAllIconsForLayout(LinearLayout ll) { - final int count = ll.getChildCount(); - for (int n = 0; n < count; n++) { - View child = ll.getChildAt(n); - if (child instanceof StatusBarIconView) { - ((StatusBarIconView) child).updateDrawable(); - } - } - } - public void addIcon(String slot, int index, int viewIndex, StatusBarIcon icon) { - if (SPEW) Log.d(TAG, "addIcon slot=" + slot + " index=" + index + " viewIndex=" + viewIndex - + " icon=" + icon); - StatusBarIconView view = new StatusBarIconView(mContext, slot, null); - view.set(icon); - mStatusIcons.addView(view, viewIndex, new LinearLayout.LayoutParams( - LayoutParams.WRAP_CONTENT, mIconSize)); - view = new StatusBarIconView(mContext, slot, null); - view.set(icon); - mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams( - LayoutParams.WRAP_CONTENT, mIconSize)); + mIconController.addSystemIcon(slot, index, viewIndex, icon); } public void updateIcon(String slot, int index, int viewIndex, StatusBarIcon old, StatusBarIcon icon) { - if (SPEW) Log.d(TAG, "updateIcon slot=" + slot + " index=" + index + " viewIndex=" + viewIndex - + " old=" + old + " icon=" + icon); - StatusBarIconView view = (StatusBarIconView) mStatusIcons.getChildAt(viewIndex); - view.set(icon); - view = (StatusBarIconView) mStatusIconsKeyguard.getChildAt(viewIndex); - view.set(icon); + mIconController.updateSystemIcon(slot, index, viewIndex, old, icon); } public void removeIcon(String slot, int index, int viewIndex) { - if (SPEW) Log.d(TAG, "removeIcon slot=" + slot + " index=" + index + " viewIndex=" + viewIndex); - mStatusIcons.removeViewAt(viewIndex); - mStatusIconsKeyguard.removeViewAt(viewIndex); + mIconController.removeSystemIcon(slot, index, viewIndex); } public UserHandle getCurrentUserHandle() { @@ -1308,27 +1153,22 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } @Override - public void addNotification(StatusBarNotification notification, RankingMap ranking) { + public void addNotification(StatusBarNotification notification, RankingMap ranking, + Entry oldEntry) { if (DEBUG) Log.d(TAG, "addNotification key=" + notification.getKey()); - if (mUseHeadsUp && shouldInterrupt(notification)) { - if (DEBUG) Log.d(TAG, "launching notification in heads up mode"); - Entry interruptionCandidate = new Entry(notification, null); - ViewGroup holder = mHeadsUpNotificationView.getHolder(); - if (inflateViewsForHeadsUp(interruptionCandidate, holder)) { - // 1. Populate mHeadsUpNotificationView - mHeadsUpNotificationView.showNotification(interruptionCandidate); - - // do not show the notification in the shade, yet. - return; - } - } Entry shadeEntry = createNotificationViews(notification); if (shadeEntry == null) { return; } + boolean isHeadsUped = mUseHeadsUp && shouldInterrupt(shadeEntry); + if (isHeadsUped) { + mHeadsUpManager.showNotification(shadeEntry); + // Mark as seen immediately + setNotificationShown(notification); + } - if (notification.getNotification().fullScreenIntent != null) { + if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) { // Stop screensaver if the notification has a full-screen intent. // (like an incoming phone call) awakenDreams(); @@ -1339,60 +1179,14 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, notification.getKey()); notification.getNotification().fullScreenIntent.send(); + shadeEntry.notifyFullScreenIntentLaunched(); + MetricsLogger.count(mContext, "note_fullscreen", 1); } catch (PendingIntent.CanceledException e) { } - } else { - // usual case: status bar visible & not immersive - - // show the ticker if there isn't already a heads up - if (mHeadsUpNotificationView.getEntry() == null) { - tick(notification, true); - } } addNotificationViews(shadeEntry, ranking); // Recalculate the position of the sliding windows and the titles. setAreThereNotifications(); - updateExpandedViewPos(EXPANDED_LEAVE_ALONE); - } - - public void displayNotificationFromHeadsUp(StatusBarNotification notification) { - NotificationData.Entry shadeEntry = createNotificationViews(notification); - if (shadeEntry == null) { - return; - } - shadeEntry.setInterruption(); - - addNotificationViews(shadeEntry, null); - // Recalculate the position of the sliding windows and the titles. - setAreThereNotifications(); - updateExpandedViewPos(EXPANDED_LEAVE_ALONE); - } - - @Override - public void resetHeadsUpDecayTimer() { - mHandler.removeMessages(MSG_DECAY_HEADS_UP); - if (mUseHeadsUp && mHeadsUpNotificationDecay > 0 - && mHeadsUpNotificationView.isClearable()) { - mHandler.sendEmptyMessageDelayed(MSG_DECAY_HEADS_UP, mHeadsUpNotificationDecay); - } - } - - @Override - public void scheduleHeadsUpOpen() { - mHandler.removeMessages(MSG_SHOW_HEADS_UP); - mHandler.sendEmptyMessage(MSG_SHOW_HEADS_UP); - } - - @Override - public void scheduleHeadsUpClose() { - mHandler.removeMessages(MSG_HIDE_HEADS_UP); - mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP); - } - - @Override - public void scheduleHeadsUpEscalation() { - mHandler.removeMessages(MSG_ESCALATE_HEADS_UP); - mHandler.sendEmptyMessage(MSG_ESCALATE_HEADS_UP); } @Override @@ -1403,23 +1197,19 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public void removeNotification(String key, RankingMap ranking) { - if (ENABLE_HEADS_UP && mHeadsUpNotificationView.getEntry() != null - && key.equals(mHeadsUpNotificationView.getEntry().notification.getKey())) { - mHeadsUpNotificationView.clear(); + boolean deferRemoval = false; + if (mHeadsUpManager.isHeadsUp(key)) { + deferRemoval = !mHeadsUpManager.removeNotification(key); + } + if (deferRemoval) { + mLatestRankingMap = ranking; + mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key)); + return; } - StatusBarNotification old = removeNotificationViews(key, ranking); if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old); if (old != null) { - // Cancel the ticker if it's still running - if (mTickerEnabled) { - mTicker.removeEntry(old); - } - - // Recalculate the position of the sliding windows and the titles. - updateExpandedViewPos(EXPANDED_LEAVE_ALONE); - if (CLOSE_PANEL_WHEN_EMPTIED && !hasActiveNotifications() && !mNotificationPanel.isTracking() && !mNotificationPanel.isQsExpanded()) { if (mState == StatusBarState.SHADE) { @@ -1437,12 +1227,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mNavigationBarView != null) { mNavigationBarView.setLayoutDirection(layoutDirection); } - refreshAllStatusBarIcons(); - } - - private void updateShowSearchHoldoff() { - mShowSearchHoldoff = mContext.getResources().getInteger( - R.integer.config_show_search_delay); } private void updateNotificationShade() { @@ -1483,10 +1267,23 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, ent.row.setShowingLegacyBackground(true); } } - toShow.add(ent.row); + if (mGroupManager.isChildInGroupWithSummary(ent.row.getStatusBarNotification())) { + ExpandableNotificationRow summary = mGroupManager.getGroupSummary( + ent.row.getStatusBarNotification()); + List<ExpandableNotificationRow> orderedChildren = + mTmpChildOrderMap.get(summary); + if (orderedChildren == null) { + orderedChildren = new ArrayList<>(); + mTmpChildOrderMap.put(summary, orderedChildren); + } + orderedChildren.add(ent.row); + } else { + toShow.add(ent.row); + } + } - ArrayList<View> toRemove = new ArrayList<View>(); + ArrayList<View> toRemove = new ArrayList<>(); for (int i=0; i< mStackScroller.getChildCount(); i++) { View child = mStackScroller.getChildAt(i); if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) { @@ -1515,29 +1312,88 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, continue; } - if (child == toShow.get(j)) { - // Everything is well, advance both lists. - j++; - continue; + ExpandableNotificationRow targetChild = toShow.get(j); + if (child != targetChild) { + // Oops, wrong notification at this position. Put the right one + // here and advance both lists. + mStackScroller.changeViewPosition(targetChild, i); } - - // Oops, wrong notification at this position. Put the right one - // here and advance both lists. - mStackScroller.changeViewPosition(toShow.get(j), i); j++; + } + + // lets handle the child notifications now + updateNotificationShadeForChildren(); + + // clear the map again for the next usage + mTmpChildOrderMap.clear(); + updateRowStates(); updateSpeedbump(); updateClearAll(); updateEmptyShadeView(); - // Disable QS if device not provisioned. - // If the user switcher is simple then disable QS during setup because - // the user intends to use the lock screen user switcher, QS in not needed. + updateQsExpansionEnabled(); + mShadeUpdates.check(); + } + + /** + * Disable QS if device not provisioned. + * If the user switcher is simple then disable QS during setup because + * the user intends to use the lock screen user switcher, QS in not needed. + */ + private void updateQsExpansionEnabled() { mNotificationPanel.setQsExpansionEnabled(isDeviceProvisioned() && (mUserSetup || mUserSwitcherController == null - || !mUserSwitcherController.isSimpleUserSwitcher())); - mShadeUpdates.check(); + || !mUserSwitcherController.isSimpleUserSwitcher()) + && ((mDisabled2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) == 0) + && !ONLY_CORE_APPS); + } + + private void updateNotificationShadeForChildren() { + ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>(); + boolean orderChanged = false; + for (int i = 0; i < mStackScroller.getChildCount(); i++) { + View view = mStackScroller.getChildAt(i); + if (!(view instanceof ExpandableNotificationRow)) { + // We don't care about non-notification views. + continue; + } + + ExpandableNotificationRow parent = (ExpandableNotificationRow) view; + List<ExpandableNotificationRow> children = parent.getNotificationChildren(); + List<ExpandableNotificationRow> orderedChildren = mTmpChildOrderMap.get(parent); + + // lets first remove all undesired children + if (children != null) { + toRemove.clear(); + for (ExpandableNotificationRow childRow : children) { + if (orderedChildren == null || !orderedChildren.contains(childRow)) { + toRemove.add(childRow); + } + } + for (ExpandableNotificationRow remove : toRemove) { + parent.removeChildNotification(remove); + mStackScroller.notifyGroupChildRemoved(remove); + } + } + + // We now add all the children which are not in there already + for (int childIndex = 0; orderedChildren != null && childIndex < orderedChildren.size(); + childIndex++) { + ExpandableNotificationRow childView = orderedChildren.get(childIndex); + if (children == null || !children.contains(childView)) { + parent.addChildNotification(childView, childIndex); + mStackScroller.notifyGroupChildAdded(childView); + } + } + + // Finally after removing and adding has been beformed we can apply the order. + orderChanged |= parent.applyChildOrder(orderedChildren); + } + if (orderChanged) { + mStackScroller.generateChildOrderChangedEvent(); + } } private boolean packageHasVisibilityOverride(String key) { @@ -1566,6 +1422,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, final int N = activeNotifications.size(); for (int i = 0; i < N; i++) { Entry entry = activeNotifications.get(i); + boolean isChild = !isTopLevelChild(entry); + if (isChild) { + continue; + } if (entry.row.getVisibility() != View.GONE && mNotificationData.isAmbient(entry.key)) { speedbumpIndex = currentIndex; @@ -1576,71 +1436,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mStackScroller.updateSpeedBumpIndex(speedbumpIndex); } + public static boolean isTopLevelChild(Entry entry) { + return entry.row.getParent() instanceof NotificationStackScrollLayout; + } + @Override protected void updateNotifications() { - // TODO: Move this into updateNotificationIcons()? - if (mNotificationIcons == null) return; - mNotificationData.filterAndSort(); updateNotificationShade(); - updateNotificationIcons(); - } - - private void updateNotificationIcons() { - final LinearLayout.LayoutParams params - = new LinearLayout.LayoutParams(mIconSize + 2*mIconHPadding, mNaturalBarHeight); - - ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications(); - final int N = activeNotifications.size(); - ArrayList<StatusBarIconView> toShow = new ArrayList<>(N); - - // Filter out notifications with low scores. - for (int i = 0; i < N; i++) { - Entry ent = activeNotifications.get(i); - if (ent.notification.getScore() < HIDE_ICONS_BELOW_SCORE && - !NotificationData.showNotificationEvenIfUnprovisioned(ent.notification)) { - continue; - } - toShow.add(ent.icon); - } - - if (DEBUG) { - Log.d(TAG, "refreshing icons: " + toShow.size() + - " notifications, mNotificationIcons=" + mNotificationIcons); - } - - ArrayList<View> toRemove = new ArrayList<View>(); - for (int i=0; i<mNotificationIcons.getChildCount(); i++) { - View child = mNotificationIcons.getChildAt(i); - if (!toShow.contains(child)) { - toRemove.add(child); - } - } - - final int toRemoveCount = toRemove.size(); - for (int i = 0; i < toRemoveCount; i++) { - mNotificationIcons.removeView(toRemove.get(i)); - } - - for (int i=0; i<toShow.size(); i++) { - View v = toShow.get(i); - if (v.getParent() == null) { - mNotificationIcons.addView(v, i, params); - } - } - - // Resort notification icons - final int childCount = mNotificationIcons.getChildCount(); - for (int i = 0; i < childCount; i++) { - View actual = mNotificationIcons.getChildAt(i); - StatusBarIconView expected = toShow.get(i); - if (actual == expected) { - continue; - } - mNotificationIcons.removeView(expected); - mNotificationIcons.addView(expected, i); - } + mIconController.updateNotificationIcons(mNotificationData); } @Override @@ -1649,53 +1454,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mNotificationPanel.notifyVisibleChildrenChanged(); } - 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("stackScrollerh=%d scrollh=%d carrierh=%d", - mStackScroller.getHeight(), mStackScroller.getHeight(), - mCarrierLabelHeight)); - } - - // Emergency calls only is shown in the expanded header now. - final boolean emergencyCallsShownElsewhere = true; - final boolean makeVisible = - !(emergencyCallsShownElsewhere && mNetworkController.isEmergencyOnly()) - && mStackScroller.getHeight() < (mNotificationPanel.getHeight() - - mCarrierLabelHeight - mStatusBarHeaderHeight) - && mStackScroller.getVisibility() == View.VISIBLE - && mState != StatusBarState.KEYGUARD; - - if (force || mCarrierLabelVisible != makeVisible) { - mCarrierLabelVisible = makeVisible; - if (DEBUG) { - Log.d(TAG, "making carrier label " + (makeVisible?"visible":"invisible")); - } - mCarrierLabel.animate().cancel(); - if (makeVisible) { - mCarrierLabel.setVisibility(View.VISIBLE); - } - mCarrierLabel.animate() - .alpha(makeVisible ? 1f : 0f) - //.setStartDelay(makeVisible ? 500 : 0) - //.setDuration(makeVisible ? 750 : 100) - .setDuration(150) - .setListener(makeVisible ? null : new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (!mCarrierLabelVisible) { // race - mCarrierLabel.setVisibility(View.INVISIBLE); - mCarrierLabel.setAlpha(0f); - } - } - }) - .start(); - } - } - @Override protected void setAreThereNotifications() { @@ -1728,8 +1486,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } findAndUpdateMediaNotifications(); - - updateCarrierLabelVisibility(false); } public void findAndUpdateMediaNotifications() { @@ -1873,11 +1629,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mBackdrop == null) return; // called too early + if (mLaunchTransitionFadingAway) { + mBackdrop.setVisibility(View.INVISIBLE); + return; + } + if (DEBUG_MEDIA) { Log.v(TAG, "DEBUG_MEDIA: updating album art for notification " + mMediaNotificationKey - + " metadata=" + mMediaMetadata - + " metaDataChanged=" + metaDataChanged - + " state=" + mState); + + " metadata=" + mMediaMetadata + + " metaDataChanged=" + metaDataChanged + + " state=" + mState); } Bitmap artworkBitmap = null; @@ -1973,14 +1734,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } - public void showClock(boolean show) { - if (mStatusBarView == null) return; - View clock = mStatusBarView.findViewById(R.id.clock); - if (clock != null) { - clock.setVisibility(show ? View.VISIBLE : View.GONE); - } - } - private int adjustDisableFlags(int state) { if (!mLaunchTransitionFadingAway && (mExpandedVisible || mBouncerShowing || mWaitingForKeyguardExit)) { @@ -1993,143 +1746,100 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, /** * State is one or more of the DISABLE constants from StatusBarManager. */ - public void disable(int state, boolean animate) { - mDisabledUnmodified = state; - state = adjustDisableFlags(state); - final int old = mDisabled; - final int diff = state ^ old; - mDisabled = state; + public void disable(int state1, int state2, boolean animate) { + animate &= mStatusBarWindowState != WINDOW_STATE_HIDDEN; + mDisabledUnmodified1 = state1; + mDisabledUnmodified2 = state2; + state1 = adjustDisableFlags(state1); + final int old1 = mDisabled1; + final int diff1 = state1 ^ old1; + mDisabled1 = state1; + + final int old2 = mDisabled2; + final int diff2 = state2 ^ old2; + mDisabled2 = state2; if (DEBUG) { - Log.d(TAG, String.format("disable: 0x%08x -> 0x%08x (diff: 0x%08x)", - old, state, diff)); + Log.d(TAG, String.format("disable1: 0x%08x -> 0x%08x (diff1: 0x%08x)", + old1, state1, diff1)); + Log.d(TAG, String.format("disable2: 0x%08x -> 0x%08x (diff2: 0x%08x)", + old2, state2, diff2)); } StringBuilder flagdbg = new StringBuilder(); flagdbg.append("disable: < "); - flagdbg.append(((state & StatusBarManager.DISABLE_EXPAND) != 0) ? "EXPAND" : "expand"); - flagdbg.append(((diff & StatusBarManager.DISABLE_EXPAND) != 0) ? "* " : " "); - flagdbg.append(((state & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) ? "ICONS" : "icons"); - 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_SYSTEM_INFO) != 0) ? "SYSTEM_INFO" : "system_info"); - flagdbg.append(((diff & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) ? "* " : " "); - flagdbg.append(((state & StatusBarManager.DISABLE_BACK) != 0) ? "BACK" : "back"); - flagdbg.append(((diff & StatusBarManager.DISABLE_BACK) != 0) ? "* " : " "); - flagdbg.append(((state & StatusBarManager.DISABLE_HOME) != 0) ? "HOME" : "home"); - flagdbg.append(((diff & StatusBarManager.DISABLE_HOME) != 0) ? "* " : " "); - flagdbg.append(((state & StatusBarManager.DISABLE_RECENT) != 0) ? "RECENT" : "recent"); - flagdbg.append(((diff & StatusBarManager.DISABLE_RECENT) != 0) ? "* " : " "); - flagdbg.append(((state & StatusBarManager.DISABLE_CLOCK) != 0) ? "CLOCK" : "clock"); - flagdbg.append(((diff & StatusBarManager.DISABLE_CLOCK) != 0) ? "* " : " "); - flagdbg.append(((state & StatusBarManager.DISABLE_SEARCH) != 0) ? "SEARCH" : "search"); - flagdbg.append(((diff & StatusBarManager.DISABLE_SEARCH) != 0) ? "* " : " "); + flagdbg.append(((state1 & StatusBarManager.DISABLE_EXPAND) != 0) ? "EXPAND" : "expand"); + flagdbg.append(((diff1 & StatusBarManager.DISABLE_EXPAND) != 0) ? "* " : " "); + flagdbg.append(((state1 & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) ? "ICONS" : "icons"); + flagdbg.append(((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) ? "* " : " "); + flagdbg.append(((state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) ? "ALERTS" : "alerts"); + flagdbg.append(((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) ? "* " : " "); + flagdbg.append(((state1 & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) ? "SYSTEM_INFO" : "system_info"); + flagdbg.append(((diff1 & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) ? "* " : " "); + flagdbg.append(((state1 & StatusBarManager.DISABLE_BACK) != 0) ? "BACK" : "back"); + flagdbg.append(((diff1 & StatusBarManager.DISABLE_BACK) != 0) ? "* " : " "); + flagdbg.append(((state1 & StatusBarManager.DISABLE_HOME) != 0) ? "HOME" : "home"); + flagdbg.append(((diff1 & StatusBarManager.DISABLE_HOME) != 0) ? "* " : " "); + flagdbg.append(((state1 & StatusBarManager.DISABLE_RECENT) != 0) ? "RECENT" : "recent"); + flagdbg.append(((diff1 & StatusBarManager.DISABLE_RECENT) != 0) ? "* " : " "); + flagdbg.append(((state1 & StatusBarManager.DISABLE_CLOCK) != 0) ? "CLOCK" : "clock"); + flagdbg.append(((diff1 & StatusBarManager.DISABLE_CLOCK) != 0) ? "* " : " "); + flagdbg.append(((state1 & StatusBarManager.DISABLE_SEARCH) != 0) ? "SEARCH" : "search"); + flagdbg.append(((diff1 & StatusBarManager.DISABLE_SEARCH) != 0) ? "* " : " "); + flagdbg.append(((state2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) != 0) ? "QUICK_SETTINGS" + : "quick_settings"); + flagdbg.append(((diff2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) != 0) ? "* " : " "); flagdbg.append(">"); Log.d(TAG, flagdbg.toString()); - if ((diff & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) { - mSystemIconArea.animate().cancel(); - if ((state & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) { - animateStatusBarHide(mSystemIconArea, animate); + if ((diff1 & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) { + if ((state1 & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) { + mIconController.hideSystemIconArea(animate); } else { - animateStatusBarShow(mSystemIconArea, animate); + mIconController.showSystemIconArea(animate); } } - if ((diff & StatusBarManager.DISABLE_CLOCK) != 0) { - boolean show = (state & StatusBarManager.DISABLE_CLOCK) == 0; - showClock(show); + if ((diff1 & StatusBarManager.DISABLE_CLOCK) != 0) { + boolean visible = (state1 & StatusBarManager.DISABLE_CLOCK) == 0; + mIconController.setClockVisibility(visible); } - if ((diff & StatusBarManager.DISABLE_EXPAND) != 0) { - if ((state & StatusBarManager.DISABLE_EXPAND) != 0) { + if ((diff1 & StatusBarManager.DISABLE_EXPAND) != 0) { + if ((state1 & StatusBarManager.DISABLE_EXPAND) != 0) { animateCollapsePanels(); } } - if ((diff & (StatusBarManager.DISABLE_HOME + if ((diff1 & (StatusBarManager.DISABLE_HOME | StatusBarManager.DISABLE_RECENT | StatusBarManager.DISABLE_BACK | StatusBarManager.DISABLE_SEARCH)) != 0) { // the nav bar will take care of these - if (mNavigationBarView != null) mNavigationBarView.setDisabledFlags(state); + if (mNavigationBarView != null) mNavigationBarView.setDisabledFlags(state1); - if ((state & StatusBarManager.DISABLE_RECENT) != 0) { + if ((state1 & StatusBarManager.DISABLE_RECENT) != 0) { // close recents if it's visible mHandler.removeMessages(MSG_HIDE_RECENT_APPS); mHandler.sendEmptyMessage(MSG_HIDE_RECENT_APPS); } } - if ((diff & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) { - if ((state & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) { - if (mTicking) { - haltTicker(); - } - animateStatusBarHide(mNotificationIconArea, animate); + if ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) { + if ((state1 & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) { + mIconController.hideNotificationIconArea(animate); } else { - animateStatusBarShow(mNotificationIconArea, animate); + mIconController.showNotificationIconArea(animate); } } - if ((diff & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) { + if ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) { mDisableNotificationAlerts = - (state & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0; + (state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0; mHeadsUpObserver.onChange(true); } - } - - /** - * Animates {@code v}, a view that is part of the status bar, out. - */ - private void animateStatusBarHide(final View v, boolean animate) { - v.animate().cancel(); - if (!animate) { - v.setAlpha(0f); - v.setVisibility(View.INVISIBLE); - return; - } - v.animate() - .alpha(0f) - .setDuration(160) - .setStartDelay(0) - .setInterpolator(ALPHA_OUT) - .withEndAction(new Runnable() { - @Override - public void run() { - v.setVisibility(View.INVISIBLE); - } - }); - } - - /** - * Animates {@code v}, a view that is part of the status bar, in. - */ - private void animateStatusBarShow(View v, boolean animate) { - v.animate().cancel(); - v.setVisibility(View.VISIBLE); - if (!animate) { - v.setAlpha(1f); - return; - } - v.animate() - .alpha(1f) - .setDuration(320) - .setInterpolator(ALPHA_IN) - .setStartDelay(50) - // We need to clean up any pending end action from animateStatusBarHide if we call - // both hide and show in the same frame before the animation actually gets started. - // cancel() doesn't really remove the end action. - .withEndAction(null); - - // Synchronize the motion with the Keyguard fading if necessary. - if (mKeyguardFadingAway) { - v.animate() - .setDuration(mKeyguardFadingAwayDuration) - .setInterpolator(mLinearOutSlowIn) - .setStartDelay(mKeyguardFadingAwayDelay) - .start(); + if ((diff2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) != 0) { + updateQsExpansionEnabled(); } } @@ -2143,12 +1853,21 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, startActivityDismissingKeyguard(intent, false, dismissShade); } - public ScrimController getScrimController() { - return mScrimController; + @Override + public void startActivity(Intent intent, boolean dismissShade, Callback callback) { + startActivityDismissingKeyguard(intent, false, dismissShade, callback); + } + + @Override + public void preventNextAnimation() { + overrideActivityPendingAppTransition(true /* keyguardShowing */); } public void setQsExpanded(boolean expanded) { mStatusBarWindowManager.setQsExpanded(expanded); + mKeyguardStatusView.setImportantForAccessibility(expanded + ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); } public boolean isGoingToNotificationShade() { @@ -2164,9 +1883,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } public boolean isFalsingThresholdNeeded() { - boolean onKeyguard = getBarState() == StatusBarState.KEYGUARD; - boolean isCurrentlyInsecure = mUnlockMethodCache.isCurrentlyInsecure(); - return onKeyguard && (isCurrentlyInsecure || mDozing || mScreenOnComingFromTouch); + return getBarState() == StatusBarState.KEYGUARD; } public boolean isDozing() { @@ -2194,6 +1911,112 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, logStateToEventlog(); } + @Override + public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) { + if (inPinnedMode) { + mStatusBarWindowManager.setHeadsUpShowing(true); + mStatusBarWindowManager.setForceStatusBarVisible(true); + if (mNotificationPanel.isFullyCollapsed()) { + // We need to ensure that the touchable region is updated before the window will be + // resized, in order to not catch any touches. A layout will ensure that + // onComputeInternalInsets will be called and after that we can resize the layout. Let's + // make sure that the window stays small for one frame until the touchableRegion is set. + mNotificationPanel.requestLayout(); + mStatusBarWindowManager.setForceWindowCollapsed(true); + mNotificationPanel.post(new Runnable() { + @Override + public void run() { + mStatusBarWindowManager.setForceWindowCollapsed(false); + } + }); + } + } else { + if (!mNotificationPanel.isFullyCollapsed() || mNotificationPanel.isTracking()) { + // We are currently tracking or is open and the shade doesn't need to be kept + // open artificially. + mStatusBarWindowManager.setHeadsUpShowing(false); + } else { + // we need to keep the panel open artificially, let's wait until the animation + // is finished. + mHeadsUpManager.setHeadsUpGoingAway(true); + mStackScroller.runAfterAnimationFinished(new Runnable() { + @Override + public void run() { + if (!mHeadsUpManager.hasPinnedHeadsUp()) { + mStatusBarWindowManager.setHeadsUpShowing(false); + mHeadsUpManager.setHeadsUpGoingAway(false); + } + } + }); + } + } + } + + @Override + public void onHeadsUpPinned(ExpandableNotificationRow headsUp) { + dismissVolumeDialog(); + } + + @Override + public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) { + } + + @Override + public void onHeadsUpStateChanged(Entry entry, boolean isHeadsUp) { + if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) { + removeNotification(entry.key, mLatestRankingMap); + mHeadsUpEntriesToRemoveOnSwitch.remove(entry); + if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) { + mLatestRankingMap = null; + } + } else { + updateNotificationRanking(null); + } + + } + + protected void updateHeadsUp(String key, Entry entry, boolean shouldInterrupt, + boolean alertAgain) { + final boolean wasHeadsUp = isHeadsUp(key); + if (wasHeadsUp) { + if (!shouldInterrupt) { + // We don't want this to be interrupting anymore, lets remove it + mHeadsUpManager.removeNotification(key); + } else { + mHeadsUpManager.updateNotification(entry, alertAgain); + } + } else if (shouldInterrupt && alertAgain) { + // This notification was updated to be a heads-up, show it! + mHeadsUpManager.showNotification(entry); + } + } + + protected void setHeadsUpUser(int newUserId) { + if (mHeadsUpManager != null) { + mHeadsUpManager.setUser(newUserId); + } + } + + public boolean isHeadsUp(String key) { + return mHeadsUpManager.isHeadsUp(key); + } + + protected boolean isSnoozedPackage(StatusBarNotification sbn) { + return mHeadsUpManager.isSnoozed(sbn.getPackageName()); + } + + public boolean isKeyguardCurrentlySecure() { + return !mUnlockMethodCache.isCurrentlyInsecure(); + } + + public void setPanelExpanded(boolean isExpanded) { + mStatusBarWindowManager.setPanelExpanded(isExpanded); + } + + public void endWindowManagerLogging() { + mStatusBarWindowManager.setLogState(false); + } + /** * All changes to the status bar and notifications funnel through here and are batched. */ @@ -2210,21 +2033,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, case MSG_CLOSE_PANELS: animateCollapsePanels(); break; - case MSG_SHOW_HEADS_UP: - setHeadsUpVisibility(true); - break; - case MSG_DECAY_HEADS_UP: - mHeadsUpNotificationView.release(); - setHeadsUpVisibility(false); - break; - case MSG_HIDE_HEADS_UP: - mHeadsUpNotificationView.release(); - setHeadsUpVisibility(false); - break; - case MSG_ESCALATE_HEADS_UP: - escalateHeadsUp(); - setHeadsUpVisibility(false); - break; case MSG_LAUNCH_TRANSITION_TIMEOUT: onLaunchTransitionTimeout(); break; @@ -2232,35 +2040,30 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } - /** if the interrupting notification had a fullscreen intent, fire it now. */ - private void escalateHeadsUp() { - if (mHeadsUpNotificationView.getEntry() != null) { - final StatusBarNotification sbn = mHeadsUpNotificationView.getEntry().notification; - mHeadsUpNotificationView.release(); + @Override + public void maybeEscalateHeadsUp() { + TreeSet<HeadsUpManager.HeadsUpEntry> entries = mHeadsUpManager.getSortedEntries(); + for (HeadsUpManager.HeadsUpEntry entry : entries) { + final StatusBarNotification sbn = entry.entry.notification; final Notification notification = sbn.getNotification(); if (notification.fullScreenIntent != null) { - if (DEBUG) + if (DEBUG) { Log.d(TAG, "converting a heads up to fullScreen"); + } try { EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_ESCALATION, sbn.getKey()); notification.fullScreenIntent.send(); + entry.entry.notifyFullScreenIntentLaunched(); } catch (PendingIntent.CanceledException e) { } } } + mHeadsUpManager.releaseAllImmediately(); } - View.OnFocusChangeListener mFocusChangeListener = new View.OnFocusChangeListener() { - public void onFocusChange(View v, boolean hasFocus) { - // Because 'v' is a ViewGroup, all its children will be (un)selected - // too, which allows marqueeing to work. - v.setSelected(hasFocus); - } - }; - boolean panelsEnabled() { - return (mDisabled & StatusBarManager.DISABLE_EXPAND) == 0; + return (mDisabled1 & StatusBarManager.DISABLE_EXPAND) == 0 && !ONLY_CORE_APPS; } void makeExpandedVisible(boolean force) { @@ -2273,18 +2076,14 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mNavigationBarView != null) mNavigationBarView.setSlippery(true); - updateCarrierLabelVisibility(true); - - updateExpandedViewPos(EXPANDED_LEAVE_ALONE); - // Expand the window to encompass the full screen in anticipation of the drag. // This is only possible to do atomically because the status bar is at the top of the screen! - mStatusBarWindowManager.setStatusBarExpanded(true); + mStatusBarWindowManager.setPanelVisible(true); mStatusBarView.setFocusable(false); visibilityChanged(true); mWaitingForKeyguardExit = false; - disable(mDisabledUnmodified, !force /* animate */); + disable(mDisabledUnmodified1, mDisabledUnmodified2, !force /* animate */); setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true); } @@ -2304,10 +2103,20 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } public void animateCollapsePanels(int flags) { - animateCollapsePanels(flags, false /* force */); + animateCollapsePanels(flags, false /* force */, false /* delayed */, + 1.0f /* speedUpFactor */); } public void animateCollapsePanels(int flags, boolean force) { + animateCollapsePanels(flags, force, false /* delayed */, 1.0f /* speedUpFactor */); + } + + public void animateCollapsePanels(int flags, boolean force, boolean delayed) { + animateCollapsePanels(flags, force, delayed, 1.0f /* speedUpFactor */); + } + + public void animateCollapsePanels(int flags, boolean force, boolean delayed, + float speedUpFactor) { if (!force && (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED)) { runPostCollapseRunnables(); @@ -2326,17 +2135,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } - if ((flags & CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL) == 0) { - mHandler.removeMessages(MSG_CLOSE_SEARCH_PANEL); - mHandler.sendEmptyMessage(MSG_CLOSE_SEARCH_PANEL); - } - if (mStatusBarWindow != null) { // release focus immediately to kick off focus change transition mStatusBarWindowManager.setStatusBarFocusable(false); mStatusBarWindow.cancelExpandHelper(); - mStatusBarView.collapseAllPanels(true); + mStatusBarView.collapseAllPanels(true /* animate */, delayed, speedUpFactor); } } @@ -2348,50 +2152,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mPostCollapseRunnables.clear(); } - public ViewPropertyAnimator setVisibilityWhenDone( - final ViewPropertyAnimator a, final View v, final int vis) { - a.setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - v.setVisibility(vis); - a.setListener(null); // oneshot - } - }); - return a; - } - - public Animator setVisibilityWhenDone( - final Animator a, final View v, final int vis) { - a.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - v.setVisibility(vis); - } - }); - return a; - } - - public Animator interpolator(TimeInterpolator ti, Animator a) { - a.setInterpolator(ti); - return a; - } - - public Animator startDelay(int d, Animator a) { - a.setStartDelay(d); - return a; - } - - public Animator start(Animator a) { - a.start(); - return a; - } - - final TimeInterpolator mAccelerateInterpolator = new AccelerateInterpolator(); - final TimeInterpolator mDecelerateInterpolator = new DecelerateInterpolator(); - final int FLIP_DURATION_OUT = 125; - final int FLIP_DURATION_IN = 225; - final int FLIP_DURATION = (FLIP_DURATION_IN + FLIP_DURATION_OUT); - Animator mScrollViewAnim, mClearButtonAnim; @Override @@ -2423,7 +2183,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, public void animateCollapseQuickSettings() { if (mState == StatusBarState.SHADE) { - mStatusBarView.collapseAllPanels(true); + mStatusBarView.collapseAllPanels(true, false /* delayed */, 1.0f /* speedUpFactor */); } } @@ -2436,14 +2196,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868) - mStatusBarView.collapseAllPanels(/*animate=*/ false); - - // reset things to their proper state - if (mScrollViewAnim != null) mScrollViewAnim.cancel(); - if (mClearButtonAnim != null) mClearButtonAnim.cancel(); - - mStackScroller.setVisibility(View.VISIBLE); - mNotificationPanel.setVisibility(View.GONE); + mStatusBarView.collapseAllPanels(/*animate=*/ false, false /* delayed*/, + 1.0f /* speedUpFactor */); mNotificationPanel.closeQs(); @@ -2453,7 +2207,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, visibilityChanged(false); // Shrink the window to the size of the status bar only - mStatusBarWindowManager.setStatusBarExpanded(false); + mStatusBarWindowManager.setPanelVisible(false); + mStatusBarWindowManager.setForceStatusBarVisible(false); mStatusBarView.setFocusable(true); // Close any "App info" popups that might have snuck on-screen @@ -2462,7 +2217,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, runPostCollapseRunnables(); setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false); showBouncer(); - disable(mDisabledUnmodified, true /* animate */); + disable(mDisabledUnmodified1, mDisabledUnmodified2, true /* animate */); // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in // the bouncer appear animation. @@ -2475,20 +2230,21 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (DEBUG_GESTURES) { if (event.getActionMasked() != MotionEvent.ACTION_MOVE) { EventLog.writeEvent(EventLogTags.SYSUI_STATUSBAR_TOUCH, - event.getActionMasked(), (int) event.getX(), (int) event.getY(), mDisabled); + event.getActionMasked(), (int) event.getX(), (int) event.getY(), + mDisabled1, mDisabled2); } } if (SPEW) { - Log.d(TAG, "Touch: rawY=" + event.getRawY() + " event=" + event + " mDisabled=" - + mDisabled + " mTracking=" + mTracking); + Log.d(TAG, "Touch: rawY=" + event.getRawY() + " event=" + event + " mDisabled1=" + + mDisabled1 + " mDisabled2=" + mDisabled2 + " mTracking=" + mTracking); } else if (CHATTY) { if (event.getAction() != MotionEvent.ACTION_MOVE) { Log.d(TAG, String.format( - "panel: %s at (%f, %f) mDisabled=0x%08x", + "panel: %s at (%f, %f) mDisabled1=0x%08x mDisabled2=0x%08x", MotionEvent.actionToString(event.getAction()), - event.getRawX(), event.getRawY(), mDisabled)); + event.getRawX(), event.getRawY(), mDisabled1, mDisabled2)); } } @@ -2533,7 +2289,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mStatusBarWindowState = state; if (DEBUG_WINDOW_STATE) Log.d(TAG, "Status bar " + windowStateToString(state)); if (!showing && mState == StatusBarState.SHADE) { - mStatusBarView.collapseAllPanels(false); + mStatusBarView.collapseAllPanels(false /* animate */, false /* delayed */, + 1.0f /* speedUpFactor */); } } if (mNavigationBarView != null @@ -2587,14 +2344,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, final boolean lightsOut = (vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0; if (lightsOut) { animateCollapsePanels(); - if (mTicking) { - haltTicker(); - } } setAreThereNotifications(); } + // ready to unhide + if ((vis & View.STATUS_BAR_UNHIDE) != 0) { + mSystemUiVisibility &= ~View.STATUS_BAR_UNHIDE; + mNoAnimationOnNextBarModeChange = true; + } + // update status bar mode final int sbMode = computeBarMode(oldVal, newVal, mStatusBarView.getBarTransitions(), View.STATUS_BAR_TRANSIENT, View.STATUS_BAR_TRANSLUCENT); @@ -2626,14 +2386,18 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } - // ready to unhide - if ((vis & View.STATUS_BAR_UNHIDE) != 0) { - mSystemUiVisibility &= ~View.STATUS_BAR_UNHIDE; - } if ((vis & View.NAVIGATION_BAR_UNHIDE) != 0) { mSystemUiVisibility &= ~View.NAVIGATION_BAR_UNHIDE; } + if ((diff & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0 || sbModeChanged) { + boolean isTransparentBar = (mStatusBarMode == MODE_TRANSPARENT + || mStatusBarMode == MODE_LIGHTS_OUT_TRANSPARENT); + boolean allowLight = isTransparentBar && !mBatteryController.isPowerSave(); + boolean light = (vis & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0; + + mIconController.setIconsDark(allowLight && light); + } // restore the recents bit if (wasRecentsVisible) { mSystemUiVisibility |= View.RECENT_APPS_VISIBLE; @@ -2666,17 +2430,21 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private void checkBarModes() { if (mDemoMode) return; - checkBarMode(mStatusBarMode, mStatusBarWindowState, mStatusBarView.getBarTransitions()); + checkBarMode(mStatusBarMode, mStatusBarWindowState, mStatusBarView.getBarTransitions(), + mNoAnimationOnNextBarModeChange); if (mNavigationBarView != null) { checkBarMode(mNavigationBarMode, - mNavigationBarWindowState, mNavigationBarView.getBarTransitions()); + mNavigationBarWindowState, mNavigationBarView.getBarTransitions(), + mNoAnimationOnNextBarModeChange); } + mNoAnimationOnNextBarModeChange = false; } - private void checkBarMode(int mode, int windowState, BarTransitions transitions) { + private void checkBarMode(int mode, int windowState, BarTransitions transitions, + boolean noAnimation) { final boolean powerSave = mBatteryController.isPowerSave(); - final boolean anim = (mScreenOn == null || mScreenOn) && windowState != WINDOW_STATE_HIDDEN - && !powerSave; + final boolean anim = !noAnimation && (mScreenOn == null || mScreenOn) + && windowState != WINDOW_STATE_HIDDEN && !powerSave; if (powerSave && getBarState() == StatusBarState.SHADE) { mode = MODE_WARNING; } @@ -2710,13 +2478,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } // manually dismiss the volume panel when interacting with the nav bar if (changing && interacting && barWindow == StatusBarManager.WINDOW_NAVIGATION_BAR) { - if (mVolumeComponent != null) { - mVolumeComponent.dismissNow(); - } + dismissVolumeDialog(); } checkBarModes(); } + private void dismissVolumeDialog() { + if (mVolumeComponent != null) { + mVolumeComponent.dismissNow(); + } + } + private void resumeSuspendedAutohide() { if (mAutohideSuspended) { scheduleAutohide(); @@ -2769,7 +2541,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private void notifyUiVisibilityChanged(int vis) { try { - mWindowManagerService.statusBarVisibilityChanged(vis); + if (mLastDispatchedSystemUiVisibility != vis) { + mWindowManagerService.statusBarVisibilityChanged(vis); + mLastDispatchedSystemUiVisibility = vis; + } } catch (RemoteException ex) { } } @@ -2805,90 +2580,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, setNavigationIconHints(flags); } - @Override - protected void tick(StatusBarNotification n, boolean firstTime) { - if (!mTickerEnabled) return; - - // no ticking in lights-out mode - if (!areLightsOn()) return; - - // no ticking in Setup - if (!isDeviceProvisioned()) return; - - // not for you - if (!isNotificationForCurrentProfiles(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 != null - && mStatusBarWindow.getWindowToken() != null) { - if (0 == (mDisabled & (StatusBarManager.DISABLE_NOTIFICATION_ICONS - | StatusBarManager.DISABLE_NOTIFICATION_TICKER))) { - mTicker.addEntry(n); - } - } - } - - private class MyTicker extends Ticker { - MyTicker(Context context, View sb) { - super(context, sb); - if (!mTickerEnabled) { - Log.w(TAG, "MyTicker instantiated with mTickerEnabled=false", new Throwable()); - } - } - - @Override - public void tickerStarting() { - if (!mTickerEnabled) return; - mTicking = true; - mStatusBarContents.setVisibility(View.GONE); - mTickerView.setVisibility(View.VISIBLE); - mTickerView.startAnimation(loadAnim(com.android.internal.R.anim.push_up_in, null)); - mStatusBarContents.startAnimation(loadAnim(com.android.internal.R.anim.push_up_out, null)); - } - - @Override - public void tickerDone() { - if (!mTickerEnabled) return; - mStatusBarContents.setVisibility(View.VISIBLE); - mTickerView.setVisibility(View.GONE); - mStatusBarContents.startAnimation(loadAnim(com.android.internal.R.anim.push_down_in, null)); - mTickerView.startAnimation(loadAnim(com.android.internal.R.anim.push_down_out, - mTickingDoneListener)); - } - - public void tickerHalting() { - if (!mTickerEnabled) return; - if (mStatusBarContents.getVisibility() != View.VISIBLE) { - mStatusBarContents.setVisibility(View.VISIBLE); - mStatusBarContents - .startAnimation(loadAnim(com.android.internal.R.anim.fade_in, null)); - } - mTickerView.setVisibility(View.GONE); - // we do not animate the ticker away at this point, just get rid of it (b/6992707) - } - } - - Animation.AnimationListener mTickingDoneListener = new Animation.AnimationListener() {; - public void onAnimationEnd(Animation animation) { - mTicking = false; - } - public void onAnimationRepeat(Animation animation) { - } - public void onAnimationStart(Animation animation) { - } - }; - - private Animation loadAnim(int id, Animation.AnimationListener listener) { - Animation anim = AnimationUtils.loadAnimation(mContext, id); - if (listener != null) { - anim.setAnimationListener(listener); - } - return anim; - } - public static String viewInfo(View v) { return "[(" + v.getLeft() + "," + v.getTop() + ")(" + v.getRight() + "," + v.getBottom() + ") " + v.getWidth() + "x" + v.getHeight() + "]"; @@ -2899,11 +2590,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, pw.println("Current Status Bar state:"); pw.println(" mExpandedVisible=" + mExpandedVisible + ", mTrackingPosition=" + mTrackingPosition); - pw.println(" mTickerEnabled=" + mTickerEnabled); - if (mTickerEnabled) { - pw.println(" mTicking=" + mTicking); - pw.println(" mTickerView: " + viewInfo(mTickerView)); - } pw.println(" mTracking=" + mTracking); pw.println(" mDisplayMetrics=" + mDisplayMetrics); pw.println(" mStackScroller: " + viewInfo(mStackScroller)); @@ -2922,8 +2608,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, pw.println(Settings.Global.zenModeToString(mZenMode)); pw.print(" mUseHeadsUp="); pw.println(mUseHeadsUp); - pw.print(" interrupting package: "); - pw.println(hunStateToString(mHeadsUpNotificationView.getEntry())); dumpBarTransitions(pw, "mStatusBarView", mStatusBarView.getBarTransitions()); if (mNavigationBarView != null) { pw.print(" mNavigationBarWindowState="); @@ -2972,12 +2656,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mNotificationData.dump(pw, " "); } - int N = mStatusIcons.getChildCount(); - pw.println(" system icons: " + N); - for (int i=0; i<N; i++) { - StatusBarIconView ic = (StatusBarIconView) mStatusIcons.getChildAt(i); - pw.println(" [" + i + "] icon=" + ic); - } + mIconController.dump(pw); if (false) { pw.println("see the logcat for a dump of the views we have created."); @@ -3017,12 +2696,20 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mNextAlarmController != null) { mNextAlarmController.dump(fd, pw, args); } + if (mAssistManager != null) { + mAssistManager.dump(fd, pw, args); + } if (mSecurityController != null) { mSecurityController.dump(fd, pw, args); } + if (mHeadsUpManager != null) { + mHeadsUpManager.dump(fd, pw, args); + } else { + pw.println(" mHeadsUpManager: null"); + } + pw.println("SharedPreferences:"); - for (Map.Entry<String, ?> entry : mContext.getSharedPreferences(mContext.getPackageName(), - Context.MODE_PRIVATE).getAll().entrySet()) { + for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) { pw.print(" "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue()); } } @@ -3049,25 +2736,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight()); } - static final float saturate(float a) { - return a < 0f ? 0f : (a > 1f ? 1f : a); - } - - @Override - public void updateExpandedViewPos(int thingy) { - if (SPEW) Log.v(TAG, "updateExpandedViewPos"); - - // on larger devices, the notification panel is propped open a bit - mNotificationPanel.setMinimumHeight( - (int)(mNotificationPanelMinHeightFrac * mCurrentDisplaySize.y)); - - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mNotificationPanel.getLayoutParams(); - lp.gravity = mNotificationPanelGravity; - mNotificationPanel.setLayoutParams(lp); - - updateCarrierLabelVisibility(false); - } - // called by makeStatusbar and also by PhoneStatusBarView void updateDisplaySize() { mDisplay.getMetrics(mDisplayMetrics); @@ -3083,12 +2751,57 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned, - final boolean dismissShade) { + boolean dismissShade) { + startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, null /* callback */); + } + + public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned, + final boolean dismissShade, final Callback callback) { if (onlyProvisioned && !isDeviceProvisioned()) return; final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity( mContext, intent, mCurrentUserId); final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing(); + Runnable runnable = new Runnable() { + public void run() { + mAssistManager.hideAssist(); + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + int result = ActivityManager.START_CANCELED; + try { + result = ActivityManagerNative.getDefault().startActivityAsUser( + null, mContext.getBasePackageName(), + intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, null, + UserHandle.CURRENT.getIdentifier()); + } catch (RemoteException e) { + Log.w(TAG, "Unable to start activity", e); + } + overrideActivityPendingAppTransition( + keyguardShowing && !afterKeyguardGone); + if (callback != null) { + callback.onActivityStarted(result); + } + } + }; + Runnable cancelRunnable = new Runnable() { + @Override + public void run() { + if (callback != null) { + callback.onActivityStarted(ActivityManager.START_CANCELED); + } + } + }; + executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShade, + afterKeyguardGone); + } + + public void executeRunnableDismissingKeyguard(final Runnable runnable, + final Runnable cancelAction, + final boolean dismissShade, + final boolean afterKeyguardGone) { + final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing(); dismissKeyguardThenExecute(new OnDismissAction() { @Override public boolean onDismiss() { @@ -3099,23 +2812,20 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, ActivityManagerNative.getDefault() .keyguardWaitingForActivityDrawn(); } - intent.setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - mContext.startActivityAsUser( - intent, new UserHandle(UserHandle.USER_CURRENT)); - overrideActivityPendingAppTransition( - keyguardShowing && !afterKeyguardGone); + if (runnable != null) { + runnable.run(); + } } catch (RemoteException e) { } } }); if (dismissShade) { - animateCollapsePanels( - CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */); + animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */, + true /* delayed*/); } return true; } - }, afterKeyguardGone); + }, cancelAction, afterKeyguardGone); } private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @@ -3135,7 +2845,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, else if (Intent.ACTION_SCREEN_OFF.equals(action)) { mScreenOn = false; notifyNavigationBarScreenOn(false); - notifyHeadsUpScreenOn(false); + notifyHeadsUpScreenOff(); finishBarAnimations(); resetUserExpandedStates(); } @@ -3175,10 +2885,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } @Override - protected void dismissKeyguardThenExecute(final OnDismissAction action, + protected void dismissKeyguardThenExecute(OnDismissAction action, boolean afterKeyguardGone) { + dismissKeyguardThenExecute(action, null /* cancelRunnable */, afterKeyguardGone); + } + + private void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancelAction, boolean afterKeyguardGone) { if (mStatusBarKeyguardViewManager.isShowing()) { - mStatusBarKeyguardViewManager.dismissWithAction(action, afterKeyguardGone); + mStatusBarKeyguardViewManager.dismissWithAction(action, cancelAction, + afterKeyguardGone); } else { action.onDismiss(); } @@ -3195,11 +2910,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateDisplaySize(); // populates mDisplayMetrics updateResources(); - updateClockSize(); repositionNavigationBar(); - updateExpandedViewPos(EXPANDED_LEAVE_ALONE); - updateShowSearchHoldoff(); updateRowStates(); + mIconController.updateResources(); mScreenPinningRequest.onConfigurationChanged(); } @@ -3212,12 +2925,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateNotifications(); resetUserSetupObserver(); setControllerUsers(); + mAssistManager.onUserSwitched(newUserId); } private void setControllerUsers() { if (mZenModeController != null) { mZenModeController.setUserId(mCurrentUserId); } + if (mSecurityController != null) { + mSecurityController.onUserSwitched(mCurrentUserId); + } } private void resetUserSetupObserver() { @@ -3225,21 +2942,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mUserSetupObserver.onChange(false); mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), true, - mUserSetupObserver, - mCurrentUserId); - } - - private void setHeadsUpVisibility(boolean vis) { - if (!ENABLE_HEADS_UP) return; - if (DEBUG) Log.v(TAG, (vis ? "showing" : "hiding") + " heads up window"); - EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_STATUS, - vis ? mHeadsUpNotificationView.getKey() : "", - vis ? 1 : 0); - mHeadsUpNotificationView.setVisibility(vis ? View.VISIBLE : View.GONE); - } - - public void onHeadsUpDismissed() { - mHeadsUpNotificationView.dismiss(); + mUserSetupObserver, mCurrentUserId); } /** @@ -3256,61 +2959,21 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } loadDimens(); - mLinearOutSlowIn = AnimationUtils.loadInterpolator( - mContext, android.R.interpolator.linear_out_slow_in); if (mNotificationPanel != null) { mNotificationPanel.updateResources(); } - if (mHeadsUpNotificationView != null) { - mHeadsUpNotificationView.updateResources(); - } if (mBrightnessMirrorController != null) { mBrightnessMirrorController.updateResources(); } } - private void updateClockSize() { - if (mStatusBarView == null) return; - TextView clock = (TextView) mStatusBarView.findViewById(R.id.clock); - if (clock != null) { - FontSizeUtils.updateFontSize(clock, R.dimen.status_bar_clock_size); - } - } protected void loadDimens() { final Resources res = mContext.getResources(); mNaturalBarHeight = res.getDimensionPixelSize( com.android.internal.R.dimen.status_bar_height); - int newIconSize = res.getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_icon_size); - int newIconHPadding = res.getDimensionPixelSize( - R.dimen.status_bar_icon_padding); - - if (newIconHPadding != mIconHPadding || newIconSize != mIconSize) { -// Log.d(TAG, "size=" + newIconSize + " padding=" + newIconHPadding); - mIconHPadding = newIconHPadding; - mIconSize = newIconSize; - //reloadAllNotificationIcons(); // reload the tray - } - - mEdgeBorder = res.getDimensionPixelSize(R.dimen.status_bar_edge_ignore); - - mNotificationPanelGravity = res.getInteger(R.integer.notification_panel_layout_gravity); - if (mNotificationPanelGravity <= 0) { - mNotificationPanelGravity = Gravity.START | Gravity.TOP; - } - - mCarrierLabelHeight = res.getDimensionPixelSize(R.dimen.carrier_label_height); - mStatusBarHeaderHeight = res.getDimensionPixelSize(R.dimen.status_bar_header_height); - - mNotificationPanelMinHeightFrac = res.getFraction(R.dimen.notification_panel_min_height_frac, 1, 1); - if (mNotificationPanelMinHeightFrac < 0f || mNotificationPanelMinHeightFrac > 1f) { - mNotificationPanelMinHeightFrac = 0f; - } - - mHeadsUpNotificationDecay = res.getInteger(R.integer.heads_up_notification_decay); mRowMinHeight = res.getDimensionPixelSize(R.dimen.notification_min_height); mRowMaxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height); @@ -3336,9 +2999,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // Report all notifications as invisible and turn down the // reporter. if (!mCurrentlyVisibleNotifications.isEmpty()) { - logNotificationVisibilityChanges( - Collections.<String>emptyList(), mCurrentlyVisibleNotifications); - mCurrentlyVisibleNotifications.clear(); + logNotificationVisibilityChanges(Collections.<NotificationVisibility>emptyList(), + mCurrentlyVisibleNotifications); + recycleAllVisibilityObjects(mCurrentlyVisibleNotifications); } mHandler.removeCallbacks(mVisibilityReporter); mStackScroller.setChildLocationsChangedListener(null); @@ -3356,17 +3019,27 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } private void logNotificationVisibilityChanges( - Collection<String> newlyVisible, Collection<String> noLongerVisible) { + Collection<NotificationVisibility> newlyVisible, + Collection<NotificationVisibility> noLongerVisible) { if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) { return; } - String[] newlyVisibleAr = newlyVisible.toArray(new String[newlyVisible.size()]); - String[] noLongerVisibleAr = noLongerVisible.toArray(new String[noLongerVisible.size()]); + NotificationVisibility[] newlyVisibleAr = + newlyVisible.toArray(new NotificationVisibility[newlyVisible.size()]); + NotificationVisibility[] noLongerVisibleAr = + noLongerVisible.toArray(new NotificationVisibility[noLongerVisible.size()]); try { mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr); } catch (RemoteException e) { // Ignore. } + + final int N = newlyVisible.size(); + String[] newlyVisibleKeyAr = new String[N]; + for (int i = 0; i < N; i++) { + newlyVisibleKeyAr[i] = newlyVisibleAr[i].key; + } + setNotificationsShown(newlyVisibleKeyAr); } // State logging @@ -3443,29 +3116,22 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, }; @Override - protected void haltTicker() { - if (mTickerEnabled) { - mTicker.halt(); - } - } - - @Override - protected boolean shouldDisableNavbarGestures() { + public boolean shouldDisableNavbarGestures() { return !isDeviceProvisioned() || mExpandedVisible - || (mDisabled & StatusBarManager.DISABLE_SEARCH) != 0; + || (mDisabled1 & StatusBarManager.DISABLE_SEARCH) != 0; } - public void postStartSettingsActivity(final Intent intent, int delay) { + public void postStartActivityDismissingKeyguard(final Intent intent, int delay) { mHandler.postDelayed(new Runnable() { @Override public void run() { - handleStartSettingsActivity(intent, true /*onlyProvisioned*/); + handleStartActivityDismissingKeyguard(intent, true /*onlyProvisioned*/); } }, delay); } - private void handleStartSettingsActivity(Intent intent, boolean onlyProvisioned) { + private void handleStartActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned) { startActivityDismissingKeyguard(intent, onlyProvisioned, true /* dismissShade */); } @@ -3486,7 +3152,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } @Override - public void setColorFilter(ColorFilter cf) { + public void setColorFilter(ColorFilter colorFilter) { } @Override @@ -3519,11 +3185,24 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mHandlerThread = null; } mContext.unregisterReceiver(mBroadcastReceiver); + mAssistManager.destroy(); + + final SignalClusterView signalCluster = + (SignalClusterView) mStatusBarView.findViewById(R.id.signal_cluster); + final SignalClusterView signalClusterKeyguard = + (SignalClusterView) mKeyguardStatusBar.findViewById(R.id.signal_cluster); + final SignalClusterView signalClusterQs = + (SignalClusterView) mHeader.findViewById(R.id.signal_cluster); + mNetworkController.removeSignalCallback(signalCluster); + mNetworkController.removeSignalCallback(signalClusterKeyguard); + mNetworkController.removeSignalCallback(signalClusterQs); + if (mQSPanel != null && mQSPanel.getHost() != null) { + mQSPanel.getHost().destroy(); + } } private boolean mDemoModeAllowed; private boolean mDemoMode; - private DemoStatusIcons mDemoStatusIcons; @Override public void dispatchDemoCommand(String command, Bundle args) { @@ -3552,10 +3231,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, dispatchDemoCommandToView(command, args, R.id.battery); } if (modeChange || command.equals(COMMAND_STATUS)) { - if (mDemoStatusIcons == null) { - mDemoStatusIcons = new DemoStatusIcons(mStatusIcons, mIconSize); - } - mDemoStatusIcons.dispatchDemoCommand(command, args); + mIconController.dispatchDemoCommand(command, args); + } if (mNetworkController != null && (modeChange || command.equals(COMMAND_NETWORK))) { mNetworkController.dispatchDemoCommand(command, args); @@ -3604,12 +3281,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mState; } + @Override + protected boolean isPanelFullyCollapsed() { + return mNotificationPanel.isFullyCollapsed(); + } + public void showKeyguard() { if (mLaunchTransitionFadingAway) { mNotificationPanel.animate().cancel(); - mNotificationPanel.setAlpha(1f); - runLaunchTransitionEndRunnable(); - mLaunchTransitionFadingAway = false; + onLaunchTransitionFadingEnded(); } mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT); setBarState(StatusBarState.KEYGUARD); @@ -3625,11 +3305,19 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mLeaveOpenOnKeyguardHide = false; if (mDraggedDownRow != null) { mDraggedDownRow.setUserLocked(false); - mDraggedDownRow.notifyHeightChanged(); + mDraggedDownRow.notifyHeightChanged(false /* needsAnimation */); mDraggedDownRow = null; } } + private void onLaunchTransitionFadingEnded() { + mNotificationPanel.setAlpha(1.0f); + runLaunchTransitionEndRunnable(); + mLaunchTransitionFadingAway = false; + mScrimController.forceHideScrims(false /* hide */); + updateMediaMetaData(true /* metaDataChanged */); + } + public boolean isCollapsing() { return mNotificationPanel.isCollapsing(); } @@ -3661,6 +3349,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (beforeFading != null) { beforeFading.run(); } + mScrimController.forceHideScrims(true /* hide */); + updateMediaMetaData(false); mNotificationPanel.setAlpha(1); mNotificationPanel.animate() .alpha(0) @@ -3670,11 +3360,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, .withEndAction(new Runnable() { @Override public void run() { - mNotificationPanel.setAlpha(1); - runLaunchTransitionEndRunnable(); - mLaunchTransitionFadingAway = false; + onLaunchTransitionFadingEnded(); } }); + mIconController.appTransitionStarting(SystemClock.uptimeMillis(), + StatusBarIconController.DEFAULT_TINT_ANIMATION_DURATION); } }; if (mNotificationPanel.isLaunchTransitionRunning()) { @@ -3742,17 +3432,32 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } /** + * Notifies the status bar that Keyguard is going away very soon. + */ + public void keyguardGoingAway() { + + // Treat Keyguard exit animation as an app transition to achieve nice transition for status + // bar. + mIconController.appTransitionPending(); + } + + /** * Notifies the status bar the Keyguard is fading away with the specified timings. * - * @param delay the animation delay in miliseconds + * @param startTime the start time of the animations in uptime millis + * @param delay the precalculated animation delay in miliseconds * @param fadeoutDuration the duration of the exit animation, in milliseconds */ - public void setKeyguardFadingAway(long delay, long fadeoutDuration) { + public void setKeyguardFadingAway(long startTime, long delay, long fadeoutDuration) { mKeyguardFadingAway = true; mKeyguardFadingAwayDelay = delay; mKeyguardFadingAwayDuration = fadeoutDuration; mWaitingForKeyguardExit = false; - disable(mDisabledUnmodified, true /* animate */); + mIconController.appTransitionStarting( + startTime + fadeoutDuration + - StatusBarIconController.DEFAULT_TINT_ANIMATION_DURATION, + StatusBarIconController.DEFAULT_TINT_ANIMATION_DURATION); + disable(mDisabledUnmodified1, mDisabledUnmodified2, true /* animate */); } public boolean isKeyguardFadingAway() { @@ -3766,9 +3471,14 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mKeyguardFadingAway = false; } + public void stopWaitingForKeyguardExit() { + mWaitingForKeyguardExit = false; + } + private void updatePublicMode() { - setLockscreenPublicMode(mStatusBarKeyguardViewManager.isShowing() - && mStatusBarKeyguardViewManager.isSecure(mCurrentUserId)); + setLockscreenPublicMode( + mStatusBarKeyguardViewManager.isShowing() && mStatusBarKeyguardViewManager + .isSecure(mCurrentUserId)); } private void updateKeyguardState(boolean goingToFullShade, boolean fromShadeLocked) { @@ -3776,6 +3486,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mKeyguardIndicationController.setVisible(true); mNotificationPanel.resetViews(); mKeyguardUserSwitcher.setKeyguard(true, fromShadeLocked); + mStatusBarView.removePendingHideExpandedRunnables(); } else { mKeyguardIndicationController.setVisible(false); mKeyguardUserSwitcher.setKeyguard(false, @@ -3783,8 +3494,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) { mScrimController.setKeyguardShowing(true); + mIconPolicy.setKeyguardShowing(true); } else { mScrimController.setKeyguardShowing(false); + mIconPolicy.setKeyguardShowing(false); } mNotificationPanel.setBarState(mState, mKeyguardFadingAway, goingToFullShade); updateDozingState(); @@ -3792,16 +3505,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateStackScrollerState(goingToFullShade); updateNotifications(); checkBarModes(); - updateCarrierLabelVisibility(false); updateMediaMetaData(false); mKeyguardMonitor.notifyKeyguardState(mStatusBarKeyguardViewManager.isShowing(), mStatusBarKeyguardViewManager.isSecure()); } private void updateDozingState() { - if (mState != StatusBarState.KEYGUARD && !mNotificationPanel.isDozing()) { - return; - } boolean animate = !mDozing && mDozeScrimController.isPulsing(); mNotificationPanel.setDozing(mDozing, animate); mStackScroller.setDark(mDozing, animate, mScreenOnTouchLocation); @@ -3878,6 +3587,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // Make our window larger and the panel expanded. makeExpandedVisible(true); mNotificationPanel.instantExpand(); + if (DEBUG_EMPTY_KEYGUARD) { + mStatusBarWindowManager.setLogState(true); + } } private void instantCollapseNotificationPanel() { @@ -3913,7 +3625,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } mState = state; + mGroupManager.setStatusBarState(state); mStatusBarWindowManager.setStatusBarState(state); + updateDozing(); } @Override @@ -3945,6 +3659,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mKeyguardIndicationController.showTransientIndication(R.string.camera_hint); } + public void onVoiceAssistHintStarted() { + mKeyguardIndicationController.showTransientIndication(R.string.voice_hint); + } + public void onPhoneHintStarted() { mKeyguardIndicationController.showTransientIndication(R.string.phone_hint); } @@ -4046,13 +3764,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } - /** - * @return a ViewGroup that spans the entire panel which contains the quick settings - */ - public ViewGroup getQuickSettingsOverlayParent() { - return mNotificationPanel; - } - public long getKeyguardFadingAwayDelay() { return mKeyguardFadingAwayDelay; } @@ -4061,18 +3772,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mKeyguardFadingAwayDuration; } - public LinearLayout getSystemIcons() { - return mSystemIcons; - } - - public LinearLayout getSystemIconArea() { - return mSystemIconArea; - } - @Override public void setBouncerShowing(boolean bouncerShowing) { super.setBouncerShowing(bouncerShowing); - disable(mDisabledUnmodified, true /* animate */); + mStatusBarView.setBouncerShowing(bouncerShowing); + disable(mDisabledUnmodified1, mDisabledUnmodified2, true /* animate */); } public void onScreenTurnedOff() { @@ -4116,7 +3820,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if ((time - mLastLockToAppLongPress) < LOCK_TO_APP_GESTURE_TOLERENCE) { activityManager.stopLockTaskModeOnCurrent(); // When exiting refresh disabled flags. - mNavigationBarView.setDisabledFlags(mDisabled, true); + mNavigationBarView.setDisabledFlags(mDisabled1, true); } else if ((v.getId() == R.id.back) && !mNavigationBarView.getRecentsButton().isPressed()) { // If we aren't pressing recents right now then they presses @@ -4133,7 +3837,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // should stop lock task. activityManager.stopLockTaskModeOnCurrent(); // When exiting refresh disabled flags. - mNavigationBarView.setDisabledFlags(mDisabled, true); + mNavigationBarView.setDisabledFlags(mDisabled1, true); } } if (sendBackLongPress) { @@ -4208,9 +3912,43 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mScreenOnComingFromTouch = true; mScreenOnTouchLocation = new PointF(event.getX(), event.getY()); mNotificationPanel.setTouchDisabled(false); + mStatusBarKeyguardViewManager.notifyScreenWakeUpRequested(); } } + @Override + public void appTransitionPending() { + + // Use own timings when Keyguard is going away, see keyguardGoingAway and + // setKeyguardFadingAway + if (!mKeyguardFadingAway) { + mIconController.appTransitionPending(); + } + } + + @Override + public void appTransitionCancelled() { + mIconController.appTransitionCancelled(); + } + + @Override + public void appTransitionStarting(long startTime, long duration) { + + // Use own timings when Keyguard is going away, see keyguardGoingAway and + // setKeyguardFadingAway + if (!mKeyguardFadingAway) { + mIconController.appTransitionStarting(startTime, duration); + } + if (mIconPolicy != null) { + mIconPolicy.appTransitionStarting(startTime, duration); + } + } + + private void updateDozing() { + mDozing = mDozingRequested && mState == StatusBarState.KEYGUARD; + updateDozingState(); + } + private final class ShadeUpdates { private final ArraySet<String> mVisibleNotifications = new ArraySet<String>(); private final ArraySet<String> mNewVisibleNotifications = new ArraySet<String>(); @@ -4316,10 +4054,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } private void handleStartDozing(@NonNull Runnable ready) { - if (!mDozing) { - mDozing = true; + if (!mDozingRequested) { + mDozingRequested = true; DozeLog.traceDozing(mContext, mDozing); - updateDozingState(); + updateDozing(); } ready.run(); } @@ -4329,10 +4067,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } private void handleStopDozing() { - if (mDozing) { - mDozing = false; + if (mDozingRequested) { + mDozingRequested = false; DozeLog.traceDozing(mContext, mDozing); - updateDozingState(); + updateDozing(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 5c254a26..59e1bba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -16,16 +16,23 @@ package com.android.systemui.statusbar.phone; +import android.app.ActivityManagerNative; import android.app.AlarmManager; +import android.app.AlarmManager.AlarmClockInfo; +import android.app.IUserSwitchObserver; import android.app.StatusBarManager; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.UserInfo; import android.media.AudioManager; import android.os.Handler; +import android.os.IRemoteCallback; +import android.os.RemoteException; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings.Global; import android.telecom.TelecomManager; import android.util.Log; @@ -33,6 +40,7 @@ import android.util.Log; import com.android.internal.telephony.IccCardConstants; import com.android.internal.telephony.TelephonyIntents; import com.android.systemui.R; +import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.CastController.CastDevice; import com.android.systemui.statusbar.policy.HotspotController; @@ -46,23 +54,21 @@ public class PhoneStatusBarPolicy { private static final String TAG = "PhoneStatusBarPolicy"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final boolean SHOW_SYNC_ICON = false; - - private static final String SLOT_SYNC_ACTIVE = "sync_active"; private static final String SLOT_CAST = "cast"; private static final String SLOT_HOTSPOT = "hotspot"; private static final String SLOT_BLUETOOTH = "bluetooth"; private static final String SLOT_TTY = "tty"; private static final String SLOT_ZEN = "zen"; private static final String SLOT_VOLUME = "volume"; - private static final String SLOT_CDMA_ERI = "cdma_eri"; private static final String SLOT_ALARM_CLOCK = "alarm_clock"; + private static final String SLOT_MANAGED_PROFILE = "managed_profile"; private final Context mContext; private final StatusBarManager mService; private final Handler mHandler = new Handler(); private final CastController mCast; private final HotspotController mHotspot; + private final AlarmManager mAlarmManager; // Assume it's all good unless we hear otherwise. We don't always seem // to get broadcasts that it *is* there. @@ -70,11 +76,16 @@ public class PhoneStatusBarPolicy { private boolean mZenVisible; private boolean mVolumeVisible; + private boolean mCurrentUserSetup; private int mZen; private boolean mBluetoothEnabled = false; + private boolean mManagedProfileFocused = false; + private boolean mManagedProfileIconVisible = true; + + private boolean mKeyguardVisible = true; private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override @@ -83,9 +94,6 @@ public class PhoneStatusBarPolicy { if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) { updateAlarm(); } - else if (action.equals(Intent.ACTION_SYNC_STATE_CHANGED)) { - updateSyncState(intent); - } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED) || action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) { updateBluetooth(); @@ -100,9 +108,6 @@ public class PhoneStatusBarPolicy { else if (action.equals(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED)) { updateTTY(intent); } - else if (action.equals(Intent.ACTION_USER_SWITCHED)) { - updateAlarm(); - } } }; @@ -110,29 +115,31 @@ public class PhoneStatusBarPolicy { mContext = context; mCast = cast; mHotspot = hotspot; - mService = (StatusBarManager)context.getSystemService(Context.STATUS_BAR_SERVICE); + mService = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE); + mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); // listen for broadcasts IntentFilter filter = new IntentFilter(); filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); - filter.addAction(Intent.ACTION_SYNC_STATE_CHANGED); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); filter.addAction(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED); - filter.addAction(Intent.ACTION_USER_SWITCHED); mContext.registerReceiver(mIntentReceiver, filter, null, mHandler); + // listen for user / profile change. + try { + ActivityManagerNative.getDefault().registerUserSwitchObserver(mUserSwitchListener); + } catch (RemoteException e) { + // Ignore + } + // TTY status mService.setIcon(SLOT_TTY, R.drawable.stat_sys_tty_mode, 0, null); mService.setIconVisibility(SLOT_TTY, false); - // Cdma Roaming Indicator, ERI - mService.setIcon(SLOT_CDMA_ERI, R.drawable.stat_sys_roaming_cdma_0, 0, null); - mService.setIconVisibility(SLOT_CDMA_ERI, false); - // bluetooth status updateBluetooth(); @@ -140,11 +147,6 @@ public class PhoneStatusBarPolicy { mService.setIcon(SLOT_ALARM_CLOCK, R.drawable.stat_sys_alarm, 0, null); mService.setIconVisibility(SLOT_ALARM_CLOCK, false); - // Sync state - mService.setIcon(SLOT_SYNC_ACTIVE, R.drawable.stat_sys_sync, 0, null); - mService.setIconVisibility(SLOT_SYNC_ACTIVE, false); - // "sync_failing" is obsolete: b/1297963 - // zen mService.setIcon(SLOT_ZEN, R.drawable.stat_sys_zen_important, 0, null); mService.setIconVisibility(SLOT_ZEN, false); @@ -160,9 +162,15 @@ public class PhoneStatusBarPolicy { mCast.addCallback(mCastCallback); // hotspot - mService.setIcon(SLOT_HOTSPOT, R.drawable.stat_sys_hotspot, 0, null); + mService.setIcon(SLOT_HOTSPOT, R.drawable.stat_sys_hotspot, 0, + mContext.getString(R.string.accessibility_status_bar_hotspot)); mService.setIconVisibility(SLOT_HOTSPOT, mHotspot.isHotspotEnabled()); mHotspot.addCallback(mHotspotCallback); + + // managed profile + mService.setIcon(SLOT_MANAGED_PROFILE, R.drawable.stat_sys_managed_profile_status, 0, + mContext.getString(R.string.accessibility_managed_profile)); + mService.setIconVisibility(SLOT_MANAGED_PROFILE, false); } public void setZenMode(int zen) { @@ -171,15 +179,12 @@ public class PhoneStatusBarPolicy { } private void updateAlarm() { - AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - boolean alarmSet = alarmManager.getNextAlarmClock(UserHandle.USER_CURRENT) != null; - mService.setIconVisibility(SLOT_ALARM_CLOCK, alarmSet); - } - - private final void updateSyncState(Intent intent) { - if (!SHOW_SYNC_ICON) return; - boolean isActive = intent.getBooleanExtra("active", false); - mService.setIconVisibility(SLOT_SYNC_ACTIVE, isActive); + final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT); + final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0; + final boolean zenNone = mZen == Global.ZEN_MODE_NO_INTERRUPTIONS; + mService.setIcon(SLOT_ALARM_CLOCK, zenNone ? R.drawable.stat_sys_alarm_dim + : R.drawable.stat_sys_alarm, 0, null); + mService.setIconVisibility(SLOT_ALARM_CLOCK, mCurrentUserSetup && hasAlarm); } private final void updateSimState(Intent intent) { @@ -221,17 +226,27 @@ public class PhoneStatusBarPolicy { int volumeIconId = 0; String volumeDescription = null; - if (mZen == Global.ZEN_MODE_NO_INTERRUPTIONS) { + if (DndTile.isVisible(mContext) || DndTile.isCombinedIcon(mContext)) { + zenVisible = mZen != Global.ZEN_MODE_OFF; + zenIconId = mZen == Global.ZEN_MODE_NO_INTERRUPTIONS + ? R.drawable.stat_sys_dnd_total_silence : R.drawable.stat_sys_dnd; + zenDescription = mContext.getString(R.string.quick_settings_dnd_label); + } else if (mZen == Global.ZEN_MODE_NO_INTERRUPTIONS) { zenVisible = true; zenIconId = R.drawable.stat_sys_zen_none; - zenDescription = mContext.getString(R.string.zen_no_interruptions); + zenDescription = mContext.getString(R.string.interruption_level_none); } else if (mZen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) { zenVisible = true; zenIconId = R.drawable.stat_sys_zen_important; - zenDescription = mContext.getString(R.string.zen_important_interruptions); + zenDescription = mContext.getString(R.string.interruption_level_priority); } - if (mZen != Global.ZEN_MODE_NO_INTERRUPTIONS && + if (DndTile.isVisible(mContext) && !DndTile.isCombinedIcon(mContext) + && audioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) { + volumeVisible = true; + volumeIconId = R.drawable.stat_sys_ringer_silent; + volumeDescription = mContext.getString(R.string.accessibility_ringer_silent); + } else if (mZen != Global.ZEN_MODE_NO_INTERRUPTIONS && mZen != Global.ZEN_MODE_ALARMS && audioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) { volumeVisible = true; volumeIconId = R.drawable.stat_sys_ringer_vibrate; @@ -253,6 +268,7 @@ public class PhoneStatusBarPolicy { mService.setIconVisibility(SLOT_VOLUME, volumeVisible); mVolumeVisible = volumeVisible; } + updateAlarm(); } private final void updateBluetooth() { @@ -311,6 +327,53 @@ public class PhoneStatusBarPolicy { mService.setIconVisibility(SLOT_CAST, isCasting); } + private void profileChanged(int userId) { + UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + UserInfo user = null; + if (userId == UserHandle.USER_CURRENT) { + try { + user = ActivityManagerNative.getDefault().getCurrentUser(); + } catch (RemoteException e) { + // Ignore + } + } else { + user = userManager.getUserInfo(userId); + } + + mManagedProfileFocused = user != null && user.isManagedProfile(); + if (DEBUG) Log.v(TAG, "profileChanged: mManagedProfileFocused: " + mManagedProfileFocused); + // Actually update the icon later when transition starts. + } + + private void updateManagedProfile() { + if (DEBUG) Log.v(TAG, "updateManagedProfile: mManagedProfileFocused: " + + mManagedProfileFocused + + " mKeyguardVisible: " + mKeyguardVisible); + boolean showIcon = mManagedProfileFocused && !mKeyguardVisible; + if (mManagedProfileIconVisible != showIcon) { + mService.setIconVisibility(SLOT_MANAGED_PROFILE, showIcon); + mManagedProfileIconVisible = showIcon; + } + } + + private final IUserSwitchObserver.Stub mUserSwitchListener = + new IUserSwitchObserver.Stub() { + @Override + public void onUserSwitching(int newUserId, IRemoteCallback reply) { + } + + @Override + public void onUserSwitchComplete(int newUserId) throws RemoteException { + updateAlarm(); + profileChanged(newUserId); + } + + @Override + public void onForegroundProfileSwitch(int newProfileId) { + profileChanged(newProfileId); + } + }; + private final HotspotController.Callback mHotspotCallback = new HotspotController.Callback() { @Override public void onHotspotChanged(boolean enabled) { @@ -324,4 +387,19 @@ public class PhoneStatusBarPolicy { updateCast(); } }; + + public void appTransitionStarting(long startTime, long duration) { + updateManagedProfile(); + } + + public void setKeyguardShowing(boolean visible) { + mKeyguardVisible = visible; + updateManagedProfile(); + } + + public void setCurrentUserSetup(boolean userSetup) { + if (mCurrentUserSetup == userSetup) return; + mCurrentUserSetup = userSetup; + updateAlarm(); + } } 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 7cbf13f..6a46924 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -20,12 +20,14 @@ import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.util.EventLog; +import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import com.android.systemui.EventLogTags; import com.android.systemui.R; +import com.android.systemui.statusbar.StatusBarState; public class PhoneStatusBarView extends PanelBar { private static final String TAG = "PhoneStatusBarView"; @@ -38,6 +40,14 @@ public class PhoneStatusBarView extends PanelBar { PanelView mNotificationPanel; private final PhoneStatusBarTransitions mBarTransitions; private ScrimController mScrimController; + private float mMinFraction; + private float mPanelFraction; + private Runnable mHideExpandedRunnable = new Runnable() { + @Override + public void run() { + mBar.makeExpandedInvisible(); + } + }; public PhoneStatusBarView(Context context, AttributeSet attrs) { super(context, attrs); @@ -77,8 +87,8 @@ public class PhoneStatusBarView extends PanelBar { } @Override - public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { - if (super.onRequestSendAccessibilityEvent(child, event)) { + public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) { + if (super.onRequestSendAccessibilityEventInternal(child, event)) { // The status bar is very small so augment the view that the user is touching // with the content of the status bar a whole. This way an accessibility service // may announce the current item as well as the entire content if appropriate. @@ -108,17 +118,20 @@ public class PhoneStatusBarView extends PanelBar { @Override public void onAllPanelsCollapsed() { super.onAllPanelsCollapsed(); - + if (PhoneStatusBar.DEBUG_EMPTY_KEYGUARD + && mBar.getBarState() == StatusBarState.KEYGUARD) { + Log.i(PhoneStatusBar.TAG, "Panel collapsed! Stacktrace: " + + Log.getStackTraceString(new Throwable())); + } // Close the status bar in the next frame so we can show the end of the animation. - postOnAnimation(new Runnable() { - @Override - public void run() { - mBar.makeExpandedInvisible(); - } - }); + postOnAnimation(mHideExpandedRunnable); mLastFullyOpenedPanel = null; } + public void removePendingHideExpandedRunnables() { + removeCallbacks(mHideExpandedRunnable); + } + @Override public void onPanelFullyOpened(PanelView openPanel) { super.onPanelFullyOpened(openPanel); @@ -174,9 +187,22 @@ public class PhoneStatusBarView extends PanelBar { } @Override + public void panelScrimMinFractionChanged(float minFraction) { + if (mMinFraction != minFraction) { + mMinFraction = minFraction; + updateScrimFraction(); + } + } + + @Override public void panelExpansionChanged(PanelView panel, float frac, boolean expanded) { super.panelExpansionChanged(panel, frac, expanded); - mScrimController.setPanelExpansion(frac); - mBar.updateCarrierLabelVisibility(false); + mPanelFraction = frac; + updateScrimFraction(); + } + + private void updateScrimFraction() { + float scrimFraction = Math.max(mPanelFraction - mMinFraction / (1.0f - mMinFraction), 0); + mScrimController.setPanelExpansion(scrimFraction); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java index 45a1386..12434ac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java @@ -19,13 +19,9 @@ package com.android.systemui.statusbar.phone; import android.content.Context; import android.content.Intent; import android.content.res.Resources; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Process; -import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; -import android.provider.Settings.Secure; +import android.os.Process; import android.util.Log; import com.android.systemui.R; @@ -35,24 +31,26 @@ import com.android.systemui.qs.tiles.BluetoothTile; import com.android.systemui.qs.tiles.CastTile; import com.android.systemui.qs.tiles.CellularTile; import com.android.systemui.qs.tiles.ColorInversionTile; +import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.qs.tiles.FlashlightTile; import com.android.systemui.qs.tiles.HotspotTile; import com.android.systemui.qs.tiles.IntentTile; import com.android.systemui.qs.tiles.LocationTile; import com.android.systemui.qs.tiles.RotationLockTile; import com.android.systemui.qs.tiles.WifiTile; -import com.android.systemui.settings.CurrentUserTracker; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.FlashlightController; +import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.LocationController; import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.RotationLockController; -import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.SecurityController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.statusbar.policy.ZenModeController; +import com.android.systemui.tuner.TunerService; +import com.android.systemui.tuner.TunerService.Tunable; import java.util.ArrayList; import java.util.Arrays; @@ -62,16 +60,16 @@ import java.util.List; import java.util.Map; /** Platform implementation of the quick settings tile host **/ -public class QSTileHost implements QSTile.Host { +public class QSTileHost implements QSTile.Host, Tunable { private static final String TAG = "QSTileHost"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final String TILES_SETTING = "sysui_qs_tiles"; + protected static final String TILES_SETTING = "sysui_qs_tiles"; private final Context mContext; private final PhoneStatusBar mStatusBar; private final LinkedHashMap<String, QSTile<?>> mTiles = new LinkedHashMap<>(); - private final Observer mObserver = new Observer(); + protected final ArrayList<String> mTileSpecs = new ArrayList<>(); private final BluetoothController mBluetooth; private final LocationController mLocation; private final RotationLockController mRotation; @@ -80,7 +78,6 @@ public class QSTileHost implements QSTile.Host { private final HotspotController mHotspot; private final CastController mCast; private final Looper mLooper; - private final CurrentUserTracker mUserTracker; private final FlashlightController mFlashlight; private final UserSwitcherController mUserSwitcherController; private final KeyguardMonitor mKeyguard; @@ -114,22 +111,11 @@ public class QSTileHost implements QSTile.Host { ht.start(); mLooper = ht.getLooper(); - mUserTracker = new CurrentUserTracker(mContext) { - @Override - public void onUserSwitched(int newUserId) { - recreateTiles(); - for (QSTile<?> tile : mTiles.values()) { - tile.userSwitch(newUserId); - } - mSecurity.onUserSwitched(newUserId); - mNetwork.onUserSwitched(newUserId); - mObserver.register(); - } - }; - recreateTiles(); + TunerService.get(mContext).addTunable(this, TILES_SETTING); + } - mUserTracker.startTracking(); - mObserver.register(); + public void destroy() { + TunerService.get(mContext).removeTunable(this); } @Override @@ -143,8 +129,8 @@ public class QSTileHost implements QSTile.Host { } @Override - public void startSettingsActivity(final Intent intent) { - mStatusBar.postStartSettingsActivity(intent, 0); + public void startActivityDismissingKeyguard(final Intent intent) { + mStatusBar.postStartActivityDismissingKeyguard(intent, 0); } @Override @@ -219,10 +205,15 @@ public class QSTileHost implements QSTile.Host { public SecurityController getSecurityController() { return mSecurity; } - - private void recreateTiles() { + + @Override + public void onTuningChanged(String key, String newValue) { + if (!TILES_SETTING.equals(key)) { + return; + } if (DEBUG) Log.d(TAG, "Recreating tiles"); - final List<String> tileSpecs = loadTileSpecs(); + final List<String> tileSpecs = loadTileSpecs(newValue); + if (tileSpecs.equals(mTileSpecs)) return; for (Map.Entry<String, QSTile<?>> tile : mTiles.entrySet()) { if (!tileSpecs.contains(tile.getKey())) { if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey()); @@ -242,7 +233,8 @@ public class QSTileHost implements QSTile.Host { } } } - if (mTiles.equals(newTiles)) return; + mTileSpecs.clear(); + mTileSpecs.addAll(tileSpecs); mTiles.clear(); mTiles.putAll(newTiles); if (mCallback != null) { @@ -250,12 +242,13 @@ public class QSTileHost implements QSTile.Host { } } - private QSTile<?> createTile(String tileSpec) { + protected QSTile<?> createTile(String tileSpec) { if (tileSpec.equals("wifi")) return new WifiTile(this); else if (tileSpec.equals("bt")) return new BluetoothTile(this); else if (tileSpec.equals("inversion")) return new ColorInversionTile(this); else if (tileSpec.equals("cell")) return new CellularTile(this); else if (tileSpec.equals("airplane")) return new AirplaneModeTile(this); + else if (tileSpec.equals("dnd")) return new DndTile(this); else if (tileSpec.equals("rotation")) return new RotationLockTile(this); else if (tileSpec.equals("flashlight")) return new FlashlightTile(this); else if (tileSpec.equals("location")) return new LocationTile(this); @@ -265,11 +258,9 @@ public class QSTileHost implements QSTile.Host { else throw new IllegalArgumentException("Bad tile spec: " + tileSpec); } - private List<String> loadTileSpecs() { + protected List<String> loadTileSpecs(String tileList) { final Resources res = mContext.getResources(); final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); - String tileList = Secure.getStringForUser(mContext.getContentResolver(), TILES_SETTING, - mUserTracker.getCurrentUserId()); if (tileList == null) { tileList = res.getString(R.string.quick_settings_tiles); if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList); @@ -292,26 +283,4 @@ public class QSTileHost implements QSTile.Host { } return tiles; } - - private class Observer extends ContentObserver { - private boolean mRegistered; - - public Observer() { - super(new Handler(Looper.getMainLooper())); - } - - public void register() { - if (mRegistered) { - mContext.getContentResolver().unregisterContentObserver(this); - } - mContext.getContentResolver().registerContentObserver(Secure.getUriFor(TILES_SETTING), - false, this, mUserTracker.getCurrentUserId()); - mRegistered = true; - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - recreateTiles(); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 0e8a794..bacf890 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Color; @@ -29,24 +30,32 @@ import android.view.animation.Interpolator; import com.android.systemui.R; import com.android.systemui.statusbar.BackDropView; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.ScrimView; +import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.stack.StackStateAnimator; /** * Controls both the scrim behind the notifications and in front of the notifications (when a * security method gets shown). */ -public class ScrimController implements ViewTreeObserver.OnPreDrawListener { +public class ScrimController implements ViewTreeObserver.OnPreDrawListener, + HeadsUpManager.OnHeadsUpChangedListener { public static final long ANIMATION_DURATION = 220; private static final float SCRIM_BEHIND_ALPHA = 0.62f; - private static final float SCRIM_BEHIND_ALPHA_KEYGUARD = 0.55f; + private static final float SCRIM_BEHIND_ALPHA_KEYGUARD = 0.45f; private static final float SCRIM_BEHIND_ALPHA_UNLOCKING = 0.2f; private static final float SCRIM_IN_FRONT_ALPHA = 0.75f; private static final int TAG_KEY_ANIM = R.id.scrim; + private static final int TAG_HUN_START_ALPHA = R.id.hun_scrim_alpha_start; + private static final int TAG_HUN_END_ALPHA = R.id.hun_scrim_alpha_end; private final ScrimView mScrimBehind; private final ScrimView mScrimInFront; private final UnlockMethodCache mUnlockMethodCache; + private final View mHeadsUpScrim; private boolean mKeyguardShowing; private float mFraction; @@ -70,15 +79,23 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { private float mDozeBehindAlpha; private float mCurrentInFrontAlpha; private float mCurrentBehindAlpha; - - public ScrimController(ScrimView scrimBehind, ScrimView scrimInFront, boolean scrimSrcEnabled) { + private float mCurrentHeadsUpAlpha = 1; + private int mPinnedHeadsUpCount; + private float mTopHeadsUpDragAmount; + private View mDraggedHeadsUpView; + private boolean mForceHideScrims; + + public ScrimController(ScrimView scrimBehind, ScrimView scrimInFront, View headsUpScrim, + boolean scrimSrcEnabled) { mScrimBehind = scrimBehind; mScrimInFront = scrimInFront; + mHeadsUpScrim = headsUpScrim; final Context context = scrimBehind.getContext(); mUnlockMethodCache = UnlockMethodCache.getInstance(context); mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); mScrimSrcEnabled = scrimSrcEnabled; + updateHeadsUpScrim(false); } public void setKeyguardShowing(boolean showing) { @@ -99,6 +116,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { if (mFraction != fraction) { mFraction = fraction; scheduleUpdate(); + if (mPinnedHeadsUpCount != 0) { + updateHeadsUpScrim(false); + } } } @@ -117,6 +137,12 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { scheduleUpdate(); } + public void abortKeyguardFadingOut() { + if (mAnimateKeyguardFadingOut) { + endAnimateKeyguardFadingOut(); + } + } + public void animateGoingToFullShade(long delay, long duration) { mDurationOverride = duration; mAnimationDelay = delay; @@ -157,7 +183,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { } private void updateScrims() { - if (mAnimateKeyguardFadingOut) { + if (mAnimateKeyguardFadingOut || mForceHideScrims) { setScrimInFrontColor(0f); setScrimBehindColor(0f); } else if (!mKeyguardShowing && !mBouncerShowing) { @@ -217,7 +243,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { } } - private void setScrimColor(ScrimView scrim, float alpha) { + private void setScrimColor(View scrim, float alpha) { Object runningAnim = scrim.getTag(TAG_KEY_ANIM); if (runningAnim instanceof ValueAnimator) { ((ValueAnimator) runningAnim).cancel(); @@ -236,25 +262,34 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { } private float getCurrentScrimAlpha(View scrim) { - return scrim == mScrimBehind ? mCurrentBehindAlpha : mCurrentInFrontAlpha; + return scrim == mScrimBehind ? mCurrentBehindAlpha + : scrim == mScrimInFront ? mCurrentInFrontAlpha + : mCurrentHeadsUpAlpha; } private void setCurrentScrimAlpha(View scrim, float alpha) { if (scrim == mScrimBehind) { mCurrentBehindAlpha = alpha; - } else { + } else if (scrim == mScrimInFront) { mCurrentInFrontAlpha = alpha; + } else { + alpha = Math.max(0.0f, Math.min(1.0f, alpha)); + mCurrentHeadsUpAlpha = alpha; } } - private void updateScrimColor(ScrimView scrim) { + private void updateScrimColor(View scrim) { float alpha1 = getCurrentScrimAlpha(scrim); - float alpha2 = getDozeAlpha(scrim); - float alpha = 1 - (1 - alpha1) * (1 - alpha2); - scrim.setScrimColor(Color.argb((int) (alpha * 255), 0, 0, 0)); + if (scrim instanceof ScrimView) { + float alpha2 = getDozeAlpha(scrim); + float alpha = 1 - (1 - alpha1) * (1 - alpha2); + ((ScrimView) scrim).setScrimColor(Color.argb((int) (alpha * 255), 0, 0, 0)); + } else { + scrim.setAlpha(alpha1); + } } - private void startScrimAnimation(final ScrimView scrim, float target) { + private void startScrimAnimation(final View scrim, float target) { float current = getCurrentScrimAlpha(scrim); ValueAnimator anim = ValueAnimator.ofFloat(current, target); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @@ -292,17 +327,21 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { mScrimBehind.getViewTreeObserver().removeOnPreDrawListener(this); mUpdatePending = false; updateScrims(); - mAnimateKeyguardFadingOut = false; mDurationOverride = -1; mAnimationDelay = 0; // Make sure that we always call the listener even if we didn't start an animation. + endAnimateKeyguardFadingOut(); + mAnimationStarted = false; + return true; + } + + private void endAnimateKeyguardFadingOut() { + mAnimateKeyguardFadingOut = false; if (!mAnimationStarted && mOnAnimationFinished != null) { mOnAnimationFinished.run(); mOnAnimationFinished = null; } - mAnimationStarted = false; - return true; } public void setBackDropView(BackDropView backDropView) { @@ -320,4 +359,102 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener { boolean asSrc = mBackDropView.getVisibility() != View.VISIBLE && mScrimSrcEnabled; mScrimBehind.setDrawAsSrc(asSrc); } + + @Override + public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) { + } + + @Override + public void onHeadsUpPinned(ExpandableNotificationRow headsUp) { + mPinnedHeadsUpCount++; + updateHeadsUpScrim(true); + } + + @Override + public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) { + mPinnedHeadsUpCount--; + if (headsUp == mDraggedHeadsUpView) { + mDraggedHeadsUpView = null; + mTopHeadsUpDragAmount = 0.0f; + } + updateHeadsUpScrim(true); + } + + @Override + public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) { + } + + private void updateHeadsUpScrim(boolean animate) { + float alpha = calculateHeadsUpAlpha(); + ValueAnimator previousAnimator = StackStateAnimator.getChildTag(mHeadsUpScrim, + TAG_KEY_ANIM); + float animEndValue = -1; + if (previousAnimator != null) { + if (animate || alpha == mCurrentHeadsUpAlpha) { + previousAnimator.cancel(); + } else { + animEndValue = StackStateAnimator.getChildTag(mHeadsUpScrim, TAG_HUN_END_ALPHA); + } + } + if (alpha != mCurrentHeadsUpAlpha && alpha != animEndValue) { + if (animate) { + startScrimAnimation(mHeadsUpScrim, alpha); + mHeadsUpScrim.setTag(TAG_HUN_START_ALPHA, mCurrentHeadsUpAlpha); + mHeadsUpScrim.setTag(TAG_HUN_END_ALPHA, alpha); + } else { + if (previousAnimator != null) { + float previousStartValue = StackStateAnimator.getChildTag(mHeadsUpScrim, + TAG_HUN_START_ALPHA); + float previousEndValue = StackStateAnimator.getChildTag(mHeadsUpScrim, + TAG_HUN_END_ALPHA); + // we need to increase all animation keyframes of the previous animator by the + // relative change to the end value + PropertyValuesHolder[] values = previousAnimator.getValues(); + float relativeDiff = alpha - previousEndValue; + float newStartValue = previousStartValue + relativeDiff; + values[0].setFloatValues(newStartValue, alpha); + mHeadsUpScrim.setTag(TAG_HUN_START_ALPHA, newStartValue); + mHeadsUpScrim.setTag(TAG_HUN_END_ALPHA, alpha); + previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); + } else { + // update the alpha directly + setCurrentScrimAlpha(mHeadsUpScrim, alpha); + updateScrimColor(mHeadsUpScrim); + } + } + } + } + + /** + * Set the amount the current top heads up view is dragged. The range is from 0 to 1 and 0 means + * the heads up is in its resting space and 1 means it's fully dragged out. + * + * @param draggedHeadsUpView the dragged view + * @param topHeadsUpDragAmount how far is it dragged + */ + public void setTopHeadsUpDragAmount(View draggedHeadsUpView, float topHeadsUpDragAmount) { + mTopHeadsUpDragAmount = topHeadsUpDragAmount; + mDraggedHeadsUpView = draggedHeadsUpView; + updateHeadsUpScrim(false); + } + + private float calculateHeadsUpAlpha() { + float alpha; + if (mPinnedHeadsUpCount >= 2) { + alpha = 1.0f; + } else if (mPinnedHeadsUpCount == 0) { + alpha = 0.0f; + } else { + alpha = 1.0f - mTopHeadsUpDragAmount; + } + float expandFactor = (1.0f - mFraction); + expandFactor = Math.max(expandFactor, 0.0f); + return alpha * expandFactor; + } + + public void forceHideScrims(boolean hide) { + mForceHideScrims = hide; + mAnimateChange = false; + scheduleUpdate(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SecureCameraLaunchManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SecureCameraLaunchManager.java index 4a43c47..45c8938 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SecureCameraLaunchManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SecureCameraLaunchManager.java @@ -27,6 +27,7 @@ import android.provider.MediaStore; import android.util.Log; import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardUpdateMonitor; import java.util.HashMap; import java.util.List; @@ -228,7 +229,7 @@ public class SecureCameraLaunchManager { // Get the list of applications that can handle the intent. final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser( - intent, PackageManager.MATCH_DEFAULT_ONLY, mLockPatternUtils.getCurrentUser()); + intent, PackageManager.MATCH_DEFAULT_ONLY, KeyguardUpdateMonitor.getCurrentUser()); if (appList.size() == 0) { if (DEBUG) Log.d(TAG, "No targets found for secure camera intent"); return false; @@ -237,7 +238,7 @@ public class SecureCameraLaunchManager { // Get the application that the intent resolves to. ResolveInfo resolved = packageManager.resolveActivityAsUser(intent, PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA, - mLockPatternUtils.getCurrentUser()); + KeyguardUpdateMonitor.getCurrentUser()); if (resolved == null || resolved.activityInfo == null) { return false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java index 181926c..a81f06e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java @@ -45,6 +45,7 @@ import com.android.systemui.R; import com.android.systemui.qs.QSPanel; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.NetworkControllerImpl.EmergencyListener; import com.android.systemui.statusbar.policy.NextAlarmController; import com.android.systemui.statusbar.policy.UserInfoController; @@ -54,7 +55,8 @@ import java.text.NumberFormat; * The view to manage the header area in the expanded status bar. */ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickListener, - BatteryController.BatteryStateChangeCallback, NextAlarmController.NextAlarmChangeCallback { + BatteryController.BatteryStateChangeCallback, NextAlarmController.NextAlarmChangeCallback, + EmergencyListener { private boolean mExpanded; private boolean mListening; @@ -110,7 +112,6 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL private NextAlarmController mNextAlarmController; private QSPanel mQSPanel; - private final Rect mClipBounds = new Rect(); private boolean mCaptureValues; @@ -121,6 +122,7 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL private float mCurrentT; private boolean mShowingDetail; + private boolean mDetailTransitioning; public StatusBarHeaderView(Context context, AttributeSet attrs) { super(context, attrs); @@ -527,7 +529,8 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL return true; } - public void setShowEmergencyCallsOnly(boolean show) { + @Override + public void setEmergencyCallsOnly(boolean show) { boolean changed = show != mShowEmergencyCallsOnly; if (changed) { mShowEmergencyCallsOnly = show; @@ -623,7 +626,7 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL mSettingsButton.setTranslationX(values.settingsTranslation); mSettingsButton.setRotation(values.settingsRotation); applyAlpha(mEmergencyCallsOnly, values.emergencyCallsOnlyAlpha); - if (!mShowingDetail) { + if (!mShowingDetail && !mDetailTransitioning) { // Otherwise it needs to stay invisible applyAlpha(mAlarmStatus, values.alarmStatusAlpha); } @@ -706,6 +709,7 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL @Override public void onShowingDetail(final QSTile.DetailAdapter detail) { + mDetailTransitioning = true; post(new Runnable() { @Override public void run() { @@ -788,6 +792,7 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL if (!in) { v.setVisibility(INVISIBLE); } + mDetailTransitioning = false; } }) .start(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java new file mode 100644 index 0000000..067e50e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.phone; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.ArraySet; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.statusbar.StatusBarIcon; +import com.android.internal.util.NotificationColorUtil; +import com.android.systemui.BatteryMeterView; +import com.android.systemui.FontSizeUtils; +import com.android.systemui.R; +import com.android.systemui.statusbar.NotificationData; +import com.android.systemui.statusbar.SignalClusterView; +import com.android.systemui.statusbar.StatusBarIconView; +import com.android.systemui.tuner.TunerService; +import com.android.systemui.tuner.TunerService.Tunable; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Controls everything regarding the icons in the status bar and on Keyguard, including, but not + * limited to: notification icons, signal cluster, additional status icons, and clock in the status + * bar. + */ +public class StatusBarIconController implements Tunable { + + public static final long DEFAULT_TINT_ANIMATION_DURATION = 120; + + public static final String ICON_BLACKLIST = "icon_blacklist"; + + private Context mContext; + private PhoneStatusBar mPhoneStatusBar; + private Interpolator mLinearOutSlowIn; + private Interpolator mFastOutSlowIn; + private DemoStatusIcons mDemoStatusIcons; + private NotificationColorUtil mNotificationColorUtil; + + private LinearLayout mSystemIconArea; + private LinearLayout mStatusIcons; + private SignalClusterView mSignalCluster; + private LinearLayout mStatusIconsKeyguard; + private IconMerger mNotificationIcons; + private View mNotificationIconArea; + private ImageView mMoreIcon; + private BatteryMeterView mBatteryMeterView; + private TextView mClock; + + private int mIconSize; + private int mIconHPadding; + + private int mIconTint = Color.WHITE; + private float mDarkIntensity; + + private boolean mTransitionPending; + private boolean mTintChangePending; + private float mPendingDarkIntensity; + private ValueAnimator mTintAnimator; + + private int mDarkModeIconColorSingleTone; + private int mLightModeIconColorSingleTone; + + private final Handler mHandler; + private boolean mTransitionDeferring; + private long mTransitionDeferringStartTime; + private long mTransitionDeferringDuration; + + private final ArraySet<String> mIconBlacklist = new ArraySet<>(); + + private final Runnable mTransitionDeferringDoneRunnable = new Runnable() { + @Override + public void run() { + mTransitionDeferring = false; + } + }; + + public StatusBarIconController(Context context, View statusBar, View keyguardStatusBar, + PhoneStatusBar phoneStatusBar) { + mContext = context; + mPhoneStatusBar = phoneStatusBar; + mNotificationColorUtil = NotificationColorUtil.getInstance(context); + mSystemIconArea = (LinearLayout) statusBar.findViewById(R.id.system_icon_area); + mStatusIcons = (LinearLayout) statusBar.findViewById(R.id.statusIcons); + mSignalCluster = (SignalClusterView) statusBar.findViewById(R.id.signal_cluster); + mNotificationIconArea = statusBar.findViewById(R.id.notification_icon_area_inner); + mNotificationIcons = (IconMerger) statusBar.findViewById(R.id.notificationIcons); + mMoreIcon = (ImageView) statusBar.findViewById(R.id.moreIcon); + mNotificationIcons.setOverflowIndicator(mMoreIcon); + mStatusIconsKeyguard = (LinearLayout) keyguardStatusBar.findViewById(R.id.statusIcons); + mBatteryMeterView = (BatteryMeterView) statusBar.findViewById(R.id.battery); + mClock = (TextView) statusBar.findViewById(R.id.clock); + mLinearOutSlowIn = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.linear_out_slow_in); + mFastOutSlowIn = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.fast_out_slow_in); + mDarkModeIconColorSingleTone = context.getColor(R.color.dark_mode_icon_color_single_tone); + mLightModeIconColorSingleTone = context.getColor(R.color.light_mode_icon_color_single_tone); + mHandler = new Handler(); + updateResources(); + + TunerService.get(mContext).addTunable(this, ICON_BLACKLIST); + } + + @Override + public void onTuningChanged(String key, String newValue) { + if (!ICON_BLACKLIST.equals(key)) { + return; + } + mIconBlacklist.clear(); + mIconBlacklist.addAll(getIconBlacklist(newValue)); + ArrayList<StatusBarIconView> views = new ArrayList<StatusBarIconView>(); + // Get all the current views. + for (int i = 0; i < mStatusIcons.getChildCount(); i++) { + views.add((StatusBarIconView) mStatusIcons.getChildAt(i)); + } + // Remove all the icons. + for (int i = views.size() - 1; i >= 0; i--) { + removeSystemIcon(views.get(i).getSlot(), i, i); + } + // Add them all back + for (int i = 0; i < views.size(); i++) { + addSystemIcon(views.get(i).getSlot(), i, i, views.get(i).getStatusBarIcon()); + } + }; + + public void updateResources() { + mIconSize = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_icon_size); + mIconHPadding = mContext.getResources().getDimensionPixelSize( + R.dimen.status_bar_icon_padding); + FontSizeUtils.updateFontSize(mClock, R.dimen.status_bar_clock_size); + } + + public void addSystemIcon(String slot, int index, int viewIndex, StatusBarIcon icon) { + boolean blocked = mIconBlacklist.contains(slot); + StatusBarIconView view = new StatusBarIconView(mContext, slot, null, blocked); + view.set(icon); + mStatusIcons.addView(view, viewIndex, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize)); + view = new StatusBarIconView(mContext, slot, null, blocked); + view.set(icon); + mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize)); + applyIconTint(); + } + + public void updateSystemIcon(String slot, int index, int viewIndex, + StatusBarIcon old, StatusBarIcon icon) { + StatusBarIconView view = (StatusBarIconView) mStatusIcons.getChildAt(viewIndex); + view.set(icon); + view = (StatusBarIconView) mStatusIconsKeyguard.getChildAt(viewIndex); + view.set(icon); + applyIconTint(); + } + + public void removeSystemIcon(String slot, int index, int viewIndex) { + mStatusIcons.removeViewAt(viewIndex); + mStatusIconsKeyguard.removeViewAt(viewIndex); + } + + public void updateNotificationIcons(NotificationData notificationData) { + final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + mIconSize + 2*mIconHPadding, mPhoneStatusBar.getStatusBarHeight()); + + ArrayList<NotificationData.Entry> activeNotifications = + notificationData.getActiveNotifications(); + final int N = activeNotifications.size(); + ArrayList<StatusBarIconView> toShow = new ArrayList<>(N); + + // Filter out ambient notifications and notification children. + for (int i = 0; i < N; i++) { + NotificationData.Entry ent = activeNotifications.get(i); + if (notificationData.isAmbient(ent.key) + && !NotificationData.showNotificationEvenIfUnprovisioned(ent.notification)) { + continue; + } + if (!PhoneStatusBar.isTopLevelChild(ent)) { + continue; + } + toShow.add(ent.icon); + } + + ArrayList<View> toRemove = new ArrayList<>(); + for (int i=0; i<mNotificationIcons.getChildCount(); i++) { + View child = mNotificationIcons.getChildAt(i); + if (!toShow.contains(child)) { + toRemove.add(child); + } + } + + final int toRemoveCount = toRemove.size(); + for (int i = 0; i < toRemoveCount; i++) { + mNotificationIcons.removeView(toRemove.get(i)); + } + + for (int i=0; i<toShow.size(); i++) { + View v = toShow.get(i); + if (v.getParent() == null) { + mNotificationIcons.addView(v, i, params); + } + } + + // Resort notification icons + final int childCount = mNotificationIcons.getChildCount(); + for (int i = 0; i < childCount; i++) { + View actual = mNotificationIcons.getChildAt(i); + StatusBarIconView expected = toShow.get(i); + if (actual == expected) { + continue; + } + mNotificationIcons.removeView(expected); + mNotificationIcons.addView(expected, i); + } + + applyNotificationIconsTint(); + } + + public void hideSystemIconArea(boolean animate) { + animateHide(mSystemIconArea, animate); + } + + public void showSystemIconArea(boolean animate) { + animateShow(mSystemIconArea, animate); + } + + public void hideNotificationIconArea(boolean animate) { + animateHide(mNotificationIconArea, animate); + } + + public void showNotificationIconArea(boolean animate) { + animateShow(mNotificationIconArea, animate); + } + + public void setClockVisibility(boolean visible) { + mClock.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + public void dump(PrintWriter pw) { + int N = mStatusIcons.getChildCount(); + pw.println(" system icons: " + N); + for (int i=0; i<N; i++) { + StatusBarIconView ic = (StatusBarIconView) mStatusIcons.getChildAt(i); + pw.println(" [" + i + "] icon=" + ic); + } + } + + public void dispatchDemoCommand(String command, Bundle args) { + if (mDemoStatusIcons == null) { + mDemoStatusIcons = new DemoStatusIcons(mStatusIcons, mIconSize); + } + mDemoStatusIcons.dispatchDemoCommand(command, args); + } + + /** + * Hides a view. + */ + private void animateHide(final View v, boolean animate) { + v.animate().cancel(); + if (!animate) { + v.setAlpha(0f); + v.setVisibility(View.INVISIBLE); + return; + } + v.animate() + .alpha(0f) + .setDuration(160) + .setStartDelay(0) + .setInterpolator(PhoneStatusBar.ALPHA_OUT) + .withEndAction(new Runnable() { + @Override + public void run() { + v.setVisibility(View.INVISIBLE); + } + }); + } + + /** + * Shows a view, and synchronizes the animation with Keyguard exit animations, if applicable. + */ + private void animateShow(View v, boolean animate) { + v.animate().cancel(); + v.setVisibility(View.VISIBLE); + if (!animate) { + v.setAlpha(1f); + return; + } + v.animate() + .alpha(1f) + .setDuration(320) + .setInterpolator(PhoneStatusBar.ALPHA_IN) + .setStartDelay(50) + + // We need to clean up any pending end action from animateHide if we call + // both hide and show in the same frame before the animation actually gets started. + // cancel() doesn't really remove the end action. + .withEndAction(null); + + // Synchronize the motion with the Keyguard fading if necessary. + if (mPhoneStatusBar.isKeyguardFadingAway()) { + v.animate() + .setDuration(mPhoneStatusBar.getKeyguardFadingAwayDuration()) + .setInterpolator(mLinearOutSlowIn) + .setStartDelay(mPhoneStatusBar.getKeyguardFadingAwayDelay()) + .start(); + } + } + + public void setIconsDark(boolean dark) { + if (mTransitionPending) { + deferIconTintChange(dark ? 1.0f : 0.0f); + } else if (mTransitionDeferring) { + animateIconTint(dark ? 1.0f : 0.0f, + Math.max(0, mTransitionDeferringStartTime - SystemClock.uptimeMillis()), + mTransitionDeferringDuration); + } else { + animateIconTint(dark ? 1.0f : 0.0f, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION); + } + } + + private void animateIconTint(float targetDarkIntensity, long delay, + long duration) { + if (mTintAnimator != null) { + mTintAnimator.cancel(); + } + if (mDarkIntensity == targetDarkIntensity) { + return; + } + mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity); + mTintAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setIconTintInternal((Float) animation.getAnimatedValue()); + } + }); + mTintAnimator.setDuration(duration); + mTintAnimator.setStartDelay(delay); + mTintAnimator.setInterpolator(mFastOutSlowIn); + mTintAnimator.start(); + } + + private void setIconTintInternal(float darkIntensity) { + mDarkIntensity = darkIntensity; + mIconTint = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, + mLightModeIconColorSingleTone, mDarkModeIconColorSingleTone); + applyIconTint(); + } + + private void deferIconTintChange(float darkIntensity) { + if (mTintChangePending && darkIntensity == mPendingDarkIntensity) { + return; + } + mTintChangePending = true; + mPendingDarkIntensity = darkIntensity; + } + + private void applyIconTint() { + for (int i = 0; i < mStatusIcons.getChildCount(); i++) { + StatusBarIconView v = (StatusBarIconView) mStatusIcons.getChildAt(i); + v.setImageTintList(ColorStateList.valueOf(mIconTint)); + } + mSignalCluster.setIconTint(mIconTint, mDarkIntensity); + mMoreIcon.setImageTintList(ColorStateList.valueOf(mIconTint)); + mBatteryMeterView.setDarkIntensity(mDarkIntensity); + mClock.setTextColor(mIconTint); + applyNotificationIconsTint(); + } + + private void applyNotificationIconsTint() { + for (int i = 0; i < mNotificationIcons.getChildCount(); i++) { + StatusBarIconView v = (StatusBarIconView) mNotificationIcons.getChildAt(i); + boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L)); + boolean colorize = !isPreL || isGrayscale(v); + if (colorize) { + v.setImageTintList(ColorStateList.valueOf(mIconTint)); + } + } + } + + private boolean isGrayscale(StatusBarIconView v) { + Object isGrayscale = v.getTag(R.id.icon_is_grayscale); + if (isGrayscale != null) { + return Boolean.TRUE.equals(isGrayscale); + } + boolean grayscale = mNotificationColorUtil.isGrayscaleIcon(v.getDrawable()); + v.setTag(R.id.icon_is_grayscale, grayscale); + return grayscale; + } + + public void appTransitionPending() { + mTransitionPending = true; + } + + public void appTransitionCancelled() { + if (mTransitionPending && mTintChangePending) { + mTintChangePending = false; + animateIconTint(mPendingDarkIntensity, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION); + } + mTransitionPending = false; + } + + public void appTransitionStarting(long startTime, long duration) { + if (mTransitionPending && mTintChangePending) { + mTintChangePending = false; + animateIconTint(mPendingDarkIntensity, + Math.max(0, startTime - SystemClock.uptimeMillis()), + duration); + + } else if (mTransitionPending) { + + // If we don't have a pending tint change yet, the change might come in the future until + // startTime is reached. + mTransitionDeferring = true; + mTransitionDeferringStartTime = startTime; + mTransitionDeferringDuration = duration; + mHandler.removeCallbacks(mTransitionDeferringDoneRunnable); + mHandler.postAtTime(mTransitionDeferringDoneRunnable, startTime); + } + mTransitionPending = false; + } + + public static ArraySet<String> getIconBlacklist(String blackListStr) { + ArraySet<String> ret = new ArraySet<String>(); + if (blackListStr != null) { + String[] blacklist = blackListStr.split(","); + for (String slot : blacklist) { + if (!TextUtils.isEmpty(slot)) { + ret.add(slot); + } + } + } + return ret; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 1724e70..a69416a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -31,6 +31,7 @@ import com.android.internal.policy.IKeyguardShowCallback; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.ViewMediatorCallback; +import com.android.systemui.statusbar.CommandQueue; import static com.android.keyguard.KeyguardHostView.OnDismissAction; @@ -72,6 +73,7 @@ public class StatusBarKeyguardViewManager { private boolean mLastBouncerShowing; private boolean mLastBouncerDismissible; private OnDismissAction mAfterKeyguardGoneAction; + private boolean mScreenWillWakeUp; public StatusBarKeyguardViewManager(Context context, ViewMediatorCallback callback, LockPatternUtils lockPatternUtils) { @@ -98,6 +100,7 @@ public class StatusBarKeyguardViewManager { public void show(Bundle options) { mShowing = true; mStatusBarWindowManager.setKeyguardShowing(true); + mScrimController.abortKeyguardFadingOut(); reset(); } @@ -125,10 +128,11 @@ public class StatusBarKeyguardViewManager { updateStates(); } - public void dismissWithAction(OnDismissAction r, boolean afterKeyguardGone) { + public void dismissWithAction(OnDismissAction r, Runnable cancelAction, + boolean afterKeyguardGone) { if (mShowing) { if (!afterKeyguardGone) { - mBouncer.showWithDismissAction(r); + mBouncer.showWithDismissAction(r, cancelAction); } else { mBouncer.show(false /* resetSecuritySelection */); mAfterKeyguardGoneAction = r; @@ -144,6 +148,7 @@ public class StatusBarKeyguardViewManager { if (mShowing) { if (mOccluded) { mPhoneStatusBar.hideKeyguard(); + mPhoneStatusBar.stopWaitingForKeyguardExit(); mBouncer.hide(false /* destroyView */); } else { showBouncerOrKeyguard(); @@ -160,6 +165,7 @@ public class StatusBarKeyguardViewManager { public void onScreenTurnedOn(final IKeyguardShowCallback callback) { mScreenOn = true; + mScreenWillWakeUp = false; mPhoneStatusBar.onScreenTurnedOn(); if (callback != null) { callbackAfterDraw(callback); @@ -179,6 +185,10 @@ public class StatusBarKeyguardViewManager { }); } + public void notifyScreenWakeUpRequested() { + mScreenWillWakeUp = !mScreenOn; + } + public void verifyUnlock() { dismiss(); } @@ -187,10 +197,6 @@ public class StatusBarKeyguardViewManager { mStatusBarWindowManager.setKeyguardNeedsInput(needsInput); } - public void updateUserActivityTimeout() { - mStatusBarWindowManager.setKeyguardUserActivityTimeout(mBouncer.getUserActivityTimeout()); - } - public void setOccluded(boolean occluded) { if (occluded && !mOccluded && mShowing) { if (mPhoneStatusBar.isInLaunchTransition()) { @@ -261,7 +267,7 @@ public class StatusBarKeyguardViewManager { } }); } else { - mPhoneStatusBar.setKeyguardFadingAway(delay, fadeoutDuration); + mPhoneStatusBar.setKeyguardFadingAway(startTime, delay, fadeoutDuration); boolean staying = mPhoneStatusBar.hideKeyguard(); if (!staying) { mStatusBarWindowManager.setKeyguardFadingAway(true); @@ -298,7 +304,7 @@ public class StatusBarKeyguardViewManager { * Dismisses the keyguard by going to the next screen or making it gone. */ public void dismiss() { - if (mScreenOn) { + if (mScreenOn || mScreenWillWakeUp) { showBouncer(); } } @@ -439,4 +445,21 @@ public class StatusBarKeyguardViewManager { public boolean isInputRestricted() { return mViewMediatorCallback.isInputRestricted(); } + + public void keyguardGoingAway() { + mPhoneStatusBar.keyguardGoingAway(); + } + + public void animateCollapsePanels(float speedUpFactor) { + mPhoneStatusBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */, + false /* delayed */, speedUpFactor); + } + + /** + * Notifies that the user has authenticated by other means than using the bouncer, for example, + * fingerprint. + */ + public void notifyKeyguardAuthenticated() { + mBouncer.notifyKeyguardAuthenticated(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java index 0dbdca1..0d816dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java @@ -21,15 +21,19 @@ import android.content.pm.ActivityInfo; import android.content.res.Resources; import android.graphics.PixelFormat; import android.os.SystemProperties; +import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import com.android.keyguard.R; +import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.statusbar.BaseStatusBar; import com.android.systemui.statusbar.StatusBarState; +import java.lang.reflect.Field; + /** * Encapsulates all logic for the status bar window state management. */ @@ -44,6 +48,7 @@ public class StatusBarWindowManager { private final boolean mKeyguardScreenRotation; private final State mCurrentState = new State(); + private boolean mLogState; public StatusBarWindowManager(Context context) { mContext = context; @@ -113,11 +118,11 @@ public class StatusBarWindowManager { } private void applyFocusableFlag(State state) { - if (state.isKeyguardShowingAndNotOccluded() && state.keyguardNeedsInput - && state.bouncerShowing) { + boolean panelFocusable = state.statusBarFocusable && state.panelExpanded; + if (state.keyguardShowing && state.keyguardNeedsInput && state.bouncerShowing) { mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - } else if (state.isKeyguardShowingAndNotOccluded() || state.statusBarFocusable) { + } else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) { mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; mLpChanged.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } else { @@ -127,8 +132,7 @@ public class StatusBarWindowManager { } private void applyHeight(State state) { - boolean expanded = state.isKeyguardShowingAndNotOccluded() || state.statusBarExpanded - || state.keyguardFadingAway || state.bouncerShowing; + boolean expanded = isExpanded(state); if (expanded) { mLpChanged.height = ViewGroup.LayoutParams.MATCH_PARENT; } else { @@ -136,6 +140,12 @@ public class StatusBarWindowManager { } } + private boolean isExpanded(State state) { + return !state.forceCollapsed && (state.isKeyguardShowingAndNotOccluded() + || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing + || state.headsUpShowing); + } + private void applyFitsSystemWindows(State state) { mStatusBarView.setFitsSystemWindows(!state.isKeyguardShowingAndNotOccluded()); } @@ -144,7 +154,7 @@ public class StatusBarWindowManager { if (state.isKeyguardShowingAndNotOccluded() && state.statusBarState == StatusBarState.KEYGUARD && !state.qsExpanded) { - mLpChanged.userActivityTimeout = state.keyguardUserActivityTimeout; + mLpChanged.userActivityTimeout = KeyguardViewMediator.AWAKE_INTERVAL_DEFAULT_MS; } else { mLpChanged.userActivityTimeout = -1; } @@ -164,17 +174,40 @@ public class StatusBarWindowManager { private void apply(State state) { applyKeyguardFlags(state); + applyForceStatusBarVisibleFlag(state); applyFocusableFlag(state); adjustScreenOrientation(state); applyHeight(state); applyUserActivityTimeout(state); applyInputFeatures(state); applyFitsSystemWindows(state); + applyModalFlag(state); if (mLp.copyFrom(mLpChanged) != 0) { + if (PhoneStatusBar.DEBUG_EMPTY_KEYGUARD && mLogState) { + logCurrentState(); + } mWindowManager.updateViewLayout(mStatusBarView, mLp); } } + private void applyForceStatusBarVisibleFlag(State state) { + if (state.forceStatusBarVisible) { + mLpChanged.privateFlags |= WindowManager + .LayoutParams.PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT; + } else { + mLpChanged.privateFlags &= ~WindowManager + .LayoutParams.PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT; + } + } + + private void applyModalFlag(State state) { + if (state.headsUpShowing) { + mLpChanged.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + } else { + mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + } + } + public void setKeyguardShowing(boolean showing) { mCurrentState.keyguardShowing = showing; apply(mCurrentState); @@ -190,9 +223,9 @@ public class StatusBarWindowManager { apply(mCurrentState); } - public void setStatusBarExpanded(boolean expanded) { - mCurrentState.statusBarExpanded = expanded; - mCurrentState.statusBarFocusable = expanded; + public void setPanelVisible(boolean visible) { + mCurrentState.panelVisible = visible; + mCurrentState.statusBarFocusable = visible; apply(mCurrentState); } @@ -201,11 +234,6 @@ public class StatusBarWindowManager { apply(mCurrentState); } - public void setKeyguardUserActivityTimeout(long timeout) { - mCurrentState.keyguardUserActivityTimeout = timeout; - apply(mCurrentState); - } - public void setBouncerShowing(boolean showing) { mCurrentState.bouncerShowing = showing; apply(mCurrentState); @@ -221,6 +249,11 @@ public class StatusBarWindowManager { apply(mCurrentState); } + public void setHeadsUpShowing(boolean showing) { + mCurrentState.headsUpShowing = showing; + apply(mCurrentState); + } + /** * @param state The {@link StatusBarState} of the status bar. */ @@ -229,16 +262,54 @@ public class StatusBarWindowManager { apply(mCurrentState); } + public void setForceStatusBarVisible(boolean forceStatusBarVisible) { + mCurrentState.forceStatusBarVisible = forceStatusBarVisible; + apply(mCurrentState); + } + + /** + * Force the window to be collapsed, even if it should theoretically be expanded. + * Used for when a heads-up comes in but we still need to wait for the touchable regions to + * be computed. + */ + public void setForceWindowCollapsed(boolean force) { + mCurrentState.forceCollapsed = force; + apply(mCurrentState); + } + + public void setPanelExpanded(boolean isExpanded) { + mCurrentState.panelExpanded = isExpanded; + apply(mCurrentState); + } + + public void setLogState(boolean logState) { + mLogState = logState; + if (logState) { + Log.w(PhoneStatusBar.TAG, "===== Started logging WM state changes ====="); + logCurrentState(); + } else { + Log.w(PhoneStatusBar.TAG, "===== Finished logging WM state changes ====="); + } + } + + private void logCurrentState() { + Log.i(PhoneStatusBar.TAG, mCurrentState.toString() + + "\n Expanded: " + isExpanded(mCurrentState)); + } + private static class State { boolean keyguardShowing; boolean keyguardOccluded; boolean keyguardNeedsInput; - boolean statusBarExpanded; + boolean panelVisible; + boolean panelExpanded; boolean statusBarFocusable; - long keyguardUserActivityTimeout; boolean bouncerShowing; boolean keyguardFadingAway; boolean qsExpanded; + boolean headsUpShowing; + boolean forceStatusBarVisible; + boolean forceCollapsed; /** * The {@link BaseStatusBar} state from the status bar. @@ -248,5 +319,31 @@ public class StatusBarWindowManager { private boolean isKeyguardShowingAndNotOccluded() { return keyguardShowing && !keyguardOccluded; } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + String newLine = "\n"; + result.append("Window State {"); + result.append(newLine); + + Field[] fields = this.getClass().getDeclaredFields(); + + // Print field names paired with their values + for (Field field : fields) { + result.append(" "); + try { + result.append(field.getName()); + result.append(": "); + //requires access to private field: + result.append(field.get(this)); + } catch (IllegalAccessException ex) { + } + result.append(newLine); + } + result.append("}"); + + return result.toString(); + } } } 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 a96f4e9..0e22aa8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone; import android.app.StatusBarManager; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PorterDuff; @@ -50,7 +51,9 @@ public class StatusBarWindowView extends FrameLayout { private NotificationPanelView mNotificationPanel; private View mBrightnessMirror; - PhoneStatusBar mService; + private int mRightInset = 0; + + private PhoneStatusBar mService; private final Paint mTransparentSrcPaint = new Paint(); public StatusBarWindowView(Context context, AttributeSet attrs) { @@ -63,17 +66,27 @@ public class StatusBarWindowView extends FrameLayout { @Override protected boolean fitSystemWindows(Rect insets) { if (getFitsSystemWindows()) { - boolean changed = insets.left != getPaddingLeft() + boolean paddingChanged = insets.left != getPaddingLeft() || insets.top != getPaddingTop() - || insets.right != getPaddingRight() || insets.bottom != getPaddingBottom(); - if (changed) { - setPadding(insets.left, insets.top, insets.right, 0); + + // Super-special right inset handling, because scrims and backdrop need to ignore it. + if (insets.right != mRightInset) { + mRightInset = insets.right; + applyMargins(); + } + // Drop top inset, apply left inset and pass through bottom inset. + if (paddingChanged) { + setPadding(insets.left, 0, 0, 0); } insets.left = 0; insets.top = 0; insets.right = 0; } else { + if (mRightInset != 0) { + mRightInset = 0; + applyMargins(); + } boolean changed = getPaddingLeft() != 0 || getPaddingRight() != 0 || getPaddingTop() != 0 @@ -81,19 +94,52 @@ public class StatusBarWindowView extends FrameLayout { if (changed) { setPadding(0, 0, 0, 0); } + insets.top = 0; } return false; } + private void applyMargins() { + final int N = getChildCount(); + for (int i = 0; i < N; i++) { + View child = getChildAt(i); + if (child.getLayoutParams() instanceof LayoutParams) { + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.ignoreRightInset && lp.rightMargin != mRightInset) { + lp.rightMargin = mRightInset; + child.requestLayout(); + } + } + } + } + @Override - protected void onAttachedToWindow () { - super.onAttachedToWindow(); + public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + @Override + protected FrameLayout.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); mStackScrollLayout = (NotificationStackScrollLayout) findViewById( R.id.notification_stack_scroller); mNotificationPanel = (NotificationPanelView) findViewById(R.id.notification_panel); - mDragDownHelper = new DragDownHelper(getContext(), this, mStackScrollLayout, mService); mBrightnessMirror = findViewById(R.id.brightness_mirror); + } + + public void setService(PhoneStatusBar service) { + mService = service; + mDragDownHelper = new DragDownHelper(getContext(), this, mStackScrollLayout, mService); + } + + @Override + protected void onAttachedToWindow () { + super.onAttachedToWindow(); // We really need to be able to animate while window animations are going on // so that activities may be started asynchronously from panel animations @@ -168,7 +214,6 @@ public class StatusBarWindowView extends FrameLayout { if (mNotificationPanel.isFullyExpanded() && mStackScrollLayout.getVisibility() == View.VISIBLE && mService.getBarState() == StatusBarState.KEYGUARD - && !mService.isQsExpanded() && !mService.isBouncerShowing()) { intercept = mDragDownHelper.onInterceptTouchEvent(ev); // wake up on a touch down event, if dozing @@ -192,7 +237,7 @@ public class StatusBarWindowView extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent ev) { boolean handled = false; - if (mService.getBarState() == StatusBarState.KEYGUARD && !mService.isQsExpanded()) { + if (mService.getBarState() == StatusBarState.KEYGUARD) { handled = mDragDownHelper.onTouchEvent(ev); } if (!handled) { @@ -242,5 +287,23 @@ public class StatusBarWindowView extends FrameLayout { mStackScrollLayout.cancelExpandHelper(); } } + + public class LayoutParams extends FrameLayout.LayoutParams { + + public boolean ignoreRightInset; + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout); + ignoreRightInset = a.getBoolean( + R.styleable.StatusBarWindowView_Layout_ignoreRightInset, false); + a.recycle(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/Ticker.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/Ticker.java deleted file mode 100644 index a6ce288..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/Ticker.java +++ /dev/null @@ -1,305 +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.phone; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.service.notification.StatusBarNotification; -import android.text.Layout.Alignment; -import android.text.StaticLayout; -import android.text.TextPaint; -import android.view.View; -import android.view.animation.AnimationUtils; -import android.widget.ImageSwitcher; -import android.widget.TextSwitcher; -import android.widget.TextView; - -import com.android.internal.statusbar.StatusBarIcon; -import com.android.systemui.R; -import com.android.systemui.statusbar.StatusBarIconView; - -import java.util.ArrayList; - -public abstract class Ticker { - private static final int TICKER_SEGMENT_DELAY = 3000; - - private Context mContext; - private Handler mHandler = new Handler(); - private ArrayList<Segment> mSegments = new ArrayList(); - private TextPaint mPaint; - private View mTickerView; - private ImageSwitcher mIconSwitcher; - private TextSwitcher mTextSwitcher; - private float mIconScale; - - public static boolean isGraphicOrEmoji(char c) { - int gc = Character.getType(c); - return gc != Character.CONTROL - && gc != Character.FORMAT - && gc != Character.UNASSIGNED - && gc != Character.LINE_SEPARATOR - && gc != Character.PARAGRAPH_SEPARATOR - && gc != Character.SPACE_SEPARATOR; - } - - private final class Segment { - StatusBarNotification notification; - Drawable icon; - CharSequence text; - int current; - int next; - boolean first; - - StaticLayout getLayout(CharSequence substr) { - int w = mTextSwitcher.getWidth() - mTextSwitcher.getPaddingLeft() - - mTextSwitcher.getPaddingRight(); - return new StaticLayout(substr, mPaint, w, Alignment.ALIGN_NORMAL, 1, 0, true); - } - - CharSequence rtrim(CharSequence substr, int start, int end) { - while (end > start && !isGraphicOrEmoji(substr.charAt(end-1))) { - end--; - } - if (end > start) { - return substr.subSequence(start, end); - } - return null; - } - - /** returns null if there is no more text */ - CharSequence getText() { - if (this.current > this.text.length()) { - return null; - } - CharSequence substr = this.text.subSequence(this.current, this.text.length()); - StaticLayout l = getLayout(substr); - int lineCount = l.getLineCount(); - if (lineCount > 0) { - int start = l.getLineStart(0); - int end = l.getLineEnd(0); - this.next = this.current + end; - return rtrim(substr, start, end); - } else { - throw new RuntimeException("lineCount=" + lineCount + " current=" + current + - " text=" + text); - } - } - - /** returns null if there is no more text */ - CharSequence advance() { - this.first = false; - int index = this.next; - final int len = this.text.length(); - while (index < len && !isGraphicOrEmoji(this.text.charAt(index))) { - index++; - } - if (index >= len) { - return null; - } - - CharSequence substr = this.text.subSequence(index, this.text.length()); - StaticLayout l = getLayout(substr); - final int lineCount = l.getLineCount(); - int i; - for (i=0; i<lineCount; i++) { - int start = l.getLineStart(i); - int end = l.getLineEnd(i); - if (i == lineCount-1) { - this.next = len; - } else { - this.next = index + l.getLineStart(i+1); - } - CharSequence result = rtrim(substr, start, end); - if (result != null) { - this.current = index + start; - return result; - } - } - this.current = len; - return null; - } - - Segment(StatusBarNotification n, Drawable icon, CharSequence text) { - this.notification = n; - this.icon = icon; - this.text = text; - int index = 0; - final int len = text.length(); - while (index < len && !isGraphicOrEmoji(text.charAt(index))) { - index++; - } - this.current = index; - this.next = index; - this.first = true; - } - }; - - public Ticker(Context context, View sb) { - mContext = context; - final Resources res = context.getResources(); - final int outerBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_size); - final int imageBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size); - mIconScale = (float)imageBounds / (float)outerBounds; - - mTickerView = sb.findViewById(R.id.ticker); - - mIconSwitcher = (ImageSwitcher)sb.findViewById(R.id.tickerIcon); - mIconSwitcher.setInAnimation( - AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in)); - mIconSwitcher.setOutAnimation( - AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out)); - mIconSwitcher.setScaleX(mIconScale); - mIconSwitcher.setScaleY(mIconScale); - - mTextSwitcher = (TextSwitcher)sb.findViewById(R.id.tickerText); - mTextSwitcher.setInAnimation( - AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in)); - mTextSwitcher.setOutAnimation( - AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out)); - - // Copy the paint style of one of the TextSwitchers children to use later for measuring - TextView text = (TextView)mTextSwitcher.getChildAt(0); - mPaint = text.getPaint(); - } - - - public void addEntry(StatusBarNotification n) { - int initialCount = mSegments.size(); - - // If what's being displayed has the same text and icon, just drop it - // (which will let the current one finish, this happens when apps do - // a notification storm). - if (initialCount > 0) { - final Segment seg = mSegments.get(0); - if (n.getPackageName().equals(seg.notification.getPackageName()) - && n.getNotification().icon == seg.notification.getNotification().icon - && n.getNotification().iconLevel == seg.notification.getNotification().iconLevel - && charSequencesEqual(seg.notification.getNotification().tickerText, - n.getNotification().tickerText)) { - return; - } - } - - final Drawable icon = StatusBarIconView.getIcon(mContext, - new StatusBarIcon(n.getPackageName(), n.getUser(), n.getNotification().icon, n.getNotification().iconLevel, 0, - n.getNotification().tickerText)); - final CharSequence text = n.getNotification().tickerText; - final Segment newSegment = new Segment(n, icon, text); - - // If there's already a notification schedule for this package and id, remove it. - for (int i=0; i<mSegments.size(); i++) { - Segment seg = mSegments.get(i); - if (n.getId() == seg.notification.getId() && n.getPackageName().equals(seg.notification.getPackageName())) { - // just update that one to use this new data instead - mSegments.remove(i--); // restart iteration here - } - } - - mSegments.add(newSegment); - - if (initialCount == 0 && mSegments.size() > 0) { - Segment seg = mSegments.get(0); - seg.first = false; - - mIconSwitcher.setAnimateFirstView(false); - mIconSwitcher.reset(); - mIconSwitcher.setImageDrawable(seg.icon); - - mTextSwitcher.setAnimateFirstView(false); - mTextSwitcher.reset(); - mTextSwitcher.setText(seg.getText()); - - tickerStarting(); - scheduleAdvance(); - } - } - - private static boolean charSequencesEqual(CharSequence a, CharSequence b) { - if (a.length() != b.length()) { - return false; - } - - int length = a.length(); - for (int i = 0; i < length; i++) { - if (a.charAt(i) != b.charAt(i)) { - return false; - } - } - return true; - } - - public void removeEntry(StatusBarNotification n) { - for (int i=mSegments.size()-1; i>=0; i--) { - Segment seg = mSegments.get(i); - if (n.getId() == seg.notification.getId() && n.getPackageName().equals(seg.notification.getPackageName())) { - mSegments.remove(i); - } - } - } - - public void halt() { - mHandler.removeCallbacks(mAdvanceTicker); - mSegments.clear(); - tickerHalting(); - } - - public void reflowText() { - if (mSegments.size() > 0) { - Segment seg = mSegments.get(0); - CharSequence text = seg.getText(); - mTextSwitcher.setCurrentText(text); - } - } - - private Runnable mAdvanceTicker = new Runnable() { - public void run() { - while (mSegments.size() > 0) { - Segment seg = mSegments.get(0); - - if (seg.first) { - // this makes the icon slide in for the first one for a given - // notification even if there are two notifications with the - // same icon in a row - mIconSwitcher.setImageDrawable(seg.icon); - } - CharSequence text = seg.advance(); - if (text == null) { - mSegments.remove(0); - continue; - } - mTextSwitcher.setText(text); - - scheduleAdvance(); - break; - } - if (mSegments.size() == 0) { - tickerDone(); - } - } - }; - - private void scheduleAdvance() { - mHandler.postDelayed(mAdvanceTicker, TICKER_SEGMENT_DELAY); - } - - public abstract void tickerStarting(); - public abstract void tickerDone(); - public abstract void tickerHalting(); -} - diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TrustDrawable.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/TrustDrawable.java index b89aa8f..56c1e10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TrustDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/TrustDrawable.java @@ -120,7 +120,7 @@ public class TrustDrawable extends Drawable { } @Override - public void setColorFilter(ColorFilter cf) { + public void setColorFilter(ColorFilter colorFilter) { throw new UnsupportedOperationException("not implemented"); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockMethodCache.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockMethodCache.java index 5ef345b..66d71f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockMethodCache.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockMethodCache.java @@ -80,8 +80,8 @@ public class UnlockMethodCache { } private void update(boolean updateAlways) { - int user = mLockPatternUtils.getCurrentUser(); - boolean secure = mLockPatternUtils.isSecure(); + int user = KeyguardUpdateMonitor.getCurrentUser(); + boolean secure = mLockPatternUtils.isSecure(user); boolean currentlyInsecure = !secure || mKeyguardUpdateMonitor.getUserHasTrust(user); boolean trustManaged = mKeyguardUpdateMonitor.getUserTrustIsManaged(user); boolean faceUnlockRunning = mKeyguardUpdateMonitor.isFaceUnlockRunning(user) @@ -125,7 +125,7 @@ public class UnlockMethodCache { } @Override - public void onFingerprintRecognized(int userId) { + public void onFingerprintAuthenticated(int userId) { update(false /* updateAlways */); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java index 4f43b4d..e153b85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java @@ -20,8 +20,6 @@ import android.content.Context; import com.android.systemui.R; -import static android.util.Pools.SynchronizedPool; - /** * A class to generate {@link VelocityTrackerInterface}, depending on the configuration. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java index ad4c211..d1e4963 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java @@ -17,35 +17,26 @@ package com.android.systemui.statusbar.policy; import android.app.ActivityManager; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; -import android.net.wifi.ScanResult; -import android.net.wifi.WifiConfiguration; -import android.net.wifi.WifiConfiguration.KeyMgmt; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.ActionListener; +import android.os.Looper; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import android.text.TextUtils; -import android.util.ArrayMap; -import android.util.ArraySet; import android.util.Log; +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTracker.WifiListener; import com.android.systemui.R; +import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; - -// TODO: Unify this logic with platform settings (see WifiSettings and AccessPoint). There is a -// fair amount of complexity here in statuses and logic beyond just connected/disconnected. -public class AccessPointControllerImpl implements NetworkController.AccessPointController { +public class AccessPointControllerImpl + implements NetworkController.AccessPointController, WifiListener { private static final String TAG = "AccessPointController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -54,7 +45,7 @@ public class AccessPointControllerImpl implements NetworkController.AccessPointC private static final String EXTRA_START_CONNECT_SSID = "wifi_start_connect_ssid"; private static final int[] ICONS = { - R.drawable.ic_qs_wifi_0, + R.drawable.ic_qs_wifi_full_0, R.drawable.ic_qs_wifi_full_1, R.drawable.ic_qs_wifi_full_2, R.drawable.ic_qs_wifi_full_3, @@ -63,25 +54,18 @@ public class AccessPointControllerImpl implements NetworkController.AccessPointC private final Context mContext; private final ArrayList<AccessPointCallback> mCallbacks = new ArrayList<AccessPointCallback>(); - private final WifiManager mWifiManager; + private final WifiTracker mWifiTracker; private final UserManager mUserManager; - private final Receiver mReceiver = new Receiver(); - private NetworkControllerImpl mNetworkController; - private boolean mScanning; private int mCurrentUser; - public AccessPointControllerImpl(Context context) { + public AccessPointControllerImpl(Context context, Looper bgLooper) { mContext = context; - mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mWifiTracker = new WifiTracker(context, this, bgLooper, false, true); mCurrentUser = ActivityManager.getCurrentUser(); } - void setNetworkController(NetworkControllerImpl networkController) { - mNetworkController = networkController; - } - public boolean canConfigWifi() { return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, new UserHandle(mCurrentUser)); @@ -96,7 +80,9 @@ public class AccessPointControllerImpl implements NetworkController.AccessPointC if (callback == null || mCallbacks.contains(callback)) return; if (DEBUG) Log.d(TAG, "addCallback " + callback); mCallbacks.add(callback); - mReceiver.setListening(!mCallbacks.isEmpty()); + if (mCallbacks.size() == 1) { + mWifiTracker.startTracking(); + } } @Override @@ -104,37 +90,40 @@ public class AccessPointControllerImpl implements NetworkController.AccessPointC if (callback == null) return; if (DEBUG) Log.d(TAG, "removeCallback " + callback); mCallbacks.remove(callback); - mReceiver.setListening(!mCallbacks.isEmpty()); + if (mCallbacks.isEmpty()) { + mWifiTracker.stopTracking(); + } } @Override public void scanForAccessPoints() { - if (mScanning) return; if (DEBUG) Log.d(TAG, "scan!"); - mScanning = mWifiManager.startScan(); - // Grab current networks immediately while we wait for scan. - updateAccessPoints(); + mWifiTracker.forceScan(); + } + + @Override + public int getIcon(AccessPoint ap) { + int level = ap.getLevel(); + return ICONS[level >= 0 ? level : 0]; } public boolean connect(AccessPoint ap) { if (ap == null) return false; - if (DEBUG) Log.d(TAG, "connect networkId=" + ap.networkId); - if (ap.networkId < 0) { + if (DEBUG) Log.d(TAG, "connect networkId=" + ap.getConfig().networkId); + if (ap.isSaved()) { + mWifiTracker.getManager().connect(ap.getConfig().networkId, mConnectListener); + } else { // Unknown network, need to add it. - if (ap.hasSecurity) { + if (ap.getSecurity() != AccessPoint.SECURITY_NONE) { Intent intent = new Intent(Settings.ACTION_WIFI_SETTINGS); - intent.putExtra(EXTRA_START_CONNECT_SSID, ap.ssid); + intent.putExtra(EXTRA_START_CONNECT_SSID, ap.getSsidStr()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); fireSettingsIntentCallback(intent); return true; } else { - WifiConfiguration config = new WifiConfiguration(); - config.SSID = "\"" + ap.ssid + "\""; - config.allowedKeyManagement.set(KeyMgmt.NONE); - mWifiManager.connect(config, mConnectListener); + ap.generateOpenNetworkConfig(); + mWifiTracker.getManager().connect(ap.getConfig(), mConnectListener); } - } else { - mWifiManager.connect(ap.networkId, mConnectListener); } return false; } @@ -145,76 +134,28 @@ public class AccessPointControllerImpl implements NetworkController.AccessPointC } } - private void fireAcccessPointsCallback(AccessPoint[] aps) { + private void fireAcccessPointsCallback(List<AccessPoint> aps) { for (AccessPointCallback callback : mCallbacks) { callback.onAccessPointsChanged(aps); } } - private static String trimDoubleQuotes(String v) { - return v != null && v.length() >= 2 && v.charAt(0) == '\"' - && v.charAt(v.length() - 1) == '\"' ? v.substring(1, v.length() - 1) : v; + public void dump(PrintWriter pw) { + mWifiTracker.dump(pw); } - private int getConnectedNetworkId(WifiInfo wifiInfo) { - return wifiInfo != null ? wifiInfo.getNetworkId() : AccessPoint.NO_NETWORK; + @Override + public void onWifiStateChanged(int state) { } - private ArrayMap<String, WifiConfiguration> getConfiguredNetworksBySsid() { - final List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks(); - if (configs == null || configs.size() == 0) return ArrayMap.EMPTY; - final ArrayMap<String, WifiConfiguration> rt = new ArrayMap<String, WifiConfiguration>(); - for (WifiConfiguration config : configs) { - rt.put(trimDoubleQuotes(config.SSID), config); - } - return rt; + @Override + public void onConnectedChanged() { + fireAcccessPointsCallback(mWifiTracker.getAccessPoints()); } - private void updateAccessPoints() { - final WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); - final int connectedNetworkId = getConnectedNetworkId(wifiInfo); - if (DEBUG) Log.d(TAG, "connectedNetworkId: " + connectedNetworkId); - final List<ScanResult> scanResults = mWifiManager.getScanResults(); - final ArrayMap<String, WifiConfiguration> configured = getConfiguredNetworksBySsid(); - if (DEBUG) Log.d(TAG, "scanResults: " + scanResults); - final List<AccessPoint> aps = new ArrayList<AccessPoint>(scanResults.size()); - final ArraySet<String> ssids = new ArraySet<String>(); - for (ScanResult scanResult : scanResults) { - if (scanResult == null) { - continue; - } - final String ssid = scanResult.SSID; - if (TextUtils.isEmpty(ssid) || ssids.contains(ssid)) continue; - ssids.add(ssid); - final WifiConfiguration config = configured.get(ssid); - final int level = WifiManager.calculateSignalLevel(scanResult.level, ICONS.length); - final AccessPoint ap = new AccessPoint(); - ap.isConfigured = config != null; - ap.networkId = config != null ? config.networkId : AccessPoint.NO_NETWORK; - ap.ssid = ssid; - // Connected if either: - // -The network ID in the active WifiInfo matches this network's ID. - // -The network is ephemeral (no configuration) but the SSID matches. - ap.isConnected = (ap.networkId != AccessPoint.NO_NETWORK - && ap.networkId == connectedNetworkId) || - (ap.networkId == WifiConfiguration.INVALID_NETWORK_ID && wifiInfo != null && - ap.ssid.equals(trimDoubleQuotes(wifiInfo.getSSID()))); - if (ap.isConnected && mNetworkController != null) { - // Ensure we have the connected network's RSSI. - ap.level = mNetworkController.getConnectedWifiLevel(); - } else { - ap.level = level; - } - ap.iconId = ICONS[ap.level]; - // Based on Settings AccessPoint#getSecurity, keep up to date - // with better methods of determining no security or not. - ap.hasSecurity = scanResult.capabilities.contains("WEP") - || scanResult.capabilities.contains("PSK") - || scanResult.capabilities.contains("EAP"); - aps.add(ap); - } - Collections.sort(aps, mByStrength); - fireAcccessPointsCallback(aps.toArray(new AccessPoint[aps.size()])); + @Override + public void onAccessPointsChanged() { + fireAcccessPointsCallback(mWifiTracker.getAccessPoints()); } private final ActionListener mConnectListener = new ActionListener() { @@ -228,49 +169,4 @@ public class AccessPointControllerImpl implements NetworkController.AccessPointC if (DEBUG) Log.d(TAG, "connect failure reason=" + reason); } }; - - private final Comparator<AccessPoint> mByStrength = new Comparator<AccessPoint> () { - @Override - public int compare(AccessPoint lhs, AccessPoint rhs) { - return -Integer.compare(score(lhs), score(rhs)); - } - - private int score(AccessPoint ap) { - return ap.level + (ap.isConnected ? 20 : 0) + (ap.isConfigured ? 10 : 0); - } - }; - - private final class Receiver extends BroadcastReceiver { - private boolean mRegistered; - - public void setListening(boolean listening) { - if (listening && !mRegistered) { - if (DEBUG) Log.d(TAG, "Registering receiver"); - final IntentFilter filter = new IntentFilter(); - filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); - filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); - filter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION); - filter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION); - filter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION); - filter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION); - filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); - filter.addAction(WifiManager.RSSI_CHANGED_ACTION); - mContext.registerReceiver(this, filter); - mRegistered = true; - } else if (!listening && mRegistered) { - if (DEBUG) Log.d(TAG, "Unregistering receiver"); - mContext.unregisterReceiver(this); - mRegistered = false; - } - } - - @Override - public void onReceive(Context context, Intent intent) { - if (DEBUG) Log.d(TAG, "onReceive " + intent.getAction()); - if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(intent.getAction())) { - updateAccessPoints(); - mScanning = false; - } - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java index 63fcbc5..8f86e2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java @@ -35,4 +35,9 @@ public class AccessibilityContentDescriptions { }; static final int WIFI_NO_CONNECTION = R.string.accessibility_no_wifi; + + static final int[] ETHERNET_CONNECTION_VALUES = { + R.string.accessibility_ethernet_disconnected, + R.string.accessibility_ethernet_connected, + }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityController.java index 89ed787..cc431dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityController.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.policy; import android.content.Context; -import android.util.Log; import android.view.accessibility.AccessibilityManager; import java.io.FileDescriptor; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java index 49693f5fe..8fa9c7e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.policy; -import java.util.Set; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; + +import java.util.Collection; public interface BluetoothController { void addStateChangedCallback(Callback callback); @@ -28,36 +30,12 @@ public interface BluetoothController { boolean isBluetoothConnecting(); String getLastDeviceName(); void setBluetoothEnabled(boolean enabled); - Set<PairedDevice> getPairedDevices(); - void connect(PairedDevice device); - void disconnect(PairedDevice device); + Collection<CachedBluetoothDevice> getDevices(); + void connect(CachedBluetoothDevice device); + void disconnect(CachedBluetoothDevice device); public interface Callback { - void onBluetoothStateChange(boolean enabled, boolean connecting); - void onBluetoothPairedDevicesChanged(); - } - - public static final class PairedDevice implements Comparable<PairedDevice> { - public static int STATE_DISCONNECTED = 0; - public static int STATE_CONNECTING = 1; - public static int STATE_CONNECTED = 2; - public static int STATE_DISCONNECTING = 3; - - public String id; - public String name; - public int state = STATE_DISCONNECTED; - public Object tag; - - public static String stateToString(int state) { - if (state == STATE_DISCONNECTED) return "STATE_DISCONNECTED"; - if (state == STATE_CONNECTING) return "STATE_CONNECTING"; - if (state == STATE_CONNECTED) return "STATE_CONNECTED"; - if (state == STATE_DISCONNECTING) return "STATE_DISCONNECTING"; - return "UNKNOWN"; - } - - public int compareTo(PairedDevice another) { - return name.compareTo(another.name); - } + void onBluetoothStateChange(boolean enabled); + void onBluetoothDevicesChanged(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java index 894f82a..daa84ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -16,143 +16,85 @@ package com.android.systemui.statusbar.policy; -import static android.bluetooth.BluetoothAdapter.ERROR; -import static com.android.systemui.statusbar.policy.BluetoothUtil.connectionStateToString; -import static com.android.systemui.statusbar.policy.BluetoothUtil.deviceToString; -import static com.android.systemui.statusbar.policy.BluetoothUtil.profileToString; -import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToProfile; -import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToString; -import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidsToString; - -import android.bluetooth.BluetoothA2dp; -import android.bluetooth.BluetoothA2dpSink; import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHeadset; -import android.bluetooth.BluetoothHeadsetClient; -import android.bluetooth.BluetoothInputDevice; -import android.bluetooth.BluetoothManager; -import android.bluetooth.BluetoothMap; -import android.bluetooth.BluetoothPan; -import android.bluetooth.BluetoothProfile; -import android.bluetooth.BluetoothProfile.ServiceListener; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.ParcelUuid; -import android.util.ArrayMap; import android.util.Log; -import android.util.SparseArray; -import com.android.systemui.statusbar.policy.BluetoothUtil.Profile; +import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Set; -import java.util.TreeSet; +import java.util.Collection; -public class BluetoothControllerImpl implements BluetoothController { +public class BluetoothControllerImpl implements BluetoothController, BluetoothCallback, + CachedBluetoothDevice.Callback { private static final String TAG = "BluetoothController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - // This controls the order in which we check the states. Since a device can only have - // one state on screen, but can have multiple profiles, the later states override the - // value of earlier states. So if a device has a profile in CONNECTING and one in - // CONNECTED, it will show as CONNECTED, theoretically this shouldn't really happen often, - // but seemed worth noting. - private static final int[] CONNECTION_STATES = { - BluetoothProfile.STATE_DISCONNECTED, - BluetoothProfile.STATE_DISCONNECTING, - BluetoothProfile.STATE_CONNECTING, - BluetoothProfile.STATE_CONNECTED, - }; - // Update all the BT device states. - private static final int MSG_UPDATE_CONNECTION_STATES = 1; - // Update just one BT device. - private static final int MSG_UPDATE_SINGLE_CONNECTION_STATE = 2; - // Update whether devices are bonded or not. - private static final int MSG_UPDATE_BONDED_DEVICES = 3; - - private static final int MSG_ADD_PROFILE = 4; - private static final int MSG_REM_PROFILE = 5; - - private final Context mContext; - private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); - private final BluetoothAdapter mAdapter; - private final Receiver mReceiver = new Receiver(); - private final ArrayMap<BluetoothDevice, DeviceInfo> mDeviceInfo = new ArrayMap<>(); - private final SparseArray<BluetoothProfile> mProfiles = new SparseArray<>(); - private final H mHandler; + private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); + private final LocalBluetoothManager mLocalBluetoothManager; private boolean mEnabled; - private boolean mConnecting; - private BluetoothDevice mLastDevice; + private int mConnectionState = BluetoothAdapter.STATE_DISCONNECTED; + private CachedBluetoothDevice mLastDevice; + + private final H mHandler = new H(); public BluetoothControllerImpl(Context context, Looper bgLooper) { - mContext = context; - mHandler = new H(bgLooper); - - final BluetoothManager bluetoothManager = - (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); - mAdapter = bluetoothManager.getAdapter(); - if (mAdapter == null) { - Log.w(TAG, "Default BT adapter not found"); - return; + mLocalBluetoothManager = LocalBluetoothManager.getInstance(context, null); + if (mLocalBluetoothManager != null) { + mLocalBluetoothManager.getEventManager().setReceiverHandler(new Handler(bgLooper)); + mLocalBluetoothManager.getEventManager().registerCallback(this); + onBluetoothStateChanged( + mLocalBluetoothManager.getBluetoothAdapter().getBluetoothState()); } - - mReceiver.register(); - setAdapterState(mAdapter.getState()); - updateBondedDevices(); - bindAllProfiles(); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("BluetoothController state:"); - pw.print(" mAdapter="); pw.println(mAdapter); + pw.print(" mLocalBluetoothManager="); pw.println(mLocalBluetoothManager); + if (mLocalBluetoothManager == null) { + return; + } pw.print(" mEnabled="); pw.println(mEnabled); - pw.print(" mConnecting="); pw.println(mConnecting); + pw.print(" mConnectionState="); pw.println(stateToString(mConnectionState)); pw.print(" mLastDevice="); pw.println(mLastDevice); pw.print(" mCallbacks.size="); pw.println(mCallbacks.size()); - pw.print(" mProfiles="); pw.println(profilesToString(mProfiles)); - pw.print(" mDeviceInfo.size="); pw.println(mDeviceInfo.size()); - for (int i = 0; i < mDeviceInfo.size(); i++) { - final BluetoothDevice device = mDeviceInfo.keyAt(i); - final DeviceInfo info = mDeviceInfo.valueAt(i); - pw.print(" "); pw.print(deviceToString(device)); - pw.print('('); pw.print(uuidsToString(device)); pw.print(')'); - pw.print(" "); pw.println(infoToString(info)); + pw.println(" Bluetooth Devices:"); + for (CachedBluetoothDevice device : + mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy()) { + pw.println(" " + getDeviceString(device)); } } - private static String infoToString(DeviceInfo info) { - return info == null ? null : ("connectionState=" + - connectionStateToString(CONNECTION_STATES[info.connectionStateIndex]) - + ",bonded=" + info.bonded + ",profiles=" - + profilesToString(info.connectedProfiles)); + private static String stateToString(int state) { + switch (state) { + case BluetoothAdapter.STATE_CONNECTED: + return "CONNECTED"; + case BluetoothAdapter.STATE_CONNECTING: + return "CONNECTING"; + case BluetoothAdapter.STATE_DISCONNECTED: + return "DISCONNECTED"; + case BluetoothAdapter.STATE_DISCONNECTING: + return "DISCONNECTING"; + } + return "UNKNOWN(" + state + ")"; } - private static String profilesToString(SparseArray<?> profiles) { - final int N = profiles.size(); - final StringBuffer buffer = new StringBuffer(); - buffer.append('['); - for (int i = 0; i < N; i++) { - if (i != 0) { - buffer.append(','); - } - buffer.append(BluetoothUtil.profileToString(profiles.keyAt(i))); - } - buffer.append(']'); - return buffer.toString(); + private String getDeviceString(CachedBluetoothDevice device) { + return device.getName() + " " + device.getBondState() + " " + device.isConnected(); } + @Override public void addStateChangedCallback(Callback cb) { mCallbacks.add(cb); - fireStateChange(cb); + mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED); } @Override @@ -162,411 +104,147 @@ public class BluetoothControllerImpl implements BluetoothController { @Override public boolean isBluetoothEnabled() { - return mAdapter != null && mAdapter.isEnabled(); + return mEnabled; } @Override public boolean isBluetoothConnected() { - return mAdapter != null - && mAdapter.getConnectionState() == BluetoothAdapter.STATE_CONNECTED; + return mConnectionState == BluetoothAdapter.STATE_CONNECTED; } @Override public boolean isBluetoothConnecting() { - return mAdapter != null - && mAdapter.getConnectionState() == BluetoothAdapter.STATE_CONNECTING; + return mConnectionState == BluetoothAdapter.STATE_CONNECTING; } @Override public void setBluetoothEnabled(boolean enabled) { - if (mAdapter != null) { - if (enabled) { - mAdapter.enable(); - } else { - mAdapter.disable(); - } + if (mLocalBluetoothManager != null) { + mLocalBluetoothManager.getBluetoothAdapter().setBluetoothEnabled(enabled); } } @Override public boolean isBluetoothSupported() { - return mAdapter != null; + return mLocalBluetoothManager != null; } @Override - public Set<PairedDevice> getPairedDevices() { - final Set<PairedDevice> rt = new TreeSet<>(); - for (int i = 0; i < mDeviceInfo.size(); i++) { - final BluetoothDevice device = mDeviceInfo.keyAt(i); - final DeviceInfo info = mDeviceInfo.valueAt(i); - if (!info.bonded) continue; - final PairedDevice paired = new PairedDevice(); - paired.id = device.getAddress(); - paired.tag = device; - paired.name = device.getAliasName(); - paired.state = connectionStateToPairedDeviceState(info.connectionStateIndex); - rt.add(paired); - } - return rt; - } - - private static int connectionStateToPairedDeviceState(int index) { - int state = CONNECTION_STATES[index]; - if (state == BluetoothAdapter.STATE_CONNECTED) return PairedDevice.STATE_CONNECTED; - if (state == BluetoothAdapter.STATE_CONNECTING) return PairedDevice.STATE_CONNECTING; - if (state == BluetoothAdapter.STATE_DISCONNECTING) return PairedDevice.STATE_DISCONNECTING; - return PairedDevice.STATE_DISCONNECTED; + public void connect(final CachedBluetoothDevice device) { + if (mLocalBluetoothManager == null || device == null) return; + device.connect(true); } @Override - public void connect(final PairedDevice pd) { - connect(pd, true); - } - - @Override - public void disconnect(PairedDevice pd) { - connect(pd, false); - } - - private void connect(PairedDevice pd, final boolean connect) { - if (mAdapter == null || pd == null || pd.tag == null) return; - final BluetoothDevice device = (BluetoothDevice) pd.tag; - final DeviceInfo info = mDeviceInfo.get(device); - final String action = connect ? "connect" : "disconnect"; - if (DEBUG) Log.d(TAG, action + " " + deviceToString(device)); - final ParcelUuid[] uuids = device.getUuids(); - if (uuids == null) { - Log.w(TAG, "No uuids returned, aborting " + action + " for " + deviceToString(device)); - return; - } - SparseArray<Boolean> profiles = new SparseArray<>(); - if (connect) { - // When connecting add every profile we can recognize by uuid. - for (ParcelUuid uuid : uuids) { - final int profile = uuidToProfile(uuid); - if (profile == 0) { - Log.w(TAG, "Device " + deviceToString(device) + " has an unsupported uuid: " - + uuidToString(uuid)); - continue; - } - final boolean connected = info.connectedProfiles.get(profile, false); - if (!connected) { - profiles.put(profile, true); - } - } - } else { - // When disconnecting, just add every profile we know they are connected to. - profiles = info.connectedProfiles; - } - for (int i = 0; i < profiles.size(); i++) { - final int profile = profiles.keyAt(i); - if (mProfiles.indexOfKey(profile) >= 0) { - final Profile p = BluetoothUtil.getProfile(mProfiles.get(profile)); - final boolean ok = connect ? p.connect(device) : p.disconnect(device); - if (DEBUG) Log.d(TAG, action + " " + profileToString(profile) + " " - + (ok ? "succeeded" : "failed")); - } else { - Log.w(TAG, "Unable get get Profile for " + profileToString(profile)); - } - } + public void disconnect(CachedBluetoothDevice device) { + if (mLocalBluetoothManager == null || device == null) return; + device.disconnect(); } @Override public String getLastDeviceName() { - return mLastDevice != null ? mLastDevice.getAliasName() : null; - } - - private void updateBondedDevices() { - mHandler.removeMessages(MSG_UPDATE_BONDED_DEVICES); - mHandler.sendEmptyMessage(MSG_UPDATE_BONDED_DEVICES); - } - - private void updateConnectionStates() { - mHandler.removeMessages(MSG_UPDATE_CONNECTION_STATES); - mHandler.removeMessages(MSG_UPDATE_SINGLE_CONNECTION_STATE); - mHandler.sendEmptyMessage(MSG_UPDATE_CONNECTION_STATES); - } - - private void updateConnectionState(BluetoothDevice device, int profile, int state) { - if (mHandler.hasMessages(MSG_UPDATE_CONNECTION_STATES)) { - // If we are about to update all the devices, then we don't need to update this one. - return; - } - mHandler.obtainMessage(MSG_UPDATE_SINGLE_CONNECTION_STATE, profile, state, device) - .sendToTarget(); + return mLastDevice != null ? mLastDevice.getName() : null; } - private void handleUpdateBondedDevices() { - if (mAdapter == null) return; - final Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices(); - for (DeviceInfo info : mDeviceInfo.values()) { - info.bonded = false; - } - int bondedCount = 0; - BluetoothDevice lastBonded = null; - if (bondedDevices != null) { - for (BluetoothDevice bondedDevice : bondedDevices) { - final boolean bonded = bondedDevice.getBondState() != BluetoothDevice.BOND_NONE; - updateInfo(bondedDevice).bonded = bonded; - if (bonded) { - bondedCount++; - lastBonded = bondedDevice; - } - } - } - if (mLastDevice == null && bondedCount == 1) { - mLastDevice = lastBonded; - } - updateConnectionStates(); - firePairedDevicesChanged(); + @Override + public Collection<CachedBluetoothDevice> getDevices() { + return mLocalBluetoothManager != null + ? mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy() + : null; } - private void handleUpdateConnectionStates() { - final int N = mDeviceInfo.size(); - for (int i = 0; i < N; i++) { - BluetoothDevice device = mDeviceInfo.keyAt(i); - DeviceInfo info = updateInfo(device); - info.connectionStateIndex = 0; - info.connectedProfiles.clear(); - for (int j = 0; j < mProfiles.size(); j++) { - int state = mProfiles.valueAt(j).getConnectionState(device); - handleUpdateConnectionState(device, mProfiles.keyAt(j), state); - } + private void updateConnected() { + // Make sure our connection state is up to date. + int state = mLocalBluetoothManager.getBluetoothAdapter().getConnectionState(); + if (state != mConnectionState) { + mConnectionState = state; + mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED); } - handleConnectionChange(); - firePairedDevicesChanged(); - } - - private void handleUpdateConnectionState(BluetoothDevice device, int profile, int state) { - if (DEBUG) Log.d(TAG, "updateConnectionState " + BluetoothUtil.deviceToString(device) - + " " + BluetoothUtil.profileToString(profile) - + " " + BluetoothUtil.connectionStateToString(state)); - DeviceInfo info = updateInfo(device); - int stateIndex = 0; - for (int i = 0; i < CONNECTION_STATES.length; i++) { - if (CONNECTION_STATES[i] == state) { - stateIndex = i; - break; - } + if (mLastDevice != null && mLastDevice.isConnected()) { + // Our current device is still valid. + return; } - info.profileStates.put(profile, stateIndex); - - info.connectionStateIndex = 0; - final int N = info.profileStates.size(); - for (int i = 0; i < N; i++) { - if (info.profileStates.valueAt(i) > info.connectionStateIndex) { - info.connectionStateIndex = info.profileStates.valueAt(i); + for (CachedBluetoothDevice device : getDevices()) { + if (device.isConnected()) { + mLastDevice = device; } } - if (state == BluetoothProfile.STATE_CONNECTED) { - info.connectedProfiles.put(profile, true); - } else { - info.connectedProfiles.remove(profile); - } } - private void handleConnectionChange() { - // If we are no longer connected to the current device, see if we are connected to - // something else, so we don't display a name we aren't connected to. - if (mLastDevice != null && - CONNECTION_STATES[mDeviceInfo.get(mLastDevice).connectionStateIndex] - != BluetoothProfile.STATE_CONNECTED) { - // Make sure we don't keep this device while it isn't connected. - mLastDevice = null; - // Look for anything else connected. - final int size = mDeviceInfo.size(); - for (int i = 0; i < size; i++) { - BluetoothDevice device = mDeviceInfo.keyAt(i); - DeviceInfo info = mDeviceInfo.valueAt(i); - if (CONNECTION_STATES[info.connectionStateIndex] - == BluetoothProfile.STATE_CONNECTED) { - mLastDevice = device; - break; - } - } - } + @Override + public void onBluetoothStateChanged(int bluetoothState) { + mEnabled = bluetoothState == BluetoothAdapter.STATE_ON; + mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED); } - private void bindAllProfiles() { - // Note: This needs to contain all of the types that can be returned by BluetoothUtil - // otherwise we can't find the profiles we need when we connect/disconnect. - mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP); - mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP_SINK); - mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.AVRCP_CONTROLLER); - mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET); - mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET_CLIENT); - mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.INPUT_DEVICE); - mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.MAP); - mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.PAN); - // Note Health is not in this list because health devices aren't 'connected'. - // If profiles are expanded to use more than just connection state and connect/disconnect - // then it should be added. + @Override + public void onScanningStateChanged(boolean started) { + // Don't care. } - private void firePairedDevicesChanged() { - for (Callback cb : mCallbacks) { - cb.onBluetoothPairedDevicesChanged(); - } + @Override + public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { + cachedDevice.registerCallback(this); + updateConnected(); + mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED); } - private void setAdapterState(int adapterState) { - final boolean enabled = adapterState == BluetoothAdapter.STATE_ON; - if (mEnabled == enabled) return; - mEnabled = enabled; - fireStateChange(); + @Override + public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { + updateConnected(); + mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED); } - private void setConnecting(boolean connecting) { - if (mConnecting == connecting) return; - mConnecting = connecting; - fireStateChange(); + @Override + public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { + updateConnected(); + mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED); } - private void fireStateChange() { - for (Callback cb : mCallbacks) { - fireStateChange(cb); - } + @Override + public void onDeviceAttributesChanged() { + updateConnected(); + mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED); } - private void fireStateChange(Callback cb) { - cb.onBluetoothStateChange(mEnabled, mConnecting); + @Override + public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + mLastDevice = cachedDevice; + updateConnected(); + mConnectionState = state; + mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED); } - private static int getProfileFromAction(String action) { - if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { - return BluetoothProfile.A2DP; - } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { - return BluetoothProfile.HEADSET; - } else if (BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { - return BluetoothProfile.A2DP_SINK; - } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { - return BluetoothProfile.HEADSET_CLIENT; - } else if (BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { - return BluetoothProfile.INPUT_DEVICE; - } else if (BluetoothMap.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { - return BluetoothProfile.MAP; - } else if (BluetoothPan.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { - return BluetoothProfile.PAN; - } - if (DEBUG) Log.d(TAG, "Unknown action " + action); - return -1; - } + private final class H extends Handler { + private static final int MSG_PAIRED_DEVICES_CHANGED = 1; + private static final int MSG_STATE_CHANGED = 2; - private final ServiceListener mProfileListener = new ServiceListener() { @Override - public void onServiceDisconnected(int profile) { - if (DEBUG) Log.d(TAG, "Disconnected from " + BluetoothUtil.profileToString(profile)); - // We lost a profile, don't do any updates until it gets removed. - mHandler.removeMessages(MSG_UPDATE_CONNECTION_STATES); - mHandler.removeMessages(MSG_UPDATE_SINGLE_CONNECTION_STATE); - mHandler.obtainMessage(MSG_REM_PROFILE, profile, 0).sendToTarget(); + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_PAIRED_DEVICES_CHANGED: + firePairedDevicesChanged(); + break; + case MSG_STATE_CHANGED: + fireStateChange(); + break; + } } - @Override - public void onServiceConnected(int profile, BluetoothProfile proxy) { - if (DEBUG) Log.d(TAG, "Connected to " + BluetoothUtil.profileToString(profile)); - mHandler.obtainMessage(MSG_ADD_PROFILE, profile, 0, proxy).sendToTarget(); - } - }; - - private final class Receiver extends BroadcastReceiver { - public void register() { - final IntentFilter filter = new IntentFilter(); - filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); - filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); - filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); - filter.addAction(BluetoothDevice.ACTION_ALIAS_CHANGED); - filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); - filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); - filter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED); - filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); - filter.addAction(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED); - filter.addAction(BluetoothMap.ACTION_CONNECTION_STATE_CHANGED); - filter.addAction(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED); - mContext.registerReceiver(this, filter); + private void firePairedDevicesChanged() { + for (BluetoothController.Callback cb : mCallbacks) { + cb.onBluetoothDevicesChanged(); + } } - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - - if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { - setAdapterState(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, ERROR)); - updateBondedDevices(); - if (DEBUG) Log.d(TAG, "ACTION_STATE_CHANGED " + mEnabled); - } else if (action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) { - updateInfo(device); - final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, - ERROR); - mLastDevice = device; - if (DEBUG) Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED " - + connectionStateToString(state) + " " + deviceToString(device)); - setConnecting(state == BluetoothAdapter.STATE_CONNECTING); - } else if (action.equals(BluetoothDevice.ACTION_ALIAS_CHANGED)) { - updateInfo(device); - mLastDevice = device; - } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { - if (DEBUG) Log.d(TAG, "ACTION_BOND_STATE_CHANGED " + device); - updateBondedDevices(); - } else { - int profile = getProfileFromAction(intent.getAction()); - int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); - if (DEBUG) Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGE " - + BluetoothUtil.profileToString(profile) - + " " + BluetoothUtil.connectionStateToString(state)); - if ((profile != -1) && (state != -1)) { - updateConnectionState(device, profile, state); - } + private void fireStateChange() { + for (BluetoothController.Callback cb : mCallbacks) { + fireStateChange(cb); } } - } - private DeviceInfo updateInfo(BluetoothDevice device) { - DeviceInfo info = mDeviceInfo.get(device); - info = info != null ? info : new DeviceInfo(); - mDeviceInfo.put(device, info); - return info; - } - - private class H extends Handler { - public H(Looper l) { - super(l); + private void fireStateChange(BluetoothController.Callback cb) { + cb.onBluetoothStateChange(mEnabled); } - - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_UPDATE_CONNECTION_STATES: - handleUpdateConnectionStates(); - firePairedDevicesChanged(); - break; - case MSG_UPDATE_SINGLE_CONNECTION_STATE: - handleUpdateConnectionState((BluetoothDevice) msg.obj, msg.arg1, msg.arg2); - handleConnectionChange(); - firePairedDevicesChanged(); - break; - case MSG_UPDATE_BONDED_DEVICES: - handleUpdateBondedDevices(); - firePairedDevicesChanged(); - break; - case MSG_ADD_PROFILE: - mProfiles.put(msg.arg1, (BluetoothProfile) msg.obj); - handleUpdateConnectionStates(); - firePairedDevicesChanged(); - break; - case MSG_REM_PROFILE: - mProfiles.remove(msg.arg1); - handleUpdateConnectionStates(); - firePairedDevicesChanged(); - break; - } - }; - }; - - private static class DeviceInfo { - int connectionStateIndex = 0; - boolean bonded; // per getBondedDevices - SparseArray<Boolean> connectedProfiles = new SparseArray<>(); - SparseArray<Integer> profileStates = new SparseArray<>(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java deleted file mode 100644 index ed8ac2c..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.policy; - -import android.bluetooth.BluetoothA2dp; -import android.bluetooth.BluetoothA2dpSink; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHeadset; -import android.bluetooth.BluetoothHeadsetClient; -import android.bluetooth.BluetoothInputDevice; -import android.bluetooth.BluetoothMap; -import android.bluetooth.BluetoothPan; -import android.bluetooth.BluetoothProfile; -import android.bluetooth.BluetoothUuid; -import android.os.ParcelUuid; -import android.text.TextUtils; - -public class BluetoothUtil { - - public static String profileToString(int profile) { - if (profile == BluetoothProfile.HEADSET) return "HEADSET"; - if (profile == BluetoothProfile.A2DP) return "A2DP"; - if (profile == BluetoothProfile.AVRCP_CONTROLLER) return "AVRCP_CONTROLLER"; - if (profile == BluetoothProfile.PAN) return "PAN"; - if (profile == BluetoothProfile.INPUT_DEVICE) return "INPUT_DEVICE"; - if (profile == BluetoothProfile.MAP) return "MAP"; - return "UNKNOWN(" + profile + ")"; - } - - public static String profileStateToString(int state) { - if (state == BluetoothProfile.STATE_CONNECTED) return "STATE_CONNECTED"; - if (state == BluetoothProfile.STATE_CONNECTING) return "STATE_CONNECTING"; - if (state == BluetoothProfile.STATE_DISCONNECTED) return "STATE_DISCONNECTED"; - if (state == BluetoothProfile.STATE_DISCONNECTING) return "STATE_DISCONNECTING"; - return "STATE_UNKNOWN"; - } - - public static String uuidToString(ParcelUuid uuid) { - if (BluetoothUuid.AudioSink.equals(uuid)) return "AudioSink"; - if (BluetoothUuid.AudioSource.equals(uuid)) return "AudioSource"; - if (BluetoothUuid.AdvAudioDist.equals(uuid)) return "AdvAudioDist"; - if (BluetoothUuid.HSP.equals(uuid)) return "HSP"; - if (BluetoothUuid.HSP_AG.equals(uuid)) return "HSP_AG"; - if (BluetoothUuid.Handsfree.equals(uuid)) return "Handsfree"; - if (BluetoothUuid.Handsfree_AG.equals(uuid)) return "Handsfree_AG"; - if (BluetoothUuid.AvrcpController.equals(uuid)) return "AvrcpController"; - if (BluetoothUuid.AvrcpTarget.equals(uuid)) return "AvrcpTarget"; - if (BluetoothUuid.ObexObjectPush.equals(uuid)) return "ObexObjectPush"; - if (BluetoothUuid.Hid.equals(uuid)) return "Hid"; - if (BluetoothUuid.Hogp.equals(uuid)) return "Hogp"; - if (BluetoothUuid.PANU.equals(uuid)) return "PANU"; - if (BluetoothUuid.NAP.equals(uuid)) return "NAP"; - if (BluetoothUuid.BNEP.equals(uuid)) return "BNEP"; - if (BluetoothUuid.PBAP_PSE.equals(uuid)) return "PBAP_PSE"; - if (BluetoothUuid.MAP.equals(uuid)) return "MAP"; - if (BluetoothUuid.MNS.equals(uuid)) return "MNS"; - if (BluetoothUuid.MAS.equals(uuid)) return "MAS"; - return uuid != null ? uuid.toString() : null; - } - - public static String connectionStateToString(int connectionState) { - if (connectionState == BluetoothAdapter.STATE_DISCONNECTED) return "STATE_DISCONNECTED"; - if (connectionState == BluetoothAdapter.STATE_CONNECTED) return "STATE_CONNECTED"; - if (connectionState == BluetoothAdapter.STATE_DISCONNECTING) return "STATE_DISCONNECTING"; - if (connectionState == BluetoothAdapter.STATE_CONNECTING) return "STATE_CONNECTING"; - return "ERROR"; - } - - public static String deviceToString(BluetoothDevice device) { - return device == null ? null : (device.getAddress() + '[' + device.getAliasName() + ']'); - } - - public static String uuidsToString(BluetoothDevice device) { - if (device == null) return null; - final ParcelUuid[] ids = device.getUuids(); - if (ids == null) return null; - final String[] tokens = new String[ids.length]; - for (int i = 0; i < tokens.length; i++) { - tokens[i] = uuidToString(ids[i]); - } - return TextUtils.join(",", tokens); - } - - public static int uuidToProfile(ParcelUuid uuid) { - if (BluetoothUuid.AudioSink.equals(uuid)) return BluetoothProfile.A2DP; - if (BluetoothUuid.AdvAudioDist.equals(uuid)) return BluetoothProfile.A2DP; - - if (BluetoothUuid.HSP.equals(uuid)) return BluetoothProfile.HEADSET; - if (BluetoothUuid.Handsfree.equals(uuid)) return BluetoothProfile.HEADSET; - - if (BluetoothUuid.MAP.equals(uuid)) return BluetoothProfile.MAP; - if (BluetoothUuid.MNS.equals(uuid)) return BluetoothProfile.MAP; - if (BluetoothUuid.MAS.equals(uuid)) return BluetoothProfile.MAP; - - if (BluetoothUuid.AvrcpController.equals(uuid)) return BluetoothProfile.AVRCP_CONTROLLER; - - if (BluetoothUuid.Hid.equals(uuid)) return BluetoothProfile.INPUT_DEVICE; - if (BluetoothUuid.Hogp.equals(uuid)) return BluetoothProfile.INPUT_DEVICE; - - if (BluetoothUuid.NAP.equals(uuid)) return BluetoothProfile.PAN; - - return 0; - } - - public static Profile getProfile(BluetoothProfile p) { - if (p instanceof BluetoothA2dp) return newProfile((BluetoothA2dp) p); - if (p instanceof BluetoothHeadset) return newProfile((BluetoothHeadset) p); - if (p instanceof BluetoothA2dpSink) return newProfile((BluetoothA2dpSink) p); - if (p instanceof BluetoothHeadsetClient) return newProfile((BluetoothHeadsetClient) p); - if (p instanceof BluetoothInputDevice) return newProfile((BluetoothInputDevice) p); - if (p instanceof BluetoothMap) return newProfile((BluetoothMap) p); - if (p instanceof BluetoothPan) return newProfile((BluetoothPan) p); - return null; - } - - private static Profile newProfile(final BluetoothA2dp a2dp) { - return new Profile() { - @Override - public boolean connect(BluetoothDevice device) { - return a2dp.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device) { - return a2dp.disconnect(device); - } - }; - } - - private static Profile newProfile(final BluetoothHeadset headset) { - return new Profile() { - @Override - public boolean connect(BluetoothDevice device) { - return headset.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device) { - return headset.disconnect(device); - } - }; - } - - private static Profile newProfile(final BluetoothA2dpSink sink) { - return new Profile() { - @Override - public boolean connect(BluetoothDevice device) { - return sink.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device) { - return sink.disconnect(device); - } - }; - } - - private static Profile newProfile(final BluetoothHeadsetClient client) { - return new Profile() { - @Override - public boolean connect(BluetoothDevice device) { - return client.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device) { - return client.disconnect(device); - } - }; - } - - private static Profile newProfile(final BluetoothInputDevice input) { - return new Profile() { - @Override - public boolean connect(BluetoothDevice device) { - return input.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device) { - return input.disconnect(device); - } - }; - } - - private static Profile newProfile(final BluetoothMap map) { - return new Profile() { - @Override - public boolean connect(BluetoothDevice device) { - return map.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device) { - return map.disconnect(device); - } - }; - } - - private static Profile newProfile(final BluetoothPan pan) { - return new Profile() { - @Override - public boolean connect(BluetoothDevice device) { - return pan.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device) { - return pan.disconnect(device); - } - }; - } - - // common abstraction for supported profiles - public interface Profile { - boolean connect(BluetoothDevice device); - boolean disconnect(BluetoothDevice device); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java index 895af62..0340984 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java @@ -58,7 +58,7 @@ public class BrightnessMirrorController { .withEndAction(new Runnable() { @Override public void run() { - mBrightnessMirror.setVisibility(View.GONE); + mBrightnessMirror.setVisibility(View.INVISIBLE); } }); } @@ -77,12 +77,18 @@ public class BrightnessMirrorController { public void setLocation(View original) { original.getLocationInWindow(mInt2Cache); + + // Original is slightly larger than the mirror, so make sure to use the center for the + // positioning. + int originalX = mInt2Cache[0] + original.getWidth()/2; int originalY = mInt2Cache[1]; + mBrightnessMirror.setTranslationX(0); + mBrightnessMirror.setTranslationY(0); mBrightnessMirror.getLocationInWindow(mInt2Cache); + int mirrorX = mInt2Cache[0] + mBrightnessMirror.getWidth()/2; int mirrorY = mInt2Cache[1]; - - mBrightnessMirror.setTranslationY(mBrightnessMirror.getTranslationY() - + originalY - mirrorY); + mBrightnessMirror.setTranslationX(originalX - mirrorX); + mBrightnessMirror.setTranslationY(originalY - mirrorY); } public View getMirror() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java new file mode 100644 index 0000000..e618cb8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.telephony.SubscriptionInfo; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.statusbar.policy.NetworkController.IconState; +import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; +import com.android.systemui.statusbar.policy.NetworkControllerImpl.EmergencyListener; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Implements network listeners and forwards the calls along onto other listeners but on + * the current or specified Looper. + */ +public class CallbackHandler extends Handler implements EmergencyListener, SignalCallback { + private static final int MSG_EMERGENCE_CHANGED = 0; + private static final int MSG_SUBS_CHANGED = 1; + private static final int MSG_NO_SIM_VISIBLE_CHANGED = 2; + private static final int MSG_ETHERNET_CHANGED = 3; + private static final int MSG_AIRPLANE_MODE_CHANGED = 4; + private static final int MSG_MOBILE_DATA_ENABLED_CHANGED = 5; + private static final int MSG_ADD_REMOVE_EMERGENCY = 6; + private static final int MSG_ADD_REMOVE_SIGNAL = 7; + + // All the callbacks. + private final ArrayList<EmergencyListener> mEmergencyListeners = new ArrayList<>(); + private final ArrayList<SignalCallback> mSignalCallbacks = new ArrayList<>(); + + public CallbackHandler() { + super(); + } + + @VisibleForTesting + CallbackHandler(Looper looper) { + super(looper); + } + + @Override + @SuppressWarnings("unchecked") + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_EMERGENCE_CHANGED: + for (EmergencyListener listener : mEmergencyListeners) { + listener.setEmergencyCallsOnly(msg.arg1 != 0); + } + break; + case MSG_SUBS_CHANGED: + for (SignalCallback signalCluster : mSignalCallbacks) { + signalCluster.setSubs((List<SubscriptionInfo>) msg.obj); + } + break; + case MSG_NO_SIM_VISIBLE_CHANGED: + for (SignalCallback signalCluster : mSignalCallbacks) { + signalCluster.setNoSims(msg.arg1 != 0); + } + break; + case MSG_ETHERNET_CHANGED: + for (SignalCallback signalCluster : mSignalCallbacks) { + signalCluster.setEthernetIndicators((IconState) msg.obj); + } + break; + case MSG_AIRPLANE_MODE_CHANGED: + for (SignalCallback signalCluster : mSignalCallbacks) { + signalCluster.setIsAirplaneMode((IconState) msg.obj); + } + break; + case MSG_MOBILE_DATA_ENABLED_CHANGED: + for (SignalCallback signalCluster : mSignalCallbacks) { + signalCluster.setMobileDataEnabled(msg.arg1 != 0); + } + break; + case MSG_ADD_REMOVE_EMERGENCY: + if (msg.arg1 != 0) { + mEmergencyListeners.add((EmergencyListener) msg.obj); + } else { + mEmergencyListeners.remove((EmergencyListener) msg.obj); + } + break; + case MSG_ADD_REMOVE_SIGNAL: + if (msg.arg1 != 0) { + mSignalCallbacks.add((SignalCallback) msg.obj); + } else { + mSignalCallbacks.remove((SignalCallback) msg.obj); + } + break; + } + } + + @Override + public void setWifiIndicators(final boolean enabled, final IconState statusIcon, + final IconState qsIcon, final boolean activityIn, final boolean activityOut, + final String description) { + post(new Runnable() { + @Override + public void run() { + for (SignalCallback callback : mSignalCallbacks) { + callback.setWifiIndicators(enabled, statusIcon, qsIcon, activityIn, activityOut, + description); + } + } + }); + } + + @Override + public void setMobileDataIndicators(final IconState statusIcon, final IconState qsIcon, + final int statusType, final int qsType,final boolean activityIn, + final boolean activityOut, final String typeContentDescription, + final String description, final boolean isWide, final int subId) { + post(new Runnable() { + @Override + public void run() { + for (SignalCallback signalCluster : mSignalCallbacks) { + signalCluster.setMobileDataIndicators(statusIcon, qsIcon, statusType, qsType, + activityIn, activityOut, typeContentDescription, description, isWide, + subId); + } + } + }); + } + + @Override + public void setSubs(List<SubscriptionInfo> subs) { + obtainMessage(MSG_SUBS_CHANGED, subs).sendToTarget(); + } + + @Override + public void setNoSims(boolean show) { + obtainMessage(MSG_NO_SIM_VISIBLE_CHANGED, show ? 1 : 0, 0).sendToTarget(); + } + + @Override + public void setMobileDataEnabled(boolean enabled) { + obtainMessage(MSG_MOBILE_DATA_ENABLED_CHANGED, enabled ? 1 : 0, 0).sendToTarget(); + } + + @Override + public void setEmergencyCallsOnly(boolean emergencyOnly) { + obtainMessage(MSG_EMERGENCE_CHANGED, emergencyOnly ? 1 : 0, 0).sendToTarget(); + } + + @Override + public void setEthernetIndicators(IconState icon) { + obtainMessage(MSG_ETHERNET_CHANGED, icon).sendToTarget();; + } + + @Override + public void setIsAirplaneMode(IconState icon) { + obtainMessage(MSG_AIRPLANE_MODE_CHANGED, icon).sendToTarget();; + } + + public void setListening(EmergencyListener listener, boolean listening) { + obtainMessage(MSG_ADD_REMOVE_EMERGENCY, listening ? 1 : 0, 0, listener).sendToTarget(); + } + + public void setListening(SignalCallback listener, boolean listening) { + obtainMessage(MSG_ADD_REMOVE_SIGNAL, listening ? 1 : 0, 0, listener).sendToTarget(); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/recent/Constants.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java index 8252a9f..b391bd9 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/Constants.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,13 @@ * limitations under the License. */ -package com.android.systemui.recent; +package com.android.systemui.statusbar.policy; -public class Constants { - static final int MAX_ESCAPE_ANIMATION_DURATION = 500; // in ms - static final int SNAP_BACK_DURATION = 250; // in ms - static final int ESCAPE_VELOCITY = 100; // speed of item required to "curate" it in dp/s - public static float ALPHA_FADE_START = 0.8f; // fraction of thumbnail width where fade starts - static final float ALPHA_FADE_END = 0.5f; // fraction of thumbnail width beyond which alpha->0 +import com.android.systemui.R; + +class EthernetIcons { + static final int[][] ETHERNET_ICONS = { + { R.drawable.stat_sys_ethernet }, + { R.drawable.stat_sys_ethernet_fully }, + }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetSignalController.java new file mode 100644 index 0000000..bd36462 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetSignalController.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy; + +import android.content.Context; +import android.net.NetworkCapabilities; + +import com.android.systemui.statusbar.policy.NetworkController.IconState; + +import java.util.BitSet; + + +public class EthernetSignalController extends + SignalController<SignalController.State, SignalController.IconGroup> { + + public EthernetSignalController(Context context, + CallbackHandler callbackHandler, NetworkControllerImpl networkController) { + super("EthernetSignalController", context, NetworkCapabilities.TRANSPORT_ETHERNET, + callbackHandler, networkController); + mCurrentState.iconGroup = mLastState.iconGroup = new IconGroup( + "Ethernet Icons", + EthernetIcons.ETHERNET_ICONS, + null, + AccessibilityContentDescriptions.ETHERNET_CONNECTION_VALUES, + 0, 0, 0, 0, + AccessibilityContentDescriptions.ETHERNET_CONNECTION_VALUES[0]); + } + + @Override + public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) { + mCurrentState.connected = connectedTransports.get(mTransportType); + super.updateConnectivity(connectedTransports, validatedTransports); + } + + @Override + public void notifyListeners() { + boolean ethernetVisible = mCurrentState.connected; + String contentDescription = getStringIfExists(getContentDescription()); + + // TODO: wire up data transfer using WifiSignalPoller. + mCallbackHandler.setEthernetIndicators(new IconState(ethernetVisible, getCurrentIconId(), + contentDescription)); + } + + @Override + public SignalController.State cleanState() { + return new SignalController.State(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java index 33f7aff..cd1914c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java @@ -17,21 +17,14 @@ package com.android.systemui.statusbar.policy; import android.content.Context; -import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; -import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CaptureRequest; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; -import android.os.SystemProperties; +import android.text.TextUtils; import android.util.Log; -import android.util.Size; -import android.view.Surface; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -45,7 +38,7 @@ public class FlashlightController { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int DISPATCH_ERROR = 0; - private static final int DISPATCH_OFF = 1; + private static final int DISPATCH_CHANGED = 1; private static final int DISPATCH_AVAILABILITY_CHANGED = 2; private final CameraManager mCameraManager; @@ -58,52 +51,50 @@ public class FlashlightController { /** Lock on {@code this} when accessing */ private boolean mFlashlightEnabled; - private String mCameraId; - private boolean mCameraAvailable; - private CameraDevice mCameraDevice; - private CaptureRequest mFlashlightRequest; - private CameraCaptureSession mSession; - private SurfaceTexture mSurfaceTexture; - private Surface mSurface; + private final String mCameraId; + private boolean mTorchAvailable; public FlashlightController(Context mContext) { mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); - initialize(); - } - public void initialize() { + String cameraId = null; try { - mCameraId = getCameraId(); + cameraId = getCameraId(); } catch (Throwable e) { Log.e(TAG, "Couldn't initialize.", e); return; + } finally { + mCameraId = cameraId; } if (mCameraId != null) { ensureHandler(); - mCameraManager.registerAvailabilityCallback(mAvailabilityCallback, mHandler); + mCameraManager.registerTorchCallback(mTorchCallback, mHandler); } } - public synchronized void setFlashlight(boolean enabled) { - if (mFlashlightEnabled != enabled) { - mFlashlightEnabled = enabled; - postUpdateFlashlight(); - } - } - - public void killFlashlight() { - boolean enabled; + public void setFlashlight(boolean enabled) { + boolean pendingError = false; synchronized (this) { - enabled = mFlashlightEnabled; + if (mFlashlightEnabled != enabled) { + mFlashlightEnabled = enabled; + try { + mCameraManager.setTorchMode(mCameraId, enabled); + } catch (CameraAccessException e) { + Log.e(TAG, "Couldn't set torch mode", e); + mFlashlightEnabled = false; + pendingError = true; + } + } } - if (enabled) { - mHandler.post(mKillFlashlightRunnable); + dispatchModeChanged(mFlashlightEnabled); + if (pendingError) { + dispatchError(); } } public synchronized boolean isAvailable() { - return mCameraAvailable; + return mTorchAvailable; } public void addListener(FlashlightListener l) { @@ -127,42 +118,6 @@ public class FlashlightController { } } - private void startDevice() throws CameraAccessException { - mCameraManager.openCamera(getCameraId(), mCameraListener, mHandler); - } - - private void startSession() throws CameraAccessException { - mSurfaceTexture = new SurfaceTexture(false); - Size size = getSmallestSize(mCameraDevice.getId()); - mSurfaceTexture.setDefaultBufferSize(size.getWidth(), size.getHeight()); - mSurface = new Surface(mSurfaceTexture); - ArrayList<Surface> outputs = new ArrayList<>(1); - outputs.add(mSurface); - mCameraDevice.createCaptureSession(outputs, mSessionListener, mHandler); - } - - private Size getSmallestSize(String cameraId) throws CameraAccessException { - Size[] outputSizes = mCameraManager.getCameraCharacteristics(cameraId) - .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) - .getOutputSizes(SurfaceTexture.class); - if (outputSizes == null || outputSizes.length == 0) { - throw new IllegalStateException( - "Camera " + cameraId + "doesn't support any outputSize."); - } - Size chosen = outputSizes[0]; - for (Size s : outputSizes) { - if (chosen.getWidth() >= s.getWidth() && chosen.getHeight() >= s.getHeight()) { - chosen = s; - } - } - return chosen; - } - - private void postUpdateFlashlight() { - ensureHandler(); - mHandler.post(mUpdateFlashlightRunnable); - } - private String getCameraId() throws CameraAccessException { String[] ids = mCameraManager.getCameraIdList(); for (String id : ids) { @@ -177,70 +132,12 @@ public class FlashlightController { return null; } - private void updateFlashlight(boolean forceDisable) { - try { - boolean enabled; - synchronized (this) { - enabled = mFlashlightEnabled && !forceDisable; - } - if (enabled) { - if (mCameraDevice == null) { - startDevice(); - return; - } - if (mSession == null) { - startSession(); - return; - } - if (mFlashlightRequest == null) { - CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest( - CameraDevice.TEMPLATE_PREVIEW); - builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); - builder.addTarget(mSurface); - CaptureRequest request = builder.build(); - mSession.capture(request, null, mHandler); - mFlashlightRequest = request; - } - } else { - if (mCameraDevice != null) { - mCameraDevice.close(); - teardown(); - } - } - - } catch (CameraAccessException|IllegalStateException|UnsupportedOperationException e) { - Log.e(TAG, "Error in updateFlashlight", e); - handleError(); - } - } - - private void teardown() { - mCameraDevice = null; - mSession = null; - mFlashlightRequest = null; - if (mSurface != null) { - mSurface.release(); - mSurfaceTexture.release(); - } - mSurface = null; - mSurfaceTexture = null; - } - - private void handleError() { - synchronized (this) { - mFlashlightEnabled = false; - } - dispatchError(); - dispatchOff(); - updateFlashlight(true /* forceDisable */); - } - - private void dispatchOff() { - dispatchListeners(DISPATCH_OFF, false /* argument (ignored) */); + private void dispatchModeChanged(boolean enabled) { + dispatchListeners(DISPATCH_CHANGED, enabled); } private void dispatchError() { - dispatchListeners(DISPATCH_ERROR, false /* argument (ignored) */); + dispatchListeners(DISPATCH_CHANGED, false /* argument (ignored) */); } private void dispatchAvailabilityChanged(boolean available) { @@ -256,8 +153,8 @@ public class FlashlightController { if (l != null) { if (message == DISPATCH_ERROR) { l.onFlashlightError(); - } else if (message == DISPATCH_OFF) { - l.onFlashlightOff(); + } else if (message == DISPATCH_CHANGED) { + l.onFlashlightChanged(argument); } else if (message == DISPATCH_AVAILABILITY_CHANGED) { l.onFlashlightAvailabilityChanged(argument); } @@ -280,106 +177,57 @@ public class FlashlightController { } } - private final CameraDevice.StateListener mCameraListener = new CameraDevice.StateListener() { - @Override - public void onOpened(CameraDevice camera) { - mCameraDevice = camera; - postUpdateFlashlight(); - } - - @Override - public void onDisconnected(CameraDevice camera) { - if (mCameraDevice == camera) { - dispatchOff(); - teardown(); - } - } - - @Override - public void onError(CameraDevice camera, int error) { - Log.e(TAG, "Camera error: camera=" + camera + " error=" + error); - if (camera == mCameraDevice || mCameraDevice == null) { - handleError(); - } - } - }; + private final CameraManager.TorchCallback mTorchCallback = + new CameraManager.TorchCallback() { - private final CameraCaptureSession.StateListener mSessionListener = - new CameraCaptureSession.StateListener() { @Override - public void onConfigured(CameraCaptureSession session) { - if (session.getDevice() == mCameraDevice) { - mSession = session; - } else { - session.close(); - } - postUpdateFlashlight(); - } - - @Override - public void onConfigureFailed(CameraCaptureSession session) { - Log.e(TAG, "Configure failed."); - if (mSession == null || mSession == session) { - handleError(); - } - } - }; - - private final Runnable mUpdateFlashlightRunnable = new Runnable() { - @Override - public void run() { - updateFlashlight(false /* forceDisable */); - } - }; - - private final Runnable mKillFlashlightRunnable = new Runnable() { - @Override - public void run() { - synchronized (this) { - mFlashlightEnabled = false; + public void onTorchModeUnavailable(String cameraId) { + if (TextUtils.equals(cameraId, mCameraId)) { + setCameraAvailable(false); } - updateFlashlight(true /* forceDisable */); - dispatchOff(); } - }; - private final CameraManager.AvailabilityCallback mAvailabilityCallback = - new CameraManager.AvailabilityCallback() { @Override - public void onCameraAvailable(String cameraId) { - if (DEBUG) Log.d(TAG, "onCameraAvailable(" + cameraId + ")"); - if (cameraId.equals(mCameraId)) { + public void onTorchModeChanged(String cameraId, boolean enabled) { + if (TextUtils.equals(cameraId, mCameraId)) { setCameraAvailable(true); - } - } - - @Override - public void onCameraUnavailable(String cameraId) { - if (DEBUG) Log.d(TAG, "onCameraUnavailable(" + cameraId + ")"); - if (cameraId.equals(mCameraId)) { - setCameraAvailable(false); + setTorchMode(enabled); } } private void setCameraAvailable(boolean available) { boolean changed; synchronized (FlashlightController.this) { - changed = mCameraAvailable != available; - mCameraAvailable = available; + changed = mTorchAvailable != available; + mTorchAvailable = available; } if (changed) { if (DEBUG) Log.d(TAG, "dispatchAvailabilityChanged(" + available + ")"); dispatchAvailabilityChanged(available); } } + + private void setTorchMode(boolean enabled) { + boolean changed; + synchronized (FlashlightController.this) { + changed = mFlashlightEnabled != enabled; + mFlashlightEnabled = enabled; + } + if (changed) { + if (DEBUG) Log.d(TAG, "dispatchModeChanged(" + enabled + ")"); + dispatchModeChanged(enabled); + } + } }; public interface FlashlightListener { /** - * Called when the flashlight turns off unexpectedly. + * Called when the flashlight was turned off or on. + * @param enabled true if the flashlight is currently turned on. */ - void onFlashlightOff(); + void onFlashlightChanged(boolean enabled); + /** * Called when there is an error that turns the flashlight off. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java new file mode 100644 index 0000000..63f5711 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -0,0 +1,631 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.SystemClock; +import android.provider.Settings; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pools; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityEvent; + +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.R; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.NotificationData; +import com.android.systemui.statusbar.phone.PhoneStatusBar; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Stack; +import java.util.TreeSet; + +/** + * A manager which handles heads up notifications which is a special mode where + * they simply peek from the top of the screen. + */ +public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener { + private static final String TAG = "HeadsUpManager"; + private static final boolean DEBUG = false; + private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; + + private final int mHeadsUpNotificationDecay; + private final int mMinimumDisplayTime; + + private final int mTouchAcceptanceDelay; + private final ArrayMap<String, Long> mSnoozedPackages; + private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>(); + private final int mDefaultSnoozeLengthMs; + private final Handler mHandler = new Handler(); + private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() { + + private Stack<HeadsUpEntry> mPoolObjects = new Stack<>(); + + @Override + public HeadsUpEntry acquire() { + if (!mPoolObjects.isEmpty()) { + return mPoolObjects.pop(); + } + return new HeadsUpEntry(); + } + + @Override + public boolean release(HeadsUpEntry instance) { + instance.reset(); + mPoolObjects.push(instance); + return true; + } + }; + + private final View mStatusBarWindowView; + private final int mStatusBarHeight; + private final int mNotificationsTopPadding; + private final Context mContext; + private PhoneStatusBar mBar; + private int mSnoozeLengthMs; + private ContentObserver mSettingsObserver; + private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>(); + private TreeSet<HeadsUpEntry> mSortedEntries = new TreeSet<>(); + private HashSet<String> mSwipedOutKeys = new HashSet<>(); + private int mUser; + private Clock mClock; + private boolean mReleaseOnExpandFinish; + private boolean mTrackingHeadsUp; + private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>(); + private boolean mIsExpanded; + private boolean mHasPinnedNotification; + private int[] mTmpTwoArray = new int[2]; + private boolean mHeadsUpGoingAway; + private boolean mWaitingOnCollapseWhenGoingAway; + private boolean mIsObserving; + + public HeadsUpManager(final Context context, View statusBarWindowView) { + mContext = context; + Resources resources = mContext.getResources(); + mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay); + mSnoozedPackages = new ArrayMap<>(); + mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms); + mSnoozeLengthMs = mDefaultSnoozeLengthMs; + mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time); + mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay); + mClock = new Clock(); + + mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(), + SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs); + mSettingsObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + final int packageSnoozeLengthMs = Settings.Global.getInt( + context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); + if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { + mSnoozeLengthMs = packageSnoozeLengthMs; + if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); + } + } + }; + context.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false, + mSettingsObserver); + mStatusBarWindowView = statusBarWindowView; + mStatusBarHeight = resources.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + mNotificationsTopPadding = context.getResources() + .getDimensionPixelSize(R.dimen.notifications_top_padding); + } + + private void updateTouchableRegionListener() { + boolean shouldObserve = mHasPinnedNotification || mHeadsUpGoingAway + || mWaitingOnCollapseWhenGoingAway; + if (shouldObserve == mIsObserving) { + return; + } + if (shouldObserve) { + mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this); + mStatusBarWindowView.requestLayout(); + } else { + mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + } + mIsObserving = shouldObserve; + } + + public void setBar(PhoneStatusBar bar) { + mBar = bar; + } + + public void addListener(OnHeadsUpChangedListener listener) { + mListeners.add(listener); + } + + public PhoneStatusBar getBar() { + return mBar; + } + + /** + * Called when posting a new notification to the heads up. + */ + public void showNotification(NotificationData.Entry headsUp) { + if (DEBUG) Log.v(TAG, "showNotification"); + MetricsLogger.count(mContext, "note_peek", 1); + addHeadsUpEntry(headsUp); + updateNotification(headsUp, true); + headsUp.setInterruption(); + } + + /** + * Called when updating or posting a notification to the heads up. + */ + public void updateNotification(NotificationData.Entry headsUp, boolean alert) { + if (DEBUG) Log.v(TAG, "updateNotification"); + + headsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */); + headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + + if (alert) { + HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key); + headsUpEntry.updateEntry(); + setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp)); + } + } + + private void addHeadsUpEntry(NotificationData.Entry entry) { + HeadsUpEntry headsUpEntry = mEntryPool.acquire(); + + // This will also add the entry to the sortedList + headsUpEntry.setEntry(entry); + mHeadsUpEntries.put(entry.key, headsUpEntry); + entry.row.setHeadsUp(true); + setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(entry)); + for (OnHeadsUpChangedListener listener : mListeners) { + listener.onHeadsUpStateChanged(entry, true); + } + entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + + private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) { + return !mIsExpanded || hasFullScreenIntent(entry); + } + + private boolean hasFullScreenIntent(NotificationData.Entry entry) { + return entry.notification.getNotification().fullScreenIntent != null; + } + + private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) { + ExpandableNotificationRow row = headsUpEntry.entry.row; + if (row.isPinned() != isPinned) { + row.setPinned(isPinned); + updatePinnedMode(); + for (OnHeadsUpChangedListener listener : mListeners) { + if (isPinned) { + listener.onHeadsUpPinned(row); + } else { + listener.onHeadsUpUnPinned(row); + } + } + } + } + + private void removeHeadsUpEntry(NotificationData.Entry entry) { + HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key); + mSortedEntries.remove(remove); + entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + entry.row.setHeadsUp(false); + setEntryPinned(remove, false /* isPinned */); + for (OnHeadsUpChangedListener listener : mListeners) { + listener.onHeadsUpStateChanged(entry, false); + } + mEntryPool.release(remove); + } + + private void updatePinnedMode() { + boolean hasPinnedNotification = hasPinnedNotificationInternal(); + if (hasPinnedNotification == mHasPinnedNotification) { + return; + } + mHasPinnedNotification = hasPinnedNotification; + updateTouchableRegionListener(); + for (OnHeadsUpChangedListener listener : mListeners) { + listener.onHeadsUpPinnedModeChanged(hasPinnedNotification); + } + } + + /** + * React to the removal of the notification in the heads up. + * + * @return true if the notification was removed and false if it still needs to be kept around + * for a bit since it wasn't shown long enough + */ + public boolean removeNotification(String key) { + if (DEBUG) Log.v(TAG, "remove"); + if (wasShownLongEnough(key)) { + releaseImmediately(key); + return true; + } else { + getHeadsUpEntry(key).removeAsSoonAsPossible(); + return false; + } + } + + private boolean wasShownLongEnough(String key) { + HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); + HeadsUpEntry topEntry = getTopEntry(); + if (mSwipedOutKeys.contains(key)) { + // We always instantly dismiss views being manually swiped out. + mSwipedOutKeys.remove(key); + return true; + } + if (headsUpEntry != topEntry) { + return true; + } + return headsUpEntry.wasShownLongEnough(); + } + + public boolean isHeadsUp(String key) { + return mHeadsUpEntries.containsKey(key); + } + + /** + * Push any current Heads Up notification down into the shade. + */ + public void releaseAllImmediately() { + if (DEBUG) Log.v(TAG, "releaseAllImmediately"); + ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet()); + for (String key : keys) { + releaseImmediately(key); + } + } + + public void releaseImmediately(String key) { + HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); + if (headsUpEntry == null) { + return; + } + NotificationData.Entry shadeEntry = headsUpEntry.entry; + removeHeadsUpEntry(shadeEntry); + } + + public boolean isSnoozed(String packageName) { + final String key = snoozeKey(packageName, mUser); + Long snoozedUntil = mSnoozedPackages.get(key); + if (snoozedUntil != null) { + if (snoozedUntil > SystemClock.elapsedRealtime()) { + if (DEBUG) Log.v(TAG, key + " snoozed"); + return true; + } + mSnoozedPackages.remove(packageName); + } + return false; + } + + public void snooze() { + for (String key : mHeadsUpEntries.keySet()) { + HeadsUpEntry entry = mHeadsUpEntries.get(key); + String packageName = entry.entry.notification.getPackageName(); + mSnoozedPackages.put(snoozeKey(packageName, mUser), + SystemClock.elapsedRealtime() + mSnoozeLengthMs); + } + mReleaseOnExpandFinish = true; + } + + private static String snoozeKey(String packageName, int user) { + return user + "," + packageName; + } + + private HeadsUpEntry getHeadsUpEntry(String key) { + return mHeadsUpEntries.get(key); + } + + public NotificationData.Entry getEntry(String key) { + return mHeadsUpEntries.get(key).entry; + } + + public TreeSet<HeadsUpEntry> getSortedEntries() { + return mSortedEntries; + } + + public HeadsUpEntry getTopEntry() { + return mSortedEntries.isEmpty() ? null : mSortedEntries.first(); + } + + /** + * Decides whether a click is invalid for a notification, i.e it has not been shown long enough + * that a user might have consciously clicked on it. + * + * @param key the key of the touched notification + * @return whether the touch is invalid and should be discarded + */ + public boolean shouldSwallowClick(String key) { + HeadsUpEntry entry = mHeadsUpEntries.get(key); + if (entry != null && mClock.currentTimeMillis() < entry.postTime) { + return true; + } + return false; + } + + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { + if (mIsExpanded) { + // The touchable region is always the full area when expanded + return; + } + if (mHasPinnedNotification) { + int minX = Integer.MAX_VALUE; + int maxX = 0; + int minY = Integer.MAX_VALUE; + int maxY = 0; + for (HeadsUpEntry entry : mSortedEntries) { + ExpandableNotificationRow row = entry.entry.row; + if (row.isPinned()) { + row.getLocationOnScreen(mTmpTwoArray); + minX = Math.min(minX, mTmpTwoArray[0]); + minY = Math.min(minY, 0); + maxX = Math.max(maxX, mTmpTwoArray[0] + row.getWidth()); + maxY = Math.max(maxY, row.getHeadsUpHeight()); + } + } + + info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + info.touchableRegion.set(minX, minY, maxX, maxY + mNotificationsTopPadding); + } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) { + info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight); + } + } + + public void setUser(int user) { + mUser = user; + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("HeadsUpManager state:"); + pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay); + pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); + pw.print(" now="); pw.println(SystemClock.elapsedRealtime()); + pw.print(" mUser="); pw.println(mUser); + for (HeadsUpEntry entry: mSortedEntries) { + pw.print(" HeadsUpEntry="); pw.println(entry.entry); + } + int N = mSnoozedPackages.size(); + pw.println(" snoozed packages: " + N); + for (int i = 0; i < N; i++) { + pw.print(" "); pw.print(mSnoozedPackages.valueAt(i)); + pw.print(", "); pw.println(mSnoozedPackages.keyAt(i)); + } + } + + public boolean hasPinnedHeadsUp() { + return mHasPinnedNotification; + } + + private boolean hasPinnedNotificationInternal() { + for (String key : mHeadsUpEntries.keySet()) { + HeadsUpEntry entry = mHeadsUpEntries.get(key); + if (entry.entry.row.isPinned()) { + return true; + } + } + return false; + } + + /** + * Notifies that a notification was swiped out and will be removed. + * + * @param key the notification key + */ + public void addSwipedOutNotification(String key) { + mSwipedOutKeys.add(key); + } + + public void unpinAll() { + for (String key : mHeadsUpEntries.keySet()) { + HeadsUpEntry entry = mHeadsUpEntries.get(key); + setEntryPinned(entry, false /* isPinned */); + } + } + + public void onExpandingFinished() { + if (mReleaseOnExpandFinish) { + releaseAllImmediately(); + mReleaseOnExpandFinish = false; + } else { + for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) { + removeHeadsUpEntry(entry); + } + mEntriesToRemoveAfterExpand.clear(); + } + } + + public void setTrackingHeadsUp(boolean trackingHeadsUp) { + mTrackingHeadsUp = trackingHeadsUp; + } + + public void setIsExpanded(boolean isExpanded) { + if (isExpanded != mIsExpanded) { + mIsExpanded = isExpanded; + if (isExpanded) { + // make sure our state is sane + mWaitingOnCollapseWhenGoingAway = false; + mHeadsUpGoingAway = false; + updateTouchableRegionListener(); + } + } + } + + public int getTopHeadsUpHeight() { + HeadsUpEntry topEntry = getTopEntry(); + return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0; + } + + /** + * Compare two entries and decide how they should be ranked. + * + * @return -1 if the first argument should be ranked higher than the second, 1 if the second + * one should be ranked higher and 0 if they are equal. + */ + public int compare(NotificationData.Entry a, NotificationData.Entry b) { + HeadsUpEntry aEntry = getHeadsUpEntry(a.key); + HeadsUpEntry bEntry = getHeadsUpEntry(b.key); + if (aEntry == null || bEntry == null) { + return aEntry == null ? 1 : -1; + } + return aEntry.compareTo(bEntry); + } + + /** + * Set that we are exiting the headsUp pinned mode, but some notifications might still be + * animating out. This is used to keep the touchable regions in a sane state. + */ + public void setHeadsUpGoingAway(boolean headsUpGoingAway) { + if (headsUpGoingAway != mHeadsUpGoingAway) { + mHeadsUpGoingAway = headsUpGoingAway; + if (!headsUpGoingAway) { + waitForStatusBarLayout(); + } + updateTouchableRegionListener(); + } + } + + /** + * We need to wait on the whole panel to collapse, before we can remove the touchable region + * listener. + */ + private void waitForStatusBarLayout() { + mWaitingOnCollapseWhenGoingAway = true; + mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, + int oldTop, int oldRight, int oldBottom) { + if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) { + mStatusBarWindowView.removeOnLayoutChangeListener(this); + mWaitingOnCollapseWhenGoingAway = false; + updateTouchableRegionListener(); + } + } + }); + } + + /** + * This represents a notification and how long it is in a heads up mode. It also manages its + * lifecycle automatically when created. + */ + public class HeadsUpEntry implements Comparable<HeadsUpEntry> { + public NotificationData.Entry entry; + public long postTime; + public long earliestRemovaltime; + private Runnable mRemoveHeadsUpRunnable; + + public void setEntry(final NotificationData.Entry entry) { + this.entry = entry; + + // The actual post time will be just after the heads-up really slided in + postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay; + mRemoveHeadsUpRunnable = new Runnable() { + @Override + public void run() { + if (!mTrackingHeadsUp) { + removeHeadsUpEntry(entry); + } else { + mEntriesToRemoveAfterExpand.add(entry); + } + } + }; + updateEntry(); + } + + public void updateEntry() { + mSortedEntries.remove(HeadsUpEntry.this); + long currentTime = mClock.currentTimeMillis(); + earliestRemovaltime = currentTime + mMinimumDisplayTime; + postTime = Math.max(postTime, currentTime); + removeAutoRemovalCallbacks(); + if (!hasFullScreenIntent(entry)) { + long finishTime = postTime + mHeadsUpNotificationDecay; + long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime); + mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay); + } + mSortedEntries.add(HeadsUpEntry.this); + } + + @Override + public int compareTo(HeadsUpEntry o) { + return postTime < o.postTime ? 1 + : postTime == o.postTime ? entry.key.compareTo(o.entry.key) + : -1; + } + + public void removeAutoRemovalCallbacks() { + mHandler.removeCallbacks(mRemoveHeadsUpRunnable); + } + + public boolean wasShownLongEnough() { + return earliestRemovaltime < mClock.currentTimeMillis(); + } + + public void removeAsSoonAsPossible() { + removeAutoRemovalCallbacks(); + mHandler.postDelayed(mRemoveHeadsUpRunnable, + earliestRemovaltime - mClock.currentTimeMillis()); + } + + public void reset() { + removeAutoRemovalCallbacks(); + entry = null; + mRemoveHeadsUpRunnable = null; + } + } + + public static class Clock { + public long currentTimeMillis() { + return SystemClock.elapsedRealtime(); + } + } + + public interface OnHeadsUpChangedListener { + /** + * The state whether there exist pinned heads-ups or not changed. + * + * @param inPinnedMode whether there are any pinned heads-ups + */ + void onHeadsUpPinnedModeChanged(boolean inPinnedMode); + + /** + * A notification was just pinned to the top. + */ + void onHeadsUpPinned(ExpandableNotificationRow headsUp); + + /** + * A notification was just unpinned from the top. + */ + void onHeadsUpUnPinned(ExpandableNotificationRow headsUp); + + /** + * A notification just became a heads up or turned back to its normal state. + * + * @param entry the entry of the changed notification + * @param isHeadsUp whether the notification is now a headsUp notification + */ + void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java deleted file mode 100644 index 2e3e67a..0000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java +++ /dev/null @@ -1,495 +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.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.graphics.Outline; -import android.graphics.Rect; -import android.os.SystemClock; -import android.provider.Settings; -import android.util.ArrayMap; -import android.util.AttributeSet; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.ViewTreeObserver; -import android.view.accessibility.AccessibilityEvent; -import android.widget.FrameLayout; - -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.ExpandableView; -import com.android.systemui.statusbar.NotificationData; -import com.android.systemui.statusbar.phone.PhoneStatusBar; - -import java.util.ArrayList; - -public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper.Callback, ExpandHelper.Callback, - ViewTreeObserver.OnComputeInternalInsetsListener { - private static final String TAG = "HeadsUpNotificationView"; - private static final boolean DEBUG = false; - private static final boolean SPEW = DEBUG; - private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; - - Rect mTmpRect = new Rect(); - int[] mTmpTwoArray = new int[2]; - - private final int mTouchSensitivityDelay; - private final float mMaxAlpha = 1f; - private final ArrayMap<String, Long> mSnoozedPackages; - private final int mDefaultSnoozeLengthMs; - - private SwipeHelper mSwipeHelper; - private EdgeSwipeHelper mEdgeSwipeHelper; - - private PhoneStatusBar mBar; - - private long mStartTouchTime; - private ViewGroup mContentHolder; - private int mSnoozeLengthMs; - private ContentObserver mSettingsObserver; - - private NotificationData.Entry mHeadsUp; - private int mUser; - private String mMostRecentPackageName; - - public HeadsUpNotificationView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public HeadsUpNotificationView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - Resources resources = context.getResources(); - mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay); - if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay); - mSnoozedPackages = new ArrayMap<>(); - mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms); - mSnoozeLengthMs = mDefaultSnoozeLengthMs; - } - - public void updateResources() { - if (mContentHolder != null) { - final LayoutParams lp = (LayoutParams) mContentHolder.getLayoutParams(); - lp.width = getResources().getDimensionPixelSize(R.dimen.notification_panel_width); - lp.gravity = getResources().getInteger(R.integer.notification_panel_layout_gravity); - mContentHolder.setLayoutParams(lp); - } - } - - public void setBar(PhoneStatusBar bar) { - mBar = bar; - } - - public ViewGroup getHolder() { - return mContentHolder; - } - - public boolean showNotification(NotificationData.Entry headsUp) { - if (mHeadsUp != null && headsUp != null && !mHeadsUp.key.equals(headsUp.key)) { - // bump any previous heads up back to the shade - release(); - } - - mHeadsUp = headsUp; - if (mContentHolder != null) { - mContentHolder.removeAllViews(); - } - - if (mHeadsUp != null) { - mMostRecentPackageName = mHeadsUp.notification.getPackageName(); - mHeadsUp.row.setSystemExpanded(true); - mHeadsUp.row.setSensitive(false); - mHeadsUp.row.setHeadsUp(true); - mHeadsUp.row.setHideSensitive( - false, false /* animated */, 0 /* delay */, 0 /* duration */); - if (mContentHolder == null) { - // too soon! - return false; - } - mContentHolder.setX(0); - mContentHolder.setVisibility(View.VISIBLE); - mContentHolder.setAlpha(mMaxAlpha); - mContentHolder.addView(mHeadsUp.row); - sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - - mSwipeHelper.snapChild(mContentHolder, 1f); - mStartTouchTime = SystemClock.elapsedRealtime() + mTouchSensitivityDelay; - - mHeadsUp.setInterruption(); - - // 2. Animate mHeadsUpNotificationView in - mBar.scheduleHeadsUpOpen(); - - // 3. Set alarm to age the notification off - mBar.resetHeadsUpDecayTimer(); - } - return true; - } - - @Override - protected void onVisibilityChanged(View changedView, int visibility) { - super.onVisibilityChanged(changedView, visibility); - if (changedView.getVisibility() == VISIBLE) { - sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - } - } - - public boolean isShowing(String key) { - return mHeadsUp != null && mHeadsUp.key.equals(key); - } - - /** Discard the Heads Up notification. */ - public void clear() { - mHeadsUp = null; - mBar.scheduleHeadsUpClose(); - } - - /** Respond to dismissal of the Heads Up window. */ - public void dismiss() { - if (mHeadsUp == null) return; - if (mHeadsUp.notification.isClearable()) { - mBar.onNotificationClear(mHeadsUp.notification); - } else { - release(); - } - mHeadsUp = null; - mBar.scheduleHeadsUpClose(); - } - - /** Push any current Heads Up notification down into the shade. */ - public void release() { - if (mHeadsUp != null) { - mBar.displayNotificationFromHeadsUp(mHeadsUp.notification); - } - mHeadsUp = null; - } - - public boolean isSnoozed(String packageName) { - final String key = snoozeKey(packageName, mUser); - Long snoozedUntil = mSnoozedPackages.get(key); - if (snoozedUntil != null) { - if (snoozedUntil > SystemClock.elapsedRealtime()) { - if (DEBUG) Log.v(TAG, key + " snoozed"); - return true; - } - mSnoozedPackages.remove(packageName); - } - return false; - } - - private void snooze() { - if (mMostRecentPackageName != null) { - mSnoozedPackages.put(snoozeKey(mMostRecentPackageName, mUser), - SystemClock.elapsedRealtime() + mSnoozeLengthMs); - } - releaseAndClose(); - } - - private static String snoozeKey(String packageName, int user) { - return user + "," + packageName; - } - - public void releaseAndClose() { - release(); - mBar.scheduleHeadsUpClose(); - } - - public NotificationData.Entry getEntry() { - return mHeadsUp; - } - - public boolean isClearable() { - return mHeadsUp == null || mHeadsUp.notification.isClearable(); - } - - // ViewGroup methods - - private static final ViewOutlineProvider CONTENT_HOLDER_OUTLINE_PROVIDER = - new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - int outlineLeft = view.getPaddingLeft(); - int outlineTop = view.getPaddingTop(); - - // Apply padding to shadow. - outline.setRect(outlineLeft, outlineTop, - view.getWidth() - outlineLeft - view.getPaddingRight(), - view.getHeight() - outlineTop - view.getPaddingBottom()); - } - }; - - @Override - public void onAttachedToWindow() { - final ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); - float touchSlop = viewConfiguration.getScaledTouchSlop(); - mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, getContext()); - mSwipeHelper.setMaxSwipeProgress(mMaxAlpha); - mEdgeSwipeHelper = new EdgeSwipeHelper(touchSlop); - - int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); - int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height); - - mContentHolder = (ViewGroup) findViewById(R.id.content_holder); - mContentHolder.setOutlineProvider(CONTENT_HOLDER_OUTLINE_PROVIDER); - - mSnoozeLengthMs = Settings.Global.getInt(mContext.getContentResolver(), - SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs); - mSettingsObserver = new ContentObserver(getHandler()) { - @Override - public void onChange(boolean selfChange) { - final int packageSnoozeLengthMs = Settings.Global.getInt( - mContext.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); - if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { - mSnoozeLengthMs = packageSnoozeLengthMs; - if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); - } - } - }; - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false, - mSettingsObserver); - if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); - - if (mHeadsUp != null) { - // whoops, we're on already! - showNotification(mHeadsUp); - } - - getViewTreeObserver().addOnComputeInternalInsetsListener(this); - } - - @Override - protected void onDetachedFromWindow() { - mContext.getContentResolver().unregisterContentObserver(mSettingsObserver); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); - if (SystemClock.elapsedRealtime() < mStartTouchTime) { - return true; - } - return mEdgeSwipeHelper.onInterceptTouchEvent(ev) - || mSwipeHelper.onInterceptTouchEvent(ev) - || super.onInterceptTouchEvent(ev); - } - - // View methods - - @Override - public void onDraw(android.graphics.Canvas c) { - super.onDraw(c); - 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(0xFFcc00cc); - c.restore(); - } - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (SystemClock.elapsedRealtime() < mStartTouchTime) { - return false; - } - mBar.resetHeadsUpDecayTimer(); - return mEdgeSwipeHelper.onTouchEvent(ev) - || mSwipeHelper.onTouchEvent(ev) - || super.onTouchEvent(ev); - } - - @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); - } - - // ExpandHelper.Callback methods - - @Override - public ExpandableView getChildAtRawPosition(float x, float y) { - return getChildAtPosition(x, y); - } - - @Override - public ExpandableView getChildAtPosition(float x, float y) { - return mHeadsUp == null ? null : mHeadsUp.row; - } - - @Override - public boolean canChildBeExpanded(View v) { - return mHeadsUp != null && mHeadsUp.row == v && mHeadsUp.row.isExpandable(); - } - - @Override - public void setUserExpandedChild(View v, boolean userExpanded) { - if (mHeadsUp != null && mHeadsUp.row == v) { - mHeadsUp.row.setUserExpanded(userExpanded); - } - } - - @Override - public void setUserLockedChild(View v, boolean userLocked) { - if (mHeadsUp != null && mHeadsUp.row == v) { - mHeadsUp.row.setUserLocked(userLocked); - } - } - - @Override - public void expansionStateChanged(boolean isExpanding) { - - } - - // SwipeHelper.Callback methods - - @Override - public boolean canChildBeDismissed(View v) { - return true; - } - - @Override - public boolean isAntiFalsingNeeded() { - return false; - } - - @Override - public float getFalsingThresholdFactor() { - return 1.0f; - } - - @Override - public void onChildDismissed(View v) { - Log.v(TAG, "User swiped heads up to dismiss"); - mBar.onHeadsUpDismissed(); - } - - @Override - public void onBeginDrag(View v) { - } - - @Override - public void onDragCancelled(View v) { - mContentHolder.setAlpha(mMaxAlpha); // sometimes this isn't quite reset - } - - @Override - public void onChildSnappedBack(View animView) { - } - - @Override - public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { - getBackground().setAlpha((int) (255 * swipeProgress)); - return false; - } - - @Override - public View getChildAtPosition(MotionEvent ev) { - return mContentHolder; - } - - @Override - public View getChildContentView(View v) { - return mContentHolder; - } - - @Override - public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { - mContentHolder.getLocationOnScreen(mTmpTwoArray); - - info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - info.touchableRegion.set(mTmpTwoArray[0], mTmpTwoArray[1], - mTmpTwoArray[0] + mContentHolder.getWidth(), - mTmpTwoArray[1] + mContentHolder.getHeight()); - } - - public void escalate() { - mBar.scheduleHeadsUpEscalation(); - } - - public String getKey() { - return mHeadsUp == null ? null : mHeadsUp.notification.getKey(); - } - - public void setUser(int user) { - mUser = user; - } - - private class EdgeSwipeHelper implements Gefingerpoken { - private static final boolean DEBUG_EDGE_SWIPE = false; - private final float mTouchSlop; - 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 && daX < daY && daY > mTouchSlop) { - snooze(); - if (dY > 0) { - if (DEBUG_EDGE_SWIPE) Log.d(TAG, "found an open"); - mBar.animateExpandNotificationsPanel(); - } - 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; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java index 0863c86..7ca91a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java @@ -22,7 +22,6 @@ public interface HotspotController { boolean isHotspotEnabled(); boolean isHotspotSupported(); void setHotspotEnabled(boolean enabled); - boolean isProvisioningNeeded(); public interface Callback { void onHotspotChanged(boolean enabled); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java index 5eff5a6..4bfd528 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java @@ -16,45 +16,38 @@ package com.android.systemui.statusbar.policy; -import android.app.ActivityManager; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.net.ConnectivityManager; import android.net.wifi.WifiManager; -import android.os.SystemProperties; import android.os.UserHandle; -import android.provider.Settings; import android.util.Log; +import com.android.settingslib.TetherUtil; + import java.util.ArrayList; public class HotspotControllerImpl implements HotspotController { private static final String TAG = "HotspotController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - // Keep these in sync with Settings TetherService.java - public static final String EXTRA_ADD_TETHER_TYPE = "extraAddTetherType"; - public static final String EXTRA_SET_ALARM = "extraSetAlarm"; - public static final String EXTRA_RUN_PROVISION = "extraRunProvision"; - public static final String EXTRA_ENABLE_WIFI_TETHER = "extraEnableWifiTether"; - // Keep this in sync with Settings TetherSettings.java - public static final int WIFI_TETHERING = 0; + private static final Intent TETHER_SERVICE_INTENT = new Intent() + .putExtra(TetherUtil.EXTRA_ADD_TETHER_TYPE, TetherUtil.TETHERING_WIFI) + .putExtra(TetherUtil.EXTRA_SET_ALARM, true) + .putExtra(TetherUtil.EXTRA_RUN_PROVISION, true) + .putExtra(TetherUtil.EXTRA_ENABLE_WIFI_TETHER, true) + .setComponent(TetherUtil.TETHER_SERVICE); private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); private final Receiver mReceiver = new Receiver(); private final Context mContext; private final WifiManager mWifiManager; - private final ConnectivityManager mConnectivityManager; public HotspotControllerImpl(Context context) { mContext = context; mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - mConnectivityManager = (ConnectivityManager) - mContext.getSystemService(Context.CONNECTIVITY_SERVICE); } public void addCallback(Callback callback) { @@ -78,54 +71,17 @@ public class HotspotControllerImpl implements HotspotController { @Override public boolean isHotspotSupported() { - final boolean isSecondaryUser = ActivityManager.getCurrentUser() != UserHandle.USER_OWNER; - return !isSecondaryUser && mConnectivityManager.isTetheringSupported(); - } - - @Override - public boolean isProvisioningNeeded() { - // Keep in sync with other usage of config_mobile_hotspot_provision_app. - // TetherSettings#isProvisioningNeeded and - // ConnectivityManager#enforceTetherChangePermission - String[] provisionApp = mContext.getResources().getStringArray( - com.android.internal.R.array.config_mobile_hotspot_provision_app); - if (SystemProperties.getBoolean("net.tethering.noprovisioning", false) - || provisionApp == null) { - return false; - } - return (provisionApp.length == 2); + return TetherUtil.isTetheringSupported(mContext); } @Override public void setHotspotEnabled(boolean enabled) { final ContentResolver cr = mContext.getContentResolver(); // Call provisioning app which is called when enabling Tethering from Settings - if (enabled) { - if (isProvisioningNeeded()) { - String tetherEnable = mContext.getResources().getString( - com.android.internal.R.string.config_wifi_tether_enable); - Intent intent = new Intent(); - intent.putExtra(EXTRA_ADD_TETHER_TYPE, WIFI_TETHERING); - intent.putExtra(EXTRA_SET_ALARM, true); - intent.putExtra(EXTRA_RUN_PROVISION, true); - intent.putExtra(EXTRA_ENABLE_WIFI_TETHER, true); - intent.setComponent(ComponentName.unflattenFromString(tetherEnable)); - mContext.startServiceAsUser(intent, UserHandle.CURRENT); - } else { - int wifiState = mWifiManager.getWifiState(); - if ((wifiState == WifiManager.WIFI_STATE_ENABLING) || - (wifiState == WifiManager.WIFI_STATE_ENABLED)) { - mWifiManager.setWifiEnabled(false); - Settings.Global.putInt(cr, Settings.Global.WIFI_SAVED_STATE, 1); - } - mWifiManager.setWifiApEnabled(null, true); - } + if (enabled && TetherUtil.isProvisioningNeeded(mContext)) { + mContext.startServiceAsUser(TETHER_SERVICE_INTENT, UserHandle.CURRENT); } else { - mWifiManager.setWifiApEnabled(null, false); - if (Settings.Global.getInt(cr, Settings.Global.WIFI_SAVED_STATE, 0) == 1) { - mWifiManager.setWifiEnabled(true); - Settings.Global.putInt(cr, Settings.Global.WIFI_SAVED_STATE, 0); - } + TetherUtil.setWifiTethering(enabled, mContext); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java index 6998791..3f63b5f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java @@ -26,7 +26,7 @@ import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; -import android.view.HardwareCanvas; +import android.view.DisplayListCanvas; import android.view.RenderNodeAnimator; import android.view.View; import android.view.animation.Interpolator; @@ -106,7 +106,7 @@ public class KeyButtonRipple extends Drawable { public void draw(Canvas canvas) { mSupportHardware = canvas.isHardwareAccelerated(); if (mSupportHardware) { - drawHardware((HardwareCanvas) canvas); + drawHardware((DisplayListCanvas) canvas); } else { drawSoftware(canvas); } @@ -118,7 +118,7 @@ public class KeyButtonRipple extends Drawable { } @Override - public void setColorFilter(ColorFilter cf) { + public void setColorFilter(ColorFilter colorFilter) { // Not supported. } @@ -131,7 +131,7 @@ public class KeyButtonRipple extends Drawable { return getBounds().width() > getBounds().height(); } - private void drawHardware(HardwareCanvas c) { + private void drawHardware(DisplayListCanvas c) { if (mDrawingHardwareGlow) { c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp, mPaintProp); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java index b9cc0f9..4c99792 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.policy; -import android.animation.Animator; -import android.animation.ObjectAnimator; import android.app.ActivityManager; import android.content.Context; import android.content.res.TypedArray; @@ -26,7 +24,6 @@ import android.media.AudioManager; import android.os.Bundle; import android.os.SystemClock; import android.util.AttributeSet; -import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.InputDevice; import android.view.KeyCharacterMap; @@ -45,20 +42,13 @@ import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; public class KeyButtonView extends ImageView { - private static final String TAG = "StatusBar.KeyButtonView"; - private static final boolean DEBUG = false; - - // TODO: Get rid of this - public static final float DEFAULT_QUIESCENT_ALPHA = 1f; private long mDownTime; private int mCode; private int mTouchSlop; - private float mDrawingAlpha = 1f; - private float mQuiescentAlpha = DEFAULT_QUIESCENT_ALPHA; private boolean mSupportsLongpress = true; private AudioManager mAudioManager; - private Animator mAnimateToQuiescent = new ObjectAnimator(); + private boolean mGestureAborted; private final Runnable mCheckLongPress = new Runnable() { public void run() { @@ -67,7 +57,7 @@ public class KeyButtonView extends ImageView { if (isLongClickable()) { // Just an old-fashioned ImageView performLongClick(); - } else { + } else if (mSupportsLongpress) { sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); } @@ -89,9 +79,6 @@ public class KeyButtonView extends ImageView { mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true); - - setDrawingAlpha(mQuiescentAlpha); - a.recycle(); setClickable(true); @@ -105,7 +92,7 @@ public class KeyButtonView extends ImageView { super.onInitializeAccessibilityNodeInfo(info); if (mCode != 0) { info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null)); - if (mSupportsLongpress) { + if (mSupportsLongpress || isLongClickable()) { info.addAction( new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null)); } @@ -121,60 +108,34 @@ public class KeyButtonView extends ImageView { } @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { if (action == ACTION_CLICK && mCode != 0) { sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis()); sendEvent(KeyEvent.ACTION_UP, 0); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); playSoundEffect(SoundEffectConstants.CLICK); return true; - } else if (action == ACTION_LONG_CLICK && mCode != 0 && mSupportsLongpress) { + } else if (action == ACTION_LONG_CLICK && mCode != 0) { sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); sendEvent(KeyEvent.ACTION_UP, 0); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); return true; } - return super.performAccessibilityAction(action, arguments); - } - - public void setQuiescentAlpha(float alpha, boolean animate) { - mAnimateToQuiescent.cancel(); - alpha = Math.min(Math.max(alpha, 0), 1); - if (alpha == mQuiescentAlpha && alpha == mDrawingAlpha) return; - mQuiescentAlpha = alpha; - if (DEBUG) Log.d(TAG, "New quiescent alpha = " + mQuiescentAlpha); - if (animate) { - mAnimateToQuiescent = animateToQuiescent(); - mAnimateToQuiescent.start(); - } else { - setDrawingAlpha(mQuiescentAlpha); - } - } - - private ObjectAnimator animateToQuiescent() { - return ObjectAnimator.ofFloat(this, "drawingAlpha", mQuiescentAlpha); - } - - public float getQuiescentAlpha() { - return mQuiescentAlpha; - } - - public float getDrawingAlpha() { - return mDrawingAlpha; - } - - public void setDrawingAlpha(float x) { - setImageAlpha((int) (x * 255)); - mDrawingAlpha = x; + return super.performAccessibilityActionInternal(action, arguments); } public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); int x, y; + if (action == MotionEvent.ACTION_DOWN) { + mGestureAborted = false; + } + if (mGestureAborted) { + return false; + } switch (action) { case MotionEvent.ACTION_DOWN: - //Log.d("KeyButtonView", "press"); mDownTime = SystemClock.uptimeMillis(); setPressed(true); if (mCode != 0) { @@ -183,10 +144,8 @@ public class KeyButtonView extends ImageView { // Provide the same haptic feedback that the system offers for virtual keys. performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); } - if (mSupportsLongpress) { - removeCallbacks(mCheckLongPress); - postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); - } + removeCallbacks(mCheckLongPress); + postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); break; case MotionEvent.ACTION_MOVE: x = (int)ev.getX(); @@ -201,9 +160,7 @@ public class KeyButtonView extends ImageView { if (mCode != 0) { sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); } - if (mSupportsLongpress) { - removeCallbacks(mCheckLongPress); - } + removeCallbacks(mCheckLongPress); break; case MotionEvent.ACTION_UP: final boolean doIt = isPressed(); @@ -222,9 +179,7 @@ public class KeyButtonView extends ImageView { performClick(); } } - if (mSupportsLongpress) { - removeCallbacks(mCheckLongPress); - } + removeCallbacks(mCheckLongPress); break; } @@ -248,6 +203,11 @@ public class KeyButtonView extends ImageView { InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } + + public void abortCurrentGesture() { + setPressed(false); + mGestureAborted = true; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java index 0b3575f..d4eb553 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java @@ -16,21 +16,59 @@ package com.android.systemui.statusbar.policy; +import android.app.ActivityManager; +import android.content.Context; + +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.settings.CurrentUserTracker; + import java.util.ArrayList; -public final class KeyguardMonitor { +public final class KeyguardMonitor extends KeyguardUpdateMonitorCallback { private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); + private final Context mContext; + private final CurrentUserTracker mUserTracker; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + + private int mCurrentUser; private boolean mShowing; private boolean mSecure; + private boolean mTrusted; + + private boolean mListening; + + public KeyguardMonitor(Context context) { + mContext = context; + mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); + mUserTracker = new CurrentUserTracker(mContext) { + @Override + public void onUserSwitched(int newUserId) { + mCurrentUser = newUserId; + updateTrustedState(); + } + }; + } public void addCallback(Callback callback) { mCallbacks.add(callback); + if (mCallbacks.size() != 0 && !mListening) { + mListening = true; + mCurrentUser = ActivityManager.getCurrentUser(); + updateTrustedState(); + mKeyguardUpdateMonitor.registerCallback(this); + mUserTracker.startTracking(); + } } public void removeCallback(Callback callback) { - mCallbacks.remove(callback); + if (mCallbacks.remove(callback) && mCallbacks.size() == 0 && mListening) { + mListening = false; + mKeyguardUpdateMonitor.removeCallback(this); + mUserTracker.stopTracking(); + } } public boolean isShowing() { @@ -41,10 +79,28 @@ public final class KeyguardMonitor { return mSecure; } + public boolean isTrusted() { + return mTrusted; + } + public void notifyKeyguardState(boolean showing, boolean secure) { if (mShowing == showing && mSecure == secure) return; mShowing = showing; mSecure = secure; + notifyKeyguardChanged(); + } + + @Override + public void onTrustChanged(int userId) { + updateTrustedState(); + notifyKeyguardChanged(); + } + + private void updateTrustedState() { + mTrusted = mKeyguardUpdateMonitor.getUserHasTrust(mCurrentUser); + } + + private void notifyKeyguardChanged() { for (Callback callback : mCallbacks) { callback.onKeyguardChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java index 1460e5f..5cf6156 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java @@ -30,7 +30,7 @@ import android.view.ViewStub; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; -import com.android.keyguard.AppearAnimationUtils; +import com.android.settingslib.animation.AppearAnimationUtils; import com.android.systemui.R; import com.android.systemui.qs.tiles.UserDetailItemView; import com.android.systemui.statusbar.phone.KeyguardStatusBarView; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java index a5fc2fe..353e07d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; -import android.graphics.LightingColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.RadialGradient; @@ -48,7 +47,7 @@ public class KeyguardUserSwitcherScrim extends Drawable public KeyguardUserSwitcherScrim(View host) { host.addOnLayoutChangeListener(this); - mDarkColor = host.getResources().getColor( + mDarkColor = host.getContext().getColor( R.color.keyguard_user_switcher_background_gradient_color); } @@ -77,7 +76,7 @@ public class KeyguardUserSwitcherScrim extends Drawable } @Override - public void setColorFilter(ColorFilter cf) { + public void setColorFilter(ColorFilter colorFilter) { } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java index d8d7042..93a8fd8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java @@ -26,6 +26,8 @@ import android.content.Intent; import android.content.IntentFilter; import android.location.LocationManager; import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -56,31 +58,21 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio private ArrayList<LocationSettingsChangeCallback> mSettingsChangeCallbacks = new ArrayList<LocationSettingsChangeCallback>(); + private final H mHandler = new H(); - public LocationControllerImpl(Context context) { + public LocationControllerImpl(Context context, Looper bgLooper) { mContext = context; + // Register to listen for changes in location settings. IntentFilter filter = new IntentFilter(); filter.addAction(LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION); - context.registerReceiverAsUser(this, UserHandle.ALL, filter, null, null); + filter.addAction(LocationManager.MODE_CHANGED_ACTION); + context.registerReceiverAsUser(this, UserHandle.ALL, filter, null, new Handler(bgLooper)); mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); mStatusBarManager = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE); - // Register to listen for changes in location settings. - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION); - context.registerReceiverAsUser(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (LocationManager.MODE_CHANGED_ACTION.equals(action)) { - locationSettingsChanged(); - } - } - }, UserHandle.ALL, intentFilter, null, new Handler()); - // Examine the current location state and initialize the status view. updateActiveLocationRequests(); refreshViews(); @@ -91,7 +83,7 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio */ public void addSettingsChangedCallback(LocationSettingsChangeCallback cb) { mSettingsChangeCallbacks.add(cb); - locationSettingsChanged(cb); + mHandler.sendEmptyMessage(H.MSG_LOCATION_SETTINGS_CHANGED); } public void removeSettingsChangedCallback(LocationSettingsChangeCallback cb) { @@ -181,8 +173,8 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio // Updates the status view based on the current state of location requests. private void refreshViews() { if (mAreActiveLocationRequests) { - mStatusBarManager.setIcon(LOCATION_STATUS_ICON_PLACEHOLDER, LOCATION_STATUS_ICON_ID, 0, - mContext.getString(R.string.accessibility_location_active)); + mStatusBarManager.setIcon(LOCATION_STATUS_ICON_PLACEHOLDER, LOCATION_STATUS_ICON_ID, + 0, mContext.getString(R.string.accessibility_location_active)); } else { mStatusBarManager.removeIcon(LOCATION_STATUS_ICON_PLACEHOLDER); } @@ -197,22 +189,33 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio } } - private void locationSettingsChanged() { - boolean isEnabled = isLocationEnabled(); - for (LocationSettingsChangeCallback cb : mSettingsChangeCallbacks) { - cb.onLocationSettingsChanged(isEnabled); - } - } - - private void locationSettingsChanged(LocationSettingsChangeCallback cb) { - cb.onLocationSettingsChanged(isLocationEnabled()); - } - @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION.equals(action)) { updateActiveLocationRequests(); + } else if (LocationManager.MODE_CHANGED_ACTION.equals(action)) { + mHandler.sendEmptyMessage(H.MSG_LOCATION_SETTINGS_CHANGED); + } + } + + private final class H extends Handler { + private static final int MSG_LOCATION_SETTINGS_CHANGED = 1; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LOCATION_SETTINGS_CHANGED: + locationSettingsChanged(); + break; + } + } + + private void locationSettingsChanged() { + boolean isEnabled = isLocationEnabled(); + for (LocationSettingsChangeCallback cb : mSettingsChangeCallbacks) { + cb.onLocationSettingsChanged(isEnabled); + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileDataControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileDataControllerImpl.java index f2b2f66..a7fdadc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileDataControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileDataControllerImpl.java @@ -162,7 +162,7 @@ public class MobileDataControllerImpl implements NetworkController.MobileDataCon usage.warningLevel = DEFAULT_WARNING_LEVEL; } if (usage != null) { - usage.carrier = mNetworkController.getMobileNetworkName(); + usage.carrier = mNetworkController.getMobileDataNetworkName(); } return usage; } catch (RemoteException e) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java new file mode 100644 index 0000000..b1c650e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy; + +import android.content.Context; +import android.content.Intent; +import android.net.NetworkCapabilities; +import android.os.Looper; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.telephony.cdma.EriInfo; +import com.android.systemui.R; +import com.android.systemui.statusbar.policy.NetworkController.IconState; +import com.android.systemui.statusbar.policy.NetworkControllerImpl.Config; +import com.android.systemui.statusbar.policy.NetworkControllerImpl.SubscriptionDefaults; + +import java.io.PrintWriter; +import java.util.BitSet; +import java.util.Objects; + + +public class MobileSignalController extends SignalController< + MobileSignalController.MobileState, MobileSignalController.MobileIconGroup> { + private final TelephonyManager mPhone; + private final SubscriptionDefaults mDefaults; + private final String mNetworkNameDefault; + private final String mNetworkNameSeparator; + @VisibleForTesting + final PhoneStateListener mPhoneStateListener; + // Save entire info for logging, we only use the id. + private final SubscriptionInfo mSubscriptionInfo; + + // @VisibleForDemoMode + final SparseArray<MobileIconGroup> mNetworkToIconLookup; + + // Since some pieces of the phone state are interdependent we store it locally, + // this could potentially become part of MobileState for simplification/complication + // of code. + private int mDataNetType = TelephonyManager.NETWORK_TYPE_UNKNOWN; + private int mDataState = TelephonyManager.DATA_DISCONNECTED; + private ServiceState mServiceState; + private SignalStrength mSignalStrength; + private MobileIconGroup mDefaultIcons; + private Config mConfig; + + // TODO: Reduce number of vars passed in, if we have the NetworkController, probably don't + // need listener lists anymore. + public MobileSignalController(Context context, Config config, boolean hasMobileData, + TelephonyManager phone, CallbackHandler callbackHandler, + NetworkControllerImpl networkController, SubscriptionInfo info, + SubscriptionDefaults defaults, Looper receiverLooper) { + super("MobileSignalController(" + info.getSubscriptionId() + ")", context, + NetworkCapabilities.TRANSPORT_CELLULAR, callbackHandler, + networkController); + mNetworkToIconLookup = new SparseArray<>(); + mConfig = config; + mPhone = phone; + mDefaults = defaults; + mSubscriptionInfo = info; + mPhoneStateListener = new MobilePhoneStateListener(info.getSubscriptionId(), + receiverLooper); + mNetworkNameSeparator = getStringIfExists(R.string.status_bar_network_name_separator); + mNetworkNameDefault = getStringIfExists( + com.android.internal.R.string.lockscreen_carrier_default); + + mapIconSets(); + + mLastState.networkName = mCurrentState.networkName = mNetworkNameDefault; + mLastState.networkNameData = mCurrentState.networkNameData = mNetworkNameDefault; + mLastState.enabled = mCurrentState.enabled = hasMobileData; + mLastState.iconGroup = mCurrentState.iconGroup = mDefaultIcons; + // Get initial data sim state. + updateDataSim(); + } + + public void setConfiguration(Config config) { + mConfig = config; + mapIconSets(); + updateTelephony(); + } + + public int getDataContentDescription() { + return getIcons().mDataContentDescription; + } + + public void setAirplaneMode(boolean airplaneMode) { + mCurrentState.airplaneMode = airplaneMode; + notifyListenersIfNecessary(); + } + + @Override + public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) { + boolean isValidated = validatedTransports.get(mTransportType); + mCurrentState.isDefault = connectedTransports.get(mTransportType); + // Only show this as not having connectivity if we are default. + mCurrentState.inetCondition = (isValidated || !mCurrentState.isDefault) ? 1 : 0; + notifyListenersIfNecessary(); + } + + public void setCarrierNetworkChangeMode(boolean carrierNetworkChangeMode) { + mCurrentState.carrierNetworkChangeMode = carrierNetworkChangeMode; + updateTelephony(); + } + + /** + * Start listening for phone state changes. + */ + public void registerListener() { + mPhone.listen(mPhoneStateListener, + PhoneStateListener.LISTEN_SERVICE_STATE + | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS + | PhoneStateListener.LISTEN_CALL_STATE + | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE + | PhoneStateListener.LISTEN_DATA_ACTIVITY + | PhoneStateListener.LISTEN_CARRIER_NETWORK_CHANGE); + } + + /** + * Stop listening for phone state changes. + */ + public void unregisterListener() { + mPhone.listen(mPhoneStateListener, 0); + } + + /** + * Produce a mapping of data network types to icon groups for simple and quick use in + * updateTelephony. + */ + private void mapIconSets() { + mNetworkToIconLookup.clear(); + + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_A, TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UMTS, TelephonyIcons.THREE_G); + + if (!mConfig.showAtLeast3G) { + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, + TelephonyIcons.UNKNOWN); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE, TelephonyIcons.E); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA, TelephonyIcons.ONE_X); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT, TelephonyIcons.ONE_X); + + mDefaultIcons = TelephonyIcons.G; + } else { + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, + TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT, + TelephonyIcons.THREE_G); + mDefaultIcons = TelephonyIcons.THREE_G; + } + + MobileIconGroup hGroup = TelephonyIcons.THREE_G; + if (mConfig.hspaDataDistinguishable) { + hGroup = TelephonyIcons.H; + } + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSDPA, hGroup); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSUPA, hGroup); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPA, hGroup); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPAP, hGroup); + + if (mConfig.show4gForLte) { + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.FOUR_G); + } else { + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.LTE); + } + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_IWLAN, TelephonyIcons.FOUR_G); + } + + @Override + public void notifyListeners() { + MobileIconGroup icons = getIcons(); + + String contentDescription = getStringIfExists(getContentDescription()); + String dataContentDescription = getStringIfExists(icons.mDataContentDescription); + + // Show icon in QS when we are connected or need to show roaming. + boolean showDataIcon = mCurrentState.dataConnected + || mCurrentState.iconGroup == TelephonyIcons.ROAMING; + IconState statusIcon = new IconState(mCurrentState.enabled && !mCurrentState.airplaneMode, + getCurrentIconId(), contentDescription); + + int qsTypeIcon = 0; + IconState qsIcon = null; + String description = null; + // Only send data sim callbacks to QS. + if (mCurrentState.dataSim) { + qsTypeIcon = showDataIcon ? icons.mQsDataType : 0; + qsIcon = new IconState(mCurrentState.enabled + && !mCurrentState.isEmergency, getQsCurrentIconId(), contentDescription); + description = mCurrentState.isEmergency ? null : mCurrentState.networkName; + } + boolean activityIn = mCurrentState.dataConnected + && !mCurrentState.carrierNetworkChangeMode + && mCurrentState.activityIn; + boolean activityOut = mCurrentState.dataConnected + && !mCurrentState.carrierNetworkChangeMode + && mCurrentState.activityOut; + showDataIcon &= mCurrentState.isDefault; + int typeIcon = showDataIcon ? icons.mDataType : 0; + mCallbackHandler.setMobileDataIndicators(statusIcon, qsIcon, typeIcon, qsTypeIcon, + activityIn, activityOut, dataContentDescription, description, icons.mIsWide, + mSubscriptionInfo.getSubscriptionId()); + } + + @Override + protected MobileState cleanState() { + return new MobileState(); + } + + private boolean hasService() { + if (mServiceState != null) { + // Consider the device to be in service if either voice or data + // service is available. Some SIM cards are marketed as data-only + // and do not support voice service, and on these SIM cards, we + // want to show signal bars for data service as well as the "no + // service" or "emergency calls only" text that indicates that voice + // is not available. + switch (mServiceState.getVoiceRegState()) { + case ServiceState.STATE_POWER_OFF: + return false; + case ServiceState.STATE_OUT_OF_SERVICE: + case ServiceState.STATE_EMERGENCY_ONLY: + return mServiceState.getDataRegState() == ServiceState.STATE_IN_SERVICE; + default: + return true; + } + } else { + return false; + } + } + + private boolean isCdma() { + return (mSignalStrength != null) && !mSignalStrength.isGsm(); + } + + public boolean isEmergencyOnly() { + return (mServiceState != null && mServiceState.isEmergencyOnly()); + } + + private boolean isRoaming() { + if (isCdma()) { + final int iconMode = mServiceState.getCdmaEriIconMode(); + return mServiceState.getCdmaEriIconIndex() != EriInfo.ROAMING_INDICATOR_OFF + && (iconMode == EriInfo.ROAMING_ICON_MODE_NORMAL + || iconMode == EriInfo.ROAMING_ICON_MODE_FLASH); + } else { + return mServiceState != null && mServiceState.getRoaming(); + } + } + + private boolean isCarrierNetworkChangeActive() { + return mCurrentState.carrierNetworkChangeMode; + } + + public void handleBroadcast(Intent intent) { + String action = intent.getAction(); + if (action.equals(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION)) { + updateNetworkName(intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_SPN, false), + intent.getStringExtra(TelephonyIntents.EXTRA_SPN), + intent.getStringExtra(TelephonyIntents.EXTRA_DATA_SPN), + intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_PLMN, false), + intent.getStringExtra(TelephonyIntents.EXTRA_PLMN)); + notifyListenersIfNecessary(); + } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) { + updateDataSim(); + notifyListenersIfNecessary(); + } + } + + private void updateDataSim() { + int defaultDataSub = mDefaults.getDefaultDataSubId(); + if (SubscriptionManager.isValidSubscriptionId(defaultDataSub)) { + mCurrentState.dataSim = defaultDataSub == mSubscriptionInfo.getSubscriptionId(); + } else { + // There doesn't seem to be a data sim selected, however if + // there isn't a MobileSignalController with dataSim set, then + // QS won't get any callbacks and will be blank. Instead + // lets just assume we are the data sim (which will basically + // show one at random) in QS until one is selected. The user + // should pick one soon after, so we shouldn't be in this state + // for long. + mCurrentState.dataSim = true; + } + } + + /** + * Updates the network's name based on incoming spn and plmn. + */ + void updateNetworkName(boolean showSpn, String spn, String dataSpn, + boolean showPlmn, String plmn) { + if (CHATTY) { + Log.d("CarrierLabel", "updateNetworkName showSpn=" + showSpn + + " spn=" + spn + " dataSpn=" + dataSpn + + " showPlmn=" + showPlmn + " plmn=" + plmn); + } + StringBuilder str = new StringBuilder(); + StringBuilder strData = new StringBuilder(); + if (showPlmn && plmn != null) { + str.append(plmn); + strData.append(plmn); + } + if (showSpn && spn != null) { + if (str.length() != 0) { + str.append(mNetworkNameSeparator); + } + str.append(spn); + } + if (str.length() != 0) { + mCurrentState.networkName = str.toString(); + } else { + mCurrentState.networkName = mNetworkNameDefault; + } + if (showSpn && dataSpn != null) { + if (strData.length() != 0) { + strData.append(mNetworkNameSeparator); + } + strData.append(dataSpn); + } + if (strData.length() != 0) { + mCurrentState.networkNameData = strData.toString(); + } else { + mCurrentState.networkNameData = mNetworkNameDefault; + } + } + + /** + * Updates the current state based on mServiceState, mSignalStrength, mDataNetType, + * mDataState, and mSimState. It should be called any time one of these is updated. + * This will call listeners if necessary. + */ + private final void updateTelephony() { + if (DEBUG) { + Log.d(mTag, "updateTelephonySignalStrength: hasService=" + hasService() + + " ss=" + mSignalStrength); + } + mCurrentState.connected = hasService() && mSignalStrength != null; + if (mCurrentState.connected) { + if (!mSignalStrength.isGsm() && mConfig.alwaysShowCdmaRssi) { + mCurrentState.level = mSignalStrength.getCdmaLevel(); + } else { + mCurrentState.level = mSignalStrength.getLevel(); + } + } + if (mNetworkToIconLookup.indexOfKey(mDataNetType) >= 0) { + mCurrentState.iconGroup = mNetworkToIconLookup.get(mDataNetType); + } else { + mCurrentState.iconGroup = mDefaultIcons; + } + mCurrentState.dataConnected = mCurrentState.connected + && mDataState == TelephonyManager.DATA_CONNECTED; + + if (isCarrierNetworkChangeActive()) { + mCurrentState.iconGroup = TelephonyIcons.CARRIER_NETWORK_CHANGE; + } else if (isRoaming()) { + mCurrentState.iconGroup = TelephonyIcons.ROAMING; + } + if (isEmergencyOnly() != mCurrentState.isEmergency) { + mCurrentState.isEmergency = isEmergencyOnly(); + mNetworkController.recalculateEmergency(); + } + // Fill in the network name if we think we have it. + if (mCurrentState.networkName == mNetworkNameDefault && mServiceState != null + && mServiceState.getOperatorAlphaShort() != null) { + mCurrentState.networkName = mServiceState.getOperatorAlphaShort(); + } + + notifyListenersIfNecessary(); + } + + @VisibleForTesting + void setActivity(int activity) { + mCurrentState.activityIn = activity == TelephonyManager.DATA_ACTIVITY_INOUT + || activity == TelephonyManager.DATA_ACTIVITY_IN; + mCurrentState.activityOut = activity == TelephonyManager.DATA_ACTIVITY_INOUT + || activity == TelephonyManager.DATA_ACTIVITY_OUT; + notifyListenersIfNecessary(); + } + + @Override + public void dump(PrintWriter pw) { + super.dump(pw); + pw.println(" mSubscription=" + mSubscriptionInfo + ","); + pw.println(" mServiceState=" + mServiceState + ","); + pw.println(" mSignalStrength=" + mSignalStrength + ","); + pw.println(" mDataState=" + mDataState + ","); + pw.println(" mDataNetType=" + mDataNetType + ","); + } + + class MobilePhoneStateListener extends PhoneStateListener { + public MobilePhoneStateListener(int subId, Looper looper) { + super(subId, looper); + } + + @Override + public void onSignalStrengthsChanged(SignalStrength signalStrength) { + if (DEBUG) { + Log.d(mTag, "onSignalStrengthsChanged signalStrength=" + signalStrength + + ((signalStrength == null) ? "" : (" level=" + signalStrength.getLevel()))); + } + mSignalStrength = signalStrength; + updateTelephony(); + } + + @Override + public void onServiceStateChanged(ServiceState state) { + if (DEBUG) { + Log.d(mTag, "onServiceStateChanged voiceState=" + state.getVoiceRegState() + + " dataState=" + state.getDataRegState()); + } + mServiceState = state; + updateTelephony(); + } + + @Override + public void onDataConnectionStateChanged(int state, int networkType) { + if (DEBUG) { + Log.d(mTag, "onDataConnectionStateChanged: state=" + state + + " type=" + networkType); + } + mDataState = state; + mDataNetType = networkType; + updateTelephony(); + } + + @Override + public void onDataActivity(int direction) { + if (DEBUG) { + Log.d(mTag, "onDataActivity: direction=" + direction); + } + setActivity(direction); + } + + @Override + public void onCarrierNetworkChange(boolean active) { + if (DEBUG) { + Log.d(mTag, "onCarrierNetworkChange: active=" + active); + } + mCurrentState.carrierNetworkChangeMode = active; + + updateTelephony(); + } + }; + + static class MobileIconGroup extends SignalController.IconGroup { + final int mDataContentDescription; // mContentDescriptionDataType + final int mDataType; + final boolean mIsWide; + final int mQsDataType; + + public MobileIconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc, + int sbNullState, int qsNullState, int sbDiscState, int qsDiscState, + int discContentDesc, int dataContentDesc, int dataType, boolean isWide, + int qsDataType) { + super(name, sbIcons, qsIcons, contentDesc, sbNullState, qsNullState, sbDiscState, + qsDiscState, discContentDesc); + mDataContentDescription = dataContentDesc; + mDataType = dataType; + mIsWide = isWide; + mQsDataType = qsDataType; + } + } + + static class MobileState extends SignalController.State { + String networkName; + String networkNameData; + boolean dataSim; + boolean dataConnected; + boolean isEmergency; + boolean airplaneMode; + boolean carrierNetworkChangeMode; + boolean isDefault; + + @Override + public void copyFrom(State s) { + super.copyFrom(s); + MobileState state = (MobileState) s; + dataSim = state.dataSim; + networkName = state.networkName; + networkNameData = state.networkNameData; + dataConnected = state.dataConnected; + isDefault = state.isDefault; + isEmergency = state.isEmergency; + airplaneMode = state.airplaneMode; + carrierNetworkChangeMode = state.carrierNetworkChangeMode; + } + + @Override + protected void toString(StringBuilder builder) { + super.toString(builder); + builder.append(','); + builder.append("dataSim=").append(dataSim).append(','); + builder.append("networkName=").append(networkName).append(','); + builder.append("networkNameData=").append(networkNameData).append(','); + builder.append("dataConnected=").append(dataConnected).append(','); + builder.append("isDefault=").append(isDefault).append(','); + builder.append("isEmergency=").append(isEmergency).append(','); + builder.append("airplaneMode=").append(airplaneMode).append(','); + builder.append("carrierNetworkChangeMode=").append(carrierNetworkChangeMode); + } + + @Override + public boolean equals(Object o) { + return super.equals(o) + && Objects.equals(((MobileState) o).networkName, networkName) + && Objects.equals(((MobileState) o).networkNameData, networkNameData) + && ((MobileState) o).dataSim == dataSim + && ((MobileState) o).dataConnected == dataConnected + && ((MobileState) o).isEmergency == isEmergency + && ((MobileState) o).airplaneMode == airplaneMode + && ((MobileState) o).carrierNetworkChangeMode == carrierNetworkChangeMode + && ((MobileState) o).isDefault == isDefault; + } + } +} 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 3cffc85..38656ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java @@ -16,30 +16,56 @@ package com.android.systemui.statusbar.policy; +import android.content.Context; import android.content.Intent; +import android.telephony.SubscriptionInfo; + +import com.android.settingslib.wifi.AccessPoint; + +import java.util.List; public interface NetworkController { boolean hasMobileDataFeature(); - void addNetworkSignalChangedCallback(NetworkSignalChangedCallback cb); - void removeNetworkSignalChangedCallback(NetworkSignalChangedCallback cb); + void addSignalCallback(SignalCallback cb); + void removeSignalCallback(SignalCallback cb); void setWifiEnabled(boolean enabled); void onUserSwitched(int newUserId); AccessPointController getAccessPointController(); MobileDataController getMobileDataController(); - public interface NetworkSignalChangedCallback { - void onWifiSignalChanged(boolean enabled, boolean connected, int wifiSignalIconId, - boolean activityIn, boolean activityOut, - String wifiSignalContentDescriptionId, String description); - void onMobileDataSignalChanged(boolean enabled, int mobileSignalIconId, - String mobileSignalContentDescriptionId, int dataTypeIconId, - boolean activityIn, boolean activityOut, - String dataTypeContentDescriptionId, String description, - boolean isDataTypeIconWide); - void onNoSimVisibleChanged(boolean visible); - void onAirplaneModeChanged(boolean enabled); - void onMobileDataEnabled(boolean enabled); + public interface SignalCallback { + void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon, + boolean activityIn, boolean activityOut, String description); + + void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType, + int qsType, boolean activityIn, boolean activityOut, String typeContentDescription, + String description, boolean isWide, int subId); + void setSubs(List<SubscriptionInfo> subs); + void setNoSims(boolean show); + + void setEthernetIndicators(IconState icon); + + void setIsAirplaneMode(IconState icon); + + void setMobileDataEnabled(boolean enabled); + } + + public static class IconState { + public final boolean visible; + public final int icon; + public final String contentDescription; + + public IconState(boolean visible, int icon, String contentDescription) { + this.visible = visible; + this.icon = icon; + this.contentDescription = contentDescription; + } + + public IconState(boolean visible, int icon, int contentDescription, + Context context) { + this(visible, icon, context.getString(contentDescription)); + } } /** @@ -50,25 +76,14 @@ public interface NetworkController { void addAccessPointCallback(AccessPointCallback callback); void removeAccessPointCallback(AccessPointCallback callback); void scanForAccessPoints(); + int getIcon(AccessPoint ap); boolean connect(AccessPoint ap); boolean canConfigWifi(); public interface AccessPointCallback { - void onAccessPointsChanged(AccessPoint[] accessPoints); + void onAccessPointsChanged(List<AccessPoint> accessPoints); void onSettingsActivityTriggered(Intent settingsIntent); } - - public static class AccessPoint { - public static final int NO_NETWORK = -1; // see WifiManager - - public int networkId; - public int iconId; - public String ssid; - public boolean isConnected; - public boolean isConfigured; - public boolean hasSecurity; - public int level; // 0 - 5 - } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java index 9a7f21e..ff0e8a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -17,10 +17,7 @@ package com.android.systemui.statusbar.policy; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; -import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; -import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; -import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; -import static android.net.NetworkCapabilities.TRANSPORT_WIFI; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -28,34 +25,23 @@ import android.content.IntentFilter; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkCapabilities; -import android.net.NetworkInfo; -import android.net.wifi.WifiConfiguration; -import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; -import android.os.Message; -import android.os.Messenger; +import android.os.Looper; import android.provider.Settings; -import android.telephony.PhoneStateListener; -import android.telephony.ServiceState; -import android.telephony.SignalStrength; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; import android.telephony.TelephonyManager; import android.text.TextUtils; -import android.text.format.DateFormat; import android.util.Log; -import android.util.SparseArray; +import android.util.MathUtils; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.telephony.IccCardConstants; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.TelephonyIntents; -import com.android.internal.telephony.cdma.EriInfo; -import com.android.internal.util.AsyncChannel; import com.android.systemui.DemoMode; import com.android.systemui.R; @@ -69,7 +55,6 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; /** Platform implementation of the network controller. **/ public class NetworkControllerImpl extends BroadcastReceiver @@ -78,13 +63,7 @@ public class NetworkControllerImpl extends BroadcastReceiver static final String TAG = "NetworkController"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); // additional diagnostics, but not logspew - static final boolean CHATTY = Log.isLoggable(TAG + ".Chat", Log.DEBUG); - // Save the previous SignalController.States of all SignalControllers for dumps. - static final boolean RECORD_HISTORY = true; - // If RECORD_HISTORY how many to save, must be a power of 2. - static final int HISTORY_SIZE = 16; - - private static final int INET_CONDITION_THRESHOLD = 50; + static final boolean CHATTY = Log.isLoggable(TAG + "Chat", Log.DEBUG); private final Context mContext; private final TelephonyManager mPhone; @@ -92,11 +71,16 @@ public class NetworkControllerImpl extends BroadcastReceiver private final ConnectivityManager mConnectivityManager; private final SubscriptionManager mSubscriptionManager; private final boolean mHasMobileDataFeature; + private final SubscriptionDefaults mSubDefaults; private Config mConfig; // Subcontrollers. @VisibleForTesting final WifiSignalController mWifiSignalController; + + @VisibleForTesting + final EthernetSignalController mEthernetSignalController; + @VisibleForTesting final Map<Integer, MobileSignalController> mMobileSignalControllers = new HashMap<Integer, MobileSignalController>(); @@ -106,12 +90,6 @@ public class NetworkControllerImpl extends BroadcastReceiver private final AccessPointControllerImpl mAccessPoints; private final MobileDataControllerImpl mMobileDataController; - // Network types that replace the carrier label if the device does not support mobile data. - private boolean mBluetoothTethered = false; - private boolean mEthernetConnected = false; - - // state of inet connection - private boolean mConnected = false; private boolean mInetCondition; // Used for Logging and demo. // BitSets indicating which network transport types (e.g., TRANSPORT_WIFI, TRANSPORT_MOBILE) are @@ -124,50 +102,57 @@ public class NetworkControllerImpl extends BroadcastReceiver private boolean mHasNoSims; private Locale mLocale = null; // This list holds our ordering. - private List<SubscriptionInfo> mCurrentSubscriptions - = new ArrayList<SubscriptionInfo>(); - - // All the callbacks. - private ArrayList<EmergencyListener> mEmergencyListeners = new ArrayList<EmergencyListener>(); - private ArrayList<CarrierLabelListener> mCarrierListeners = - new ArrayList<CarrierLabelListener>(); - private ArrayList<SignalCluster> mSignalClusters = new ArrayList<SignalCluster>(); - private ArrayList<NetworkSignalChangedCallback> mSignalsChangedCallbacks = - new ArrayList<NetworkSignalChangedCallback>(); + private List<SubscriptionInfo> mCurrentSubscriptions = new ArrayList<>(); + @VisibleForTesting boolean mListening; // The current user ID. private int mCurrentUserId; + private OnSubscriptionsChangedListener mSubscriptionListener; + + // Handler that all broadcasts are received on. + private final Handler mReceiverHandler; + // Handler that all callbacks are made on. + private final CallbackHandler mCallbackHandler; + /** * Construct this controller object and register for updates. */ - public NetworkControllerImpl(Context context) { + public NetworkControllerImpl(Context context, Looper bgLooper) { this(context, (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE), (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE), (WifiManager) context.getSystemService(Context.WIFI_SERVICE), - SubscriptionManager.from(context), Config.readConfig(context), - new AccessPointControllerImpl(context), new MobileDataControllerImpl(context)); - registerListeners(); + SubscriptionManager.from(context), Config.readConfig(context), bgLooper, + new CallbackHandler(), + new AccessPointControllerImpl(context, bgLooper), + new MobileDataControllerImpl(context), + new SubscriptionDefaults()); + mReceiverHandler.post(mRegisterListeners); } @VisibleForTesting NetworkControllerImpl(Context context, ConnectivityManager connectivityManager, TelephonyManager telephonyManager, WifiManager wifiManager, - SubscriptionManager subManager, Config config, + SubscriptionManager subManager, Config config, Looper bgLooper, + CallbackHandler callbackHandler, AccessPointControllerImpl accessPointController, - MobileDataControllerImpl mobileDataController) { + MobileDataControllerImpl mobileDataController, + SubscriptionDefaults defaultsHandler) { mContext = context; mConfig = config; + mReceiverHandler = new Handler(bgLooper); + mCallbackHandler = callbackHandler; mSubscriptionManager = subManager; + mSubDefaults = defaultsHandler; mConnectivityManager = connectivityManager; mHasMobileDataFeature = mConnectivityManager.isNetworkSupported(ConnectivityManager.TYPE_MOBILE); // telephony - mPhone = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + mPhone = telephonyManager; // wifi mWifiManager = wifiManager; @@ -180,21 +165,25 @@ public class NetworkControllerImpl extends BroadcastReceiver mMobileDataController.setCallback(new MobileDataControllerImpl.Callback() { @Override public void onMobileDataEnabled(boolean enabled) { - notifyMobileDataEnabled(enabled); + mCallbackHandler.setMobileDataEnabled(enabled); } }); mWifiSignalController = new WifiSignalController(mContext, mHasMobileDataFeature, - mSignalsChangedCallbacks, mSignalClusters, this); + mCallbackHandler, this); + + mEthernetSignalController = new EthernetSignalController(mContext, mCallbackHandler, this); // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it updateAirplaneMode(true /* force callback */); - mAccessPoints.setNetworkController(this); } private void registerListeners() { for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.registerListener(); } + if (mSubscriptionListener == null) { + mSubscriptionListener = new SubListener(); + } mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionListener); // broadcasts @@ -206,11 +195,11 @@ public class NetworkControllerImpl extends BroadcastReceiver filter.addAction(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED); filter.addAction(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED); filter.addAction(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION); - filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); filter.addAction(ConnectivityManager.INET_CONDITION_ACTION); filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); - mContext.registerReceiver(this, filter); + mContext.registerReceiver(this, filter, null, mReceiverHandler); mListening = true; updateMobileControllers(); @@ -240,20 +229,8 @@ public class NetworkControllerImpl extends BroadcastReceiver } public void addEmergencyListener(EmergencyListener listener) { - mEmergencyListeners.add(listener); - listener.setEmergencyCallsOnly(isEmergencyOnly()); - } - - public void addCarrierLabel(CarrierLabelListener listener) { - mCarrierListeners.add(listener); - refreshCarrierLabel(); - } - - private void notifyMobileDataEnabled(boolean enabled) { - final int length = mSignalsChangedCallbacks.size(); - for (int i = 0; i < length; i++) { - mSignalsChangedCallbacks.get(i).onMobileDataEnabled(enabled); - } + mCallbackHandler.setListening(listener, true); + mCallbackHandler.setEmergencyCallsOnly(isEmergencyOnly()); } public boolean hasMobileDataFeature() { @@ -265,7 +242,7 @@ public class NetworkControllerImpl extends BroadcastReceiver } private MobileSignalController getDataController() { - int dataSubId = SubscriptionManager.getDefaultDataSubId(); + int dataSubId = mSubDefaults.getDefaultDataSubId(); if (!SubscriptionManager.isValidSubscriptionId(dataSubId)) { if (DEBUG) Log.e(TAG, "No data sim selected"); return mDefaultSignalController; @@ -277,23 +254,25 @@ public class NetworkControllerImpl extends BroadcastReceiver return mDefaultSignalController; } - public String getMobileNetworkName() { + public String getMobileDataNetworkName() { MobileSignalController controller = getDataController(); - return controller != null ? controller.getState().networkName : ""; + return controller != null ? controller.getState().networkNameData : ""; } public boolean isEmergencyOnly() { - int voiceSubId = SubscriptionManager.getDefaultVoiceSubId(); + int voiceSubId = mSubDefaults.getDefaultVoiceSubId(); if (!SubscriptionManager.isValidSubscriptionId(voiceSubId)) { for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { - if (!mobileSignalController.isEmergencyOnly()) { + if (!mobileSignalController.getState().isEmergency) { + if (DEBUG) Log.d(TAG, "Found emergency " + mobileSignalController.mTag); return false; } } } if (mMobileSignalControllers.containsKey(voiceSubId)) { - return mMobileSignalControllers.get(voiceSubId).isEmergencyOnly(); + if (DEBUG) Log.d(TAG, "Getting emergency from " + voiceSubId); + return mMobileSignalControllers.get(voiceSubId).getState().isEmergency; } if (DEBUG) Log.e(TAG, "Cannot find controller for voice sub: " + voiceSubId); // Something is wrong, better assume we can't make calls... @@ -305,40 +284,25 @@ public class NetworkControllerImpl extends BroadcastReceiver * so we should recheck and send out the state to listeners. */ void recalculateEmergency() { - final boolean emergencyOnly = isEmergencyOnly(); - final int length = mEmergencyListeners.size(); - for (int i = 0; i < length; i++) { - mEmergencyListeners.get(i).setEmergencyCallsOnly(emergencyOnly); - } - // If the emergency has a chance to change, then so does the carrier - // label. - refreshCarrierLabel(); - } - - public void addSignalCluster(SignalCluster cluster) { - mSignalClusters.add(cluster); - cluster.setSubs(mCurrentSubscriptions); - cluster.setIsAirplaneMode(mAirplaneMode, TelephonyIcons.FLIGHT_MODE_ICON, - R.string.accessibility_airplane_mode); - cluster.setNoSims(mHasNoSims); - mWifiSignalController.notifyListeners(); - for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { - mobileSignalController.notifyListeners(); - } + mCallbackHandler.setEmergencyCallsOnly(isEmergencyOnly()); } - public void addNetworkSignalChangedCallback(NetworkSignalChangedCallback cb) { - mSignalsChangedCallbacks.add(cb); - cb.onAirplaneModeChanged(mAirplaneMode); - cb.onNoSimVisibleChanged(mHasNoSims); + public void addSignalCallback(SignalCallback cb) { + mCallbackHandler.setListening(cb, true); + mCallbackHandler.setSubs(mCurrentSubscriptions); + mCallbackHandler.setIsAirplaneMode(new IconState(mAirplaneMode, + TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode, mContext)); + mCallbackHandler.setNoSims(mHasNoSims); mWifiSignalController.notifyListeners(); + mEthernetSignalController.notifyListeners(); for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.notifyListeners(); } } - public void removeNetworkSignalChangedCallback(NetworkSignalChangedCallback cb) { - mSignalsChangedCallbacks.remove(cb); + @Override + public void removeSignalCallback(SignalCallback cb) { + mCallbackHandler.setListening(cb, false); } @Override @@ -364,7 +328,6 @@ public class NetworkControllerImpl extends BroadcastReceiver mCurrentUserId = newUserId; mAccessPoints.onUserSwitched(newUserId); updateConnectivity(); - refreshCarrierLabel(); } @Override @@ -373,17 +336,15 @@ public class NetworkControllerImpl extends BroadcastReceiver Log.d(TAG, "onReceive: intent=" + intent); } final String action = intent.getAction(); - if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE) || + if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION) || action.equals(ConnectivityManager.INET_CONDITION_ACTION)) { updateConnectivity(); - refreshCarrierLabel(); } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) { mConfig = Config.readConfig(mContext); handleConfigurationChanged(); } else if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { refreshLocale(); updateAirplaneMode(false); - refreshCarrierLabel(); } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED)) { // We are using different subs now, we might be able to make calls. recalculateEmergency(); @@ -419,13 +380,17 @@ public class NetworkControllerImpl extends BroadcastReceiver mobileSignalController.setConfiguration(mConfig); } refreshLocale(); - refreshCarrierLabel(); } private void updateMobileControllers() { if (!mListening) { return; } + doUpdateMobileControllers(); + } + + @VisibleForTesting + void doUpdateMobileControllers() { List<SubscriptionInfo> subscriptions = mSubscriptionManager.getActiveSubscriptionInfoList(); if (subscriptions == null) { subscriptions = Collections.emptyList(); @@ -440,6 +405,7 @@ public class NetworkControllerImpl extends BroadcastReceiver } setCurrentSubscriptions(subscriptions); updateNoSims(); + recalculateEmergency(); } @VisibleForTesting @@ -447,7 +413,7 @@ public class NetworkControllerImpl extends BroadcastReceiver boolean hasNoSims = mHasMobileDataFeature && mMobileSignalControllers.size() == 0; if (hasNoSims != mHasNoSims) { mHasNoSims = hasNoSims; - notifyListeners(); + mCallbackHandler.setNoSims(mHasNoSims); } } @@ -461,10 +427,6 @@ public class NetworkControllerImpl extends BroadcastReceiver : lhs.getSimSlotIndex() - rhs.getSimSlotIndex(); } }); - final int length = mSignalClusters.size(); - for (int i = 0; i < length; i++) { - mSignalClusters.get(i).setSubs(subscriptions); - } mCurrentSubscriptions = subscriptions; HashMap<Integer, MobileSignalController> cachedControllers = @@ -478,8 +440,8 @@ public class NetworkControllerImpl extends BroadcastReceiver mMobileSignalControllers.put(subId, cachedControllers.remove(subId)); } else { MobileSignalController controller = new MobileSignalController(mContext, mConfig, - mHasMobileDataFeature, mPhone, mSignalsChangedCallbacks, mSignalClusters, - this, subscriptions.get(i)); + mHasMobileDataFeature, mPhone, mCallbackHandler, + this, subscriptions.get(i), mSubDefaults, mReceiverHandler.getLooper()); mMobileSignalControllers.put(subId, controller); if (subscriptions.get(i).getSimSlotIndex() == 0) { mDefaultSignalController = controller; @@ -497,6 +459,9 @@ public class NetworkControllerImpl extends BroadcastReceiver cachedControllers.get(key).unregisterListener(); } } + mCallbackHandler.setSubs(subscriptions); + notifyAllListeners(); + // There may be new MobileSignalControllers around, make sure they get the current // inet condition and airplane mode. pushConnectivityToSignals(); @@ -525,7 +490,6 @@ public class NetworkControllerImpl extends BroadcastReceiver mobileSignalController.setAirplaneMode(mAirplaneMode); } notifyListeners(); - refreshCarrierLabel(); } } @@ -547,6 +511,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mobileSignalController.notifyListeners(); } mWifiSignalController.notifyListeners(); + mEthernetSignalController.notifyListeners(); } /** @@ -555,17 +520,9 @@ public class NetworkControllerImpl extends BroadcastReceiver * notifyAllListeners. */ private void notifyListeners() { - int length = mSignalClusters.size(); - for (int i = 0; i < length; i++) { - mSignalClusters.get(i).setIsAirplaneMode(mAirplaneMode, TelephonyIcons.FLIGHT_MODE_ICON, - R.string.accessibility_airplane_mode); - mSignalClusters.get(i).setNoSims(mHasNoSims); - } - int signalsChangedLength = mSignalsChangedCallbacks.size(); - for (int i = 0; i < signalsChangedLength; i++) { - mSignalsChangedCallbacks.get(i).onAirplaneModeChanged(mAirplaneMode); - mSignalsChangedCallbacks.get(i).onNoSimVisibleChanged(mHasNoSims); - } + mCallbackHandler.setIsAirplaneMode(new IconState(mAirplaneMode, + TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode, mContext)); + mCallbackHandler.setNoSims(mHasNoSims); } /** @@ -589,10 +546,7 @@ public class NetworkControllerImpl extends BroadcastReceiver Log.d(TAG, "updateConnectivity: mValidatedTransports=" + mValidatedTransports); } - mConnected = !mConnectedTransports.isEmpty(); mInetCondition = !mValidatedTransports.isEmpty(); - mBluetoothTethered = mConnectedTransports.get(TRANSPORT_BLUETOOTH); - mEthernetConnected = mConnectedTransports.get(TRANSPORT_ETHERNET); pushConnectivityToSignals(); } @@ -603,65 +557,10 @@ public class NetworkControllerImpl extends BroadcastReceiver private void pushConnectivityToSignals() { // We want to update all the icons, all at once, for any condition change for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { - mobileSignalController.setInetCondition( - mInetCondition ? 1 : 0, - mValidatedTransports.get(mobileSignalController.getTransportType()) ? 1 : 0); - } - mWifiSignalController.setInetCondition( - mValidatedTransports.get(mWifiSignalController.getTransportType()) ? 1 : 0); - } - - /** - * Recalculate and update the carrier label. - */ - void refreshCarrierLabel() { - Context context = mContext; - - WifiSignalController.WifiState wifiState = mWifiSignalController.getState(); - String label = ""; - for (MobileSignalController controller : mMobileSignalControllers.values()) { - label = controller.getLabel(label, mConnected, mHasMobileDataFeature); - } - - // TODO Simplify this ugliness, some of the flows below shouldn't be possible anymore - // but stay for the sake of history. - if (mBluetoothTethered && !mHasMobileDataFeature) { - label = mContext.getString(R.string.bluetooth_tethered); - } - - if (mEthernetConnected && !mHasMobileDataFeature) { - label = context.getString(R.string.ethernet_label); - } - - if (mAirplaneMode && !isEmergencyOnly()) { - // combined values from connected wifi take precedence over airplane mode - if (wifiState.connected && mHasMobileDataFeature) { - // Suppress "No internet connection." from mobile if wifi connected. - label = ""; - } else { - if (!mHasMobileDataFeature) { - label = context.getString( - R.string.status_bar_settings_signal_meter_disconnected); - } - } - } else if (!isMobileDataConnected() && !wifiState.connected && !mBluetoothTethered && - !mEthernetConnected && !mHasMobileDataFeature) { - // Pretty much no connection. - label = context.getString(R.string.status_bar_settings_signal_meter_disconnected); - } - - // for mobile devices, we always show mobile connection info here (SPN/PLMN) - // for other devices, we show whatever network is connected - // This is determined above by references to mHasMobileDataFeature. - int length = mCarrierListeners.size(); - for (int i = 0; i < length; i++) { - mCarrierListeners.get(i).setCarrierLabel(label); + mobileSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports); } - } - - private boolean isMobileDataConnected() { - MobileSignalController controller = getDataController(); - return controller != null ? controller.getState().dataConnected : false; + mWifiSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports); + mEthernetSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { @@ -671,10 +570,6 @@ public class NetworkControllerImpl extends BroadcastReceiver pw.print(" hasVoiceCallingFeature()="); pw.println(hasVoiceCallingFeature()); - pw.println(" - Bluetooth ----"); - pw.print(" mBtReverseTethered="); - pw.println(mBluetoothTethered); - pw.println(" - connectivity ------"); pw.print(" mConnectedTransports="); pw.println(mConnectedTransports); @@ -691,10 +586,14 @@ public class NetworkControllerImpl extends BroadcastReceiver mobileSignalController.dump(pw); } mWifiSignalController.dump(pw); + + mEthernetSignalController.dump(pw); + + mAccessPoints.dump(pw); } private boolean mDemoMode; - private int mDemoInetCondition; + private boolean mDemoInetCondition; private WifiSignalController.WifiState mDemoWifiState; @Override @@ -703,7 +602,7 @@ public class NetworkControllerImpl extends BroadcastReceiver if (DEBUG) Log.d(TAG, "Entering demo mode"); unregisterListeners(); mDemoMode = true; - mDemoInetCondition = mInetCondition ? 1 : 0; + mDemoInetCondition = mInetCondition; mDemoWifiState = mWifiSignalController.getState(); } else if (mDemoMode && command.equals(COMMAND_EXIT)) { if (DEBUG) Log.d(TAG, "Exiting demo mode"); @@ -715,25 +614,30 @@ public class NetworkControllerImpl extends BroadcastReceiver controller.resetLastState(); } mWifiSignalController.resetLastState(); - registerListeners(); + mReceiverHandler.post(mRegisterListeners); notifyAllListeners(); - refreshCarrierLabel(); } else if (mDemoMode && command.equals(COMMAND_NETWORK)) { String airplane = args.getString("airplane"); if (airplane != null) { boolean show = airplane.equals("show"); - int length = mSignalClusters.size(); - for (int i = 0; i < length; i++) { - mSignalClusters.get(i).setIsAirplaneMode(show, TelephonyIcons.FLIGHT_MODE_ICON, - R.string.accessibility_airplane_mode); - } + mCallbackHandler.setIsAirplaneMode(new IconState(show, + TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode, + mContext)); } String fully = args.getString("fully"); if (fully != null) { - mDemoInetCondition = Boolean.parseBoolean(fully) ? 1 : 0; - mWifiSignalController.setInetCondition(mDemoInetCondition); + mDemoInetCondition = Boolean.parseBoolean(fully); + BitSet connected = new BitSet(); + + if (mDemoInetCondition) { + connected.set(mWifiSignalController.mTransportType); + } + mWifiSignalController.updateConnectivity(connected, connected); for (MobileSignalController controller : mMobileSignalControllers.values()) { - controller.setInetCondition(mDemoInetCondition, mDemoInetCondition); + if (mDemoInetCondition) { + connected.set(controller.mTransportType); + } + controller.updateConnectivity(connected, connected); } } String wifi = args.getString("wifi"); @@ -750,32 +654,21 @@ public class NetworkControllerImpl extends BroadcastReceiver } String sims = args.getString("sims"); if (sims != null) { - int num = Integer.parseInt(sims); - List<SubscriptionInfo> subs = new ArrayList<SubscriptionInfo>(); + int num = MathUtils.constrain(Integer.parseInt(sims), 1, 8); + List<SubscriptionInfo> subs = new ArrayList<>(); if (num != mMobileSignalControllers.size()) { mMobileSignalControllers.clear(); int start = mSubscriptionManager.getActiveSubscriptionInfoCountMax(); for (int i = start /* get out of normal index range */; i < start + num; i++) { - SubscriptionInfo info = new SubscriptionInfo(i, "", i, "", "", 0, 0, "", 0, - null, 0, 0, ""); - subs.add(info); - mMobileSignalControllers.put(i, new MobileSignalController(mContext, - mConfig, mHasMobileDataFeature, mPhone, mSignalsChangedCallbacks, - mSignalClusters, this, info)); + subs.add(addSignalController(i, i)); } } - final int n = mSignalClusters.size(); - for (int i = 0; i < n; i++) { - mSignalClusters.get(i).setSubs(subs); - } + mCallbackHandler.setSubs(subs); } String nosim = args.getString("nosim"); if (nosim != null) { - boolean show = nosim.equals("show"); - final int n = mSignalClusters.size(); - for (int i = 0; i < n; i++) { - mSignalClusters.get(i).setNoSims(show); - } + mHasNoSims = nosim.equals("show"); + mCallbackHandler.setNoSims(mHasNoSims); } String mobile = args.getString("mobile"); if (mobile != null) { @@ -783,6 +676,16 @@ public class NetworkControllerImpl extends BroadcastReceiver String datatype = args.getString("datatype"); String slotString = args.getString("slot"); int slot = TextUtils.isEmpty(slotString) ? 0 : Integer.parseInt(slotString); + slot = MathUtils.constrain(slot, 0, 8); + // Ensure we have enough sim slots + List<SubscriptionInfo> subs = new ArrayList<>(); + while (mMobileSignalControllers.size() <= slot) { + int nextSlot = mMobileSignalControllers.size(); + subs.add(addSignalController(nextSlot, nextSlot)); + } + if (!subs.isEmpty()) { + mCallbackHandler.setSubs(subs); + } // Hack to index linearly for easy use. MobileSignalController controller = mMobileSignalControllers .values().toArray(new MobileSignalController[0])[slot]; @@ -809,995 +712,55 @@ public class NetworkControllerImpl extends BroadcastReceiver controller.getState().enabled = show; controller.notifyListeners(); } - refreshCarrierLabel(); - } - } - - private final OnSubscriptionsChangedListener mSubscriptionListener = - new OnSubscriptionsChangedListener() { - @Override - public void onSubscriptionsChanged() { - updateMobileControllers(); - }; - }; - - // TODO: Move to its own file. - static class WifiSignalController extends - SignalController<WifiSignalController.WifiState, SignalController.IconGroup> { - private final WifiManager mWifiManager; - private final AsyncChannel mWifiChannel; - private final boolean mHasMobileData; - - public WifiSignalController(Context context, boolean hasMobileData, - List<NetworkSignalChangedCallback> signalCallbacks, - List<SignalCluster> signalClusters, NetworkControllerImpl networkController) { - super("WifiSignalController", context, NetworkCapabilities.TRANSPORT_WIFI, - signalCallbacks, signalClusters, networkController); - mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - mHasMobileData = hasMobileData; - Handler handler = new WifiHandler(); - mWifiChannel = new AsyncChannel(); - Messenger wifiMessenger = mWifiManager.getWifiServiceMessenger(); - if (wifiMessenger != null) { - mWifiChannel.connect(context, handler, wifiMessenger); - } - // WiFi only has one state. - mCurrentState.iconGroup = mLastState.iconGroup = new IconGroup( - "Wi-Fi Icons", - WifiIcons.WIFI_SIGNAL_STRENGTH, - WifiIcons.QS_WIFI_SIGNAL_STRENGTH, - AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH, - WifiIcons.WIFI_NO_NETWORK, - WifiIcons.QS_WIFI_NO_NETWORK, - WifiIcons.WIFI_NO_NETWORK, - WifiIcons.QS_WIFI_NO_NETWORK, - AccessibilityContentDescriptions.WIFI_NO_CONNECTION - ); - } - - @Override - protected WifiState cleanState() { - return new WifiState(); - } - - @Override - public void notifyListeners() { - // only show wifi in the cluster if connected or if wifi-only - boolean wifiVisible = mCurrentState.enabled - && (mCurrentState.connected || !mHasMobileData); - String wifiDesc = wifiVisible ? mCurrentState.ssid : null; - boolean ssidPresent = wifiVisible && mCurrentState.ssid != null; - String contentDescription = getStringIfExists(getContentDescription()); - int length = mSignalsChangedCallbacks.size(); - for (int i = 0; i < length; i++) { - mSignalsChangedCallbacks.get(i).onWifiSignalChanged(mCurrentState.enabled, - mCurrentState.connected, getQsCurrentIconId(), - ssidPresent && mCurrentState.activityIn, - ssidPresent && mCurrentState.activityOut, contentDescription, wifiDesc); - } - - int signalClustersLength = mSignalClusters.size(); - for (int i = 0; i < signalClustersLength; i++) { - mSignalClusters.get(i).setWifiIndicators(wifiVisible, getCurrentIconId(), - contentDescription); - } - } - - /** - * Extract wifi state directly from broadcasts about changes in wifi state. - */ - public void handleBroadcast(Intent intent) { - String action = intent.getAction(); - if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { - mCurrentState.enabled = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, - WifiManager.WIFI_STATE_UNKNOWN) == WifiManager.WIFI_STATE_ENABLED; - } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { - final NetworkInfo networkInfo = (NetworkInfo) - intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); - mCurrentState.connected = networkInfo != null && networkInfo.isConnected(); - // If Connected grab the signal strength and ssid. - if (mCurrentState.connected) { - // try getting it out of the intent first - WifiInfo info = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) != null - ? (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) - : mWifiManager.getConnectionInfo(); - if (info != null) { - mCurrentState.ssid = getSsid(info); - } else { - mCurrentState.ssid = null; - } - } else if (!mCurrentState.connected) { - mCurrentState.ssid = null; - } - } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) { - // Default to -200 as its below WifiManager.MIN_RSSI. - mCurrentState.rssi = intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200); - mCurrentState.level = WifiManager.calculateSignalLevel( - mCurrentState.rssi, WifiIcons.WIFI_LEVEL_COUNT); - } - - notifyListenersIfNecessary(); - } - - private String getSsid(WifiInfo info) { - String ssid = info.getSSID(); - if (ssid != null) { - return ssid; - } - // OK, it's not in the connectionInfo; we have to go hunting for it - List<WifiConfiguration> networks = mWifiManager.getConfiguredNetworks(); - int length = networks.size(); - for (int i = 0; i < length; i++) { - if (networks.get(i).networkId == info.getNetworkId()) { - return networks.get(i).SSID; - } - } - return null; - } - - @VisibleForTesting - void setActivity(int wifiActivity) { - mCurrentState.activityIn = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT - || wifiActivity == WifiManager.DATA_ACTIVITY_IN; - mCurrentState.activityOut = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT - || wifiActivity == WifiManager.DATA_ACTIVITY_OUT; - notifyListenersIfNecessary(); - } - - /** - * Handler to receive the data activity on wifi. - */ - class WifiHandler extends Handler { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: - if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) { - mWifiChannel.sendMessage(Message.obtain(this, - AsyncChannel.CMD_CHANNEL_FULL_CONNECTION)); - } else { - Log.e(mTag, "Failed to connect to wifi"); - } - break; - case WifiManager.DATA_ACTIVITY_NOTIFICATION: - setActivity(msg.arg1); - break; - default: - // Ignore - break; + String carrierNetworkChange = args.getString("carriernetworkchange"); + if (carrierNetworkChange != null) { + boolean show = carrierNetworkChange.equals("show"); + for (MobileSignalController controller : mMobileSignalControllers.values()) { + controller.setCarrierNetworkChangeMode(show); } } } - - static class WifiState extends SignalController.State { - String ssid; - - @Override - public void copyFrom(State s) { - super.copyFrom(s); - WifiState state = (WifiState) s; - ssid = state.ssid; - } - - @Override - protected void toString(StringBuilder builder) { - super.toString(builder); - builder.append(',').append("ssid=").append(ssid); - } - - @Override - public boolean equals(Object o) { - return super.equals(o) - && Objects.equals(((WifiState) o).ssid, ssid); - } - } } - // TODO: Move to its own file. - public static class MobileSignalController extends SignalController< - MobileSignalController.MobileState, MobileSignalController.MobileIconGroup> { - private final TelephonyManager mPhone; - private final String mNetworkNameDefault; - private final String mNetworkNameSeparator; - @VisibleForTesting - final PhoneStateListener mPhoneStateListener; - // Save entire info for logging, we only use the id. - private final SubscriptionInfo mSubscriptionInfo; - - // @VisibleForDemoMode - final SparseArray<MobileIconGroup> mNetworkToIconLookup; - - // Since some pieces of the phone state are interdependent we store it locally, - // this could potentially become part of MobileState for simplification/complication - // of code. - private IccCardConstants.State mSimState = IccCardConstants.State.READY; - private int mDataNetType = TelephonyManager.NETWORK_TYPE_UNKNOWN; - private int mDataState = TelephonyManager.DATA_DISCONNECTED; - private ServiceState mServiceState; - private SignalStrength mSignalStrength; - private MobileIconGroup mDefaultIcons; - private Config mConfig; - - // TODO: Reduce number of vars passed in, if we have the NetworkController, probably don't - // need listener lists anymore. - public MobileSignalController(Context context, Config config, boolean hasMobileData, - TelephonyManager phone, List<NetworkSignalChangedCallback> signalCallbacks, - List<SignalCluster> signalClusters, NetworkControllerImpl networkController, - SubscriptionInfo info) { - super("MobileSignalController(" + info.getSubscriptionId() + ")", context, - NetworkCapabilities.TRANSPORT_CELLULAR, signalCallbacks, signalClusters, - networkController); - mNetworkToIconLookup = new SparseArray<>(); - mConfig = config; - mPhone = phone; - mSubscriptionInfo = info; - mPhoneStateListener = new MobilePhoneStateListener(info.getSubscriptionId()); - mNetworkNameSeparator = getStringIfExists(R.string.status_bar_network_name_separator); - mNetworkNameDefault = getStringIfExists( - com.android.internal.R.string.lockscreen_carrier_default); - - mapIconSets(); - - mLastState.networkName = mCurrentState.networkName = mNetworkNameDefault; - mLastState.enabled = mCurrentState.enabled = hasMobileData; - mLastState.iconGroup = mCurrentState.iconGroup = mDefaultIcons; - // Get initial data sim state. - updateDataSim(); - } - - public void setConfiguration(Config config) { - mConfig = config; - mapIconSets(); - updateTelephony(); - } - - /** - * Get (the mobile parts of) the carrier string. - * - * @param currentLabel can be used for concatenation, currently just empty - * @param connected whether the device has connection to the internet at all - * @param isMobileLabel whether to always return the network or just when data is connected - */ - public String getLabel(String currentLabel, boolean connected, boolean isMobileLabel) { - if (!mCurrentState.enabled) { - return ""; - } else { - String mobileLabel = ""; - // We want to show the carrier name if in service and either: - // - We are connected to mobile data, or - // - We are not connected to mobile data, as long as the *reason* packets are not - // being routed over that link is that we have better connectivity via wifi. - // If data is disconnected for some other reason but wifi (or ethernet/bluetooth) - // is connected, we show nothing. - // Otherwise (nothing connected) we show "No internet connection". - if (mCurrentState.dataConnected) { - mobileLabel = mCurrentState.networkName; - } else if (connected || mCurrentState.isEmergency) { - if (mCurrentState.connected || mCurrentState.isEmergency) { - // The isEmergencyOnly test covers the case of a phone with no SIM - mobileLabel = mCurrentState.networkName; - } - } else { - mobileLabel = mContext.getString( - R.string.status_bar_settings_signal_meter_disconnected); - } - - if (currentLabel.length() != 0) { - currentLabel = currentLabel + mNetworkNameSeparator; - } - // Now for things that should only be shown when actually using mobile data. - if (isMobileLabel) { - return currentLabel + mobileLabel; - } else { - return currentLabel - + (mCurrentState.dataConnected ? mobileLabel : currentLabel); - } - } - } - - public int getDataContentDescription() { - return getIcons().mDataContentDescription; - } - - public void setAirplaneMode(boolean airplaneMode) { - mCurrentState.airplaneMode = airplaneMode; - notifyListenersIfNecessary(); - } - - public void setInetCondition(int inetCondition, int inetConditionForNetwork) { - // For mobile data, use general inet condition for phone signal indexing, - // and network specific for data indexing (I think this might be a bug, but - // keeping for now). - // TODO: Update with explanation of why. - mCurrentState.inetForNetwork = inetConditionForNetwork; - setInetCondition(inetCondition); - } - - /** - * Start listening for phone state changes. - */ - public void registerListener() { - mPhone.listen(mPhoneStateListener, - PhoneStateListener.LISTEN_SERVICE_STATE - | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS - | PhoneStateListener.LISTEN_CALL_STATE - | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE - | PhoneStateListener.LISTEN_DATA_ACTIVITY); - } - - /** - * Stop listening for phone state changes. - */ - public void unregisterListener() { - mPhone.listen(mPhoneStateListener, 0); - } - - /** - * Produce a mapping of data network types to icon groups for simple and quick use in - * updateTelephony. - */ - private void mapIconSets() { - mNetworkToIconLookup.clear(); - - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyIcons.THREE_G); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_A, TelephonyIcons.THREE_G); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyIcons.THREE_G); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyIcons.THREE_G); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UMTS, TelephonyIcons.THREE_G); - - if (!mConfig.showAtLeast3G) { - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, - TelephonyIcons.UNKNOWN); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE, TelephonyIcons.E); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA, TelephonyIcons.ONE_X); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT, TelephonyIcons.ONE_X); - - mDefaultIcons = TelephonyIcons.G; - } else { - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, - TelephonyIcons.THREE_G); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE, - TelephonyIcons.THREE_G); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA, - TelephonyIcons.THREE_G); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT, - TelephonyIcons.THREE_G); - mDefaultIcons = TelephonyIcons.THREE_G; - } - - MobileIconGroup hGroup = TelephonyIcons.THREE_G; - if (mConfig.hspaDataDistinguishable) { - hGroup = TelephonyIcons.H; - } - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSDPA, hGroup); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSUPA, hGroup); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPA, hGroup); - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPAP, hGroup); - - if (mConfig.show4gForLte) { - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.FOUR_G); - } else { - mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.LTE); - } - } - - @Override - public void notifyListeners() { - MobileIconGroup icons = getIcons(); - - String contentDescription = getStringIfExists(getContentDescription()); - String dataContentDescription = getStringIfExists(icons.mDataContentDescription); - - boolean showDataIcon = mCurrentState.dataConnected && mCurrentState.inetForNetwork != 0 - || mCurrentState.iconGroup == TelephonyIcons.ROAMING; - - // Only send data sim callbacks to QS. - if (mCurrentState.dataSim) { - int qsTypeIcon = showDataIcon ? icons.mQsDataType[mCurrentState.inetForNetwork] : 0; - int length = mSignalsChangedCallbacks.size(); - for (int i = 0; i < length; i++) { - mSignalsChangedCallbacks.get(i).onMobileDataSignalChanged(mCurrentState.enabled - && !mCurrentState.isEmergency, - getQsCurrentIconId(), contentDescription, - qsTypeIcon, - mCurrentState.dataConnected && mCurrentState.activityIn, - mCurrentState.dataConnected && mCurrentState.activityOut, - dataContentDescription, - mCurrentState.isEmergency ? null : mCurrentState.networkName, - // Only wide if actually showing something. - icons.mIsWide && qsTypeIcon != 0); - } - } - int typeIcon = showDataIcon ? icons.mDataType : 0; - int signalClustersLength = mSignalClusters.size(); - for (int i = 0; i < signalClustersLength; i++) { - mSignalClusters.get(i).setMobileDataIndicators( - mCurrentState.enabled && !mCurrentState.airplaneMode, - getCurrentIconId(), - typeIcon, - contentDescription, - dataContentDescription, - // Only wide if actually showing something. - icons.mIsWide && typeIcon != 0, - mSubscriptionInfo.getSubscriptionId()); - } - } - - @Override - protected MobileState cleanState() { - return new MobileState(); - } - - private boolean hasService() { - if (mServiceState != null) { - // Consider the device to be in service if either voice or data - // service is available. Some SIM cards are marketed as data-only - // and do not support voice service, and on these SIM cards, we - // want to show signal bars for data service as well as the "no - // service" or "emergency calls only" text that indicates that voice - // is not available. - switch (mServiceState.getVoiceRegState()) { - case ServiceState.STATE_POWER_OFF: - return false; - case ServiceState.STATE_OUT_OF_SERVICE: - case ServiceState.STATE_EMERGENCY_ONLY: - return mServiceState.getDataRegState() == ServiceState.STATE_IN_SERVICE; - default: - return true; - } - } else { - return false; - } - } - - private boolean isCdma() { - return (mSignalStrength != null) && !mSignalStrength.isGsm(); - } - - public boolean isEmergencyOnly() { - return (mServiceState != null && mServiceState.isEmergencyOnly()); - } - - private boolean isRoaming() { - if (isCdma()) { - final int iconMode = mServiceState.getCdmaEriIconMode(); - return mServiceState.getCdmaEriIconIndex() != EriInfo.ROAMING_INDICATOR_OFF - && (iconMode == EriInfo.ROAMING_ICON_MODE_NORMAL - || iconMode == EriInfo.ROAMING_ICON_MODE_FLASH); - } else { - return mServiceState != null && mServiceState.getRoaming(); - } - } - - public void handleBroadcast(Intent intent) { - String action = intent.getAction(); - if (action.equals(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION)) { - updateNetworkName(intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_SPN, false), - intent.getStringExtra(TelephonyIntents.EXTRA_SPN), - intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_PLMN, false), - intent.getStringExtra(TelephonyIntents.EXTRA_PLMN)); - notifyListenersIfNecessary(); - } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) { - updateDataSim(); - } - } - - private void updateDataSim() { - int defaultDataSub = SubscriptionManager.getDefaultDataSubId(); - if (SubscriptionManager.isValidSubscriptionId(defaultDataSub)) { - mCurrentState.dataSim = defaultDataSub == mSubscriptionInfo.getSubscriptionId(); - } else { - // There doesn't seem to be a data sim selected, however if - // there isn't a MobileSignalController with dataSim set, then - // QS won't get any callbacks and will be blank. Instead - // lets just assume we are the data sim (which will basically - // show one at random) in QS until one is selected. The user - // should pick one soon after, so we shouldn't be in this state - // for long. - mCurrentState.dataSim = true; - } - notifyListenersIfNecessary(); - } - - /** - * Updates the network's name based on incoming spn and plmn. - */ - void updateNetworkName(boolean showSpn, String spn, boolean showPlmn, String plmn) { - if (CHATTY) { - Log.d("CarrierLabel", "updateNetworkName showSpn=" + showSpn + " spn=" + spn - + " showPlmn=" + showPlmn + " plmn=" + plmn); - } - StringBuilder str = new StringBuilder(); - if (showPlmn && plmn != null) { - str.append(plmn); - } - if (showSpn && spn != null) { - if (str.length() != 0) { - str.append(mNetworkNameSeparator); - } - str.append(spn); - } - if (str.length() != 0) { - mCurrentState.networkName = str.toString(); - } else { - mCurrentState.networkName = mNetworkNameDefault; - } - } - - /** - * Updates the current state based on mServiceState, mSignalStrength, mDataNetType, - * mDataState, and mSimState. It should be called any time one of these is updated. - * This will call listeners if necessary. - */ - private final void updateTelephony() { - if (DEBUG) { - Log.d(TAG, "updateTelephonySignalStrength: hasService=" + hasService() - + " ss=" + mSignalStrength); - } - mCurrentState.connected = hasService() && mSignalStrength != null; - if (mCurrentState.connected) { - if (!mSignalStrength.isGsm() && mConfig.alwaysShowCdmaRssi) { - mCurrentState.level = mSignalStrength.getCdmaLevel(); - } else { - mCurrentState.level = mSignalStrength.getLevel(); - } - } - if (mNetworkToIconLookup.indexOfKey(mDataNetType) >= 0) { - mCurrentState.iconGroup = mNetworkToIconLookup.get(mDataNetType); - } else { - mCurrentState.iconGroup = mDefaultIcons; - } - mCurrentState.dataConnected = mCurrentState.connected - && mDataState == TelephonyManager.DATA_CONNECTED; - - if (isRoaming()) { - mCurrentState.iconGroup = TelephonyIcons.ROAMING; - } - if (isEmergencyOnly() != mCurrentState.isEmergency) { - mCurrentState.isEmergency = isEmergencyOnly(); - mNetworkController.recalculateEmergency(); - } - // Fill in the network name if we think we have it. - if (mCurrentState.networkName == mNetworkNameDefault && mServiceState != null - && mServiceState.getOperatorAlphaShort() != null) { - mCurrentState.networkName = mServiceState.getOperatorAlphaShort(); - } - notifyListenersIfNecessary(); - } - - @VisibleForTesting - void setActivity(int activity) { - mCurrentState.activityIn = activity == TelephonyManager.DATA_ACTIVITY_INOUT - || activity == TelephonyManager.DATA_ACTIVITY_IN; - mCurrentState.activityOut = activity == TelephonyManager.DATA_ACTIVITY_INOUT - || activity == TelephonyManager.DATA_ACTIVITY_OUT; - notifyListenersIfNecessary(); - } + private SubscriptionInfo addSignalController(int id, int simSlotIndex) { + SubscriptionInfo info = new SubscriptionInfo(id, "", simSlotIndex, "", "", 0, 0, "", 0, + null, 0, 0, ""); + mMobileSignalControllers.put(id, new MobileSignalController(mContext, + mConfig, mHasMobileDataFeature, mPhone, mCallbackHandler, this, info, + mSubDefaults, mReceiverHandler.getLooper())); + return info; + } + private class SubListener extends OnSubscriptionsChangedListener { @Override - public void dump(PrintWriter pw) { - super.dump(pw); - pw.println(" mSubscription=" + mSubscriptionInfo + ","); - pw.println(" mServiceState=" + mServiceState + ","); - pw.println(" mSignalStrength=" + mSignalStrength + ","); - pw.println(" mDataState=" + mDataState + ","); - pw.println(" mDataNetType=" + mDataNetType + ","); - } - - class MobilePhoneStateListener extends PhoneStateListener { - public MobilePhoneStateListener(int subId) { - super(subId); - } - - @Override - public void onSignalStrengthsChanged(SignalStrength signalStrength) { - if (DEBUG) { - Log.d(mTag, "onSignalStrengthsChanged signalStrength=" + signalStrength + - ((signalStrength == null) ? "" : (" level=" + signalStrength.getLevel()))); - } - mSignalStrength = signalStrength; - updateTelephony(); - } - - @Override - public void onServiceStateChanged(ServiceState state) { - if (DEBUG) { - Log.d(mTag, "onServiceStateChanged voiceState=" + state.getVoiceRegState() - + " dataState=" + state.getDataRegState()); - } - mServiceState = state; - updateTelephony(); - } - - @Override - public void onDataConnectionStateChanged(int state, int networkType) { - if (DEBUG) { - Log.d(mTag, "onDataConnectionStateChanged: state=" + state - + " type=" + networkType); - } - mDataState = state; - mDataNetType = networkType; - updateTelephony(); - } - - @Override - public void onDataActivity(int direction) { - if (DEBUG) { - Log.d(mTag, "onDataActivity: direction=" + direction); - } - setActivity(direction); - } - }; - - static class MobileIconGroup extends SignalController.IconGroup { - final int mDataContentDescription; // mContentDescriptionDataType - final int mDataType; - final boolean mIsWide; - final int[] mQsDataType; - - public MobileIconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc, - int sbNullState, int qsNullState, int sbDiscState, int qsDiscState, - int discContentDesc, int dataContentDesc, int dataType, boolean isWide, - int[] qsDataType) { - super(name, sbIcons, qsIcons, contentDesc, sbNullState, qsNullState, sbDiscState, - qsDiscState, discContentDesc); - mDataContentDescription = dataContentDesc; - mDataType = dataType; - mIsWide = isWide; - mQsDataType = qsDataType; - } - } - - static class MobileState extends SignalController.State { - String networkName; - boolean dataSim; - boolean dataConnected; - boolean isEmergency; - boolean airplaneMode; - int inetForNetwork; - - @Override - public void copyFrom(State s) { - super.copyFrom(s); - MobileState state = (MobileState) s; - dataSim = state.dataSim; - networkName = state.networkName; - dataConnected = state.dataConnected; - inetForNetwork = state.inetForNetwork; - isEmergency = state.isEmergency; - airplaneMode = state.airplaneMode; - } - - @Override - protected void toString(StringBuilder builder) { - super.toString(builder); - builder.append(','); - builder.append("dataSim=").append(dataSim).append(','); - builder.append("networkName=").append(networkName).append(','); - builder.append("dataConnected=").append(dataConnected).append(','); - builder.append("inetForNetwork=").append(inetForNetwork).append(','); - builder.append("isEmergency=").append(isEmergency).append(','); - builder.append("airplaneMode=").append(airplaneMode); - } - - @Override - public boolean equals(Object o) { - return super.equals(o) - && Objects.equals(((MobileState) o).networkName, networkName) - && ((MobileState) o).dataSim == dataSim - && ((MobileState) o).dataConnected == dataConnected - && ((MobileState) o).isEmergency == isEmergency - && ((MobileState) o).airplaneMode == airplaneMode - && ((MobileState) o).inetForNetwork == inetForNetwork; - } + public void onSubscriptionsChanged() { + updateMobileControllers(); } } /** - * Common base class for handling signal for both wifi and mobile data. + * Used to register listeners from the BG Looper, this way the PhoneStateListeners that + * get created will also run on the BG Looper. */ - static abstract class SignalController<T extends SignalController.State, - I extends SignalController.IconGroup> { - protected final String mTag; - protected final T mCurrentState; - protected final T mLastState; - protected final int mTransportType; - protected final Context mContext; - // The owner of the SignalController (i.e. NetworkController will maintain the following - // lists and call notifyListeners whenever the list has changed to ensure everyone - // is aware of current state. - protected final List<NetworkSignalChangedCallback> mSignalsChangedCallbacks; - protected final List<SignalCluster> mSignalClusters; - protected final NetworkControllerImpl mNetworkController; - - // Save the previous HISTORY_SIZE states for logging. - private final State[] mHistory; - // Where to copy the next state into. - private int mHistoryIndex; - - public SignalController(String tag, Context context, int type, - List<NetworkSignalChangedCallback> signalCallbacks, - List<SignalCluster> signalClusters, NetworkControllerImpl networkController) { - mTag = TAG + "." + tag; - mNetworkController = networkController; - mTransportType = type; - mContext = context; - mSignalsChangedCallbacks = signalCallbacks; - mSignalClusters = signalClusters; - mCurrentState = cleanState(); - mLastState = cleanState(); - if (RECORD_HISTORY) { - mHistory = new State[HISTORY_SIZE]; - for (int i = 0; i < HISTORY_SIZE; i++) { - mHistory[i] = cleanState(); - } - } - } - - public T getState() { - return mCurrentState; - } - - public int getTransportType() { - return mTransportType; - } - - public void setInetCondition(int inetCondition) { - mCurrentState.inetCondition = inetCondition; - notifyListenersIfNecessary(); - } - - /** - * Used at the end of demo mode to clear out any ugly state that it has created. - * Since we haven't had any callbacks, then isDirty will not have been triggered, - * so we can just take the last good state directly from there. - * - * Used for demo mode. - */ - void resetLastState() { - mCurrentState.copyFrom(mLastState); - } - - /** - * Determines if the state of this signal controller has changed and - * needs to trigger callbacks related to it. - */ - public boolean isDirty() { - if (!mLastState.equals(mCurrentState)) { - if (DEBUG) { - Log.d(mTag, "Change in state from: " + mLastState + "\n" - + "\tto: " + mCurrentState); - } - return true; - } - return false; - } - - public void saveLastState() { - if (RECORD_HISTORY) { - recordLastState(); - } - // Updates the current time. - mCurrentState.time = System.currentTimeMillis(); - mLastState.copyFrom(mCurrentState); - } - - /** - * Gets the signal icon for QS based on current state of connected, enabled, and level. - */ - public int getQsCurrentIconId() { - if (mCurrentState.connected) { - return getIcons().mQsIcons[mCurrentState.inetCondition][mCurrentState.level]; - } else if (mCurrentState.enabled) { - return getIcons().mQsDiscState; - } else { - return getIcons().mQsNullState; - } - } - - /** - * Gets the signal icon for SB based on current state of connected, enabled, and level. - */ - public int getCurrentIconId() { - if (mCurrentState.connected) { - return getIcons().mSbIcons[mCurrentState.inetCondition][mCurrentState.level]; - } else if (mCurrentState.enabled) { - return getIcons().mSbDiscState; - } else { - return getIcons().mSbNullState; - } - } - - /** - * Gets the content description id for the signal based on current state of connected and - * level. - */ - public int getContentDescription() { - if (mCurrentState.connected) { - return getIcons().mContentDesc[mCurrentState.level]; - } else { - return getIcons().mDiscContentDesc; - } - } - - public void notifyListenersIfNecessary() { - if (isDirty()) { - saveLastState(); - notifyListeners(); - mNetworkController.refreshCarrierLabel(); - } - } - - /** - * Returns the resource if resId is not 0, and an empty string otherwise. - */ - protected String getStringIfExists(int resId) { - return resId != 0 ? mContext.getString(resId) : ""; - } - - protected I getIcons() { - return (I) mCurrentState.iconGroup; - } - - /** - * Saves the last state of any changes, so we can log the current - * and last value of any state data. - */ - protected void recordLastState() { - mHistory[mHistoryIndex++ & (HISTORY_SIZE - 1)].copyFrom(mLastState); - } - - public void dump(PrintWriter pw) { - pw.println(" - " + mTag + " -----"); - pw.println(" Current State: " + mCurrentState); - if (RECORD_HISTORY) { - // Count up the states that actually contain time stamps, and only display those. - int size = 0; - for (int i = 0; i < HISTORY_SIZE; i++) { - if (mHistory[i].time != 0) size++; - } - // Print out the previous states in ordered number. - for (int i = mHistoryIndex + HISTORY_SIZE - 1; - i >= mHistoryIndex + HISTORY_SIZE - size; i--) { - pw.println(" Previous State(" + (mHistoryIndex + HISTORY_SIZE - i) + ": " - + mHistory[i & (HISTORY_SIZE - 1)]); - } - } - } - - /** - * Trigger callbacks based on current state. The callbacks should be completely - * based on current state, and only need to be called in the scenario where - * mCurrentState != mLastState. - */ - public abstract void notifyListeners(); - - /** - * Generate a blank T. - */ - protected abstract T cleanState(); - - /* - * Holds icons for a given state. Arrays are generally indexed as inet - * state (full connectivity or not) first, and second dimension as - * signal strength. - */ - static class IconGroup { - final int[][] mSbIcons; - final int[][] mQsIcons; - final int[] mContentDesc; - final int mSbNullState; - final int mQsNullState; - final int mSbDiscState; - final int mQsDiscState; - final int mDiscContentDesc; - // For logging. - final String mName; - - public IconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc, - int sbNullState, int qsNullState, int sbDiscState, int qsDiscState, - int discContentDesc) { - mName = name; - mSbIcons = sbIcons; - mQsIcons = qsIcons; - mContentDesc = contentDesc; - mSbNullState = sbNullState; - mQsNullState = qsNullState; - mSbDiscState = sbDiscState; - mQsDiscState = qsDiscState; - mDiscContentDesc = discContentDesc; - } - - @Override - public String toString() { - return "IconGroup(" + mName + ")"; - } - } - - static class State { - boolean connected; - boolean enabled; - boolean activityIn; - boolean activityOut; - int level; - IconGroup iconGroup; - int inetCondition; - int rssi; // Only for logging. - - // Not used for comparison, just used for logging. - long time; - - public void copyFrom(State state) { - connected = state.connected; - enabled = state.enabled; - level = state.level; - iconGroup = state.iconGroup; - inetCondition = state.inetCondition; - activityIn = state.activityIn; - activityOut = state.activityOut; - rssi = state.rssi; - time = state.time; - } - - @Override - public String toString() { - if (time != 0) { - StringBuilder builder = new StringBuilder(); - toString(builder); - return builder.toString(); - } else { - return "Empty " + getClass().getSimpleName(); - } - } - - protected void toString(StringBuilder builder) { - builder.append("connected=").append(connected).append(',') - .append("enabled=").append(enabled).append(',') - .append("level=").append(level).append(',') - .append("inetCondition=").append(inetCondition).append(',') - .append("iconGroup=").append(iconGroup).append(',') - .append("activityIn=").append(activityIn).append(',') - .append("activityOut=").append(activityOut).append(',') - .append("rssi=").append(rssi).append(',') - .append("lastModified=").append(DateFormat.format("MM-dd hh:mm:ss", time)); - } - - @Override - public boolean equals(Object o) { - if (!o.getClass().equals(getClass())) { - return false; - } - State other = (State) o; - return other.connected == connected - && other.enabled == enabled - && other.level == level - && other.inetCondition == inetCondition - && other.iconGroup == iconGroup - && other.activityIn == activityIn - && other.activityOut == activityOut - && other.rssi == rssi; - } + private final Runnable mRegisterListeners = new Runnable() { + @Override + public void run() { + registerListeners(); } - } - - public interface SignalCluster { - void setWifiIndicators(boolean visible, int strengthIcon, String contentDescription); - - void setMobileDataIndicators(boolean visible, int strengthIcon, int typeIcon, - String contentDescription, String typeContentDescription, boolean isTypeIconWide, - int subId); - void setSubs(List<SubscriptionInfo> subs); - void setNoSims(boolean show); - - void setIsAirplaneMode(boolean is, int airplaneIcon, int contentDescription); - } + }; public interface EmergencyListener { void setEmergencyCallsOnly(boolean emergencyOnly); } - public interface CarrierLabelListener { - void setCarrierLabel(String label); + public static class SubscriptionDefaults { + public int getDefaultVoiceSubId() { + return SubscriptionManager.getDefaultVoiceSubId(); + } + + public int getDefaultDataSubId() { + return SubscriptionManager.getDefaultDataSubId(); + } } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java index 030cd6d..93d0ec3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java @@ -16,16 +16,19 @@ package com.android.systemui.statusbar.policy; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.os.UserHandle; +import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.statusbar.phone.KeyguardPreviewContainer; import java.util.List; @@ -49,6 +52,15 @@ public class PreviewInflater { public View inflatePreview(Intent intent) { WidgetInfo info = getWidgetInfo(intent); + return inflatePreview(info); + } + + public View inflatePreviewFromService(ComponentName componentName) { + WidgetInfo info = getWidgetInfoFromService(componentName); + return inflatePreview(info); + } + + private KeyguardPreviewContainer inflatePreview(WidgetInfo info) { if (info == null) { return null; } @@ -76,46 +88,79 @@ public class PreviewInflater { return widgetView; } - private WidgetInfo getWidgetInfo(Intent intent) { + private WidgetInfo getWidgetInfoFromService(ComponentName componentName) { + PackageManager packageManager = mContext.getPackageManager(); + // Look for the preview specified in the service meta-data + try { + Bundle metaData = packageManager.getServiceInfo( + componentName, PackageManager.GET_META_DATA).metaData; + return getWidgetInfoFromMetaData(componentName.getPackageName(), metaData); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Failed to load preview; " + componentName.flattenToShortString() + + " not found", e); + } + return null; + } + + private WidgetInfo getWidgetInfoFromMetaData(String contextPackage, + Bundle metaData) { + if (metaData == null) { + return null; + } + int layoutId = metaData.getInt(META_DATA_KEYGUARD_LAYOUT); + if (layoutId == 0) { + return null; + } WidgetInfo info = new WidgetInfo(); + info.contextPackage = contextPackage; + info.layoutId = layoutId; + return info; + } + + private WidgetInfo getWidgetInfo(Intent intent) { PackageManager packageManager = mContext.getPackageManager(); final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser( - intent, PackageManager.MATCH_DEFAULT_ONLY, mLockPatternUtils.getCurrentUser()); + intent, PackageManager.MATCH_DEFAULT_ONLY, KeyguardUpdateMonitor.getCurrentUser()); if (appList.size() == 0) { return null; } ResolveInfo resolved = packageManager.resolveActivityAsUser(intent, PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA, - mLockPatternUtils.getCurrentUser()); + KeyguardUpdateMonitor.getCurrentUser()); if (wouldLaunchResolverActivity(resolved, appList)) { return null; } if (resolved == null || resolved.activityInfo == null) { return null; } - if (resolved.activityInfo.metaData == null || resolved.activityInfo.metaData.isEmpty()) { - return null; - } - int layoutId = resolved.activityInfo.metaData.getInt(META_DATA_KEYGUARD_LAYOUT); - if (layoutId == 0) { - return null; - } - info.contextPackage = resolved.activityInfo.packageName; - info.layoutId = layoutId; - return info; + return getWidgetInfoFromMetaData(resolved.activityInfo.packageName, + resolved.activityInfo.metaData); } public static boolean wouldLaunchResolverActivity(Context ctx, Intent intent, int currentUserId) { + return getTargetActivityInfo(ctx, intent, currentUserId) == null; + } + + /** + * @return the target activity info of the intent it resolves to a specific package or + * {@code null} if it resolved to the resolver activity + */ + public static ActivityInfo getTargetActivityInfo(Context ctx, Intent intent, + int currentUserId) { PackageManager packageManager = ctx.getPackageManager(); final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser( intent, PackageManager.MATCH_DEFAULT_ONLY, currentUserId); if (appList.size() == 0) { - return false; + return null; } ResolveInfo resolved = packageManager.resolveActivityAsUser(intent, PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA, currentUserId); - return wouldLaunchResolverActivity(resolved, appList); + if (resolved == null || wouldLaunchResolverActivity(resolved, appList)) { + return null; + } else { + return resolved.activityInfo; + } } private static boolean wouldLaunchResolverActivity( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java new file mode 100644 index 0000000..7d721c2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.policy; + +import com.android.systemui.R; + +import android.annotation.NonNull; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +/** + * Host for the remote input. + */ +public class RemoteInputView extends FrameLayout implements View.OnClickListener { + + private static final String TAG = "RemoteInput"; + + private RemoteEditText mEditText; + private ProgressBar mProgressBar; + private PendingIntent mPendingIntent; + private RemoteInput mRemoteInput; + private Notification.Action mAction; + + public RemoteInputView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mProgressBar = (ProgressBar) findViewById(R.id.remote_input_progress); + + mEditText = (RemoteEditText) getChildAt(0); + mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + + // Check if this was the result of hitting the enter key + final boolean isSoftImeEvent = event == null + && (actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT + || actionId == EditorInfo.IME_ACTION_SEND); + final boolean isKeyboardEnterKey = event != null + && KeyEvent.isConfirmKey(event.getKeyCode()) + && event.getAction() == KeyEvent.ACTION_DOWN; + + if (isSoftImeEvent || isKeyboardEnterKey) { + sendRemoteInput(); + return true; + } + return false; + } + }); + mEditText.setOnClickListener(this); + mEditText.setInnerFocusable(false); + } + + private void sendRemoteInput() { + Bundle results = new Bundle(); + results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString()); + Intent fillInIntent = new Intent(); + RemoteInput.addResultsToIntent(mAction.getRemoteInputs(), fillInIntent, + results); + + mEditText.setEnabled(false); + mProgressBar.setVisibility(VISIBLE); + + try { + mPendingIntent.send(mContext, 0, fillInIntent); + } catch (PendingIntent.CanceledException e) { + Log.i(TAG, "Unable to send remote input result", e); + } + } + + public static RemoteInputView inflate(Context context, ViewGroup root, + Notification.Action action, RemoteInput remoteInput) { + RemoteInputView v = (RemoteInputView) + LayoutInflater.from(context).inflate(R.layout.remote_input, root, false); + + v.mEditText.setHint(action.title); + v.mPendingIntent = action.actionIntent; + v.mRemoteInput = remoteInput; + v.mAction = action; + + return v; + } + + @Override + public void onClick(View v) { + if (v == mEditText) { + if (!mEditText.isFocusable()) { + mEditText.setInnerFocusable(true); + InputMethodManager imm = InputMethodManager.getInstance(); + if (imm != null) { + imm.viewClicked(mEditText); + imm.showSoftInput(mEditText, 0); + } + } + } + } + + /** + * An EditText that changes appearance based on whether it's focusable and becomes + * un-focusable whenever the user navigates away from it or it becomes invisible. + */ + public static class RemoteEditText extends EditText { + + private final Drawable mBackground; + + public RemoteEditText(Context context, AttributeSet attrs) { + super(context, attrs); + mBackground = getBackground(); + } + + private void defocusIfNeeded() { + if (isFocusable() && isEnabled()) { + setInnerFocusable(false); + } + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + + if (!isShown()) { + defocusIfNeeded(); + } + } + + @Override + protected void onFocusLost() { + super.onFocusLost(); + defocusIfNeeded(); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + defocusIfNeeded(); + } + return super.onKeyPreIme(keyCode, event); + } + + + void setInnerFocusable(boolean focusable) { + setFocusableInTouchMode(focusable); + setFocusable(focusable); + setCursorVisible(focusable); + + if (focusable) { + requestFocus(); + setBackground(mBackground); + } else { + setBackground(null); + } + + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java index 6148feb..40984d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java @@ -22,10 +22,8 @@ public interface SecurityController { String getDeviceOwnerName(); String getProfileOwnerName(); boolean isVpnEnabled(); - String getVpnApp(); - boolean isLegacyVpn(); - String getLegacyVpnName(); - void disconnectFromVpn(); + String getPrimaryVpnName(); + String getProfileVpnName(); void onUserSwitched(int newUserId); void addCallback(SecurityControllerCallback callback); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index f0dd943..b505d9d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -19,6 +19,7 @@ import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserInfo; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.IConnectivityManager; @@ -27,10 +28,15 @@ import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; +import android.os.UserManager; import android.text.TextUtils; import android.util.Log; +import android.util.SparseArray; import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnInfo; +import com.android.systemui.R; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -50,16 +56,15 @@ public class SecurityControllerImpl implements SecurityController { private final Context mContext; private final ConnectivityManager mConnectivityManager; - private final IConnectivityManager mConnectivityService = IConnectivityManager.Stub.asInterface( - ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); + private final IConnectivityManager mConnectivityManagerService; private final DevicePolicyManager mDevicePolicyManager; + private final UserManager mUserManager; private final ArrayList<SecurityControllerCallback> mCallbacks = new ArrayList<SecurityControllerCallback>(); - private VpnConfig mVpnConfig; - private String mVpnName; - private int mCurrentVpnNetworkId = NO_NETWORK; + private SparseArray<VpnConfig> mCurrentVpns = new SparseArray<>(); private int mCurrentUserId; + private int mVpnUserId; public SecurityControllerImpl(Context context) { mContext = context; @@ -67,17 +72,28 @@ public class SecurityControllerImpl implements SecurityController { context.getSystemService(Context.DEVICE_POLICY_SERVICE); mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + mConnectivityManagerService = IConnectivityManager.Stub.asInterface( + ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); + mUserManager = (UserManager) + context.getSystemService(Context.USER_SERVICE); // TODO: re-register network callback on user change. mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback); - mCurrentUserId = ActivityManager.getCurrentUser(); + onUserSwitched(ActivityManager.getCurrentUser()); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("SecurityController state:"); - pw.print(" mCurrentVpnNetworkId="); pw.println(mCurrentVpnNetworkId); - pw.print(" mVpnConfig="); pw.println(mVpnConfig); - pw.print(" mVpnName="); pw.println(mVpnName); + pw.print(" mCurrentVpns={"); + for (int i = 0 ; i < mCurrentVpns.size(); i++) { + if (i > 0) { + pw.print(", "); + } + pw.print(mCurrentVpns.keyAt(i)); + pw.print('='); + pw.print(mCurrentVpns.valueAt(i).user); + } + pw.println("}"); } @Override @@ -86,56 +102,58 @@ public class SecurityControllerImpl implements SecurityController { } @Override - public boolean hasProfileOwner() { - return !TextUtils.isEmpty(mDevicePolicyManager.getProfileOwnerNameAsUser(mCurrentUserId)); - } - - @Override public String getDeviceOwnerName() { return mDevicePolicyManager.getDeviceOwnerName(); } @Override - public String getProfileOwnerName() { - return mDevicePolicyManager.getProfileOwnerNameAsUser(mCurrentUserId); - } - - - @Override - public boolean isVpnEnabled() { - return mCurrentVpnNetworkId != NO_NETWORK; + public boolean hasProfileOwner() { + return mDevicePolicyManager.getProfileOwnerAsUser(mCurrentUserId) != null; } @Override - public boolean isLegacyVpn() { - return mVpnConfig.legacy; + public String getProfileOwnerName() { + for (UserInfo profile : mUserManager.getProfiles(mCurrentUserId)) { + String name = mDevicePolicyManager.getProfileOwnerNameAsUser(profile.id); + if (name != null) { + return name; + } + } + return null; } @Override - public String getVpnApp() { - return mVpnName; + public String getPrimaryVpnName() { + VpnConfig cfg = mCurrentVpns.get(mVpnUserId); + if (cfg != null) { + return getNameForVpnConfig(cfg, new UserHandle(mVpnUserId)); + } else { + return null; + } } @Override - public String getLegacyVpnName() { - return mVpnConfig.session; + public String getProfileVpnName() { + for (UserInfo profile : mUserManager.getProfiles(mVpnUserId)) { + if (profile.id == mVpnUserId) { + continue; + } + VpnConfig cfg = mCurrentVpns.get(profile.id); + if (cfg != null) { + return getNameForVpnConfig(cfg, profile.getUserHandle()); + } + } + return null; } @Override - public void disconnectFromVpn() { - try { - if (isLegacyVpn()) { - mConnectivityService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN); - } else { - // Prevent this app from initiating VPN connections in the future without user - // intervention. - mConnectivityService.setVpnPackageAuthorization(false); - - mConnectivityService.prepareVpn(mVpnConfig.user, VpnConfig.LEGACY_VPN); + public boolean isVpnEnabled() { + for (UserInfo profile : mUserManager.getProfiles(mVpnUserId)) { + if (mCurrentVpns.get(profile.id) != null) { + return true; } - } catch (Exception e) { - Log.e(TAG, "Unable to disconnect from VPN", e); } + return false; } @Override @@ -155,14 +173,28 @@ public class SecurityControllerImpl implements SecurityController { @Override public void onUserSwitched(int newUserId) { mCurrentUserId = newUserId; + if (mUserManager.getUserInfo(newUserId).isRestricted()) { + // VPN for a restricted profile is routed through its owner user + mVpnUserId = UserHandle.USER_OWNER; + } else { + mVpnUserId = mCurrentUserId; + } fireCallbacks(); } - private void setCurrentNetid(int netId) { - if (netId != mCurrentVpnNetworkId) { - mCurrentVpnNetworkId = netId; - updateState(); - fireCallbacks(); + private String getNameForVpnConfig(VpnConfig cfg, UserHandle user) { + if (cfg.legacy) { + return mContext.getString(R.string.legacy_vpn_name); + } + // The package name for an active VPN is stored in the 'user' field of its VpnConfig + final String vpnPackage = cfg.user; + try { + Context userContext = mContext.createPackageContextAsUser(mContext.getPackageName(), + 0 /* flags */, user); + return VpnConfig.getVpnLabel(userContext, vpnPackage).toString(); + } catch (NameNotFoundException nnfe) { + Log.e(TAG, "Package " + vpnPackage + " is not present", nnfe); + return null; } } @@ -173,27 +205,29 @@ public class SecurityControllerImpl implements SecurityController { } private void updateState() { + // Find all users with an active VPN + SparseArray<VpnConfig> vpns = new SparseArray<>(); try { - mVpnConfig = mConnectivityService.getVpnConfig(); - - if (mVpnConfig != null && !mVpnConfig.legacy) { - mVpnName = VpnConfig.getVpnLabel(mContext, mVpnConfig.user).toString(); + for (UserInfo user : mUserManager.getUsers()) { + VpnConfig cfg = mConnectivityManagerService.getVpnConfig(user.id); + if (cfg != null) { + vpns.put(user.id, cfg); + } } - } catch (RemoteException | NameNotFoundException e) { - Log.w(TAG, "Unable to get current VPN", e); + } catch (RemoteException rme) { + // Roll back to previous state + Log.e(TAG, "Unable to list active VPNs", rme); + return; } + mCurrentVpns = vpns; } private final NetworkCallback mNetworkCallback = new NetworkCallback() { @Override public void onAvailable(Network network) { - NetworkCapabilities networkCapabilities = - mConnectivityManager.getNetworkCapabilities(network); - if (DEBUG) Log.d(TAG, "onAvailable " + network.netId + " : " + networkCapabilities); - if (networkCapabilities != null && - networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { - setCurrentNetid(network.netId); - } + if (DEBUG) Log.d(TAG, "onAvailable " + network.netId); + updateState(); + fireCallbacks(); }; // TODO Find another way to receive VPN lost. This may be delayed depending on @@ -201,10 +235,8 @@ public class SecurityControllerImpl implements SecurityController { @Override public void onLost(Network network) { if (DEBUG) Log.d(TAG, "onLost " + network.netId); - if (mCurrentVpnNetworkId == network.netId) { - setCurrentNetid(NO_NETWORK); - } + updateState(); + fireCallbacks(); }; }; - } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalCallbackAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalCallbackAdapter.java new file mode 100644 index 0000000..dce889f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalCallbackAdapter.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy; + +import android.telephony.SubscriptionInfo; + +import com.android.systemui.statusbar.policy.NetworkController.IconState; +import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; + +import java.util.List; + + +/** + * Provides empty implementations of SignalCallback for those that only want some of + * the callbacks. + */ +public class SignalCallbackAdapter implements SignalCallback { + + @Override + public void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon, + boolean activityIn, boolean activityOut, String description) { + } + + @Override + public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType, + int qsType, boolean activityIn, boolean activityOut, String typeContentDescription, + String description, boolean isWide, int subId) { + } + + @Override + public void setSubs(List<SubscriptionInfo> subs) { + } + + @Override + public void setNoSims(boolean show) { + } + + @Override + public void setEthernetIndicators(IconState icon) { + } + + @Override + public void setIsAirplaneMode(IconState icon) { + } + + @Override + public void setMobileDataEnabled(boolean enabled) { + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java new file mode 100644 index 0000000..5e9447e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy; + +import static com.android.systemui.statusbar.policy.NetworkControllerImpl.TAG; + +import android.content.Context; +import android.text.format.DateFormat; +import android.util.Log; + +import java.io.PrintWriter; +import java.util.BitSet; + + +/** + * Common base class for handling signal for both wifi and mobile data. + */ +public abstract class SignalController<T extends SignalController.State, + I extends SignalController.IconGroup> { + // Save the previous SignalController.States of all SignalControllers for dumps. + static final boolean RECORD_HISTORY = true; + // If RECORD_HISTORY how many to save, must be a power of 2. + static final int HISTORY_SIZE = 64; + + protected static final boolean DEBUG = NetworkControllerImpl.DEBUG; + protected static final boolean CHATTY = NetworkControllerImpl.CHATTY; + + protected final String mTag; + protected final T mCurrentState; + protected final T mLastState; + protected final int mTransportType; + protected final Context mContext; + // The owner of the SignalController (i.e. NetworkController will maintain the following + // lists and call notifyListeners whenever the list has changed to ensure everyone + // is aware of current state. + protected final NetworkControllerImpl mNetworkController; + + protected final CallbackHandler mCallbackHandler; + + // Save the previous HISTORY_SIZE states for logging. + private final State[] mHistory; + // Where to copy the next state into. + private int mHistoryIndex; + + public SignalController(String tag, Context context, int type, CallbackHandler callbackHandler, + NetworkControllerImpl networkController) { + mTag = TAG + "." + tag; + mNetworkController = networkController; + mTransportType = type; + mContext = context; + mCallbackHandler = callbackHandler; + mCurrentState = cleanState(); + mLastState = cleanState(); + if (RECORD_HISTORY) { + mHistory = new State[HISTORY_SIZE]; + for (int i = 0; i < HISTORY_SIZE; i++) { + mHistory[i] = cleanState(); + } + } + } + + public T getState() { + return mCurrentState; + } + + public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) { + mCurrentState.inetCondition = validatedTransports.get(mTransportType) ? 1 : 0; + notifyListenersIfNecessary(); + } + + /** + * Used at the end of demo mode to clear out any ugly state that it has created. + * Since we haven't had any callbacks, then isDirty will not have been triggered, + * so we can just take the last good state directly from there. + * + * Used for demo mode. + */ + public void resetLastState() { + mCurrentState.copyFrom(mLastState); + } + + /** + * Determines if the state of this signal controller has changed and + * needs to trigger callbacks related to it. + */ + public boolean isDirty() { + if (!mLastState.equals(mCurrentState)) { + if (DEBUG) { + Log.d(mTag, "Change in state from: " + mLastState + "\n" + + "\tto: " + mCurrentState); + } + return true; + } + return false; + } + + public void saveLastState() { + if (RECORD_HISTORY) { + recordLastState(); + } + // Updates the current time. + mCurrentState.time = System.currentTimeMillis(); + mLastState.copyFrom(mCurrentState); + } + + /** + * Gets the signal icon for QS based on current state of connected, enabled, and level. + */ + public int getQsCurrentIconId() { + if (mCurrentState.connected) { + return getIcons().mQsIcons[mCurrentState.inetCondition][mCurrentState.level]; + } else if (mCurrentState.enabled) { + return getIcons().mQsDiscState; + } else { + return getIcons().mQsNullState; + } + } + + /** + * Gets the signal icon for SB based on current state of connected, enabled, and level. + */ + public int getCurrentIconId() { + if (mCurrentState.connected) { + return getIcons().mSbIcons[mCurrentState.inetCondition][mCurrentState.level]; + } else if (mCurrentState.enabled) { + return getIcons().mSbDiscState; + } else { + return getIcons().mSbNullState; + } + } + + /** + * Gets the content description id for the signal based on current state of connected and + * level. + */ + public int getContentDescription() { + if (mCurrentState.connected) { + return getIcons().mContentDesc[mCurrentState.level]; + } else { + return getIcons().mDiscContentDesc; + } + } + + public void notifyListenersIfNecessary() { + if (isDirty()) { + saveLastState(); + notifyListeners(); + } + } + + /** + * Returns the resource if resId is not 0, and an empty string otherwise. + */ + protected String getStringIfExists(int resId) { + return resId != 0 ? mContext.getString(resId) : ""; + } + + protected I getIcons() { + return (I) mCurrentState.iconGroup; + } + + /** + * Saves the last state of any changes, so we can log the current + * and last value of any state data. + */ + protected void recordLastState() { + mHistory[mHistoryIndex++ & (HISTORY_SIZE - 1)].copyFrom(mLastState); + } + + public void dump(PrintWriter pw) { + pw.println(" - " + mTag + " -----"); + pw.println(" Current State: " + mCurrentState); + if (RECORD_HISTORY) { + // Count up the states that actually contain time stamps, and only display those. + int size = 0; + for (int i = 0; i < HISTORY_SIZE; i++) { + if (mHistory[i].time != 0) size++; + } + // Print out the previous states in ordered number. + for (int i = mHistoryIndex + HISTORY_SIZE - 1; + i >= mHistoryIndex + HISTORY_SIZE - size; i--) { + pw.println(" Previous State(" + (mHistoryIndex + HISTORY_SIZE - i) + "): " + + mHistory[i & (HISTORY_SIZE - 1)]); + } + } + } + + /** + * Trigger callbacks based on current state. The callbacks should be completely + * based on current state, and only need to be called in the scenario where + * mCurrentState != mLastState. + */ + public abstract void notifyListeners(); + + /** + * Generate a blank T. + */ + protected abstract T cleanState(); + + /* + * Holds icons for a given state. Arrays are generally indexed as inet + * state (full connectivity or not) first, and second dimension as + * signal strength. + */ + static class IconGroup { + final int[][] mSbIcons; + final int[][] mQsIcons; + final int[] mContentDesc; + final int mSbNullState; + final int mQsNullState; + final int mSbDiscState; + final int mQsDiscState; + final int mDiscContentDesc; + // For logging. + final String mName; + + public IconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc, + int sbNullState, int qsNullState, int sbDiscState, int qsDiscState, + int discContentDesc) { + mName = name; + mSbIcons = sbIcons; + mQsIcons = qsIcons; + mContentDesc = contentDesc; + mSbNullState = sbNullState; + mQsNullState = qsNullState; + mSbDiscState = sbDiscState; + mQsDiscState = qsDiscState; + mDiscContentDesc = discContentDesc; + } + + @Override + public String toString() { + return "IconGroup(" + mName + ")"; + } + } + + static class State { + boolean connected; + boolean enabled; + boolean activityIn; + boolean activityOut; + int level; + IconGroup iconGroup; + int inetCondition; + int rssi; // Only for logging. + + // Not used for comparison, just used for logging. + long time; + + public void copyFrom(State state) { + connected = state.connected; + enabled = state.enabled; + level = state.level; + iconGroup = state.iconGroup; + inetCondition = state.inetCondition; + activityIn = state.activityIn; + activityOut = state.activityOut; + rssi = state.rssi; + time = state.time; + } + + @Override + public String toString() { + if (time != 0) { + StringBuilder builder = new StringBuilder(); + toString(builder); + return builder.toString(); + } else { + return "Empty " + getClass().getSimpleName(); + } + } + + protected void toString(StringBuilder builder) { + builder.append("connected=").append(connected).append(',') + .append("enabled=").append(enabled).append(',') + .append("level=").append(level).append(',') + .append("inetCondition=").append(inetCondition).append(',') + .append("iconGroup=").append(iconGroup).append(',') + .append("activityIn=").append(activityIn).append(',') + .append("activityOut=").append(activityOut).append(',') + .append("rssi=").append(rssi).append(',') + .append("lastModified=").append(DateFormat.format("MM-dd hh:mm:ss", time)); + } + + @Override + public boolean equals(Object o) { + if (!o.getClass().equals(getClass())) { + return false; + } + State other = (State) o; + return other.connected == connected + && other.enabled == enabled + && other.level == level + && other.inetCondition == inetCondition + && other.iconGroup == iconGroup + && other.activityIn == activityIn + && other.activityOut == activityOut + && other.rssi == rssi; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java index 4091619..8a27653 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.policy; import com.android.systemui.R; -import com.android.systemui.statusbar.policy.NetworkControllerImpl.MobileSignalController.MobileIconGroup; +import com.android.systemui.statusbar.policy.MobileSignalController.MobileIconGroup; class TelephonyIcons { //***** Signal strength icons @@ -68,11 +68,35 @@ class TelephonyIcons { R.drawable.stat_sys_signal_4_fully } }; - static final int[] QS_DATA_R = { - R.drawable.ic_qs_signal_r, - R.drawable.ic_qs_signal_r + //CarrierNetworkChange + static final int[][] TELEPHONY_CARRIER_NETWORK_CHANGE = { + { R.drawable.stat_sys_signal_carrier_network_change_animation, + R.drawable.stat_sys_signal_carrier_network_change_animation, + R.drawable.stat_sys_signal_carrier_network_change_animation, + R.drawable.stat_sys_signal_carrier_network_change_animation, + R.drawable.stat_sys_signal_carrier_network_change_animation }, + { R.drawable.stat_sys_signal_carrier_network_change_animation, + R.drawable.stat_sys_signal_carrier_network_change_animation, + R.drawable.stat_sys_signal_carrier_network_change_animation, + R.drawable.stat_sys_signal_carrier_network_change_animation, + R.drawable.stat_sys_signal_carrier_network_change_animation } + }; + + static final int[][] QS_TELEPHONY_CARRIER_NETWORK_CHANGE = { + { R.drawable.ic_qs_signal_carrier_network_change_animation, + R.drawable.ic_qs_signal_carrier_network_change_animation, + R.drawable.ic_qs_signal_carrier_network_change_animation, + R.drawable.ic_qs_signal_carrier_network_change_animation, + R.drawable.ic_qs_signal_carrier_network_change_animation }, + { R.drawable.ic_qs_signal_carrier_network_change_animation, + R.drawable.ic_qs_signal_carrier_network_change_animation, + R.drawable.ic_qs_signal_carrier_network_change_animation, + R.drawable.ic_qs_signal_carrier_network_change_animation, + R.drawable.ic_qs_signal_carrier_network_change_animation } }; + static final int QS_DATA_R = R.drawable.ic_qs_signal_r; + //***** Data connection icons //GSM/UMTS @@ -87,10 +111,7 @@ class TelephonyIcons { R.drawable.stat_sys_data_fully_connected_g } }; - static final int[] QS_DATA_G = { - R.drawable.ic_qs_signal_g, - R.drawable.ic_qs_signal_g - }; + static final int QS_DATA_G = R.drawable.ic_qs_signal_g; static final int[][] DATA_3G = { { R.drawable.stat_sys_data_fully_connected_3g, @@ -103,10 +124,7 @@ class TelephonyIcons { R.drawable.stat_sys_data_fully_connected_3g } }; - static final int[] QS_DATA_3G = { - R.drawable.ic_qs_signal_3g, - R.drawable.ic_qs_signal_3g - }; + static final int QS_DATA_3G = R.drawable.ic_qs_signal_3g; static final int[][] DATA_E = { { R.drawable.stat_sys_data_fully_connected_e, @@ -119,10 +137,7 @@ class TelephonyIcons { R.drawable.stat_sys_data_fully_connected_e } }; - static final int[] QS_DATA_E = { - R.drawable.ic_qs_signal_e, - R.drawable.ic_qs_signal_e - }; + static final int QS_DATA_E = R.drawable.ic_qs_signal_e; //3.5G static final int[][] DATA_H = { @@ -136,10 +151,7 @@ class TelephonyIcons { R.drawable.stat_sys_data_fully_connected_h } }; - static final int[] QS_DATA_H = { - R.drawable.ic_qs_signal_h, - R.drawable.ic_qs_signal_h - }; + static final int QS_DATA_H = R.drawable.ic_qs_signal_h; //CDMA // Use 3G icons for EVDO data and 1x icons for 1XRTT data @@ -154,10 +166,7 @@ class TelephonyIcons { R.drawable.stat_sys_data_fully_connected_1x } }; - static final int[] QS_DATA_1X = { - R.drawable.ic_qs_signal_1x, - R.drawable.ic_qs_signal_1x - }; + static final int QS_DATA_1X = R.drawable.ic_qs_signal_1x; // LTE and eHRPD static final int[][] DATA_4G = { @@ -171,10 +180,7 @@ class TelephonyIcons { R.drawable.stat_sys_data_fully_connected_4g } }; - static final int[] QS_DATA_4G = { - R.drawable.ic_qs_signal_4g, - R.drawable.ic_qs_signal_4g - }; + static final int QS_DATA_4G = R.drawable.ic_qs_signal_4g; // LTE branded "LTE" static final int[][] DATA_LTE = { @@ -188,10 +194,7 @@ class TelephonyIcons { R.drawable.stat_sys_data_fully_connected_lte } }; - static final int[] QS_DATA_LTE = { - R.drawable.ic_qs_signal_lte, - R.drawable.ic_qs_signal_lte - }; + static final int QS_DATA_LTE = R.drawable.ic_qs_signal_lte; static final int FLIGHT_MODE_ICON = R.drawable.stat_sys_airplane_mode; static final int ROAMING_ICON = R.drawable.stat_sys_data_fully_connected_roam; @@ -202,11 +205,30 @@ class TelephonyIcons { static final int ICON_3G = R.drawable.stat_sys_data_fully_connected_3g; static final int ICON_4G = R.drawable.stat_sys_data_fully_connected_4g; static final int ICON_1X = R.drawable.stat_sys_data_fully_connected_1x; + static final int ICON_CARRIER_NETWORK_CHANGE = + R.drawable.stat_sys_signal_carrier_network_change_animation; static final int QS_ICON_LTE = R.drawable.ic_qs_signal_lte; static final int QS_ICON_3G = R.drawable.ic_qs_signal_3g; static final int QS_ICON_4G = R.drawable.ic_qs_signal_4g; static final int QS_ICON_1X = R.drawable.ic_qs_signal_1x; + static final int QS_ICON_CARRIER_NETWORK_CHANGE = + R.drawable.ic_qs_signal_carrier_network_change_animation; + + static final MobileIconGroup CARRIER_NETWORK_CHANGE = new MobileIconGroup( + "CARRIER_NETWORK_CHANGE", + TelephonyIcons.TELEPHONY_CARRIER_NETWORK_CHANGE, + TelephonyIcons.QS_TELEPHONY_CARRIER_NETWORK_CHANGE, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.ICON_CARRIER_NETWORK_CHANGE, + TelephonyIcons.QS_ICON_CARRIER_NETWORK_CHANGE, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_carrier_network_change_mode, + 0, + false, + 0 + ); static final MobileIconGroup THREE_G = new MobileIconGroup( "3G", @@ -232,7 +254,7 @@ class TelephonyIcons { TelephonyIcons.TELEPHONY_NO_NETWORK, TelephonyIcons.QS_TELEPHONY_NO_NETWORK, AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], - 0, 0, false, new int[2] + 0, 0, false, 0 ); static final MobileIconGroup E = new MobileIconGroup( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index 4ac41a1..41fc967 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -19,6 +19,9 @@ package com.android.systemui.statusbar.policy; import android.app.ActivityManager; import android.app.ActivityManagerNative; import android.app.Dialog; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -40,6 +43,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; +import com.android.internal.logging.MetricsLogger; import com.android.internal.util.UserIcons; import com.android.systemui.BitmapHelper; import com.android.systemui.GuestResumeSessionReceiver; @@ -63,6 +67,7 @@ public class UserSwitcherController { private static final boolean DEBUG = false; private static final String SIMPLE_USER_SWITCHER_GLOBAL_SETTING = "lockscreenSimpleUserSwitcher"; + private static final String ACTION_REMOVE_GUEST = "com.android.systemui.REMOVE_GUEST"; private final Context mContext; private final UserManager mUserManager; @@ -89,6 +94,7 @@ public class UserSwitcherController { filter.addAction(Intent.ACTION_USER_INFO_CHANGED); filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(Intent.ACTION_USER_STOPPING); + filter.addAction(ACTION_REMOVE_GUEST); mContext.registerReceiverAsUser(mReceiver, UserHandle.OWNER, filter, null /* permission */, null /* scheduler */); @@ -296,6 +302,22 @@ public class UserSwitcherController { Log.v(TAG, "Broadcast: a=" + intent.getAction() + " user=" + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)); } + if (ACTION_REMOVE_GUEST.equals(intent.getAction())) { + int currentUser = ActivityManager.getCurrentUser(); + UserInfo userInfo = mUserManager.getUserInfo(currentUser); + if (userInfo != null && userInfo.isGuest()) { + showExitGuestDialog(currentUser); + } + return; + } + if (Intent.ACTION_USER_ADDED.equals(intent.getAction())) { + final int currentId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + UserInfo userInfo = mUserManager.getUserInfo(currentId); + if (userInfo != null && userInfo.isGuest()) { + showGuestNotification(currentId); + } + } + if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) { if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { mExitGuestDialog.cancel(); @@ -329,6 +351,24 @@ public class UserSwitcherController { } refreshUsers(forcePictureLoadForId); } + + private void showGuestNotification(int guestUserId) { + PendingIntent removeGuestPI = PendingIntent.getBroadcastAsUser(mContext, + 0, new Intent(ACTION_REMOVE_GUEST), 0, UserHandle.OWNER); + Notification notification = new Notification.Builder(mContext) + .setVisibility(Notification.VISIBILITY_SECRET) + .setPriority(Notification.PRIORITY_MIN) + .setSmallIcon(R.drawable.ic_person) + .setContentTitle(mContext.getString(R.string.guest_notification_title)) + .setContentText(mContext.getString(R.string.guest_notification_text)) + .setShowWhen(false) + .addAction(R.drawable.ic_delete, + mContext.getString(R.string.guest_notification_remove_action), + removeGuestPI) + .build(); + NotificationManager.from(mContext).notifyAsUser(null, 0, notification, + new UserHandle(guestUserId)); + } }; private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) { @@ -371,7 +411,8 @@ public class UserSwitcherController { @Override public int getCount() { boolean secureKeyguardShowing = mController.mKeyguardMonitor.isShowing() - && mController.mKeyguardMonitor.isSecure(); + && mController.mKeyguardMonitor.isSecure() + && !mController.mKeyguardMonitor.isTrusted(); if (!secureKeyguardShowing) { return mController.mUsers.size(); } @@ -509,6 +550,11 @@ public class UserSwitcherController { @Override public void setToggleState(boolean state) { } + + @Override + public int getMetricsCategory() { + return MetricsLogger.QS_USERDETAIL; + } }; private final KeyguardMonitor.Callback mCallback = new KeyguardMonitor.Callback() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java new file mode 100644 index 0000000..9b1e72a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy; + +import android.content.Context; +import android.content.Intent; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.AsyncChannel; +import com.android.systemui.statusbar.policy.NetworkController.IconState; + +import java.util.List; +import java.util.Objects; + + +public class WifiSignalController extends + SignalController<WifiSignalController.WifiState, SignalController.IconGroup> { + private final WifiManager mWifiManager; + private final AsyncChannel mWifiChannel; + private final boolean mHasMobileData; + + public WifiSignalController(Context context, boolean hasMobileData, + CallbackHandler callbackHandler, NetworkControllerImpl networkController) { + super("WifiSignalController", context, NetworkCapabilities.TRANSPORT_WIFI, + callbackHandler, networkController); + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + mHasMobileData = hasMobileData; + Handler handler = new WifiHandler(); + mWifiChannel = new AsyncChannel(); + Messenger wifiMessenger = mWifiManager.getWifiServiceMessenger(); + if (wifiMessenger != null) { + mWifiChannel.connect(context, handler, wifiMessenger); + } + // WiFi only has one state. + mCurrentState.iconGroup = mLastState.iconGroup = new IconGroup( + "Wi-Fi Icons", + WifiIcons.WIFI_SIGNAL_STRENGTH, + WifiIcons.QS_WIFI_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH, + WifiIcons.WIFI_NO_NETWORK, + WifiIcons.QS_WIFI_NO_NETWORK, + WifiIcons.WIFI_NO_NETWORK, + WifiIcons.QS_WIFI_NO_NETWORK, + AccessibilityContentDescriptions.WIFI_NO_CONNECTION + ); + } + + @Override + protected WifiState cleanState() { + return new WifiState(); + } + + @Override + public void notifyListeners() { + // only show wifi in the cluster if connected or if wifi-only + boolean wifiVisible = mCurrentState.enabled + && (mCurrentState.connected || !mHasMobileData); + String wifiDesc = wifiVisible ? mCurrentState.ssid : null; + boolean ssidPresent = wifiVisible && mCurrentState.ssid != null; + String contentDescription = getStringIfExists(getContentDescription()); + + IconState statusIcon = new IconState(wifiVisible, getCurrentIconId(), contentDescription); + IconState qsIcon = new IconState(mCurrentState.connected, getQsCurrentIconId(), + contentDescription); + mCallbackHandler.setWifiIndicators(mCurrentState.enabled, statusIcon, qsIcon, + ssidPresent && mCurrentState.activityIn, ssidPresent && mCurrentState.activityOut, + wifiDesc); + } + + /** + * Extract wifi state directly from broadcasts about changes in wifi state. + */ + public void handleBroadcast(Intent intent) { + String action = intent.getAction(); + if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { + mCurrentState.enabled = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, + WifiManager.WIFI_STATE_UNKNOWN) == WifiManager.WIFI_STATE_ENABLED; + } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { + final NetworkInfo networkInfo = (NetworkInfo) + intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); + mCurrentState.connected = networkInfo != null && networkInfo.isConnected(); + // If Connected grab the signal strength and ssid. + if (mCurrentState.connected) { + // try getting it out of the intent first + WifiInfo info = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) != null + ? (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) + : mWifiManager.getConnectionInfo(); + if (info != null) { + mCurrentState.ssid = getSsid(info); + } else { + mCurrentState.ssid = null; + } + } else if (!mCurrentState.connected) { + mCurrentState.ssid = null; + } + } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) { + // Default to -200 as its below WifiManager.MIN_RSSI. + mCurrentState.rssi = intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200); + mCurrentState.level = WifiManager.calculateSignalLevel( + mCurrentState.rssi, WifiIcons.WIFI_LEVEL_COUNT); + } + + notifyListenersIfNecessary(); + } + + private String getSsid(WifiInfo info) { + String ssid = info.getSSID(); + if (ssid != null) { + return ssid; + } + // OK, it's not in the connectionInfo; we have to go hunting for it + List<WifiConfiguration> networks = mWifiManager.getConfiguredNetworks(); + int length = networks.size(); + for (int i = 0; i < length; i++) { + if (networks.get(i).networkId == info.getNetworkId()) { + return networks.get(i).SSID; + } + } + return null; + } + + @VisibleForTesting + void setActivity(int wifiActivity) { + mCurrentState.activityIn = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT + || wifiActivity == WifiManager.DATA_ACTIVITY_IN; + mCurrentState.activityOut = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT + || wifiActivity == WifiManager.DATA_ACTIVITY_OUT; + notifyListenersIfNecessary(); + } + + /** + * Handler to receive the data activity on wifi. + */ + private class WifiHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: + if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) { + mWifiChannel.sendMessage(Message.obtain(this, + AsyncChannel.CMD_CHANNEL_FULL_CONNECTION)); + } else { + Log.e(mTag, "Failed to connect to wifi"); + } + break; + case WifiManager.DATA_ACTIVITY_NOTIFICATION: + setActivity(msg.arg1); + break; + default: + // Ignore + break; + } + } + } + + static class WifiState extends SignalController.State { + String ssid; + + @Override + public void copyFrom(State s) { + super.copyFrom(s); + WifiState state = (WifiState) s; + ssid = state.ssid; + } + + @Override + protected void toString(StringBuilder builder) { + super.toString(builder); + builder.append(',').append("ssid=").append(ssid); + } + + @Override + public boolean equals(Object o) { + return super.equals(o) + && Objects.equals(((WifiState) o).ssid, ssid); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java index 600b750..b2df40a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java @@ -17,27 +17,35 @@ package com.android.systemui.statusbar.policy; import android.content.ComponentName; +import android.net.Uri; import android.service.notification.Condition; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; public interface ZenModeController { void addCallback(Callback callback); void removeCallback(Callback callback); - void setZen(int zen); + void setZen(int zen, Uri conditionId, String reason); int getZen(); void requestConditions(boolean request); - void setExitCondition(Condition exitCondition); - Condition getExitCondition(); + ZenRule getManualRule(); + ZenModeConfig getConfig(); long getNextAlarm(); void setUserId(int userId); boolean isZenAvailable(); ComponentName getEffectsSuppressor(); + boolean isCountdownConditionSupported(); + int getCurrentUser(); + boolean isVolumeRestricted(); public static class Callback { public void onZenChanged(int zen) {} - public void onExitConditionChanged(Condition exitCondition) {} public void onConditionsChanged(Condition[] conditions) {} public void onNextAlarmChanged() {} public void onZenAvailableChanged(boolean available) {} public void onEffectsSupressorChanged() {} + public void onManualRuleChanged(ZenRule rule) {} + public void onConfigChanged(ZenModeConfig config) {} } + }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index 37ed7d8..c07f1a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -16,8 +16,8 @@ package com.android.systemui.statusbar.policy; +import android.app.ActivityManager; import android.app.AlarmManager; -import android.app.INotificationManager; import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -28,14 +28,14 @@ import android.content.IntentFilter; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings.Global; import android.provider.Settings.Secure; import android.service.notification.Condition; import android.service.notification.IConditionListener; import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; import android.util.Log; import android.util.Slog; @@ -43,6 +43,7 @@ import com.android.systemui.qs.GlobalSetting; import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.Objects; /** Platform implementation of the zen mode controller. **/ public class ZenModeControllerImpl implements ZenModeController { @@ -53,14 +54,16 @@ public class ZenModeControllerImpl implements ZenModeController { private final Context mContext; private final GlobalSetting mModeSetting; private final GlobalSetting mConfigSetting; - private final INotificationManager mNoMan; + private final NotificationManager mNoMan; private final LinkedHashMap<Uri, Condition> mConditions = new LinkedHashMap<Uri, Condition>(); private final AlarmManager mAlarmManager; private final SetupObserver mSetupObserver; + private final UserManager mUserManager; private int mUserId; private boolean mRequesting; private boolean mRegistered; + private ZenModeConfig mConfig; public ZenModeControllerImpl(Context context, Handler handler) { mContext = context; @@ -73,16 +76,23 @@ public class ZenModeControllerImpl implements ZenModeController { mConfigSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE_CONFIG_ETAG) { @Override protected void handleValueChanged(int value) { - fireExitConditionChanged(); + updateZenModeConfig(); } }; + mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + mConfig = mNoMan.getZenModeConfig(); mModeSetting.setListening(true); mConfigSetting.setListening(true); - mNoMan = INotificationManager.Stub.asInterface( - ServiceManager.getService(Context.NOTIFICATION_SERVICE)); mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mSetupObserver = new SetupObserver(handler); mSetupObserver.register(); + mUserManager = context.getSystemService(UserManager.class); + } + + @Override + public boolean isVolumeRestricted() { + return mUserManager.hasUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME, + new UserHandle(mUserId)); } @Override @@ -101,8 +111,8 @@ public class ZenModeControllerImpl implements ZenModeController { } @Override - public void setZen(int zen) { - mModeSetting.setValue(zen); + public void setZen(int zen, Uri conditionId, String reason) { + mNoMan.setZenMode(zen, conditionId, reason); } @Override @@ -113,36 +123,20 @@ public class ZenModeControllerImpl implements ZenModeController { @Override public void requestConditions(boolean request) { mRequesting = request; - try { - mNoMan.requestZenModeConditions(mListener, request ? Condition.FLAG_RELEVANT_NOW : 0); - } catch (RemoteException e) { - // noop - } + mNoMan.requestZenModeConditions(mListener, request ? Condition.FLAG_RELEVANT_NOW : 0); if (!mRequesting) { mConditions.clear(); } } @Override - public void setExitCondition(Condition exitCondition) { - try { - mNoMan.setZenModeCondition(exitCondition); - } catch (RemoteException e) { - // noop - } + public ZenRule getManualRule() { + return mConfig == null ? null : mConfig.manualRule; } @Override - public Condition getExitCondition() { - try { - final ZenModeConfig config = mNoMan.getZenModeConfig(); - if (config != null) { - return config.exitCondition; - } - } catch (RemoteException e) { - // noop - } - return null; + public ZenModeConfig getConfig() { + return mConfig; } @Override @@ -169,6 +163,17 @@ public class ZenModeControllerImpl implements ZenModeController { return NotificationManager.from(mContext).getEffectsSuppressor(); } + @Override + public boolean isCountdownConditionSupported() { + return NotificationManager.from(mContext) + .isSystemConditionProviderEnabled(ZenModeConfig.COUNTDOWN_PATH); + } + + @Override + public int getCurrentUser() { + return ActivityManager.getCurrentUser(); + } + private void fireNextAlarmChanged() { for (Callback cb : mCallbacks) { cb.onNextAlarmChanged(); @@ -199,11 +204,15 @@ public class ZenModeControllerImpl implements ZenModeController { } } - private void fireExitConditionChanged() { - final Condition exitCondition = getExitCondition(); - if (DEBUG) Slog.d(TAG, "exitCondition changed: " + exitCondition); + private void fireManualRuleChanged(ZenRule rule) { for (Callback cb : mCallbacks) { - cb.onExitConditionChanged(exitCondition); + cb.onManualRuleChanged(rule); + } + } + + private void fireConfigChanged(ZenModeConfig config) { + for (Callback cb : mCallbacks) { + cb.onConfigChanged(config); } } @@ -217,6 +226,17 @@ public class ZenModeControllerImpl implements ZenModeController { mConditions.values().toArray(new Condition[mConditions.values().size()])); } + private void updateZenModeConfig() { + final ZenModeConfig config = mNoMan.getZenModeConfig(); + if (Objects.equals(config, mConfig)) return; + final ZenRule oldRule = mConfig != null ? mConfig.manualRule : null; + mConfig = config; + fireConfigChanged(config); + final ZenRule newRule = config != null ? config.manualRule : null; + if (Objects.equals(oldRule, newRule)) return; + fireManualRuleChanged(newRule); + } + private final IConditionListener mListener = new IConditionListener.Stub() { @Override public void onConditionsReceived(Condition[] conditions) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java index 8e677f1..4a7ea96 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java @@ -17,7 +17,10 @@ package com.android.systemui.statusbar.stack; import android.view.View; + import com.android.systemui.statusbar.ActivatableNotificationView; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.policy.HeadsUpManager; import java.util.ArrayList; @@ -34,6 +37,12 @@ public class AmbientState { private int mSpeedBumpIndex = -1; private boolean mDark; private boolean mHideSensitive; + private HeadsUpManager mHeadsUpManager; + private float mStackTranslation; + private int mLayoutHeight; + private int mTopPadding; + private boolean mShadeExpanded; + private float mMaxHeadsUpTranslation; public int getScrollY() { return mScrollY; @@ -115,4 +124,63 @@ public class AmbientState { public void setSpeedBumpIndex(int speedBumpIndex) { mSpeedBumpIndex = speedBumpIndex; } + + public void setHeadsUpManager(HeadsUpManager headsUpManager) { + mHeadsUpManager = headsUpManager; + } + + public float getStackTranslation() { + return mStackTranslation; + } + + public void setStackTranslation(float stackTranslation) { + mStackTranslation = stackTranslation; + } + + public int getLayoutHeight() { + return mLayoutHeight; + } + + public void setLayoutHeight(int layoutHeight) { + mLayoutHeight = layoutHeight; + } + + public float getTopPadding() { + return mTopPadding; + } + + public void setTopPadding(int topPadding) { + mTopPadding = topPadding; + } + + public int getInnerHeight() { + return mLayoutHeight - mTopPadding - getTopHeadsUpPushIn(); + } + + private int getTopHeadsUpPushIn() { + ExpandableNotificationRow topHeadsUpEntry = getTopHeadsUpEntry(); + return topHeadsUpEntry != null ? topHeadsUpEntry.getHeadsUpHeight() + - topHeadsUpEntry.getMinHeight(): 0; + } + + public boolean isShadeExpanded() { + return mShadeExpanded; + } + + public void setShadeExpanded(boolean shadeExpanded) { + mShadeExpanded = shadeExpanded; + } + + public void setMaxHeadsUpTranslation(float maxHeadsUpTranslation) { + mMaxHeadsUpTranslation = maxHeadsUpTranslation; + } + + public float getMaxHeadsUpTranslation() { + return mMaxHeadsUpTranslation; + } + + public ExpandableNotificationRow getTopHeadsUpEntry() { + HeadsUpManager.HeadsUpEntry topEntry = mHeadsUpManager.getTopEntry(); + return topEntry == null ? null : topEntry.entry.row; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/HeadsUpAppearInterpolator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/HeadsUpAppearInterpolator.java new file mode 100644 index 0000000..05c0099 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/HeadsUpAppearInterpolator.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.stack; + +import android.graphics.Path; +import android.view.animation.PathInterpolator; + +/** + * An interpolator specifically designed for the appear animation of heads up notifications. + */ +public class HeadsUpAppearInterpolator extends PathInterpolator { + public HeadsUpAppearInterpolator() { + super(getAppearPath()); + } + + private static Path getAppearPath() { + Path path = new Path(); + path.moveTo(0, 0); + float x1 = 250f; + float x2 = 150f; + float x3 = 100f; + float y1 = 90f; + float y2 = 78f; + float y3 = 80f; + float xTot = (x1 + x2 + x3); + path.cubicTo(x1 * 0.9f / xTot, 0f, + x1 * 0.8f / xTot, y1 / y3, + x1 / xTot , y1 / y3); + path.cubicTo((x1 + x2 * 0.4f) / xTot, y1 / y3, + (x1 + x2 * 0.2f) / xTot, y2 / y3, + (x1 + x2) / xTot, y2 / y3); + path.cubicTo((x1 + x2 + x3 * 0.4f) / xTot, y2 / y3, + (x1 + x2 + x3 * 0.2f) / xTot, 1f, + 1f, 1f); + return path; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java new file mode 100644 index 0000000..3c9e8cf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.stack; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import com.android.systemui.R; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.ExpandableView; + +import java.util.ArrayList; +import java.util.List; + +/** + * A container containing child notifications + */ +public class NotificationChildrenContainer extends ViewGroup { + + private final int mChildPadding; + private final int mDividerHeight; + private final int mMaxNotificationHeight; + private final List<View> mDividers = new ArrayList<>(); + private final List<ExpandableNotificationRow> mChildren = new ArrayList<>(); + private final View mCollapseButton; + private final View mCollapseDivider; + private final int mCollapseButtonHeight; + private final int mNotificationAppearDistance; + + public NotificationChildrenContainer(Context context) { + this(context, null); + } + + public NotificationChildrenContainer(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mChildPadding = getResources().getDimensionPixelSize( + R.dimen.notification_children_padding); + mDividerHeight = getResources().getDimensionPixelSize( + R.dimen.notification_children_divider_height); + mMaxNotificationHeight = getResources().getDimensionPixelSize( + R.dimen.notification_max_height); + mNotificationAppearDistance = getResources().getDimensionPixelSize( + R.dimen.notification_appear_distance); + LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class); + mCollapseButton = inflater.inflate(R.layout.notification_collapse_button, this, + false); + mCollapseButtonHeight = getResources().getDimensionPixelSize( + R.dimen.notification_bottom_decor_height); + addView(mCollapseButton); + mCollapseDivider = inflateDivider(); + addView(mCollapseDivider); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int childCount = mChildren.size(); + boolean firstChild = true; + for (int i = 0; i < childCount; i++) { + View child = mChildren.get(i); + boolean viewGone = child.getVisibility() == View.GONE; + if (i != 0) { + View divider = mDividers.get(i - 1); + int dividerVisibility = divider.getVisibility(); + int newVisibility = viewGone ? INVISIBLE : VISIBLE; + if (dividerVisibility != newVisibility) { + divider.setVisibility(newVisibility); + } + } + if (viewGone) { + continue; + } + child.layout(0, 0, getWidth(), child.getMeasuredHeight()); + if (!firstChild) { + mDividers.get(i - 1).layout(0, 0, getWidth(), mDividerHeight); + } else { + firstChild = false; + } + } + mCollapseButton.layout(0, 0, getWidth(), mCollapseButtonHeight); + mCollapseDivider.layout(0, mCollapseButtonHeight - mDividerHeight, getWidth(), + mCollapseButtonHeight); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int ownMaxHeight = mMaxNotificationHeight; + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; + boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; + if (hasFixedHeight || isHeightLimited) { + int size = MeasureSpec.getSize(heightMeasureSpec); + ownMaxHeight = Math.min(ownMaxHeight, size); + } + int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST); + int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY); + int collapseButtonHeightSpec = MeasureSpec.makeMeasureSpec(mCollapseButtonHeight, + MeasureSpec.EXACTLY); + mCollapseButton.measure(widthMeasureSpec, collapseButtonHeightSpec); + mCollapseDivider.measure(widthMeasureSpec, dividerHeightSpec); + int height = mCollapseButtonHeight; + int childCount = mChildren.size(); + boolean firstChild = true; + for (int i = 0; i < childCount; i++) { + View child = mChildren.get(i); + if (child.getVisibility() == View.GONE) { + continue; + } + child.measure(widthMeasureSpec, newHeightSpec); + height += child.getMeasuredHeight(); + if (!firstChild) { + // layout the divider + View divider = mDividers.get(i - 1); + divider.measure(widthMeasureSpec, dividerHeightSpec); + height += mChildPadding; + } else { + firstChild = false; + } + } + int width = MeasureSpec.getSize(widthMeasureSpec); + height = hasFixedHeight ? ownMaxHeight + : isHeightLimited ? Math.min(ownMaxHeight, height) + : height; + setMeasuredDimension(width, height); + } + + /** + * Add a child notification to this view. + * + * @param row the row to add + * @param childIndex the index to add it at, if -1 it will be added at the end + */ + public void addNotification(ExpandableNotificationRow row, int childIndex) { + int newIndex = childIndex < 0 ? mChildren.size() : childIndex; + mChildren.add(newIndex, row); + addView(row); + if (mChildren.size() != 1) { + View divider = inflateDivider(); + addView(divider); + mDividers.add(Math.max(newIndex - 1, 0), divider); + } + // TODO: adapt background corners + // TODO: fix overdraw + } + + public void removeNotification(ExpandableNotificationRow row) { + int childIndex = mChildren.indexOf(row); + mChildren.remove(row); + removeView(row); + if (!mDividers.isEmpty()) { + View divider = mDividers.remove(Math.max(childIndex - 1, 0)); + removeView(divider); + } + row.setSystemChildExpanded(false); + // TODO: adapt background corners + } + + private View inflateDivider() { + return LayoutInflater.from(mContext).inflate( + R.layout.notification_children_divider, this, false); + } + + public List<ExpandableNotificationRow> getNotificationChildren() { + return mChildren; + } + + /** + * Apply the order given in the list to the children. + * + * @param childOrder the new list order + * @return whether the list order has changed + */ + public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) { + if (childOrder == null) { + return false; + } + boolean result = false; + for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) { + ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow desiredChild = childOrder.get(i); + if (child != desiredChild) { + mChildren.remove(desiredChild); + mChildren.add(i, desiredChild); + result = true; + } + } + + // Let's make the first child expanded! + boolean first = true; + for (int i = 0; i < childOrder.size(); i++) { + ExpandableNotificationRow child = childOrder.get(i); + child.setSystemChildExpanded(first); + first = false; + } + return result; + } + + public int getIntrinsicHeight() { + int childCount = mChildren.size(); + int intrinsicHeight = 0; + int visibleChildren = 0; + for (int i = 0; i < childCount; i++) { + ExpandableNotificationRow child = mChildren.get(i); + if (child.getVisibility() == View.GONE) { + continue; + } + intrinsicHeight += child.getIntrinsicHeight(); + visibleChildren++; + } + if (visibleChildren > 0) { + intrinsicHeight += (visibleChildren - 1) * mDividerHeight; + } + return intrinsicHeight; + } + + /** + * Update the state of all its children based on a linear layout algorithm. + * + * @param resultState the state to update + * @param parentState the state of the parent + */ + public void getState(StackScrollState resultState, StackViewState parentState) { + int childCount = mChildren.size(); + int yPosition = mCollapseButtonHeight; + boolean firstChild = true; + for (int i = 0; i < childCount; i++) { + ExpandableNotificationRow child = mChildren.get(i); + if (child.getVisibility() == View.GONE) { + continue; + } + if (!firstChild) { + // There's a divider + yPosition += mChildPadding; + } else { + firstChild = false; + } + StackViewState childState = resultState.getViewStateForView(child); + int intrinsicHeight = child.getIntrinsicHeight(); + childState.yTranslation = yPosition; + childState.zTranslation = 0; + childState.height = intrinsicHeight; + childState.dimmed = parentState.dimmed; + childState.dark = parentState.dark; + childState.hideSensitive = parentState.hideSensitive; + childState.belowSpeedBump = parentState.belowSpeedBump; + childState.scale = parentState.scale; + childState.clipTopAmount = 0; + childState.topOverLap = 0; + childState.location = parentState.location; + yPosition += intrinsicHeight; + } + } + + public void applyState(StackScrollState state) { + int childCount = mChildren.size(); + boolean firstChild = true; + ViewState dividerState = new ViewState(); + for (int i = 0; i < childCount; i++) { + ExpandableNotificationRow child = mChildren.get(i); + StackViewState viewState = state.getViewStateForView(child); + if (child.getVisibility() == View.GONE) { + continue; + } + if (!firstChild) { + // layout the divider + View divider = mDividers.get(i - 1); + dividerState.initFrom(divider); + dividerState.yTranslation = (int) (viewState.yTranslation + - (mChildPadding + mDividerHeight) / 2.0f); + dividerState.alpha = 1; + state.applyViewState(divider, dividerState); + } else { + firstChild = false; + } + state.applyState(child, viewState); + } + } + + public void setCollapseClickListener(OnClickListener collapseClickListener) { + mCollapseButton.setOnClickListener(collapseClickListener); + } + + /** + * This is called when the children expansion has changed and positions the children properly + * for an appear animation. + * + * @param state the new state we animate to + */ + public void prepareExpansionChanged(StackScrollState state) { + int childCount = mChildren.size(); + boolean firstChild = true; + StackViewState sourceState = new StackViewState(); + ViewState dividerState = new ViewState(); + for (int i = 0; i < childCount; i++) { + ExpandableNotificationRow child = mChildren.get(i); + StackViewState viewState = state.getViewStateForView(child); + if (child.getVisibility() == View.GONE) { + continue; + } + if (!firstChild) { + // layout the divider + View divider = mDividers.get(i - 1); + dividerState.initFrom(divider); + dividerState.yTranslation = viewState.yTranslation + - (mChildPadding + mDividerHeight) / 2.0f + mNotificationAppearDistance; + dividerState.alpha = 0; + state.applyViewState(divider, dividerState); + } else { + firstChild = false; + } + sourceState.copyFrom(viewState); + sourceState.alpha = 0; + sourceState.yTranslation += mNotificationAppearDistance; + state.applyState(child, sourceState); + } + mCollapseButton.setAlpha(0); + mCollapseDivider.setAlpha(0); + mCollapseDivider.setTranslationY(mNotificationAppearDistance / 4); + } + + public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator, + boolean withDelays, long baseDelay, long duration) { + int childCount = mChildren.size(); + boolean firstChild = true; + ViewState dividerState = new ViewState(); + int notGoneIndex = 0; + for (int i = 0; i < childCount; i++) { + ExpandableNotificationRow child = mChildren.get(i); + StackViewState viewState = state.getViewStateForView(child); + if (child.getVisibility() == View.GONE) { + continue; + } + int difference = Math.min(StackStateAnimator.DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN, + notGoneIndex + 1); + long delay = withDelays + ? difference * StackStateAnimator.ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN + : 0; + delay += baseDelay; + if (!firstChild) { + // layout the divider + View divider = mDividers.get(i - 1); + dividerState.initFrom(divider); + dividerState.yTranslation = viewState.yTranslation + - (mChildPadding + mDividerHeight) / 2.0f; + dividerState.alpha = 1; + stateAnimator.startViewAnimations(divider, dividerState, delay, duration); + } else { + firstChild = false; + } + stateAnimator.startStackAnimations(child, viewState, state, -1, delay); + notGoneIndex++; + } + dividerState.initFrom(mCollapseButton); + dividerState.alpha = 1.0f; + stateAnimator.startViewAnimations(mCollapseButton, dividerState, baseDelay, duration); + dividerState.initFrom(mCollapseDivider); + dividerState.alpha = 1.0f; + dividerState.yTranslation = 0.0f; + stateAnimator.startViewAnimations(mCollapseDivider, dividerState, baseDelay, duration); + } + + public ExpandableNotificationRow getViewAtPosition(float y) { + // find the view under the pointer, accounting for GONE views + final int count = mChildren.size(); + for (int childIdx = 0; childIdx < count; childIdx++) { + ExpandableNotificationRow slidingChild = mChildren.get(childIdx); + float childTop = slidingChild.getTranslationY(); + float top = childTop + slidingChild.getClipTopAmount(); + float bottom = childTop + slidingChild.getActualHeight(); + if (y >= top && y <= bottom) { + return slidingChild; + } + } + return null; + } + + public void setTintColor(int color) { + ExpandableNotificationRow.applyTint(mCollapseDivider, color); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java index 6dcbed6..7040864 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -24,6 +24,7 @@ import android.graphics.Paint; import android.graphics.PointF; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; @@ -41,12 +42,16 @@ import com.android.systemui.statusbar.DismissView; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; +import com.android.systemui.statusbar.NotificationData; +import com.android.systemui.statusbar.NotificationOverflowContainer; import com.android.systemui.statusbar.SpeedBumpView; import com.android.systemui.statusbar.StackScrollerDecorView; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.statusbar.phone.ScrimController; +import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.ScrollAdapter; -import com.android.systemui.statusbar.stack.StackScrollState.ViewState; import java.util.ArrayList; import java.util.HashSet; @@ -56,7 +61,7 @@ import java.util.HashSet; */ public class NotificationStackScrollLayout extends ViewGroup implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter, - ExpandableView.OnHeightChangedListener { + ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener { private static final String TAG = "NotificationStackScrollLayout"; private static final boolean DEBUG = false; @@ -119,15 +124,16 @@ public class NotificationStackScrollLayout extends ViewGroup */ private StackScrollState mCurrentStackScrollState = new StackScrollState(this); private AmbientState mAmbientState = new AmbientState(); - private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>(); - private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>(); - private ArrayList<View> mSnappedBackChildren = new ArrayList<View>(); - private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>(); - private ArrayList<View> mChildrenChangingPositions = new ArrayList<View>(); + private NotificationGroupManager mGroupManager; + private ArrayList<View> mChildrenToAddAnimated = new ArrayList<>(); + private ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>(); + private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<>(); + private ArrayList<View> mSnappedBackChildren = new ArrayList<>(); + private ArrayList<View> mDragAnimPendingChildren = new ArrayList<>(); + private ArrayList<View> mChildrenChangingPositions = new ArrayList<>(); private HashSet<View> mFromMoreCardAdditions = new HashSet<>(); - private ArrayList<AnimationEvent> mAnimationEvents - = new ArrayList<AnimationEvent>(); - private ArrayList<View> mSwipedOutViews = new ArrayList<View>(); + private ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>(); + private ArrayList<View> mSwipedOutViews = new ArrayList<>(); private final StackStateAnimator mStateAnimator = new StackStateAnimator(this); private boolean mAnimationsEnabled; private boolean mChangePositionInProgress; @@ -141,7 +147,6 @@ public class NotificationStackScrollLayout extends ViewGroup * The raw amount of the overScroll on the bottom, which is not rubber-banded. */ private float mOverScrolledBottomPixels; - private OnChildLocationsChangedListener mListener; private OnOverscrollTopChangedListener mOverscrollTopChangedListener; private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; @@ -158,6 +163,7 @@ public class NotificationStackScrollLayout extends ViewGroup private boolean mChildrenUpdateRequested; private SpeedBumpView mSpeedBumpView; private boolean mIsExpansionChanging; + private boolean mPanelTracking; private boolean mExpandingNotification; private boolean mExpandedInThisMotion; private boolean mScrollingEnabled; @@ -169,7 +175,6 @@ public class NotificationStackScrollLayout extends ViewGroup * Was the scroller scrolled to the top when the down motion was observed? */ private boolean mScrolledToTopOnFirstDown; - /** * The minimal amount of over scroll which is needed in order to switch to the quick settings * when over scrolling on a expanded card. @@ -177,10 +182,12 @@ public class NotificationStackScrollLayout extends ViewGroup private float mMinTopOverScrollToEscape; private int mIntrinsicPadding; private int mNotificationTopPadding; + private float mStackTranslation; private float mTopPaddingOverflow; private boolean mDontReportNextOverScroll; private boolean mRequestViewResizeAnimationOnLayout; private boolean mNeedViewResizeAnimation; + private View mExpandedGroupView; private boolean mEverythingNeedsAnimation; /** @@ -201,7 +208,6 @@ public class NotificationStackScrollLayout extends ViewGroup private boolean mDelegateToScrollView; private boolean mDisallowScrollingInThisMotion; private long mGoToFullShadeDelay; - private ViewTreeObserver.OnPreDrawListener mChildrenUpdater = new ViewTreeObserver.OnPreDrawListener() { @Override @@ -214,6 +220,16 @@ public class NotificationStackScrollLayout extends ViewGroup }; private PhoneStatusBar mPhoneStatusBar; private int[] mTempInt2 = new int[2]; + private boolean mGenerateChildOrderChangedEvent; + private HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>(); + private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations + = new HashSet<>(); + private HeadsUpManager mHeadsUpManager; + private boolean mTrackingHeadsUp; + private ScrimController mScrimController; + private boolean mForceNoOverlappingRendering; + private NotificationOverflowContainer mOverflowContainer; + private final ArrayList<Pair<ExpandableNotificationRow, Boolean>> mTmpList = new ArrayList<>(); public NotificationStackScrollLayout(Context context) { this(context, null); @@ -309,7 +325,7 @@ public class NotificationStackScrollLayout extends ViewGroup private void notifyHeightChangeListener(ExpandableView view) { if (mOnHeightChangedListener != null) { - mOnHeightChangedListener.onHeightChanged(view); + mOnHeightChangedListener.onHeightChanged(view, false /* needsAnimation */); } } @@ -329,6 +345,9 @@ public class NotificationStackScrollLayout extends ViewGroup float centerX = getWidth() / 2.0f; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } float width = child.getMeasuredWidth(); float height = child.getMeasuredHeight(); child.layout((int) (centerX - width / 2.0f), @@ -339,16 +358,18 @@ public class NotificationStackScrollLayout extends ViewGroup setMaxLayoutHeight(getHeight()); updateContentHeight(); clampScrollPosition(); - requestAnimationOnViewResize(); + if (mRequestViewResizeAnimationOnLayout) { + requestAnimationOnViewResize(); + mRequestViewResizeAnimationOnLayout = false; + } requestChildrenUpdate(); } private void requestAnimationOnViewResize() { - if (mRequestViewResizeAnimationOnLayout && mIsExpanded && mAnimationsEnabled) { + if (mIsExpanded && mAnimationsEnabled) { mNeedViewResizeAnimation = true; mNeedsAnimation = true; } - mRequestViewResizeAnimationOnLayout = false; } public void updateSpeedBumpIndex(int newIndex) { @@ -375,15 +396,15 @@ public class NotificationStackScrollLayout extends ViewGroup * 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 + * @return one of {@link StackViewState}'s <code>LOCATION_*</code> constants */ public int getChildLocation(View child) { - ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child); + StackViewState childViewState = mCurrentStackScrollState.getViewStateForView(child); if (childViewState == null) { - return ViewState.LOCATION_UNKNOWN; + return StackViewState.LOCATION_UNKNOWN; } if (childViewState.gone) { - return ViewState.LOCATION_GONE; + return StackViewState.LOCATION_GONE; } return childViewState.location; } @@ -394,8 +415,8 @@ public class NotificationStackScrollLayout extends ViewGroup } private void updateAlgorithmHeightAndPadding() { - mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight()); - mStackScrollAlgorithm.setTopPadding(mTopPadding); + mAmbientState.setLayoutHeight(getLayoutHeight()); + mAmbientState.setTopPadding(mTopPadding); } /** @@ -468,9 +489,13 @@ public class NotificationStackScrollLayout extends ViewGroup int newStackHeight = (int) height; int minStackHeight = getMinStackHeight(); int stackHeight; - if (newStackHeight - mTopPadding - mTopPaddingOverflow >= minStackHeight + float paddingOffset; + boolean trackingHeadsUp = mTrackingHeadsUp || mHeadsUpManager.hasPinnedHeadsUp(); + int normalUnfoldPositionStart = trackingHeadsUp ? mHeadsUpManager.getTopHeadsUpHeight() + : minStackHeight; + if (newStackHeight - mTopPadding - mTopPaddingOverflow >= normalUnfoldPositionStart || getNotGoneChildCount() == 0) { - setTranslationY(mTopPaddingOverflow); + paddingOffset = mTopPaddingOverflow; stackHeight = newStackHeight; } else { @@ -482,9 +507,13 @@ public class NotificationStackScrollLayout extends ViewGroup float partiallyThere = (newStackHeight - mTopPadding - mTopPaddingOverflow) / minStackHeight; partiallyThere = Math.max(0, partiallyThere); - translationY += (1 - partiallyThere) * (mBottomStackPeekSize + - mCollapseSecondCardPadding); - setTranslationY(translationY - mTopPadding); + if (!trackingHeadsUp) { + translationY += (1 - partiallyThere) * (mBottomStackPeekSize + + mCollapseSecondCardPadding); + } else { + translationY = (int) (height - mHeadsUpManager.getTopHeadsUpHeight()); + } + paddingOffset = translationY - mTopPadding; stackHeight = (int) (height - (translationY - mTopPadding)); } if (stackHeight != mCurrentStackHeight) { @@ -492,6 +521,19 @@ public class NotificationStackScrollLayout extends ViewGroup updateAlgorithmHeightAndPadding(); requestChildrenUpdate(); } + setStackTranslation(paddingOffset); + } + + public float getStackTranslation() { + return mStackTranslation; + } + + private void setStackTranslation(float stackTranslation) { + if (stackTranslation != mStackTranslation) { + mStackTranslation = stackTranslation; + mAmbientState.setStackTranslation(stackTranslation); + requestChildrenUpdate(); + } } /** @@ -533,11 +575,6 @@ public class NotificationStackScrollLayout extends ViewGroup if (mDismissAllInProgress) { return; } - 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); if (mDragAnimPendingChildren.contains(v)) { // We start the swipe and finish it in the same frame, we don't want any animation @@ -546,6 +583,17 @@ public class NotificationStackScrollLayout extends ViewGroup } mSwipedOutViews.add(v); mAmbientState.onDragFinished(v); + if (v instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) v; + if (row.isHeadsUp()) { + mHeadsUpManager.addSwipedOutNotification(row.getStatusBarNotification().getKey()); + } + } + final View veto = v.findViewById(R.id.veto); + if (veto != null && veto.getVisibility() != View.GONE) { + veto.performClick(); + } + if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); } @Override @@ -565,28 +613,48 @@ public class NotificationStackScrollLayout extends ViewGroup @Override public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { + if (!mIsExpanded && isPinnedHeadsUp(animView) && canChildBeDismissed(animView)) { + mScrimController.setTopHeadsUpDragAmount(animView, + Math.min(Math.abs(swipeProgress - 1.0f), 1.0f)); + } return false; } - @Override - public float getFalsingThresholdFactor() { - return mPhoneStatusBar.isScreenOnComingFromTouch() ? 1.5f : 1.0f; - } - public void onBeginDrag(View v) { setSwipingInProgress(true); mAmbientState.onBeginDrag(v); - if (mAnimationsEnabled) { + if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) { mDragAnimPendingChildren.add(v); mNeedsAnimation = true; } requestChildrenUpdate(); } + public static boolean isPinnedHeadsUp(View v) { + if (v instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) v; + return row.isHeadsUp() && row.isPinned(); + } + return false; + } + + private boolean isHeadsUp(View v) { + if (v instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) v; + return row.isHeadsUp(); + } + return false; + } + public void onDragCancelled(View v) { setSwipingInProgress(false); } + @Override + public float getFalsingThresholdFactor() { + return mPhoneStatusBar.isScreenOnComingFromTouch() ? 1.5f : 1.0f; + } + public View getChildAtPosition(MotionEvent ev) { return getChildAtPosition(ev.getX(), ev.getY()); } @@ -645,6 +713,14 @@ public class NotificationStackScrollLayout extends ViewGroup int right = getWidth(); if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { + if (slidingChild instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) slidingChild; + if (!mIsExpanded && row.isHeadsUp() && row.isPinned() + && mHeadsUpManager.getTopEntry().entry.row != row) { + continue; + } + return row.getViewAtPosition(touchY - childTop); + } return slidingChild; } } @@ -653,7 +729,8 @@ public class NotificationStackScrollLayout extends ViewGroup public boolean canChildBeExpanded(View v) { return v instanceof ExpandableNotificationRow - && ((ExpandableNotificationRow) v).isExpandable(); + && ((ExpandableNotificationRow) v).isExpandable() + && !((ExpandableNotificationRow) v).isHeadsUp(); } public void setUserExpandedChild(View v, boolean userExpanded) { @@ -723,7 +800,6 @@ public class NotificationStackScrollLayout extends ViewGroup } public void dismissViewAnimated(View child, Runnable endRunnable, int delay, long duration) { - child.setClipBounds(null); mSwipeHelper.dismissChild(child, 0, endRunnable, delay, true, duration); } @@ -740,7 +816,7 @@ public class NotificationStackScrollLayout extends ViewGroup } handleEmptySpaceClick(ev); boolean expandWantsIt = false; - if (!mSwipingInProgress && !mOnlyScrollingInThisMotion && isScrollingEnabled()) { + if (mIsExpanded && !mSwipingInProgress && !mOnlyScrollingInThisMotion) { if (isCancelOrUp) { mExpandHelper.onlyObserveMovements(false); } @@ -752,7 +828,8 @@ public class NotificationStackScrollLayout extends ViewGroup } } boolean scrollerWantsIt = false; - if (!mSwipingInProgress && !mExpandingNotification && !mDisallowScrollingInThisMotion) { + if (mIsExpanded && !mSwipingInProgress && !mExpandingNotification + && !mDisallowScrollingInThisMotion) { scrollerWantsIt = onScrollTouch(ev); } boolean horizontalSwipeWantsIt = false; @@ -1295,17 +1372,11 @@ public class NotificationStackScrollLayout extends ViewGroup int childCount = getChildCount(); int count = 0; for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child.getVisibility() != View.GONE) { + ExpandableView child = (ExpandableView) getChildAt(i); + if (child.getVisibility() != View.GONE && !child.willBeGone()) { count++; } } - if (mDismissView.willBeGone()) { - count--; - } - if (mEmptyShadeView.willBeGone()) { - count--; - } return count; } @@ -1330,12 +1401,9 @@ public class NotificationStackScrollLayout extends ViewGroup // add the padding before this element height += mPaddingBetweenElements; } - if (child instanceof ExpandableNotificationRow) { - ExpandableNotificationRow row = (ExpandableNotificationRow) child; - height += row.getIntrinsicHeight(); - } else if (child instanceof ExpandableView) { + if (child instanceof ExpandableView) { ExpandableView expandableView = (ExpandableView) child; - height += expandableView.getActualHeight(); + height += expandableView.getIntrinsicHeight(); } } } @@ -1446,7 +1514,7 @@ public class NotificationStackScrollLayout extends ViewGroup } if (mExpandedInThisMotion) { return RUBBER_BAND_FACTOR_AFTER_EXPAND; - } else if (mIsExpansionChanging) { + } else if (mIsExpansionChanging || mPanelTracking) { return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND; } else if (mScrolledToTopOnFirstDown) { return 1.0f; @@ -1460,7 +1528,7 @@ public class NotificationStackScrollLayout extends ViewGroup * overscroll view (e.g. expand QS). */ private boolean isRubberbanded(boolean onTop) { - return !onTop || mExpandedInThisMotion || mIsExpansionChanging + return !onTop || mExpandedInThisMotion || mIsExpansionChanging || mPanelTracking || !mScrolledToTopOnFirstDown; } @@ -1496,7 +1564,7 @@ public class NotificationStackScrollLayout extends ViewGroup initDownStates(ev); handleEmptySpaceClick(ev); boolean expandWantsIt = false; - if (!mSwipingInProgress && !mOnlyScrollingInThisMotion && isScrollingEnabled()) { + if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) { expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev); } boolean scrollWantsIt = false; @@ -1542,8 +1610,16 @@ public class NotificationStackScrollLayout extends ViewGroup } @Override - protected void onViewRemoved(View child) { + public void onViewRemoved(View child) { super.onViewRemoved(child); + // we only call our internal methods if this is actually a removal and not just a + // notification which becomes a child notification + if (!isChildInGroup(child)) { + onViewRemovedInternal(child); + } + } + + private void onViewRemovedInternal(View child) { mStackScrollAlgorithm.notifyChildrenChanged(this); if (mChangePositionInProgress) { // This is only a position change, don't do anything special @@ -1561,7 +1637,13 @@ public class NotificationStackScrollLayout extends ViewGroup updateAnimationState(false, child); // Make sure the clipRect we might have set is removed - child.setClipBounds(null); + ((ExpandableView) child).setClipTopOptimization(0); + } + + private boolean isChildInGroup(View child) { + return child instanceof ExpandableNotificationRow + && mGroupManager.isChildInGroupWithSummary( + ((ExpandableNotificationRow) child).getStatusBarNotification()); } /** @@ -1571,7 +1653,11 @@ public class NotificationStackScrollLayout extends ViewGroup * @return Whether an animation was generated. */ private boolean generateRemoveAnimation(View child) { - if (mIsExpanded && mAnimationsEnabled) { + if (removeRemovedChildFromHeadsUpChangeAnimations(child)) { + mAddedHeadsUpChildren.remove(child); + return false; + } + if (mIsExpanded && mAnimationsEnabled && !isChildInInvisibleGroup(child)) { if (!mChildrenToAddAnimated.contains(child)) { // Generate Animations mChildrenToRemoveAnimated.add(child); @@ -1587,6 +1673,46 @@ public class NotificationStackScrollLayout extends ViewGroup } /** + * Remove a removed child view from the heads up animations if it was just added there + * + * @return whether any child was removed from the list to animate + */ + private boolean removeRemovedChildFromHeadsUpChangeAnimations(View child) { + boolean hasAddEvent = false; + for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) { + ExpandableNotificationRow row = eventPair.first; + boolean isHeadsUp = eventPair.second; + if (child == row) { + mTmpList.add(eventPair); + hasAddEvent |= isHeadsUp; + } + } + if (hasAddEvent) { + // This child was just added lets remove all events. + mHeadsUpChangeAnimations.removeAll(mTmpList); + } + mTmpList.clear(); + return hasAddEvent; + } + + /** + * @param child the child to query + * @return whether a view is not a top level child but a child notification and that group is + * not expanded + */ + private boolean isChildInInvisibleGroup(View child) { + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + ExpandableNotificationRow groupSummary = + mGroupManager.getGroupSummary(row.getStatusBarNotification()); + if (groupSummary != null && groupSummary != row) { + return !groupSummary.areChildrenExpanded(); + } + } + return false; + } + + /** * Updates the scroll position when a child was removed * * @param removedChild the removed child @@ -1632,8 +1758,12 @@ public class NotificationStackScrollLayout extends ViewGroup } @Override - protected void onViewAdded(View child) { + public void onViewAdded(View child) { super.onViewAdded(child); + onViewAddedInternal(child); + } + + private void onViewAddedInternal(View child) { mStackScrollAlgorithm.notifyChildrenChanged(this); ((ExpandableView) child).setOnHeightChangedListener(this); generateAddAnimation(child, false /* fromMoreCard */); @@ -1646,22 +1776,31 @@ public class NotificationStackScrollLayout extends ViewGroup } } + public void notifyGroupChildRemoved(View row) { + onViewRemovedInternal(row); + } + + public void notifyGroupChildAdded(View row) { + onViewAddedInternal(row); + } + public void setAnimationsEnabled(boolean animationsEnabled) { mAnimationsEnabled = animationsEnabled; updateNotificationAnimationStates(); } private void updateNotificationAnimationStates() { - boolean running = mIsExpanded && mAnimationsEnabled; + boolean running = mAnimationsEnabled; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); + running &= mIsExpanded || isPinnedHeadsUp(child); updateAnimationState(running, child); } } private void updateAnimationState(View child) { - updateAnimationState(mAnimationsEnabled && mIsExpanded, child); + updateAnimationState((mAnimationsEnabled || isPinnedHeadsUp(child)) && mIsExpanded, child); } @@ -1691,6 +1830,10 @@ public class NotificationStackScrollLayout extends ViewGroup } mNeedsAnimation = true; } + if (isHeadsUp(child)) { + mAddedHeadsUpChildren.add(child); + mChildrenToAddAnimated.remove(child); + } } /** @@ -1729,6 +1872,7 @@ public class NotificationStackScrollLayout extends ViewGroup } private void generateChildHierarchyEvents() { + generateHeadsUpAnimationEvents(); generateChildRemovalEvents(); generateChildAdditionEvents(); generatePositionChangeEvents(); @@ -1741,10 +1885,62 @@ public class NotificationStackScrollLayout extends ViewGroup generateDarkEvent(); generateGoToFullShadeEvent(); generateViewResizeEvent(); + generateGroupExpansionEvent(); generateAnimateEverythingEvent(); mNeedsAnimation = false; } + private void generateHeadsUpAnimationEvents() { + for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) { + ExpandableNotificationRow row = eventPair.first; + boolean isHeadsUp = eventPair.second; + int type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER; + boolean onBottom = false; + boolean pinnedAndClosed = row.isPinned() && !mIsExpanded; + if (!mIsExpanded && !isHeadsUp) { + type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR; + } else { + StackViewState viewState = mCurrentStackScrollState.getViewStateForView(row); + if (viewState == null) { + // A view state was never generated for this view, so we don't need to animate + // this. This may happen with notification children. + continue; + } + if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) { + if (pinnedAndClosed || shouldHunAppearFromBottom(viewState)) { + // Our custom add animation + type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR; + } else { + // Normal add animation + type = AnimationEvent.ANIMATION_TYPE_ADD; + } + onBottom = !pinnedAndClosed; + } + } + AnimationEvent event = new AnimationEvent(row, type); + event.headsUpFromBottom = onBottom; + mAnimationEvents.add(event); + } + mHeadsUpChangeAnimations.clear(); + mAddedHeadsUpChildren.clear(); + } + + private boolean shouldHunAppearFromBottom(StackViewState viewState) { + if (viewState.yTranslation + viewState.height < mAmbientState.getMaxHeadsUpTranslation()) { + return false; + } + return true; + } + + private void generateGroupExpansionEvent() { + // Generate a group expansion/collapsing event if there is such a group at all + if (mExpandedGroupView != null) { + mAnimationEvents.add(new AnimationEvent(mExpandedGroupView, + AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED)); + mExpandedGroupView = null; + } + } + private void generateViewResizeEvent() { if (mNeedViewResizeAnimation) { mAnimationEvents.add( @@ -1791,6 +1987,11 @@ public class NotificationStackScrollLayout extends ViewGroup AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)); } mChildrenChangingPositions.clear(); + if (mGenerateChildOrderChangedEvent) { + mAnimationEvents.add(new AnimationEvent(null, + AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)); + mGenerateChildOrderChangedEvent = false; + } } private void generateChildAdditionEvents() { @@ -2049,6 +2250,18 @@ public class NotificationStackScrollLayout extends ViewGroup } } + public void onPanelTrackingStarted() { + mPanelTracking = true; + } + public void onPanelTrackingStopped() { + mPanelTracking = false; + } + + public void resetScrollPosition() { + mScroller.abortAnimation(); + mOwnScrollY = 0; + } + private void setIsExpanded(boolean isExpanded) { boolean changed = isExpanded != mIsExpanded; mIsExpanded = isExpanded; @@ -2059,11 +2272,14 @@ public class NotificationStackScrollLayout extends ViewGroup } @Override - public void onHeightChanged(ExpandableView view) { + public void onHeightChanged(ExpandableView view, boolean needsAnimation) { updateContentHeight(); updateScrollPositionOnExpandInBottom(view); clampScrollPosition(); notifyHeightChangeListener(view); + if (needsAnimation) { + requestAnimationOnViewResize(); + } requestChildrenUpdate(); } @@ -2079,11 +2295,11 @@ public class NotificationStackScrollLayout extends ViewGroup private void updateScrollPositionOnExpandInBottom(ExpandableView view) { if (view instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) view; - if (row.isUserLocked()) { + if (row.isUserLocked() && row != getFirstChildNotGone()) { // We are actually expanding this view float endPosition = row.getTranslationY() + row.getActualHeight(); int stackEnd = mMaxLayoutHeight - mBottomStackPeekSize - - mBottomStackSlowDownHeight; + mBottomStackSlowDownHeight + (int) mStackTranslation; if (endPosition > stackEnd) { mOwnScrollY += endPosition - stackEnd; mDisallowScrollingInThisMotion = true; @@ -2103,6 +2319,14 @@ public class NotificationStackScrollLayout extends ViewGroup public void onChildAnimationFinished() { requestChildrenUpdate(); + runAnimationFinishedRunnables(); + } + + private void runAnimationFinishedRunnables() { + for (Runnable runnable : mAnimationFinishedRunnables) { + runnable.run(); + } + mAnimationFinishedRunnables.clear(); } /** @@ -2156,6 +2380,7 @@ public class NotificationStackScrollLayout extends ViewGroup if (mListener != null) { mListener.onChildLocationsChanged(this); } + runAnimationFinishedRunnables(); } public void setSpeedBumpView(SpeedBumpView speedBumpView) { @@ -2204,7 +2429,7 @@ public class NotificationStackScrollLayout extends ViewGroup * @return the y position of the first notification */ public float getNotificationsTopY() { - return mTopPadding + getTranslationY(); + return mTopPadding + getStackTranslation(); } @Override @@ -2278,7 +2503,7 @@ public class NotificationStackScrollLayout extends ViewGroup mEmptyShadeView.setVisibility(newVisibility); mEmptyShadeView.setWillBeGone(false); updateContentHeight(); - notifyHeightChangeListener(mDismissView); + notifyHeightChangeListener(mEmptyShadeView); } else { Runnable onFinishedRunnable = new Runnable() { @Override @@ -2286,7 +2511,7 @@ public class NotificationStackScrollLayout extends ViewGroup mEmptyShadeView.setVisibility(GONE); mEmptyShadeView.setWillBeGone(false); updateContentHeight(); - notifyHeightChangeListener(mDismissView); + notifyHeightChangeListener(mEmptyShadeView); } }; if (mAnimationsEnabled) { @@ -2300,6 +2525,45 @@ public class NotificationStackScrollLayout extends ViewGroup } } + public void setOverflowContainer(NotificationOverflowContainer overFlowContainer) { + mOverflowContainer = overFlowContainer; + addView(mOverflowContainer); + } + + public void updateOverflowContainerVisibility(boolean visible) { + int oldVisibility = mOverflowContainer.willBeGone() ? GONE + : mOverflowContainer.getVisibility(); + final int newVisibility = visible ? VISIBLE : GONE; + if (oldVisibility != newVisibility) { + Runnable onFinishedRunnable = new Runnable() { + @Override + public void run() { + mOverflowContainer.setVisibility(newVisibility); + mOverflowContainer.setWillBeGone(false); + updateContentHeight(); + notifyHeightChangeListener(mOverflowContainer); + } + }; + if (!mAnimationsEnabled || !mIsExpanded) { + mOverflowContainer.cancelAppearDrawing(); + onFinishedRunnable.run(); + } else if (newVisibility != GONE) { + mOverflowContainer.performAddAnimation(0, + StackStateAnimator.ANIMATION_DURATION_STANDARD); + mOverflowContainer.setVisibility(newVisibility); + mOverflowContainer.setWillBeGone(false); + updateContentHeight(); + notifyHeightChangeListener(mOverflowContainer); + } else { + mOverflowContainer.performRemoveAnimation( + StackStateAnimator.ANIMATION_DURATION_STANDARD, + 0.0f, + onFinishedRunnable); + mOverflowContainer.setWillBeGone(true); + } + } + } + public void updateDismissView(boolean visible) { int oldVisibility = mDismissView.willBeGone() ? GONE : mDismissView.getVisibility(); int newVisibility = visible ? VISIBLE : GONE; @@ -2338,6 +2602,20 @@ public class NotificationStackScrollLayout extends ViewGroup public void setDismissAllInProgress(boolean dismissAllInProgress) { mDismissAllInProgress = dismissAllInProgress; mDismissView.setDismissAllInProgress(dismissAllInProgress); + if (dismissAllInProgress) { + disableClipOptimization(); + } + } + + private void disableClipOptimization() { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + ExpandableView child = (ExpandableView) getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } + child.setClipTopOptimization(0); + } } public boolean isDismissViewNotGone() { @@ -2377,7 +2655,7 @@ public class NotificationStackScrollLayout extends ViewGroup max = bottom; } } - return max + getTranslationY(); + return max + getStackTranslation(); } /** @@ -2392,29 +2670,147 @@ public class NotificationStackScrollLayout extends ViewGroup this.mPhoneStatusBar = phoneStatusBar; } + public void setGroupManager(NotificationGroupManager groupManager) { + this.mGroupManager = groupManager; + } + public void onGoToKeyguard() { + requestAnimateEverything(); + } + + private void requestAnimateEverything() { if (mIsExpanded && mAnimationsEnabled) { mEverythingNeedsAnimation = true; + mNeedsAnimation = true; requestChildrenUpdate(); } } - private boolean isBelowLastNotification(float touchX, float touchY) { - ExpandableView lastChildNotGone = (ExpandableView) getLastChildNotGone(); - if (lastChildNotGone == null) { - return touchY > mIntrinsicPadding; + public boolean isBelowLastNotification(float touchX, float touchY) { + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + ExpandableView child = (ExpandableView) getChildAt(i); + if (child.getVisibility() != View.GONE) { + float childTop = child.getY(); + if (childTop > touchY) { + // we are above a notification entirely let's abort + return false; + } + boolean belowChild = touchY > childTop + child.getActualHeight(); + if (child == mDismissView) { + if(!belowChild && !mDismissView.isOnEmptySpace(touchX - mDismissView.getX(), + touchY - childTop)) { + // We clicked on the dismiss button + return false; + } + } else if (child == mEmptyShadeView) { + // We arrived at the empty shade view, for which we accept all clicks + return true; + } else if (!belowChild){ + // We are on a child + return false; + } + } + } + return touchY > mTopPadding + mStackTranslation; + } + + private void updateExpandButtons() { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + row.updateExpandButton(); + } } - if (lastChildNotGone != mDismissView && lastChildNotGone != mEmptyShadeView) { - return touchY > lastChildNotGone.getY() + lastChildNotGone.getActualHeight(); - } else if (lastChildNotGone == mEmptyShadeView) { - return touchY > mEmptyShadeView.getY(); - } else { - float dismissY = mDismissView.getY(); - boolean belowDismissView = touchY > dismissY + mDismissView.getActualHeight(); - return belowDismissView || (touchY > dismissY - && mDismissView.isOnEmptySpace(touchX - mDismissView.getX(), - touchY - dismissY)); + } + + @Override + public void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded) { + boolean animated = mAnimationsEnabled && mIsExpanded; + if (animated) { + mExpandedGroupView = changedRow; + mNeedsAnimation = true; } + changedRow.setChildrenExpanded(expanded, animated); + onHeightChanged(changedRow, false /* needsAnimation */); + } + + @Override + public void onGroupsProhibitedChanged() { + updateExpandButtons(); + } + + @Override + public void onGroupCreatedFromChildren(NotificationGroupManager.NotificationGroup group) { + for (NotificationData.Entry entry : group.children) { + ExpandableNotificationRow row = entry.row; + if (indexOfChild(row) != -1) { + removeView(row); + group.summary.row.addChildNotification(row); + } + } + } + + public void generateChildOrderChangedEvent() { + if (mIsExpanded && mAnimationsEnabled) { + mGenerateChildOrderChangedEvent = true; + mNeedsAnimation = true; + requestChildrenUpdate(); + } + } + + public void runAfterAnimationFinished(Runnable runnable) { + mAnimationFinishedRunnables.add(runnable); + } + + public void setHeadsUpManager(HeadsUpManager headsUpManager) { + mHeadsUpManager = headsUpManager; + mAmbientState.setHeadsUpManager(headsUpManager); + mStackScrollAlgorithm.setHeadsUpManager(headsUpManager); + } + + public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) { + if (mAnimationsEnabled) { + mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp)); + mNeedsAnimation = true; + requestChildrenUpdate(); + } + } + + public void setShadeExpanded(boolean shadeExpanded) { + mAmbientState.setShadeExpanded(shadeExpanded); + mStateAnimator.setShadeExpanded(shadeExpanded); + } + + /** + * Set the boundary for the bottom heads up position. The heads up will always be above this + * position. + * + * @param height the height of the screen + * @param bottomBarHeight the height of the bar on the bottom + */ + public void setHeadsUpBoundaries(int height, int bottomBarHeight) { + mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight); + mStateAnimator.setHeadsUpAppearHeightBottom(height); + requestChildrenUpdate(); + } + + public void setTrackingHeadsUp(boolean trackingHeadsUp) { + mTrackingHeadsUp = trackingHeadsUp; + } + + public void setScrimController(ScrimController scrimController) { + mScrimController = scrimController; + } + + public void forceNoOverlappingRendering(boolean force) { + mForceNoOverlappingRendering = force; + } + + @Override + public boolean hasOverlappingRendering() { + return !mForceNoOverlappingRendering && super.hasOverlappingRendering(); } /** @@ -2553,6 +2949,38 @@ public class NotificationStackScrollLayout extends ViewGroup .animateY() .animateZ(), + // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED + new AnimationFilter() + .animateAlpha() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ(), + + // ANIMATION_TYPE_HEADS_UP_APPEAR + new AnimationFilter() + .animateAlpha() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ(), + + // ANIMATION_TYPE_HEADS_UP_DISAPPEAR + new AnimationFilter() + .animateAlpha() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ(), + + // ANIMATION_TYPE_HEADS_UP_OTHER + new AnimationFilter() + .animateAlpha() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ(), + // ANIMATION_TYPE_EVERYTHING new AnimationFilter() .animateAlpha() @@ -2607,6 +3035,18 @@ public class NotificationStackScrollLayout extends ViewGroup // ANIMATION_TYPE_VIEW_RESIZE StackStateAnimator.ANIMATION_DURATION_STANDARD, + // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED + StackStateAnimator.ANIMATION_DURATION_EXPAND_CLICKED, + + // ANIMATION_TYPE_HEADS_UP_APPEAR + StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR, + + // ANIMATION_TYPE_HEADS_UP_DISAPPEAR + StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR, + + // ANIMATION_TYPE_HEADS_UP_OTHER + StackStateAnimator.ANIMATION_DURATION_STANDARD, + // ANIMATION_TYPE_EVERYTHING StackStateAnimator.ANIMATION_DURATION_STANDARD, }; @@ -2624,7 +3064,11 @@ public class NotificationStackScrollLayout extends ViewGroup static final int ANIMATION_TYPE_GO_TO_FULL_SHADE = 10; static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11; static final int ANIMATION_TYPE_VIEW_RESIZE = 12; - static final int ANIMATION_TYPE_EVERYTHING = 13; + static final int ANIMATION_TYPE_GROUP_EXPANSION_CHANGED = 13; + static final int ANIMATION_TYPE_HEADS_UP_APPEAR = 14; + static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR = 15; + static final int ANIMATION_TYPE_HEADS_UP_OTHER = 16; + static final int ANIMATION_TYPE_EVERYTHING = 17; static final int DARK_ANIMATION_ORIGIN_INDEX_ABOVE = -1; static final int DARK_ANIMATION_ORIGIN_INDEX_BELOW = -2; @@ -2636,6 +3080,7 @@ public class NotificationStackScrollLayout extends ViewGroup final long length; View viewAfterChangingView; int darkAnimationOriginIndex; + boolean headsUpFromBottom; AnimationEvent(View view, int type) { this(view, type, LENGTHS[type]); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java index ddc4251..5c604b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java @@ -25,8 +25,10 @@ import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; +import com.android.systemui.statusbar.policy.HeadsUpManager; import java.util.ArrayList; +import java.util.List; /** * The Algorithm of the {@link com.android.systemui.statusbar.stack @@ -53,11 +55,6 @@ public class StackScrollAlgorithm { 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; @@ -67,12 +64,14 @@ public class StackScrollAlgorithm { private int mTopStackTotalSize; private int mPaddingBetweenElementsDimmed; private int mPaddingBetweenElementsNormal; + private int mNotificationsTopPadding; private int mBottomStackSlowDownLength; private int mTopStackSlowDownLength; private int mCollapseSecondCardPadding; private boolean mIsSmallScreen; private int mMaxNotificationHeight; private boolean mScaleDimmed; + private HeadsUpManager mHeadsUpManager; public StackScrollAlgorithm(Context context) { initConstants(context); @@ -106,6 +105,8 @@ public class StackScrollAlgorithm { .getDimensionPixelSize(R.dimen.notification_padding_dimmed); mPaddingBetweenElementsNormal = context.getResources() .getDimensionPixelSize(R.dimen.notification_padding); + mNotificationsTopPadding = context.getResources() + .getDimensionPixelSize(R.dimen.notifications_top_padding); mCollapsedSize = context.getResources() .getDimensionPixelSize(R.dimen.notification_min_height); mMaxNotificationHeight = context.getResources() @@ -159,18 +160,31 @@ public class StackScrollAlgorithm { updateVisibleChildren(resultState, algorithmState); // Phase 1: - findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState); + findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState, ambientState); // Phase 2: - updatePositionsForState(resultState, algorithmState); + updatePositionsForState(resultState, algorithmState, ambientState); // Phase 3: updateZValuesForState(resultState, algorithmState); handleDraggedViews(ambientState, resultState, algorithmState); updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState); - updateClipping(resultState, algorithmState); + updateClipping(resultState, algorithmState, ambientState); updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex()); + getNotificationChildrenStates(resultState, algorithmState); + } + + private void getNotificationChildrenStates(StackScrollState resultState, + StackScrollAlgorithmState algorithmState) { + int childCount = algorithmState.visibleChildren.size(); + for (int i = 0; i < childCount; i++) { + ExpandableView v = algorithmState.visibleChildren.get(i); + if (v instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) v; + row.getChildrenStates(resultState); + } + } } private void updateSpeedBumpState(StackScrollState resultState, @@ -178,7 +192,7 @@ public class StackScrollAlgorithm { int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); - StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); + StackViewState childViewState = resultState.getViewStateForView(child); // The speed bump can also be gone, so equality needs to be taken when comparing // indices. @@ -187,14 +201,14 @@ public class StackScrollAlgorithm { } private void updateClipping(StackScrollState resultState, - StackScrollAlgorithmState algorithmState) { + StackScrollAlgorithmState algorithmState, AmbientState ambientState) { float previousNotificationEnd = 0; float previousNotificationStart = 0; boolean previousNotificationIsSwiped = false; int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableView child = algorithmState.visibleChildren.get(i); - StackScrollState.ViewState state = resultState.getViewStateForView(child); + StackViewState state = resultState.getViewStateForView(child); float newYTranslation = state.yTranslation + state.height * (1f - state.scale) / 2f; float newHeight = state.height * state.scale; // apply clipping and shadow @@ -228,7 +242,7 @@ public class StackScrollAlgorithm { // otherwise we would clip to a transparent view. previousNotificationStart = newYTranslation + state.clipTopAmount * state.scale; previousNotificationEnd = newNotificationEnd; - previousNotificationIsSwiped = child.getTranslationX() != 0; + previousNotificationIsSwiped = ambientState.getDraggedViews().contains(child); } } } @@ -242,8 +256,8 @@ public class StackScrollAlgorithm { * @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(StackScrollState.ViewState state, - float realHeight, float clipHeight, float backgroundHeight) { + private void updateChildClippingAndBackground(StackViewState state, float realHeight, + float clipHeight, float backgroundHeight) { if (realHeight > clipHeight) { // Rather overlap than create a hole. state.topOverLap = (int) Math.floor((realHeight - clipHeight) / state.scale); @@ -270,7 +284,7 @@ public class StackScrollAlgorithm { int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); - StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); + StackViewState childViewState = resultState.getViewStateForView(child); childViewState.dimmed = dimmed; childViewState.dark = dark; childViewState.hideSensitive = hideSensitive; @@ -297,14 +311,16 @@ public class StackScrollAlgorithm { if (!draggedViews.contains(nextChild)) { // only if the view is not dragged itself we modify its state to be fully // visible - StackScrollState.ViewState viewState = resultState.getViewStateForView( + StackViewState viewState = resultState.getViewStateForView( nextChild); // The child below the dragged one must be fully visible - viewState.alpha = 1; + if (ambientState.isShadeExpanded()) { + viewState.alpha = 1; + } } // Lets set the alpha to the one it currently has, as its currently being dragged - StackScrollState.ViewState viewState = resultState.getViewStateForView(draggedView); + StackViewState viewState = resultState.getViewStateForView(draggedView); // The dragged child should keep the set alpha viewState.alpha = draggedView.getAlpha(); } @@ -320,27 +336,54 @@ public class StackScrollAlgorithm { int childCount = hostView.getChildCount(); state.visibleChildren.clear(); state.visibleChildren.ensureCapacity(childCount); + int notGoneIndex = 0; for (int i = 0; i < childCount; i++) { ExpandableView v = (ExpandableView) hostView.getChildAt(i); if (v.getVisibility() != View.GONE) { - StackScrollState.ViewState viewState = resultState.getViewStateForView(v); - viewState.notGoneIndex = state.visibleChildren.size(); - state.visibleChildren.add(v); + notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v); + if (v instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) v; + + // handle the notgoneIndex for the children as well + List<ExpandableNotificationRow> children = + row.getNotificationChildren(); + if (row.areChildrenExpanded() && children != null) { + for (ExpandableNotificationRow childRow : children) { + if (childRow.getVisibility() != View.GONE) { + StackViewState childState + = resultState.getViewStateForView(childRow); + childState.notGoneIndex = notGoneIndex; + notGoneIndex++; + } + } + } + } } } } + private int updateNotGoneIndex(StackScrollState resultState, + StackScrollAlgorithmState state, int notGoneIndex, + ExpandableView v) { + StackViewState viewState = resultState.getViewStateForView(v); + viewState.notGoneIndex = notGoneIndex; + state.visibleChildren.add(v); + notGoneIndex++; + return notGoneIndex; + } + /** * Determine the positions for the views. This is the main part of the algorithm. * * @param resultState The result state to update if a change to the properties of a child occurs * @param algorithmState The state in which the current pass of the algorithm is currently in + * @param ambientState The current ambient state */ private void updatePositionsForState(StackScrollState resultState, - StackScrollAlgorithmState algorithmState) { + StackScrollAlgorithmState algorithmState, AmbientState ambientState) { // The starting position of the bottom stack peek - float bottomPeekStart = mInnerHeight - mBottomStackPeekSize; + float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize; // The position where the bottom stack starts. float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength; @@ -351,13 +394,17 @@ public class StackScrollAlgorithm { // How far in is the element currently transitioning into the bottom stack. float yPositionInScrollView = 0.0f; + // If we have a heads-up higher than the collapsed height we need to add the difference to + // the padding of all other elements, i.e push in the top stack slightly. + ExpandableNotificationRow topHeadsUpEntry = ambientState.getTopHeadsUpEntry(); + 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); + StackViewState childViewState = resultState.getViewStateForView(child); + childViewState.location = StackViewState.LOCATION_UNKNOWN; + int childHeight = getMaxAllowedChildHeight(child, ambientState); float yPositionInScrollViewAfterElement = yPositionInScrollView + childHeight + mPaddingBetweenElements; @@ -394,7 +441,8 @@ public class StackScrollAlgorithm { bottomPeekStart, childViewState.yTranslation, childViewState, childHeight); } - clampPositionToBottomStackStart(childViewState, childViewState.height); + clampPositionToBottomStackStart(childViewState, childViewState.height, + ambientState); } else if (nextYPosition >= bottomStackStart) { // Case 2: // We are in the bottom stack. @@ -402,7 +450,7 @@ public class StackScrollAlgorithm { // 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); + bottomStackStart, childViewState, childHeight, ambientState); } else { // According to the regular scroll view we are currently translating out of / // into the bottom of the screen @@ -413,8 +461,8 @@ public class StackScrollAlgorithm { } else { // Case 3: // We are in the regular scroll area. - childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; - clampYTranslation(childViewState, childHeight); + childViewState.location = StackViewState.LOCATION_MAIN_AREA; + clampYTranslation(childViewState, childHeight, ambientState); } // The first card is always rendered. @@ -427,15 +475,64 @@ public class StackScrollAlgorithm { bottomPeekStart - mCollapseSecondCardPadding - childViewState.yTranslation, mCollapsedSize); } - childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD; + childViewState.location = StackViewState.LOCATION_FIRST_CARD; } - if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) { + if (childViewState.location == StackViewState.LOCATION_UNKNOWN) { Log.wtf(LOG_TAG, "Failed to assign location for child " + i); } currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements; yPositionInScrollView = yPositionInScrollViewAfterElement; - childViewState.yTranslation += mTopPadding; + if (ambientState.isShadeExpanded() && topHeadsUpEntry != null + && child != topHeadsUpEntry) { + childViewState.yTranslation += topHeadsUpEntry.getHeadsUpHeight() - mCollapsedSize; + } + childViewState.yTranslation += ambientState.getTopPadding() + + ambientState.getStackTranslation(); + } + updateHeadsUpStates(resultState, algorithmState, ambientState); + } + + private void updateHeadsUpStates(StackScrollState resultState, + StackScrollAlgorithmState algorithmState, AmbientState ambientState) { + int childCount = algorithmState.visibleChildren.size(); + ExpandableNotificationRow topHeadsUpEntry = null; + for (int i = 0; i < childCount; i++) { + View child = algorithmState.visibleChildren.get(i); + if (!(child instanceof ExpandableNotificationRow)) { + break; + } + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + if (!row.isHeadsUp()) { + break; + } else if (topHeadsUpEntry == null) { + topHeadsUpEntry = row; + } + StackViewState childState = resultState.getViewStateForView(row); + boolean isTopEntry = topHeadsUpEntry == row; + if (mIsExpanded) { + if (isTopEntry) { + childState.height += row.getHeadsUpHeight() - mCollapsedSize; + } + childState.height = Math.max(childState.height, row.getHeadsUpHeight()); + // Ensure that the heads up is always visible even when scrolled off from the bottom + float bottomPosition = ambientState.getMaxHeadsUpTranslation() - childState.height; + childState.yTranslation = Math.min(childState.yTranslation, + bottomPosition); + } + if (row.isPinned()) { + childState.yTranslation = Math.max(childState.yTranslation, + mNotificationsTopPadding); + childState.height = row.getHeadsUpHeight(); + if (!isTopEntry) { + // Ensure that a headsUp doesn't vertically extend further than the heads-up at + // the top most z-position + StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry); + childState.height = row.getHeadsUpHeight(); + childState.yTranslation = topState.yTranslation + topState.height + - childState.height; + } + } } } @@ -445,8 +542,9 @@ public class StackScrollAlgorithm { * @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); + private void clampYTranslation(StackViewState childViewState, int childHeight, + AmbientState ambientState) { + clampPositionToBottomStackStart(childViewState, childHeight, ambientState); clampPositionToTopStackEnd(childViewState, childHeight); } @@ -457,39 +555,45 @@ public class StackScrollAlgorithm { * @param childViewState the view state of the child * @param childHeight the height of this child */ - private void clampPositionToBottomStackStart(StackScrollState.ViewState childViewState, - int childHeight) { + private void clampPositionToBottomStackStart(StackViewState childViewState, + int childHeight, AmbientState ambientState) { childViewState.yTranslation = Math.min(childViewState.yTranslation, - mInnerHeight - mBottomStackPeekSize - mCollapseSecondCardPadding - childHeight); + ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding + - childHeight); } /** * Clamp the yTranslation of the child up such that its end is at lest on the end of the top - * stack.get + * stack. * * @param childViewState the view state of the child * @param childHeight the height of this child */ - private void clampPositionToTopStackEnd(StackScrollState.ViewState childViewState, + private void clampPositionToTopStackEnd(StackViewState childViewState, int childHeight) { childViewState.yTranslation = Math.max(childViewState.yTranslation, mCollapsedSize - childHeight); } - private int getMaxAllowedChildHeight(View child) { + private int getMaxAllowedChildHeight(View child, AmbientState ambientState) { if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; + if (ambientState == null && row.isHeadsUp() + || ambientState != null && ambientState.getTopHeadsUpEntry() == child) { + int extraSize = row.getIntrinsicHeight() - row.getHeadsUpHeight(); + return mCollapsedSize + extraSize; + } return row.getIntrinsicHeight(); } else if (child instanceof ExpandableView) { ExpandableView expandableView = (ExpandableView) child; - return expandableView.getActualHeight(); + return expandableView.getIntrinsicHeight(); } return child == null? mCollapsedSize : child.getHeight(); } private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, float bottomPeakStart, float currentYPosition, - StackScrollState.ViewState childViewState, int childHeight) { + StackViewState childViewState, int childHeight) { // This is the transitioning element on top of bottom stack, calculate how far we are in. algorithmState.partialInBottom = 1.0f - ( @@ -510,13 +614,12 @@ public class StackScrollAlgorithm { // We want at least to be at the end of the top stack when collapsing clampPositionToTopStackEnd(childViewState, newHeight); - childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; + childViewState.location = StackViewState.LOCATION_MAIN_AREA; } private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, - float transitioningPositionStart, StackScrollState.ViewState childViewState, - int childHeight) { - + float transitioningPositionStart, StackViewState childViewState, + int childHeight, AmbientState ambientState) { float currentYPosition; algorithmState.itemsInBottomStack += 1.0f; if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { @@ -524,7 +627,7 @@ public class StackScrollAlgorithm { currentYPosition = transitioningPositionStart + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack) - mPaddingBetweenElements; - childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING; + childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING; } else { // we are fully inside the stack if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { @@ -533,8 +636,8 @@ public class StackScrollAlgorithm { > MAX_ITEMS_IN_BOTTOM_STACK + 1) { childViewState.alpha = 1.0f - algorithmState.partialInBottom; } - childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN; - currentYPosition = mInnerHeight; + childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN; + currentYPosition = ambientState.getInnerHeight(); } childViewState.yTranslation = currentYPosition - childHeight; clampPositionToTopStackEnd(childViewState, childHeight); @@ -542,7 +645,7 @@ public class StackScrollAlgorithm { private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, int numberOfElementsCompletelyIn, int i, int childHeight, - StackScrollState.ViewState childViewState, float scrollOffset) { + StackViewState childViewState, float scrollOffset) { // First we calculate the index relative to the current stack window of size at most @@ -574,7 +677,7 @@ public class StackScrollAlgorithm { - mTopStackIndentationFunctor.getValue(numItemsBefore); childViewState.yTranslation = currentChildEndY - childHeight; } - childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; + childViewState.location = StackViewState.LOCATION_TOP_STACK_PEEKING; } else { if (paddedIndex == -1) { childViewState.alpha = 1.0f - algorithmState.partialInTop; @@ -583,7 +686,7 @@ public class StackScrollAlgorithm { childViewState.alpha = 0.0f; } childViewState.yTranslation = mCollapsedSize - childHeight; - childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN; + childViewState.location = StackViewState.LOCATION_TOP_STACK_HIDDEN; } @@ -596,7 +699,7 @@ public class StackScrollAlgorithm { * @param algorithmState The state in which the current pass of the algorithm is currently in */ private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, - StackScrollAlgorithmState algorithmState) { + StackScrollAlgorithmState algorithmState, AmbientState ambientState) { // The y Position if the element would be in a regular scrollView float yPositionInScrollView = 0.0f; @@ -605,8 +708,8 @@ public class StackScrollAlgorithm { // 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); + StackViewState childViewState = resultState.getViewStateForView(child); + int childHeight = getMaxAllowedChildHeight(child, ambientState); float yPositionInScrollViewAfterElement = yPositionInScrollView + childHeight + mPaddingBetweenElements; @@ -614,7 +717,7 @@ public class StackScrollAlgorithm { if (i == 0 && algorithmState.scrollY <= mCollapsedSize) { // The starting position of the bottom stack peek - int bottomPeekStart = mInnerHeight - mBottomStackPeekSize - + int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding; // Collapse and expand the first child while the shade is being expanded float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding @@ -676,7 +779,7 @@ public class StackScrollAlgorithm { int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); - StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); + StackViewState childViewState = resultState.getViewStateForView(child); if (i < algorithmState.itemsInTopStack) { float stackIndex = algorithmState.itemsInTopStack - i; @@ -711,21 +814,6 @@ public class StackScrollAlgorithm { } } - public void setLayoutHeight(int layoutHeight) { - this.mLayoutHeight = layoutHeight; - updateInnerHeight(); - } - - public void setTopPadding(int topPadding) { - mTopPadding = topPadding; - updateInnerHeight(); - } - - private void updateInnerHeight() { - mInnerHeight = mLayoutHeight - mTopPadding; - } - - /** * Update whether the device is very small, i.e. Notifications can be in both the top and the * bottom stack at the same time @@ -755,6 +843,13 @@ public class StackScrollAlgorithm { // current height or the end value of the animation. mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight( mFirstChildWhileExpanding); + if (mFirstChildWhileExpanding instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = + (ExpandableNotificationRow) mFirstChildWhileExpanding; + if (row.isHeadsUp()) { + mFirstChildMaxHeight += mCollapsedSize - row.getHeadsUpHeight(); + } + } } else { updateFirstChildMaxSizeToMaxHeight(); } @@ -776,7 +871,7 @@ public class StackScrollAlgorithm { int oldBottom) { if (mFirstChildWhileExpanding != null) { mFirstChildMaxHeight = getMaxAllowedChildHeight( - mFirstChildWhileExpanding); + mFirstChildWhileExpanding, null); } else { mFirstChildMaxHeight = 0; } @@ -784,7 +879,7 @@ public class StackScrollAlgorithm { } }); } else { - mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding); + mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding, null); } } @@ -837,6 +932,10 @@ public class StackScrollAlgorithm { } } + public void setHeadsUpManager(HeadsUpManager headsUpManager) { + mHeadsUpManager = headsUpManager; + } + class StackScrollAlgorithmState { /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java index 0b1ce8f..3768ca4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.stack; -import android.graphics.Rect; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -24,10 +23,12 @@ import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.statusbar.DismissView; import com.android.systemui.statusbar.EmptyShadeView; +import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.SpeedBumpView; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -39,13 +40,12 @@ public class StackScrollState { private static final String CHILD_NOT_FOUND_TAG = "StackScrollStateNoSuchChild"; private final ViewGroup mHostView; - private Map<ExpandableView, ViewState> mStateMap; - private final Rect mClipRect = new Rect(); + private Map<ExpandableView, StackViewState> mStateMap; private final int mClearAllTopPadding; public StackScrollState(ViewGroup hostView) { mHostView = hostView; - mStateMap = new HashMap<ExpandableView, ViewState>(); + mStateMap = new HashMap<ExpandableView, StackViewState>(); mClearAllTopPadding = hostView.getContext().getResources().getDimensionPixelSize( R.dimen.clear_all_padding_top); } @@ -58,20 +58,36 @@ public class StackScrollState { 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); + resetViewState(child); + + // handling reset for child notifications + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + List<ExpandableNotificationRow> children = + row.getNotificationChildren(); + if (row.areChildrenExpanded() && children != null) { + for (ExpandableNotificationRow childRow : children) { + resetViewState(childRow); + } + } } - // initialize with the default values of the view - viewState.height = child.getIntrinsicHeight(); - viewState.gone = child.getVisibility() == View.GONE; - viewState.alpha = 1; - viewState.notGoneIndex = -1; } } - public ViewState getViewStateForView(View requestedView) { + private void resetViewState(ExpandableView view) { + StackViewState viewState = mStateMap.get(view); + if (viewState == null) { + viewState = new StackViewState(); + mStateMap.put(view, viewState); + } + // initialize with the default values of the view + viewState.height = view.getIntrinsicHeight(); + viewState.gone = view.getVisibility() == View.GONE; + viewState.alpha = 1; + viewState.notGoneIndex = -1; + } + + public StackViewState getViewStateForView(View requestedView) { return mStateMap.get(requestedView); } @@ -87,126 +103,142 @@ public class StackScrollState { int numChildren = mHostView.getChildCount(); 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"); + StackViewState state = mStateMap.get(child); + if (!applyState(child, state)) { continue; } - if (!state.gone) { - float alpha = child.getAlpha(); - float yTranslation = child.getTranslationY(); - float xTranslation = child.getTranslationX(); - float zTranslation = child.getTranslationZ(); - float scale = child.getScaleX(); - int height = child.getActualHeight(); - float newAlpha = state.alpha; - float newYTranslation = state.yTranslation; - float newZTranslation = state.zTranslation; - float newScale = state.scale; - int newHeight = state.height; - boolean becomesInvisible = newAlpha == 0.0f; - if (alpha != newAlpha && xTranslation == 0) { - // 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); - } + if(child instanceof SpeedBumpView) { + performSpeedBumpAnimation(i, (SpeedBumpView) child, state, 0); + } else if (child instanceof DismissView) { + DismissView dismissView = (DismissView) child; + boolean visible = state.topOverLap < mClearAllTopPadding; + dismissView.performVisibilityAnimation(visible && !dismissView.willBeGone()); + } else if (child instanceof EmptyShadeView) { + EmptyShadeView emptyShadeView = (EmptyShadeView) child; + boolean visible = state.topOverLap <= 0; + emptyShadeView.performVisibilityAnimation( + visible && !emptyShadeView.willBeGone()); + } + } + } - // apply alpha - child.setAlpha(newAlpha); - } + /** + * Applies a {@link StackViewState} to an {@link ExpandableView}. + * + * @return whether the state was applied correctly + */ + public boolean applyState(ExpandableView view, StackViewState state) { + if (state == null) { + Log.wtf(CHILD_NOT_FOUND_TAG, "No child state was found when applying this state " + + "to the hostView"); + return false; + } + if (state.gone) { + return false; + } + applyViewState(view, state); - // apply visibility - int oldVisibility = child.getVisibility(); - int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE; - if (newVisibility != oldVisibility) { - child.setVisibility(newVisibility); - } + int height = view.getActualHeight(); + int newHeight = state.height; - // apply yTranslation - if (yTranslation != newYTranslation) { - child.setTranslationY(newYTranslation); - } + // apply height + if (height != newHeight) { + view.setActualHeight(newHeight, false /* notifyListeners */); + } - // apply zTranslation - if (zTranslation != newZTranslation) { - child.setTranslationZ(newZTranslation); - } + // apply dimming + view.setDimmed(state.dimmed, false /* animate */); - // apply scale - if (scale != newScale) { - child.setScaleX(newScale); - child.setScaleY(newScale); - } + // apply hiding sensitive + view.setHideSensitive( + state.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */); - // apply height - if (height != newHeight) { - child.setActualHeight(newHeight, false /* notifyListeners */); - } + // apply speed bump state + view.setBelowSpeedBump(state.belowSpeedBump); - // apply dimming - child.setDimmed(state.dimmed, false /* animate */); + // apply dark + view.setDark(state.dark, false /* animate */, 0 /* delay */); - // apply dark - child.setDark(state.dark, false /* animate */, 0 /* delay */); + // apply clipping + float oldClipTopAmount = view.getClipTopAmount(); + if (oldClipTopAmount != state.clipTopAmount) { + view.setClipTopAmount(state.clipTopAmount); + } + float oldClipTopOptimization = view.getClipTopOptimization(); + if (oldClipTopOptimization != state.topOverLap) { + view.setClipTopOptimization(state.topOverLap); + } + if (view instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) view; + row.applyChildrenState(this); + } + return true; + } - // apply hiding sensitive - child.setHideSensitive( - state.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */); + /** + * Applies a {@link ViewState} to a normal view. + */ + public void applyViewState(View view, ViewState state) { + float alpha = view.getAlpha(); + float yTranslation = view.getTranslationY(); + float xTranslation = view.getTranslationX(); + float zTranslation = view.getTranslationZ(); + float scale = view.getScaleX(); + float newAlpha = state.alpha; + float newYTranslation = state.yTranslation; + float newZTranslation = state.zTranslation; + float newScale = state.scale; + boolean becomesInvisible = newAlpha == 0.0f; + if (alpha != newAlpha && xTranslation == 0) { + // apply layer type + boolean becomesFullyVisible = newAlpha == 1.0f; + boolean newLayerTypeIsHardware = !becomesInvisible && !becomesFullyVisible + && view.hasOverlappingRendering(); + int layerType = view.getLayerType(); + int newLayerType = newLayerTypeIsHardware + ? View.LAYER_TYPE_HARDWARE + : View.LAYER_TYPE_NONE; + if (layerType != newLayerType) { + view.setLayerType(newLayerType, null); + } - // apply speed bump state - child.setBelowSpeedBump(state.belowSpeedBump); + // apply alpha + view.setAlpha(newAlpha); + } - // apply clipping - float oldClipTopAmount = child.getClipTopAmount(); - if (oldClipTopAmount != state.clipTopAmount) { - child.setClipTopAmount(state.clipTopAmount); - } - updateChildClip(child, newHeight, state.topOverLap); - - if(child instanceof SpeedBumpView) { - performSpeedBumpAnimation(i, (SpeedBumpView) child, state, 0); - } else if (child instanceof DismissView) { - DismissView dismissView = (DismissView) child; - boolean visible = state.topOverLap < mClearAllTopPadding; - dismissView.performVisibilityAnimation(visible && !dismissView.willBeGone()); - } else if (child instanceof EmptyShadeView) { - EmptyShadeView emptyShadeView = (EmptyShadeView) child; - boolean visible = state.topOverLap <= 0; - emptyShadeView.performVisibilityAnimation( - visible && !emptyShadeView.willBeGone()); - } + // apply visibility + int oldVisibility = view.getVisibility(); + int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE; + if (newVisibility != oldVisibility) { + if (!(view instanceof ExpandableView) || !((ExpandableView) view).willBeGone()) { + // We don't want views to change visibility when they are animating to GONE + view.setVisibility(newVisibility); } } - } - /** - * Updates the clipping of a view - * - * @param child the view to update - * @param height the currently applied height of the view - * @param clipInset how much should this view be clipped from the top - */ - private void updateChildClip(View child, int height, int clipInset) { - mClipRect.set(0, - clipInset, - child.getWidth(), - height); - child.setClipBounds(mClipRect); + // apply yTranslation + if (yTranslation != newYTranslation) { + view.setTranslationY(newYTranslation); + } + + // apply zTranslation + if (zTranslation != newZTranslation) { + view.setTranslationZ(newZTranslation); + } + + // apply scale + if (scale != newScale) { + view.setScaleX(newScale); + view.setScaleY(newScale); + } } - public void performSpeedBumpAnimation(int i, SpeedBumpView speedBump, ViewState state, + public void performSpeedBumpAnimation(int i, SpeedBumpView speedBump, StackViewState state, long delay) { View nextChild = getNextChildNotGone(i); if (nextChild != null) { float lineEnd = state.yTranslation + state.height / 2; - ViewState nextState = getViewStateForView(nextChild); + StackViewState nextState = getViewStateForView(nextChild); boolean startIsAboveNext = nextState.yTranslation > lineEnd; speedBump.animateDivider(startIsAboveNext, delay, null /* onFinishedRunnable */); } @@ -223,53 +255,4 @@ public class StackScrollState { return null; } - 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; - /** The view isn't layouted at all. */ - public static final int LOCATION_GONE = 0x40; - - float alpha; - float yTranslation; - float zTranslation; - int height; - boolean gone; - float scale; - boolean dimmed; - boolean dark; - boolean hideSensitive; - boolean belowSpeedBump; - - /** - * The amount which the view should be clipped from the top. This is calculated to - * perceive consistent shadows. - */ - int clipTopAmount; - - /** - * How much does the child overlap with the previous view on the top? Can be used for - * a clipping optimization - */ - int topOverLap; - - /** - * The index of the view, only accounting for views not equal to GONE - */ - int notGoneIndex; - - /** - * 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 index b027787..5b8fe89 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java @@ -26,12 +26,12 @@ import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import com.android.systemui.R; +import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.SpeedBumpView; import java.util.ArrayList; import java.util.HashSet; -import java.util.Set; import java.util.Stack; /** @@ -42,12 +42,17 @@ public class StackStateAnimator { public static final int ANIMATION_DURATION_STANDARD = 360; public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448; public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464; + public static final int ANIMATION_DURATION_EXPAND_CLICKED = 360; public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220; + public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 650; + public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 230; public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80; + public static final int ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN = 54; public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32; public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48; public static final int ANIMATION_DELAY_PER_ELEMENT_DARK = 24; - private static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2; + public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2; + public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN = 3; private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag; private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag; @@ -69,12 +74,16 @@ public class StackStateAnimator { private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag; private final Interpolator mFastOutSlowInInterpolator; + private final Interpolator mHeadsUpAppearInterpolator; private final int mGoToFullShadeAppearingTranslation; + private final StackViewState mTmpState = new StackViewState(); public NotificationStackScrollLayout mHostLayout; private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents = new ArrayList<>(); private ArrayList<View> mNewAddChildren = new ArrayList<>(); - private Set<Animator> mAnimatorSet = new HashSet<>(); + private HashSet<View> mHeadsUpAppearChildren = new HashSet<>(); + private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>(); + private HashSet<Animator> mAnimatorSet = new HashSet<>(); private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>(); private AnimationFilter mAnimationFilter = new AnimationFilter(); private long mCurrentLength; @@ -82,9 +91,12 @@ public class StackStateAnimator { /** The current index for the last child which was not added in this event set. */ private int mCurrentLastNotAddedIndex; - private ValueAnimator mTopOverScrollAnimator; private ValueAnimator mBottomOverScrollAnimator; + private ExpandableNotificationRow mChildExpandingView; + private int mHeadsUpAppearHeightBottom; + private boolean mShadeExpanded; + private ArrayList<View> mChildrenToClearFromOverlay = new ArrayList<>(); public StackStateAnimator(NotificationStackScrollLayout hostLayout) { mHostLayout = hostLayout; @@ -93,6 +105,7 @@ public class StackStateAnimator { mGoToFullShadeAppearingTranslation = hostLayout.getContext().getResources().getDimensionPixelSize( R.dimen.go_to_full_shade_appearing_translation); + mHeadsUpAppearInterpolator = new HeadsUpAppearInterpolator(); } public boolean isRunning() { @@ -113,20 +126,50 @@ public class StackStateAnimator { for (int i = 0; i < childCount; i++) { final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i); - StackScrollState.ViewState viewState = finalState.getViewStateForView(child); - if (viewState == null || child.getVisibility() == View.GONE) { + StackViewState viewState = finalState.getViewStateForView(child); + if (viewState == null || child.getVisibility() == View.GONE + || applyWithoutAnimation(child, viewState, finalState)) { continue; } - child.setClipBounds(null); - startAnimations(child, viewState, finalState, i); + child.setClipTopOptimization(0); + startStackAnimations(child, viewState, finalState, i, -1 /* fixedDelay */); } if (!isRunning()) { // no child has preformed any animation, lets finish onAnimationFinished(); } + mHeadsUpAppearChildren.clear(); + mHeadsUpDisappearChildren.clear(); mNewEvents.clear(); mNewAddChildren.clear(); + mChildExpandingView = null; + } + + /** + * Determines if a view should not perform an animation and applies it directly. + * + * @return true if no animation should be performed + */ + private boolean applyWithoutAnimation(ExpandableView child, StackViewState viewState, + StackScrollState finalState) { + if (mShadeExpanded) { + return false; + } + if (getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y) != null) { + // A Y translation animation is running + return false; + } + if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) { + // This is a heads up animation + return false; + } + if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) { + // This is another headsUp which might move. Let's animate! + return false; + } + finalState.applyState(child, viewState); + return true; } private int findLastNotAddedIndex(StackScrollState finalState) { @@ -134,7 +177,7 @@ public class StackStateAnimator { for (int i = childCount - 1; i >= 0; i--) { final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i); - StackScrollState.ViewState viewState = finalState.getViewStateForView(child); + StackViewState viewState = finalState.getViewStateForView(child); if (viewState == null || child.getVisibility() == View.GONE) { continue; } @@ -145,18 +188,29 @@ public class StackStateAnimator { return -1; } + /** - * Start an animation to the given viewState + * Start an animation to the given {@link StackViewState}. + * + * @param child the child to start the animation on + * @param viewState the {@link StackViewState} of the view to animate to + * @param finalState the final state after the animation + * @param i the index of the view; only relevant if the view is the speed bump and is + * ignored otherwise + * @param fixedDelay a fixed delay if desired or -1 if the delay should be calculated */ - private void startAnimations(final ExpandableView child, StackScrollState.ViewState viewState, - StackScrollState finalState, int i) { - int childVisibility = child.getVisibility(); - boolean wasVisible = childVisibility == View.VISIBLE; + public void startStackAnimations(final ExpandableView child, StackViewState viewState, + StackScrollState finalState, int i, long fixedDelay) { final float alpha = viewState.alpha; - if (!wasVisible && alpha != 0 && !viewState.gone) { - child.setVisibility(View.VISIBLE); + boolean wasAdded = mNewAddChildren.contains(child); + long duration = mCurrentLength; + if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) { + child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation); + float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex; + longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f); + duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 + + (long) (100 * longerDurationFactor); } - boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation; boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation; boolean scaleChanging = child.getScaleX() != viewState.scale; @@ -164,94 +218,40 @@ public class StackStateAnimator { boolean heightChanging = viewState.height != child.getActualHeight(); boolean darkChanging = viewState.dark != child.isDark(); boolean topInsetChanging = viewState.clipTopAmount != child.getClipTopAmount(); - boolean wasAdded = mNewAddChildren.contains(child); boolean hasDelays = mAnimationFilter.hasDelays; boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || scaleChanging || alphaChanging || heightChanging || topInsetChanging || darkChanging; - boolean noAnimation = wasAdded; long delay = 0; - long duration = mCurrentLength; - if (hasDelays && isDelayRelevant || wasAdded) { + if (fixedDelay != -1) { + delay = fixedDelay; + } else if (hasDelays && isDelayRelevant || wasAdded) { delay = mCurrentAdditionalDelay + calculateChildAnimationDelay(viewState, finalState); } - if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) { - child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation); - yTranslationChanging = true; - float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex; - longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f); - duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 + - (long) (100 * longerDurationFactor); - } - - // start translationY animation - if (yTranslationChanging) { - if (noAnimation && !mAnimationFilter.hasGoToFullShadeEvent) { - child.setTranslationY(viewState.yTranslation); - } else { - startYTranslationAnimation(child, viewState, duration, delay); - } - } - - // start translationZ animation - if (zTranslationChanging) { - if (noAnimation) { - child.setTranslationZ(viewState.zTranslation); - } else { - startZTranslationAnimation(child, viewState, duration, delay); - } - } - - // start scale animation - if (scaleChanging) { - if (noAnimation) { - child.setScaleX(viewState.scale); - child.setScaleY(viewState.scale); - } else { - startScaleAnimation(child, viewState, duration); - } - } - - // start alpha animation - if (alphaChanging && child.getTranslationX() == 0) { - if (noAnimation) { - child.setAlpha(viewState.alpha); - } else { - startAlphaAnimation(child, viewState, duration, delay); - } - } + startViewAnimations(child, viewState, delay, duration); // start height animation if (heightChanging && child.getActualHeight() != 0) { - if (noAnimation) { - child.setActualHeight(viewState.height, false); - } else { - startHeightAnimation(child, viewState, duration, delay); - } + startHeightAnimation(child, viewState, duration, delay); } // start top inset animation if (topInsetChanging) { - if (noAnimation) { - child.setClipTopAmount(viewState.clipTopAmount); - } else { - startInsetAnimation(child, viewState, duration, delay); - } + startInsetAnimation(child, viewState, duration, delay); } // start dimmed animation - child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed && !wasAdded - && !noAnimation); - - // start dark animation - child.setDark(viewState.dark, mAnimationFilter.animateDark && !noAnimation, delay); + child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed); // apply speed bump state child.setBelowSpeedBump(viewState.belowSpeedBump); // start hiding sensitive animation - child.setHideSensitive(viewState.hideSensitive, mAnimationFilter.animateHideSensitive && - !wasAdded && !noAnimation, delay, duration); + child.setHideSensitive(viewState.hideSensitive, mAnimationFilter.animateHideSensitive, + delay, duration); + + // start dark animation + child.setDark(viewState.dark, mAnimationFilter.animateDark, delay); if (wasAdded) { child.performAddAnimation(delay, mCurrentLength); @@ -259,10 +259,59 @@ public class StackStateAnimator { if (child instanceof SpeedBumpView) { finalState.performSpeedBumpAnimation(i, (SpeedBumpView) child, viewState, delay + duration); + } else if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + row.startChildAnimation(finalState, this, child == mChildExpandingView, delay, + duration); } } - private long calculateChildAnimationDelay(StackScrollState.ViewState viewState, + /** + * Start an animation to a new {@link ViewState}. + * + * @param child the child to start the animation on + * @param viewState the {@link StackViewState} of the view to animate to + * @param delay a fixed delay + * @param duration the duration of the animation + */ + public void startViewAnimations(View child, ViewState viewState, long delay, long duration) { + boolean wasVisible = child.getVisibility() == View.VISIBLE; + final float alpha = viewState.alpha; + if (!wasVisible && alpha != 0 && !viewState.gone) { + child.setVisibility(View.VISIBLE); + } + boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation; + boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation; + boolean scaleChanging = child.getScaleX() != viewState.scale; + float childAlpha = child.getVisibility() == View.INVISIBLE ? 0.0f : child.getAlpha(); + boolean alphaChanging = viewState.alpha != childAlpha; + if (child instanceof ExpandableView) { + // We don't want views to change visibility when they are animating to GONE + alphaChanging &= !((ExpandableView) child).willBeGone(); + } + + // start translationY animation + if (yTranslationChanging) { + startYTranslationAnimation(child, viewState, duration, delay); + } + + // start translationZ animation + if (zTranslationChanging) { + startZTranslationAnimation(child, viewState, duration, delay); + } + + // start scale animation + if (scaleChanging) { + startScaleAnimation(child, viewState, duration); + } + + // start alpha animation + if (alphaChanging && child.getTranslationX() == 0) { + startAlphaAnimation(child, viewState, duration, delay); + } + } + + private long calculateChildAnimationDelay(StackViewState viewState, StackScrollState finalState) { if (mAnimationFilter.hasDarkEvent) { return calculateDelayDark(viewState); @@ -314,7 +363,7 @@ public class StackStateAnimator { return minDelay; } - private long calculateDelayDark(StackScrollState.ViewState viewState) { + private long calculateDelayDark(StackViewState viewState) { int referenceIndex; if (mAnimationFilter.darkAnimationOriginIndex == NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE) { @@ -328,14 +377,14 @@ public class StackStateAnimator { return Math.abs(referenceIndex - viewState.notGoneIndex) * ANIMATION_DELAY_PER_ELEMENT_DARK; } - private long calculateDelayGoToFullShade(StackScrollState.ViewState viewState) { + private long calculateDelayGoToFullShade(StackViewState viewState) { float index = viewState.notGoneIndex; index = (float) Math.pow(index, 0.7f); return (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE); } private void startHeightAnimation(final ExpandableView child, - StackScrollState.ViewState viewState, long duration, long delay) { + StackViewState viewState, long duration, long delay) { Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT); Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT); int newEndValue = viewState.height; @@ -394,7 +443,7 @@ public class StackStateAnimator { } private void startInsetAnimation(final ExpandableView child, - StackScrollState.ViewState viewState, long duration, long delay) { + StackViewState viewState, long duration, long delay) { Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET); Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET); int newEndValue = viewState.clipTopAmount; @@ -451,8 +500,8 @@ public class StackStateAnimator { child.setTag(TAG_END_TOP_INSET, newEndValue); } - private void startAlphaAnimation(final ExpandableView child, - final StackScrollState.ViewState viewState, long duration, long delay) { + private void startAlphaAnimation(final View child, + final ViewState viewState, long duration, long delay) { Float previousStartValue = getChildTag(child,TAG_START_ALPHA); Float previousEndValue = getChildTag(child,TAG_END_ALPHA); final float newEndValue = viewState.alpha; @@ -525,8 +574,8 @@ public class StackStateAnimator { child.setTag(TAG_END_ALPHA, newEndValue); } - private void startZTranslationAnimation(final ExpandableView child, - final StackScrollState.ViewState viewState, long duration, long delay) { + private void startZTranslationAnimation(final View child, + final ViewState viewState, long duration, long delay) { Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z); Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z); float newEndValue = viewState.zTranslation; @@ -577,8 +626,8 @@ public class StackStateAnimator { child.setTag(TAG_END_TRANSLATION_Z, newEndValue); } - private void startYTranslationAnimation(final ExpandableView child, - StackScrollState.ViewState viewState, long duration, long delay) { + private void startYTranslationAnimation(final View child, + ViewState viewState, long duration, long delay) { Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y); Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y); float newEndValue = viewState.yTranslation; @@ -608,7 +657,9 @@ public class StackStateAnimator { ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, child.getTranslationY(), newEndValue); - animator.setInterpolator(mFastOutSlowInInterpolator); + Interpolator interpolator = mHeadsUpAppearChildren.contains(child) ? + mHeadsUpAppearInterpolator :mFastOutSlowInInterpolator; + animator.setInterpolator(interpolator); long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator); animator.setDuration(newDuration); if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) { @@ -630,8 +681,8 @@ public class StackStateAnimator { child.setTag(TAG_END_TRANSLATION_Y, newEndValue); } - private void startScaleAnimation(final ExpandableView child, - StackScrollState.ViewState viewState, long duration) { + private void startScaleAnimation(final View child, + ViewState viewState, long duration) { Float previousStartValue = getChildTag(child, TAG_START_SCALE); Float previousEndValue = getChildTag(child, TAG_END_SCALE); float newEndValue = viewState.scale; @@ -723,7 +774,7 @@ public class StackStateAnimator { }; } - private static <T> T getChildTag(View child, int tag) { + public static <T> T getChildTag(View child, int tag) { return (T) child.getTag(tag); } @@ -748,6 +799,10 @@ public class StackStateAnimator { private void onAnimationFinished() { mHostLayout.onChildAnimationFinished(); + for (View v : mChildrenToClearFromOverlay) { + mHostLayout.getOverlay().remove(v); + } + mChildrenToClearFromOverlay.clear(); } /** @@ -765,7 +820,7 @@ public class StackStateAnimator { NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) { // This item is added, initialize it's properties. - StackScrollState.ViewState viewState = finalState + StackViewState viewState = finalState .getViewStateForView(changingView); if (viewState == null) { // The position for this child was never generated, let's continue. @@ -776,10 +831,7 @@ public class StackStateAnimator { finalState.removeViewStateForView(changingView); continue; } - changingView.setAlpha(viewState.alpha); - changingView.setTranslationY(viewState.yTranslation); - changingView.setTranslationZ(viewState.zTranslation); - changingView.setActualHeight(viewState.height, false); + finalState.applyState(changingView, viewState); mNewAddChildren.add(changingView); } else if (event.animationType == @@ -791,7 +843,7 @@ public class StackStateAnimator { // Find the amount to translate up. This is needed in order to understand the // direction of the remove animation (either downwards or upwards) - StackScrollState.ViewState viewState = finalState + StackViewState viewState = finalState .getViewStateForView(event.viewAfterChangingView); int actualHeight = changingView.getActualHeight(); // upwards by default @@ -813,11 +865,43 @@ public class StackStateAnimator { mHostLayout.getOverlay().remove(changingView); } }); - } else if (event.animationType == + } else if (event.animationType == NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) { // A race condition can trigger the view to be added to the overlay even though // it is swiped out. So let's remove it mHostLayout.getOverlay().remove(changingView); + } else if (event.animationType == NotificationStackScrollLayout + .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) { + ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView; + row.prepareExpansionChanged(finalState); + mChildExpandingView = row; + } else if (event.animationType == NotificationStackScrollLayout + .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) { + // This item is added, initialize it's properties. + StackViewState viewState = finalState.getViewStateForView(changingView); + mTmpState.copyFrom(viewState); + if (event.headsUpFromBottom) { + mTmpState.yTranslation = mHeadsUpAppearHeightBottom; + } else { + mTmpState.yTranslation = -mTmpState.height; + } + mHeadsUpAppearChildren.add(changingView); + finalState.applyState(changingView, mTmpState); + } else if (event.animationType == NotificationStackScrollLayout + .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR) { + mHeadsUpDisappearChildren.add(changingView); + if (mHostLayout.indexOfChild(changingView) == -1) { + // This notification was actually removed, so we need to add it to the overlay + mHostLayout.getOverlay().add(changingView); + mTmpState.initFrom(changingView); + mTmpState.yTranslation = -changingView.getActualHeight(); + // We temporarily enable Y animations, the real filter will be combined + // afterwards anyway + mAnimationFilter.animateY = true; + startViewAnimations(changingView, mTmpState, 0, + ANIMATION_DURATION_HEADS_UP_DISAPPEAR); + mChildrenToClearFromOverlay.add(changingView); + } } mNewEvents.add(event); } @@ -883,4 +967,12 @@ public class StackStateAnimator { return getChildTag(view, TAG_END_HEIGHT); } } + + public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) { + mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; + } + + public void setShadeExpanded(boolean shadeExpanded) { + mShadeExpanded = shadeExpanded; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackViewState.java new file mode 100644 index 0000000..55ef440 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackViewState.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.stack; + +import android.view.View; + +import com.android.systemui.statusbar.ExpandableView; + +/** +* A state of an expandable view +*/ +public class StackViewState extends 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; + /** The view isn't layouted at all. */ + public static final int LOCATION_GONE = 0x40; + + public int height; + public boolean dimmed; + public boolean dark; + public boolean hideSensitive; + public boolean belowSpeedBump; + + /** + * The amount which the view should be clipped from the top. This is calculated to + * perceive consistent shadows. + */ + public int clipTopAmount; + + /** + * How much does the child overlap with the previous view on the top? Can be used for + * a clipping optimization + */ + public int topOverLap; + + /** + * The index of the view, only accounting for views not equal to GONE + */ + public int notGoneIndex; + + /** + * The location this view is currently rendered at. + * + * <p>See <code>LOCATION_</code> flags.</p> + */ + public int location; + + @Override + public void copyFrom(ViewState viewState) { + super.copyFrom(viewState); + if (viewState instanceof StackViewState) { + StackViewState svs = (StackViewState) viewState; + height = svs.height; + dimmed = svs.dimmed; + dark = svs.dark; + hideSensitive = svs.hideSensitive; + belowSpeedBump = svs.belowSpeedBump; + clipTopAmount = svs.clipTopAmount; + topOverLap = svs.topOverLap; + notGoneIndex = svs.notGoneIndex; + location = svs.location; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java new file mode 100644 index 0000000..3e538df --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.stack; + +import android.view.View; + +/** + * A state of a view. This can be used to apply a set of view properties to a view with + * {@link com.android.systemui.statusbar.stack.StackScrollState} or start animations with + * {@link com.android.systemui.statusbar.stack.StackStateAnimator}. +*/ +public class ViewState { + + public float alpha; + public float yTranslation; + public float zTranslation; + public boolean gone; + public float scale; + + public void copyFrom(ViewState viewState) { + alpha = viewState.alpha; + yTranslation = viewState.yTranslation; + zTranslation = viewState.zTranslation; + gone = viewState.gone; + scale = viewState.scale; + } + + public void initFrom(View view) { + alpha = view.getVisibility() == View.INVISIBLE ? 0.0f : view.getAlpha(); + yTranslation = view.getTranslationY(); + zTranslation = view.getTranslationZ(); + gone = view.getVisibility() == View.GONE; + scale = view.getScaleX(); + } +} 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 08732e5..920b682 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java @@ -20,12 +20,11 @@ import android.os.IBinder; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; import android.view.View; -import android.view.ViewGroup.LayoutParams; -import android.view.WindowManager; import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.statusbar.ActivatableNotificationView; import com.android.systemui.statusbar.BaseStatusBar; +import com.android.systemui.statusbar.NotificationData; /* * Status bar implementation for "large screen" products that mostly present no on-screen nav @@ -47,7 +46,8 @@ public class TvStatusBar extends BaseStatusBar { } @Override - public void addNotification(StatusBarNotification notification, RankingMap ranking) { + public void addNotification(StatusBarNotification notification, RankingMap ranking, + NotificationData.Entry entry) { } @Override @@ -59,7 +59,7 @@ public class TvStatusBar extends BaseStatusBar { } @Override - public void disable(int state, boolean animate) { + public void disable(int state1, int state2, boolean animate) { } @Override @@ -104,82 +104,82 @@ public class TvStatusBar extends BaseStatusBar { } @Override - protected WindowManager.LayoutParams getSearchLayoutParams( - LayoutParams layoutParams) { - return null; + protected void setAreThereNotifications() { } @Override - protected void haltTicker() { + protected void updateNotifications() { } @Override - protected void setAreThereNotifications() { + public boolean shouldDisableNavbarGestures() { + return true; } - @Override - protected void updateNotifications() { + public View getStatusBarView() { + return null; } @Override - protected void tick(StatusBarNotification n, boolean firstTime) { + public void maybeEscalateHeadsUp() { } @Override - protected void updateExpandedViewPos(int expandedPosition) { + protected boolean isPanelFullyCollapsed() { + return false; } @Override - protected boolean shouldDisableNavbarGestures() { - return true; + protected int getMaxKeyguardNotifications() { + return 0; } - public View getStatusBarView() { - return null; + @Override + public void animateExpandSettingsPanel() { } @Override - public void resetHeadsUpDecayTimer() { + protected void createAndAddWindows() { } @Override - public void scheduleHeadsUpOpen() { + protected void refreshLayout(int layoutDirection) { } @Override - public void scheduleHeadsUpEscalation() { + public void onActivated(ActivatableNotificationView view) { } @Override - public void scheduleHeadsUpClose() { + public void onActivationReset(ActivatableNotificationView view) { } @Override - protected int getMaxKeyguardNotifications() { - return 0; + public void showScreenPinningRequest() { } @Override - public void animateExpandSettingsPanel() { + public void appTransitionPending() { } @Override - protected void createAndAddWindows() { + public void appTransitionCancelled() { } @Override - protected void refreshLayout(int layoutDirection) { + public void appTransitionStarting(long startTime, long duration) { } @Override - public void onActivated(ActivatableNotificationView view) { + protected void updateHeadsUp(String key, NotificationData.Entry entry, boolean shouldInterrupt, + boolean alertAgain) { } @Override - public void onActivationReset(ActivatableNotificationView view) { + protected void setHeadsUpUser(int newUserId) { } - @Override - public void showScreenPinningRequest() { + protected boolean isSnoozedPackage(StatusBarNotification sbn) { + return false; } } diff --git a/packages/SystemUI/src/com/android/systemui/tuner/AutoScrollView.java b/packages/SystemUI/src/com/android/systemui/tuner/AutoScrollView.java new file mode 100644 index 0000000..6aea2cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tuner/AutoScrollView.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.tuner; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.DragEvent; +import android.widget.ScrollView; + +public class AutoScrollView extends ScrollView { + + private static final float SCROLL_PERCENT = .10f; + + public AutoScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public boolean onDragEvent(DragEvent event) { + switch (event.getAction()) { + case DragEvent.ACTION_DRAG_LOCATION: + int y = (int) event.getY(); + int height = getHeight(); + int scrollPadding = (int) (height * SCROLL_PERCENT); + if (y < scrollPadding) { + scrollBy(0, y - scrollPadding); + } else if (y > height - scrollPadding) { + scrollBy(0, y - height + scrollPadding); + } + break; + } + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java new file mode 100644 index 0000000..ca6aaeb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.tuner; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.Settings; + +import com.android.systemui.DemoMode; +import com.android.systemui.R; + +public class DemoModeFragment extends PreferenceFragment implements OnPreferenceChangeListener { + + private static final String DEMO_MODE_ALLOWED = "sysui_demo_allowed"; + private static final String DEMO_MODE_ON = "sysui_tuner_demo_on"; + + private static final String[] STATUS_ICONS = { + "volume", + "bluetooth", + "location", + "alarm", + "zen", + "sync", + "tty", + "eri", + "mute", + "speakerphone", + "managed_profile", + }; + + private SwitchPreference mEnabledSwitch; + private SwitchPreference mOnSwitch; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Context context = getContext(); + mEnabledSwitch = new SwitchPreference(context); + mEnabledSwitch.setTitle(R.string.enable_demo_mode); + mEnabledSwitch.setOnPreferenceChangeListener(this); + mOnSwitch = new SwitchPreference(context); + mOnSwitch.setTitle(R.string.show_demo_mode); + mOnSwitch.setEnabled(false); + mOnSwitch.setOnPreferenceChangeListener(this); + + PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(context); + screen.addPreference(mEnabledSwitch); + screen.addPreference(mOnSwitch); + setPreferenceScreen(screen); + + updateDemoModeEnabled(); + updateDemoModeOn(); + ContentResolver contentResolver = getContext().getContentResolver(); + contentResolver.registerContentObserver(Settings.Global.getUriFor(DEMO_MODE_ALLOWED), false, + mDemoModeObserver); + contentResolver.registerContentObserver(Settings.Global.getUriFor(DEMO_MODE_ON), false, + mDemoModeObserver); + } + + @Override + public void onDestroy() { + getContext().getContentResolver().unregisterContentObserver(mDemoModeObserver); + super.onDestroy(); + } + + private void updateDemoModeEnabled() { + boolean enabled = Settings.Global.getInt(getContext().getContentResolver(), + DEMO_MODE_ALLOWED, 0) != 0; + mEnabledSwitch.setChecked(enabled); + mOnSwitch.setEnabled(enabled); + } + + private void updateDemoModeOn() { + boolean enabled = Settings.Global.getInt(getContext().getContentResolver(), + DEMO_MODE_ON, 0) != 0; + mOnSwitch.setChecked(enabled); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference == mEnabledSwitch) { + setGlobal(DEMO_MODE_ALLOWED, newValue == Boolean.TRUE ? 1 : 0); + } else if (preference == mOnSwitch) { + if (newValue == Boolean.TRUE) { + startDemoMode(); + } else { + stopDemoMode(); + } + } else { + return false; + } + return true; + } + + private void startDemoMode() { + Intent intent = new Intent(DemoMode.ACTION_DEMO); + + intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_ENTER); + getContext().sendBroadcast(intent); + + intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_CLOCK); + intent.putExtra("hhmm", "0520"); + getContext().sendBroadcast(intent); + + intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_NETWORK); + intent.putExtra("wifi", "show"); + intent.putExtra("mobile", "show"); + intent.putExtra("sims", "1"); + intent.putExtra("nosim", "false"); + intent.putExtra("level", "4"); + intent.putExtra("datatypel", ""); + getContext().sendBroadcast(intent); + + // Need to send this after so that the sim controller already exists. + intent.putExtra("fully", "true"); + getContext().sendBroadcast(intent); + + intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_BATTERY); + intent.putExtra("level", "100"); + intent.putExtra("plugged", "false"); + getContext().sendBroadcast(intent); + + intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_STATUS); + for (String icon : STATUS_ICONS) { + intent.putExtra(icon, "hide"); + } + getContext().sendBroadcast(intent); + + intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_NOTIFICATIONS); + intent.putExtra("visible", "false"); + getContext().sendBroadcast(intent); + + setGlobal(DEMO_MODE_ON, 1); + } + + private void stopDemoMode() { + Intent intent = new Intent(DemoMode.ACTION_DEMO); + intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_EXIT); + getContext().sendBroadcast(intent); + setGlobal(DEMO_MODE_ON, 0); + } + + private void setGlobal(String key, int value) { + Settings.Global.putInt(getContext().getContentResolver(), key, value); + } + + private final ContentObserver mDemoModeObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + public void onChange(boolean selfChange) { + updateDemoModeEnabled(); + updateDemoModeOn(); + }; + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/tuner/QsTuner.java b/packages/SystemUI/src/com/android/systemui/tuner/QsTuner.java new file mode 100644 index 0000000..a5b244e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tuner/QsTuner.java @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.tuner; + +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.ClipData; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.provider.Settings.Secure; +import android.text.TextUtils; +import android.util.Log; +import android.view.DragEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnDragListener; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ScrollView; + +import com.android.systemui.R; +import com.android.systemui.qs.QSPanel; +import com.android.systemui.qs.QSTile; +import com.android.systemui.qs.QSTile.Host.Callback; +import com.android.systemui.qs.QSTile.ResourceIcon; +import com.android.systemui.qs.QSTileView; +import com.android.systemui.qs.tiles.IntentTile; +import com.android.systemui.statusbar.phone.QSTileHost; +import com.android.systemui.statusbar.policy.SecurityController; + +import java.util.ArrayList; +import java.util.List; + +public class QsTuner extends Fragment implements Callback { + + private static final String TAG = "QsTuner"; + + private static final int MENU_RESET = Menu.FIRST; + + private DraggableQsPanel mQsPanel; + private CustomHost mTileHost; + + private FrameLayout mDropTarget; + + private ScrollView mScrollRoot; + + private FrameLayout mAddTarget; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(0, MENU_RESET, 0, com.android.internal.R.string.reset); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_RESET: + mTileHost.reset(); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mScrollRoot = (ScrollView) inflater.inflate(R.layout.tuner_qs, container, false); + + mQsPanel = new DraggableQsPanel(getContext()); + mTileHost = new CustomHost(getContext()); + mTileHost.setCallback(this); + mQsPanel.setTiles(mTileHost.getTiles()); + mQsPanel.setHost(mTileHost); + mQsPanel.refreshAllTiles(); + ((ViewGroup) mScrollRoot.findViewById(R.id.all_details)).addView(mQsPanel, 0); + + mDropTarget = (FrameLayout) mScrollRoot.findViewById(R.id.remove_target); + setupDropTarget(); + mAddTarget = (FrameLayout) mScrollRoot.findViewById(R.id.add_target); + setupAddTarget(); + return mScrollRoot; + } + + @Override + public void onDestroyView() { + mTileHost.destroy(); + super.onDestroyView(); + } + + private void setupDropTarget() { + QSTileView tileView = new QSTileView(getContext()); + QSTile.State state = new QSTile.State(); + state.visible = true; + state.icon = ResourceIcon.get(R.drawable.ic_delete); + state.label = getString(com.android.internal.R.string.delete); + tileView.onStateChanged(state); + mDropTarget.addView(tileView); + mDropTarget.setVisibility(View.GONE); + new DragHelper(tileView, new DropListener() { + @Override + public void onDrop(String sourceText) { + mTileHost.remove(sourceText); + } + }); + } + + private void setupAddTarget() { + QSTileView tileView = new QSTileView(getContext()); + QSTile.State state = new QSTile.State(); + state.visible = true; + state.icon = ResourceIcon.get(R.drawable.ic_add_circle_qs); + state.label = getString(R.string.add_tile); + tileView.onStateChanged(state); + mAddTarget.addView(tileView); + tileView.setClickable(true); + tileView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mTileHost.showAddDialog(); + } + }); + } + + public void onStartDrag() { + mDropTarget.post(new Runnable() { + @Override + public void run() { + mDropTarget.setVisibility(View.VISIBLE); + mAddTarget.setVisibility(View.GONE); + } + }); + } + + public void stopDrag() { + mDropTarget.post(new Runnable() { + @Override + public void run() { + mDropTarget.setVisibility(View.GONE); + mAddTarget.setVisibility(View.VISIBLE); + } + }); + } + + @Override + public void onTilesChanged() { + mQsPanel.setTiles(mTileHost.getTiles()); + } + + private static int getLabelResource(String spec) { + if (spec.equals("wifi")) return R.string.quick_settings_wifi_label; + else if (spec.equals("bt")) return R.string.quick_settings_bluetooth_label; + else if (spec.equals("inversion")) return R.string.quick_settings_inversion_label; + else if (spec.equals("cell")) return R.string.quick_settings_cellular_detail_title; + else if (spec.equals("airplane")) return R.string.airplane_mode; + else if (spec.equals("dnd")) return R.string.quick_settings_dnd_label; + else if (spec.equals("rotation")) return R.string.quick_settings_rotation_locked_label; + else if (spec.equals("flashlight")) return R.string.quick_settings_flashlight_label; + else if (spec.equals("location")) return R.string.quick_settings_location_label; + else if (spec.equals("cast")) return R.string.quick_settings_cast_title; + else if (spec.equals("hotspot")) return R.string.quick_settings_hotspot_label; + return 0; + } + + private static class CustomHost extends QSTileHost { + + public CustomHost(Context context) { + super(context, null, null, null, null, null, null, null, null, null, + null, null, new BlankSecurityController()); + } + + @Override + protected QSTile<?> createTile(String tileSpec) { + return new DraggableTile(this, tileSpec); + } + + public void replace(String oldTile, String newTile) { + if (oldTile.equals(newTile)) { + return; + } + List<String> order = new ArrayList<>(mTileSpecs); + int index = order.indexOf(oldTile); + if (index < 0) { + Log.e(TAG, "Can't find " + oldTile); + return; + } + order.remove(newTile); + order.add(index, newTile); + setTiles(order); + } + + public void remove(String tile) { + List<String> tiles = new ArrayList<>(mTileSpecs); + tiles.remove(tile); + setTiles(tiles); + } + + public void add(String tile) { + List<String> tiles = new ArrayList<>(mTileSpecs); + tiles.add(tile); + setTiles(tiles); + } + + public void reset() { + Secure.putStringForUser(getContext().getContentResolver(), + TILES_SETTING, "default", ActivityManager.getCurrentUser()); + } + + private void setTiles(List<String> tiles) { + Secure.putStringForUser(getContext().getContentResolver(), TILES_SETTING, + TextUtils.join(",", tiles), ActivityManager.getCurrentUser()); + } + + public void showAddDialog() { + List<String> tiles = mTileSpecs; + int numBroadcast = 0; + for (int i = 0; i < tiles.size(); i++) { + if (tiles.get(i).startsWith(IntentTile.PREFIX)) { + numBroadcast++; + } + } + String[] defaults = + getContext().getString(R.string.quick_settings_tiles_default).split(","); + final String[] available = new String[defaults.length + 1 + - (tiles.size() - numBroadcast)]; + final String[] availableTiles = new String[available.length]; + int index = 0; + for (int i = 0; i < defaults.length; i++) { + if (tiles.contains(defaults[i])) { + continue; + } + int resource = getLabelResource(defaults[i]); + if (resource != 0) { + availableTiles[index] = defaults[i]; + available[index++] = getContext().getString(resource); + } else { + availableTiles[index] = defaults[i]; + available[index++] = defaults[i]; + } + } + available[index++] = getContext().getString(R.string.broadcast_tile); + new AlertDialog.Builder(getContext()) + .setTitle(R.string.add_tile) + .setItems(available, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which < available.length - 1) { + add(availableTiles[which]); + } else { + showBroadcastTileDialog(); + } + } + }).show(); + } + + public void showBroadcastTileDialog() { + final EditText editText = new EditText(getContext()); + new AlertDialog.Builder(getContext()) + .setTitle(R.string.broadcast_tile) + .setView(editText) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String action = editText.getText().toString(); + if (isValid(action)) { + add(IntentTile.PREFIX + action + ')'); + } + } + }).show(); + } + + private boolean isValid(String action) { + for (int i = 0; i < action.length(); i++) { + char c = action.charAt(i); + if (!Character.isAlphabetic(c) && !Character.isDigit(c) && c != '.') { + return false; + } + } + return true; + } + + private static class BlankSecurityController implements SecurityController { + @Override + public boolean hasDeviceOwner() { + return false; + } + + @Override + public boolean hasProfileOwner() { + return false; + } + + @Override + public String getDeviceOwnerName() { + return null; + } + + @Override + public String getProfileOwnerName() { + return null; + } + + @Override + public boolean isVpnEnabled() { + return false; + } + + @Override + public String getPrimaryVpnName() { + return null; + } + + @Override + public String getProfileVpnName() { + return null; + } + + @Override + public void onUserSwitched(int newUserId) { + } + + @Override + public void addCallback(SecurityControllerCallback callback) { + } + + @Override + public void removeCallback(SecurityControllerCallback callback) { + } + } + } + + private static class DraggableTile extends QSTile<QSTile.State> + implements DropListener { + private String mSpec; + private QSTileView mView; + + protected DraggableTile(QSTile.Host host, String tileSpec) { + super(host); + Log.d(TAG, "Creating tile " + tileSpec); + mSpec = tileSpec; + } + + @Override + public QSTileView createTileView(Context context) { + mView = super.createTileView(context); + return mView; + } + + @Override + public boolean supportsDualTargets() { + return "wifi".equals(mSpec) || "bt".equals(mSpec); + } + + @Override + public void setListening(boolean listening) { + } + + @Override + protected QSTile.State newTileState() { + return new QSTile.State(); + } + + @Override + protected void handleClick() { + } + + @Override + protected void handleUpdateState(QSTile.State state, Object arg) { + state.visible = true; + state.icon = ResourceIcon.get(getIcon()); + state.label = getLabel(); + } + + private String getLabel() { + int resource = getLabelResource(mSpec); + if (resource != 0) { + return mContext.getString(resource); + } + if (mSpec.startsWith(IntentTile.PREFIX)) { + int lastDot = mSpec.lastIndexOf('.'); + if (lastDot >= 0) { + return mSpec.substring(lastDot + 1, mSpec.length() - 1); + } else { + return mSpec.substring(IntentTile.PREFIX.length(), mSpec.length() - 1); + } + } + return mSpec; + } + + private int getIcon() { + if (mSpec.equals("wifi")) return R.drawable.ic_qs_wifi_full_3; + else if (mSpec.equals("bt")) return R.drawable.ic_qs_bluetooth_connected; + else if (mSpec.equals("inversion")) return R.drawable.ic_invert_colors_enable; + else if (mSpec.equals("cell")) return R.drawable.ic_qs_signal_full_3; + else if (mSpec.equals("airplane")) return R.drawable.ic_signal_airplane_enable; + else if (mSpec.equals("dnd")) return R.drawable.ic_qs_dnd_on; + else if (mSpec.equals("rotation")) return R.drawable.ic_portrait_from_auto_rotate; + else if (mSpec.equals("flashlight")) return R.drawable.ic_signal_flashlight_enable; + else if (mSpec.equals("location")) return R.drawable.ic_signal_location_enable; + else if (mSpec.equals("cast")) return R.drawable.ic_qs_cast_on; + else if (mSpec.equals("hotspot")) return R.drawable.ic_hotspot_enable; + return R.drawable.android; + } + + @Override + public int getMetricsCategory() { + return 20000; + } + + @Override + public boolean equals(Object o) { + if (o instanceof DraggableTile) { + return mSpec.equals(((DraggableTile) o).mSpec); + } + return false; + } + + @Override + public void onDrop(String sourceText) { + ((CustomHost) mHost).replace(mSpec, sourceText); + } + + } + + private class DragHelper implements OnDragListener { + + private final View mView; + private final DropListener mListener; + + public DragHelper(View view, DropListener dropListener) { + mView = view; + mListener = dropListener; + mView.setOnDragListener(this); + } + + @Override + public boolean onDrag(View v, DragEvent event) { + switch (event.getAction()) { + case DragEvent.ACTION_DRAG_ENTERED: + mView.setBackgroundColor(0x77ffffff); + break; + case DragEvent.ACTION_DRAG_ENDED: + stopDrag(); + case DragEvent.ACTION_DRAG_EXITED: + mView.setBackgroundColor(0x0); + break; + case DragEvent.ACTION_DROP: + stopDrag(); + String text = event.getClipData().getItemAt(0).getText().toString(); + mListener.onDrop(text); + break; + } + return true; + } + + } + + public interface DropListener { + void onDrop(String sourceText); + } + + private class DraggableQsPanel extends QSPanel implements OnTouchListener { + public DraggableQsPanel(Context context) { + super(context); + mBrightnessView.setVisibility(View.GONE); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + for (TileRecord r : mRecords) { + new DragHelper(r.tileView, (DraggableTile) r.tile); + r.tileView.setTag(r.tile); + r.tileView.setOnTouchListener(this); + + for (int i = 0; i < r.tileView.getChildCount(); i++) { + r.tileView.getChildAt(i).setClickable(false); + } + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + String tileSpec = (String) ((DraggableTile) v.getTag()).mSpec; + ClipData data = ClipData.newPlainText(tileSpec, tileSpec); + v.startDrag(data, new View.DragShadowBuilder(v), null, 0); + onStartDrag(); + return true; + } + return false; + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/tuner/StatusBarSwitch.java b/packages/SystemUI/src/com/android/systemui/tuner/StatusBarSwitch.java new file mode 100644 index 0000000..d4cc56d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tuner/StatusBarSwitch.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.tuner; + +import android.app.ActivityManager; +import android.content.ContentResolver; +import android.content.Context; +import android.preference.SwitchPreference; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.AttributeSet; + +import com.android.systemui.statusbar.phone.StatusBarIconController; +import com.android.systemui.tuner.TunerService.Tunable; + +import java.util.Set; + +public class StatusBarSwitch extends SwitchPreference implements Tunable { + + private Set<String> mBlacklist; + + public StatusBarSwitch(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onTuningChanged(String key, String newValue) { + if (!StatusBarIconController.ICON_BLACKLIST.equals(key)) { + return; + } + mBlacklist = StatusBarIconController.getIconBlacklist(newValue); + setChecked(!mBlacklist.contains(getKey())); + } + + @Override + protected boolean persistBoolean(boolean value) { + if (!value) { + // If not enabled add to blacklist. + if (!mBlacklist.contains(getKey())) { + mBlacklist.add(getKey()); + setList(mBlacklist); + } + } else { + if (mBlacklist.remove(getKey())) { + setList(mBlacklist); + } + } + return true; + } + + private void setList(Set<String> blacklist) { + ContentResolver contentResolver = getContext().getContentResolver(); + Settings.Secure.putStringForUser(contentResolver, StatusBarIconController.ICON_BLACKLIST, + TextUtils.join(",", blacklist), ActivityManager.getCurrentUser()); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsCallback.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java index deb5670..c84f618 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/RecentsCallback.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,19 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.systemui.tuner; -package com.android.systemui.recent; +import android.app.Activity; +import android.os.Bundle; -import android.view.View; +public class TunerActivity extends Activity { -public interface RecentsCallback { - static final int SWIPE_LEFT = 0; - static final int SWIPE_RIGHT = 1; - static final int SWIPE_UP = 2; - static final int SWIPE_DOWN = 3; + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getFragmentManager().beginTransaction().replace(android.R.id.content, new TunerFragment()) + .commit(); + } - void handleOnClick(View selectedView); - void handleSwipe(View selectedView); - void handleLongPress(View selectedView, View anchorView, View thumbnailView); - void dismiss(); } diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java new file mode 100644 index 0000000..4a8c2e4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.tuner; + +import static com.android.systemui.BatteryMeterView.SHOW_PERCENT_SETTING; + +import android.app.FragmentTransaction; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; +import android.preference.SwitchPreference; +import android.provider.Settings.System; +import android.view.MenuItem; + +import com.android.systemui.R; +import com.android.systemui.statusbar.phone.StatusBarIconController; +import com.android.systemui.tuner.TunerService.Tunable; + +public class TunerFragment extends PreferenceFragment { + + private static final String KEY_QS_TUNER = "qs_tuner"; + private static final String KEY_DEMO_MODE = "demo_mode"; + private static final String KEY_BATTERY_PCT = "battery_pct"; + + private final SettingObserver mSettingObserver = new SettingObserver(); + + private SwitchPreference mBatteryPct; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.tuner_prefs); + getActivity().getActionBar().setDisplayHomeAsUpEnabled(true); + setHasOptionsMenu(true); + + findPreference(KEY_QS_TUNER).setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.replace(android.R.id.content, new QsTuner(), "QsTuner"); + ft.addToBackStack(null); + ft.commit(); + return true; + } + }); + findPreference(KEY_DEMO_MODE).setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.replace(android.R.id.content, new DemoModeFragment(), "DemoMode"); + ft.addToBackStack(null); + ft.commit(); + return true; + } + }); + mBatteryPct = (SwitchPreference) findPreference(KEY_BATTERY_PCT); + } + + @Override + public void onResume() { + super.onResume(); + updateBatteryPct(); + getContext().getContentResolver().registerContentObserver( + System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver); + + registerPrefs(getPreferenceScreen()); + } + + @Override + public void onPause() { + super.onPause(); + getContext().getContentResolver().unregisterContentObserver(mSettingObserver); + + unregisterPrefs(getPreferenceScreen()); + } + + private void registerPrefs(PreferenceGroup group) { + TunerService tunerService = TunerService.get(getContext()); + final int N = group.getPreferenceCount(); + for (int i = 0; i < N; i++) { + Preference pref = group.getPreference(i); + if (pref instanceof StatusBarSwitch) { + tunerService.addTunable((Tunable) pref, StatusBarIconController.ICON_BLACKLIST); + } else if (pref instanceof PreferenceGroup) { + registerPrefs((PreferenceGroup) pref); + } + } + } + + private void unregisterPrefs(PreferenceGroup group) { + TunerService tunerService = TunerService.get(getContext()); + final int N = group.getPreferenceCount(); + for (int i = 0; i < N; i++) { + Preference pref = group.getPreference(i); + if (pref instanceof Tunable) { + tunerService.removeTunable((Tunable) pref); + } else if (pref instanceof PreferenceGroup) { + registerPrefs((PreferenceGroup) pref); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + getActivity().finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void updateBatteryPct() { + mBatteryPct.setOnPreferenceChangeListener(null); + mBatteryPct.setChecked(System.getInt(getContext().getContentResolver(), + SHOW_PERCENT_SETTING, 0) != 0); + mBatteryPct.setOnPreferenceChangeListener(mBatteryPctChange); + } + + private final class SettingObserver extends ContentObserver { + public SettingObserver() { + super(new Handler()); + } + + @Override + public void onChange(boolean selfChange, Uri uri, int userId) { + super.onChange(selfChange, uri, userId); + updateBatteryPct(); + } + } + + private final OnPreferenceChangeListener mBatteryPctChange = new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean v = (Boolean) newValue; + System.putInt(getContext().getContentResolver(), SHOW_PERCENT_SETTING, v ? 1 : 0); + return true; + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java new file mode 100644 index 0000000..de5aaf6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.tuner; + +import android.app.ActivityManager; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.util.ArrayMap; + +import com.android.systemui.SystemUI; +import com.android.systemui.SystemUIApplication; +import com.android.systemui.settings.CurrentUserTracker; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + + +public class TunerService extends SystemUI { + + private final Observer mObserver = new Observer(); + // Map of Uris we listen on to their settings keys. + private final ArrayMap<Uri, String> mListeningUris = new ArrayMap<>(); + // Map of settings keys to the listener. + private final HashMap<String, List<Tunable>> mTunableLookup = new HashMap<>(); + + private ContentResolver mContentResolver; + private int mCurrentUser; + private CurrentUserTracker mUserTracker; + + @Override + public void start() { + mContentResolver = mContext.getContentResolver(); + putComponent(TunerService.class, this); + + mCurrentUser = ActivityManager.getCurrentUser(); + mUserTracker = new CurrentUserTracker(mContext) { + @Override + public void onUserSwitched(int newUserId) { + mCurrentUser = newUserId; + reloadAll(); + reregisterAll(); + } + }; + mUserTracker.startTracking(); + } + + public void addTunable(Tunable tunable, String... keys) { + for (String key : keys) { + addTunable(tunable, key); + } + } + + private void addTunable(Tunable tunable, String key) { + if (!mTunableLookup.containsKey(key)) { + mTunableLookup.put(key, new ArrayList<Tunable>()); + } + mTunableLookup.get(key).add(tunable); + Uri uri = Settings.Secure.getUriFor(key); + if (!mListeningUris.containsKey(uri)) { + mListeningUris.put(uri, key); + mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser); + } + // Send the first state. + String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); + tunable.onTuningChanged(key, value); + } + + public void removeTunable(Tunable tunable) { + for (List<Tunable> list : mTunableLookup.values()) { + list.remove(tunable); + } + } + + protected void reregisterAll() { + if (mListeningUris.size() == 0) { + return; + } + mContentResolver.unregisterContentObserver(mObserver); + for (Uri uri : mListeningUris.keySet()) { + mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser); + } + } + + public void reloadSetting(Uri uri) { + String key = mListeningUris.get(uri); + String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); + for (Tunable tunable : mTunableLookup.get(key)) { + tunable.onTuningChanged(key, value); + } + } + + private void reloadAll() { + for (String key : mTunableLookup.keySet()) { + String value = Settings.Secure.getStringForUser(mContentResolver, key, + mCurrentUser); + for (Tunable tunable : mTunableLookup.get(key)) { + tunable.onTuningChanged(key, value); + } + } + } + + // Only used in other processes, such as the tuner. + private static TunerService sInstance; + + public static TunerService get(Context context) { + SystemUIApplication sysUi = (SystemUIApplication) context.getApplicationContext(); + TunerService service = sysUi.getComponent(TunerService.class); + if (service == null) { + // Can't get it as a component, must in the tuner, lets just create one for now. + return getStaticService(context); + } + return service; + } + + private static TunerService getStaticService(Context context) { + if (sInstance == null) { + sInstance = new TunerService(); + sInstance.mContext = context.getApplicationContext(); + sInstance.mComponents = new HashMap<>(); + sInstance.start(); + } + return sInstance; + } + + private class Observer extends ContentObserver { + public Observer() { + super(new Handler(Looper.getMainLooper())); + } + + @Override + public void onChange(boolean selfChange, Uri uri, int userId) { + if (userId == ActivityManager.getCurrentUser()) { + reloadSetting(uri); + } + } + } + + public interface Tunable { + void onTuningChanged(String key, String newValue); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java index f804736..180d918 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java +++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 Google Inc. + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,424 +17,652 @@ package com.android.systemui.usb; import android.app.Notification; +import android.app.Notification.Action; import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; -import android.os.Environment; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.MoveCallback; +import android.os.Bundle; import android.os.Handler; -import android.os.HandlerThread; import android.os.UserHandle; +import android.os.storage.DiskInfo; import android.os.storage.StorageEventListener; import android.os.storage.StorageManager; -import android.os.storage.StorageVolume; -import android.provider.Settings; +import android.os.storage.VolumeInfo; +import android.os.storage.VolumeRecord; +import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.Log; +import android.util.SparseArray; +import com.android.internal.R; import com.android.systemui.SystemUI; +import java.util.List; + public class StorageNotification extends SystemUI { private static final String TAG = "StorageNotification"; - private static final boolean DEBUG = false; - - private static final boolean POP_UMS_ACTIVITY_ON_CONNECT = true; - - /** - * The notification that is shown when a USB mass storage host - * is connected. - * <p> - * This is lazily created, so use {@link #setUsbStorageNotification()}. - */ - private Notification mUsbStorageNotification; - - /** - * The notification that is shown when the following media events occur: - * - Media is being checked - * - Media is blank (or unknown filesystem) - * - Media is corrupt - * - Media is safe to unmount - * - Media is missing - * <p> - * This is lazily created, so use {@link #setMediaStorageNotification()}. - */ - private Notification mMediaStorageNotification; - private boolean mUmsAvailable; + + private static final int PUBLIC_ID = 0x53505542; // SPUB + private static final int PRIVATE_ID = 0x53505256; // SPRV + private static final int DISK_ID = 0x5344534b; // SDSK + private static final int MOVE_ID = 0x534d4f56; // SMOV + + private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME"; + private static final String ACTION_FINISH_WIZARD = "com.android.systemui.action.FINISH_WIZARD"; + + // TODO: delay some notifications to avoid bumpy fast operations + + private NotificationManager mNotificationManager; private StorageManager mStorageManager; - private Handler mAsyncEventHandler; + private static class MoveInfo { + public int moveId; + public Bundle extras; + public String packageName; + public String label; + public String volumeUuid; + } + + private final SparseArray<MoveInfo> mMoves = new SparseArray<>(); - private class StorageNotificationEventListener extends StorageEventListener { - public void onUsbMassStorageConnectionChanged(final boolean connected) { - mAsyncEventHandler.post(new Runnable() { - @Override - public void run() { - onUsbMassStorageConnectionChangedAsync(connected); - } - }); + private final StorageEventListener mListener = new StorageEventListener() { + @Override + public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { + onVolumeStateChangedInternal(vol); } - public void onStorageStateChanged(final String path, - final String oldState, final String newState) { - mAsyncEventHandler.post(new Runnable() { - @Override - public void run() { - onStorageStateChangedAsync(path, oldState, newState); - } - }); + + @Override + public void onVolumeRecordChanged(VolumeRecord rec) { + // Avoid kicking notifications when getting early metadata before + // mounted. If already mounted, we're being kicked because of a + // nickname or init'ed change. + final VolumeInfo vol = mStorageManager.findVolumeByUuid(rec.getFsUuid()); + if (vol != null && vol.isMountedReadable()) { + onVolumeStateChangedInternal(vol); + } } - } + + @Override + public void onVolumeForgotten(String fsUuid) { + // Stop annoying the user + mNotificationManager.cancelAsUser(fsUuid, PRIVATE_ID, UserHandle.ALL); + } + + @Override + public void onDiskScanned(DiskInfo disk, int volumeCount) { + onDiskScannedInternal(disk, volumeCount); + } + }; + + private final BroadcastReceiver mSnoozeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // TODO: kick this onto background thread + final String fsUuid = intent.getStringExtra(VolumeRecord.EXTRA_FS_UUID); + mStorageManager.setVolumeSnoozed(fsUuid, true); + } + }; + + private final BroadcastReceiver mFinishReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // When finishing the adoption wizard, clean up any notifications + // for moving primary storage + mNotificationManager.cancelAsUser(null, MOVE_ID, UserHandle.ALL); + } + }; + + private final MoveCallback mMoveCallback = new MoveCallback() { + @Override + public void onCreated(int moveId, Bundle extras) { + final MoveInfo move = new MoveInfo(); + move.moveId = moveId; + move.extras = extras; + if (extras != null) { + move.packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); + move.label = extras.getString(Intent.EXTRA_TITLE); + move.volumeUuid = extras.getString(VolumeRecord.EXTRA_FS_UUID); + } + mMoves.put(moveId, move); + } + + @Override + public void onStatusChanged(int moveId, int status, long estMillis) { + final MoveInfo move = mMoves.get(moveId); + if (move == null) { + Log.w(TAG, "Ignoring unknown move " + moveId); + return; + } + + if (PackageManager.isMoveStatusFinished(status)) { + onMoveFinished(move, status); + } else { + onMoveProgress(move, status, estMillis); + } + } + }; @Override public void start() { - mStorageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); - final boolean connected = mStorageManager.isUsbMassStorageConnected(); - if (DEBUG) Log.d(TAG, String.format( "Startup with UMS connection %s (media state %s)", - mUmsAvailable, Environment.getExternalStorageState())); - - HandlerThread thr = new HandlerThread("SystemUI StorageNotification"); - thr.start(); - mAsyncEventHandler = new Handler(thr.getLooper()); - - StorageNotificationEventListener listener = new StorageNotificationEventListener(); - listener.onUsbMassStorageConnectionChanged(connected); - mStorageManager.registerListener(listener); - } + mNotificationManager = mContext.getSystemService(NotificationManager.class); + + mStorageManager = mContext.getSystemService(StorageManager.class); + mStorageManager.registerListener(mListener); + + mContext.registerReceiver(mSnoozeReceiver, new IntentFilter(ACTION_SNOOZE_VOLUME), + android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null); + mContext.registerReceiver(mFinishReceiver, new IntentFilter(ACTION_FINISH_WIZARD), + android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null); - private void onUsbMassStorageConnectionChangedAsync(boolean connected) { - mUmsAvailable = connected; - /* - * Even though we may have a UMS host connected, we the SD card - * may not be in a state for export. - */ - String st = Environment.getExternalStorageState(); - - if (DEBUG) Log.i(TAG, String.format("UMS connection changed to %s (media state %s)", - connected, st)); - - if (connected && (st.equals( - Environment.MEDIA_REMOVED) || st.equals(Environment.MEDIA_CHECKING))) { - /* - * No card or card being checked = don't display - */ - connected = false; + // Kick current state into place + final List<DiskInfo> disks = mStorageManager.getDisks(); + for (DiskInfo disk : disks) { + onDiskScannedInternal(disk, disk.volumeCount); } - updateUsbMassStorageNotification(connected); + + final List<VolumeInfo> vols = mStorageManager.getVolumes(); + for (VolumeInfo vol : vols) { + onVolumeStateChangedInternal(vol); + } + + mContext.getPackageManager().registerMoveCallback(mMoveCallback, new Handler()); + + updateMissingPrivateVolumes(); } - private void onStorageStateChangedAsync(String path, String oldState, String newState) { - if (DEBUG) Log.i(TAG, String.format( - "Media {%s} state changed from {%s} -> {%s}", path, oldState, newState)); - if (newState.equals(Environment.MEDIA_SHARED)) { - /* - * Storage is now shared. Modify the UMS notification - * for stopping UMS. - */ - Intent intent = new Intent(); - intent.setClass(mContext, com.android.systemui.usb.UsbStorageActivity.class); - PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); - setUsbStorageNotification( - com.android.internal.R.string.usb_storage_stop_notification_title, - com.android.internal.R.string.usb_storage_stop_notification_message, - com.android.internal.R.drawable.stat_sys_warning, false, true, pi); - } else if (newState.equals(Environment.MEDIA_CHECKING)) { - /* - * Storage is now checking. Update media notification and disable - * UMS notification. - */ - setMediaStorageNotification( - com.android.internal.R.string.ext_media_checking_notification_title, - com.android.internal.R.string.ext_media_checking_notification_message, - com.android.internal.R.drawable.stat_notify_sdcard_prepare, true, false, null); - updateUsbMassStorageNotification(false); - } else if (newState.equals(Environment.MEDIA_MOUNTED)) { - /* - * Storage is now mounted. Dismiss any media notifications, - * and enable UMS notification if connected. - */ - setMediaStorageNotification(0, 0, 0, false, false, null); - updateUsbMassStorageNotification(mUmsAvailable); - } else if (newState.equals(Environment.MEDIA_UNMOUNTED)) { - /* - * Storage is now unmounted. We may have been unmounted - * because the user is enabling/disabling UMS, in which case we don't - * want to display the 'safe to unmount' notification. - */ - if (!mStorageManager.isUsbMassStorageEnabled()) { - if (oldState.equals(Environment.MEDIA_SHARED)) { - /* - * The unmount was due to UMS being enabled. Dismiss any - * media notifications, and enable UMS notification if connected - */ - setMediaStorageNotification(0, 0, 0, false, false, null); - updateUsbMassStorageNotification(mUmsAvailable); - } else { - /* - * Show safe to unmount media notification, and enable UMS - * notification if connected. - */ - if (Environment.isExternalStorageRemovable()) { - setMediaStorageNotification( - com.android.internal.R.string.ext_media_safe_unmount_notification_title, - com.android.internal.R.string.ext_media_safe_unmount_notification_message, - com.android.internal.R.drawable.stat_notify_sdcard, true, true, null); - } else { - // This device does not have removable storage, so - // don't tell the user they can remove it. - setMediaStorageNotification(0, 0, 0, false, false, null); - } - updateUsbMassStorageNotification(mUmsAvailable); - } + private void updateMissingPrivateVolumes() { + final List<VolumeRecord> recs = mStorageManager.getVolumeRecords(); + for (VolumeRecord rec : recs) { + if (rec.getType() != VolumeInfo.TYPE_PRIVATE) continue; + + final String fsUuid = rec.getFsUuid(); + final VolumeInfo info = mStorageManager.findVolumeByUuid(fsUuid); + if ((info != null && info.isMountedWritable()) || rec.isSnoozed()) { + // Yay, private volume is here, or user snoozed + mNotificationManager.cancelAsUser(fsUuid, PRIVATE_ID, UserHandle.ALL); + } else { - /* - * The unmount was due to UMS being enabled. Dismiss any - * media notifications, and disable the UMS notification - */ - setMediaStorageNotification(0, 0, 0, false, false, null); - updateUsbMassStorageNotification(false); + // Boo, annoy the user to reinsert the private volume + final CharSequence title = mContext.getString(R.string.ext_media_missing_title, + rec.getNickname()); + final CharSequence text = mContext.getString(R.string.ext_media_missing_message); + + final Notification notif = new Notification.Builder(mContext) + .setSmallIcon(R.drawable.ic_sd_card_48dp) + .setColor(mContext.getColor(R.color.system_notification_accent_color)) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(buildForgetPendingIntent(rec)) + .setStyle(new Notification.BigTextStyle().bigText(text)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setLocalOnly(true) + .setCategory(Notification.CATEGORY_SYSTEM) + .setDeleteIntent(buildSnoozeIntent(fsUuid)) + .build(); + + mNotificationManager.notifyAsUser(fsUuid, PRIVATE_ID, notif, UserHandle.ALL); } - } else if (newState.equals(Environment.MEDIA_NOFS)) { - /* - * Storage has no filesystem. Show blank media notification, - * and enable UMS notification if connected. - */ - Intent intent = new Intent(); - intent.setClass(mContext, com.android.internal.app.ExternalMediaFormatActivity.class); - intent.putExtra(StorageVolume.EXTRA_STORAGE_VOLUME, - getVolumeByPath(mStorageManager.getVolumeList(), path)); - PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); - - setMediaStorageNotification( - com.android.internal.R.string.ext_media_nofs_notification_title, - com.android.internal.R.string.ext_media_nofs_notification_message, - com.android.internal.R.drawable.stat_notify_sdcard_usb, true, false, pi); - updateUsbMassStorageNotification(mUmsAvailable); - } else if (newState.equals(Environment.MEDIA_UNMOUNTABLE)) { - /* - * Storage is corrupt. Show corrupt media notification, - * and enable UMS notification if connected. - */ - Intent intent = new Intent(); - intent.setClass(mContext, com.android.internal.app.ExternalMediaFormatActivity.class); - intent.putExtra(StorageVolume.EXTRA_STORAGE_VOLUME, - getVolumeByPath(mStorageManager.getVolumeList(), path)); - PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); - - setMediaStorageNotification( - com.android.internal.R.string.ext_media_unmountable_notification_title, - com.android.internal.R.string.ext_media_unmountable_notification_message, - com.android.internal.R.drawable.stat_notify_sdcard_usb, true, false, pi); - updateUsbMassStorageNotification(mUmsAvailable); - } else if (newState.equals(Environment.MEDIA_REMOVED)) { - /* - * Storage has been removed. Show nomedia media notification, - * and disable UMS notification regardless of connection state. - */ - setMediaStorageNotification( - com.android.internal.R.string.ext_media_nomedia_notification_title, - com.android.internal.R.string.ext_media_nomedia_notification_message, - com.android.internal.R.drawable.stat_notify_sdcard_usb, - true, false, null); - updateUsbMassStorageNotification(false); - } else if (newState.equals(Environment.MEDIA_BAD_REMOVAL)) { - /* - * Storage has been removed unsafely. Show bad removal media notification, - * and disable UMS notification regardless of connection state. - */ - setMediaStorageNotification( - com.android.internal.R.string.ext_media_badremoval_notification_title, - com.android.internal.R.string.ext_media_badremoval_notification_message, - com.android.internal.R.drawable.stat_sys_warning, - true, true, null); - updateUsbMassStorageNotification(false); + } + } + + private void onDiskScannedInternal(DiskInfo disk, int volumeCount) { + if (volumeCount == 0 && disk.size > 0) { + // No supported volumes found, give user option to format + final CharSequence title = mContext.getString( + R.string.ext_media_unsupported_notification_title, disk.getDescription()); + final CharSequence text = mContext.getString( + R.string.ext_media_unsupported_notification_message, disk.getDescription()); + + final Notification notif = new Notification.Builder(mContext) + .setSmallIcon(getSmallIcon(disk, VolumeInfo.STATE_UNMOUNTABLE)) + .setColor(mContext.getColor(R.color.system_notification_accent_color)) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(buildInitPendingIntent(disk)) + .setStyle(new Notification.BigTextStyle().bigText(text)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setLocalOnly(true) + .setCategory(Notification.CATEGORY_ERROR) + .build(); + + mNotificationManager.notifyAsUser(disk.getId(), DISK_ID, notif, UserHandle.ALL); + } else { - Log.w(TAG, String.format("Ignoring unknown state {%s}", newState)); + // Yay, we have volumes! + mNotificationManager.cancelAsUser(disk.getId(), DISK_ID, UserHandle.ALL); } } - /** - * Get the corresponding StorageVolume object for a specific path. - */ - private final StorageVolume getVolumeByPath(StorageVolume[] volumes, String path) { - for (StorageVolume volume : volumes) { - if (volume.getPath().equals(path)) { - return volume; - } + private void onVolumeStateChangedInternal(VolumeInfo vol) { + switch (vol.getType()) { + case VolumeInfo.TYPE_PRIVATE: + onPrivateVolumeStateChangedInternal(vol); + break; + case VolumeInfo.TYPE_PUBLIC: + onPublicVolumeStateChangedInternal(vol); + break; } - Log.w(TAG, "No storage found"); - return null; } - /** - * Update the state of the USB mass storage notification - */ - void updateUsbMassStorageNotification(boolean available) { - - if (available) { - Intent intent = new Intent(); - intent.setClass(mContext, com.android.systemui.usb.UsbStorageActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); - setUsbStorageNotification( - com.android.internal.R.string.usb_storage_notification_title, - com.android.internal.R.string.usb_storage_notification_message, - com.android.internal.R.drawable.stat_sys_data_usb, - false, true, pi); + private void onPrivateVolumeStateChangedInternal(VolumeInfo vol) { + Log.d(TAG, "Notifying about private volume: " + vol.toString()); + + updateMissingPrivateVolumes(); + } + + private void onPublicVolumeStateChangedInternal(VolumeInfo vol) { + Log.d(TAG, "Notifying about public volume: " + vol.toString()); + + final Notification notif; + switch (vol.getState()) { + case VolumeInfo.STATE_UNMOUNTED: + notif = onVolumeUnmounted(vol); + break; + case VolumeInfo.STATE_CHECKING: + notif = onVolumeChecking(vol); + break; + case VolumeInfo.STATE_MOUNTED: + case VolumeInfo.STATE_MOUNTED_READ_ONLY: + notif = onVolumeMounted(vol); + break; + case VolumeInfo.STATE_FORMATTING: + notif = onVolumeFormatting(vol); + break; + case VolumeInfo.STATE_EJECTING: + notif = onVolumeEjecting(vol); + break; + case VolumeInfo.STATE_UNMOUNTABLE: + notif = onVolumeUnmountable(vol); + break; + case VolumeInfo.STATE_REMOVED: + notif = onVolumeRemoved(vol); + break; + case VolumeInfo.STATE_BAD_REMOVAL: + notif = onVolumeBadRemoval(vol); + break; + default: + notif = null; + break; + } + + if (notif != null) { + mNotificationManager.notifyAsUser(vol.getId(), PUBLIC_ID, notif, UserHandle.ALL); } else { - setUsbStorageNotification(0, 0, 0, false, false, null); + mNotificationManager.cancelAsUser(vol.getId(), PUBLIC_ID, UserHandle.ALL); } } - /** - * Sets the USB storage notification. - */ - private synchronized void setUsbStorageNotification(int titleId, int messageId, int icon, - boolean sound, boolean visible, PendingIntent pi) { + private Notification onVolumeUnmounted(VolumeInfo vol) { + // Ignored + return null; + } - if (!visible && mUsbStorageNotification == null) { - return; - } + private Notification onVolumeChecking(VolumeInfo vol) { + final DiskInfo disk = vol.getDisk(); + final CharSequence title = mContext.getString( + R.string.ext_media_checking_notification_title, disk.getDescription()); + final CharSequence text = mContext.getString( + R.string.ext_media_checking_notification_message, disk.getDescription()); + + return buildNotificationBuilder(vol, title, text) + .setCategory(Notification.CATEGORY_PROGRESS) + .setPriority(Notification.PRIORITY_LOW) + .setOngoing(true) + .build(); + } - NotificationManager notificationManager = (NotificationManager) mContext - .getSystemService(Context.NOTIFICATION_SERVICE); + private Notification onVolumeMounted(VolumeInfo vol) { + final VolumeRecord rec = mStorageManager.findRecordByUuid(vol.getFsUuid()); + final DiskInfo disk = vol.getDisk(); - if (notificationManager == null) { - return; + // Don't annoy when user dismissed in past. (But make sure the disk is adoptable; we + // used to allow snoozing non-adoptable disks too.) + if (rec.isSnoozed() && disk.isAdoptable()) { + return null; } - if (visible) { - Resources r = Resources.getSystem(); - CharSequence title = r.getText(titleId); - CharSequence message = r.getText(messageId); + if (disk.isAdoptable() && !rec.isInited()) { + final CharSequence title = disk.getDescription(); + final CharSequence text = mContext.getString( + R.string.ext_media_new_notification_message, disk.getDescription()); + + final PendingIntent initIntent = buildInitPendingIntent(vol); + return buildNotificationBuilder(vol, title, text) + .addAction(new Action(R.drawable.ic_settings_24dp, + mContext.getString(R.string.ext_media_init_action), initIntent)) + .addAction(new Action(R.drawable.ic_eject_24dp, + mContext.getString(R.string.ext_media_unmount_action), + buildUnmountPendingIntent(vol))) + .setContentIntent(initIntent) + .setDeleteIntent(buildSnoozeIntent(vol.getFsUuid())) + .setCategory(Notification.CATEGORY_SYSTEM) + .build(); - if (mUsbStorageNotification == null) { - mUsbStorageNotification = new Notification(); - mUsbStorageNotification.icon = icon; - mUsbStorageNotification.when = 0; + } else { + final CharSequence title = disk.getDescription(); + final CharSequence text = mContext.getString( + R.string.ext_media_ready_notification_message, disk.getDescription()); + + final PendingIntent browseIntent = buildBrowsePendingIntent(vol); + final Notification.Builder builder = buildNotificationBuilder(vol, title, text) + .addAction(new Action(R.drawable.ic_folder_24dp, + mContext.getString(R.string.ext_media_browse_action), + browseIntent)) + .addAction(new Action(R.drawable.ic_eject_24dp, + mContext.getString(R.string.ext_media_unmount_action), + buildUnmountPendingIntent(vol))) + .setContentIntent(browseIntent) + .setCategory(Notification.CATEGORY_SYSTEM) + .setPriority(Notification.PRIORITY_LOW); + // Non-adoptable disks can't be snoozed. + if (disk.isAdoptable()) { + builder.setDeleteIntent(buildSnoozeIntent(vol.getFsUuid())); } - if (sound) { - mUsbStorageNotification.defaults |= Notification.DEFAULT_SOUND; - } else { - mUsbStorageNotification.defaults &= ~Notification.DEFAULT_SOUND; - } + return builder.build(); + } + } - mUsbStorageNotification.flags = Notification.FLAG_ONGOING_EVENT; + private Notification onVolumeFormatting(VolumeInfo vol) { + // Ignored + return null; + } - mUsbStorageNotification.tickerText = title; - if (pi == null) { - Intent intent = new Intent(); - pi = PendingIntent.getBroadcastAsUser(mContext, 0, intent, 0, - UserHandle.CURRENT); - } - mUsbStorageNotification.color = mContext.getResources().getColor( - com.android.internal.R.color.system_notification_accent_color); - 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, - 0); - - if (POP_UMS_ACTIVITY_ON_CONNECT && !adbOn) { - // Pop up a full-screen alert to coach the user through enabling UMS. The average - // user has attached the device to USB either to charge the phone (in which case - // this is harmless) or transfer files, and in the latter case this alert saves - // several steps (as well as subtly indicates that you shouldn't mix UMS with other - // activities on the device). - // - // If ADB is enabled, however, we suppress this dialog (under the assumption that a - // developer (a) knows how to enable UMS, and (b) is probably using USB to install - // builds or use adb commands. - mUsbStorageNotification.fullScreenIntent = pi; - } - } + private Notification onVolumeEjecting(VolumeInfo vol) { + final DiskInfo disk = vol.getDisk(); + final CharSequence title = mContext.getString( + R.string.ext_media_unmounting_notification_title, disk.getDescription()); + final CharSequence text = mContext.getString( + R.string.ext_media_unmounting_notification_message, disk.getDescription()); + + return buildNotificationBuilder(vol, title, text) + .setCategory(Notification.CATEGORY_PROGRESS) + .setPriority(Notification.PRIORITY_LOW) + .setOngoing(true) + .build(); + } - final int notificationId = mUsbStorageNotification.icon; - if (visible) { - notificationManager.notifyAsUser(null, notificationId, mUsbStorageNotification, - UserHandle.ALL); - } else { - notificationManager.cancelAsUser(null, notificationId, UserHandle.ALL); + private Notification onVolumeUnmountable(VolumeInfo vol) { + final DiskInfo disk = vol.getDisk(); + final CharSequence title = mContext.getString( + R.string.ext_media_unmountable_notification_title, disk.getDescription()); + final CharSequence text = mContext.getString( + R.string.ext_media_unmountable_notification_message, disk.getDescription()); + + return buildNotificationBuilder(vol, title, text) + .setContentIntent(buildInitPendingIntent(vol)) + .setCategory(Notification.CATEGORY_ERROR) + .build(); + } + + private Notification onVolumeRemoved(VolumeInfo vol) { + if (!vol.isPrimary()) { + // Ignore non-primary media + return null; } + + final DiskInfo disk = vol.getDisk(); + final CharSequence title = mContext.getString( + R.string.ext_media_nomedia_notification_title, disk.getDescription()); + final CharSequence text = mContext.getString( + R.string.ext_media_nomedia_notification_message, disk.getDescription()); + + return buildNotificationBuilder(vol, title, text) + .setCategory(Notification.CATEGORY_ERROR) + .build(); } - private synchronized boolean getMediaStorageNotificationDismissable() { - if ((mMediaStorageNotification != null) && - ((mMediaStorageNotification.flags & Notification.FLAG_AUTO_CANCEL) == - Notification.FLAG_AUTO_CANCEL)) - return true; + private Notification onVolumeBadRemoval(VolumeInfo vol) { + if (!vol.isPrimary()) { + // Ignore non-primary media + return null; + } - return false; + final DiskInfo disk = vol.getDisk(); + final CharSequence title = mContext.getString( + R.string.ext_media_badremoval_notification_title, disk.getDescription()); + final CharSequence text = mContext.getString( + R.string.ext_media_badremoval_notification_message, disk.getDescription()); + + return buildNotificationBuilder(vol, title, text) + .setCategory(Notification.CATEGORY_ERROR) + .build(); } - /** - * Sets the media storage notification. - */ - private synchronized void setMediaStorageNotification(int titleId, int messageId, int icon, boolean visible, - boolean dismissable, PendingIntent pi) { + private void onMoveProgress(MoveInfo move, int status, long estMillis) { + final CharSequence title; + if (!TextUtils.isEmpty(move.label)) { + title = mContext.getString(R.string.ext_media_move_specific_title, move.label); + } else { + title = mContext.getString(R.string.ext_media_move_title); + } - if (!visible && mMediaStorageNotification == null) { - return; + final CharSequence text; + if (estMillis < 0) { + text = null; + } else { + text = DateUtils.formatDuration(estMillis); } - NotificationManager notificationManager = (NotificationManager) mContext - .getSystemService(Context.NOTIFICATION_SERVICE); + final PendingIntent intent; + if (move.packageName != null) { + intent = buildWizardMovePendingIntent(move); + } else { + intent = buildWizardMigratePendingIntent(move); + } - if (notificationManager == null) { + final Notification notif = new Notification.Builder(mContext) + .setSmallIcon(R.drawable.ic_sd_card_48dp) + .setColor(mContext.getColor(R.color.system_notification_accent_color)) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(intent) + .setStyle(new Notification.BigTextStyle().bigText(text)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setLocalOnly(true) + .setCategory(Notification.CATEGORY_PROGRESS) + .setPriority(Notification.PRIORITY_LOW) + .setProgress(100, status, false) + .setOngoing(true) + .build(); + + mNotificationManager.notifyAsUser(move.packageName, MOVE_ID, notif, UserHandle.ALL); + } + + private void onMoveFinished(MoveInfo move, int status) { + if (move.packageName != null) { + // We currently ignore finished app moves; just clear the last + // published progress + mNotificationManager.cancelAsUser(move.packageName, MOVE_ID, UserHandle.ALL); return; } - if (mMediaStorageNotification != null && visible) { - /* - * Dismiss the previous notification - we're about to - * re-use it. - */ - final int notificationId = mMediaStorageNotification.icon; - notificationManager.cancel(notificationId); + final VolumeInfo privateVol = mContext.getPackageManager().getPrimaryStorageCurrentVolume(); + final String descrip = mStorageManager.getBestVolumeDescription(privateVol); + + final CharSequence title; + final CharSequence text; + if (status == PackageManager.MOVE_SUCCEEDED) { + title = mContext.getString(R.string.ext_media_move_success_title); + text = mContext.getString(R.string.ext_media_move_success_message, descrip); + } else { + title = mContext.getString(R.string.ext_media_move_failure_title); + text = mContext.getString(R.string.ext_media_move_failure_message); + } + + // Jump back into the wizard flow if we moved to a real disk + final PendingIntent intent; + if (privateVol != null && privateVol.getDisk() != null) { + intent = buildWizardReadyPendingIntent(privateVol.getDisk()); + } else if (privateVol != null) { + intent = buildVolumeSettingsPendingIntent(privateVol); + } else { + intent = null; } - if (visible) { - Resources r = Resources.getSystem(); - CharSequence title = r.getText(titleId); - CharSequence message = r.getText(messageId); + final Notification notif = new Notification.Builder(mContext) + .setSmallIcon(R.drawable.ic_sd_card_48dp) + .setColor(mContext.getColor(R.color.system_notification_accent_color)) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(intent) + .setStyle(new Notification.BigTextStyle().bigText(text)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setLocalOnly(true) + .setCategory(Notification.CATEGORY_SYSTEM) + .setPriority(Notification.PRIORITY_LOW) + .setAutoCancel(true) + .build(); + + mNotificationManager.notifyAsUser(move.packageName, MOVE_ID, notif, UserHandle.ALL); + } - if (mMediaStorageNotification == null) { - mMediaStorageNotification = new Notification(); - mMediaStorageNotification.when = 0; + private int getSmallIcon(DiskInfo disk, int state) { + if (disk.isSd()) { + switch (state) { + case VolumeInfo.STATE_CHECKING: + case VolumeInfo.STATE_EJECTING: + return R.drawable.ic_sd_card_48dp; + default: + return R.drawable.ic_sd_card_48dp; } + } else if (disk.isUsb()) { + return R.drawable.ic_usb_48dp; + } else { + return R.drawable.ic_sd_card_48dp; + } + } - mMediaStorageNotification.defaults &= ~Notification.DEFAULT_SOUND; + private Notification.Builder buildNotificationBuilder(VolumeInfo vol, CharSequence title, + CharSequence text) { + return new Notification.Builder(mContext) + .setSmallIcon(getSmallIcon(vol.getDisk(), vol.getState())) + .setColor(mContext.getColor(R.color.system_notification_accent_color)) + .setContentTitle(title) + .setContentText(text) + .setStyle(new Notification.BigTextStyle().bigText(text)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setLocalOnly(true); + } - if (dismissable) { - mMediaStorageNotification.flags = Notification.FLAG_AUTO_CANCEL; - } else { - mMediaStorageNotification.flags = Notification.FLAG_ONGOING_EVENT; - } + private PendingIntent buildInitPendingIntent(DiskInfo disk) { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.deviceinfo.StorageWizardInit"); + intent.putExtra(DiskInfo.EXTRA_DISK_ID, disk.getId()); - mMediaStorageNotification.tickerText = title; - if (pi == null) { - Intent intent = new Intent(); - pi = PendingIntent.getBroadcastAsUser(mContext, 0, intent, 0, - UserHandle.CURRENT); - } + final int requestKey = disk.getId().hashCode(); + return PendingIntent.getActivityAsUser(mContext, requestKey, intent, + PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); + } - mMediaStorageNotification.icon = icon; - mMediaStorageNotification.color = mContext.getResources().getColor( - com.android.internal.R.color.system_notification_accent_color); - mMediaStorageNotification.setLatestEventInfo(mContext, title, message, pi); - mMediaStorageNotification.visibility = Notification.VISIBILITY_PUBLIC; - mMediaStorageNotification.category = Notification.CATEGORY_SYSTEM; - } + private PendingIntent buildInitPendingIntent(VolumeInfo vol) { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.deviceinfo.StorageWizardInit"); + intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); - final int notificationId = mMediaStorageNotification.icon; - if (visible) { - notificationManager.notifyAsUser(null, notificationId, - mMediaStorageNotification, UserHandle.ALL); - } else { - notificationManager.cancelAsUser(null, notificationId, UserHandle.ALL); + final int requestKey = vol.getId().hashCode(); + return PendingIntent.getActivityAsUser(mContext, requestKey, intent, + PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); + } + + private PendingIntent buildUnmountPendingIntent(VolumeInfo vol) { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.deviceinfo.StorageUnmountReceiver"); + intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); + + final int requestKey = vol.getId().hashCode(); + return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent, + PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT); + } + + private PendingIntent buildBrowsePendingIntent(VolumeInfo vol) { + final Intent intent = vol.buildBrowseIntent(); + + final int requestKey = vol.getId().hashCode(); + return PendingIntent.getActivityAsUser(mContext, requestKey, intent, + PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); + } + + private PendingIntent buildVolumeSettingsPendingIntent(VolumeInfo vol) { + final Intent intent = new Intent(); + switch (vol.getType()) { + case VolumeInfo.TYPE_PRIVATE: + intent.setClassName("com.android.settings", + "com.android.settings.Settings$PrivateVolumeSettingsActivity"); + break; + case VolumeInfo.TYPE_PUBLIC: + intent.setClassName("com.android.settings", + "com.android.settings.Settings$PublicVolumeSettingsActivity"); + break; + default: + return null; } + intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); + + final int requestKey = vol.getId().hashCode(); + return PendingIntent.getActivityAsUser(mContext, requestKey, intent, + PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); + } + + private PendingIntent buildSnoozeIntent(String fsUuid) { + final Intent intent = new Intent(ACTION_SNOOZE_VOLUME); + intent.putExtra(VolumeRecord.EXTRA_FS_UUID, fsUuid); + + final int requestKey = fsUuid.hashCode(); + return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent, + PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT); + } + + private PendingIntent buildForgetPendingIntent(VolumeRecord rec) { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.Settings$PrivateVolumeForgetActivity"); + intent.putExtra(VolumeRecord.EXTRA_FS_UUID, rec.getFsUuid()); + + final int requestKey = rec.getFsUuid().hashCode(); + return PendingIntent.getActivityAsUser(mContext, requestKey, intent, + PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); + } + + private PendingIntent buildWizardMigratePendingIntent(MoveInfo move) { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.deviceinfo.StorageWizardMigrateProgress"); + intent.putExtra(PackageManager.EXTRA_MOVE_ID, move.moveId); + + final VolumeInfo vol = mStorageManager.findVolumeByQualifiedUuid(move.volumeUuid); + intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); + + return PendingIntent.getActivityAsUser(mContext, move.moveId, intent, + PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); + } + + private PendingIntent buildWizardMovePendingIntent(MoveInfo move) { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.deviceinfo.StorageWizardMoveProgress"); + intent.putExtra(PackageManager.EXTRA_MOVE_ID, move.moveId); + + return PendingIntent.getActivityAsUser(mContext, move.moveId, intent, + PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); + } + + private PendingIntent buildWizardReadyPendingIntent(DiskInfo disk) { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.deviceinfo.StorageWizardReady"); + intent.putExtra(DiskInfo.EXTRA_DISK_ID, disk.getId()); + + final int requestKey = disk.getId().hashCode(); + return PendingIntent.getActivityAsUser(mContext, requestKey, intent, + PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); } } diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbDebuggingSecondaryUserActivity.java b/packages/SystemUI/src/com/android/systemui/usb/UsbDebuggingSecondaryUserActivity.java new file mode 100644 index 0000000..9ce771b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/usb/UsbDebuggingSecondaryUserActivity.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.usb; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.UserInfo; +import android.hardware.usb.UsbManager; +import android.os.Bundle; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.UserManager; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; +import com.android.systemui.R; + +public class UsbDebuggingSecondaryUserActivity extends AlertActivity + implements DialogInterface.OnClickListener { + private UsbDisconnectedReceiver mDisconnectedReceiver; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + if (SystemProperties.getInt("service.adb.tcp.port", 0) == 0) { + mDisconnectedReceiver = new UsbDisconnectedReceiver(this); + } + + final AlertController.AlertParams ap = mAlertParams; + ap.mTitle = getString(R.string.usb_debugging_secondary_user_title); + UserInfo user = UserManager.get(this).getUserInfo(UserHandle.USER_OWNER); + ap.mMessage = getString(R.string.usb_debugging_secondary_user_message, user.name); + ap.mPositiveButtonText = getString(android.R.string.ok); + ap.mPositiveButtonListener = this; + + setupAlert(); + } + + private class UsbDisconnectedReceiver extends BroadcastReceiver { + private final Activity mActivity; + public UsbDisconnectedReceiver(Activity activity) { + mActivity = activity; + } + + @Override + public void onReceive(Context content, Intent intent) { + String action = intent.getAction(); + if (UsbManager.ACTION_USB_STATE.equals(action)) { + boolean connected = intent.getBooleanExtra(UsbManager.USB_CONNECTED, false); + if (!connected) { + mActivity.finish(); + } + } + } + } + + @Override + public void onStart() { + super.onStart(); + + IntentFilter filter = new IntentFilter(UsbManager.ACTION_USB_STATE); + registerReceiver(mDisconnectedReceiver, filter); + } + + @Override + protected void onStop() { + if (mDisconnectedReceiver != null) { + unregisterReceiver(mDisconnectedReceiver); + } + super.onStop(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + finish(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java index 9928f7f..ca32567 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java +++ b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java @@ -93,7 +93,8 @@ public class UsbResolverActivity extends ResolverActivity { } @Override - protected void onIntentSelected(ResolveInfo ri, Intent intent, boolean alwaysCheck) { + protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { + final ResolveInfo ri = target.getResolveInfo(); try { IBinder b = ServiceManager.getService(USB_SERVICE); IUsbManager service = IUsbManager.Stub.asInterface(b); @@ -121,12 +122,13 @@ public class UsbResolverActivity extends ResolverActivity { } try { - startActivityAsUser(intent, new UserHandle(userId)); + target.startAsUser(this, null, new UserHandle(userId)); } catch (ActivityNotFoundException e) { Log.e(TAG, "startActivity failed", e); } } catch (RemoteException e) { Log.e(TAG, "onIntentSelected failed", e); } + return true; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarPanel.java b/packages/SystemUI/src/com/android/systemui/volume/D.java index 272c321..db7c853 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarPanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/D.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,10 @@ * limitations under the License. */ -package com.android.systemui.statusbar; +package com.android.systemui.volume; -public interface StatusBarPanel { - public boolean isInContentArea(int x, int y); +import android.util.Log; + +class D { + public static boolean BUG = Log.isLoggable("volume", Log.DEBUG); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/Events.java b/packages/SystemUI/src/com/android/systemui/volume/Events.java new file mode 100644 index 0000000..893c939 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/Events.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.Context; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.provider.Settings.Global; +import android.util.Log; + +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.volume.VolumeDialogController.State; + +import java.util.Arrays; + +/** + * Interesting events related to the volume. + */ +public class Events { + private static final String TAG = Util.logTag(Events.class); + + public static final int EVENT_SHOW_DIALOG = 0; // (reason|int) (keyguard|bool) + public static final int EVENT_DISMISS_DIALOG = 1; // (reason|int) + public static final int EVENT_ACTIVE_STREAM_CHANGED = 2; // (stream|int) + public static final int EVENT_EXPAND = 3; // (expand|bool) + public static final int EVENT_KEY = 4; + public static final int EVENT_COLLECTION_STARTED = 5; + public static final int EVENT_COLLECTION_STOPPED = 6; + public static final int EVENT_ICON_CLICK = 7; // (stream|int) (icon_state|int) + public static final int EVENT_SETTINGS_CLICK = 8; + public static final int EVENT_TOUCH_LEVEL_CHANGED = 9; // (stream|int) (level|int) + public static final int EVENT_LEVEL_CHANGED = 10; // (stream|int) (level|int) + public static final int EVENT_INTERNAL_RINGER_MODE_CHANGED = 11; // (mode|int) + public static final int EVENT_EXTERNAL_RINGER_MODE_CHANGED = 12; // (mode|int) + public static final int EVENT_ZEN_MODE_CHANGED = 13; // (mode|int) + public static final int EVENT_SUPPRESSOR_CHANGED = 14; // (component|string) (name|string) + public static final int EVENT_MUTE_CHANGED = 15; // (stream|int) (muted|bool) + public static final int EVENT_TOUCH_LEVEL_DONE = 16; // (stream|int) (level|bool) + + private static final String[] EVENT_TAGS = { + "show_dialog", + "dismiss_dialog", + "active_stream_changed", + "expand", + "key", + "collection_started", + "collection_stopped", + "icon_click", + "settings_click", + "touch_level_changed", + "level_changed", + "internal_ringer_mode_changed", + "external_ringer_mode_changed", + "zen_mode_changed", + "suppressor_changed", + "mute_changed", + "touch_level_done", + }; + + public static final int DISMISS_REASON_UNKNOWN = 0; + public static final int DISMISS_REASON_TOUCH_OUTSIDE = 1; + public static final int DISMISS_REASON_VOLUME_CONTROLLER = 2; + public static final int DISMISS_REASON_TIMEOUT = 3; + public static final int DISMISS_REASON_SCREEN_OFF = 4; + public static final int DISMISS_REASON_SETTINGS_CLICKED = 5; + public static final int DISMISS_REASON_DONE_CLICKED = 6; + public static final String[] DISMISS_REASONS = { + "unknown", + "touch_outside", + "volume_controller", + "timeout", + "screen_off", + "settings_clicked", + "done_clicked", + }; + + public static final int SHOW_REASON_UNKNOWN = 0; + public static final int SHOW_REASON_VOLUME_CHANGED = 1; + public static final int SHOW_REASON_REMOTE_VOLUME_CHANGED = 2; + public static final String[] SHOW_REASONS = { + "unknown", + "volume_changed", + "remote_volume_changed" + }; + + public static final int ICON_STATE_UNKNOWN = 0; + public static final int ICON_STATE_UNMUTE = 1; + public static final int ICON_STATE_MUTE = 2; + public static final int ICON_STATE_VIBRATE = 3; + + public static Callback sCallback; + + public static void writeEvent(Context context, int tag, Object... list) { + final long time = System.currentTimeMillis(); + final StringBuilder sb = new StringBuilder("writeEvent ").append(EVENT_TAGS[tag]); + if (list != null && list.length > 0) { + sb.append(" "); + switch (tag) { + case EVENT_SHOW_DIALOG: + MetricsLogger.visible(context, MetricsLogger.VOLUME_DIALOG); + MetricsLogger.histogram(context, "volume_from_keyguard", + (Boolean) list[1] ? 1 : 0); + sb.append(SHOW_REASONS[(Integer) list[0]]).append(" keyguard=").append(list[1]); + break; + case EVENT_EXPAND: + MetricsLogger.visibility(context, MetricsLogger.VOLUME_DIALOG_DETAILS, + (Boolean) list[0]); + sb.append(list[0]); + break; + case EVENT_DISMISS_DIALOG: + MetricsLogger.hidden(context, MetricsLogger.VOLUME_DIALOG); + sb.append(DISMISS_REASONS[(Integer) list[0]]); + break; + case EVENT_ACTIVE_STREAM_CHANGED: + MetricsLogger.action(context, MetricsLogger.ACTION_VOLUME_STREAM, + (Integer) list[0]); + sb.append(AudioSystem.streamToString((Integer) list[0])); + break; + case EVENT_ICON_CLICK: + MetricsLogger.action(context, MetricsLogger.ACTION_VOLUME_ICON, + (Integer) list[1]); + sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ') + .append(iconStateToString((Integer) list[1])); + break; + case EVENT_TOUCH_LEVEL_DONE: + MetricsLogger.action(context, MetricsLogger.ACTION_VOLUME_SLIDER, + (Integer) list[1]); + // fall through + case EVENT_TOUCH_LEVEL_CHANGED: + case EVENT_LEVEL_CHANGED: + case EVENT_MUTE_CHANGED: + sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ') + .append(list[1]); + break; + case EVENT_KEY: + MetricsLogger.action(context, MetricsLogger.ACTION_VOLUME_KEY, + (Integer) list[1]); + sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ') + .append(list[1]); + break; + case EVENT_EXTERNAL_RINGER_MODE_CHANGED: + MetricsLogger.action(context, MetricsLogger.ACTION_RINGER_MODE, + (Integer) list[0]); + // fall through + case EVENT_INTERNAL_RINGER_MODE_CHANGED: + sb.append(ringerModeToString((Integer) list[0])); + break; + case EVENT_ZEN_MODE_CHANGED: + sb.append(zenModeToString((Integer) list[0])); + break; + case EVENT_SUPPRESSOR_CHANGED: + sb.append(list[0]).append(' ').append(list[1]); + break; + default: + sb.append(Arrays.asList(list)); + break; + } + } + Log.i(TAG, sb.toString()); + if (sCallback != null) { + sCallback.writeEvent(time, tag, list); + } + } + + public static void writeState(long time, State state) { + if (sCallback != null) { + sCallback.writeState(time, state); + } + } + + private static String iconStateToString(int iconState) { + switch (iconState) { + case ICON_STATE_UNMUTE: return "unmute"; + case ICON_STATE_MUTE: return "mute"; + case ICON_STATE_VIBRATE: return "vibrate"; + default: return "unknown_state_" + iconState; + } + } + + private static String ringerModeToString(int ringerMode) { + switch (ringerMode) { + case AudioManager.RINGER_MODE_SILENT: return "silent"; + case AudioManager.RINGER_MODE_VIBRATE: return "vibrate"; + case AudioManager.RINGER_MODE_NORMAL: return "normal"; + default: return "unknown"; + } + } + + private static String zenModeToString(int zenMode) { + switch (zenMode) { + case Global.ZEN_MODE_OFF: return "off"; + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: return "important_interruptions"; + case Global.ZEN_MODE_ALARMS: return "alarms"; + case Global.ZEN_MODE_NO_INTERRUPTIONS: return "no_interruptions"; + default: return "unknown"; + } + } + + public interface Callback { + void writeEvent(long time, int tag, Object[] list); + void writeState(long time, State state); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/MediaSessions.java b/packages/SystemUI/src/com/android/systemui/volume/MediaSessions.java new file mode 100644 index 0000000..712ea27 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/MediaSessions.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.media.IRemoteVolumeController; +import android.media.MediaMetadata; +import android.media.session.ISessionController; +import android.media.session.MediaController; +import android.media.session.MediaController.PlaybackInfo; +import android.media.session.MediaSession.QueueItem; +import android.media.session.MediaSession.Token; +import android.media.session.MediaSessionManager; +import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Convenience client for all media session updates. Provides a callback interface for events + * related to remote media sessions. + */ +public class MediaSessions { + private static final String TAG = Util.logTag(MediaSessions.class); + + private static final boolean USE_SERVICE_LABEL = false; + + private final Context mContext; + private final H mHandler; + private final MediaSessionManager mMgr; + private final Map<Token, MediaControllerRecord> mRecords = new HashMap<>(); + private final Callbacks mCallbacks; + + private boolean mInit; + + public MediaSessions(Context context, Looper looper, Callbacks callbacks) { + mContext = context; + mHandler = new H(looper); + mMgr = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); + mCallbacks = callbacks; + } + + public void dump(PrintWriter writer) { + writer.println(getClass().getSimpleName() + " state:"); + writer.print(" mInit: "); writer.println(mInit); + writer.print(" mRecords.size: "); writer.println(mRecords.size()); + int i = 0; + for (MediaControllerRecord r : mRecords.values()) { + dump(++i, writer, r.controller); + } + } + + public void init() { + if (D.BUG) Log.d(TAG, "init"); + // will throw if no permission + mMgr.addOnActiveSessionsChangedListener(mSessionsListener, null, mHandler); + mInit = true; + postUpdateSessions(); + mMgr.setRemoteVolumeController(mRvc); + } + + protected void postUpdateSessions() { + if (!mInit) return; + mHandler.sendEmptyMessage(H.UPDATE_SESSIONS); + } + + public void destroy() { + if (D.BUG) Log.d(TAG, "destroy"); + mInit = false; + mMgr.removeOnActiveSessionsChangedListener(mSessionsListener); + } + + public void setVolume(Token token, int level) { + final MediaControllerRecord r = mRecords.get(token); + if (r == null) { + Log.w(TAG, "setVolume: No record found for token " + token); + return; + } + if (D.BUG) Log.d(TAG, "Setting level to " + level); + r.controller.setVolumeTo(level, 0); + } + + private void onRemoteVolumeChangedH(ISessionController session, int flags) { + final MediaController controller = new MediaController(mContext, session); + if (D.BUG) Log.d(TAG, "remoteVolumeChangedH " + controller.getPackageName() + " " + + Util.audioManagerFlagsToString(flags)); + final Token token = controller.getSessionToken(); + mCallbacks.onRemoteVolumeChanged(token, flags); + } + + private void onUpdateRemoteControllerH(ISessionController session) { + final MediaController controller = session != null ? new MediaController(mContext, session) + : null; + final String pkg = controller != null ? controller.getPackageName() : null; + if (D.BUG) Log.d(TAG, "updateRemoteControllerH " + pkg); + // this may be our only indication that a remote session is changed, refresh + postUpdateSessions(); + } + + protected void onActiveSessionsUpdatedH(List<MediaController> controllers) { + if (D.BUG) Log.d(TAG, "onActiveSessionsUpdatedH n=" + controllers.size()); + final Set<Token> toRemove = new HashSet<Token>(mRecords.keySet()); + for (MediaController controller : controllers) { + final Token token = controller.getSessionToken(); + final PlaybackInfo pi = controller.getPlaybackInfo(); + toRemove.remove(token); + if (!mRecords.containsKey(token)) { + final MediaControllerRecord r = new MediaControllerRecord(controller); + r.name = getControllerName(controller); + mRecords.put(token, r); + controller.registerCallback(r, mHandler); + } + final MediaControllerRecord r = mRecords.get(token); + final boolean remote = isRemote(pi); + if (remote) { + updateRemoteH(token, r.name, pi); + r.sentRemote = true; + } + } + for (Token t : toRemove) { + final MediaControllerRecord r = mRecords.get(t); + r.controller.unregisterCallback(r); + mRecords.remove(t); + if (D.BUG) Log.d(TAG, "Removing " + r.name + " sentRemote=" + r.sentRemote); + if (r.sentRemote) { + mCallbacks.onRemoteRemoved(t); + r.sentRemote = false; + } + } + } + + private static boolean isRemote(PlaybackInfo pi) { + return pi != null && pi.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE; + } + + protected String getControllerName(MediaController controller) { + final PackageManager pm = mContext.getPackageManager(); + final String pkg = controller.getPackageName(); + try { + if (USE_SERVICE_LABEL) { + final List<ResolveInfo> ris = pm.queryIntentServices( + new Intent("android.media.MediaRouteProviderService").setPackage(pkg), 0); + if (ris != null) { + for (ResolveInfo ri : ris) { + if (ri.serviceInfo == null) continue; + if (pkg.equals(ri.serviceInfo.packageName)) { + final String serviceLabel = + Objects.toString(ri.serviceInfo.loadLabel(pm), "").trim(); + if (serviceLabel.length() > 0) { + return serviceLabel; + } + } + } + } + } + final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0); + final String appLabel = Objects.toString(ai.loadLabel(pm), "").trim(); + if (appLabel.length() > 0) { + return appLabel; + } + } catch (NameNotFoundException e) { } + return pkg; + } + + private void updateRemoteH(Token token, String name, PlaybackInfo pi) { + if (mCallbacks != null) { + mCallbacks.onRemoteUpdate(token, name, pi); + } + } + + private static void dump(int n, PrintWriter writer, MediaController c) { + writer.println(" Controller " + n + ": " + c.getPackageName()); + final Bundle extras = c.getExtras(); + final long flags = c.getFlags(); + final MediaMetadata mm = c.getMetadata(); + final PlaybackInfo pi = c.getPlaybackInfo(); + final PlaybackState playbackState = c.getPlaybackState(); + final List<QueueItem> queue = c.getQueue(); + final CharSequence queueTitle = c.getQueueTitle(); + final int ratingType = c.getRatingType(); + final PendingIntent sessionActivity = c.getSessionActivity(); + + writer.println(" PlaybackState: " + Util.playbackStateToString(playbackState)); + writer.println(" PlaybackInfo: " + Util.playbackInfoToString(pi)); + if (mm != null) { + writer.println(" MediaMetadata.desc=" + mm.getDescription()); + } + writer.println(" RatingType: " + ratingType); + writer.println(" Flags: " + flags); + if (extras != null) { + writer.println(" Extras:"); + for (String key : extras.keySet()) { + writer.println(" " + key + "=" + extras.get(key)); + } + } + if (queueTitle != null) { + writer.println(" QueueTitle: " + queueTitle); + } + if (queue != null && !queue.isEmpty()) { + writer.println(" Queue:"); + for (QueueItem qi : queue) { + writer.println(" " + qi); + } + } + if (pi != null) { + writer.println(" sessionActivity: " + sessionActivity); + } + } + + public static void dumpMediaSessions(Context context) { + final MediaSessionManager mgr = (MediaSessionManager) context + .getSystemService(Context.MEDIA_SESSION_SERVICE); + try { + final List<MediaController> controllers = mgr.getActiveSessions(null); + final int N = controllers.size(); + if (D.BUG) Log.d(TAG, N + " controllers"); + for (int i = 0; i < N; i++) { + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw, true); + dump(i + 1, pw, controllers.get(i)); + if (D.BUG) Log.d(TAG, sw.toString()); + } + } catch (SecurityException e) { + Log.w(TAG, "Not allowed to get sessions", e); + } + } + + private final class MediaControllerRecord extends MediaController.Callback { + private final MediaController controller; + + private boolean sentRemote; + private String name; + + private MediaControllerRecord(MediaController controller) { + this.controller = controller; + } + + private String cb(String method) { + return method + " " + controller.getPackageName() + " "; + } + + @Override + public void onAudioInfoChanged(PlaybackInfo info) { + if (D.BUG) Log.d(TAG, cb("onAudioInfoChanged") + Util.playbackInfoToString(info) + + " sentRemote=" + sentRemote); + final boolean remote = isRemote(info); + if (!remote && sentRemote) { + mCallbacks.onRemoteRemoved(controller.getSessionToken()); + sentRemote = false; + } else if (remote) { + updateRemoteH(controller.getSessionToken(), name, info); + sentRemote = true; + } + } + + @Override + public void onExtrasChanged(Bundle extras) { + if (D.BUG) Log.d(TAG, cb("onExtrasChanged") + extras); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + if (D.BUG) Log.d(TAG, cb("onMetadataChanged") + Util.mediaMetadataToString(metadata)); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (D.BUG) Log.d(TAG, cb("onPlaybackStateChanged") + Util.playbackStateToString(state)); + } + + @Override + public void onQueueChanged(List<QueueItem> queue) { + if (D.BUG) Log.d(TAG, cb("onQueueChanged") + queue); + } + + @Override + public void onQueueTitleChanged(CharSequence title) { + if (D.BUG) Log.d(TAG, cb("onQueueTitleChanged") + title); + } + + @Override + public void onSessionDestroyed() { + if (D.BUG) Log.d(TAG, cb("onSessionDestroyed")); + } + + @Override + public void onSessionEvent(String event, Bundle extras) { + if (D.BUG) Log.d(TAG, cb("onSessionEvent") + "event=" + event + " extras=" + extras); + } + } + + private final OnActiveSessionsChangedListener mSessionsListener = + new OnActiveSessionsChangedListener() { + @Override + public void onActiveSessionsChanged(List<MediaController> controllers) { + onActiveSessionsUpdatedH(controllers); + } + }; + + private final IRemoteVolumeController mRvc = new IRemoteVolumeController.Stub() { + @Override + public void remoteVolumeChanged(ISessionController session, int flags) + throws RemoteException { + mHandler.obtainMessage(H.REMOTE_VOLUME_CHANGED, flags, 0, session).sendToTarget(); + } + + @Override + public void updateRemoteController(final ISessionController session) + throws RemoteException { + mHandler.obtainMessage(H.UPDATE_REMOTE_CONTROLLER, session).sendToTarget(); + } + }; + + private final class H extends Handler { + private static final int UPDATE_SESSIONS = 1; + private static final int REMOTE_VOLUME_CHANGED = 2; + private static final int UPDATE_REMOTE_CONTROLLER = 3; + + private H(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_SESSIONS: + onActiveSessionsUpdatedH(mMgr.getActiveSessions(null)); + break; + case REMOTE_VOLUME_CHANGED: + onRemoteVolumeChangedH((ISessionController) msg.obj, msg.arg1); + break; + case UPDATE_REMOTE_CONTROLLER: + onUpdateRemoteControllerH((ISessionController) msg.obj); + break; + } + } + } + + public interface Callbacks { + void onRemoteUpdate(Token token, String name, PlaybackInfo pi); + void onRemoteRemoved(Token t); + void onRemoteVolumeChanged(Token token, int flags); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/SafetyWarningDialog.java b/packages/SystemUI/src/com/android/systemui/volume/SafetyWarningDialog.java new file mode 100644 index 0000000..04640a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/SafetyWarningDialog.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.util.Log; +import android.view.KeyEvent; +import android.view.WindowManager; + +import com.android.systemui.statusbar.phone.SystemUIDialog; + +abstract public class SafetyWarningDialog extends SystemUIDialog + implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener { + + private static final String TAG = Util.logTag(SafetyWarningDialog.class); + + private static final int KEY_CONFIRM_ALLOWED_AFTER = 1000; // milliseconds + + private final Context mContext; + private final AudioManager mAudioManager; + + private long mShowTime; + private boolean mNewVolumeUp; + + public SafetyWarningDialog(Context context, AudioManager audioManager) { + super(context); + mContext = context; + mAudioManager = audioManager; + + getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR); + setMessage(mContext.getString(com.android.internal.R.string.safe_media_volume_warning)); + setButton(DialogInterface.BUTTON_POSITIVE, + mContext.getString(com.android.internal.R.string.yes), this); + setButton(DialogInterface.BUTTON_NEGATIVE, + mContext.getString(com.android.internal.R.string.no), (OnClickListener) null); + setOnDismissListener(this); + + final IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + context.registerReceiver(mReceiver, filter); + } + + abstract protected void cleanUp(); + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && event.getRepeatCount() == 0) { + mNewVolumeUp = true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mNewVolumeUp + && (System.currentTimeMillis() - mShowTime) > KEY_CONFIRM_ALLOWED_AFTER) { + if (D.BUG) Log.d(TAG, "Confirmed warning via VOLUME_UP"); + mAudioManager.disableSafeMediaVolume(); + dismiss(); + } + return super.onKeyUp(keyCode, event); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + mAudioManager.disableSafeMediaVolume(); + } + + @Override + protected void onStart() { + super.onStart(); + mShowTime = System.currentTimeMillis(); + } + + @Override + public void onDismiss(DialogInterface unused) { + mContext.unregisterReceiver(mReceiver); + cleanUp(); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { + if (D.BUG) Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS"); + cancel(); + cleanUp(); + } + } + }; +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java b/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java index 2f02f7c..f432808 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java +++ b/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java @@ -17,6 +17,7 @@ package com.android.systemui.volume; import android.content.Context; +import android.graphics.Typeface; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -30,9 +31,12 @@ import java.util.Objects; public class SegmentedButtons extends LinearLayout { private static final int LABEL_RES_KEY = R.id.label; + private static final Typeface REGULAR = Typeface.create("sans-serif", Typeface.NORMAL); + private static final Typeface MEDIUM = Typeface.create("sans-serif-medium", Typeface.NORMAL); private final Context mContext; private final LayoutInflater mInflater; + private final SpTexts mSpTexts; private Callback mCallback; private Object mSelectedValue; @@ -42,6 +46,7 @@ public class SegmentedButtons extends LinearLayout { mContext = context; mInflater = LayoutInflater.from(mContext); setOrientation(HORIZONTAL); + mSpTexts = new SpTexts(mContext); } public void setCallback(Callback callback) { @@ -52,7 +57,7 @@ public class SegmentedButtons extends LinearLayout { return mSelectedValue; } - public void setSelectedValue(Object value) { + public void setSelectedValue(Object value, boolean fromClick) { if (Objects.equals(value, mSelectedValue)) return; mSelectedValue = value; for (int i = 0; i < getChildCount(); i++) { @@ -60,17 +65,16 @@ public class SegmentedButtons extends LinearLayout { final Object tag = c.getTag(); final boolean selected = Objects.equals(mSelectedValue, tag); c.setSelected(selected); - c.getCompoundDrawables()[1].setTint(mContext.getResources().getColor(selected - ? R.color.segmented_button_selected : R.color.segmented_button_unselected)); + c.setTypeface(selected ? MEDIUM : REGULAR); } - fireOnSelected(); + fireOnSelected(fromClick); } - public void addButton(int labelResId, int iconResId, Object value) { + public void addButton(int labelResId, int contentDescriptionResId, Object value) { final Button b = (Button) mInflater.inflate(R.layout.segmented_button, this, false); b.setTag(LABEL_RES_KEY, labelResId); b.setText(labelResId); - b.setCompoundDrawablesWithIntrinsicBounds(0, iconResId, 0, 0); + b.setContentDescription(getResources().getString(contentDescriptionResId)); final LayoutParams lp = (LayoutParams) b.getLayoutParams(); if (getChildCount() == 0) { lp.leftMargin = lp.rightMargin = 0; // first button has no margin @@ -85,6 +89,7 @@ public class SegmentedButtons extends LinearLayout { fireInteraction(); } }); + mSpTexts.add(b); } public void updateLocale() { @@ -95,9 +100,9 @@ public class SegmentedButtons extends LinearLayout { } } - private void fireOnSelected() { + private void fireOnSelected(boolean fromClick) { if (mCallback != null) { - mCallback.onSelected(mSelectedValue); + mCallback.onSelected(mSelectedValue, fromClick); } } @@ -110,11 +115,11 @@ public class SegmentedButtons extends LinearLayout { private final View.OnClickListener mClick = new View.OnClickListener() { @Override public void onClick(View v) { - setSelectedValue(v.getTag()); + setSelectedValue(v.getTag(), true /* fromClick */); } }; public interface Callback extends Interaction.Callback { - void onSelected(Object value); + void onSelected(Object value, boolean fromClick); } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/SpTexts.java b/packages/SystemUI/src/com/android/systemui/volume/SpTexts.java new file mode 100644 index 0000000..d8e53db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/SpTexts.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.Context; +import android.content.res.Resources; +import android.util.ArrayMap; +import android.util.TypedValue; +import android.view.View; +import android.view.View.OnAttachStateChangeListener; +import android.widget.TextView; + +/** + * Capture initial sp values for registered textviews, and update properly when configuration + * changes. + */ +public class SpTexts { + + private final Context mContext; + private final ArrayMap<TextView, Integer> mTexts = new ArrayMap<>(); + + public SpTexts(Context context) { + mContext = context; + } + + public int add(final TextView text) { + if (text == null) return 0; + final Resources res = mContext.getResources(); + final float fontScale = res.getConfiguration().fontScale; + final float density = res.getDisplayMetrics().density; + final float px = text.getTextSize(); + final int sp = (int)(px / fontScale / density); + mTexts.put(text, sp); + text.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { + @Override + public void onViewDetachedFromWindow(View v) { + } + + @Override + public void onViewAttachedToWindow(View v) { + setTextSizeH(text, sp); + } + }); + return sp; + } + + public void update() { + if (mTexts.isEmpty()) return; + mTexts.keyAt(0).post(mUpdateAll); + } + + private void setTextSizeH(TextView text, int sp) { + text.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp); + } + + private final Runnable mUpdateAll = new Runnable() { + @Override + public void run() { + for (int i = 0; i < mTexts.size(); i++) { + setTextSizeH(mTexts.keyAt(i), mTexts.valueAt(i)); + } + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/Util.java b/packages/SystemUI/src/com/android/systemui/volume/Util.java new file mode 100644 index 0000000..a46a44d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/Util.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.VolumeProvider; +import android.media.session.MediaController.PlaybackInfo; +import android.media.session.PlaybackState; +import android.telephony.TelephonyManager; +import android.view.View; +import android.widget.TextView; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Objects; + +/** + * Static helpers for the volume dialog. + */ +class Util { + + // Note: currently not shown (only used in the text footer) + private static final SimpleDateFormat HMMAA = new SimpleDateFormat("h:mm aa", Locale.US); + + private static int[] AUDIO_MANAGER_FLAGS = new int[] { + AudioManager.FLAG_SHOW_UI, + AudioManager.FLAG_VIBRATE, + AudioManager.FLAG_PLAY_SOUND, + AudioManager.FLAG_ALLOW_RINGER_MODES, + AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE, + AudioManager.FLAG_SHOW_VIBRATE_HINT, + AudioManager.FLAG_SHOW_SILENT_HINT, + AudioManager.FLAG_FROM_KEY, + AudioManager.FLAG_SHOW_UI_WARNINGS, + }; + + private static String[] AUDIO_MANAGER_FLAG_NAMES = new String[] { + "SHOW_UI", + "VIBRATE", + "PLAY_SOUND", + "ALLOW_RINGER_MODES", + "REMOVE_SOUND_AND_VIBRATE", + "SHOW_VIBRATE_HINT", + "SHOW_SILENT_HINT", + "FROM_KEY", + "SHOW_UI_WARNINGS", + }; + + public static String logTag(Class<?> c) { + final String tag = "vol." + c.getSimpleName(); + return tag.length() < 23 ? tag : tag.substring(0, 23); + } + + public static String ringerModeToString(int ringerMode) { + switch (ringerMode) { + case AudioManager.RINGER_MODE_SILENT: return "RINGER_MODE_SILENT"; + case AudioManager.RINGER_MODE_VIBRATE: return "RINGER_MODE_VIBRATE"; + case AudioManager.RINGER_MODE_NORMAL: return "RINGER_MODE_NORMAL"; + default: return "RINGER_MODE_UNKNOWN_" + ringerMode; + } + } + + public static String mediaMetadataToString(MediaMetadata metadata) { + return metadata.getDescription().toString(); + } + + public static String playbackInfoToString(PlaybackInfo info) { + if (info == null) return null; + final String type = playbackInfoTypeToString(info.getPlaybackType()); + final String vc = volumeProviderControlToString(info.getVolumeControl()); + return String.format("PlaybackInfo[vol=%s,max=%s,type=%s,vc=%s],atts=%s", + info.getCurrentVolume(), info.getMaxVolume(), type, vc, info.getAudioAttributes()); + } + + public static String playbackInfoTypeToString(int type) { + switch (type) { + case PlaybackInfo.PLAYBACK_TYPE_LOCAL: return "LOCAL"; + case PlaybackInfo.PLAYBACK_TYPE_REMOTE: return "REMOTE"; + default: return "UNKNOWN_" + type; + } + } + + public static String playbackStateStateToString(int state) { + switch (state) { + case PlaybackState.STATE_NONE: return "STATE_NONE"; + case PlaybackState.STATE_STOPPED: return "STATE_STOPPED"; + case PlaybackState.STATE_PAUSED: return "STATE_PAUSED"; + case PlaybackState.STATE_PLAYING: return "STATE_PLAYING"; + default: return "UNKNOWN_" + state; + } + } + + public static String volumeProviderControlToString(int control) { + switch (control) { + case VolumeProvider.VOLUME_CONTROL_ABSOLUTE: return "VOLUME_CONTROL_ABSOLUTE"; + case VolumeProvider.VOLUME_CONTROL_FIXED: return "VOLUME_CONTROL_FIXED"; + case VolumeProvider.VOLUME_CONTROL_RELATIVE: return "VOLUME_CONTROL_RELATIVE"; + default: return "VOLUME_CONTROL_UNKNOWN_" + control; + } + } + + public static String playbackStateToString(PlaybackState playbackState) { + if (playbackState == null) return null; + return playbackStateStateToString(playbackState.getState()) + " " + playbackState; + } + + public static String audioManagerFlagsToString(int value) { + return bitFieldToString(value, AUDIO_MANAGER_FLAGS, AUDIO_MANAGER_FLAG_NAMES); + } + + private static String bitFieldToString(int value, int[] values, String[] names) { + if (value == 0) return ""; + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + if ((value & values[i]) != 0) { + if (sb.length() > 0) sb.append(','); + sb.append(names[i]); + } + value &= ~values[i]; + } + if (value != 0) { + if (sb.length() > 0) sb.append(','); + sb.append("UNKNOWN_").append(value); + } + return sb.toString(); + } + + public static String getShortTime(long millis) { + return HMMAA.format(new Date(millis)); + } + + private static CharSequence emptyToNull(CharSequence str) { + return str == null || str.length() == 0 ? null : str; + } + + public static boolean setText(TextView tv, CharSequence text) { + if (Objects.equals(emptyToNull(tv.getText()), emptyToNull(text))) return false; + tv.setText(text); + return true; + } + + public static final void setVisOrGone(View v, boolean vis) { + if (v == null || (v.getVisibility() == View.VISIBLE) == vis) return; + v.setVisibility(vis ? View.VISIBLE : View.GONE); + } + + public static final void setVisOrInvis(View v, boolean vis) { + if (v == null || (v.getVisibility() == View.VISIBLE) == vis) return; + v.setVisibility(vis ? View.VISIBLE : View.INVISIBLE); + } + + public static boolean isVoiceCapable(Context context) { + final TelephonyManager telephony = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + return telephony != null && telephony.isVoiceCapable(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java index e3f8f3d..1f0ee57 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java @@ -16,10 +16,18 @@ package com.android.systemui.volume; +import android.content.res.Configuration; + import com.android.systemui.DemoMode; import com.android.systemui.statusbar.policy.ZenModeController; +import java.io.FileDescriptor; +import java.io.PrintWriter; + public interface VolumeComponent extends DemoMode { ZenModeController getZenController(); void dismissNow(); + void onConfigurationChanged(Configuration newConfig); + void dump(FileDescriptor fd, PrintWriter pw, String[] args); + void register(); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java new file mode 100644 index 0000000..3964820 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java @@ -0,0 +1,1129 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK; +import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.animation.LayoutTransition; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.os.Debug; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.provider.Settings.Global; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.View.OnAttachStateChangeListener; +import android.view.View.OnClickListener; +import android.view.View.OnLayoutChangeListener; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.Window; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; + +import com.android.systemui.R; +import com.android.systemui.statusbar.policy.ZenModeController; +import com.android.systemui.volume.VolumeDialogController.State; +import com.android.systemui.volume.VolumeDialogController.StreamState; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Visual presentation of the volume dialog. + * + * A client of VolumeDialogController and its state model. + * + * Methods ending in "H" must be called on the (ui) handler. + */ +public class VolumeDialog { + private static final String TAG = Util.logTag(VolumeDialog.class); + + private static final long USER_ATTEMPT_GRACE_PERIOD = 1000; + private static final int WAIT_FOR_RIPPLE = 200; + private static final int UPDATE_ANIMATION_DURATION = 80; + + private final Context mContext; + private final H mHandler = new H(); + private final VolumeDialogController mController; + + private final CustomDialog mDialog; + private final ViewGroup mDialogView; + private final ViewGroup mDialogContentView; + private final ImageButton mExpandButton; + private final View mSettingsButton; + private final List<VolumeRow> mRows = new ArrayList<VolumeRow>(); + private final SpTexts mSpTexts; + private final SparseBooleanArray mDynamic = new SparseBooleanArray(); + private final KeyguardManager mKeyguard; + private final int mExpandButtonAnimationDuration; + private final ZenFooter mZenFooter; + private final LayoutTransition mLayoutTransition; + private final Object mSafetyWarningLock = new Object(); + private final Accessibility mAccessibility = new Accessibility(); + private final ColorStateList mActiveSliderTint; + private final ColorStateList mInactiveSliderTint; + private final VolumeDialogMotion mMotion; + + private boolean mShowing; + private boolean mExpanded; + private int mActiveStream; + private boolean mShowHeaders = VolumePrefs.DEFAULT_SHOW_HEADERS; + private boolean mAutomute = VolumePrefs.DEFAULT_ENABLE_AUTOMUTE; + private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE; + private State mState; + private int mExpandButtonRes; + private boolean mExpandButtonAnimationRunning; + private SafetyWarningDialog mSafetyWarning; + private Callback mCallback; + private boolean mPendingStateChanged; + private boolean mPendingRecheckAll; + private long mCollapseTime; + + public VolumeDialog(Context context, int windowType, VolumeDialogController controller, + ZenModeController zenModeController, Callback callback) { + mContext = context; + mController = controller; + mCallback = callback; + mSpTexts = new SpTexts(mContext); + mKeyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + + mDialog = new CustomDialog(mContext); + + final Window window = mDialog.getWindow(); + window.requestFeature(Window.FEATURE_NO_TITLE); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); + mDialog.setCanceledOnTouchOutside(true); + final Resources res = mContext.getResources(); + final WindowManager.LayoutParams lp = window.getAttributes(); + lp.type = windowType; + lp.format = PixelFormat.TRANSLUCENT; + lp.setTitle(VolumeDialog.class.getSimpleName()); + lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + lp.y = res.getDimensionPixelSize(R.dimen.volume_offset_top); + lp.gravity = Gravity.TOP; + window.setAttributes(lp); + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); + + mActiveSliderTint = loadColorStateList(R.color.system_accent_color); + mInactiveSliderTint = loadColorStateList(R.color.volume_slider_inactive); + mDialog.setContentView(R.layout.volume_dialog); + mDialogView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog); + mDialogContentView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog_content); + mExpandButton = (ImageButton) mDialogView.findViewById(R.id.volume_expand_button); + mExpandButton.setOnClickListener(mClickExpand); + updateWindowWidthH(); + updateExpandButtonH(); + mLayoutTransition = new LayoutTransition(); + mLayoutTransition.setDuration(new ValueAnimator().getDuration() / 2); + mDialogContentView.setLayoutTransition(mLayoutTransition); + mMotion = new VolumeDialogMotion(mDialog, mDialogView, mDialogContentView, mExpandButton, + new VolumeDialogMotion.Callback() { + @Override + public void onAnimatingChanged(boolean animating) { + if (animating) return; + if (mPendingStateChanged) { + mHandler.sendEmptyMessage(H.STATE_CHANGED); + mPendingStateChanged = false; + } + if (mPendingRecheckAll) { + mHandler.sendEmptyMessage(H.RECHECK_ALL); + mPendingRecheckAll = false; + } + } + }); + + addRow(AudioManager.STREAM_RING, + R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true); + addRow(AudioManager.STREAM_MUSIC, + R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true); + addRow(AudioManager.STREAM_ALARM, + R.drawable.ic_volume_alarm, R.drawable.ic_volume_alarm_mute, false); + addRow(AudioManager.STREAM_VOICE_CALL, + R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, false); + addRow(AudioManager.STREAM_BLUETOOTH_SCO, + R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false); + addRow(AudioManager.STREAM_SYSTEM, + R.drawable.ic_volume_system, R.drawable.ic_volume_system_mute, false); + + mSettingsButton = mDialog.findViewById(R.id.volume_settings_button); + mSettingsButton.setOnClickListener(mClickSettings); + mExpandButtonAnimationDuration = res.getInteger(R.integer.volume_expand_animation_duration); + mZenFooter = (ZenFooter) mDialog.findViewById(R.id.volume_zen_footer); + mZenFooter.init(zenModeController); + + mAccessibility.init(); + + controller.addCallback(mControllerCallbackH, mHandler); + controller.getState(); + } + + private ColorStateList loadColorStateList(int colorResId) { + return ColorStateList.valueOf(mContext.getColor(colorResId)); + } + + private void updateWindowWidthH() { + final ViewGroup.LayoutParams lp = mDialogView.getLayoutParams(); + final DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); + if (D.BUG) Log.d(TAG, "updateWindowWidth dm.w=" + dm.widthPixels); + int w = dm.widthPixels; + final int max = mContext.getResources() + .getDimensionPixelSize(R.dimen.standard_notification_panel_width); + if (w > max) { + w = max; + } + w -= mContext.getResources().getDimensionPixelSize(R.dimen.notification_side_padding) * 2; + lp.width = w; + mDialogView.setLayoutParams(lp); + } + + public void setStreamImportant(int stream, boolean important) { + mHandler.obtainMessage(H.SET_STREAM_IMPORTANT, stream, important ? 1 : 0).sendToTarget(); + } + + public void setShowHeaders(boolean showHeaders) { + if (showHeaders == mShowHeaders) return; + mShowHeaders = showHeaders; + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + public void setAutomute(boolean automute) { + if (mAutomute == automute) return; + mAutomute = automute; + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + public void setSilentMode(boolean silentMode) { + if (mSilentMode == silentMode) return; + mSilentMode = silentMode; + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + private void addRow(int stream, int iconRes, int iconMuteRes, boolean important) { + final VolumeRow row = initRow(stream, iconRes, iconMuteRes, important); + if (!mRows.isEmpty()) { + final View v = new View(mContext); + v.setId(android.R.id.background); + final int h = mContext.getResources() + .getDimensionPixelSize(R.dimen.volume_slider_interspacing); + final LinearLayout.LayoutParams lp = + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, h); + mDialogContentView.addView(v, mDialogContentView.getChildCount() - 1, lp); + row.space = v; + } + row.settingsButton.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) { + final boolean moved = oldLeft != left || oldTop != top; + if (D.BUG) Log.d(TAG, "onLayoutChange moved=" + moved + + " old=" + new Rect(oldLeft, oldTop, oldRight, oldBottom).toShortString() + + " new=" + new Rect(left,top,right,bottom).toShortString()); + if (moved) { + for (int i = 0; i < mDialogContentView.getChildCount(); i++) { + final View c = mDialogContentView.getChildAt(i); + if (!c.isShown()) continue; + if (c == row.view) { + repositionExpandAnim(row); + } + return; + } + } + } + }); + // add new row just before the footer + mDialogContentView.addView(row.view, mDialogContentView.getChildCount() - 1); + mRows.add(row); + } + + private boolean isAttached() { + return mDialogContentView != null && mDialogContentView.isAttachedToWindow(); + } + + private VolumeRow getActiveRow() { + for (VolumeRow row : mRows) { + if (row.stream == mActiveStream) { + return row; + } + } + return mRows.get(0); + } + + private VolumeRow findRow(int stream) { + for (VolumeRow row : mRows) { + if (row.stream == stream) return row; + } + return null; + } + + private void repositionExpandAnim(VolumeRow row) { + final int[] loc = new int[2]; + row.settingsButton.getLocationInWindow(loc); + final MarginLayoutParams mlp = (MarginLayoutParams) mDialogView.getLayoutParams(); + final int x = loc[0] - mlp.leftMargin; + final int y = loc[1] - mlp.topMargin; + if (D.BUG) Log.d(TAG, "repositionExpandAnim x=" + x + " y=" + y); + mExpandButton.setTranslationX(x); + mExpandButton.setTranslationY(y); + mExpandButton.setTag((Integer) y); + } + + public void dump(PrintWriter writer) { + writer.println(VolumeDialog.class.getSimpleName() + " state:"); + writer.print(" mShowing: "); writer.println(mShowing); + writer.print(" mExpanded: "); writer.println(mExpanded); + writer.print(" mExpandButtonAnimationRunning: "); + writer.println(mExpandButtonAnimationRunning); + writer.print(" mActiveStream: "); writer.println(mActiveStream); + writer.print(" mDynamic: "); writer.println(mDynamic); + writer.print(" mShowHeaders: "); writer.println(mShowHeaders); + writer.print(" mAutomute: "); writer.println(mAutomute); + writer.print(" mSilentMode: "); writer.println(mSilentMode); + writer.print(" mCollapseTime: "); writer.println(mCollapseTime); + writer.print(" mAccessibility.mFeedbackEnabled: "); + writer.println(mAccessibility.mFeedbackEnabled); + } + + private static int getImpliedLevel(SeekBar seekBar, int progress) { + final int m = seekBar.getMax(); + final int n = m / 100 - 1; + final int level = progress == 0 ? 0 + : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n)); + return level; + } + + @SuppressLint("InflateParams") + private VolumeRow initRow(final int stream, int iconRes, int iconMuteRes, boolean important) { + final VolumeRow row = new VolumeRow(); + row.stream = stream; + row.iconRes = iconRes; + row.iconMuteRes = iconMuteRes; + row.important = important; + row.view = mDialog.getLayoutInflater().inflate(R.layout.volume_dialog_row, null); + row.view.setTag(row); + row.header = (TextView) row.view.findViewById(R.id.volume_row_header); + mSpTexts.add(row.header); + row.slider = (SeekBar) row.view.findViewById(R.id.volume_row_slider); + row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); + + // forward events above the slider into the slider + row.view.setOnTouchListener(new OnTouchListener() { + private final Rect mSliderHitRect = new Rect(); + private boolean mDragging; + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + row.slider.getHitRect(mSliderHitRect); + if (!mDragging && event.getActionMasked() == MotionEvent.ACTION_DOWN + && event.getY() < mSliderHitRect.top) { + mDragging = true; + } + if (mDragging) { + event.offsetLocation(-mSliderHitRect.left, -mSliderHitRect.top); + row.slider.dispatchTouchEvent(event); + if (event.getActionMasked() == MotionEvent.ACTION_UP + || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { + mDragging = false; + } + return true; + } + return false; + } + }); + row.icon = (ImageButton) row.view.findViewById(R.id.volume_row_icon); + row.icon.setImageResource(iconRes); + row.icon.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, row.stream, row.iconState); + mController.setActiveStream(row.stream); + if (row.stream == AudioManager.STREAM_RING) { + final boolean hasVibrator = mController.hasVibrator(); + if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) { + if (hasVibrator) { + mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false); + } else { + final boolean wasZero = row.ss.level == 0; + mController.setStreamVolume(stream, wasZero ? row.lastAudibleLevel : 0); + } + } else { + mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); + if (row.ss.level == 0) { + mController.setStreamVolume(stream, 1); + } + } + } else { + final boolean vmute = row.ss.level == 0; + mController.setStreamVolume(stream, vmute ? row.lastAudibleLevel : 0); + } + row.userAttempt = 0; // reset the grace period, slider should update immediately + } + }); + row.settingsButton = (ImageButton) row.view.findViewById(R.id.volume_settings_button); + row.settingsButton.setOnClickListener(mClickSettings); + return row; + } + + public void destroy() { + mController.removeCallback(mControllerCallbackH); + } + + public void show(int reason) { + mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget(); + } + + public void dismiss(int reason) { + mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget(); + } + + private void showH(int reason) { + if (D.BUG) Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]); + mHandler.removeMessages(H.SHOW); + mHandler.removeMessages(H.DISMISS); + rescheduleTimeoutH(); + if (mShowing) return; + mShowing = true; + mMotion.startShow(); + Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked()); + mController.notifyVisible(true); + } + + protected void rescheduleTimeoutH() { + mHandler.removeMessages(H.DISMISS); + final int timeout = computeTimeoutH(); + mHandler.sendMessageDelayed(mHandler + .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout); + if (D.BUG) Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller()); + mController.userActivity(); + } + + private int computeTimeoutH() { + if (mAccessibility.mFeedbackEnabled) return 20000; + if (mSafetyWarning != null) return 5000; + if (mExpanded || mExpandButtonAnimationRunning) return 5000; + if (mActiveStream == AudioManager.STREAM_MUSIC) return 1500; + return 3000; + } + + protected void dismissH(int reason) { + mHandler.removeMessages(H.DISMISS); + mHandler.removeMessages(H.SHOW); + if (!mShowing) return; + mShowing = false; + mMotion.startDismiss(new Runnable() { + @Override + public void run() { + setExpandedH(false); + } + }); + Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason); + mController.notifyVisible(false); + synchronized (mSafetyWarningLock) { + if (mSafetyWarning != null) { + if (D.BUG) Log.d(TAG, "SafetyWarning dismissed"); + mSafetyWarning.dismiss(); + } + } + } + + private void updateDialogBottomMarginH() { + final long diff = System.currentTimeMillis() - mCollapseTime; + final boolean collapsing = mCollapseTime != 0 && diff < getConservativeCollapseDuration(); + final ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) mDialogView.getLayoutParams(); + final int bottomMargin = collapsing ? mDialogContentView.getHeight() : + mContext.getResources().getDimensionPixelSize(R.dimen.volume_dialog_margin_bottom); + if (bottomMargin != mlp.bottomMargin) { + if (D.BUG) Log.d(TAG, "bottomMargin " + mlp.bottomMargin + " -> " + bottomMargin); + mlp.bottomMargin = bottomMargin; + mDialogView.setLayoutParams(mlp); + } + } + + private long getConservativeCollapseDuration() { + return mExpandButtonAnimationDuration * 3; + } + + private void prepareForCollapse() { + mHandler.removeMessages(H.UPDATE_BOTTOM_MARGIN); + mCollapseTime = System.currentTimeMillis(); + updateDialogBottomMarginH(); + mHandler.sendEmptyMessageDelayed(H.UPDATE_BOTTOM_MARGIN, getConservativeCollapseDuration()); + } + + private void setExpandedH(boolean expanded) { + if (mExpanded == expanded) return; + mExpanded = expanded; + mExpandButtonAnimationRunning = isAttached(); + if (D.BUG) Log.d(TAG, "setExpandedH " + expanded); + if (!mExpanded && mExpandButtonAnimationRunning) { + prepareForCollapse(); + } + updateRowsH(); + if (mExpandButtonAnimationRunning) { + final Drawable d = mExpandButton.getDrawable(); + if (d instanceof AnimatedVectorDrawable) { + // workaround to reset drawable + final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) d.getConstantState() + .newDrawable(); + mExpandButton.setImageDrawable(avd); + avd.start(); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mExpandButtonAnimationRunning = false; + updateExpandButtonH(); + rescheduleTimeoutH(); + } + }, mExpandButtonAnimationDuration); + } + } + rescheduleTimeoutH(); + } + + private void updateExpandButtonH() { + if (D.BUG) Log.d(TAG, "updateExpandButtonH"); + mExpandButton.setClickable(!mExpandButtonAnimationRunning); + if (mExpandButtonAnimationRunning && isAttached()) return; + final int res = mExpanded ? R.drawable.ic_volume_collapse_animation + : R.drawable.ic_volume_expand_animation; + if (res == mExpandButtonRes) return; + mExpandButtonRes = res; + mExpandButton.setImageResource(res); + mExpandButton.setContentDescription(mContext.getString(mExpanded ? + R.string.accessibility_volume_collapse : R.string.accessibility_volume_expand)); + } + + private boolean isVisibleH(VolumeRow row, boolean isActive) { + return mExpanded && row.view.getVisibility() == View.VISIBLE + || (mExpanded && (row.important || isActive)) + || !mExpanded && isActive; + } + + private void updateRowsH() { + if (D.BUG) Log.d(TAG, "updateRowsH"); + final VolumeRow activeRow = getActiveRow(); + updateFooterH(); + updateExpandButtonH(); + if (!mShowing) { + trimObsoleteH(); + } + // apply changes to all rows + for (VolumeRow row : mRows) { + final boolean isActive = row == activeRow; + final boolean visible = isVisibleH(row, isActive); + Util.setVisOrGone(row.view, visible); + Util.setVisOrGone(row.space, visible && mExpanded); + final int expandButtonRes = mExpanded ? R.drawable.ic_volume_settings : 0; + if (expandButtonRes != row.cachedExpandButtonRes) { + row.cachedExpandButtonRes = expandButtonRes; + if (expandButtonRes == 0) { + row.settingsButton.setImageDrawable(null); + } else { + row.settingsButton.setImageResource(expandButtonRes); + } + } + Util.setVisOrInvis(row.settingsButton, false); + updateVolumeRowHeaderVisibleH(row); + row.header.setAlpha(mExpanded && isActive ? 1 : 0.5f); + updateVolumeRowSliderTintH(row, isActive); + } + } + + private void trimObsoleteH() { + if (D.BUG) Log.d(TAG, "trimObsoleteH"); + for (int i = mRows.size() -1; i >= 0; i--) { + final VolumeRow row = mRows.get(i); + if (row.ss == null || !row.ss.dynamic) continue; + if (!mDynamic.get(row.stream)) { + mRows.remove(i); + mDialogContentView.removeView(row.view); + mDialogContentView.removeView(row.space); + } + } + } + + private void onStateChangedH(State state) { + final boolean animating = mMotion.isAnimating(); + if (D.BUG) Log.d(TAG, "onStateChangedH animating=" + animating); + mState = state; + if (animating) { + mPendingStateChanged = true; + return; + } + mDynamic.clear(); + // add any new dynamic rows + for (int i = 0; i < state.states.size(); i++) { + final int stream = state.states.keyAt(i); + final StreamState ss = state.states.valueAt(i); + if (!ss.dynamic) continue; + mDynamic.put(stream, true); + if (findRow(stream) == null) { + addRow(stream, R.drawable.ic_volume_remote, R.drawable.ic_volume_remote_mute, true); + } + } + + if (mActiveStream != state.activeStream) { + mActiveStream = state.activeStream; + updateRowsH(); + rescheduleTimeoutH(); + } + for (VolumeRow row : mRows) { + updateVolumeRowH(row); + } + updateFooterH(); + } + + private void updateFooterH() { + if (D.BUG) Log.d(TAG, "updateFooterH"); + final boolean wasVisible = mZenFooter.getVisibility() == View.VISIBLE; + final boolean visible = mState.zenMode != Global.ZEN_MODE_OFF; + if (wasVisible != visible && !visible) { + prepareForCollapse(); + } + Util.setVisOrGone(mZenFooter, visible); + mZenFooter.update(); + } + + private void updateVolumeRowH(VolumeRow row) { + if (D.BUG) Log.d(TAG, "updateVolumeRowH s=" + row.stream); + if (mState == null) return; + final StreamState ss = mState.states.get(row.stream); + if (ss == null) return; + row.ss = ss; + if (ss.level > 0) { + row.lastAudibleLevel = ss.level; + } + if (ss.level == row.requestedLevel) { + row.requestedLevel = -1; + } + final boolean isRingStream = row.stream == AudioManager.STREAM_RING; + final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM; + final boolean isAlarmStream = row.stream == AudioManager.STREAM_ALARM; + final boolean isMusicStream = row.stream == AudioManager.STREAM_MUSIC; + final boolean isRingVibrate = isRingStream + && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; + final boolean isRingSilent = isRingStream + && mState.ringerModeInternal == AudioManager.RINGER_MODE_SILENT; + final boolean isZenAlarms = mState.zenMode == Global.ZEN_MODE_ALARMS; + final boolean isZenNone = mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS; + final boolean isZenPriority = mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + final boolean isRingZenNone = (isRingStream || isSystemStream) && isZenNone; + final boolean isRingLimited = isRingStream && isZenPriority; + final boolean zenMuted = isZenAlarms ? (isRingStream || isSystemStream) + : isZenNone ? (isRingStream || isSystemStream || isAlarmStream || isMusicStream) + : false; + + // update slider max + final int max = ss.levelMax * 100; + if (max != row.slider.getMax()) { + row.slider.setMax(max); + } + + // update header visible + updateVolumeRowHeaderVisibleH(row); + + // update header text + String text = ss.name; + if (mShowHeaders) { + if (isRingZenNone) { + text = mContext.getString(R.string.volume_stream_muted_dnd, ss.name); + } else if (isRingVibrate && isRingLimited) { + text = mContext.getString(R.string.volume_stream_vibrate_dnd, ss.name); + } else if (isRingVibrate) { + text = mContext.getString(R.string.volume_stream_vibrate, ss.name); + } else if (ss.muted || mAutomute && ss.level == 0) { + text = mContext.getString(R.string.volume_stream_muted, ss.name); + } else if (isRingLimited) { + text = mContext.getString(R.string.volume_stream_limited_dnd, ss.name); + } + } + Util.setText(row.header, text); + + // update icon + final boolean iconEnabled = (mAutomute || ss.muteSupported) && !zenMuted; + row.icon.setEnabled(iconEnabled); + row.icon.setAlpha(iconEnabled ? 1 : 0.5f); + final int iconRes = + isRingVibrate ? R.drawable.ic_volume_ringer_vibrate + : isRingSilent || zenMuted ? row.cachedIconRes + : ss.routedToBluetooth ? + (ss.muted ? R.drawable.ic_volume_media_bt_mute + : R.drawable.ic_volume_media_bt) + : mAutomute && ss.level == 0 ? row.iconMuteRes + : (ss.muted ? row.iconMuteRes : row.iconRes); + if (iconRes != row.cachedIconRes) { + if (row.cachedIconRes != 0 && isRingVibrate) { + mController.vibrate(); + } + row.cachedIconRes = iconRes; + row.icon.setImageResource(iconRes); + } + row.iconState = + iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE + : (iconRes == R.drawable.ic_volume_media_bt_mute || iconRes == row.iconMuteRes) + ? Events.ICON_STATE_MUTE + : (iconRes == R.drawable.ic_volume_media_bt || iconRes == row.iconRes) + ? Events.ICON_STATE_UNMUTE + : Events.ICON_STATE_UNKNOWN; + row.icon.setContentDescription(ss.name); + + // update slider + final boolean enableSlider = !zenMuted; + final int vlevel = row.ss.muted && (isRingVibrate || !isRingStream && !zenMuted) ? 0 + : row.ss.level; + updateVolumeRowSliderH(row, enableSlider, vlevel); + } + + private void updateVolumeRowHeaderVisibleH(VolumeRow row) { + final boolean dynamic = row.ss != null && row.ss.dynamic; + final boolean showHeaders = mShowHeaders || mExpanded && dynamic; + if (row.cachedShowHeaders != showHeaders) { + row.cachedShowHeaders = showHeaders; + Util.setVisOrGone(row.header, showHeaders); + } + } + + private void updateVolumeRowSliderTintH(VolumeRow row, boolean isActive) { + if (isActive && mExpanded) { + row.slider.requestFocus(); + } + final ColorStateList tint = isActive && row.slider.isEnabled() ? mActiveSliderTint + : mInactiveSliderTint; + if (tint == row.cachedSliderTint) return; + row.cachedSliderTint = tint; + row.slider.setProgressTintList(tint); + row.slider.setThumbTintList(tint); + } + + private void updateVolumeRowSliderH(VolumeRow row, boolean enable, int vlevel) { + row.slider.setEnabled(enable); + updateVolumeRowSliderTintH(row, row.stream == mActiveStream); + if (row.tracking) { + return; // don't update if user is sliding + } + final int progress = row.slider.getProgress(); + final int level = getImpliedLevel(row.slider, progress); + final boolean rowVisible = row.view.getVisibility() == View.VISIBLE; + final boolean inGracePeriod = (SystemClock.uptimeMillis() - row.userAttempt) + < USER_ATTEMPT_GRACE_PERIOD; + mHandler.removeMessages(H.RECHECK, row); + if (mShowing && rowVisible && inGracePeriod) { + if (D.BUG) Log.d(TAG, "inGracePeriod"); + mHandler.sendMessageAtTime(mHandler.obtainMessage(H.RECHECK, row), + row.userAttempt + USER_ATTEMPT_GRACE_PERIOD); + return; // don't update if visible and in grace period + } + if (vlevel == level) { + if (mShowing && rowVisible) { + return; // don't clamp if visible + } + } + final int newProgress = vlevel * 100; + if (progress != newProgress) { + if (mShowing && rowVisible) { + // animate! + if (row.anim != null && row.anim.isRunning() + && row.animTargetProgress == newProgress) { + return; // already animating to the target progress + } + // start/update animation + if (row.anim == null) { + row.anim = ObjectAnimator.ofInt(row.slider, "progress", progress, newProgress); + row.anim.setInterpolator(new DecelerateInterpolator()); + } else { + row.anim.cancel(); + row.anim.setIntValues(progress, newProgress); + } + row.animTargetProgress = newProgress; + row.anim.setDuration(UPDATE_ANIMATION_DURATION); + row.anim.start(); + } else { + // update slider directly to clamped value + if (row.anim != null) { + row.anim.cancel(); + } + row.slider.setProgress(newProgress); + } + } + } + + private void recheckH(VolumeRow row) { + if (row == null) { + if (D.BUG) Log.d(TAG, "recheckH ALL"); + trimObsoleteH(); + for (VolumeRow r : mRows) { + updateVolumeRowH(r); + } + } else { + if (D.BUG) Log.d(TAG, "recheckH " + row.stream); + updateVolumeRowH(row); + } + } + + private void setStreamImportantH(int stream, boolean important) { + for (VolumeRow row : mRows) { + if (row.stream == stream) { + row.important = important; + return; + } + } + } + + private void showSafetyWarningH(int flags) { + if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0 + || mShowing) { + synchronized (mSafetyWarningLock) { + if (mSafetyWarning != null) { + return; + } + mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) { + @Override + protected void cleanUp() { + synchronized (mSafetyWarningLock) { + mSafetyWarning = null; + } + recheckH(null); + } + }; + mSafetyWarning.show(); + } + recheckH(null); + } + rescheduleTimeoutH(); + } + + private final VolumeDialogController.Callbacks mControllerCallbackH + = new VolumeDialogController.Callbacks() { + @Override + public void onShowRequested(int reason) { + showH(reason); + } + + @Override + public void onDismissRequested(int reason) { + dismissH(reason); + } + + @Override + public void onScreenOff() { + dismissH(Events.DISMISS_REASON_SCREEN_OFF); + } + + @Override + public void onStateChanged(State state) { + onStateChangedH(state); + } + + @Override + public void onLayoutDirectionChanged(int layoutDirection) { + mDialogView.setLayoutDirection(layoutDirection); + } + + @Override + public void onConfigurationChanged() { + updateWindowWidthH(); + mSpTexts.update(); + mZenFooter.onConfigurationChanged(); + } + + @Override + public void onShowVibrateHint() { + if (mSilentMode) { + mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false); + } + } + + @Override + public void onShowSilentHint() { + if (mSilentMode) { + mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); + } + } + + @Override + public void onShowSafetyWarning(int flags) { + showSafetyWarningH(flags); + } + }; + + private final OnClickListener mClickExpand = new OnClickListener() { + @Override + public void onClick(View v) { + if (mExpandButtonAnimationRunning) return; + final boolean newExpand = !mExpanded; + Events.writeEvent(mContext, Events.EVENT_EXPAND, newExpand); + setExpandedH(newExpand); + } + }; + + private final OnClickListener mClickSettings = new OnClickListener() { + @Override + public void onClick(View v) { + mSettingsButton.postDelayed(new Runnable() { + @Override + public void run() { + Events.writeEvent(mContext, Events.EVENT_SETTINGS_CLICK); + if (mCallback != null) { + mCallback.onSettingsClicked(); + } + } + }, WAIT_FOR_RIPPLE); + } + }; + + private final class H extends Handler { + private static final int SHOW = 1; + private static final int DISMISS = 2; + private static final int RECHECK = 3; + private static final int RECHECK_ALL = 4; + private static final int SET_STREAM_IMPORTANT = 5; + private static final int RESCHEDULE_TIMEOUT = 6; + private static final int STATE_CHANGED = 7; + private static final int UPDATE_BOTTOM_MARGIN = 8; + + public H() { + super(Looper.getMainLooper()); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SHOW: showH(msg.arg1); break; + case DISMISS: dismissH(msg.arg1); break; + case RECHECK: recheckH((VolumeRow) msg.obj); break; + case RECHECK_ALL: recheckH(null); break; + case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break; + case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break; + case STATE_CHANGED: onStateChangedH(mState); break; + case UPDATE_BOTTOM_MARGIN: updateDialogBottomMarginH(); break; + } + } + } + + private final class CustomDialog extends Dialog { + public CustomDialog(Context context) { + super(context); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + rescheduleTimeoutH(); + return super.dispatchTouchEvent(ev); + } + + @Override + protected void onStop() { + super.onStop(); + final boolean animating = mMotion.isAnimating(); + if (D.BUG) Log.d(TAG, "onStop animating=" + animating); + if (animating) { + mPendingRecheckAll = true; + return; + } + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (isShowing()) { + if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { + dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE); + return true; + } + } + return false; + } + } + + private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener { + private final VolumeRow mRow; + + private VolumeSeekBarChangeListener(VolumeRow row) { + mRow = row; + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (mRow.ss == null) return; + if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream) + + " onProgressChanged " + progress + " fromUser=" + fromUser); + if (!fromUser) return; + if (mRow.ss.levelMin > 0) { + final int minProgress = mRow.ss.levelMin * 100; + if (progress < minProgress) { + seekBar.setProgress(minProgress); + } + } + final int userLevel = getImpliedLevel(seekBar, progress); + if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) { + mRow.userAttempt = SystemClock.uptimeMillis(); + if (mRow.requestedLevel != userLevel) { + mController.setStreamVolume(mRow.stream, userLevel); + mRow.requestedLevel = userLevel; + Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_CHANGED, mRow.stream, + userLevel); + } + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream); + mController.setActiveStream(mRow.stream); + mRow.tracking = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream); + mRow.tracking = false; + mRow.userAttempt = SystemClock.uptimeMillis(); + int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); + Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_DONE, mRow.stream, userLevel); + if (mRow.ss.level != userLevel) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow), + USER_ATTEMPT_GRACE_PERIOD); + } + } + } + + private final class Accessibility extends AccessibilityDelegate { + private AccessibilityManager mMgr; + private boolean mFeedbackEnabled; + + public void init() { + mMgr = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + mDialogView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { + @Override + public void onViewDetachedFromWindow(View v) { + if (D.BUG) Log.d(TAG, "onViewDetachedFromWindow"); + // noop + } + + @Override + public void onViewAttachedToWindow(View v) { + if (D.BUG) Log.d(TAG, "onViewAttachedToWindow"); + updateFeedbackEnabled(); + } + }); + mDialogView.setAccessibilityDelegate(this); + mMgr.addAccessibilityStateChangeListener(new AccessibilityStateChangeListener() { + @Override + public void onAccessibilityStateChanged(boolean enabled) { + updateFeedbackEnabled(); + } + }); + updateFeedbackEnabled(); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + rescheduleTimeoutH(); + return super.onRequestSendAccessibilityEvent(host, child, event); + } + + private void updateFeedbackEnabled() { + mFeedbackEnabled = computeFeedbackEnabled(); + } + + private boolean computeFeedbackEnabled() { + // are there any enabled non-generic a11y services? + final List<AccessibilityServiceInfo> services = + mMgr.getEnabledAccessibilityServiceList(FEEDBACK_ALL_MASK); + for (AccessibilityServiceInfo asi : services) { + if (asi.feedbackType != 0 && asi.feedbackType != FEEDBACK_GENERIC) { + return true; + } + } + return false; + } + } + + private static class VolumeRow { + private View view; + private View space; + private TextView header; + private ImageButton icon; + private SeekBar slider; + private ImageButton settingsButton; + private int stream; + private StreamState ss; + private long userAttempt; // last user-driven slider change + private boolean tracking; // tracking slider touch + private int requestedLevel = -1; // pending user-requested level via progress changed + private int iconRes; + private int iconMuteRes; + private boolean important; + private int cachedIconRes; + private ColorStateList cachedSliderTint; + private int iconState; // from Events + private boolean cachedShowHeaders = VolumePrefs.DEFAULT_SHOW_HEADERS; + private int cachedExpandButtonRes; + private ObjectAnimator anim; // slider progress animation for non-touch-related updates + private int animTargetProgress; + private int lastAudibleLevel = 1; + } + + public interface Callback { + void onSettingsClicked(); + void onZenSettingsClicked(); + void onZenPrioritySettingsClicked(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java new file mode 100644 index 0000000..1083f40 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.media.AudioManager; +import android.media.VolumePolicy; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; +import android.view.WindowManager; + +import com.android.systemui.SystemUI; +import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.qs.tiles.DndTile; +import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.statusbar.policy.ZenModeController; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Implementation of VolumeComponent backed by the new volume dialog. + */ +public class VolumeDialogComponent implements VolumeComponent { + private final SystemUI mSysui; + private final Context mContext; + private final VolumeDialogController mController; + private final ZenModeController mZenModeController; + private final VolumeDialog mDialog; + private final VolumePolicy mVolumePolicy = new VolumePolicy( + true, // volumeDownToEnterSilent + true, // volumeUpToExitSilent + true, // doNotDisturbWhenSilent + 400 // vibrateToSilentDebounce + ); + + public VolumeDialogComponent(SystemUI sysui, Context context, Handler handler, + ZenModeController zen) { + mSysui = sysui; + mContext = context; + mController = new VolumeDialogController(context, null) { + @Override + protected void onUserActivityW() { + sendUserActivity(); + } + }; + mZenModeController = zen; + mDialog = new VolumeDialog(context, WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY, + mController, zen, mVolumeDialogCallback); + applyConfiguration(); + } + + private void sendUserActivity() { + final KeyguardViewMediator kvm = mSysui.getComponent(KeyguardViewMediator.class); + if (kvm != null) { + kvm.userActivity(); + } + } + + private void applyConfiguration() { + mDialog.setStreamImportant(AudioManager.STREAM_ALARM, true); + mDialog.setStreamImportant(AudioManager.STREAM_SYSTEM, false); + mDialog.setShowHeaders(false); + mDialog.setAutomute(true); + mDialog.setSilentMode(false); + mController.setVolumePolicy(mVolumePolicy); + mController.showDndTile(true); + } + + @Override + public ZenModeController getZenController() { + return mZenModeController; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + // noop + } + + @Override + public void dismissNow() { + mController.dismiss(); + } + + @Override + public void dispatchDemoCommand(String command, Bundle args) { + // noop + } + + @Override + public void register() { + mController.register(); + DndTile.setCombinedIcon(mContext, true); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mController.dump(fd, pw, args); + mDialog.dump(pw); + } + + private void startSettings(Intent intent) { + mSysui.getComponent(PhoneStatusBar.class).startActivityDismissingKeyguard(intent, + true /* onlyProvisioned */, true /* dismissShade */); + } + + private final VolumeDialog.Callback mVolumeDialogCallback = new VolumeDialog.Callback() { + @Override + public void onSettingsClicked() { + startSettings(new Intent(Settings.ACTION_NOTIFICATION_SETTINGS)); + } + + @Override + public void onZenSettingsClicked() { + startSettings(ZenModePanel.ZEN_SETTINGS); + } + + @Override + public void onZenPrioritySettingsClicked() { + startSettings(ZenModePanel.ZEN_PRIORITY_SETTINGS); + } + }; + +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java new file mode 100644 index 0000000..32d6805 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java @@ -0,0 +1,1024 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.media.IVolumeController; +import android.media.VolumePolicy; +import android.media.session.MediaController.PlaybackInfo; +import android.media.session.MediaSession.Token; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.Vibrator; +import android.provider.Settings; +import android.service.notification.Condition; +import android.service.notification.ZenModeConfig; +import android.util.Log; +import android.util.SparseArray; + +import com.android.systemui.R; +import com.android.systemui.qs.tiles.DndTile; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Source of truth for all state / events related to the volume dialog. No presentation. + * + * All work done on a dedicated background worker thread & associated worker. + * + * Methods ending in "W" must be called on the worker thread. + */ +public class VolumeDialogController { + private static final String TAG = Util.logTag(VolumeDialogController.class); + + private static final int DYNAMIC_STREAM_START_INDEX = 100; + private static final int VIBRATE_HINT_DURATION = 50; + + private static final int[] STREAMS = { + AudioSystem.STREAM_ALARM, + AudioSystem.STREAM_BLUETOOTH_SCO, + AudioSystem.STREAM_DTMF, + AudioSystem.STREAM_MUSIC, + AudioSystem.STREAM_NOTIFICATION, + AudioSystem.STREAM_RING, + AudioSystem.STREAM_SYSTEM, + AudioSystem.STREAM_SYSTEM_ENFORCED, + AudioSystem.STREAM_TTS, + AudioSystem.STREAM_VOICE_CALL, + }; + + private final HandlerThread mWorkerThread; + private final W mWorker; + private final Context mContext; + private final AudioManager mAudio; + private final NotificationManager mNoMan; + private final ComponentName mComponent; + private final SettingObserver mObserver; + private final Receiver mReceiver = new Receiver(); + private final MediaSessions mMediaSessions; + private final VC mVolumeController = new VC(); + private final C mCallbacks = new C(); + private final State mState = new State(); + private final String[] mStreamTitles; + private final MediaSessionsCallbacks mMediaSessionsCallbacksW = new MediaSessionsCallbacks(); + private final Vibrator mVibrator; + private final boolean mHasVibrator; + + private boolean mEnabled; + private boolean mDestroyed; + private VolumePolicy mVolumePolicy; + private boolean mShowDndTile = true; + + public VolumeDialogController(Context context, ComponentName component) { + mContext = context.getApplicationContext(); + Events.writeEvent(mContext, Events.EVENT_COLLECTION_STARTED); + mComponent = component; + mWorkerThread = new HandlerThread(VolumeDialogController.class.getSimpleName()); + mWorkerThread.start(); + mWorker = new W(mWorkerThread.getLooper()); + mMediaSessions = createMediaSessions(mContext, mWorkerThread.getLooper(), + mMediaSessionsCallbacksW); + mAudio = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + mNoMan = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + mObserver = new SettingObserver(mWorker); + mObserver.init(); + mReceiver.init(); + mStreamTitles = mContext.getResources().getStringArray(R.array.volume_stream_titles); + mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + mHasVibrator = mVibrator != null && mVibrator.hasVibrator(); + } + + public AudioManager getAudioManager() { + return mAudio; + } + + public ZenModeConfig getZenModeConfig() { + return mNoMan.getZenModeConfig(); + } + + public void dismiss() { + mCallbacks.onDismissRequested(Events.DISMISS_REASON_VOLUME_CONTROLLER); + } + + public void register() { + try { + mAudio.setVolumeController(mVolumeController); + } catch (SecurityException e) { + Log.w(TAG, "Unable to set the volume controller", e); + return; + } + setVolumePolicy(mVolumePolicy); + showDndTile(mShowDndTile); + try { + mMediaSessions.init(); + } catch (SecurityException e) { + Log.w(TAG, "No access to media sessions", e); + } + } + + public void setVolumePolicy(VolumePolicy policy) { + mVolumePolicy = policy; + if (mVolumePolicy == null) return; + try { + mAudio.setVolumePolicy(mVolumePolicy); + } catch (NoSuchMethodError e) { + Log.w(TAG, "No volume policy api"); + } + } + + protected MediaSessions createMediaSessions(Context context, Looper looper, + MediaSessions.Callbacks callbacks) { + return new MediaSessions(context, looper, callbacks); + } + + public void destroy() { + if (D.BUG) Log.d(TAG, "destroy"); + if (mDestroyed) return; + mDestroyed = true; + Events.writeEvent(mContext, Events.EVENT_COLLECTION_STOPPED); + mMediaSessions.destroy(); + mObserver.destroy(); + mReceiver.destroy(); + mWorkerThread.quitSafely(); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println(VolumeDialogController.class.getSimpleName() + " state:"); + pw.print(" mEnabled: "); pw.println(mEnabled); + pw.print(" mDestroyed: "); pw.println(mDestroyed); + pw.print(" mVolumePolicy: "); pw.println(mVolumePolicy); + pw.print(" mState: "); pw.println(mState.toString(4)); + pw.print(" mShowDndTile: "); pw.println(mShowDndTile); + pw.print(" mHasVibrator: "); pw.println(mHasVibrator); + pw.print(" mRemoteStreams: "); pw.println(mMediaSessionsCallbacksW.mRemoteStreams + .values()); + pw.println(); + mMediaSessions.dump(pw); + } + + public void addCallback(Callbacks callback, Handler handler) { + mCallbacks.add(callback, handler); + } + + public void removeCallback(Callbacks callback) { + mCallbacks.remove(callback); + } + + public void getState() { + if (mDestroyed) return; + mWorker.sendEmptyMessage(W.GET_STATE); + } + + public void notifyVisible(boolean visible) { + if (mDestroyed) return; + mWorker.obtainMessage(W.NOTIFY_VISIBLE, visible ? 1 : 0, 0).sendToTarget(); + } + + public void userActivity() { + if (mDestroyed) return; + mWorker.removeMessages(W.USER_ACTIVITY); + mWorker.sendEmptyMessage(W.USER_ACTIVITY); + } + + public void setRingerMode(int value, boolean external) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_RINGER_MODE, value, external ? 1 : 0).sendToTarget(); + } + + public void setZenMode(int value) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_ZEN_MODE, value, 0).sendToTarget(); + } + + public void setExitCondition(Condition condition) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_EXIT_CONDITION, condition).sendToTarget(); + } + + public void setStreamMute(int stream, boolean mute) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_STREAM_MUTE, stream, mute ? 1 : 0).sendToTarget(); + } + + public void setStreamVolume(int stream, int level) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_STREAM_VOLUME, stream, level).sendToTarget(); + } + + public void setActiveStream(int stream) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_ACTIVE_STREAM, stream, 0).sendToTarget(); + } + + public void vibrate() { + if (mHasVibrator) { + mVibrator.vibrate(VIBRATE_HINT_DURATION); + } + } + + public boolean hasVibrator() { + return mHasVibrator; + } + + private void onNotifyVisibleW(boolean visible) { + if (mDestroyed) return; + mAudio.notifyVolumeControllerVisible(mVolumeController, visible); + if (!visible) { + if (updateActiveStreamW(-1)) { + mCallbacks.onStateChanged(mState); + } + } + } + + protected void onUserActivityW() { + // hook for subclasses + } + + private void onShowSafetyWarningW(int flags) { + mCallbacks.onShowSafetyWarning(flags); + } + + private boolean checkRoutedToBluetoothW(int stream) { + boolean changed = false; + if (stream == AudioManager.STREAM_MUSIC) { + final boolean routedToBluetooth = + (mAudio.getDevicesForStream(AudioManager.STREAM_MUSIC) & + (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP | + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0; + changed |= updateStreamRoutedToBluetoothW(stream, routedToBluetooth); + } + return changed; + } + + private void onVolumeChangedW(int stream, int flags) { + final boolean showUI = (flags & AudioManager.FLAG_SHOW_UI) != 0; + final boolean fromKey = (flags & AudioManager.FLAG_FROM_KEY) != 0; + final boolean showVibrateHint = (flags & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0; + final boolean showSilentHint = (flags & AudioManager.FLAG_SHOW_SILENT_HINT) != 0; + boolean changed = false; + if (showUI) { + changed |= updateActiveStreamW(stream); + } + int lastAudibleStreamVolume = mAudio.getLastAudibleStreamVolume(stream); + changed |= updateStreamLevelW(stream, lastAudibleStreamVolume); + changed |= checkRoutedToBluetoothW(showUI ? AudioManager.STREAM_MUSIC : stream); + if (changed) { + mCallbacks.onStateChanged(mState); + } + if (showUI) { + mCallbacks.onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED); + } + if (showVibrateHint) { + mCallbacks.onShowVibrateHint(); + } + if (showSilentHint) { + mCallbacks.onShowSilentHint(); + } + if (changed && fromKey) { + Events.writeEvent(mContext, Events.EVENT_KEY, stream, lastAudibleStreamVolume); + } + } + + private boolean updateActiveStreamW(int activeStream) { + if (activeStream == mState.activeStream) return false; + mState.activeStream = activeStream; + Events.writeEvent(mContext, Events.EVENT_ACTIVE_STREAM_CHANGED, activeStream); + if (D.BUG) Log.d(TAG, "updateActiveStreamW " + activeStream); + final int s = activeStream < DYNAMIC_STREAM_START_INDEX ? activeStream : -1; + if (D.BUG) Log.d(TAG, "forceVolumeControlStream " + s); + mAudio.forceVolumeControlStream(s); + return true; + } + + private StreamState streamStateW(int stream) { + StreamState ss = mState.states.get(stream); + if (ss == null) { + ss = new StreamState(); + mState.states.put(stream, ss); + } + return ss; + } + + private void onGetStateW() { + for (int stream : STREAMS) { + updateStreamLevelW(stream, mAudio.getLastAudibleStreamVolume(stream)); + streamStateW(stream).levelMin = mAudio.getStreamMinVolume(stream); + streamStateW(stream).levelMax = mAudio.getStreamMaxVolume(stream); + updateStreamMuteW(stream, mAudio.isStreamMute(stream)); + final StreamState ss = streamStateW(stream); + ss.muteSupported = mAudio.isStreamAffectedByMute(stream); + ss.name = mStreamTitles[stream]; + checkRoutedToBluetoothW(stream); + } + updateRingerModeExternalW(mAudio.getRingerMode()); + updateZenModeW(); + updateEffectsSuppressorW(mNoMan.getEffectsSuppressor()); + updateZenModeConfigW(); + mCallbacks.onStateChanged(mState); + } + + private boolean updateStreamRoutedToBluetoothW(int stream, boolean routedToBluetooth) { + final StreamState ss = streamStateW(stream); + if (ss.routedToBluetooth == routedToBluetooth) return false; + ss.routedToBluetooth = routedToBluetooth; + if (D.BUG) Log.d(TAG, "updateStreamRoutedToBluetoothW stream=" + stream + + " routedToBluetooth=" + routedToBluetooth); + return true; + } + + private boolean updateStreamLevelW(int stream, int level) { + final StreamState ss = streamStateW(stream); + if (ss.level == level) return false; + ss.level = level; + if (isLogWorthy(stream)) { + Events.writeEvent(mContext, Events.EVENT_LEVEL_CHANGED, stream, level); + } + return true; + } + + private static boolean isLogWorthy(int stream) { + switch (stream) { + case AudioSystem.STREAM_ALARM: + case AudioSystem.STREAM_BLUETOOTH_SCO: + case AudioSystem.STREAM_MUSIC: + case AudioSystem.STREAM_RING: + case AudioSystem.STREAM_SYSTEM: + case AudioSystem.STREAM_VOICE_CALL: + return true; + } + return false; + } + + private boolean updateStreamMuteW(int stream, boolean muted) { + final StreamState ss = streamStateW(stream); + if (ss.muted == muted) return false; + ss.muted = muted; + if (isLogWorthy(stream)) { + Events.writeEvent(mContext, Events.EVENT_MUTE_CHANGED, stream, muted); + } + if (muted && isRinger(stream)) { + updateRingerModeInternalW(mAudio.getRingerModeInternal()); + } + return true; + } + + private static boolean isRinger(int stream) { + return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION; + } + + private boolean updateZenModeConfigW() { + final ZenModeConfig zenModeConfig = getZenModeConfig(); + if (Objects.equals(mState.zenModeConfig, zenModeConfig)) return false; + mState.zenModeConfig = zenModeConfig; + return true; + } + + private boolean updateEffectsSuppressorW(ComponentName effectsSuppressor) { + if (Objects.equals(mState.effectsSuppressor, effectsSuppressor)) return false; + mState.effectsSuppressor = effectsSuppressor; + mState.effectsSuppressorName = getApplicationName(mContext, mState.effectsSuppressor); + Events.writeEvent(mContext, Events.EVENT_SUPPRESSOR_CHANGED, mState.effectsSuppressor, + mState.effectsSuppressorName); + return true; + } + + private static String getApplicationName(Context context, ComponentName component) { + if (component == null) return null; + final PackageManager pm = context.getPackageManager(); + final String pkg = component.getPackageName(); + try { + final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0); + final String rt = Objects.toString(ai.loadLabel(pm), "").trim(); + if (rt.length() > 0) { + return rt; + } + } catch (NameNotFoundException e) {} + return pkg; + } + + private boolean updateZenModeW() { + final int zen = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF); + if (mState.zenMode == zen) return false; + mState.zenMode = zen; + Events.writeEvent(mContext, Events.EVENT_ZEN_MODE_CHANGED, zen); + return true; + } + + private boolean updateRingerModeExternalW(int rm) { + if (rm == mState.ringerModeExternal) return false; + mState.ringerModeExternal = rm; + Events.writeEvent(mContext, Events.EVENT_EXTERNAL_RINGER_MODE_CHANGED, rm); + return true; + } + + private boolean updateRingerModeInternalW(int rm) { + if (rm == mState.ringerModeInternal) return false; + mState.ringerModeInternal = rm; + Events.writeEvent(mContext, Events.EVENT_INTERNAL_RINGER_MODE_CHANGED, rm); + return true; + } + + private void onSetRingerModeW(int mode, boolean external) { + if (external) { + mAudio.setRingerMode(mode); + } else { + mAudio.setRingerModeInternal(mode); + } + } + + private void onSetStreamMuteW(int stream, boolean mute) { + mAudio.adjustStreamVolume(stream, mute ? AudioManager.ADJUST_MUTE + : AudioManager.ADJUST_UNMUTE, 0); + } + + private void onSetStreamVolumeW(int stream, int level) { + if (D.BUG) Log.d(TAG, "onSetStreamVolume " + stream + " level=" + level); + if (stream >= DYNAMIC_STREAM_START_INDEX) { + mMediaSessionsCallbacksW.setStreamVolume(stream, level); + return; + } + mAudio.setStreamVolume(stream, level, 0); + } + + private void onSetActiveStreamW(int stream) { + boolean changed = updateActiveStreamW(stream); + if (changed) { + mCallbacks.onStateChanged(mState); + } + } + + private void onSetExitConditionW(Condition condition) { + mNoMan.setZenMode(mState.zenMode, condition != null ? condition.id : null, TAG); + } + + private void onSetZenModeW(int mode) { + if (D.BUG) Log.d(TAG, "onSetZenModeW " + mode); + mNoMan.setZenMode(mode, null, TAG); + } + + private void onDismissRequestedW(int reason) { + mCallbacks.onDismissRequested(reason); + } + + public void showDndTile(boolean visible) { + if (D.BUG) Log.d(TAG, "showDndTile"); + DndTile.setVisible(mContext, visible); + } + + private final class VC extends IVolumeController.Stub { + private final String TAG = VolumeDialogController.TAG + ".VC"; + + @Override + public void displaySafeVolumeWarning(int flags) throws RemoteException { + if (D.BUG) Log.d(TAG, "displaySafeVolumeWarning " + + Util.audioManagerFlagsToString(flags)); + if (mDestroyed) return; + mWorker.obtainMessage(W.SHOW_SAFETY_WARNING, flags, 0).sendToTarget(); + } + + @Override + public void volumeChanged(int streamType, int flags) throws RemoteException { + if (D.BUG) Log.d(TAG, "volumeChanged " + AudioSystem.streamToString(streamType) + + " " + Util.audioManagerFlagsToString(flags)); + if (mDestroyed) return; + mWorker.obtainMessage(W.VOLUME_CHANGED, streamType, flags).sendToTarget(); + } + + @Override + public void masterMuteChanged(int flags) throws RemoteException { + if (D.BUG) Log.d(TAG, "masterMuteChanged"); + } + + @Override + public void setLayoutDirection(int layoutDirection) throws RemoteException { + if (D.BUG) Log.d(TAG, "setLayoutDirection"); + if (mDestroyed) return; + mWorker.obtainMessage(W.LAYOUT_DIRECTION_CHANGED, layoutDirection, 0).sendToTarget(); + } + + @Override + public void dismiss() throws RemoteException { + if (D.BUG) Log.d(TAG, "dismiss requested"); + if (mDestroyed) return; + mWorker.obtainMessage(W.DISMISS_REQUESTED, Events.DISMISS_REASON_VOLUME_CONTROLLER, 0) + .sendToTarget(); + mWorker.sendEmptyMessage(W.DISMISS_REQUESTED); + } + } + + private final class W extends Handler { + private static final int VOLUME_CHANGED = 1; + private static final int DISMISS_REQUESTED = 2; + private static final int GET_STATE = 3; + private static final int SET_RINGER_MODE = 4; + private static final int SET_ZEN_MODE = 5; + private static final int SET_EXIT_CONDITION = 6; + private static final int SET_STREAM_MUTE = 7; + private static final int LAYOUT_DIRECTION_CHANGED = 8; + private static final int CONFIGURATION_CHANGED = 9; + private static final int SET_STREAM_VOLUME = 10; + private static final int SET_ACTIVE_STREAM = 11; + private static final int NOTIFY_VISIBLE = 12; + private static final int USER_ACTIVITY = 13; + private static final int SHOW_SAFETY_WARNING = 14; + + W(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case VOLUME_CHANGED: onVolumeChangedW(msg.arg1, msg.arg2); break; + case DISMISS_REQUESTED: onDismissRequestedW(msg.arg1); break; + case GET_STATE: onGetStateW(); break; + case SET_RINGER_MODE: onSetRingerModeW(msg.arg1, msg.arg2 != 0); break; + case SET_ZEN_MODE: onSetZenModeW(msg.arg1); break; + case SET_EXIT_CONDITION: onSetExitConditionW((Condition) msg.obj); break; + case SET_STREAM_MUTE: onSetStreamMuteW(msg.arg1, msg.arg2 != 0); break; + case LAYOUT_DIRECTION_CHANGED: mCallbacks.onLayoutDirectionChanged(msg.arg1); break; + case CONFIGURATION_CHANGED: mCallbacks.onConfigurationChanged(); break; + case SET_STREAM_VOLUME: onSetStreamVolumeW(msg.arg1, msg.arg2); break; + case SET_ACTIVE_STREAM: onSetActiveStreamW(msg.arg1); break; + case NOTIFY_VISIBLE: onNotifyVisibleW(msg.arg1 != 0); break; + case USER_ACTIVITY: onUserActivityW(); break; + case SHOW_SAFETY_WARNING: onShowSafetyWarningW(msg.arg1); break; + } + } + } + + private final class C implements Callbacks { + private final HashMap<Callbacks, Handler> mCallbackMap = new HashMap<>(); + + public void add(Callbacks callback, Handler handler) { + if (callback == null || handler == null) throw new IllegalArgumentException(); + mCallbackMap.put(callback, handler); + } + + public void remove(Callbacks callback) { + mCallbackMap.remove(callback); + } + + @Override + public void onShowRequested(final int reason) { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onShowRequested(reason); + } + }); + } + } + + @Override + public void onDismissRequested(final int reason) { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onDismissRequested(reason); + } + }); + } + } + + @Override + public void onStateChanged(final State state) { + final long time = System.currentTimeMillis(); + final State copy = state.copy(); + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onStateChanged(copy); + } + }); + } + Events.writeState(time, copy); + } + + @Override + public void onLayoutDirectionChanged(final int layoutDirection) { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onLayoutDirectionChanged(layoutDirection); + } + }); + } + } + + @Override + public void onConfigurationChanged() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onConfigurationChanged(); + } + }); + } + } + + @Override + public void onShowVibrateHint() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onShowVibrateHint(); + } + }); + } + } + + @Override + public void onShowSilentHint() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onShowSilentHint(); + } + }); + } + } + + @Override + public void onScreenOff() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onScreenOff(); + } + }); + } + } + + @Override + public void onShowSafetyWarning(final int flags) { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onShowSafetyWarning(flags); + } + }); + } + } + } + + + private final class SettingObserver extends ContentObserver { + private final Uri SERVICE_URI = Settings.Secure.getUriFor( + Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT); + private final Uri ZEN_MODE_URI = + Settings.Global.getUriFor(Settings.Global.ZEN_MODE); + private final Uri ZEN_MODE_CONFIG_URI = + Settings.Global.getUriFor(Settings.Global.ZEN_MODE_CONFIG_ETAG); + + public SettingObserver(Handler handler) { + super(handler); + } + + public void init() { + mContext.getContentResolver().registerContentObserver(SERVICE_URI, false, this); + mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this); + mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_URI, false, this); + onChange(true, SERVICE_URI); + } + + public void destroy() { + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + boolean changed = false; + if (SERVICE_URI.equals(uri)) { + final String setting = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT); + final boolean enabled = setting != null && mComponent != null + && mComponent.equals(ComponentName.unflattenFromString(setting)); + if (enabled == mEnabled) return; + if (enabled) { + register(); + } + mEnabled = enabled; + } + if (ZEN_MODE_URI.equals(uri)) { + changed = updateZenModeW(); + } + if (ZEN_MODE_CONFIG_URI.equals(uri)) { + changed = updateZenModeConfigW(); + } + if (changed) { + mCallbacks.onStateChanged(mState); + } + } + } + + private final class Receiver extends BroadcastReceiver { + + public void init() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(AudioManager.VOLUME_CHANGED_ACTION); + filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION); + filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); + filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); + filter.addAction(AudioManager.STREAM_MUTE_CHANGED_ACTION); + filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + mContext.registerReceiver(this, filter, null, mWorker); + } + + public void destroy() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + boolean changed = false; + if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { + final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final int level = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1); + final int oldLevel = intent + .getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, -1); + if (D.BUG) Log.d(TAG, "onReceive VOLUME_CHANGED_ACTION stream=" + stream + + " level=" + level + " oldLevel=" + oldLevel); + changed = updateStreamLevelW(stream, level); + } else if (action.equals(AudioManager.STREAM_DEVICES_CHANGED_ACTION)) { + final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final int devices = intent + .getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_DEVICES, -1); + final int oldDevices = intent + .getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_DEVICES, -1); + if (D.BUG) Log.d(TAG, "onReceive STREAM_DEVICES_CHANGED_ACTION stream=" + + stream + " devices=" + devices + " oldDevices=" + oldDevices); + changed = checkRoutedToBluetoothW(stream); + } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { + final int rm = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1); + if (D.BUG) Log.d(TAG, "onReceive RINGER_MODE_CHANGED_ACTION rm=" + + Util.ringerModeToString(rm)); + changed = updateRingerModeExternalW(rm); + } else if (action.equals(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)) { + final int rm = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1); + if (D.BUG) Log.d(TAG, "onReceive INTERNAL_RINGER_MODE_CHANGED_ACTION rm=" + + Util.ringerModeToString(rm)); + changed = updateRingerModeInternalW(rm); + } else if (action.equals(AudioManager.STREAM_MUTE_CHANGED_ACTION)) { + final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final boolean muted = intent + .getBooleanExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, false); + if (D.BUG) Log.d(TAG, "onReceive STREAM_MUTE_CHANGED_ACTION stream=" + stream + + " muted=" + muted); + changed = updateStreamMuteW(stream, muted); + } else if (action.equals(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED)) { + if (D.BUG) Log.d(TAG, "onReceive ACTION_EFFECTS_SUPPRESSOR_CHANGED"); + changed = updateEffectsSuppressorW(mNoMan.getEffectsSuppressor()); + } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) { + if (D.BUG) Log.d(TAG, "onReceive ACTION_CONFIGURATION_CHANGED"); + mCallbacks.onConfigurationChanged(); + } else if (action.equals(Intent.ACTION_SCREEN_OFF)) { + if (D.BUG) Log.d(TAG, "onReceive ACTION_SCREEN_OFF"); + mCallbacks.onScreenOff(); + } else if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) { + if (D.BUG) Log.d(TAG, "onReceive ACTION_CLOSE_SYSTEM_DIALOGS"); + dismiss(); + } + if (changed) { + mCallbacks.onStateChanged(mState); + } + } + } + + private final class MediaSessionsCallbacks implements MediaSessions.Callbacks { + private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); + + private int mNextStream = DYNAMIC_STREAM_START_INDEX; + + @Override + public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) { + if (!mRemoteStreams.containsKey(token)) { + mRemoteStreams.put(token, mNextStream); + if (D.BUG) Log.d(TAG, "onRemoteUpdate: " + name + " is stream " + mNextStream); + mNextStream++; + } + final int stream = mRemoteStreams.get(token); + boolean changed = mState.states.indexOfKey(stream) < 0; + final StreamState ss = streamStateW(stream); + ss.dynamic = true; + ss.levelMin = 0; + ss.levelMax = pi.getMaxVolume(); + if (ss.level != pi.getCurrentVolume()) { + ss.level = pi.getCurrentVolume(); + changed = true; + } + if (!Objects.equals(ss.name, name)) { + ss.name = name; + changed = true; + } + if (changed) { + if (D.BUG) Log.d(TAG, "onRemoteUpdate: " + name + ": " + ss.level + + " of " + ss.levelMax); + mCallbacks.onStateChanged(mState); + } + } + + @Override + public void onRemoteVolumeChanged(Token token, int flags) { + final int stream = mRemoteStreams.get(token); + final boolean showUI = (flags & AudioManager.FLAG_SHOW_UI) != 0; + boolean changed = updateActiveStreamW(stream); + if (showUI) { + changed |= checkRoutedToBluetoothW(AudioManager.STREAM_MUSIC); + } + if (changed) { + mCallbacks.onStateChanged(mState); + } + if (showUI) { + mCallbacks.onShowRequested(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED); + } + } + + @Override + public void onRemoteRemoved(Token token) { + final int stream = mRemoteStreams.get(token); + mState.states.remove(stream); + if (mState.activeStream == stream) { + updateActiveStreamW(-1); + } + mCallbacks.onStateChanged(mState); + } + + public void setStreamVolume(int stream, int level) { + final Token t = findToken(stream); + if (t == null) { + Log.w(TAG, "setStreamVolume: No token found for stream: " + stream); + return; + } + mMediaSessions.setVolume(t, level); + } + + private Token findToken(int stream) { + for (Map.Entry<Token, Integer> entry : mRemoteStreams.entrySet()) { + if (entry.getValue().equals(stream)) { + return entry.getKey(); + } + } + return null; + } + } + + public static final class StreamState { + public boolean dynamic; + public int level; + public int levelMin; + public int levelMax; + public boolean muted; + public boolean muteSupported; + public String name; + public boolean routedToBluetooth; + + public StreamState copy() { + final StreamState rt = new StreamState(); + rt.dynamic = dynamic; + rt.level = level; + rt.levelMin = levelMin; + rt.levelMax = levelMax; + rt.muted = muted; + rt.muteSupported = muteSupported; + rt.name = name; + rt.routedToBluetooth = routedToBluetooth; + return rt; + } + } + + public static final class State { + public static int NO_ACTIVE_STREAM = -1; + + public final SparseArray<StreamState> states = new SparseArray<StreamState>(); + + public int ringerModeInternal; + public int ringerModeExternal; + public int zenMode; + public ComponentName effectsSuppressor; + public String effectsSuppressorName; + public ZenModeConfig zenModeConfig; + public int activeStream = NO_ACTIVE_STREAM; + + public State copy() { + final State rt = new State(); + for (int i = 0; i < states.size(); i++) { + rt.states.put(states.keyAt(i), states.valueAt(i).copy()); + } + rt.ringerModeExternal = ringerModeExternal; + rt.ringerModeInternal = ringerModeInternal; + rt.zenMode = zenMode; + if (effectsSuppressor != null) rt.effectsSuppressor = effectsSuppressor.clone(); + rt.effectsSuppressorName = effectsSuppressorName; + if (zenModeConfig != null) rt.zenModeConfig = zenModeConfig.copy(); + rt.activeStream = activeStream; + return rt; + } + + @Override + public String toString() { + return toString(0); + } + + public String toString(int indent) { + final StringBuilder sb = new StringBuilder("{"); + if (indent > 0) sep(sb, indent); + for (int i = 0; i < states.size(); i++) { + if (i > 0) { + sep(sb, indent); + } + final int stream = states.keyAt(i); + final StreamState ss = states.valueAt(i); + sb.append(AudioSystem.streamToString(stream)).append(":").append(ss.level) + .append('[').append(ss.levelMin).append("..").append(ss.levelMax) + .append(']'); + if (ss.muted) sb.append(" [MUTED]"); + } + sep(sb, indent); sb.append("ringerModeExternal:").append(ringerModeExternal); + sep(sb, indent); sb.append("ringerModeInternal:").append(ringerModeInternal); + sep(sb, indent); sb.append("zenMode:").append(zenMode); + sep(sb, indent); sb.append("effectsSuppressor:").append(effectsSuppressor); + sep(sb, indent); sb.append("effectsSuppressorName:").append(effectsSuppressorName); + sep(sb, indent); sb.append("zenModeConfig:").append(zenModeConfig); + sep(sb, indent); sb.append("activeStream:").append(activeStream); + if (indent > 0) sep(sb, indent); + return sb.append('}').toString(); + } + + private static void sep(StringBuilder sb, int indent) { + if (indent > 0) { + sb.append('\n'); + for (int i = 0; i < indent; i++) { + sb.append(' '); + } + } else { + sb.append(','); + } + } + + public Condition getManualExitCondition() { + return zenModeConfig != null && zenModeConfig.manualRule != null + ? zenModeConfig.manualRule.condition : null; + } + } + + public interface Callbacks { + void onShowRequested(int reason); + void onDismissRequested(int reason); + void onStateChanged(State state); + void onLayoutDirectionChanged(int layoutDirection); + void onConfigurationChanged(); + void onShowVibrateHint(); + void onShowSilentHint(); + void onScreenOff(); + void onShowSafetyWarning(int flags); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogMotion.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogMotion.java new file mode 100644 index 0000000..4bb1011 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogMotion.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.content.DialogInterface.OnShowListener; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.PathInterpolator; + +public class VolumeDialogMotion { + private static final String TAG = Util.logTag(VolumeDialogMotion.class); + + private static final float ANIMATION_SCALE = 1.0f; + private static final int PRE_DISMISS_DELAY = 50; + private static final int POST_SHOW_DELAY = 200; + + private final Dialog mDialog; + private final View mDialogView; + private final ViewGroup mContents; // volume rows + zen footer + private final View mChevron; + private final Handler mHandler = new Handler(); + private final Callback mCallback; + + private boolean mAnimating; // show or dismiss animation is running + private boolean mShowing; // show animation is running + private boolean mDismissing; // dismiss animation is running + private ValueAnimator mChevronPositionAnimator; + private ValueAnimator mContentsPositionAnimator; + + public VolumeDialogMotion(Dialog dialog, View dialogView, ViewGroup contents, View chevron, + Callback callback) { + mDialog = dialog; + mDialogView = dialogView; + mContents = contents; + mChevron = chevron; + mCallback = callback; + mDialog.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (D.BUG) Log.d(TAG, "mDialog.onDismiss"); + } + }); + mDialog.setOnShowListener(new OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + if (D.BUG) Log.d(TAG, "mDialog.onShow"); + final int h = mDialogView.getHeight(); + mDialogView.setTranslationY(-h); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + startShowAnimation(); + } + }, POST_SHOW_DELAY); + } + }); + } + + public boolean isAnimating() { + return mAnimating; + } + + private void setShowing(boolean showing) { + if (showing == mShowing) return; + mShowing = showing; + if (D.BUG) Log.d(TAG, "mShowing = " + mShowing); + updateAnimating(); + } + + private void setDismissing(boolean dismissing) { + if (dismissing == mDismissing) return; + mDismissing = dismissing; + if (D.BUG) Log.d(TAG, "mDismissing = " + mDismissing); + updateAnimating(); + } + + private void updateAnimating() { + final boolean animating = mShowing || mDismissing; + if (animating == mAnimating) return; + mAnimating = animating; + if (D.BUG) Log.d(TAG, "mAnimating = " + mAnimating); + if (mCallback != null) { + mCallback.onAnimatingChanged(mAnimating); + } + } + + public void startShow() { + if (D.BUG) Log.d(TAG, "startShow"); + if (mShowing) return; + setShowing(true); + if (mDismissing) { + mDialogView.animate().cancel(); + setDismissing(false); + startShowAnimation(); + return; + } + if (D.BUG) Log.d(TAG, "mDialog.show()"); + mDialog.show(); + } + + private int chevronDistance() { + return mChevron.getHeight() / 6; + } + + private int chevronPosY() { + final Object tag = mChevron == null ? null : mChevron.getTag(); + return tag == null ? 0 : (Integer) tag; + } + + private void startShowAnimation() { + if (D.BUG) Log.d(TAG, "startShowAnimation"); + mDialogView.animate() + .translationY(0) + .setDuration(scaledDuration(300)) + .setInterpolator(new LogDecelerateInterpolator()) + .setListener(null) + .setUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (mChevronPositionAnimator == null) return; + // reposition chevron + final float v = (Float) mChevronPositionAnimator.getAnimatedValue(); + final int posY = chevronPosY(); + mChevron.setTranslationY(posY + v + -mDialogView.getTranslationY()); + }}) + .start(); + + mContentsPositionAnimator = ValueAnimator.ofFloat(-chevronDistance(), 0) + .setDuration(scaledDuration(400)); + mContentsPositionAnimator.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationEnd(Animator animation) { + if (mCancelled) return; + if (D.BUG) Log.d(TAG, "show.onAnimationEnd"); + setShowing(false); + } + @Override + public void onAnimationCancel(Animator animation) { + if (D.BUG) Log.d(TAG, "show.onAnimationCancel"); + mCancelled = true; + } + }); + mContentsPositionAnimator.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float v = (Float) animation.getAnimatedValue(); + mContents.setTranslationY(v + -mDialogView.getTranslationY()); + } + }); + mContentsPositionAnimator.setInterpolator(new LogDecelerateInterpolator()); + mContentsPositionAnimator.start(); + + mContents.setAlpha(0); + mContents.animate() + .alpha(1) + .setDuration(scaledDuration(150)) + .setInterpolator(new PathInterpolator(0f, 0f, .2f, 1f)) + .start(); + + mChevronPositionAnimator = ValueAnimator.ofFloat(-chevronDistance(), 0) + .setDuration(scaledDuration(250)); + mChevronPositionAnimator.setInterpolator(new PathInterpolator(.4f, 0f, .2f, 1f)); + mChevronPositionAnimator.start(); + + mChevron.setAlpha(0); + mChevron.animate() + .alpha(1) + .setStartDelay(scaledDuration(50)) + .setDuration(scaledDuration(150)) + .setInterpolator(new PathInterpolator(.4f, 0f, 1f, 1f)) + .start(); + } + + public void startDismiss(final Runnable onComplete) { + if (D.BUG) Log.d(TAG, "startDismiss"); + if (mDismissing) return; + setDismissing(true); + if (mShowing) { + mDialogView.animate().cancel(); + if (mContentsPositionAnimator != null) { + mContentsPositionAnimator.cancel(); + } + mContents.animate().cancel(); + if (mChevronPositionAnimator != null) { + mChevronPositionAnimator.cancel(); + } + mChevron.animate().cancel(); + setShowing(false); + } + mDialogView.animate() + .translationY(-mDialogView.getHeight()) + .setDuration(scaledDuration(250)) + .setInterpolator(new LogAccelerateInterpolator()) + .setUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mContents.setTranslationY(-mDialogView.getTranslationY()); + final int posY = chevronPosY(); + mChevron.setTranslationY(posY + -mDialogView.getTranslationY()); + } + }) + .setListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + @Override + public void onAnimationEnd(Animator animation) { + if (mCancelled) return; + if (D.BUG) Log.d(TAG, "dismiss.onAnimationEnd"); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (D.BUG) Log.d(TAG, "mDialog.dismiss()"); + mDialog.dismiss(); + onComplete.run(); + setDismissing(false); + } + }, PRE_DISMISS_DELAY); + + } + @Override + public void onAnimationCancel(Animator animation) { + if (D.BUG) Log.d(TAG, "dismiss.onAnimationCancel"); + mCancelled = true; + } + }).start(); + } + + private static int scaledDuration(int base) { + return (int) (base * ANIMATION_SCALE); + } + + private static final class LogDecelerateInterpolator implements TimeInterpolator { + private final float mBase; + private final float mDrift; + private final float mTimeScale; + private final float mOutputScale; + + private LogDecelerateInterpolator() { + this(400f, 1.4f, 0); + } + + private LogDecelerateInterpolator(float base, float timeScale, float drift) { + mBase = base; + mDrift = drift; + mTimeScale = 1f / timeScale; + + mOutputScale = 1f / computeLog(1f); + } + + private float computeLog(float t) { + return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t); + } + + @Override + public float getInterpolation(float t) { + return computeLog(t) * mOutputScale; + } + } + + private static final class LogAccelerateInterpolator implements TimeInterpolator { + private final int mBase; + private final int mDrift; + private final float mLogScale; + + private LogAccelerateInterpolator() { + this(100, 0); + } + + private LogAccelerateInterpolator(int base, int drift) { + mBase = base; + mDrift = drift; + mLogScale = 1f / computeLog(1, mBase, mDrift); + } + + private static float computeLog(float t, int base, int drift) { + return (float) -Math.pow(base, -t) + 1 + (drift * t); + } + + @Override + public float getInterpolation(float t) { + return 1 - computeLog(1 - t, mBase, mDrift) * mLogScale; + } + } + + public interface Callback { + void onAnimatingChanged(boolean animating); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java deleted file mode 100644 index acdcfc1..0000000 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java +++ /dev/null @@ -1,1664 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.volume; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnDismissListener; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.PixelFormat; -import android.graphics.drawable.ColorDrawable; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.AudioService; -import android.media.AudioSystem; -import android.media.RingtoneManager; -import android.media.ToneGenerator; -import android.media.VolumeProvider; -import android.media.session.MediaController; -import android.media.session.MediaController.PlaybackInfo; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Debug; -import android.os.Handler; -import android.os.Message; -import android.os.Vibrator; -import android.util.Log; -import android.util.SparseArray; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.view.WindowManager.LayoutParams; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.widget.ImageView; -import android.widget.SeekBar; -import android.widget.SeekBar.OnSeekBarChangeListener; -import android.widget.TextView; - -import com.android.internal.R; -import com.android.systemui.DemoMode; -import com.android.systemui.statusbar.phone.SystemUIDialog; -import com.android.systemui.statusbar.policy.ZenModeController; - -import java.io.FileDescriptor; -import java.io.PrintWriter; - -/** - * Handles the user interface for the volume keys. - * - * @hide - */ -public class VolumePanel extends Handler implements DemoMode { - private static final String TAG = "VolumePanel"; - private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); - - private static final int PLAY_SOUND_DELAY = AudioService.PLAY_SOUND_DELAY; - - /** - * The delay before vibrating. This small period exists so if the user is - * moving to silent mode, it will not emit a short vibrate (it normally - * would since vibrate is between normal mode and silent mode using hardware - * keys). - */ - public static final int VIBRATE_DELAY = 300; - - private static final int VIBRATE_DURATION = 300; - private static final int BEEP_DURATION = 150; - private static final int MAX_VOLUME = 100; - private static final int FREE_DELAY = 10000; - private static final int TIMEOUT_DELAY = 3000; - private static final int TIMEOUT_DELAY_SHORT = 1500; - private static final int TIMEOUT_DELAY_COLLAPSED = 4500; - private static final int TIMEOUT_DELAY_SAFETY_WARNING = 5000; - private static final int TIMEOUT_DELAY_EXPANDED = 10000; - - private static final int MSG_VOLUME_CHANGED = 0; - private static final int MSG_FREE_RESOURCES = 1; - private static final int MSG_PLAY_SOUND = 2; - private static final int MSG_STOP_SOUNDS = 3; - private static final int MSG_VIBRATE = 4; - private static final int MSG_TIMEOUT = 5; - private static final int MSG_RINGER_MODE_CHANGED = 6; - private static final int MSG_MUTE_CHANGED = 7; - private static final int MSG_REMOTE_VOLUME_CHANGED = 8; - private static final int MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN = 9; - private static final int MSG_SLIDER_VISIBILITY_CHANGED = 10; - private static final int MSG_DISPLAY_SAFE_VOLUME_WARNING = 11; - private static final int MSG_LAYOUT_DIRECTION = 12; - private static final int MSG_ZEN_MODE_AVAILABLE_CHANGED = 13; - private static final int MSG_USER_ACTIVITY = 14; - private static final int MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED = 15; - private static final int MSG_INTERNAL_RINGER_MODE_CHANGED = 16; - - // Pseudo stream type for master volume - private static final int STREAM_MASTER = -100; - // Pseudo stream type for remote volume - private static final int STREAM_REMOTE_MUSIC = -200; - - private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .build(); - - private static final int IC_AUDIO_VOL = com.android.systemui.R.drawable.ic_audio_vol; - private static final int IC_AUDIO_VOL_MUTE = com.android.systemui.R.drawable.ic_audio_vol_mute; - private static final int IC_AUDIO_BT = com.android.systemui.R.drawable.ic_audio_bt; - private static final int IC_AUDIO_BT_MUTE = com.android.systemui.R.drawable.ic_audio_bt_mute; - - private final String mTag; - protected final Context mContext; - private final AudioManager mAudioManager; - private final ZenModeController mZenController; - private boolean mRingIsSilent; - private boolean mVoiceCapable; - private boolean mZenModeAvailable; - private boolean mZenPanelExpanded; - private int mTimeoutDelay = TIMEOUT_DELAY; - private float mDisabledAlpha; - private int mLastRingerMode = AudioManager.RINGER_MODE_NORMAL; - private int mLastRingerProgress = 0; - private int mDemoIcon; - - // True if we want to play tones on the system stream when the master stream is specified. - private final boolean mPlayMasterStreamTones; - - - /** Volume panel content view */ - private final View mView; - /** Dialog hosting the panel */ - private final Dialog mDialog; - - /** The visible portion of the volume overlay */ - private final ViewGroup mPanel; - /** Contains the slider and its touchable icons */ - private final ViewGroup mSliderPanel; - /** The zen mode configuration panel view */ - private ZenModePanel mZenPanel; - /** The component currently suppressing notification stream effects */ - private ComponentName mNotificationEffectsSuppressor; - - private Callback mCallback; - - /** Currently active stream that shows up at the top of the list of sliders */ - private int mActiveStreamType = -1; - /** All the slider controls mapped by stream type */ - private SparseArray<StreamControl> mStreamControls; - private final AccessibilityManager mAccessibilityManager; - private final SecondaryIconTransition mSecondaryIconTransition; - private final IconPulser mIconPulser; - - private enum StreamResources { - BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO, - R.string.volume_icon_description_bluetooth, - IC_AUDIO_BT, - IC_AUDIO_BT_MUTE, - false), - RingerStream(AudioManager.STREAM_RING, - R.string.volume_icon_description_ringer, - com.android.systemui.R.drawable.ic_ringer_audible, - com.android.systemui.R.drawable.ic_ringer_mute, - false), - VoiceStream(AudioManager.STREAM_VOICE_CALL, - R.string.volume_icon_description_incall, - com.android.systemui.R.drawable.ic_audio_phone, - com.android.systemui.R.drawable.ic_audio_phone, - false), - AlarmStream(AudioManager.STREAM_ALARM, - R.string.volume_alarm, - com.android.systemui.R.drawable.ic_audio_alarm, - com.android.systemui.R.drawable.ic_audio_alarm_mute, - false), - MediaStream(AudioManager.STREAM_MUSIC, - R.string.volume_icon_description_media, - IC_AUDIO_VOL, - IC_AUDIO_VOL_MUTE, - true), - NotificationStream(AudioManager.STREAM_NOTIFICATION, - R.string.volume_icon_description_notification, - com.android.systemui.R.drawable.ic_ringer_audible, - com.android.systemui.R.drawable.ic_ringer_mute, - true), - // for now, use media resources for master volume - MasterStream(STREAM_MASTER, - R.string.volume_icon_description_media, //FIXME should have its own description - IC_AUDIO_VOL, - IC_AUDIO_VOL_MUTE, - false), - RemoteStream(STREAM_REMOTE_MUSIC, - R.string.volume_icon_description_media, //FIXME should have its own description - com.android.systemui.R.drawable.ic_audio_remote, - com.android.systemui.R.drawable.ic_audio_remote, - false);// will be dynamically updated - - int streamType; - int descRes; - int iconRes; - int iconMuteRes; - // RING, VOICE_CALL & BLUETOOTH_SCO are hidden unless explicitly requested - boolean show; - - StreamResources(int streamType, int descRes, int iconRes, int iconMuteRes, boolean show) { - this.streamType = streamType; - this.descRes = descRes; - this.iconRes = iconRes; - this.iconMuteRes = iconMuteRes; - this.show = show; - } - } - - // List of stream types and their order - private static final StreamResources[] STREAMS = { - StreamResources.BluetoothSCOStream, - StreamResources.RingerStream, - StreamResources.VoiceStream, - StreamResources.MediaStream, - StreamResources.NotificationStream, - StreamResources.AlarmStream, - StreamResources.MasterStream, - StreamResources.RemoteStream - }; - - /** Object that contains data for each slider */ - private class StreamControl { - int streamType; - MediaController controller; - ViewGroup group; - ImageView icon; - SeekBar seekbarView; - TextView suppressorView; - View divider; - ImageView secondaryIcon; - int iconRes; - int iconMuteRes; - int iconSuppressedRes; - } - - // Synchronize when accessing this - private ToneGenerator mToneGenerators[]; - private Vibrator mVibrator; - private boolean mHasVibrator; - - private static AlertDialog sSafetyWarning; - private static Object sSafetyWarningLock = new Object(); - - private static class SafetyWarning extends SystemUIDialog - implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener { - private final Context mContext; - private final VolumePanel mVolumePanel; - private final AudioManager mAudioManager; - - private boolean mNewVolumeUp; - - SafetyWarning(Context context, VolumePanel volumePanel, AudioManager audioManager) { - super(context); - mContext = context; - mVolumePanel = volumePanel; - mAudioManager = audioManager; - - setMessage(mContext.getString(com.android.internal.R.string.safe_media_volume_warning)); - setButton(DialogInterface.BUTTON_POSITIVE, - mContext.getString(com.android.internal.R.string.yes), this); - setButton(DialogInterface.BUTTON_NEGATIVE, - mContext.getString(com.android.internal.R.string.no), (OnClickListener) null); - setOnDismissListener(this); - - IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - context.registerReceiver(mReceiver, filter); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && event.getRepeatCount() == 0) { - mNewVolumeUp = true; - } - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mNewVolumeUp) { - if (LOGD) Log.d(TAG, "Confirmed warning via VOLUME_UP"); - mAudioManager.disableSafeMediaVolume(); - dismiss(); - } - return super.onKeyUp(keyCode, event); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - mAudioManager.disableSafeMediaVolume(); - } - - @Override - public void onDismiss(DialogInterface unused) { - mContext.unregisterReceiver(mReceiver); - cleanUp(); - } - - private void cleanUp() { - synchronized (sSafetyWarningLock) { - sSafetyWarning = null; - } - mVolumePanel.forceTimeout(0); - mVolumePanel.updateStates(); - } - - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { - if (LOGD) Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS"); - cancel(); - cleanUp(); - } - } - }; - } - - public VolumePanel(Context context, ZenModeController zenController) { - mTag = String.format("%s.%08x", TAG, hashCode()); - mContext = context; - mZenController = zenController; - mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - mAccessibilityManager = (AccessibilityManager) context.getSystemService( - Context.ACCESSIBILITY_SERVICE); - mSecondaryIconTransition = new SecondaryIconTransition(); - mIconPulser = new IconPulser(context); - - // For now, only show master volume if master volume is supported - final Resources res = context.getResources(); - final boolean useMasterVolume = res.getBoolean(R.bool.config_useMasterVolume); - if (useMasterVolume) { - for (int i = 0; i < STREAMS.length; i++) { - StreamResources streamRes = STREAMS[i]; - streamRes.show = (streamRes.streamType == STREAM_MASTER); - } - } - if (LOGD) Log.d(mTag, "new VolumePanel"); - - mDisabledAlpha = 0.5f; - if (mContext.getTheme() != null) { - final TypedArray arr = mContext.getTheme().obtainStyledAttributes( - new int[] { android.R.attr.disabledAlpha }); - mDisabledAlpha = arr.getFloat(0, mDisabledAlpha); - arr.recycle(); - } - - mDialog = new Dialog(context) { - @Override - public boolean onTouchEvent(MotionEvent event) { - if (isShowing() && event.getAction() == MotionEvent.ACTION_OUTSIDE && - sSafetyWarning == null) { - forceTimeout(0); - return true; - } - return false; - } - }; - - final Window window = mDialog.getWindow(); - window.requestFeature(Window.FEATURE_NO_TITLE); - mDialog.setCanceledOnTouchOutside(true); - mDialog.setContentView(com.android.systemui.R.layout.volume_dialog); - mDialog.setOnDismissListener(new OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - mActiveStreamType = -1; - mAudioManager.forceVolumeControlStream(mActiveStreamType); - setZenPanelVisible(false); - mDemoIcon = 0; - mSecondaryIconTransition.cancel(); - } - }); - - mDialog.create(); - - final LayoutParams lp = window.getAttributes(); - lp.token = null; - lp.y = res.getDimensionPixelOffset(com.android.systemui.R.dimen.volume_panel_top); - lp.type = LayoutParams.TYPE_STATUS_BAR_PANEL; - lp.format = PixelFormat.TRANSLUCENT; - lp.windowAnimations = com.android.systemui.R.style.VolumePanelAnimation; - lp.setTitle(TAG); - window.setAttributes(lp); - - updateWidth(); - - window.setBackgroundDrawable(new ColorDrawable(0x00000000)); - window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); - window.addFlags(LayoutParams.FLAG_NOT_FOCUSABLE - | LayoutParams.FLAG_NOT_TOUCH_MODAL - | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH - | LayoutParams.FLAG_HARDWARE_ACCELERATED); - mView = window.findViewById(R.id.content); - Interaction.register(mView, new Interaction.Callback() { - @Override - public void onInteraction() { - resetTimeout(); - } - }); - - mPanel = (ViewGroup) mView.findViewById(com.android.systemui.R.id.visible_panel); - mSliderPanel = (ViewGroup) mView.findViewById(com.android.systemui.R.id.slider_panel); - mZenPanel = (ZenModePanel) mView.findViewById(com.android.systemui.R.id.zen_mode_panel); - initZenModePanel(); - - mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; - mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); - mHasVibrator = mVibrator != null && mVibrator.hasVibrator(); - mVoiceCapable = context.getResources().getBoolean(R.bool.config_voice_capable); - - if (mZenController != null && !useMasterVolume) { - mZenModeAvailable = mZenController.isZenAvailable(); - mNotificationEffectsSuppressor = mZenController.getEffectsSuppressor(); - mZenController.addCallback(mZenCallback); - } - - final boolean masterVolumeOnly = res.getBoolean(R.bool.config_useMasterVolume); - final boolean masterVolumeKeySounds = res.getBoolean(R.bool.config_useVolumeKeySounds); - mPlayMasterStreamTones = masterVolumeOnly && masterVolumeKeySounds; - - registerReceiver(); - } - - public void onConfigurationChanged(Configuration newConfig) { - updateWidth(); - if (mZenPanel != null) { - mZenPanel.updateLocale(); - } - } - - private void updateWidth() { - final Resources res = mContext.getResources(); - final LayoutParams lp = mDialog.getWindow().getAttributes(); - lp.width = res.getDimensionPixelSize(com.android.systemui.R.dimen.notification_panel_width); - lp.gravity = - res.getInteger(com.android.systemui.R.integer.notification_panel_layout_gravity); - mDialog.getWindow().setAttributes(lp); - } - - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("VolumePanel state:"); - pw.print(" mTag="); pw.println(mTag); - pw.print(" mRingIsSilent="); pw.println(mRingIsSilent); - pw.print(" mVoiceCapable="); pw.println(mVoiceCapable); - pw.print(" mHasVibrator="); pw.println(mHasVibrator); - pw.print(" mZenModeAvailable="); pw.println(mZenModeAvailable); - pw.print(" mZenPanelExpanded="); pw.println(mZenPanelExpanded); - pw.print(" mNotificationEffectsSuppressor="); pw.println(mNotificationEffectsSuppressor); - pw.print(" mTimeoutDelay="); pw.println(mTimeoutDelay); - pw.print(" mDisabledAlpha="); pw.println(mDisabledAlpha); - pw.print(" mLastRingerMode="); pw.println(mLastRingerMode); - pw.print(" mLastRingerProgress="); pw.println(mLastRingerProgress); - pw.print(" mPlayMasterStreamTones="); pw.println(mPlayMasterStreamTones); - pw.print(" isShowing()="); pw.println(isShowing()); - pw.print(" mCallback="); pw.println(mCallback); - pw.print(" sConfirmSafeVolumeDialog="); - pw.println(sSafetyWarning != null ? "<not null>" : null); - pw.print(" mActiveStreamType="); pw.println(mActiveStreamType); - pw.print(" mStreamControls="); - if (mStreamControls == null) { - pw.println("null"); - } else { - final int N = mStreamControls.size(); - pw.print("<size "); pw.print(N); pw.println('>'); - for (int i = 0; i < N; i++) { - final StreamControl sc = mStreamControls.valueAt(i); - pw.print(" stream "); pw.print(sc.streamType); pw.print(":"); - if (sc.seekbarView != null) { - pw.print(" progress="); pw.print(sc.seekbarView.getProgress()); - pw.print(" of "); pw.print(sc.seekbarView.getMax()); - if (!sc.seekbarView.isEnabled()) pw.print(" (disabled)"); - } - if (sc.icon != null && sc.icon.isClickable()) pw.print(" (clickable)"); - pw.println(); - } - } - if (mZenPanel != null) { - mZenPanel.dump(fd, pw, args); - } - } - - private void initZenModePanel() { - mZenPanel.init(mZenController); - mZenPanel.setCallback(new ZenModePanel.Callback() { - @Override - public void onMoreSettings() { - if (mCallback != null) { - mCallback.onZenSettings(); - } - } - - @Override - public void onInteraction() { - resetTimeout(); - } - - @Override - public void onExpanded(boolean expanded) { - if (mZenPanelExpanded == expanded) return; - mZenPanelExpanded = expanded; - updateTimeoutDelay(); - resetTimeout(); - } - }); - } - - private void setLayoutDirection(int layoutDirection) { - mPanel.setLayoutDirection(layoutDirection); - updateStates(); - } - - private void registerReceiver() { - final IntentFilter filter = new IntentFilter(); - filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); - filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); - filter.addAction(Intent.ACTION_SCREEN_OFF); - mContext.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - - if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { - removeMessages(MSG_RINGER_MODE_CHANGED); - sendEmptyMessage(MSG_RINGER_MODE_CHANGED); - } - - if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) { - removeMessages(MSG_INTERNAL_RINGER_MODE_CHANGED); - sendEmptyMessage(MSG_INTERNAL_RINGER_MODE_CHANGED); - } - - if (Intent.ACTION_SCREEN_OFF.equals(action)) { - postDismiss(0); - } - } - }, filter); - } - - private boolean isMuted(int streamType) { - if (streamType == STREAM_MASTER) { - return mAudioManager.isMasterMute(); - } else if (streamType == STREAM_REMOTE_MUSIC) { - // TODO do we need to support a distinct mute property for remote? - return false; - } else { - return mAudioManager.isStreamMute(streamType); - } - } - - private int getStreamMaxVolume(int streamType) { - if (streamType == STREAM_MASTER) { - return mAudioManager.getMasterMaxVolume(); - } else if (streamType == STREAM_REMOTE_MUSIC) { - if (mStreamControls != null) { - StreamControl sc = mStreamControls.get(streamType); - if (sc != null && sc.controller != null) { - PlaybackInfo ai = sc.controller.getPlaybackInfo(); - return ai.getMaxVolume(); - } - } - return -1; - } else { - return mAudioManager.getStreamMaxVolume(streamType); - } - } - - private int getStreamVolume(int streamType) { - if (streamType == STREAM_MASTER) { - return mAudioManager.getMasterVolume(); - } else if (streamType == STREAM_REMOTE_MUSIC) { - if (mStreamControls != null) { - StreamControl sc = mStreamControls.get(streamType); - if (sc != null && sc.controller != null) { - PlaybackInfo ai = sc.controller.getPlaybackInfo(); - return ai.getCurrentVolume(); - } - } - return -1; - } else { - return mAudioManager.getStreamVolume(streamType); - } - } - - private void setStreamVolume(StreamControl sc, int index, int flags) { - if (sc.streamType == STREAM_REMOTE_MUSIC) { - if (sc.controller != null) { - sc.controller.setVolumeTo(index, flags); - } else { - Log.w(mTag, "Adjusting remote volume without a controller!"); - } - } else if (getStreamVolume(sc.streamType) != index) { - if (sc.streamType == STREAM_MASTER) { - mAudioManager.setMasterVolume(index, flags); - } else { - mAudioManager.setStreamVolume(sc.streamType, index, flags); - } - } - } - - private void createSliders() { - final Resources res = mContext.getResources(); - final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - - mStreamControls = new SparseArray<StreamControl>(STREAMS.length); - - final StreamResources notificationStream = StreamResources.NotificationStream; - for (int i = 0; i < STREAMS.length; i++) { - StreamResources streamRes = STREAMS[i]; - - final int streamType = streamRes.streamType; - final boolean isNotification = isNotificationOrRing(streamType); - - final StreamControl sc = new StreamControl(); - sc.streamType = streamType; - sc.group = (ViewGroup) inflater.inflate( - com.android.systemui.R.layout.volume_panel_item, null); - sc.group.setTag(sc); - sc.icon = (ImageView) sc.group.findViewById(com.android.systemui.R.id.stream_icon); - sc.icon.setTag(sc); - sc.icon.setContentDescription(res.getString(streamRes.descRes)); - sc.iconRes = streamRes.iconRes; - sc.iconMuteRes = streamRes.iconMuteRes; - sc.icon.setImageResource(sc.iconRes); - sc.icon.setClickable(isNotification && mHasVibrator); - if (isNotification) { - if (mHasVibrator) { - sc.icon.setSoundEffectsEnabled(false); - sc.iconMuteRes = com.android.systemui.R.drawable.ic_ringer_vibrate; - sc.icon.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - resetTimeout(); - toggleRinger(sc); - } - }); - } - sc.iconSuppressedRes = com.android.systemui.R.drawable.ic_ringer_mute; - } - sc.seekbarView = (SeekBar) sc.group.findViewById(com.android.systemui.R.id.seekbar); - sc.suppressorView = - (TextView) sc.group.findViewById(com.android.systemui.R.id.suppressor); - sc.suppressorView.setVisibility(View.GONE); - final boolean showSecondary = !isNotification && notificationStream.show; - sc.divider = sc.group.findViewById(com.android.systemui.R.id.divider); - sc.secondaryIcon = (ImageView) sc.group - .findViewById(com.android.systemui.R.id.secondary_icon); - sc.secondaryIcon.setImageResource(com.android.systemui.R.drawable.ic_ringer_audible); - sc.secondaryIcon.setContentDescription(res.getString(notificationStream.descRes)); - sc.secondaryIcon.setClickable(showSecondary); - sc.divider.setVisibility(showSecondary ? View.VISIBLE : View.GONE); - sc.secondaryIcon.setVisibility(showSecondary ? View.VISIBLE : View.GONE); - if (showSecondary) { - sc.secondaryIcon.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mSecondaryIconTransition.start(sc); - } - }); - } - final int plusOne = (streamType == AudioSystem.STREAM_BLUETOOTH_SCO || - streamType == AudioSystem.STREAM_VOICE_CALL) ? 1 : 0; - sc.seekbarView.setMax(getStreamMaxVolume(streamType) + plusOne); - sc.seekbarView.setOnSeekBarChangeListener(mSeekListener); - sc.seekbarView.setTag(sc); - mStreamControls.put(streamType, sc); - } - } - - private void toggleRinger(StreamControl sc) { - if (!mHasVibrator) return; - if (mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_NORMAL) { - mAudioManager.setRingerModeInternal(AudioManager.RINGER_MODE_VIBRATE); - postVolumeChanged(sc.streamType, AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE); - } else { - mAudioManager.setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL); - postVolumeChanged(sc.streamType, AudioManager.FLAG_PLAY_SOUND); - } - } - - private void reorderSliders(int activeStreamType) { - mSliderPanel.removeAllViews(); - - final StreamControl active = mStreamControls.get(activeStreamType); - if (active == null) { - Log.e(TAG, "Missing stream type! - " + activeStreamType); - mActiveStreamType = -1; - } else { - mSliderPanel.addView(active.group); - mActiveStreamType = activeStreamType; - active.group.setVisibility(View.VISIBLE); - updateSlider(active, true /*forceReloadIcon*/); - updateTimeoutDelay(); - updateZenPanelVisible(); - } - } - - private void updateSliderProgress(StreamControl sc, int progress) { - final boolean isRinger = isNotificationOrRing(sc.streamType); - if (isRinger && mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) { - progress = mLastRingerProgress; - } - if (progress < 0) { - progress = getStreamVolume(sc.streamType); - } - sc.seekbarView.setProgress(progress); - if (isRinger) { - mLastRingerProgress = progress; - } - } - - private void updateSliderIcon(StreamControl sc, boolean muted) { - ComponentName suppressor = null; - if (isNotificationOrRing(sc.streamType)) { - suppressor = mNotificationEffectsSuppressor; - int ringerMode = mAudioManager.getRingerModeInternal(); - if (ringerMode == AudioManager.RINGER_MODE_SILENT) { - ringerMode = mLastRingerMode; - } else { - mLastRingerMode = ringerMode; - } - if (mHasVibrator) { - muted = ringerMode == AudioManager.RINGER_MODE_VIBRATE; - } else { - muted = false; - } - } - sc.icon.setImageResource(mDemoIcon != 0 ? mDemoIcon - : suppressor != null ? sc.iconSuppressedRes - : muted ? sc.iconMuteRes - : sc.iconRes); - } - - private void updateSliderSuppressor(StreamControl sc) { - final ComponentName suppressor = isNotificationOrRing(sc.streamType) - ? mNotificationEffectsSuppressor : null; - if (suppressor == null) { - sc.seekbarView.setVisibility(View.VISIBLE); - sc.suppressorView.setVisibility(View.GONE); - } else { - sc.seekbarView.setVisibility(View.GONE); - sc.suppressorView.setVisibility(View.VISIBLE); - sc.suppressorView.setText(mContext.getString(R.string.muted_by, - getSuppressorCaption(suppressor))); - } - } - - private String getSuppressorCaption(ComponentName suppressor) { - final PackageManager pm = mContext.getPackageManager(); - try { - final ServiceInfo info = pm.getServiceInfo(suppressor, 0); - if (info != null) { - final CharSequence seq = info.loadLabel(pm); - if (seq != null) { - final String str = seq.toString().trim(); - if (str.length() > 0) { - return str; - } - } - } - } catch (Throwable e) { - Log.w(TAG, "Error loading suppressor caption", e); - } - return suppressor.getPackageName(); - } - - /** Update the mute and progress state of a slider */ - private void updateSlider(StreamControl sc, boolean forceReloadIcon) { - updateSliderProgress(sc, -1); - final boolean muted = isMuted(sc.streamType); - if (forceReloadIcon) { - sc.icon.setImageDrawable(null); - } - updateSliderIcon(sc, muted); - updateSliderEnabled(sc, muted, false); - updateSliderSuppressor(sc); - } - - private void updateSliderEnabled(final StreamControl sc, boolean muted, boolean fixedVolume) { - final boolean wasEnabled = sc.seekbarView.isEnabled(); - final boolean isRinger = isNotificationOrRing(sc.streamType); - if (sc.streamType == STREAM_REMOTE_MUSIC) { - // never disable touch interactions for remote playback, the muting is not tied to - // the state of the phone. - sc.seekbarView.setEnabled(!fixedVolume); - } else if (isRinger && mNotificationEffectsSuppressor != null) { - sc.icon.setEnabled(true); - sc.icon.setAlpha(1f); - sc.icon.setClickable(false); - } else if (isRinger - && mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) { - sc.seekbarView.setEnabled(false); - sc.icon.setEnabled(false); - sc.icon.setAlpha(mDisabledAlpha); - sc.icon.setClickable(false); - } else if (fixedVolume || - (sc.streamType != mAudioManager.getMasterStreamType() && !isRinger && muted) || - (sSafetyWarning != null)) { - sc.seekbarView.setEnabled(false); - } else { - sc.seekbarView.setEnabled(true); - sc.icon.setEnabled(true); - sc.icon.setAlpha(1f); - } - // show the silent hint when the disabled slider is touched in silent mode - if (isRinger && wasEnabled != sc.seekbarView.isEnabled()) { - if (sc.seekbarView.isEnabled()) { - sc.group.setOnTouchListener(null); - sc.icon.setClickable(mHasVibrator); - } else { - final View.OnTouchListener showHintOnTouch = new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - resetTimeout(); - showSilentHint(); - return false; - } - }; - sc.group.setOnTouchListener(showHintOnTouch); - } - } - } - - private void showSilentHint() { - if (mZenPanel != null) { - mZenPanel.showSilentHint(); - } - } - - private void showVibrateHint() { - final StreamControl active = mStreamControls.get(mActiveStreamType); - if (active != null) { - mIconPulser.start(active.icon); - if (!hasMessages(MSG_VIBRATE)) { - sendEmptyMessageDelayed(MSG_VIBRATE, VIBRATE_DELAY); - } - } - } - - private static boolean isNotificationOrRing(int streamType) { - return streamType == AudioManager.STREAM_RING - || streamType == AudioManager.STREAM_NOTIFICATION; - } - - public void setCallback(Callback callback) { - mCallback = callback; - } - - private void updateTimeoutDelay() { - mTimeoutDelay = mDemoIcon != 0 ? TIMEOUT_DELAY_EXPANDED - : sSafetyWarning != null ? TIMEOUT_DELAY_SAFETY_WARNING - : mActiveStreamType == AudioManager.STREAM_MUSIC ? TIMEOUT_DELAY_SHORT - : mZenPanelExpanded ? TIMEOUT_DELAY_EXPANDED - : isZenPanelVisible() ? TIMEOUT_DELAY_COLLAPSED - : TIMEOUT_DELAY; - } - - private boolean isZenPanelVisible() { - return mZenPanel != null && mZenPanel.getVisibility() == View.VISIBLE; - } - - private void setZenPanelVisible(boolean visible) { - if (LOGD) Log.d(mTag, "setZenPanelVisible " + visible + " mZenPanel=" + mZenPanel); - final boolean changing = visible != isZenPanelVisible(); - if (visible) { - mZenPanel.setHidden(false); - resetTimeout(); - } else { - mZenPanel.setHidden(true); - } - if (changing) { - updateTimeoutDelay(); - resetTimeout(); - } - } - - private void updateStates() { - final int count = mSliderPanel.getChildCount(); - for (int i = 0; i < count; i++) { - StreamControl sc = (StreamControl) mSliderPanel.getChildAt(i).getTag(); - updateSlider(sc, true /*forceReloadIcon*/); - } - } - - private void updateActiveSlider() { - final StreamControl active = mStreamControls.get(mActiveStreamType); - if (active != null) { - updateSlider(active, false /*forceReloadIcon*/); - } - } - - private void updateZenPanelVisible() { - setZenPanelVisible(mZenModeAvailable && isNotificationOrRing(mActiveStreamType)); - } - - public void postVolumeChanged(int streamType, int flags) { - if (hasMessages(MSG_VOLUME_CHANGED)) return; - synchronized (this) { - if (mStreamControls == null) { - createSliders(); - } - } - removeMessages(MSG_FREE_RESOURCES); - obtainMessage(MSG_VOLUME_CHANGED, streamType, flags).sendToTarget(); - } - - public void postRemoteVolumeChanged(MediaController controller, int flags) { - if (hasMessages(MSG_REMOTE_VOLUME_CHANGED)) return; - synchronized (this) { - if (mStreamControls == null) { - createSliders(); - } - } - removeMessages(MSG_FREE_RESOURCES); - obtainMessage(MSG_REMOTE_VOLUME_CHANGED, flags, 0, controller).sendToTarget(); - } - - public void postRemoteSliderVisibility(boolean visible) { - obtainMessage(MSG_SLIDER_VISIBILITY_CHANGED, - STREAM_REMOTE_MUSIC, visible ? 1 : 0).sendToTarget(); - } - - /** - * Called by AudioService when it has received new remote playback information that - * would affect the VolumePanel display (mainly volumes). The difference with - * {@link #postRemoteVolumeChanged(int, int)} is that the handling of the posted message - * (MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN) will only update the volume slider if it is being - * displayed. - * This special code path is due to the fact that remote volume updates arrive to AudioService - * asynchronously. So after AudioService has sent the volume update (which should be treated - * as a request to update the volume), the application will likely set a new volume. If the UI - * is still up, we need to refresh the display to show this new value. - */ - public void postHasNewRemotePlaybackInfo() { - if (hasMessages(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN)) return; - // don't create or prevent resources to be freed, if they disappear, this update came too - // late and shouldn't warrant the panel to be displayed longer - obtainMessage(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN).sendToTarget(); - } - - public void postMasterVolumeChanged(int flags) { - postVolumeChanged(STREAM_MASTER, flags); - } - - public void postMuteChanged(int streamType, int flags) { - if (hasMessages(MSG_VOLUME_CHANGED)) return; - synchronized (this) { - if (mStreamControls == null) { - createSliders(); - } - } - removeMessages(MSG_FREE_RESOURCES); - obtainMessage(MSG_MUTE_CHANGED, streamType, flags).sendToTarget(); - } - - public void postMasterMuteChanged(int flags) { - postMuteChanged(STREAM_MASTER, flags); - } - - public void postDisplaySafeVolumeWarning(int flags) { - if (hasMessages(MSG_DISPLAY_SAFE_VOLUME_WARNING)) return; - obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, flags, 0).sendToTarget(); - } - - public void postDismiss(long delay) { - forceTimeout(delay); - } - - public void postLayoutDirection(int layoutDirection) { - removeMessages(MSG_LAYOUT_DIRECTION); - obtainMessage(MSG_LAYOUT_DIRECTION, layoutDirection, 0).sendToTarget(); - } - - private static String flagsToString(int flags) { - return flags == 0 ? "0" : (flags + "=" + AudioManager.flagsToString(flags)); - } - - private static String streamToString(int stream) { - return AudioService.streamToString(stream); - } - - /** - * Override this if you have other work to do when the volume changes (for - * example, vibrating, playing a sound, etc.). Make sure to call through to - * the superclass implementation. - */ - protected void onVolumeChanged(int streamType, int flags) { - - if (LOGD) Log.d(mTag, "onVolumeChanged(streamType: " + streamToString(streamType) - + ", flags: " + flagsToString(flags) + ")"); - - if ((flags & AudioManager.FLAG_SHOW_UI) != 0) { - synchronized (this) { - if (mActiveStreamType != streamType) { - reorderSliders(streamType); - } - onShowVolumeChanged(streamType, flags, null); - } - } - - if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) { - removeMessages(MSG_PLAY_SOUND); - sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY); - } - - if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) { - removeMessages(MSG_PLAY_SOUND); - removeMessages(MSG_VIBRATE); - onStopSounds(); - } - - removeMessages(MSG_FREE_RESOURCES); - sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); - resetTimeout(); - } - - protected void onMuteChanged(int streamType, int flags) { - - if (LOGD) Log.d(mTag, "onMuteChanged(streamType: " + streamToString(streamType) - + ", flags: " + flagsToString(flags) + ")"); - - StreamControl sc = mStreamControls.get(streamType); - if (sc != null) { - updateSliderIcon(sc, isMuted(sc.streamType)); - } - - onVolumeChanged(streamType, flags); - } - - protected void onShowVolumeChanged(int streamType, int flags, MediaController controller) { - int index = getStreamVolume(streamType); - - mRingIsSilent = false; - - if (LOGD) { - Log.d(mTag, "onShowVolumeChanged(streamType: " + streamToString(streamType) - + ", flags: " + flagsToString(flags) + "), index: " + index); - } - - // get max volume for progress bar - - int max = getStreamMaxVolume(streamType); - StreamControl sc = mStreamControls.get(streamType); - - switch (streamType) { - - case AudioManager.STREAM_RING: { - Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( - mContext, RingtoneManager.TYPE_RINGTONE); - if (ringuri == null) { - mRingIsSilent = true; - } - break; - } - - case AudioManager.STREAM_MUSIC: { - // Special case for when Bluetooth is active for music - if ((mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC) & - (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP | - AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | - AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) { - setMusicIcon(IC_AUDIO_BT, IC_AUDIO_BT_MUTE); - } else { - setMusicIcon(IC_AUDIO_VOL, IC_AUDIO_VOL_MUTE); - } - break; - } - - case AudioManager.STREAM_VOICE_CALL: { - /* - * For in-call voice call volume, there is no inaudible volume. - * Rescale the UI control so the progress bar doesn't go all - * the way to zero and don't show the mute icon. - */ - index++; - max++; - break; - } - - case AudioManager.STREAM_ALARM: { - break; - } - - case AudioManager.STREAM_NOTIFICATION: { - Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( - mContext, RingtoneManager.TYPE_NOTIFICATION); - if (ringuri == null) { - mRingIsSilent = true; - } - break; - } - - case AudioManager.STREAM_BLUETOOTH_SCO: { - /* - * For in-call voice call volume, there is no inaudible volume. - * Rescale the UI control so the progress bar doesn't go all - * the way to zero and don't show the mute icon. - */ - index++; - max++; - break; - } - - case STREAM_REMOTE_MUSIC: { - if (controller == null && sc != null) { - // If we weren't passed one try using the last one set. - controller = sc.controller; - } - if (controller == null) { - // We still don't have one, ignore the command. - Log.w(mTag, "sent remote volume change without a controller!"); - } else { - PlaybackInfo vi = controller.getPlaybackInfo(); - index = vi.getCurrentVolume(); - max = vi.getMaxVolume(); - if ((vi.getVolumeControl() & VolumeProvider.VOLUME_CONTROL_FIXED) != 0) { - // if the remote volume is fixed add the flag for the UI - flags |= AudioManager.FLAG_FIXED_VOLUME; - } - } - if (LOGD) { Log.d(mTag, "showing remote volume "+index+" over "+ max); } - break; - } - } - - if (sc != null) { - if (streamType == STREAM_REMOTE_MUSIC && controller != sc.controller) { - if (sc.controller != null) { - sc.controller.unregisterCallback(mMediaControllerCb); - } - sc.controller = controller; - if (controller != null) { - sc.controller.registerCallback(mMediaControllerCb); - } - } - if (sc.seekbarView.getMax() != max) { - sc.seekbarView.setMax(max); - } - updateSliderProgress(sc, index); - final boolean muted = isMuted(streamType); - updateSliderEnabled(sc, muted, (flags & AudioManager.FLAG_FIXED_VOLUME) != 0); - if (isNotificationOrRing(streamType)) { - // check for secondary-icon transition completion - if (mSecondaryIconTransition.isRunning()) { - mSecondaryIconTransition.cancel(); // safe to reset - sc.seekbarView.setAlpha(0); sc.seekbarView.animate().alpha(1); - mZenPanel.setAlpha(0); mZenPanel.animate().alpha(1); - } - updateSliderIcon(sc, muted); - } - } - - if (!isShowing()) { - int stream = (streamType == STREAM_REMOTE_MUSIC) ? -1 : streamType; - // when the stream is for remote playback, use -1 to reset the stream type evaluation - if (stream != STREAM_MASTER) { - mAudioManager.forceVolumeControlStream(stream); - } - mDialog.show(); - if (mCallback != null) { - mCallback.onVisible(true); - } - announceDialogShown(); - } - - // Do a little vibrate if applicable (only when going into vibrate mode) - if ((streamType != STREAM_REMOTE_MUSIC) && - ((flags & AudioManager.FLAG_VIBRATE) != 0) && - isNotificationOrRing(streamType) && - mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) { - sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY); - } - - // Pulse the zen icon if an adjustment was suppressed due to silent mode. - if ((flags & AudioManager.FLAG_SHOW_SILENT_HINT) != 0) { - showSilentHint(); - } - - // Pulse the slider icon & vibrate if an adjustment down was suppressed due to vibrate mode. - if ((flags & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0) { - showVibrateHint(); - } - } - - private void announceDialogShown() { - mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - } - - private boolean isShowing() { - return mDialog.isShowing(); - } - - protected void onPlaySound(int streamType, int flags) { - - if (hasMessages(MSG_STOP_SOUNDS)) { - removeMessages(MSG_STOP_SOUNDS); - // Force stop right now - onStopSounds(); - } - - synchronized (this) { - ToneGenerator toneGen = getOrCreateToneGenerator(streamType); - if (toneGen != null) { - toneGen.startTone(ToneGenerator.TONE_PROP_BEEP); - sendMessageDelayed(obtainMessage(MSG_STOP_SOUNDS), BEEP_DURATION); - } - } - } - - protected void onStopSounds() { - - synchronized (this) { - int numStreamTypes = AudioSystem.getNumStreamTypes(); - for (int i = numStreamTypes - 1; i >= 0; i--) { - ToneGenerator toneGen = mToneGenerators[i]; - if (toneGen != null) { - toneGen.stopTone(); - } - } - } - } - - protected void onVibrate() { - - // Make sure we ended up in vibrate ringer mode - if (mAudioManager.getRingerModeInternal() != AudioManager.RINGER_MODE_VIBRATE) { - return; - } - if (mVibrator != null) { - mVibrator.vibrate(VIBRATE_DURATION, VIBRATION_ATTRIBUTES); - } - } - - protected void onRemoteVolumeChanged(MediaController controller, int flags) { - if (LOGD) Log.d(mTag, "onRemoteVolumeChanged(controller:" + controller + ", flags: " - + flagsToString(flags) + ")"); - - if (((flags & AudioManager.FLAG_SHOW_UI) != 0) || isShowing()) { - synchronized (this) { - if (mActiveStreamType != STREAM_REMOTE_MUSIC) { - reorderSliders(STREAM_REMOTE_MUSIC); - } - onShowVolumeChanged(STREAM_REMOTE_MUSIC, flags, controller); - } - } else { - if (LOGD) Log.d(mTag, "not calling onShowVolumeChanged(), no FLAG_SHOW_UI or no UI"); - } - - removeMessages(MSG_FREE_RESOURCES); - sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); - resetTimeout(); - } - - protected void onRemoteVolumeUpdateIfShown() { - if (LOGD) Log.d(mTag, "onRemoteVolumeUpdateIfShown()"); - if (isShowing() - && (mActiveStreamType == STREAM_REMOTE_MUSIC) - && (mStreamControls != null)) { - onShowVolumeChanged(STREAM_REMOTE_MUSIC, 0, null); - } - } - - /** - * Clear the current remote stream controller. - */ - private void clearRemoteStreamController() { - if (mStreamControls != null) { - StreamControl sc = mStreamControls.get(STREAM_REMOTE_MUSIC); - if (sc != null) { - if (sc.controller != null) { - sc.controller.unregisterCallback(mMediaControllerCb); - sc.controller = null; - } - } - } - } - - /** - * Handler for MSG_SLIDER_VISIBILITY_CHANGED Hide or show a slider - * - * @param streamType can be a valid stream type value, or - * VolumePanel.STREAM_MASTER, or VolumePanel.STREAM_REMOTE_MUSIC - * @param visible - */ - synchronized protected void onSliderVisibilityChanged(int streamType, int visible) { - if (LOGD) Log.d(mTag, "onSliderVisibilityChanged(stream="+streamType+", visi="+visible+")"); - boolean isVisible = (visible == 1); - for (int i = STREAMS.length - 1 ; i >= 0 ; i--) { - StreamResources streamRes = STREAMS[i]; - if (streamRes.streamType == streamType) { - streamRes.show = isVisible; - if (!isVisible && (mActiveStreamType == streamType)) { - mActiveStreamType = -1; - } - break; - } - } - } - - protected void onDisplaySafeVolumeWarning(int flags) { - if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0 - || isShowing()) { - synchronized (sSafetyWarningLock) { - if (sSafetyWarning != null) { - return; - } - sSafetyWarning = new SafetyWarning(mContext, this, mAudioManager); - sSafetyWarning.show(); - } - updateStates(); - } - if (mAccessibilityManager.isTouchExplorationEnabled()) { - removeMessages(MSG_TIMEOUT); - } else { - updateTimeoutDelay(); - resetTimeout(); - } - } - - /** - * Lock on this VolumePanel instance as long as you use the returned ToneGenerator. - */ - private ToneGenerator getOrCreateToneGenerator(int streamType) { - if (streamType == STREAM_MASTER) { - // For devices that use the master volume setting only but still want to - // play a volume-changed tone, direct the master volume pseudostream to - // the system stream's tone generator. - if (mPlayMasterStreamTones) { - streamType = AudioManager.STREAM_SYSTEM; - } else { - return null; - } - } - synchronized (this) { - if (mToneGenerators[streamType] == null) { - try { - mToneGenerators[streamType] = new ToneGenerator(streamType, MAX_VOLUME); - } catch (RuntimeException e) { - if (LOGD) { - Log.d(mTag, "ToneGenerator constructor failed with " - + "RuntimeException: " + e); - } - } - } - return mToneGenerators[streamType]; - } - } - - - /** - * Switch between icons because Bluetooth music is same as music volume, but with - * different icons. - */ - private void setMusicIcon(int resId, int resMuteId) { - StreamControl sc = mStreamControls.get(AudioManager.STREAM_MUSIC); - if (sc != null) { - sc.iconRes = resId; - sc.iconMuteRes = resMuteId; - updateSliderIcon(sc, isMuted(sc.streamType)); - } - } - - protected void onFreeResources() { - synchronized (this) { - for (int i = mToneGenerators.length - 1; i >= 0; i--) { - if (mToneGenerators[i] != null) { - mToneGenerators[i].release(); - } - mToneGenerators[i] = null; - } - } - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - - case MSG_VOLUME_CHANGED: { - onVolumeChanged(msg.arg1, msg.arg2); - break; - } - - case MSG_MUTE_CHANGED: { - onMuteChanged(msg.arg1, msg.arg2); - break; - } - - case MSG_FREE_RESOURCES: { - onFreeResources(); - break; - } - - case MSG_STOP_SOUNDS: { - onStopSounds(); - break; - } - - case MSG_PLAY_SOUND: { - onPlaySound(msg.arg1, msg.arg2); - break; - } - - case MSG_VIBRATE: { - onVibrate(); - break; - } - - case MSG_TIMEOUT: { - if (isShowing()) { - mDialog.dismiss(); - clearRemoteStreamController(); - mActiveStreamType = -1; - if (mCallback != null) { - mCallback.onVisible(false); - } - } - synchronized (sSafetyWarningLock) { - if (sSafetyWarning != null) { - if (LOGD) Log.d(mTag, "SafetyWarning timeout"); - sSafetyWarning.dismiss(); - } - } - break; - } - - case MSG_RINGER_MODE_CHANGED: - case MSG_INTERNAL_RINGER_MODE_CHANGED: - case MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED: { - if (isShowing()) { - updateActiveSlider(); - } - break; - } - - case MSG_REMOTE_VOLUME_CHANGED: { - onRemoteVolumeChanged((MediaController) msg.obj, msg.arg1); - break; - } - - case MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN: - onRemoteVolumeUpdateIfShown(); - break; - - case MSG_SLIDER_VISIBILITY_CHANGED: - onSliderVisibilityChanged(msg.arg1, msg.arg2); - break; - - case MSG_DISPLAY_SAFE_VOLUME_WARNING: - onDisplaySafeVolumeWarning(msg.arg1); - break; - - case MSG_LAYOUT_DIRECTION: - setLayoutDirection(msg.arg1); - break; - - case MSG_ZEN_MODE_AVAILABLE_CHANGED: - mZenModeAvailable = msg.arg1 != 0; - updateZenPanelVisible(); - break; - - case MSG_USER_ACTIVITY: - if (mCallback != null) { - mCallback.onInteraction(); - } - break; - } - } - - private void resetTimeout() { - final boolean touchExploration = mAccessibilityManager.isTouchExplorationEnabled(); - if (LOGD) Log.d(mTag, "resetTimeout at " + System.currentTimeMillis() - + " delay=" + mTimeoutDelay + " touchExploration=" + touchExploration); - if (sSafetyWarning == null || !touchExploration) { - removeMessages(MSG_TIMEOUT); - sendEmptyMessageDelayed(MSG_TIMEOUT, mTimeoutDelay); - removeMessages(MSG_USER_ACTIVITY); - sendEmptyMessage(MSG_USER_ACTIVITY); - } - } - - private void forceTimeout(long delay) { - if (LOGD) Log.d(mTag, "forceTimeout delay=" + delay + " callers=" + Debug.getCallers(3)); - removeMessages(MSG_TIMEOUT); - sendEmptyMessageDelayed(MSG_TIMEOUT, delay); - } - - public ZenModeController getZenController() { - return mZenController; - } - - @Override - public void dispatchDemoCommand(String command, Bundle args) { - if (!COMMAND_VOLUME.equals(command)) return; - String icon = args.getString("icon"); - final String iconMute = args.getString("iconmute"); - final boolean mute = iconMute != null; - icon = mute ? iconMute : icon; - icon = icon.endsWith("Stream") ? icon : (icon + "Stream"); - final StreamResources sr = StreamResources.valueOf(icon); - mDemoIcon = mute ? sr.iconMuteRes : sr.iconRes; - final int forcedStreamType = StreamResources.MediaStream.streamType; - mAudioManager.forceVolumeControlStream(forcedStreamType); - mAudioManager.adjustStreamVolume(forcedStreamType, AudioManager.ADJUST_SAME, - AudioManager.FLAG_SHOW_UI); - } - - private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - final Object tag = seekBar.getTag(); - if (fromUser && tag instanceof StreamControl) { - StreamControl sc = (StreamControl) tag; - setStreamVolume(sc, progress, - AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE); - } - resetTimeout(); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }; - - private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() { - @Override - public void onZenAvailableChanged(boolean available) { - obtainMessage(MSG_ZEN_MODE_AVAILABLE_CHANGED, available ? 1 : 0, 0).sendToTarget(); - } - - @Override - public void onEffectsSupressorChanged() { - mNotificationEffectsSuppressor = mZenController.getEffectsSuppressor(); - sendEmptyMessage(MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED); - } - }; - - private final MediaController.Callback mMediaControllerCb = new MediaController.Callback() { - public void onAudioInfoChanged(PlaybackInfo info) { - onRemoteVolumeUpdateIfShown(); - } - }; - - private final class SecondaryIconTransition extends AnimatorListenerAdapter - implements Runnable { - private static final int ANIMATION_TIME = 400; - private static final int WAIT_FOR_SWITCH_TIME = 1000; - - private final int mAnimationTime = (int)(ANIMATION_TIME * ValueAnimator.getDurationScale()); - private final int mFadeOutTime = mAnimationTime / 2; - private final int mDelayTime = mAnimationTime / 3; - - private final Interpolator mIconInterpolator = - AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); - - private StreamControl mTarget; - - public void start(StreamControl sc) { - if (sc == null) throw new IllegalArgumentException(); - if (LOGD) Log.d(mTag, "Secondary icon animation start"); - if (mTarget != null) { - cancel(); - } - mTarget = sc; - mTimeoutDelay = mAnimationTime + WAIT_FOR_SWITCH_TIME; - resetTimeout(); - mTarget.secondaryIcon.setClickable(false); - final int N = mTarget.group.getChildCount(); - for (int i = 0; i < N; i++) { - final View child = mTarget.group.getChildAt(i); - if (child != mTarget.secondaryIcon) { - child.animate().alpha(0).setDuration(mFadeOutTime).start(); - } - } - mTarget.secondaryIcon.animate() - .translationXBy(mTarget.icon.getX() - mTarget.secondaryIcon.getX()) - .setInterpolator(mIconInterpolator) - .setStartDelay(mDelayTime) - .setDuration(mAnimationTime - mDelayTime) - .setListener(this) - .start(); - } - - public boolean isRunning() { - return mTarget != null; - } - - public void cancel() { - if (mTarget == null) return; - mTarget.secondaryIcon.setClickable(true); - final int N = mTarget.group.getChildCount(); - for (int i = 0; i < N; i++) { - final View child = mTarget.group.getChildAt(i); - if (child != mTarget.secondaryIcon) { - child.animate().cancel(); - child.setAlpha(1); - } - } - mTarget.secondaryIcon.animate().cancel(); - mTarget.secondaryIcon.setTranslationX(0); - mTarget = null; - } - - @Override - public void onAnimationEnd(Animator animation) { - if (mTarget == null) return; - AsyncTask.execute(this); - } - - @Override - public void run() { - if (mTarget == null) return; - if (LOGD) Log.d(mTag, "Secondary icon animation complete, show notification slider"); - mAudioManager.forceVolumeControlStream(StreamResources.NotificationStream.streamType); - mAudioManager.adjustStreamVolume(StreamResources.NotificationStream.streamType, - AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI); - } - } - - public interface Callback { - void onZenSettings(); - void onInteraction(); - void onVisible(boolean visible); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePrefs.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePrefs.java new file mode 100644 index 0000000..04339eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePrefs.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.preference.PreferenceManager; + +/** + * Configuration for the volume dialog + related policy. + */ +public class VolumePrefs { + + public static final String PREF_ENABLE_PROTOTYPE = "pref_enable_prototype"; // not persistent + public static final String PREF_SHOW_ALARMS = "pref_show_alarms"; + public static final String PREF_SHOW_SYSTEM = "pref_show_system"; + public static final String PREF_SHOW_HEADERS = "pref_show_headers"; + public static final String PREF_SHOW_FAKE_REMOTE_1 = "pref_show_fake_remote_1"; + public static final String PREF_SHOW_FAKE_REMOTE_2 = "pref_show_fake_remote_2"; + public static final String PREF_ENABLE_AUTOMUTE = "pref_enable_automute"; + public static final String PREF_ENABLE_SILENT_MODE = "pref_enable_silent_mode"; + public static final String PREF_DEBUG_LOGGING = "pref_debug_logging"; + public static final String PREF_SEND_LOGS = "pref_send_logs"; + public static final String PREF_ADJUST_SYSTEM = "pref_adjust_system"; + public static final String PREF_ADJUST_VOICE_CALLS = "pref_adjust_voice_calls"; + public static final String PREF_ADJUST_BLUETOOTH_SCO = "pref_adjust_bluetooth_sco"; + public static final String PREF_ADJUST_MEDIA = "pref_adjust_media"; + public static final String PREF_ADJUST_ALARMS = "pref_adjust_alarms"; + public static final String PREF_ADJUST_NOTIFICATION = "pref_adjust_notification"; + + public static final boolean DEFAULT_SHOW_HEADERS = true; + public static final boolean DEFAULT_ENABLE_AUTOMUTE = true; + public static final boolean DEFAULT_ENABLE_SILENT_MODE = true; + + public static void unregisterCallbacks(Context c, OnSharedPreferenceChangeListener listener) { + prefs(c).unregisterOnSharedPreferenceChangeListener(listener); + } + + public static void registerCallbacks(Context c, OnSharedPreferenceChangeListener listener) { + prefs(c).registerOnSharedPreferenceChangeListener(listener); + } + + private static SharedPreferences prefs(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); + } + + public static boolean get(Context context, String key, boolean def) { + return prefs(context).getBoolean(key, def); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java index 7102c2a..2688813 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java @@ -1,216 +1,249 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.systemui.volume; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; import android.content.res.Configuration; -import android.database.ContentObserver; import android.media.AudioManager; -import android.media.IRemoteVolumeController; -import android.media.IVolumeController; -import android.media.session.ISessionController; -import android.media.session.MediaController; import android.media.session.MediaSessionManager; -import android.net.Uri; -import android.os.Bundle; import android.os.Handler; -import android.os.RemoteException; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; +import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.SystemUI; -import com.android.systemui.keyguard.KeyguardViewMediator; -import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.qs.tiles.DndTile; +import com.android.systemui.statusbar.ServiceMonitor; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.ZenModeControllerImpl; import java.io.FileDescriptor; import java.io.PrintWriter; -/* - * 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. - */ - public class VolumeUI extends SystemUI { private static final String TAG = "VolumeUI"; - private static final String SETTING = "systemui_volume_controller"; // for testing - private static final Uri SETTING_URI = Settings.Global.getUriFor(SETTING); - private static final int DEFAULT = 1; // enabled by default + private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); private final Handler mHandler = new Handler(); + private final Receiver mReceiver = new Receiver(); + private final RestorationNotification mRestorationNotification = new RestorationNotification(); private boolean mEnabled; private AudioManager mAudioManager; + private NotificationManager mNotificationManager; private MediaSessionManager mMediaSessionManager; - private VolumeController mVolumeController; - private RemoteVolumeController mRemoteVolumeController; + private ServiceMonitor mVolumeControllerService; - private VolumePanel mPanel; - private int mDismissDelay; + private VolumeDialogComponent mVolumeComponent; @Override public void start() { mEnabled = mContext.getResources().getBoolean(R.bool.enable_volume_ui); if (!mEnabled) return; mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + mNotificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); mMediaSessionManager = (MediaSessionManager) mContext .getSystemService(Context.MEDIA_SESSION_SERVICE); - initPanel(); - mVolumeController = new VolumeController(); - mRemoteVolumeController = new RemoteVolumeController(); - putComponent(VolumeComponent.class, mVolumeController); - updateController(); - mContext.getContentResolver().registerContentObserver(SETTING_URI, false, mObserver); + final ZenModeController zenController = new ZenModeControllerImpl(mContext, mHandler); + mVolumeComponent = new VolumeDialogComponent(this, mContext, null, zenController); + putComponent(VolumeComponent.class, getVolumeComponent()); + mReceiver.start(); + mVolumeControllerService = new ServiceMonitor(TAG, LOGD, + mContext, Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT, + new ServiceMonitorCallbacks()); + mVolumeControllerService.start(); + } + + private VolumeComponent getVolumeComponent() { + return mVolumeComponent; } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - if (mPanel != null) { - mPanel.onConfigurationChanged(newConfig); - } + if (!mEnabled) return; + getVolumeComponent().onConfigurationChanged(newConfig); } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.print("mEnabled="); pw.println(mEnabled); - if (mPanel != null) { - mPanel.dump(fd, pw, args); - } + if (!mEnabled) return; + pw.print("mVolumeControllerService="); pw.println(mVolumeControllerService.getComponent()); + getVolumeComponent().dump(fd, pw, args); } - private void updateController() { - if (Settings.Global.getInt(mContext.getContentResolver(), SETTING, DEFAULT) != 0) { - Log.d(TAG, "Registering volume controller"); - mAudioManager.setVolumeController(mVolumeController); - mMediaSessionManager.setRemoteVolumeController(mRemoteVolumeController); + private void setDefaultVolumeController(boolean register) { + if (register) { + DndTile.setVisible(mContext, true); + if (LOGD) Log.d(TAG, "Registering default volume controller"); + getVolumeComponent().register(); } else { - Log.d(TAG, "Unregistering volume controller"); + if (LOGD) Log.d(TAG, "Unregistering default volume controller"); mAudioManager.setVolumeController(null); mMediaSessionManager.setRemoteVolumeController(null); } } - private void initPanel() { - mDismissDelay = mContext.getResources().getInteger(R.integer.volume_panel_dismiss_delay); - mPanel = new VolumePanel(mContext, new ZenModeControllerImpl(mContext, mHandler)); - mPanel.setCallback(new VolumePanel.Callback() { - @Override - public void onZenSettings() { - mHandler.removeCallbacks(mStartZenSettings); - mHandler.post(mStartZenSettings); - } - - @Override - public void onInteraction() { - final KeyguardViewMediator kvm = getComponent(KeyguardViewMediator.class); - if (kvm != null) { - kvm.userActivity(); - } + private String getAppLabel(ComponentName component) { + final String pkg = component.getPackageName(); + try { + final ApplicationInfo ai = mContext.getPackageManager().getApplicationInfo(pkg, 0); + final String rt = mContext.getPackageManager().getApplicationLabel(ai).toString(); + if (!TextUtils.isEmpty(rt)) { + return rt; } + } catch (Exception e) { + Log.w(TAG, "Error loading app label", e); + } + return pkg; + } + private void showServiceActivationDialog(final ComponentName component) { + final SystemUIDialog d = new SystemUIDialog(mContext); + d.setMessage(mContext.getString(R.string.volumeui_prompt_message, getAppLabel(component))); + d.setPositiveButton(R.string.volumeui_prompt_allow, new OnClickListener() { @Override - public void onVisible(boolean visible) { - if (mAudioManager != null && mVolumeController != null) { - mAudioManager.notifyVolumeControllerVisible(mVolumeController, visible); - } + public void onClick(DialogInterface dialog, int which) { + mVolumeControllerService.setComponent(component); } }); + d.setNegativeButton(R.string.volumeui_prompt_deny, null); + d.show(); } - private final ContentObserver mObserver = new ContentObserver(mHandler) { - public void onChange(boolean selfChange, Uri uri) { - if (SETTING_URI.equals(uri)) { - updateController(); - } - } - }; - - private final Runnable mStartZenSettings = new Runnable() { - @Override - public void run() { - getComponent(PhoneStatusBar.class).startActivityDismissingKeyguard( - ZenModePanel.ZEN_SETTINGS, true /* onlyProvisioned */, true /* dismissShade */); - mPanel.postDismiss(mDismissDelay); - } - }; - - /** For now, simply host an unmodified base volume panel in this process. */ - private final class VolumeController extends IVolumeController.Stub implements VolumeComponent { - - @Override - public void displaySafeVolumeWarning(int flags) throws RemoteException { - mPanel.postDisplaySafeVolumeWarning(flags); - } - + private final class ServiceMonitorCallbacks implements ServiceMonitor.Callbacks { @Override - public void volumeChanged(int streamType, int flags) - throws RemoteException { - mPanel.postVolumeChanged(streamType, flags); - } - - @Override - public void masterVolumeChanged(int flags) throws RemoteException { - mPanel.postMasterVolumeChanged(flags); - } - - @Override - public void masterMuteChanged(int flags) throws RemoteException { - mPanel.postMasterMuteChanged(flags); + public void onNoService() { + if (LOGD) Log.d(TAG, "onNoService"); + setDefaultVolumeController(true); + mRestorationNotification.hide(); + if (!mVolumeControllerService.isPackageAvailable()) { + mVolumeControllerService.setComponent(null); + } } @Override - public void setLayoutDirection(int layoutDirection) - throws RemoteException { - mPanel.postLayoutDirection(layoutDirection); + public long onServiceStartAttempt() { + if (LOGD) Log.d(TAG, "onServiceStartAttempt"); + // poke the setting to update the uid + mVolumeControllerService.setComponent(mVolumeControllerService.getComponent()); + setDefaultVolumeController(false); + getVolumeComponent().dismissNow(); + mRestorationNotification.show(); + return 0; } + } - @Override - public void dismiss() throws RemoteException { - dismissNow(); - } + private final class Receiver extends BroadcastReceiver { + private static final String ENABLE = "com.android.systemui.vui.ENABLE"; + private static final String DISABLE = "com.android.systemui.vui.DISABLE"; + private static final String EXTRA_COMPONENT = "component"; - @Override - public ZenModeController getZenController() { - return mPanel.getZenController(); - } + private static final String PREF = "com.android.systemui.PREF"; + private static final String EXTRA_KEY = "key"; + private static final String EXTRA_VALUE = "value"; - @Override - public void dispatchDemoCommand(String command, Bundle args) { - mPanel.dispatchDemoCommand(command, args); + public void start() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(ENABLE); + filter.addAction(DISABLE); + filter.addAction(PREF); + mContext.registerReceiver(this, filter, null, mHandler); } @Override - public void dismissNow() { - mPanel.postDismiss(0); + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (PREF.equals(action)) { + final String key = intent.getStringExtra(EXTRA_KEY); + if (key != null && intent.getExtras() != null) { + final Object value = intent.getExtras().get(EXTRA_VALUE); + if (value == null) { + Prefs.remove(mContext, key); + } else if (value instanceof Boolean) { + Prefs.putBoolean(mContext, key, (Boolean) value); + } else if (value instanceof Integer) { + Prefs.putInt(mContext, key, (Integer) value); + } else if (value instanceof Long) { + Prefs.putLong(mContext, key, (Long) value); + } + } + return; + } + final ComponentName component = intent.getParcelableExtra(EXTRA_COMPONENT); + final boolean current = component != null + && component.equals(mVolumeControllerService.getComponent()); + if (ENABLE.equals(action) && component != null) { + if (!current) { + showServiceActivationDialog(component); + } + } + if (DISABLE.equals(action) && component != null) { + if (current) { + mVolumeControllerService.setComponent(null); + } + } } } - private final class RemoteVolumeController extends IRemoteVolumeController.Stub { - - @Override - public void remoteVolumeChanged(ISessionController binder, int flags) - throws RemoteException { - MediaController controller = new MediaController(mContext, binder); - mPanel.postRemoteVolumeChanged(controller, flags); + private final class RestorationNotification { + public void hide() { + mNotificationManager.cancel(R.id.notification_volumeui); } - @Override - public void updateRemoteController(ISessionController session) throws RemoteException { - mPanel.postRemoteSliderVisibility(session != null); - // TODO stash default session in case the slider can be opened other - // than by remoteVolumeChanged. + public void show() { + final ComponentName component = mVolumeControllerService.getComponent(); + if (component == null) { + Log.w(TAG, "Not showing restoration notification, component not active"); + return; + } + final Intent intent = new Intent(Receiver.DISABLE) + .putExtra(Receiver.EXTRA_COMPONENT, component); + mNotificationManager.notify(R.id.notification_volumeui, + new Notification.Builder(mContext) + .setSmallIcon(R.drawable.ic_volume_media) + .setWhen(0) + .setShowWhen(false) + .setOngoing(true) + .setContentTitle(mContext.getString( + R.string.volumeui_notification_title, getAppLabel(component))) + .setContentText(mContext.getString(R.string.volumeui_notification_text)) + .setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT)) + .setPriority(Notification.PRIORITY_MIN) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setColor(mContext.getColor( + com.android.internal.R.color.system_notification_accent_color)) + .build()); } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java new file mode 100644 index 0000000..af7ee08 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.volume; + +import android.animation.LayoutTransition; +import android.animation.ValueAnimator; +import android.content.Context; +import android.provider.Settings.Global; +import android.service.notification.ZenModeConfig; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.systemui.R; +import com.android.systemui.statusbar.policy.ZenModeController; + +import java.util.Objects; + +/** + * Zen mode information (and end button) attached to the bottom of the volume dialog. + */ +public class ZenFooter extends LinearLayout { + private static final String TAG = Util.logTag(ZenFooter.class); + + private final Context mContext; + private final SpTexts mSpTexts; + + private ImageView mIcon; + private TextView mSummaryLine1; + private TextView mSummaryLine2; + private TextView mEndNowButton; + private int mZen = -1; + private ZenModeConfig mConfig; + private ZenModeController mController; + + public ZenFooter(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mSpTexts = new SpTexts(mContext); + final LayoutTransition layoutTransition = new LayoutTransition(); + layoutTransition.setDuration(new ValueAnimator().getDuration() / 2); + setLayoutTransition(layoutTransition); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mIcon = (ImageView) findViewById(R.id.volume_zen_icon); + mSummaryLine1 = (TextView) findViewById(R.id.volume_zen_summary_line_1); + mSummaryLine2 = (TextView) findViewById(R.id.volume_zen_summary_line_2); + mEndNowButton = (TextView) findViewById(R.id.volume_zen_end_now); + mSpTexts.add(mSummaryLine1); + mSpTexts.add(mSummaryLine2); + mSpTexts.add(mEndNowButton); + } + + public void init(final ZenModeController controller) { + controller.addCallback(new ZenModeController.Callback() { + @Override + public void onZenChanged(int zen) { + setZen(zen); + } + @Override + public void onConfigChanged(ZenModeConfig config) { + setConfig(config); + } + }); + mEndNowButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + controller.setZen(Global.ZEN_MODE_OFF, null, TAG); + } + }); + mZen = controller.getZen(); + mConfig = controller.getConfig(); + mController = controller; + update(); + } + + private void setZen(int zen) { + if (mZen == zen) return; + mZen = zen; + update(); + } + + private void setConfig(ZenModeConfig config) { + if (Objects.equals(mConfig, config)) return; + mConfig = config; + update(); + } + + public boolean isZen() { + return isZenPriority() || isZenAlarms() || isZenNone(); + } + + private boolean isZenPriority() { + return mZen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + } + + private boolean isZenAlarms() { + return mZen == Global.ZEN_MODE_ALARMS; + } + + private boolean isZenNone() { + return mZen == Global.ZEN_MODE_NO_INTERRUPTIONS; + } + + public void update() { + mIcon.setImageResource(isZenNone() ? R.drawable.ic_dnd_total_silence : R.drawable.ic_dnd); + final String line1 = + isZenPriority() ? mContext.getString(R.string.interruption_level_priority) + : isZenAlarms() ? mContext.getString(R.string.interruption_level_alarms) + : isZenNone() ? mContext.getString(R.string.interruption_level_none) + : null; + Util.setText(mSummaryLine1, line1); + + final boolean isForever = mConfig != null && mConfig.manualRule != null + && mConfig.manualRule.conditionId == null; + final String line2 = + isForever ? mContext.getString(com.android.internal.R.string.zen_mode_forever_dnd) + : ZenModeConfig.getConditionSummary(mContext, mConfig, mController.getCurrentUser(), + true /*shortVersion*/); + Util.setText(mSummaryLine2, line2); + } + + public void onConfigurationChanged() { + mSpTexts.update(); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java index d40a2c0..3c9a7fc 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java @@ -19,12 +19,11 @@ package com.android.systemui.volume; import android.animation.LayoutTransition; import android.animation.LayoutTransition.TransitionListener; import android.app.ActivityManager; -import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.res.Resources; +import android.content.res.Configuration; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; @@ -34,7 +33,9 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.service.notification.Condition; import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; import android.text.TextUtils; +import android.text.format.DateFormat; import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; @@ -42,8 +43,6 @@ import android.util.MathUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.ImageView; @@ -51,12 +50,15 @@ import android.widget.LinearLayout; import android.widget.RadioButton; import android.widget.TextView; +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.statusbar.policy.ZenModeController; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Arrays; +import java.util.Locale; import java.util.Objects; public class ZenModePanel extends LinearLayout { @@ -75,37 +77,38 @@ public class ZenModePanel extends LinearLayout { private static final int FOREVER_CONDITION_INDEX = 0; private static final int COUNTDOWN_CONDITION_INDEX = 1; - public static final Intent ZEN_SETTINGS = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); + public static final Intent ZEN_SETTINGS + = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); + public static final Intent ZEN_PRIORITY_SETTINGS + = new Intent(Settings.ACTION_ZEN_MODE_PRIORITY_SETTINGS); private final Context mContext; private final LayoutInflater mInflater; private final H mHandler = new H(); - private final Prefs mPrefs; + private final ZenPrefs mPrefs; private final IconPulser mIconPulser; - private final int mSubheadWarningColor; - private final int mSubheadColor; - private final Interpolator mInterpolator; - private final int mMaxConditions; - private final int mMaxOptionalConditions; - private final boolean mCountdownConditionSupported; - private final int mFirstConditionIndex; private final TransitionHelper mTransitionHelper = new TransitionHelper(); private final Uri mForeverId; + private final SpTexts mSpTexts; private String mTag = TAG + "/" + Integer.toHexString(System.identityHashCode(this)); private SegmentedButtons mZenButtons; - private View mZenSubhead; - private TextView mZenSubheadCollapsed; - private TextView mZenSubheadExpanded; - private View mMoreSettings; + private View mZenIntroduction; + private TextView mZenIntroductionMessage; + private View mZenIntroductionConfirm; + private TextView mZenIntroductionCustomize; private LinearLayout mZenConditions; + private TextView mZenAlarmWarning; private Callback mCallback; private ZenModeController mController; + private boolean mCountdownConditionSupported; + private int mMaxConditions; + private int mMaxOptionalConditions; + private int mFirstConditionIndex; private boolean mRequestingConditions; private Condition mExitCondition; - private String mExitConditionText; private int mBucketIndex = -1; private boolean mExpanded; private boolean mHidden; @@ -115,27 +118,17 @@ public class ZenModePanel extends LinearLayout { private Condition mSessionExitCondition; private Condition[] mConditions; private Condition mTimeCondition; + private boolean mVoiceCapable; public ZenModePanel(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; - mPrefs = new Prefs(); + mPrefs = new ZenPrefs(); mInflater = LayoutInflater.from(mContext.getApplicationContext()); mIconPulser = new IconPulser(mContext); - final Resources res = mContext.getResources(); - mSubheadWarningColor = res.getColor(R.color.system_warning_color); - mSubheadColor = res.getColor(R.color.qs_subhead); - mInterpolator = AnimationUtils.loadInterpolator(mContext, - com.android.internal.R.interpolator.fast_out_slow_in); - mCountdownConditionSupported = NotificationManager.from(mContext) - .isSystemConditionProviderEnabled(ZenModeConfig.COUNTDOWN_PATH); - final int countdownDelta = mCountdownConditionSupported ? 1 : 0; - mFirstConditionIndex = COUNTDOWN_CONDITION_INDEX + countdownDelta; - final int minConditions = 1 /*forever*/ + countdownDelta; - mMaxConditions = MathUtils.constrain(res.getInteger(R.integer.zen_mode_max_conditions), - minConditions, 100); - mMaxOptionalConditions = mMaxConditions - minConditions; mForeverId = Condition.newId(mContext).appendPath("forever").build(); + mSpTexts = new SpTexts(mContext); + mVoiceCapable = Util.isVoiceCapable(mContext); if (DEBUG) Log.d(mTag, "new ZenModePanel"); } @@ -149,6 +142,11 @@ public class ZenModePanel extends LinearLayout { pw.print(" mExpanded="); pw.println(mExpanded); pw.print(" mSessionZen="); pw.println(mSessionZen); pw.print(" mAttachedZen="); pw.println(mAttachedZen); + pw.print(" mConfirmedPriorityIntroduction="); + pw.println(mPrefs.mConfirmedPriorityIntroduction); + pw.print(" mConfirmedSilenceIntroduction="); + pw.println(mPrefs.mConfirmedSilenceIntroduction); + pw.print(" mVoiceCapable="); pw.println(mVoiceCapable); mTransitionHelper.dump(fd, pw, args); } @@ -157,58 +155,68 @@ public class ZenModePanel extends LinearLayout { super.onFinishInflate(); mZenButtons = (SegmentedButtons) findViewById(R.id.zen_buttons); - mZenButtons.addButton(R.string.interruption_level_none, R.drawable.ic_zen_none, + mZenButtons.addButton(R.string.interruption_level_none_twoline, + R.string.interruption_level_none_with_warning, Global.ZEN_MODE_NO_INTERRUPTIONS); - mZenButtons.addButton(R.string.interruption_level_priority, R.drawable.ic_zen_important, + mZenButtons.addButton(R.string.interruption_level_alarms_twoline, + R.string.interruption_level_alarms, + Global.ZEN_MODE_ALARMS); + mZenButtons.addButton(R.string.interruption_level_priority_twoline, + R.string.interruption_level_priority, Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); - mZenButtons.addButton(R.string.interruption_level_all, R.drawable.ic_zen_all, - Global.ZEN_MODE_OFF); mZenButtons.setCallback(mZenButtonsCallback); - final ViewGroup zenButtonsContainer = (ViewGroup) findViewById(R.id.zen_buttons_container); - zenButtonsContainer.setLayoutTransition(newLayoutTransition(null)); - - mZenSubhead = findViewById(R.id.zen_subhead); - - mZenSubheadCollapsed = (TextView) findViewById(R.id.zen_subhead_collapsed); - mZenSubheadCollapsed.setOnClickListener(new View.OnClickListener() { + mZenIntroduction = findViewById(R.id.zen_introduction); + mZenIntroductionMessage = (TextView) findViewById(R.id.zen_introduction_message); + mSpTexts.add(mZenIntroductionMessage); + mZenIntroductionConfirm = findViewById(R.id.zen_introduction_confirm); + mZenIntroductionConfirm.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - setExpanded(true); + confirmZenIntroduction(); } }); - Interaction.register(mZenSubheadCollapsed, mInteractionCallback); - - mZenSubheadExpanded = (TextView) findViewById(R.id.zen_subhead_expanded); - Interaction.register(mZenSubheadExpanded, mInteractionCallback); - - mMoreSettings = findViewById(R.id.zen_more_settings); - mMoreSettings.setOnClickListener(new View.OnClickListener() { + mZenIntroductionCustomize = (TextView) findViewById(R.id.zen_introduction_customize); + mZenIntroductionCustomize.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - fireMoreSettings(); + confirmZenIntroduction(); + if (mCallback != null) { + mCallback.onPrioritySettings(); + } } }); - Interaction.register(mMoreSettings, mInteractionCallback); + mSpTexts.add(mZenIntroductionCustomize); mZenConditions = (LinearLayout) findViewById(R.id.zen_conditions); - for (int i = 0; i < mMaxConditions; i++) { - mZenConditions.addView(mInflater.inflate(R.layout.zen_mode_condition, this, false)); + mZenAlarmWarning = (TextView) findViewById(R.id.zen_alarm_warning); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (mZenButtons != null) { + mZenButtons.updateLocale(); } + } - setLayoutTransition(newLayoutTransition(mTransitionHelper)); + private void confirmZenIntroduction() { + final String prefKey = prefKeyForConfirmation(getSelectedZen(Global.ZEN_MODE_OFF)); + if (prefKey == null) return; + if (DEBUG) Log.d(TAG, "confirmZenIntroduction " + prefKey); + Prefs.putBoolean(mContext, prefKey, true); + mHandler.sendEmptyMessage(H.UPDATE_WIDGETS); } - private LayoutTransition newLayoutTransition(TransitionListener listener) { - final LayoutTransition transition = new LayoutTransition(); - transition.disableTransitionType(LayoutTransition.DISAPPEARING); - transition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); - transition.disableTransitionType(LayoutTransition.APPEARING); - transition.setInterpolator(LayoutTransition.CHANGE_APPEARING, mInterpolator); - if (listener != null) { - transition.addTransitionListener(listener); + private static String prefKeyForConfirmation(int zen) { + switch (zen) { + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + return Prefs.Key.DND_CONFIRMED_PRIORITY_INTRODUCTION; + case Global.ZEN_MODE_NO_INTERRUPTIONS: + return Prefs.Key.DND_CONFIRMED_SILENCE_INTRODUCTION; + default: + return null; } - return transition; } @Override @@ -220,7 +228,6 @@ public class ZenModePanel extends LinearLayout { mSessionZen = mAttachedZen; mTransitionHelper.clear(); setSessionExitCondition(copy(mExitCondition)); - refreshExitConditionText(); updateWidgets(); setRequestingConditions(!mHidden); } @@ -234,7 +241,6 @@ public class ZenModePanel extends LinearLayout { mAttachedZen = -1; mSessionZen = -1; setSessionExitCondition(null); - setExpanded(false); setRequestingConditions(false); mTransitionHelper.clear(); } @@ -266,8 +272,9 @@ public class ZenModePanel extends LinearLayout { private void setExpanded(boolean expanded) { if (expanded == mExpanded) return; + if (DEBUG) Log.d(mTag, "setExpanded " + expanded); mExpanded = expanded; - if (mExpanded) { + if (mExpanded && isShown()) { ensureSelection(); } updateWidgets(); @@ -288,7 +295,7 @@ public class ZenModePanel extends LinearLayout { }); } if (mRequestingConditions) { - mTimeCondition = parseExistingTimeCondition(mExitCondition); + mTimeCondition = parseExistingTimeCondition(mContext, mExitCondition); if (mTimeCondition != null) { mBucketIndex = -1; } else { @@ -306,10 +313,18 @@ public class ZenModePanel extends LinearLayout { public void init(ZenModeController controller) { mController = controller; - setExitCondition(mController.getExitCondition()); - refreshExitConditionText(); + mCountdownConditionSupported = mController.isCountdownConditionSupported(); + final int countdownDelta = mCountdownConditionSupported ? 1 : 0; + mFirstConditionIndex = COUNTDOWN_CONDITION_INDEX + countdownDelta; + final int minConditions = 1 /*forever*/ + countdownDelta; + mMaxConditions = MathUtils.constrain(mContext.getResources() + .getInteger(R.integer.zen_mode_max_conditions), minConditions, 100); + mMaxOptionalConditions = mMaxConditions - minConditions; + for (int i = 0; i < mMaxConditions; i++) { + mZenConditions.addView(mInflater.inflate(R.layout.zen_mode_condition, this, false)); + } mSessionZen = getSelectedZen(-1); - handleUpdateZen(mController.getZen()); + handleUpdateManualRule(mController.getManualRule()); if (DEBUG) Log.d(mTag, "init mExitCondition=" + mExitCondition); hideAllConditions(); mController.addCallback(mZenCallback); @@ -323,7 +338,6 @@ public class ZenModePanel extends LinearLayout { if (Objects.equals(mExitCondition, exitCondition)) return; mExitCondition = exitCondition; if (DEBUG) Log.d(mTag, "mExitCondition=" + getConditionId(mExitCondition)); - refreshExitConditionText(); updateWidgets(); } @@ -331,6 +345,10 @@ public class ZenModePanel extends LinearLayout { return condition != null ? condition.id : null; } + private Uri getRealConditionId(Condition condition) { + return isForever(condition) ? null : getConditionId(condition); + } + private static boolean sameConditionId(Condition lhs, Condition rhs) { return lhs == null ? rhs == null : rhs != null && lhs.id.equals(rhs.id); } @@ -339,14 +357,14 @@ public class ZenModePanel extends LinearLayout { return condition == null ? null : condition.copy(); } - private void refreshExitConditionText() { - if (mExitCondition == null) { - mExitConditionText = foreverSummary(); - } else if (isCountdown(mExitCondition)) { - final Condition condition = parseExistingTimeCondition(mExitCondition); - mExitConditionText = condition != null ? condition.summary : foreverSummary(); + public static String getExitConditionText(Context context, Condition exitCondition) { + if (exitCondition == null) { + return foreverSummary(context); + } else if (isCountdown(exitCondition)) { + final Condition condition = parseExistingTimeCondition(context, exitCondition); + return condition != null ? condition.summary : foreverSummary(context); } else { - mExitConditionText = mExitCondition.summary; + return exitCondition.summary; } } @@ -361,12 +379,19 @@ public class ZenModePanel extends LinearLayout { mIconPulser.start(noneButton); } + private void handleUpdateManualRule(ZenRule rule) { + final int zen = rule != null ? rule.zenMode : Global.ZEN_MODE_OFF; + handleUpdateZen(zen); + final Condition c = rule != null ? rule.condition : null; + handleExitConditionChanged(c); + } + private void handleUpdateZen(int zen) { if (mSessionZen != -1 && mSessionZen != zen) { - setExpanded(zen != Global.ZEN_MODE_OFF); + setExpanded(isShown()); mSessionZen = zen; } - mZenButtons.setSelectedValue(zen); + mZenButtons.setSelectedValue(zen, false /* fromClick */); updateWidgets(); handleUpdateConditions(); if (mExpanded) { @@ -377,6 +402,20 @@ public class ZenModePanel extends LinearLayout { } } + private void handleExitConditionChanged(Condition exitCondition) { + setExitCondition(exitCondition); + if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitCondition); + final int N = getVisibleConditions(); + for (int i = 0; i < N; i++) { + final ConditionTag tag = getConditionTagAt(i); + if (tag != null) { + if (sameConditionId(tag.condition, mExitCondition)) { + bind(exitCondition, mZenConditions.getChildAt(i)); + } + } + } + } + private Condition getSelectedCondition() { final int N = getVisibleConditions(); for (int i = 0; i < N; i++) { @@ -399,38 +438,65 @@ public class ZenModePanel extends LinearLayout { return; } final int zen = getSelectedZen(Global.ZEN_MODE_OFF); - final boolean zenOff = zen == Global.ZEN_MODE_OFF; final boolean zenImportant = zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS; - final boolean expanded = !mHidden && mExpanded; + final boolean introduction = (zenImportant && !mPrefs.mConfirmedPriorityIntroduction + || zenNone && !mPrefs.mConfirmedSilenceIntroduction); mZenButtons.setVisibility(mHidden ? GONE : VISIBLE); - mZenSubhead.setVisibility(!mHidden && !zenOff ? VISIBLE : GONE); - mZenSubheadExpanded.setVisibility(expanded ? VISIBLE : GONE); - mZenSubheadCollapsed.setVisibility(!expanded ? VISIBLE : GONE); - mMoreSettings.setVisibility(zenImportant && expanded ? VISIBLE : GONE); - mZenConditions.setVisibility(!zenOff && expanded ? VISIBLE : GONE); + mZenIntroduction.setVisibility(introduction ? VISIBLE : GONE); + if (introduction) { + mZenIntroductionMessage.setText(zenImportant ? R.string.zen_priority_introduction + : mVoiceCapable ? R.string.zen_silence_introduction_voice + : R.string.zen_silence_introduction); + mZenIntroductionCustomize.setVisibility(zenImportant ? VISIBLE : GONE); + } + final String warning = computeAlarmWarningText(zenNone); + mZenAlarmWarning.setVisibility(warning != null ? VISIBLE : GONE); + mZenAlarmWarning.setText(warning); + } - if (zenNone) { - mZenSubheadExpanded.setText(R.string.zen_no_interruptions_with_warning); - mZenSubheadCollapsed.setText(mExitConditionText); - } else if (zenImportant) { - mZenSubheadExpanded.setText(R.string.zen_important_interruptions); - mZenSubheadCollapsed.setText(mExitConditionText); + private String computeAlarmWarningText(boolean zenNone) { + if (!zenNone) { + return null; + } + final long now = System.currentTimeMillis(); + final long nextAlarm = mController.getNextAlarm(); + if (nextAlarm < now) { + return null; } - mZenSubheadExpanded.setTextColor(zenNone && mPrefs.isNoneDangerous() - ? mSubheadWarningColor : mSubheadColor); + int warningRes = 0; + if (mSessionExitCondition == null || isForever(mSessionExitCondition)) { + warningRes = R.string.zen_alarm_warning_indef; + } else { + final long time = ZenModeConfig.tryParseCountdownConditionId(mSessionExitCondition.id); + if (time > now && nextAlarm < time) { + warningRes = R.string.zen_alarm_warning; + } + } + if (warningRes == 0) { + return null; + } + final boolean soon = (nextAlarm - now) < 24 * 60 * 60 * 1000; + final boolean is24 = DateFormat.is24HourFormat(mContext, ActivityManager.getCurrentUser()); + final String skeleton = soon ? (is24 ? "Hm" : "hma") : (is24 ? "EEEHm" : "EEEhma"); + final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); + final CharSequence formattedTime = DateFormat.format(pattern, nextAlarm); + final int templateRes = soon ? R.string.alarm_template : R.string.alarm_template_far; + final String template = getResources().getString(templateRes, formattedTime); + return getResources().getString(warningRes, template); } - private Condition parseExistingTimeCondition(Condition condition) { + private static Condition parseExistingTimeCondition(Context context, Condition condition) { if (condition == null) return null; final long time = ZenModeConfig.tryParseCountdownConditionId(condition.id); if (time == 0) return null; final long now = System.currentTimeMillis(); final long span = time - now; if (span <= 0 || span > MAX_BUCKET_MINUTES * MINUTES_MS) return null; - return ZenModeConfig.toTimeCondition(mContext, - time, Math.round(span / (float) MINUTES_MS), now, ActivityManager.getCurrentUser()); + return ZenModeConfig.toTimeCondition(context, + time, Math.round(span / (float) MINUTES_MS), now, ActivityManager.getCurrentUser(), + false /*shortVersion*/); } private void handleUpdateConditions(Condition[] conditions) { @@ -489,18 +555,18 @@ public class ZenModePanel extends LinearLayout { mZenConditions.getChildAt(i).setVisibility(GONE); } // ensure something is selected - if (mExpanded) { + if (mExpanded && isShown()) { ensureSelection(); } } private Condition forever() { - return new Condition(mForeverId, foreverSummary(), "", "", 0 /*icon*/, Condition.STATE_TRUE, - 0 /*flags*/); + return new Condition(mForeverId, foreverSummary(mContext), "", "", 0 /*icon*/, + Condition.STATE_TRUE, 0 /*flags*/); } - private String foreverSummary() { - return mContext.getString(com.android.internal.R.string.zen_mode_forever); + private static String foreverSummary(Context context) { + return context.getString(com.android.internal.R.string.zen_mode_forever); } private ConditionTag getConditionTagAt(int index) { @@ -549,21 +615,7 @@ public class ZenModePanel extends LinearLayout { } } - private void handleExitConditionChanged(Condition exitCondition) { - setExitCondition(exitCondition); - if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitCondition); - final int N = getVisibleConditions(); - for (int i = 0; i < N; i++) { - final ConditionTag tag = getConditionTagAt(i); - if (tag != null) { - if (sameConditionId(tag.condition, mExitCondition)) { - bind(exitCondition, mZenConditions.getChildAt(i)); - } - } - } - } - - private boolean isCountdown(Condition c) { + private static boolean isCountdown(Condition c) { return c != null && ZenModeConfig.isValidCountdownConditionId(c.id); } @@ -605,6 +657,7 @@ public class ZenModePanel extends LinearLayout { if (childTag == null || childTag == tag) continue; childTag.rb.setChecked(false); } + MetricsLogger.action(mContext, MetricsLogger.QS_DND_CONDITION_SELECT); select(tag.condition); announceConditionSelection(tag); } @@ -616,9 +669,11 @@ public class ZenModePanel extends LinearLayout { } if (tag.line1 == null) { tag.line1 = (TextView) row.findViewById(android.R.id.text1); + mSpTexts.add(tag.line1); } if (tag.line2 == null) { tag.line2 = (TextView) row.findViewById(android.R.id.text2); + mSpTexts.add(tag.line2); } final String line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1 : condition.summary; @@ -691,12 +746,15 @@ public class ZenModePanel extends LinearLayout { String modeText; switch(zen) { case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: - modeText = mContext.getString(R.string.zen_important_interruptions); + modeText = mContext.getString(R.string.interruption_level_priority); break; case Global.ZEN_MODE_NO_INTERRUPTIONS: - modeText = mContext.getString(R.string.zen_no_interruptions); + modeText = mContext.getString(R.string.interruption_level_none); + break; + case Global.ZEN_MODE_ALARMS: + modeText = mContext.getString(R.string.interruption_level_alarms); break; - default: + default: return; } announceForAccessibility(mContext.getString(R.string.zen_mode_and_condition, modeText, @@ -704,6 +762,7 @@ public class ZenModePanel extends LinearLayout { } private void onClickTimeButton(View row, ConditionTag tag, boolean up) { + MetricsLogger.action(mContext, MetricsLogger.QS_DND_TIME, up); Condition newCondition = null; final int N = MINUTE_BUCKETS.length; if (mBucketIndex == -1) { @@ -718,7 +777,8 @@ public class ZenModePanel extends LinearLayout { if (up && bucketTime > time || !up && bucketTime < time) { mBucketIndex = j; newCondition = ZenModeConfig.toTimeCondition(mContext, - bucketTime, bucketMinutes, now, ActivityManager.getCurrentUser()); + bucketTime, bucketMinutes, now, ActivityManager.getCurrentUser(), + false /*shortVersion*/); break; } } @@ -742,17 +802,21 @@ public class ZenModePanel extends LinearLayout { private void select(final Condition condition) { if (DEBUG) Log.d(mTag, "select " + condition); - final boolean isForever = isForever(condition); + if (mSessionZen == -1 || mSessionZen == Global.ZEN_MODE_OFF) { + if (DEBUG) Log.d(mTag, "Ignoring condition selection outside of manual zen"); + return; + } + final Uri realConditionId = getRealConditionId(condition); if (mController != null) { AsyncTask.execute(new Runnable() { @Override public void run() { - mController.setExitCondition(isForever ? null : condition); + mController.setZen(mSessionZen, realConditionId, TAG + ".selectCondition"); } }); } setExitCondition(condition); - if (isForever) { + if (realConditionId == null) { mPrefs.setMinuteIndex(-1); } else if (isCountdown(condition) && mBucketIndex != -1) { mPrefs.setMinuteIndex(mBucketIndex); @@ -760,12 +824,6 @@ public class ZenModePanel extends LinearLayout { setSessionExitCondition(copy(condition)); } - private void fireMoreSettings() { - if (mCallback != null) { - mCallback.onMoreSettings(); - } - } - private void fireInteraction() { if (mCallback != null) { mCallback.onInteraction(); @@ -780,24 +838,20 @@ public class ZenModePanel extends LinearLayout { private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() { @Override - public void onZenChanged(int zen) { - mHandler.obtainMessage(H.UPDATE_ZEN, zen, 0).sendToTarget(); - } - @Override public void onConditionsChanged(Condition[] conditions) { mHandler.obtainMessage(H.UPDATE_CONDITIONS, conditions).sendToTarget(); } @Override - public void onExitConditionChanged(Condition exitCondition) { - mHandler.obtainMessage(H.EXIT_CONDITION_CHANGED, exitCondition).sendToTarget(); + public void onManualRuleChanged(ZenRule rule) { + mHandler.obtainMessage(H.MANUAL_RULE_CHANGED, rule).sendToTarget(); } }; private final class H extends Handler { private static final int UPDATE_CONDITIONS = 1; - private static final int EXIT_CONDITION_CHANGED = 2; - private static final int UPDATE_ZEN = 3; + private static final int MANUAL_RULE_CHANGED = 2; + private static final int UPDATE_WIDGETS = 3; private H() { super(Looper.getMainLooper()); @@ -805,18 +859,16 @@ public class ZenModePanel extends LinearLayout { @Override public void handleMessage(Message msg) { - if (msg.what == UPDATE_CONDITIONS) { - handleUpdateConditions((Condition[]) msg.obj); - } else if (msg.what == EXIT_CONDITION_CHANGED) { - handleExitConditionChanged((Condition) msg.obj); - } else if (msg.what == UPDATE_ZEN) { - handleUpdateZen(msg.arg1); + switch (msg.what) { + case UPDATE_CONDITIONS: handleUpdateConditions((Condition[]) msg.obj); break; + case MANUAL_RULE_CHANGED: handleUpdateManualRule((ZenRule) msg.obj); break; + case UPDATE_WIDGETS: updateWidgets(); break; } } } public interface Callback { - void onMoreSettings(); + void onPrioritySettings(); void onInteraction(); void onExpanded(boolean expanded); } @@ -830,32 +882,29 @@ public class ZenModePanel extends LinearLayout { Condition condition; } - private final class Prefs implements OnSharedPreferenceChangeListener { - private static final String KEY_MINUTE_INDEX = "minuteIndex"; - private static final String KEY_NONE_SELECTED = "noneSelected"; - + private final class ZenPrefs implements OnSharedPreferenceChangeListener { private final int mNoneDangerousThreshold; private int mMinuteIndex; private int mNoneSelected; + private boolean mConfirmedPriorityIntroduction; + private boolean mConfirmedSilenceIntroduction; - private Prefs() { + private ZenPrefs() { mNoneDangerousThreshold = mContext.getResources() .getInteger(R.integer.zen_mode_alarm_warning_threshold); - prefs().registerOnSharedPreferenceChangeListener(this); + Prefs.registerListener(mContext, this); updateMinuteIndex(); updateNoneSelected(); - } - - public boolean isNoneDangerous() { - return mNoneSelected < mNoneDangerousThreshold; + updateConfirmedPriorityIntroduction(); + updateConfirmedSilenceIntroduction(); } public void trackNoneSelected() { mNoneSelected = clampNoneSelected(mNoneSelected + 1); if (DEBUG) Log.d(mTag, "Setting none selected: " + mNoneSelected + " threshold=" + mNoneDangerousThreshold); - prefs().edit().putInt(KEY_NONE_SELECTED, mNoneSelected).apply(); + Prefs.putInt(mContext, Prefs.Key.DND_NONE_SELECTED, mNoneSelected); } public int getMinuteIndex() { @@ -867,21 +916,20 @@ public class ZenModePanel extends LinearLayout { if (minuteIndex == mMinuteIndex) return; mMinuteIndex = clampIndex(minuteIndex); if (DEBUG) Log.d(mTag, "Setting favorite minute index: " + mMinuteIndex); - prefs().edit().putInt(KEY_MINUTE_INDEX, mMinuteIndex).apply(); + Prefs.putInt(mContext, Prefs.Key.DND_FAVORITE_BUCKET_INDEX, mMinuteIndex); } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { updateMinuteIndex(); updateNoneSelected(); - } - - private SharedPreferences prefs() { - return mContext.getSharedPreferences(mContext.getPackageName(), 0); + updateConfirmedPriorityIntroduction(); + updateConfirmedSilenceIntroduction(); } private void updateMinuteIndex() { - mMinuteIndex = clampIndex(prefs().getInt(KEY_MINUTE_INDEX, DEFAULT_BUCKET_INDEX)); + mMinuteIndex = clampIndex(Prefs.getInt(mContext, + Prefs.Key.DND_FAVORITE_BUCKET_INDEX, DEFAULT_BUCKET_INDEX)); if (DEBUG) Log.d(mTag, "Favorite minute index: " + mMinuteIndex); } @@ -890,24 +938,51 @@ public class ZenModePanel extends LinearLayout { } private void updateNoneSelected() { - mNoneSelected = clampNoneSelected(prefs().getInt(KEY_NONE_SELECTED, 0)); + mNoneSelected = clampNoneSelected(Prefs.getInt(mContext, + Prefs.Key.DND_NONE_SELECTED, 0)); if (DEBUG) Log.d(mTag, "None selected: " + mNoneSelected); } private int clampNoneSelected(int noneSelected) { return MathUtils.constrain(noneSelected, 0, Integer.MAX_VALUE); } + + private void updateConfirmedPriorityIntroduction() { + final boolean confirmed = Prefs.getBoolean(mContext, + Prefs.Key.DND_CONFIRMED_PRIORITY_INTRODUCTION, false); + if (confirmed == mConfirmedPriorityIntroduction) return; + mConfirmedPriorityIntroduction = confirmed; + if (DEBUG) Log.d(mTag, "Confirmed priority introduction: " + + mConfirmedPriorityIntroduction); + } + + private void updateConfirmedSilenceIntroduction() { + final boolean confirmed = Prefs.getBoolean(mContext, + Prefs.Key.DND_CONFIRMED_SILENCE_INTRODUCTION, false); + if (confirmed == mConfirmedSilenceIntroduction) return; + mConfirmedSilenceIntroduction = confirmed; + if (DEBUG) Log.d(mTag, "Confirmed silence introduction: " + + mConfirmedSilenceIntroduction); + } } private final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() { @Override - public void onSelected(final Object value) { - if (value != null && mZenButtons.isShown()) { - if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + value); + public void onSelected(final Object value, boolean fromClick) { + if (value != null && mZenButtons.isShown() && isAttachedToWindow()) { + final int zen = (Integer) value; + if (fromClick) { + MetricsLogger.action(mContext, MetricsLogger.QS_DND_ZEN_SELECT, zen); + } + if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + zen); + final Uri realConditionId = getRealConditionId(mSessionExitCondition); AsyncTask.execute(new Runnable() { @Override public void run() { - mController.setZen((Integer) value); + mController.setZen(zen, realConditionId, TAG + ".selectZen"); + if (zen != Global.ZEN_MODE_OFF) { + Prefs.putInt(mContext, Prefs.Key.DND_FAVORITE_ZEN, zen); + } } }); } |
