diff options
Diffstat (limited to 'packages/SystemUI/src/com')
173 files changed, 20459 insertions, 1880 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryLevelTextView.java b/packages/SystemUI/src/com/android/systemui/BatteryLevelTextView.java new file mode 100644 index 0000000..247e965 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/BatteryLevelTextView.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryStateRegistar; + +import android.content.Context; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.TextView; + +import java.text.NumberFormat; + +public class BatteryLevelTextView extends TextView implements + BatteryController.BatteryStateChangeCallback{ + + private BatteryStateRegistar mBatteryStateRegistar; + private boolean mBatteryPresent; + + private boolean mBatteryCharging; + private boolean mForceShow; + private boolean mAttached; + private int mRequestedVisibility; + private int mStyle; + private int mPercentMode; + + public BatteryLevelTextView(Context context, AttributeSet attrs) { + super(context, attrs); + // setBatteryStateRegistar (if called) will made the view visible and ready to be hidden + // if the view shouldn't be displayed. Otherwise this view should be hidden from start. + mRequestedVisibility = GONE; + } + + public void setForceShown(boolean forceShow) { + mForceShow = forceShow; + updateVisibility(); + } + + public void setBatteryStateRegistar(BatteryStateRegistar batteryStateRegistar) { + mRequestedVisibility = VISIBLE; + mBatteryStateRegistar = batteryStateRegistar; + if (mAttached) { + mBatteryStateRegistar.addStateChangedCallback(this); + } + } + + @Override + public void setVisibility(int visibility) { + mRequestedVisibility = visibility; + updateVisibility(); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + // Respect font size setting. + setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimensionPixelSize(R.dimen.battery_level_text_size)); + } + + @Override + public void onBatteryLevelChanged(boolean present, int level, boolean pluggedIn, + boolean charging) { + String percentage = NumberFormat.getPercentInstance().format((double) level / 100.0); + setText(percentage); + if (mBatteryPresent != present || mBatteryCharging != charging) { + mBatteryPresent = present; + mBatteryCharging = charging; + updateVisibility(); + } + } + + @Override + public void onPowerSaveChanged() { + // Not used + } + + @Override + public void onBatteryStyleChanged(int style, int percentMode) { + mStyle = style; + mPercentMode = percentMode; + updateVisibility(); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mBatteryStateRegistar != null) { + mBatteryStateRegistar.addStateChangedCallback(this); + } + + mAttached = true; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mAttached = false; + + if (mBatteryStateRegistar != null) { + mBatteryStateRegistar.removeStateChangedCallback(this); + } + } + + private void updateVisibility() { + boolean showNextPercent = mBatteryPresent && ( + mPercentMode == BatteryController.PERCENTAGE_MODE_OUTSIDE + || (mBatteryCharging && mPercentMode == BatteryController.PERCENTAGE_MODE_INSIDE)); + if (mStyle == BatteryController.STYLE_GONE) { + showNextPercent = false; + } else if (mStyle == BatteryController.STYLE_TEXT) { + showNextPercent = true; + } + + if (mBatteryStateRegistar != null && (showNextPercent || mForceShow)) { + super.setVisibility(mRequestedVisibility); + } else { + super.setVisibility(GONE); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java index 95b58e5..06c2957 100755 --- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * Copyright (C) 2013-14 The Android Open Source Project + * Copyright (C) 2016 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,63 +22,59 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.res.Resources; +import android.content.res.ThemeConfig; import android.content.res.TypedArray; -import android.database.ContentObserver; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.RectF; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; import android.graphics.Typeface; -import android.net.Uri; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; import android.os.BatteryManager; import android.os.Bundle; -import android.os.Handler; -import android.provider.Settings; import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; import android.view.View; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryStateRegistar; + +import org.cyanogenmod.graphics.drawable.StopMotionVectorDrawable; 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 SINGLE_DIGIT_PERCENT = false; - - private static final int FULL = 96; - - private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction private final int[] mColors; - 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; + protected boolean mShowPercent = true; + + public enum BatteryMeterMode { + BATTERY_METER_GONE, + BATTERY_METER_ICON_PORTRAIT, + BATTERY_METER_ICON_LANDSCAPE, + BATTERY_METER_CIRCLE, + BATTERY_METER_TEXT + } private int mHeight; private int mWidth; private String mWarningString; private final int mCriticalLevel; - private int mChargeColor; - private final float[] mBoltPoints; - private final Path mBoltPath = new Path(); - - private final RectF mFrame = new RectF(); - private final RectF mButtonFrame = new RectF(); - private final RectF mBoltFrame = new RectF(); - private final Path mShapePath = new Path(); - private final Path mClipPath = new Path(); - private final Path mTextPath = new Path(); + private boolean mAnimationsEnabled; + private BatteryStateRegistar mBatteryStateRegistar; private BatteryController mBatteryController; private boolean mPowerSaveEnabled; @@ -87,8 +84,135 @@ public class BatteryMeterView extends View implements DemoMode, private int mLightModeBackgroundColor; private int mLightModeFillColor; - private BatteryTracker mTracker = new BatteryTracker(); - private final SettingObserver mSettingObserver = new SettingObserver(); + protected BatteryMeterMode mMeterMode = null; + + protected boolean mAttached; + + private boolean mDemoMode; + protected BatteryTracker mDemoTracker = new BatteryTracker(); + protected BatteryTracker mTracker = new BatteryTracker(); + private BatteryMeterDrawable mBatteryMeterDrawable; + private int mIconTint = Color.WHITE; + + private int mCurrentBackgroundColor = 0; + private int mCurrentFillColor = 0; + + protected class BatteryTracker extends BroadcastReceiver { + public static final int UNKNOWN_LEVEL = -1; + + // current battery status + boolean present = true; + 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)); + + present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); + 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)); + if (mBatteryMeterDrawable != null) { + setVisibility(View.VISIBLE); + invalidate(); + } + } 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); + } + }); + } + } + + protected boolean shouldIndicateCharging() { + if (status == BatteryManager.BATTERY_STATUS_CHARGING) { + return true; + } + if (plugged) { + return status == BatteryManager.BATTERY_STATUS_FULL; + } + return false; + } + } + + @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); + } + if (mBatteryStateRegistar != null) { + mBatteryStateRegistar.addStateChangedCallback(this); + } + mAttached = true; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mAttached = false; + getContext().unregisterReceiver(mTracker); + if (mBatteryStateRegistar != null) { + mBatteryStateRegistar.removeStateChangedCallback(this); + } + } public BatteryMeterView(Context context) { this(context, null, 0); @@ -104,8 +228,6 @@ public class BatteryMeterView extends View implements DemoMode, final Resources res = context.getResources(); TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView, defStyle, 0); - final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor, - 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); @@ -118,44 +240,9 @@ public class BatteryMeterView extends View implements DemoMode, levels.recycle(); colors.recycle(); atts.recycle(); - updateShowPercent(); mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol); - mCriticalLevel = mContext.getResources().getInteger( + mCriticalLevel = getContext().getResources().getInteger( com.android.internal.R.integer.config_criticalBatteryWarningLevel); - mButtonHeightFraction = context.getResources().getFraction( - R.fraction.battery_button_height_fraction, 1, 1); - mSubpixelSmoothingLeft = context.getResources().getFraction( - R.fraction.battery_subpixel_smoothing_left, 1, 1); - mSubpixelSmoothingRight = context.getResources().getFraction( - R.fraction.battery_subpixel_smoothing_right, 1, 1); - - mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mFramePaint.setColor(frameColor); - mFramePaint.setDither(true); - mFramePaint.setStrokeWidth(0); - mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE); - - mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mBatteryPaint.setDither(true); - mBatteryPaint.setStrokeWidth(0); - mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE); - - mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD); - mTextPaint.setTypeface(font); - mTextPaint.setTextAlign(Paint.Align.CENTER); - - mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mWarningTextPaint.setColor(mColors[1]); - font = Typeface.create("sans-serif", Typeface.BOLD); - mWarningTextPaint.setTypeface(font); - mWarningTextPaint.setTextAlign(Paint.Align.CENTER); - - mChargeColor = context.getColor(R.color.batterymeter_charge_color); - - mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mBoltPaint.setColor(context.getColor(R.color.batterymeter_bolt_color)); - mBoltPoints = loadBoltPoints(res); mDarkModeBackgroundColor = context.getColor(R.color.dark_mode_icon_color_dual_tone_background); @@ -163,32 +250,38 @@ public class BatteryMeterView extends View implements DemoMode, 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(); + setAnimationsEnabled(true); + } - 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); + protected BatteryMeterDrawable createBatteryMeterDrawable(BatteryMeterMode mode) { + Resources res = getResources(); + switch (mode) { + case BATTERY_METER_TEXT: + case BATTERY_METER_GONE: + return null; + default: + return new AllInOneBatteryMeterDrawable(res, mode); } - mBatteryController.addStateChangedCallback(this); - getContext().getContentResolver().registerContentObserver( - Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver); } @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); - getContext().unregisterReceiver(mTracker); - mBatteryController.removeStateChangedCallback(this); - getContext().getContentResolver().unregisterContentObserver(mSettingObserver); + if (mMeterMode == BatteryMeterMode.BATTERY_METER_TEXT) { + onSizeChanged(width, height, 0, 0); // Force a size changed event + } + + setMeasuredDimension(width, height); + } + + public void setBatteryStateRegistar(BatteryStateRegistar batteryStateRegistar) { + mBatteryStateRegistar = batteryStateRegistar; + if (!mAttached) { + mBatteryStateRegistar.addStateChangedCallback(this); + } } public void setBatteryController(BatteryController batteryController) { @@ -197,7 +290,8 @@ public class BatteryMeterView extends View implements DemoMode, } @Override - public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { + public void onBatteryLevelChanged(boolean present, int level, boolean pluggedIn, + boolean charging) { // TODO: Use this callback instead of own broadcast receiver. } @@ -207,36 +301,80 @@ public class BatteryMeterView extends View implements DemoMode, invalidate(); } - private static float[] loadBoltPoints(Resources res) { - final int[] pts = res.getIntArray(R.array.batterymeter_bolt_points); - int maxX = 0, maxY = 0; - for (int i = 0; i < pts.length; i += 2) { - maxX = Math.max(maxX, pts[i]); - maxY = Math.max(maxY, pts[i + 1]); + public void setAnimationsEnabled(boolean enabled) { + if (mAnimationsEnabled != enabled) { + mAnimationsEnabled = enabled; + setLayerType(mAnimationsEnabled ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, null); + invalidate(); } - final float[] ptsF = new float[pts.length]; - for (int i = 0; i < pts.length; i += 2) { - ptsF[i] = (float)pts[i] / maxX; - ptsF[i + 1] = (float)pts[i + 1] / maxY; + } + + @Override + public void onBatteryStyleChanged(int style, int percentMode) { + boolean showInsidePercent = percentMode == BatteryController.PERCENTAGE_MODE_INSIDE; + BatteryMeterMode meterMode = BatteryMeterMode.BATTERY_METER_ICON_PORTRAIT; + + switch (style) { + case BatteryController.STYLE_CIRCLE: + meterMode = BatteryMeterMode.BATTERY_METER_CIRCLE; + break; + case BatteryController.STYLE_GONE: + meterMode = BatteryMeterMode.BATTERY_METER_GONE; + showInsidePercent = false; + break; + case BatteryController.STYLE_ICON_LANDSCAPE: + meterMode = BatteryMeterMode.BATTERY_METER_ICON_LANDSCAPE; + break; + case BatteryController.STYLE_TEXT: + meterMode = BatteryMeterMode.BATTERY_METER_TEXT; + showInsidePercent = false; + break; + default: + break; } - return ptsF; + + setMode(meterMode); + mShowPercent = showInsidePercent; + invalidate(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); mHeight = h; mWidth = w; - mWarningTextPaint.setTextSize(h * 0.75f); - mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent; + if (mBatteryMeterDrawable != null) { + mBatteryMeterDrawable.onSizeChanged(w, h, oldw, oldh); + } } - private void updateShowPercent() { - mShowPercent = 0 != Settings.System.getInt(getContext().getContentResolver(), - SHOW_PERCENT_SETTING, 0); - } + public void setMode(BatteryMeterMode mode) { + if (mMeterMode == mode) { + return; + } - private int getColorForLevel(int percent) { + mMeterMode = mode; + BatteryTracker tracker = mDemoMode ? mDemoTracker : mTracker; + if (mode == BatteryMeterMode.BATTERY_METER_GONE || + mode == BatteryMeterMode.BATTERY_METER_TEXT) { + setVisibility(View.GONE); + mBatteryMeterDrawable = null; + } else { + if (mBatteryMeterDrawable != null) { + mBatteryMeterDrawable.onDispose(); + } + mBatteryMeterDrawable = createBatteryMeterDrawable(mode); + if (tracker.present) { + setVisibility(View.VISIBLE); + requestLayout(); + invalidate(); + } else { + setVisibility(View.GONE); + } + } + } + public int getColorForLevel(int percent) { // If we are in power save mode, always use the normal color. if (mPowerSaveEnabled) { return mColors[mColors.length-1]; @@ -259,13 +397,11 @@ public class BatteryMeterView extends View implements DemoMode, } public void setDarkIntensity(float darkIntensity) { - int backgroundColor = getBackgroundColor(darkIntensity); - int fillColor = getFillColor(darkIntensity); - mIconTint = fillColor; - mFramePaint.setColor(backgroundColor); - mBoltPaint.setColor(fillColor); - mChargeColor = fillColor; - invalidate(); + if (mBatteryMeterDrawable != null) { + mCurrentBackgroundColor = getBackgroundColor(darkIntensity); + mCurrentFillColor = getFillColor(darkIntensity); + mBatteryMeterDrawable.setDarkIntensity(mCurrentBackgroundColor, mCurrentFillColor); + } } private int getBackgroundColor(float darkIntensity) { @@ -283,261 +419,425 @@ public class BatteryMeterView extends View implements DemoMode, } @Override - public void draw(Canvas c) { - BatteryTracker tracker = mDemoMode ? mDemoTracker : mTracker; - final int level = tracker.level; - - if (level == BatteryTracker.UNKNOWN_LEVEL) return; - - float drawFrac = (float) level / 100f; - final int pt = getPaddingTop(); - final int pl = getPaddingLeft(); - final int pr = getPaddingRight(); - final int pb = getPaddingBottom(); - final int height = mHeight - pt - pb; - final int width = mWidth - pl - pr; - - final int buttonHeight = (int) (height * mButtonHeightFraction); - - mFrame.set(0, 0, width, height); - mFrame.offset(pl, pt); - - // button-frame: area above the battery body - mButtonFrame.set( - mFrame.left + Math.round(width * 0.25f), - mFrame.top, - mFrame.right - Math.round(width * 0.25f), - mFrame.top + buttonHeight); - - mButtonFrame.top += mSubpixelSmoothingLeft; - mButtonFrame.left += mSubpixelSmoothingLeft; - mButtonFrame.right -= mSubpixelSmoothingRight; - - // frame: battery body area - mFrame.top += buttonHeight; - mFrame.left += mSubpixelSmoothingLeft; - mFrame.top += mSubpixelSmoothingLeft; - mFrame.right -= mSubpixelSmoothingRight; - mFrame.bottom -= mSubpixelSmoothingRight; - - // set the battery charging color - mBatteryPaint.setColor(tracker.plugged ? mChargeColor : getColorForLevel(level)); - - if (level >= FULL) { - drawFrac = 1f; - } else if (level <= mCriticalLevel) { - drawFrac = 0f; - } - - final float levelTop = drawFrac == 1f ? mButtonFrame.top - : (mFrame.top + (mFrame.height() * (1f - drawFrac))); - - // define the battery shape - mShapePath.reset(); - mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top); - mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top); - mShapePath.lineTo(mButtonFrame.right, mFrame.top); - mShapePath.lineTo(mFrame.right, mFrame.top); - mShapePath.lineTo(mFrame.right, mFrame.bottom); - mShapePath.lineTo(mFrame.left, mFrame.bottom); - mShapePath.lineTo(mFrame.left, mFrame.top); - mShapePath.lineTo(mButtonFrame.left, mFrame.top); - mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top); - - if (tracker.plugged) { - // define the bolt shape - final float bl = mFrame.left + mFrame.width() / 4.5f; - final float bt = mFrame.top + mFrame.height() / 6f; - final float br = mFrame.right - mFrame.width() / 7f; - final float bb = mFrame.bottom - mFrame.height() / 10f; - if (mBoltFrame.left != bl || mBoltFrame.top != bt - || mBoltFrame.right != br || mBoltFrame.bottom != bb) { - mBoltFrame.set(bl, bt, br, bb); - mBoltPath.reset(); - mBoltPath.moveTo( - mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), - mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); - for (int i = 2; i < mBoltPoints.length; i += 2) { - mBoltPath.lineTo( - mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(), - mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height()); + protected void onDraw(Canvas canvas) { + if (mBatteryMeterDrawable != null) { + BatteryTracker tracker = mDemoMode ? mDemoTracker : mTracker; + mBatteryMeterDrawable.onDraw(canvas, tracker); + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + @Override + public void dispatchDemoCommand(String command, Bundle args) { + if (getVisibility() == View.VISIBLE) { + if (!mDemoMode && command.equals(COMMAND_ENTER)) { + mDemoMode = true; + mDemoTracker.level = mTracker.level; + mDemoTracker.plugged = mTracker.plugged; + } else if (mDemoMode && command.equals(COMMAND_EXIT)) { + mDemoMode = false; + postInvalidate(); + } else if (mDemoMode && command.equals(COMMAND_BATTERY)) { + String level = args.getString("level"); + String plugged = args.getString("plugged"); + if (level != null) { + mDemoTracker.level = Math.min(Math.max(Integer.parseInt(level), 0), 100); + } + if (plugged != null) { + mDemoTracker.plugged = Boolean.parseBoolean(plugged); } - mBoltPath.lineTo( - mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), - mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); + postInvalidate(); } + } + } + + protected interface BatteryMeterDrawable { + void onDraw(Canvas c, BatteryTracker tracker); + void onSizeChanged(int w, int h, int oldw, int oldh); + void onDispose(); + void setDarkIntensity(int backgroundColor, int fillColor); + } + + protected class AllInOneBatteryMeterDrawable implements BatteryMeterDrawable { + private static final boolean SINGLE_DIGIT_PERCENT = false; + private static final boolean SHOW_100_PERCENT = false; + + private boolean mDisposed; - float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top); - boltPct = Math.min(Math.max(boltPct, 0), 1); - if (boltPct <= BOLT_LEVEL_THRESHOLD) { - // draw the bolt if opaque - c.drawPath(mBoltPath, mBoltPaint); + private boolean mIsAnimating; // stores charge-animation status to remove callbacks + + private float mTextX, mTextY; // precalculated position for drawText() to appear centered + + private boolean mInitialized; + + private Paint mTextAndBoltPaint; + private Paint mWarningTextPaint; + private Paint mClearPaint; + + private LayerDrawable mBatteryDrawable; + private Drawable mFrameDrawable; + private StopMotionVectorDrawable mLevelDrawable; + private Drawable mBoltDrawable; + + private BatteryMeterMode mMode; + private int mTextGravity; + + public AllInOneBatteryMeterDrawable(Resources res, BatteryMeterMode mode) { + super(); + + loadBatteryDrawables(res, mode); + + mMode = mode; + mDisposed = false; + + // load text gravity and blend mode + int[] attrs = new int[] {android.R.attr.gravity, R.attr.blendMode}; + int resId = getBatteryDrawableStyleResourceForMode(mode); + PorterDuff.Mode xferMode = PorterDuff.Mode.XOR; + if (resId != 0) { + TypedArray a = getContext().obtainStyledAttributes( + getBatteryDrawableStyleResourceForMode(mode), attrs); + mTextGravity = a.getInt(0, Gravity.CENTER); + xferMode = PorterDuff.intToMode(a.getInt(1, + PorterDuff.modeToInt(PorterDuff.Mode.XOR))); } else { - // otherwise cut the bolt out of the overall shape - mShapePath.op(mBoltPath, Path.Op.DIFFERENCE); + mTextGravity = Gravity.CENTER; } + Log.d(TAG, "mTextGravity=" + mTextGravity); + + mTextAndBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD); + mTextAndBoltPaint.setTypeface(font); + mTextAndBoltPaint.setTextAlign(getPaintAlignmentFromGravity(mTextGravity)); + mTextAndBoltPaint.setXfermode(new PorterDuffXfermode(xferMode)); + mTextAndBoltPaint.setColor(mCurrentFillColor != 0 + ? mCurrentFillColor + : res.getColor(R.color.batterymeter_bolt_color)); + + mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mWarningTextPaint.setColor(mColors[1]); + font = Typeface.create("sans-serif", Typeface.BOLD); + mWarningTextPaint.setTypeface(font); + mWarningTextPaint.setTextAlign(getPaintAlignmentFromGravity(mTextGravity)); + + mClearPaint = new Paint(); + mClearPaint.setColor(0); } - // compute percentage text - boolean pctOpaque = false; - float pctX = 0, pctY = 0; - String pctText = null; - if (!tracker.plugged && level > mCriticalLevel && mShowPercent) { - mTextPaint.setColor(getColorForLevel(level)); - mTextPaint.setTextSize(height * - (SINGLE_DIGIT_PERCENT ? 0.75f - : (tracker.level == 100 ? 0.38f : 0.5f))); - mTextHeight = -mTextPaint.getFontMetrics().ascent; - pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); - pctX = mWidth * 0.5f; - pctY = (mHeight + mTextHeight) * 0.47f; - pctOpaque = levelTop > pctY; - if (!pctOpaque) { - mTextPath.reset(); - mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath); - // cut the percentage text out of the overall shape - mShapePath.op(mTextPath, Path.Op.DIFFERENCE); + @Override + public void onDraw(Canvas c, BatteryTracker tracker) { + if (mDisposed) return; + + if (!mInitialized) { + init(); + } + + drawBattery(c, tracker); + if (mAnimationsEnabled) { + // TODO: Allow custom animations to be used } } - // draw the battery shape background - c.drawPath(mShapePath, mFramePaint); + @Override + public void onDispose() { + mDisposed = true; + } - // draw the battery shape, clipped to charging level - mFrame.top = levelTop; - mClipPath.reset(); - mClipPath.addRect(mFrame, Path.Direction.CCW); - mShapePath.op(mClipPath, Path.Op.INTERSECT); - c.drawPath(mShapePath, mBatteryPaint); + @Override + public void setDarkIntensity(int backgroundColor, int fillColor) { + mIconTint = fillColor; + // Make bolt fully opaque for increased visibility + mBoltDrawable.setTint(0xff000000 | fillColor); + mFrameDrawable.setTint(backgroundColor); + updateBoltDrawableLayer(mBatteryDrawable, mBoltDrawable); + invalidate(); + } - if (!tracker.plugged) { - if (level <= mCriticalLevel) { - // draw the warning text - final float x = mWidth * 0.5f; - final float y = (mHeight + mWarningTextHeight) * 0.48f; - c.drawText(mWarningString, x, y, mWarningTextPaint); - } else if (pctOpaque) { - // draw the percentage text - c.drawText(pctText, pctX, pctY, mTextPaint); + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + init(); + } + + private boolean isThemeApplied() { + ThemeConfig themeConfig = ThemeConfig.getBootTheme(getContext().getContentResolver()); + return themeConfig != null && + !ThemeConfig.SYSTEM_DEFAULT.equals(themeConfig.getOverlayForStatusBar()); + } + + private void checkBatteryMeterDrawableValid(Resources res, BatteryMeterMode mode) { + final int resId = getBatteryDrawableResourceForMode(mode); + final Drawable batteryDrawable; + try { + batteryDrawable = res.getDrawable(resId); + } catch (Resources.NotFoundException e) { + throw new BatteryMeterDrawableException(res.getResourceName(resId) + " is an " + + "invalid drawable", e); + } + + // check that the drawable is a LayerDrawable + if (!(batteryDrawable instanceof LayerDrawable)) { + throw new BatteryMeterDrawableException("Expected a LayerDrawable but received a " + + batteryDrawable.getClass().getSimpleName()); + } + + final LayerDrawable layerDrawable = (LayerDrawable) batteryDrawable; + final Drawable frame = layerDrawable.findDrawableByLayerId(R.id.battery_frame); + final Drawable level = layerDrawable.findDrawableByLayerId(R.id.battery_fill); + final Drawable bolt = layerDrawable.findDrawableByLayerId( + R.id.battery_charge_indicator); + // now check that the required layers exist and are of the correct type + if (frame == null) { + throw new BatteryMeterDrawableException("Missing battery_frame drawble"); + } + if (bolt == null) { + throw new BatteryMeterDrawableException( + "Missing battery_charge_indicator drawable"); + } + if (level != null) { + // check that the level drawable is an AnimatedVectorDrawable + if (!(level instanceof AnimatedVectorDrawable)) { + throw new BatteryMeterDrawableException("Expected a AnimatedVectorDrawable " + + "but received a " + level.getClass().getSimpleName()); + } + // make sure we can stop motion animate the level drawable + try { + StopMotionVectorDrawable smvd = new StopMotionVectorDrawable(level); + smvd.setCurrentFraction(0.5f); + } catch (Exception e) { + throw new BatteryMeterDrawableException("Unable to perform stop motion on " + + "battery_fill drawable", e); + } + } else { + throw new BatteryMeterDrawableException("Missing battery_fill drawable"); } } - } - @Override - public boolean hasOverlappingRendering() { - return false; - } + private void loadBatteryDrawables(Resources res, BatteryMeterMode mode) { + if (isThemeApplied()) { + try { + checkBatteryMeterDrawableValid(res, mode); + } catch (BatteryMeterDrawableException e) { + Log.w(TAG, "Invalid themed battery meter drawable, falling back to system", e); + final Context context = getContext(); + PackageManager pm = getContext().getPackageManager(); + try { + res = pm.getThemedResourcesForApplication(context.getPackageName(), + ThemeConfig.SYSTEM_DEFAULT); + } catch (PackageManager.NameNotFoundException nnfe) { + /* ignore, this should not happen */ + } + } + } - private boolean mDemoMode; - private BatteryTracker mDemoTracker = new BatteryTracker(); + int drawableResId = getBatteryDrawableResourceForMode(mode); + mBatteryDrawable = (LayerDrawable) res.getDrawable(drawableResId); + mFrameDrawable = mBatteryDrawable.findDrawableByLayerId(R.id.battery_frame); + mFrameDrawable.setTint(mCurrentBackgroundColor != 0 + ? mCurrentBackgroundColor + : res.getColor(R.color.batterymeter_frame_color)); + // set the animated vector drawable we will be stop animating + Drawable levelDrawable = mBatteryDrawable.findDrawableByLayerId(R.id.battery_fill); + mLevelDrawable = new StopMotionVectorDrawable(levelDrawable); + mBoltDrawable = mBatteryDrawable.findDrawableByLayerId(R.id.battery_charge_indicator); + } - @Override - public void dispatchDemoCommand(String command, Bundle args) { - if (!mDemoMode && command.equals(COMMAND_ENTER)) { - mDemoMode = true; - mDemoTracker.level = mTracker.level; - mDemoTracker.plugged = mTracker.plugged; - } else if (mDemoMode && command.equals(COMMAND_EXIT)) { - mDemoMode = false; - postInvalidate(); - } else if (mDemoMode && command.equals(COMMAND_BATTERY)) { - String level = args.getString("level"); - String plugged = args.getString("plugged"); - if (level != null) { - mDemoTracker.level = Math.min(Math.max(Integer.parseInt(level), 0), 100); - } - if (plugged != null) { - mDemoTracker.plugged = Boolean.parseBoolean(plugged); - } - postInvalidate(); + private void drawBattery(Canvas canvas, BatteryTracker tracker) { + boolean unknownStatus = tracker.status == BatteryManager.BATTERY_STATUS_UNKNOWN; + int level = tracker.level; + + if (unknownStatus || tracker.status == BatteryManager.BATTERY_STATUS_FULL) { + level = 100; + } + + mTextAndBoltPaint.setColor(getColorForLevel(level)); + + // Make sure we don't draw the charge indicator if not plugged in + Drawable d = mBatteryDrawable.findDrawableByLayerId(R.id.battery_charge_indicator); + if (d instanceof BitmapDrawable) { + // In case we are using a BitmapDrawable, which we should be unless something bad + // happened, we need to change the paint rather than the alpha in case the blendMode + // has been set to clear. Clear always clears regardless of alpha level ;) + BitmapDrawable bd = (BitmapDrawable) d; + bd.getPaint().set(tracker.plugged ? mTextAndBoltPaint : mClearPaint); + } else { + d.setAlpha(tracker.plugged ? 255 : 0); + } + + // Now draw the level indicator + // set the level and tint color of the fill drawable + mLevelDrawable.setCurrentFraction(level / 100f); + mLevelDrawable.setTint(getColorForLevel(level)); + mBatteryDrawable.draw(canvas); + + // if chosen by options, draw percentage text in the middle + // always skip percentage when 100, so layout doesnt break + if (unknownStatus) { + mTextAndBoltPaint.setColor(getContext().getColor(R.color.batterymeter_frame_color)); + canvas.drawText("?", mTextX, mTextY, mTextAndBoltPaint); + + } else if (!tracker.plugged) { + drawPercentageText(canvas, tracker); + } } - } - private final class BatteryTracker extends BroadcastReceiver { - public static final int UNKNOWN_LEVEL = -1; + private void drawPercentageText(Canvas canvas, BatteryTracker tracker) { + final int level = tracker.level; + if (level > mCriticalLevel + && (mShowPercent && !(level == 100 && !SHOW_100_PERCENT))) { + // draw the percentage text + String pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); + mTextAndBoltPaint.setColor(getColorForLevel(level)); + canvas.drawText(pctText, mTextX, mTextY, mTextAndBoltPaint); + } else if (level <= mCriticalLevel) { + // draw the warning text + canvas.drawText(mWarningString, mTextX, mTextY, mWarningTextPaint); + } + } - // 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; + /** + * initializes all size dependent variables + */ + private void init() { + // not much we can do with zero width or height, we'll get another pass later + if (mWidth <= 0 || mHeight <=0) return; + + final float widthDiv2 = mWidth / 2f; + // text size is width / 2 - 2dp for wiggle room + final float textSize = widthDiv2 - getResources().getDisplayMetrics().density * 2; + mTextAndBoltPaint.setTextSize(textSize); + mWarningTextPaint.setTextSize(textSize); + + int pLeft = getPaddingLeft(); + Rect iconBounds = new Rect(pLeft, 0, pLeft + mWidth, mHeight); + mBatteryDrawable.setBounds(iconBounds); + + // calculate text position + Rect bounds = new Rect(); + mTextAndBoltPaint.getTextBounds("99", 0, "99".length(), bounds); + boolean isRtl = isLayoutRtl(); + + // compute mTextX based on text gravity + if ((mTextGravity & Gravity.START) == Gravity.START) { + mTextX = isRtl ? mWidth : 0; + } else if ((mTextGravity & Gravity.END) == Gravity.END) { + mTextX = isRtl ? 0 : mWidth; + } else if ((mTextGravity & Gravity.LEFT) == Gravity.LEFT) { + mTextX = 0; + }else if ((mTextGravity & Gravity.RIGHT) == Gravity.RIGHT) { + mTextX = mWidth; + } else { + mTextX = widthDiv2 + pLeft; + } - @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; + // compute mTextY based on text gravity + if ((mTextGravity & Gravity.TOP) == Gravity.TOP) { + mTextY = bounds.height(); + } else if ((mTextGravity & Gravity.BOTTOM) == Gravity.BOTTOM) { + mTextY = mHeight; + } else { + mTextY = widthDiv2 + bounds.height() / 2.0f; + } - level = (int)(100f - * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) - / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); + updateBoltDrawableLayer(mBatteryDrawable, mBoltDrawable); - 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); + mInitialized = true; + } - 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); + private int getBatteryDrawableResourceForMode(BatteryMeterMode mode) { + switch (mode) { + case BATTERY_METER_ICON_LANDSCAPE: + return R.drawable.ic_battery_landscape; + case BATTERY_METER_CIRCLE: + return R.drawable.ic_battery_circle; + case BATTERY_METER_ICON_PORTRAIT: + return R.drawable.ic_battery_portrait; + default: + return 0; + } + } - if (!testmode) return; + private int getBatteryDrawableStyleResourceForMode(BatteryMeterMode mode) { + switch (mode) { + case BATTERY_METER_ICON_LANDSCAPE: + return R.style.BatteryMeterViewDrawable_Landscape; + case BATTERY_METER_CIRCLE: + return R.style.BatteryMeterViewDrawable_Circle; + case BATTERY_METER_ICON_PORTRAIT: + return R.style.BatteryMeterViewDrawable_Portrait; + default: + return R.style.BatteryMeterViewDrawable; + } + } - curLevel += incr; - if (curLevel == 100) { - incr *= -1; - } - postDelayed(this, 200); - } - }); + private Paint.Align getPaintAlignmentFromGravity(int gravity) { + boolean isRtl = isLayoutRtl(); + if ((gravity & Gravity.START) == Gravity.START) { + return isRtl ? Paint.Align.RIGHT : Paint.Align.LEFT; + } + if ((gravity & Gravity.END) == Gravity.END) { + return isRtl ? Paint.Align.LEFT : Paint.Align.RIGHT; } + if ((gravity & Gravity.LEFT) == Gravity.LEFT) return Paint.Align.LEFT; + if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) return Paint.Align.RIGHT; + + // default to center + return Paint.Align.CENTER; } - } - private final class SettingObserver extends ContentObserver { - public SettingObserver() { - super(new Handler()); + // Creates a BitmapDrawable of the bolt so we can make use of the XOR xfer mode with vector + // based drawables + private void updateBoltDrawableLayer(LayerDrawable batteryDrawable, Drawable boltDrawable) { + BitmapDrawable newBoltDrawable; + if (boltDrawable instanceof BitmapDrawable) { + newBoltDrawable = (BitmapDrawable) boltDrawable.mutate(); + } else { + Bitmap boltBitmap = createBoltBitmap(boltDrawable); + if (boltBitmap == null) { + // not much to do with a null bitmap so keep original bolt for now + return; + } + Rect bounds = boltDrawable.getBounds(); + newBoltDrawable = new BitmapDrawable(getResources(), boltBitmap); + newBoltDrawable.setBounds(bounds); + } + newBoltDrawable.getPaint().set(mTextAndBoltPaint); + batteryDrawable.setDrawableByLayerId(R.id.battery_charge_indicator, newBoltDrawable); } - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - updateShowPercent(); - postInvalidate(); + private Bitmap createBoltBitmap(Drawable boltDrawable) { + // not much we can do with zero width or height, we'll get another pass later + if (mWidth <= 0 || mHeight <= 0) return null; + + Bitmap bolt; + if (!(boltDrawable instanceof BitmapDrawable)) { + int pLeft = getPaddingLeft(); + Rect iconBounds = new Rect(pLeft, 0, pLeft + mWidth, mHeight); + bolt = Bitmap.createBitmap(iconBounds.width(), iconBounds.height(), + Bitmap.Config.ARGB_8888); + if (bolt != null) { + Canvas c = new Canvas(bolt); + c.drawColor(-1, PorterDuff.Mode.CLEAR); + boltDrawable.draw(c); + } + } else { + bolt = ((BitmapDrawable) boltDrawable).getBitmap(); + } + + return bolt; } - } + private class BatteryMeterDrawableException extends RuntimeException { + public BatteryMeterDrawableException(String detailMessage) { + super(detailMessage); + } + + public BatteryMeterDrawableException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/DockBatteryLevelTextView.java b/packages/SystemUI/src/com/android/systemui/DockBatteryLevelTextView.java new file mode 100644 index 0000000..1678e94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/DockBatteryLevelTextView.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.content.Context; +import android.graphics.Paint; +import android.util.AttributeSet; + +public class DockBatteryLevelTextView extends BatteryLevelTextView { + + public DockBatteryLevelTextView(Context context, AttributeSet attrs) { + super(context, attrs); + setPaintFlags(getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/DockBatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/DockBatteryMeterView.java new file mode 100755 index 0000000..a29b16c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/DockBatteryMeterView.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.content.Context; +import android.content.Intent; +import android.os.BatteryManager; +import android.util.AttributeSet; +import android.view.View; + +public class DockBatteryMeterView extends BatteryMeterView { + + private BatteryManager mBatteryService; + private final boolean mSupported; + + private class DockBatteryTracker extends BatteryTracker { + + public DockBatteryTracker() { + super(); + present = 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; + + if (mSupported) { + level = (int)(100f + * intent.getIntExtra(BatteryManager.EXTRA_DOCK_LEVEL, 0) + / intent.getIntExtra(BatteryManager.EXTRA_DOCK_SCALE, 100)); + + present = intent.getBooleanExtra(BatteryManager.EXTRA_DOCK_PRESENT, false); + plugType = intent.getIntExtra(BatteryManager.EXTRA_DOCK_PLUGGED, 0); + // We need to add a extra check over the status because of dock batteries + // PlugType doesn't means that the dock battery is charging (some devices + // doesn't charge under dock usb) + plugged = plugType != 0 && (status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL); + health = intent.getIntExtra(BatteryManager.EXTRA_DOCK_HEALTH, + BatteryManager.BATTERY_HEALTH_UNKNOWN); + status = intent.getIntExtra(BatteryManager.EXTRA_DOCK_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN); + technology = intent.getStringExtra(BatteryManager.EXTRA_DOCK_TECHNOLOGY); + voltage = intent.getIntExtra(BatteryManager.EXTRA_DOCK_VOLTAGE, 0); + temperature = intent.getIntExtra(BatteryManager.EXTRA_DOCK_TEMPERATURE, 0); + + + if (present && (mMeterMode != BatteryMeterMode.BATTERY_METER_GONE && + mMeterMode != BatteryMeterMode.BATTERY_METER_TEXT)) { + setContentDescription(context.getString( + R.string.accessibility_dock_battery_level, level)); + setVisibility(View.VISIBLE); + invalidate(); + } else { + setContentDescription(null); + setVisibility(View.GONE); + } + } else { + setContentDescription(null); + setVisibility(View.GONE); + + // If dock is not supported then we don't need this receiver anymore + getContext().unregisterReceiver(this); + } + } 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_DOCK_PLUGGED_AC : 0); + dummy.putExtra("testmode", true); + } + getContext().sendBroadcast(dummy); + + if (!testmode) return; + + curLevel += incr; + if (curLevel == 100) { + incr *= -1; + } + postDelayed(this, 200); + } + }); + } + } + } + + public DockBatteryMeterView(Context context) { + this(context, null, 0); + } + + public DockBatteryMeterView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DockBatteryMeterView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mBatteryService = ((BatteryManager) context.getSystemService(Context.BATTERY_SERVICE)); + mSupported = mBatteryService.isDockBatterySupported(); + mDemoTracker = new DockBatteryTracker(); + mTracker = new DockBatteryTracker(); + } + + @Override + public void onDetachedFromWindow() { + // We already unregistered the listener once when we decided + // support was absent. Don't do it again. + if (mSupported) { + super.onDetachedFromWindow(); + } + } + + @Override + public void setMode(BatteryMeterMode mode) { + super.setMode(mode); + int visibility = getVisibility(); + if (visibility == View.VISIBLE && !mSupported) { + setVisibility(View.GONE); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags index a584cf6..a08d4b7 100644 --- a/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags +++ b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags @@ -51,3 +51,28 @@ option java_package com.android.systemui; # SearchPanelView.java # --------------------------- 36050 sysui_searchpanel_touch (type|1),(x|1),(y|1) + +# --------------------------- +# LiveLockScreenController.java +# --------------------------- +# sysui_lls_keyguard_showing +## screenOn: 0:screen turned off +## 1:screen turned on +36060 sysui_lls_keyguard_showing (screenOn|1) +# sysui_lls_keyguard_dismissed: Logged when user unlocks the device +## onLls: 0:dismissed while showing notifications +## 1:dismissed while user interacting with LLS +36061 sysui_lls_keyguard_dismissed (onLls|1) +# sysui_lls_notification_panel_shown: Logged when the notification panel is swiped in and out +## shown: 0:panel is hidden +## 1:panel is visible +36062 sysui_lls_notification_panel_shown (shown|1) + +# --------------------------- +# RecentsView.java +# --------------------------- +36070 sysui_recents_event (what|1) +## what: 1: OPEN +## 2: CLOSE +## 3: CHOSE_TASK +## 4: CLOSE_ALL_TASKS diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java index 1dca149..5911916 100644 --- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java +++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java @@ -36,6 +36,7 @@ import android.view.ViewConfiguration; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.FlingAnimationUtils; +import com.android.systemui.statusbar.MediaExpandableNotificationRow; import com.android.systemui.statusbar.policy.ScrollAdapter; public class ExpandHelper implements Gefingerpoken { @@ -96,7 +97,7 @@ public class ExpandHelper implements Gefingerpoken { private float mCurrentHeight; private int mSmallSize; - private int mLargeSize; + private int mLargeSize, mInitialLargeSize; private float mMaximumStretch; private boolean mOnlyMovements; @@ -161,6 +162,7 @@ public class ExpandHelper implements Gefingerpoken { mSmallSize = small; mMaximumStretch = mSmallSize * STRETCH_INTERVAL; mLargeSize = large; + mInitialLargeSize = large; mContext = context; mCallback = callback; mScaler = new ViewScaler(); @@ -511,7 +513,14 @@ public class ExpandHelper implements Gefingerpoken { mCurrentHeight = mOldHeight; if (mCallback.canChildBeExpanded(v)) { if (DEBUG) Log.d(TAG, "working on an expandable child"); - mNaturalHeight = mScaler.getNaturalHeight(mLargeSize); + if (v instanceof MediaExpandableNotificationRow) { + final int maxHeight = ((MediaExpandableNotificationRow) v).getMaxContentHeight(); + mLargeSize = maxHeight; + mNaturalHeight = maxHeight; + } else { + mLargeSize = mInitialLargeSize; + mNaturalHeight = mScaler.getNaturalHeight(mLargeSize); + } } else { if (DEBUG) Log.d(TAG, "working on a non-expandable child"); mNaturalHeight = mOldHeight; diff --git a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java index 8556afc..5a6f3c9 100644 --- a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java @@ -236,7 +236,9 @@ public class ImageWallpaper extends WallpaperService { Log.d(TAG, "Visibility changed to visible=" + visible); } mVisible = visible; - drawFrame(); + if (visible) { + drawFrame(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index 33f6564..c3e5043 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -45,9 +45,15 @@ public class SwipeHelper implements Gefingerpoken { public static final int X = 0; public static final int Y = 1; + public static final int SWIPE_ZONE_LEFT = 0x1; + public static final int SWIPE_ZONE_RIGHT = 0x2; + public static final int SWIPE_ZONE_TOP = 0x4; + public static final int SWIPE_ZONE_BOTTOM = 0x8; private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); private final Interpolator mFastOutLinearInInterpolator; + private final int mTouchSlop; + private int mSwipeZone; private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms @@ -69,6 +75,7 @@ public class SwipeHelper implements Gefingerpoken { private VelocityTracker mVelocityTracker; private float mInitialTouchPos; + private float mPerpendicularInitialTouchPos; private boolean mDragging; private View mCurrView; private View mCurrAnimView; @@ -84,6 +91,8 @@ public class SwipeHelper implements Gefingerpoken { private int mFalsingThreshold; private boolean mTouchAboveFalsingThreshold; + private float mSwipeProgressFadeEnd; + public SwipeHelper(int swipeDirection, Callback callback, Context context) { mCallback = callback; mHandler = new Handler(); @@ -91,12 +100,28 @@ public class SwipeHelper implements Gefingerpoken { mVelocityTracker = VelocityTracker.obtain(); mDensityScale = context.getResources().getDisplayMetrics().density; mPagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press! mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in); mFalsingThreshold = context.getResources().getDimensionPixelSize( R.dimen.swipe_helper_falsing_threshold); + if (swipeDirection == X) { + mSwipeZone = SWIPE_ZONE_LEFT | SWIPE_ZONE_RIGHT; + } else { + mSwipeZone = SWIPE_ZONE_TOP | SWIPE_ZONE_BOTTOM; + } + mSwipeProgressFadeEnd = SWIPE_PROGRESS_FADE_END; + } + + public SwipeHelper(int swipeDirection, int swipeZone, Callback callback, Context context) { + this(swipeDirection, callback, context); + mSwipeZone = swipeZone; + } + + public boolean isDragging() { + return mDragging; } public void setLongPressListener(LongPressListener listener) { @@ -115,6 +140,10 @@ public class SwipeHelper implements Gefingerpoken { return mSwipeDirection == X ? ev.getX() : ev.getY(); } + private float getPerpendicularPos(MotionEvent ev) { + return mSwipeDirection == X ? ev.getY() : ev.getX(); + } + private float getTranslation(View v) { return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); } @@ -158,7 +187,7 @@ public class SwipeHelper implements Gefingerpoken { private float getSwipeProgressForOffset(View view) { float viewSize = getSize(view); - final float fadeSize = SWIPE_PROGRESS_FADE_END * viewSize; + final float fadeSize = mSwipeProgressFadeEnd * viewSize; float result = 1.0f; float pos = getTranslation(view); if (pos >= viewSize * SWIPE_PROGRESS_FADE_START) { @@ -236,6 +265,7 @@ public class SwipeHelper implements Gefingerpoken { mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); mVelocityTracker.addMovement(ev); mInitialTouchPos = getPos(ev); + mPerpendicularInitialTouchPos = getPerpendicularPos(ev); if (mLongPressListener != null) { if (mWatchLongPress == null) { @@ -413,11 +443,30 @@ public class SwipeHelper implements Gefingerpoken { case MotionEvent.ACTION_OUTSIDE: case MotionEvent.ACTION_MOVE: if (mCurrView != null) { + float pos = getPos(ev); + float altPos = getPerpendicularPos(ev); float delta = getPos(ev) - mInitialTouchPos; float absDelta = Math.abs(delta); if (absDelta >= getFalsingThreshold()) { mTouchAboveFalsingThreshold = true; } + + boolean touchBeyondZoneLimit = true; + if (mSwipeDirection == X) { + if ((mSwipeZone & SWIPE_ZONE_RIGHT) == 0 && pos > mInitialTouchPos) { + touchBeyondZoneLimit = false; + } else if ((mSwipeZone & SWIPE_ZONE_LEFT) == 0 && pos < mInitialTouchPos) { + touchBeyondZoneLimit = false; + } + } else { + if ((mSwipeZone & SWIPE_ZONE_TOP) == 0 && altPos < mPerpendicularInitialTouchPos) { + touchBeyondZoneLimit = false; + } else if ((mSwipeZone & SWIPE_ZONE_BOTTOM) == 0 && pos > mInitialTouchPos) { + touchBeyondZoneLimit = false; + } + } + if (!touchBeyondZoneLimit) return false; + // don't let items that can't be dismissed be dragged more than // maxScrollDistance if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { @@ -470,6 +519,10 @@ public class SwipeHelper implements Gefingerpoken { return true; } + public void setSwipeProgressFadeEnd(float end) { + mSwipeProgressFadeEnd = end; + } + private int getFalsingThreshold() { float factor = mCallback.getFalsingThresholdFactor(); return (int) (mFalsingThreshold * factor); @@ -505,6 +558,49 @@ public class SwipeHelper implements Gefingerpoken { float getFalsingThresholdFactor(); } + public static abstract class SimpleCallback implements Callback { + public abstract View getChildAtPosition(MotionEvent ev); + public abstract View getChildContentView(View v); + + @Override + public boolean canChildBeDismissed(View v) { + return false; + } + + @Override + public boolean isAntiFalsingNeeded() { + return false; + } + + @Override + public void onBeginDrag(View v) { + } + + @Override + public void onChildDismissed(View v) { + } + + @Override + public void onDragCancelled(View v) { + } + + @Override + public void onChildSnappedBack(View animView) { + } + + @Override + public boolean updateSwipeProgress(View animView, + boolean dismissable, + float swipeProgress) { + return false; + } + + @Override + public float getFalsingThresholdFactor() { + return 0; + } + } + /** * Equivalent to View.OnLongClickListener with coordinates */ diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 0b066af..565fbd7 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; +import android.content.res.ThemeConfig; import android.os.SystemProperties; import android.util.Log; @@ -58,6 +59,7 @@ public class SystemUIApplication extends Application { private boolean mServicesStarted; private boolean mBootCompleted; private final Map<Class<?>, Object> mComponents = new HashMap<Class<?>, Object>(); + private Configuration mConfig; @Override public void onCreate() { @@ -85,6 +87,7 @@ public class SystemUIApplication extends Application { } } }, filter); + mConfig = new Configuration(getResources().getConfiguration()); } /** @@ -134,6 +137,12 @@ public class SystemUIApplication extends Application { @Override public void onConfigurationChanged(Configuration newConfig) { + if (isThemeChange(mConfig, newConfig)) { + // theme resource changed so recreate styles and attributes + recreateTheme(); + } + + mConfig.setTo(newConfig); if (mServicesStarted) { int len = mServices.length; for (int i = 0; i < len; i++) { @@ -150,4 +159,11 @@ public class SystemUIApplication extends Application { public SystemUI[] getServices() { return mServices; } + + private static boolean isThemeChange(Configuration oldConfig, Configuration newConfig) { + final ThemeConfig oldThemeConfig = oldConfig != null ? oldConfig.themeConfig : null; + final ThemeConfig newThemeConfig = newConfig != null ? newConfig.themeConfig : null; + + return newThemeConfig != null && !newThemeConfig.equals(oldThemeConfig); + } } diff --git a/packages/SystemUI/src/com/android/systemui/cm/GlowBackground.java b/packages/SystemUI/src/com/android/systemui/cm/GlowBackground.java new file mode 100644 index 0000000..8b4bf06 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cm/GlowBackground.java @@ -0,0 +1,83 @@ +package com.android.systemui.cm; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.view.animation.AnticipateOvershootInterpolator; + +public class GlowBackground extends Drawable implements ValueAnimator.AnimatorUpdateListener { + + private static final int MAX_CIRCLE_SIZE = 150; + + private final Paint mPaint; + private Animator mAnimator; + private float mCircleSize; + + public GlowBackground(int color) { + mPaint = new Paint(); + mPaint.setColor(color); + } + + private void startAnimation(boolean hide) { + if (mAnimator != null) { + mAnimator.cancel(); + } + if (hide && mCircleSize == 0f) { + return; + } else if (!hide && mCircleSize == MAX_CIRCLE_SIZE) { + return; + } + mAnimator = getAnimator(hide); + mAnimator.start(); + } + + private Animator getAnimator(boolean hide) { + float start = mCircleSize; + float end = MAX_CIRCLE_SIZE; + if (hide) { + end = 0f; + } + ValueAnimator animator = ObjectAnimator.ofFloat(start, end); + animator.setInterpolator(new AnticipateOvershootInterpolator()); + animator.setDuration(300); + animator.addUpdateListener(this); + return animator; + } + + @Override + public void draw(Canvas canvas) { + canvas.drawCircle(getBounds().width() / 2, getBounds().height() / 2, mCircleSize, mPaint); + } + + @Override + public void setAlpha(int i) { + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + } + + @Override + public int getOpacity() { + return 0; + } + + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + mCircleSize = (Float) valueAnimator.getAnimatedValue(); + invalidateSelf(); + } + + public void hideGlow() { + startAnimation(true); + } + + public void showGlow() { + startAnimation(false); + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/cm/LockscreenShortcutsActivity.java b/packages/SystemUI/src/com/android/systemui/cm/LockscreenShortcutsActivity.java new file mode 100644 index 0000000..3cd86fe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cm/LockscreenShortcutsActivity.java @@ -0,0 +1,265 @@ +package com.android.systemui.cm; + +import com.android.internal.app.AssistUtils; +import com.android.settingslib.cm.ShortcutPickHelper; +import com.android.systemui.R; +import com.android.systemui.cm.LockscreenShortcutsHelper.Shortcuts; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; + +import java.util.ArrayList; + +public class LockscreenShortcutsActivity extends Activity implements View.OnClickListener, + ShortcutPickHelper.OnPickListener, View.OnTouchListener, LockscreenShortcutsHelper.OnChangeListener { + + private static final int[] sIconIds = new int[]{R.id.left_button, R.id.right_button}; + private static final String ACTION_APP = "action_app"; + + private ActionHolder mActions; + private ShortcutPickHelper mPicker; + private LockscreenShortcutsHelper mShortcutHelper; + private View mSelectedView; + private ColorMatrixColorFilter mFilter; + private ColorStateList mDefaultTintList; + private AssistUtils mAssistUtils; + + @Override + public void shortcutPicked(String uri, String friendlyName, boolean isApplication) { + onTargetChange(uri); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + GlowBackground background = (GlowBackground) v.getBackground(); + background.showGlow(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + background = (GlowBackground) v.getBackground(); + background.hideGlow(); + break; + } + return false; + } + + @Override + public void onChange() { + updateDrawables(); + } + + private class ActionHolder { + private ArrayList<CharSequence> mAvailableEntries = new ArrayList<CharSequence>(); + private ArrayList<String> mAvailableValues = new ArrayList<String>(); + + public void addAction(String action, int entryResId) { + mAvailableEntries.add(getString(entryResId)); + mAvailableValues.add(action); + } + + public int getActionIndex(String action) { + int count = mAvailableValues.size(); + for (int i = 0; i < count; i++) { + if (TextUtils.equals(mAvailableValues.get(i), action)) { + return i; + } + } + + return -1; + } + + public String getAction(int index) { + if (index > mAvailableValues.size()) { + return null; + } + + return mAvailableValues.get(index); + } + + public CharSequence[] getEntries() { + return mAvailableEntries.toArray(new CharSequence[mAvailableEntries.size()]); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.lockscreen_shortcuts); + getActionBar().setDisplayHomeAsUpEnabled(true); + + mPicker = new ShortcutPickHelper(this, this); + mShortcutHelper = new LockscreenShortcutsHelper(this, this); + ColorMatrix cm = new ColorMatrix(); + cm.setSaturation(0); + mFilter = new ColorMatrixColorFilter(cm); + ImageView unlockButton = (ImageView) findViewById(R.id.middle_button); + mDefaultTintList = unlockButton.getImageTintList(); + mAssistUtils = new AssistUtils(this); + createActionList(); + initiateViews(); + updateDrawables(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void initiateViews() { + int size = getResources().getDimensionPixelSize(R.dimen.lockscreen_icon_size); + for (int id : sIconIds) { + View v = findViewById(id); + v.setOnClickListener(this); + v.setOnTouchListener(this); + GlowBackground background = new GlowBackground(Color.WHITE); + background.setBounds(0, 0, size, size); + v.setBackground(background); + } + } + + private void updateDrawables() { + for (int i = 0; i < sIconIds.length; i++) { + int id = sIconIds[i]; + ImageView v = (ImageView) findViewById(id); + v.setImageTintList(null); + v.setColorFilter(null); + Shortcuts shortcut = Shortcuts.values()[i]; + v.setTag(mShortcutHelper.getUriForTarget(shortcut)); + Drawable drawable; + if (mShortcutHelper.isTargetEmpty(shortcut)) { + drawable = getResources().getDrawable(R.drawable.ic_lockscreen_shortcuts_blank); + } else { + if (shortcut == Shortcuts.LEFT_SHORTCUT && + !mShortcutHelper.isTargetCustom(shortcut)) { + drawable = getLeftAffordanceDrawable(); + v.setImageTintList(mDefaultTintList); + } else { + drawable = mShortcutHelper.getDrawableForTarget(shortcut); + if (drawable == null) { + drawable = (shortcut == Shortcuts.LEFT_SHORTCUT) ? + getLeftAffordanceDrawable() + : getResources().getDrawable(R.drawable.ic_camera_alt_24dp); + v.setImageTintList(mDefaultTintList); + } else { + v.setColorFilter(mFilter); + } + } + } + v.setImageDrawable(drawable); + } + } + + private Drawable getLeftAffordanceDrawable() { + Drawable drawable; + if (canLaunchVoiceAssist()) { + drawable = getResources().getDrawable(R.drawable.ic_mic_26dp); + } else { + drawable = getResources().getDrawable(R.drawable.ic_phone_24dp); + } + return drawable; + } + + private boolean canLaunchVoiceAssist() { + return mAssistUtils.activeServiceSupportsLaunchFromKeyguard(); + } + + private void createActionList() { + mActions = new ActionHolder(); + mActions.addAction(LockscreenShortcutsHelper.NONE, R.string.lockscreen_none_target); + mActions.addAction(LockscreenShortcutsHelper.DEFAULT, R.string.lockscreen_default_target); + mActions.addAction(ACTION_APP, R.string.select_application); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + mPicker.onActivityResult(requestCode, resultCode, data); + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void onClick(View v) { + mSelectedView = v; + + final GlowBackground background = (GlowBackground) mSelectedView.getBackground(); + + mSelectedView.postOnAnimation(new Runnable() { + @Override + public void run() { + background.showGlow(); + } + }); + + final DialogInterface.OnClickListener l = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int item) { + onTargetChange(mActions.getAction(item)); + dialog.dismiss(); + } + }; + + final DialogInterface.OnCancelListener cancel = new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + onTargetChange(null); + } + }; + + final AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle(R.string.lockscreen_choose_action_title) + .setItems(mActions.getEntries(), l) + .setOnCancelListener(cancel) + .create(); + + dialog.show(); + } + + private void onTargetChange(String uri) { + if (uri == null) { + if (mSelectedView != null) { + final GlowBackground background = (GlowBackground) mSelectedView.getBackground(); + background.hideGlow(); + } + return; + } + + if (uri.equals(ACTION_APP)) { + mPicker.pickShortcut(null, null, 0, false); + } else { + mSelectedView.setTag(uri); + saveCustomActions(); + GlowBackground background = (GlowBackground) mSelectedView.getBackground(); + background.hideGlow(); + } + } + + private void saveCustomActions() { + ArrayList<String> targets = new ArrayList<String>(); + for (int id : sIconIds) { + View v = findViewById(id); + String uri = (String) v.getTag(); + targets.add(uri); + } + mShortcutHelper.saveTargets(targets); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/cm/LockscreenShortcutsHelper.java b/packages/SystemUI/src/com/android/systemui/cm/LockscreenShortcutsHelper.java new file mode 100644 index 0000000..b47b69f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cm/LockscreenShortcutsHelper.java @@ -0,0 +1,201 @@ +package com.android.systemui.cm; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.graphics.ColorFilter; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +import com.android.systemui.R; +import cyanogenmod.providers.CMSettings; + +public class LockscreenShortcutsHelper { + + private Handler mHandler; + + public enum Shortcuts { + LEFT_SHORTCUT(0), + RIGHT_SHORTCUT(1); + + private final int index; + + Shortcuts(int index) { + this.index = index; + } + } + + public static final String DEFAULT = "default"; + public static final String NONE = "none"; + private static final String DELIMITER = "|"; + + private final Context mContext; + private OnChangeListener mListener; + private List<String> mTargetActivities; + + public interface OnChangeListener { + void onChange(); + } + + public LockscreenShortcutsHelper(Context context, OnChangeListener listener) { + mContext = context; + if (listener != null) { + mListener = listener; + mHandler = new Handler(Looper.getMainLooper()); + registerAndFetchTargets(); + } else { + fetchTargets(); + } + } + + public void registerAndFetchTargets() { + mContext.getContentResolver().registerContentObserver( + CMSettings.Secure.getUriFor(CMSettings.Secure.LOCKSCREEN_TARGETS), false, mObserver); + fetchTargets(); + } + + private ContentObserver mObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange, Uri uri) { + fetchTargets(); + if (mListener != null && mHandler != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onChange(); + } + }); + } + } + }; + + public void cleanup() { + mContext.getContentResolver().unregisterContentObserver(mObserver); + } + + public static class TargetInfo { + public Drawable icon; + public ColorFilter colorFilter; + public String uri; + public TargetInfo(Drawable icon, ColorFilter colorFilter, String uri) { + this.icon = icon; + this.colorFilter = colorFilter; + this.uri = uri; + } + } + + private void fetchTargets() { + mTargetActivities = CMSettings.Secure.getDelimitedStringAsList(mContext.getContentResolver(), + CMSettings.Secure.LOCKSCREEN_TARGETS, DELIMITER); + int itemsToPad = Shortcuts.values().length - mTargetActivities.size(); + if (itemsToPad > 0) { + for (int i = 0; i < itemsToPad; i++) { + mTargetActivities.add(DEFAULT); + } + } + } + + public Drawable getDrawableForTarget(Shortcuts shortcut) { + Intent intent = getIntent(shortcut); + if (intent != null) { + PackageManager pm = mContext.getPackageManager(); + ActivityInfo info = intent.resolveActivityInfo(pm, PackageManager.GET_ACTIVITIES); + if (info != null) { + return getScaledDrawable(info.loadIcon(pm)); + } + } + return null; + } + + public String getUriForTarget(Shortcuts shortcuts) { + return mTargetActivities.get(shortcuts.index); + } + + private Drawable getScaledDrawable(Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + Resources res = mContext.getResources(); + int width = res.getDimensionPixelSize(R.dimen.keyguard_affordance_icon_width); + int height = res.getDimensionPixelSize(R.dimen.keyguard_affordance_icon_height); + return new BitmapDrawable(mContext.getResources(), + Bitmap.createScaledBitmap(((BitmapDrawable) drawable).getBitmap(), + width, height, true)); + } else { + return drawable; + } + } + + private String getFriendlyActivityName(Intent intent, boolean labelOnly) { + PackageManager pm = mContext.getPackageManager(); + ActivityInfo ai = intent.resolveActivityInfo(pm, PackageManager.GET_ACTIVITIES); + String friendlyName = null; + if (ai != null) { + friendlyName = ai.loadLabel(pm).toString(); + if (friendlyName == null && !labelOnly) { + friendlyName = ai.name; + } + } + return friendlyName != null || labelOnly ? friendlyName : intent.toUri(0); + } + + private String getFriendlyShortcutName(Intent intent) { + String activityName = getFriendlyActivityName(intent, true); + String name = intent.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); + + if (activityName != null && name != null) { + return activityName + ": " + name; + } + return name != null ? name : intent.toUri(0); + } + + public String getFriendlyNameForUri(Shortcuts shortcut) { + Intent intent = getIntent(shortcut); + if (Intent.ACTION_MAIN.equals(intent.getAction())) { + return getFriendlyActivityName(intent, false); + } + return getFriendlyShortcutName(intent); + } + + public boolean isTargetCustom(Shortcuts shortcut) { + if (mTargetActivities == null || mTargetActivities.isEmpty()) { + return false; + } + String action = mTargetActivities.get(shortcut.index); + if (DEFAULT.equals(action)) { + return false; + } + + return NONE.equals(action) || getIntent(shortcut) != null; + } + + public boolean isTargetEmpty(Shortcuts shortcut) { + return mTargetActivities != null && + !mTargetActivities.isEmpty() && + mTargetActivities.get(shortcut.index).equals(NONE); + } + + public Intent getIntent(Shortcuts shortcut) { + Intent intent = null; + try { + intent = Intent.parseUri(mTargetActivities.get(shortcut.index), 0); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + return intent; + } + + public void saveTargets(ArrayList<String> targets) { + CMSettings.Secure.putListAsDelimitedString(mContext.getContentResolver(), + CMSettings.Secure.LOCKSCREEN_TARGETS, DELIMITER, targets); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cm/SpamMessageProvider.java b/packages/SystemUI/src/com/android/systemui/cm/SpamMessageProvider.java new file mode 100644 index 0000000..7afa04d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cm/SpamMessageProvider.java @@ -0,0 +1,204 @@ +package com.android.systemui.cm; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.text.TextUtils; + +import com.android.internal.util.cm.SpamFilter; +import com.android.internal.util.cm.SpamFilter.SpamContract.PackageTable; +import com.android.internal.util.cm.SpamFilter.SpamContract.NotificationTable; + +public class SpamMessageProvider extends ContentProvider { + public static final String AUTHORITY = SpamFilter.AUTHORITY; + + private static final String UPDATE_COUNT_QUERY = + "UPDATE " + NotificationTable.TABLE_NAME + + " SET " + NotificationTable.LAST_BLOCKED + "=%d," + + NotificationTable.COUNT + "=" + NotificationTable.COUNT + "+1 " + + " WHERE " + NotificationTable.ID + "='%s'"; + + private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + private static final int PACKAGES = 0; + private static final int MESSAGES = 1; + private static final int PACKAGE_FOR_ID = 2; + private static final int MESSAGE_UPDATE_COUNT = 3; + private static final int MESSAGE_FOR_ID = 4; + static { + sURIMatcher.addURI(AUTHORITY, "packages", PACKAGES); + sURIMatcher.addURI(AUTHORITY, "messages", MESSAGES); + sURIMatcher.addURI(AUTHORITY, "message/#", MESSAGE_FOR_ID); + sURIMatcher.addURI(AUTHORITY, "message/inc_count/#", MESSAGE_UPDATE_COUNT); + } + + private SpamOpenHelper mDbHelper; + + @Override + public boolean onCreate() { + mDbHelper = new SpamOpenHelper(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + int match = sURIMatcher.match(uri); + switch (match) { + case PACKAGE_FOR_ID: + return mDbHelper.getReadableDatabase().query(PackageTable.TABLE_NAME, + new String[]{NotificationTable.ID}, PackageTable.PACKAGE_NAME + "=?", + new String[]{uri.getLastPathSegment()}, null, null, null); + case PACKAGES: + return mDbHelper.getReadableDatabase().query(PackageTable.TABLE_NAME, + projection, selection, selectionArgs, null, null, sortOrder); + case MESSAGES: + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(NotificationTable.TABLE_NAME + " LEFT OUTER JOIN " + PackageTable.TABLE_NAME + + " ON (" + NotificationTable.TABLE_NAME + "." + NotificationTable.PACKAGE_ID + " = " + + PackageTable.TABLE_NAME + "." + PackageTable.ID + ")"); + SQLiteDatabase db = mDbHelper.getReadableDatabase(); + return qb.query(db, projection, selection, selectionArgs, + null, null, null); + case MESSAGE_FOR_ID: + qb = new SQLiteQueryBuilder(); + qb.setTables(NotificationTable.TABLE_NAME + " LEFT OUTER JOIN " + PackageTable.TABLE_NAME + + " ON (" + NotificationTable.TABLE_NAME + "." + NotificationTable.PACKAGE_ID + " = " + + PackageTable.TABLE_NAME + "." + PackageTable.ID + ")"); + qb.appendWhere(NotificationTable.TABLE_NAME + "." + NotificationTable.ID + "=" + + uri.getLastPathSegment()); + return qb.query(mDbHelper.getReadableDatabase(), + null, null, null, null, + null, null); + default: + return null; + } + } + + private long getPackageId(String pkg) { + long rowId = -1; + Cursor idCursor = mDbHelper.getReadableDatabase().query(PackageTable.TABLE_NAME, + new String[]{NotificationTable.ID}, PackageTable.PACKAGE_NAME + "=?", + new String[]{pkg}, null, null, null); + if (idCursor != null) { + if (idCursor.moveToFirst()) { + rowId = idCursor.getLong(0); + } + idCursor.close(); + } + return rowId; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + if (values == null) { + return null; + } + int match = sURIMatcher.match(uri); + switch (match) { + case MESSAGES: + String msgText = values.getAsString(NotificationTable.MESSAGE_TEXT); + String packageName = values.getAsString(PackageTable.PACKAGE_NAME); + if (TextUtils.isEmpty(msgText) || TextUtils.isEmpty(packageName)) { + return null; + } + values.clear(); + values.put(PackageTable.PACKAGE_NAME, packageName); + long packageId = getPackageId(packageName); + if (packageId == -1) { + SQLiteDatabase writableDb = mDbHelper.getWritableDatabase(); + packageId = writableDb.insert( + PackageTable.TABLE_NAME, null, values); + } + if (packageId != -1) { + values.clear(); + values.put(NotificationTable.MESSAGE_TEXT, msgText); + values.put(NotificationTable.NORMALIZED_TEXT, + SpamFilter.getNormalizedContent(msgText)); + values.put(NotificationTable.PACKAGE_ID, packageId); + values.put(NotificationTable.LAST_BLOCKED, System.currentTimeMillis()); + long id = mDbHelper.getReadableDatabase().insert(NotificationTable.TABLE_NAME, + null, values); + if (id != -1) { + notifyChange(String.valueOf(id)); + } + } + return null; + default: + return null; + } + } + + private void notifyChange(String id) { + Uri uri = Uri.withAppendedPath(SpamFilter.NOTIFICATION_URI, + id); + getContext().getContentResolver().notifyChange(uri, null); + } + + private void removePackageIfNecessary(int packageId) { + long numEntries = DatabaseUtils.queryNumEntries(mDbHelper.getReadableDatabase(), + NotificationTable.TABLE_NAME, NotificationTable.PACKAGE_ID + "=?", + new String[]{String.valueOf(packageId)}); + if (numEntries == 0) { + SQLiteDatabase writableDb = mDbHelper.getWritableDatabase(); + writableDb.delete(PackageTable.TABLE_NAME, PackageTable.ID + "=?", + new String[]{String.valueOf(packageId)}); + } + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int match = sURIMatcher.match(uri); + switch (match) { + case MESSAGE_FOR_ID: + int packageId = -1; + Cursor idCursor = mDbHelper.getReadableDatabase().query(NotificationTable.TABLE_NAME, + new String[]{NotificationTable.PACKAGE_ID}, NotificationTable.ID + "=?", + new String[]{uri.getLastPathSegment()}, null, null, null); + if (idCursor != null) { + if (idCursor.moveToFirst()) { + packageId = idCursor.getInt(0); + } + idCursor.close(); + } + SQLiteDatabase writableDb = mDbHelper.getWritableDatabase(); + String id = uri.getLastPathSegment(); + int result = writableDb.delete(NotificationTable.TABLE_NAME, + NotificationTable.ID + "=?", new String[]{id}); + removePackageIfNecessary(packageId); + if (result > 0) { + notifyChange(id); + } + return result; + default: + return 0; + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int match = sURIMatcher.match(uri); + switch (match) { + case MESSAGE_UPDATE_COUNT: + String id = uri.getLastPathSegment(); + String formattedQuery = String.format(UPDATE_COUNT_QUERY, + System.currentTimeMillis(), id); + SQLiteDatabase writableDb = mDbHelper.getWritableDatabase(); + writableDb.execSQL(formattedQuery); + notifyChange(id); + return 0; + default: + return 0; + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/cm/SpamOpenHelper.java b/packages/SystemUI/src/com/android/systemui/cm/SpamOpenHelper.java new file mode 100644 index 0000000..45dc91c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cm/SpamOpenHelper.java @@ -0,0 +1,46 @@ +package com.android.systemui.cm; + +import com.android.internal.util.cm.SpamFilter.SpamContract.NotificationTable; +import com.android.internal.util.cm.SpamFilter.SpamContract.PackageTable; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class SpamOpenHelper extends SQLiteOpenHelper { + + private static final String DATABASE_NAME = "spam.db"; + private static final int VERSION = 4; + private static final String CREATE_PACKAGES_TABLE = + "create table " + PackageTable.TABLE_NAME + "(" + + PackageTable.ID + " INTEGER PRIMARY KEY," + + PackageTable.PACKAGE_NAME + " TEXT UNIQUE);"; + private static final String CREATE_NOTIFICATIONS_TABLE = + "create table " + NotificationTable.TABLE_NAME + "(" + + NotificationTable.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + NotificationTable.PACKAGE_ID + " INTEGER," + + NotificationTable.MESSAGE_TEXT + " STRING," + + NotificationTable.LAST_BLOCKED + " INTEGER," + + NotificationTable.NORMALIZED_TEXT + " STRING," + + NotificationTable.COUNT + " INTEGER DEFAULT 0);"; + + private Context mContext; + + public SpamOpenHelper(Context context) { + super(context, DATABASE_NAME, null, VERSION); + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_PACKAGES_TABLE); + db.execSQL(CREATE_NOTIFICATIONS_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + mContext.deleteDatabase(DATABASE_NAME); + onCreate(db); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/cm/UserContentObserver.java b/packages/SystemUI/src/com/android/systemui/cm/UserContentObserver.java new file mode 100644 index 0000000..5ece744 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cm/UserContentObserver.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.cm; + +import android.app.ActivityManagerNative; +import android.app.IUserSwitchObserver; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.IRemoteCallback; +import android.os.RemoteException; +import android.util.Log; + +/** + * Simple extension of ContentObserver that also listens for user switch events to call update + */ +public abstract class UserContentObserver extends ContentObserver { + private static final String TAG = "UserContentObserver"; + + private Runnable mUpdateRunnable; + + private IUserSwitchObserver mUserSwitchObserver = new IUserSwitchObserver.Stub() { + @Override + public void onUserSwitching(int newUserId, IRemoteCallback reply) { + } + @Override + public void onUserSwitchComplete(int newUserId) throws RemoteException { + mHandler.post(mUpdateRunnable); + } + @Override + public void onForegroundProfileSwitch(int newProfileId) { + } + }; + + private Handler mHandler; + + public UserContentObserver(Handler handler) { + super(handler); + mHandler = handler; + mUpdateRunnable = new Runnable() { + @Override + public void run() { + update(); + } + }; + } + + protected void observe() { + try { + ActivityManagerNative.getDefault().registerUserSwitchObserver(mUserSwitchObserver); + } catch (RemoteException e) { + Log.w(TAG, "Unable to register user switch observer!", e); + } + } + + protected void unobserve() { + try { + mHandler.removeCallbacks(mUpdateRunnable); + ActivityManagerNative.getDefault().unregisterUserSwitchObserver(mUserSwitchObserver); + } catch (RemoteException e) { + Log.w(TAG, "Unable to unregister user switch observer!", e); + } + } + + protected abstract void update(); + + @Override + public void onChange(boolean selfChange) { + update(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + update(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index 39423f2..f223400 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -218,7 +218,7 @@ public class DozeService extends DreamService { // Here we need a wakelock to stay awake until the pulse is finished. mWakeLock.acquire(); mPulsing = true; - if (!mDozeParameters.getProxCheckBeforePulse()) { + if (!mDozeParameters.getProxCheckBeforePulse(reason)) { // skip proximity check continuePulsing(reason); return; @@ -265,6 +265,7 @@ public class DozeService extends DreamService { @Override public void onPulseStarted() { if (mPulsing && mDreaming) { + mContext.sendBroadcast(new Intent(Intent.ACTION_DOZE_PULSE_STARTING)); turnDisplayOn(); } } @@ -356,7 +357,7 @@ public class DozeService extends DreamService { if (DEBUG) Log.d(mTag, "No more schedule resets remaining"); return; } - final long pulseDuration = mDozeParameters.getPulseDuration(false /*pickup*/); + final long pulseDuration = mDozeParameters.getPulseDuration(DozeLog.PULSE_REASON_NOTIFICATION); boolean pulseImmediately = System.currentTimeMillis() >= notificationTimeMs; if ((notificationTimeMs - mLastScheduleResetTime) >= pulseDuration) { mScheduleResetsRemaining--; diff --git a/packages/SystemUI/src/com/android/systemui/egg/CMLand.java b/packages/SystemUI/src/com/android/systemui/egg/CMLand.java new file mode 100644 index 0000000..6020b45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/egg/CMLand.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014-2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.android.systemui.R; + +import android.content.Context; +import android.util.AttributeSet; + +public class CMLand extends MLand { + public static final String TAG = "CMLand"; + + public CMLand(Context context) { + this(context, null); + } + + public CMLand(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CMLand(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected int getEggPlayer() { + return R.drawable.cid; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/egg/MLand.java b/packages/SystemUI/src/com/android/systemui/egg/MLand.java index b84777b..1b22e04 100644 --- a/packages/SystemUI/src/com/android/systemui/egg/MLand.java +++ b/packages/SystemUI/src/com/android/systemui/egg/MLand.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2014-2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -210,6 +211,8 @@ public class MLand extends FrameLayout { // we assume everything will be laid out left|top setLayoutDirection(LAYOUT_DIRECTION_LTR); + Player.eggPlayer = getEggPlayer(); + setupPlayers(DEFAULT_PLAYERS); MetricsLogger.count(getContext(), "egg_mland_create", 1); @@ -1007,7 +1010,7 @@ public class MLand extends FrameLayout { public void step(long t_ms, long dt_ms, float t, float dt); } - private static class Player extends ImageView implements GameView { + protected static class Player extends ImageView implements GameView { public float dv; public int color; private MLand mLand; @@ -1017,6 +1020,8 @@ public class MLand extends FrameLayout { private int mScore; private TextView mScoreField; + protected static int eggPlayer; + private final int[] sColors = new int[] { //0xFF78C557, 0xFFDB4437, @@ -1088,7 +1093,7 @@ public class MLand extends FrameLayout { public Player(Context context) { super(context); - setBackgroundResource(R.drawable.android); + setBackgroundResource(eggPlayer); getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP); color = sColors[(sNextColor++%sColors.length)]; getBackground().setTint(color); @@ -1438,4 +1443,8 @@ public class MLand extends FrameLayout { v = z = 0; } } + + protected int getEggPlayer() { + return R.drawable.android; + } } diff --git a/packages/SystemUI/src/com/android/systemui/egg/MLandActivity.java b/packages/SystemUI/src/com/android/systemui/egg/MLandActivity.java index cdda45f..6fd7387 100644 --- a/packages/SystemUI/src/com/android/systemui/egg/MLandActivity.java +++ b/packages/SystemUI/src/com/android/systemui/egg/MLandActivity.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2014-2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +30,14 @@ public class MLandActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.mland); + final boolean isCM = getIntent().getBooleanExtra("is_cm", false); + if (isCM) { + setContentView(R.layout.cmland); + mLand = (CMLand) findViewById(R.id.world); + } else { + setContentView(R.layout.mland); + mLand = (MLand) findViewById(R.id.world); + } mLand = (MLand) findViewById(R.id.world); mLand.setScoreFieldHolder((ViewGroup) findViewById(R.id.scores)); final View welcome = findViewById(R.id.welcome); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index d2c60ef..a6ca6a0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -90,6 +90,12 @@ public class KeyguardService extends Service { mKeyguardViewMediator.setOccluded(isOccluded); } + @Override + public void showKeyguard() { + checkPermission(); + mKeyguardViewMediator.showKeyguard(); + } + @Override // Binder interface public void dismiss() { checkPermission(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 055b5ef..2833759 100644..100755 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -23,13 +23,16 @@ import android.app.AlarmManager; import android.app.PendingIntent; import android.app.SearchManager; import android.app.StatusBarManager; +import android.app.admin.DevicePolicyManager; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.UserInfo; +import android.database.ContentObserver; import android.media.AudioManager; import android.media.SoundPool; import android.os.Bundle; @@ -50,12 +53,19 @@ import android.util.EventLog; import android.util.Log; import android.util.Slog; import android.view.IWindowManager; +import android.view.View; import android.view.ViewGroup; import android.view.WindowManagerGlobal; import android.view.WindowManagerPolicy; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import com.android.systemui.cm.UserContentObserver; +import com.android.systemui.qs.tiles.LockscreenToggleTile; +import com.android.systemui.statusbar.StatusBarState; +import cyanogenmod.app.Profile; +import cyanogenmod.app.ProfileManager; + import com.android.internal.policy.IKeyguardDrawnCallback; import com.android.internal.policy.IKeyguardExitCallback; import com.android.internal.policy.IKeyguardStateCallback; @@ -73,6 +83,7 @@ import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.StatusBarWindowManager; +import cyanogenmod.providers.CMSettings; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -135,6 +146,15 @@ public class KeyguardViewMediator extends SystemUI { private static final String DELAYED_KEYGUARD_ACTION = "com.android.internal.policy.impl.PhoneWindowManager.DELAYED_KEYGUARD"; + private static final String DISMISS_KEYGUARD_SECURELY_ACTION = + "com.android.keyguard.action.DISMISS_KEYGUARD_SECURELY"; + + private static final String KEYGUARD_SERVICE_ACTION_STATE_CHANGE = + "com.android.internal.action.KEYGUARD_SERVICE_STATE_CHANGED"; + private static final String KEYGUARD_SERVICE_EXTRA_ACTIVE = "active"; + + private static final String DECRYPT_STATE = "trigger_restart_framework"; + // used for handler messages private static final int SHOW = 2; private static final int HIDE = 3; @@ -155,6 +175,7 @@ public class KeyguardViewMediator extends SystemUI { private static final int NOTIFY_SCREEN_TURNED_ON = 22; private static final int NOTIFY_SCREEN_TURNED_OFF = 23; private static final int NOTIFY_STARTED_GOING_TO_SLEEP = 24; + private static final int NOTIFY_KEYGUARD_PANEL_FOCUS_CHANGED = 25; /** * The default amount of time we stay awake (used for all key input) @@ -187,7 +208,6 @@ public class KeyguardViewMediator extends SystemUI { private AudioManager mAudioManager; private StatusBarManager mStatusBarManager; private boolean mSwitchingUser; - private boolean mSystemReady; private boolean mBootCompleted; private boolean mBootSendUserPresent; @@ -237,6 +257,8 @@ public class KeyguardViewMediator extends SystemUI { // true if the keyguard is hidden by another window private boolean mOccluded = false; + private boolean mKeyguardPanelFocused = false; + /** * Helps remember whether the screen has turned on since the last time * it turned off due to timeout. see {@link #onScreenTurnedOff(int)} @@ -250,6 +272,11 @@ public class KeyguardViewMediator extends SystemUI { */ private IKeyguardExitCallback mExitSecureCallback; + /** + * Whether we are bound to the service delegate + */ + private boolean mKeyguardBound; + // the properties of the keyguard private KeyguardUpdateMonitor mUpdateMonitor; @@ -267,6 +294,11 @@ public class KeyguardViewMediator extends SystemUI { private boolean mHiding; /** + * Whether we are disabling the lock screen internally + */ + private boolean mInternallyDisabled = false; + + /** * we send this intent when the keyguard is dismissed. */ private static final Intent USER_PRESENT_INTENT = new Intent(Intent.ACTION_USER_PRESENT) @@ -318,9 +350,57 @@ public class KeyguardViewMediator extends SystemUI { */ private boolean mPendingLock; + private boolean mCryptKeeperEnabled = true; + private boolean mWakeAndUnlocking; private IKeyguardDrawnCallback mDrawnCallback; + private LockscreenEnabledSettingsObserver mSettingsObserver; + private PhoneStatusBar mStatusBar; + + public static class LockscreenEnabledSettingsObserver extends UserContentObserver { + + private static final String KEY_ENABLED = "lockscreen_enabled"; + + private boolean mObserving; + private SharedPreferences mPrefs; + private Context mContext; + + public LockscreenEnabledSettingsObserver(Context context, Handler handler) { + super(handler); + mContext = context; + mPrefs = mContext.getSharedPreferences("quicksettings", Context.MODE_PRIVATE); + } + + public boolean getPersistedDefaultOldSetting() { + return mPrefs.getBoolean(KEY_ENABLED, true); + } + + @Override + public void observe() { + if (mObserving) { + return; + } + mObserving = true; + mContext.getContentResolver().registerContentObserver(CMSettings.Secure.getUriFor( + CMSettings.Secure.LOCKSCREEN_INTERNALLY_ENABLED), false, this, + UserHandle.USER_ALL); + update(); + } + + @Override + public void unobserve() { + if (mObserving) { + mObserving = false; + mContext.getContentResolver().unregisterContentObserver(this); + } + } + + @Override + public void update() { + } + } + KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { @Override @@ -407,21 +487,20 @@ public class KeyguardViewMediator extends SystemUI { // only force lock screen in case of missing sim if user hasn't // gone through setup wizard synchronized (this) { - if (shouldWaitForProvisioning()) { - if (!mShowing) { - if (DEBUG_SIM_STATES) Log.d(TAG, "ICC_ABSENT isn't showing," - + " we need to show the keyguard since the " - + "device isn't provisioned yet."); - doKeyguardLocked(null); - } else { - resetStateLocked(); - } + if (shouldWaitForProvisioning() && !mShowing) { + if (DEBUG_SIM_STATES) Log.d(TAG, "ICC_ABSENT isn't showing," + + " we need to show the keyguard since the " + + "device isn't provisioned yet."); + doKeyguardLocked(null); + } else { + resetStateLocked(); } } break; case PIN_REQUIRED: case PUK_REQUIRED: synchronized (this) { + mStatusBar.hideHeadsUp(); if (!mShowing) { if (DEBUG_SIM_STATES) Log.d(TAG, "INTENT_VALUE_ICC_LOCKED and keygaurd isn't " @@ -447,7 +526,10 @@ public class KeyguardViewMediator extends SystemUI { break; case READY: synchronized (this) { - if (mShowing) { + if ((mInternallyDisabled || isProfileDisablingKeyguard()) + && !mUpdateMonitor.isSimPinSecure()) { + hideLocked(); + } else if (mShowing) { resetStateLocked(); } } @@ -555,8 +637,12 @@ public class KeyguardViewMediator extends SystemUI { mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard"); mShowKeyguardWakeLock.setReferenceCounted(false); - mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(DELAYED_KEYGUARD_ACTION)); + mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(DISMISS_KEYGUARD_SECURELY_ACTION), + android.Manifest.permission.CONTROL_KEYGUARD, null); + mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(KEYGUARD_SERVICE_ACTION_STATE_CHANGE), + android.Manifest.permission.CONTROL_KEYGUARD, null); + mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED)); mKeyguardDisplayManager = new KeyguardDisplayManager(mContext); @@ -608,6 +694,25 @@ public class KeyguardViewMediator extends SystemUI { mHideAnimation = AnimationUtils.loadAnimation(mContext, com.android.internal.R.anim.lock_screen_behind_enter); + + mSettingsObserver = new LockscreenEnabledSettingsObserver(mContext, new Handler()) { + @Override + public void update() { + boolean newDisabledState = CMSettings.Secure.getIntForUser(mContext.getContentResolver(), + CMSettings.Secure.LOCKSCREEN_INTERNALLY_ENABLED, + getPersistedDefaultOldSetting() ? 1 : 0, + UserHandle.USER_CURRENT) == 0; + + synchronized (KeyguardViewMediator.this) { + if (mKeyguardBound) { + if (newDisabledState != mInternallyDisabled) { + // it was updated, + setKeyguardEnabledInternal(!newDisabledState); + } + } + } + } + }; } @Override @@ -662,7 +767,7 @@ public class KeyguardViewMediator extends SystemUI { Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); } mExitSecureCallback = null; - if (!mExternallyEnabled) { + if (!mInternallyDisabled && !mExternallyEnabled) { hideLocked(); } } else if (mShowing) { @@ -762,6 +867,10 @@ public class KeyguardViewMediator extends SystemUI { mDelayedShowingSequence++; } + public boolean isKeyguardBound() { + return mKeyguardBound; + } + /** * Let's us know when the device is waking up. */ @@ -793,7 +902,7 @@ public class KeyguardViewMediator extends SystemUI { } private void maybeSendUserPresentBroadcast() { - if (mSystemReady && mLockPatternUtils.isLockScreenDisabled( + if (mSystemReady && isKeyguardDisabled( 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 @@ -802,6 +911,37 @@ public class KeyguardViewMediator extends SystemUI { } } + private boolean isKeyguardDisabled(int userId) { + if (!mExternallyEnabled) { + if (DEBUG) Log.d(TAG, "isKeyguardDisabled: keyguard is disabled externally"); + return true; + } + if (mLockPatternUtils.isLockScreenDisabled(userId)) { + if (DEBUG) Log.d(TAG, "isKeyguardDisabled: keyguard is disabled by setting"); + return true; + } + if (mInternallyDisabled) { + if (DEBUG) Log.d(TAG, "isKeyguardDisabled: keyguard is disabled internally"); + return true; + } + if (isProfileDisablingKeyguard()) { + if (DEBUG) Log.d(TAG, "isKeyguardDisabled: keyguard is disabled by profile"); + return true; + } + return false; + } + + private boolean isCryptKeeperEnabled() { + if (!mCryptKeeperEnabled) { + // once it's disabled, it's disabled. + return false; + } + final String state = SystemProperties.get("vold.decrypt"); + mCryptKeeperEnabled = !"".equals(state) && !DECRYPT_STATE.equals(state); + if (DEBUG) Log.w(TAG, "updated crypt keeper state to: " + mCryptKeeperEnabled); + return mCryptKeeperEnabled; + } + /** * A dream started. We should lock after the usual screen-off lock timeout but only * if there is a secure lock pattern. @@ -827,6 +967,33 @@ public class KeyguardViewMediator extends SystemUI { } /** + * Set the internal keyguard enabled state. This allows SystemUI to disable the lockscreen, + * overriding any apps. + * @param enabled + */ + public void setKeyguardEnabledInternal(boolean enabled) { + mInternallyDisabled = !enabled; + if (!mUpdateMonitor.isSimPinSecure()) { + // disable when sim is ready + return; + } + setKeyguardEnabled(enabled); + if (mInternallyDisabled) { + mNeedToReshowWhenReenabled = false; + } + } + + public boolean getKeyguardEnabledInternal() { + return !mInternallyDisabled; + } + + public boolean isProfileDisablingKeyguard() { + final Profile activeProfile = ProfileManager.getInstance(mContext).getActiveProfile(); + return activeProfile != null + && activeProfile.getScreenLockMode().getValue() == Profile.LockMode.DISABLE; + } + + /** * Same semantics as {@link android.view.WindowManagerPolicy#enableKeyguard}; provide * a way for external stuff to override normal keyguard behavior. For instance * the phone app disables the keyguard when it receives incoming calls. @@ -834,8 +1001,14 @@ public class KeyguardViewMediator extends SystemUI { public void setKeyguardEnabled(boolean enabled) { synchronized (this) { if (DEBUG) Log.d(TAG, "setKeyguardEnabled(" + enabled + ")"); - mExternallyEnabled = enabled; + if (mInternallyDisabled + && enabled + && !lockscreenEnforcedByDevicePolicy()) { + // if keyguard is forcefully disabled internally (by lock screen tile), don't allow + // it to be enabled externally, unless the device policy manager says so. + return; + } if (!enabled && mShowing) { if (mExitSecureCallback != null) { @@ -848,7 +1021,7 @@ public class KeyguardViewMediator extends SystemUI { // hiding keyguard that is showing, remember to reshow later if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, " + "disabling status bar expansion"); - mNeedToReshowWhenReenabled = true; + mNeedToReshowWhenReenabled = !isProfileDisablingKeyguard(); updateInputRestrictedLocked(); hideLocked(); } else if (enabled && mNeedToReshowWhenReenabled) { @@ -1028,22 +1201,6 @@ public class KeyguardViewMediator extends SystemUI { * Enable the keyguard if the settings are appropriate. */ private void doKeyguardLocked(Bundle options) { - // if another app is disabling us, don't show - if (!mExternallyEnabled) { - if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled"); - - // note: we *should* set mNeedToReshowWhenReenabled=true here, but that makes - // for an occasional ugly flicker in this situation: - // 1) receive a call with the screen on (no keyguard) or make a call - // 2) screen times out - // 3) user hits key to turn screen back on - // instead, we reenable the keyguard when we know the screen is off and the call - // ends (see the broadcast receiver below) - // TODO: clean this up when we have better support at the window manager level - // for apps that wish to be on top of the keyguard - return; - } - // if the keyguard is already showing, don't bother if (mStatusBarKeyguardViewManager.isShowing()) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); @@ -1051,6 +1208,14 @@ public class KeyguardViewMediator extends SystemUI { return; } + // Ugly hack to ensure keyguard is not shown on top of the CryptKeeper which prevents + // a user from being able to decrypt their device. + if (isCryptKeeperEnabled()) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because CryptKeeper is enabled"); + resetStateLocked(); + return; + } + // if the setup wizard hasn't run yet, don't show final boolean requireSim = !SystemProperties.getBoolean("keyguard.no_require_sim", false); final boolean absent = SubscriptionManager.isValidSubscriptionId( @@ -1066,9 +1231,30 @@ public class KeyguardViewMediator extends SystemUI { return; } - if (mLockPatternUtils.isLockScreenDisabled(KeyguardUpdateMonitor.getCurrentUser()) + // if another app is disabling us, don't show + if (!mExternallyEnabled && !lockedOrMissing) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled"); + + // note: we *should* set mNeedToReshowWhenReenabled=true here, but that makes + // for an occasional ugly flicker in this situation: + // 1) receive a call with the screen on (no keyguard) or make a call + // 2) screen times out + // 3) user hits key to turn screen back on + // instead, we reenable the keyguard when we know the screen is off and the call + // ends (see the broadcast receiver below) + // TODO: clean this up when we have better support at the window manager level + // for apps that wish to be on top of the keyguard + return; + } + + if (isKeyguardDisabled(KeyguardUpdateMonitor.getCurrentUser()) && !lockedOrMissing) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off"); + // update state + setShowingLocked(false); + updateActivityLockScreenState(); + adjustStatusBarLocked(); + userActivity(); return; } @@ -1089,6 +1275,15 @@ public class KeyguardViewMediator extends SystemUI { return !mUpdateMonitor.isDeviceProvisioned() && !isSecure(); } + public boolean lockscreenEnforcedByDevicePolicy() { + DevicePolicyManager dpm = (DevicePolicyManager) + mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); + if (dpm != null) { + return dpm.requireSecureKeyguard(); + } + return false; + } + /** * Dismiss the keyguard through the security layers. */ @@ -1102,6 +1297,36 @@ public class KeyguardViewMediator extends SystemUI { mHandler.sendEmptyMessage(DISMISS); } + public void showKeyguard() { + // This is to prevent left edge from interfering + // with affordances. + if (mStatusBar.isAffordanceSwipeInProgress() + || mStatusBar.getBarState() == StatusBarState.KEYGUARD) { + return; + } + + // Disable edge detector once we're back on lockscreen + try { + WindowManagerGlobal.getWindowManagerService() + .setLiveLockscreenEdgeDetector(false); + } catch (RemoteException e){ + Log.e(TAG, e.getMessage()); + } + + mHandler.post(new Runnable() { + @Override + public void run() { + // Hide status bar window to avoid flicker, + // slideNotificationPanelIn will make it visible later. + mStatusBar.getStatusBarWindow().setVisibility(View.INVISIBLE); + // Get the keyguard into the correct state by calling mStatusBar.showKeyguard() + mStatusBar.showKeyguard(); + // Now have the notification panel slid back into view + mStatusBar.slideNotificationPanelIn(); + } + }); + } + /** * Send message to keyguard telling it to reset its state. * @see #handleReset @@ -1203,6 +1428,20 @@ public class KeyguardViewMediator extends SystemUI { doKeyguardLocked(null); } } + } else if (DISMISS_KEYGUARD_SECURELY_ACTION.equals(intent.getAction())) { + synchronized (KeyguardViewMediator.this) { + dismiss(); + } + } else if (KEYGUARD_SERVICE_ACTION_STATE_CHANGE.equals(intent.getAction())) { + mKeyguardBound = intent.getBooleanExtra(KEYGUARD_SERVICE_EXTRA_ACTIVE, false); + if (mKeyguardBound) { + mSettingsObserver.observe(); + } else { + mSettingsObserver.unobserve(); + } + } else if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(intent.getAction())) { + mPhoneState = intent.getStringExtra(TelephonyManager.EXTRA_STATE); + if (DEBUG) Log.d(TAG, "phone state change, new state: " + mPhoneState); } } }; @@ -1282,6 +1521,9 @@ public class KeyguardViewMediator extends SystemUI { case ON_ACTIVITY_DRAWN: handleOnActivityDrawn(); break; + case NOTIFY_KEYGUARD_PANEL_FOCUS_CHANGED: + notifyKeyguardPanelFocusChanged(msg.arg1 != 0); + break; } } }; @@ -1368,6 +1610,10 @@ public class KeyguardViewMediator extends SystemUI { private void playSound(int soundId) { if (soundId == 0) return; + if (mInternallyDisabled) { + Log.d(TAG, "suppressing lock screen sounds because it is disabled"); + return; + } final ContentResolver cr = mContext.getContentResolver(); if (Settings.System.getInt(cr, Settings.System.LOCKSCREEN_SOUNDS_ENABLED, 1) == 1) { @@ -1437,7 +1683,8 @@ public class KeyguardViewMediator extends SystemUI { ActivityManagerNative.getDefault().keyguardGoingAway( mStatusBarKeyguardViewManager.shouldDisableWindowAnimationsForUnlock() || mWakeAndUnlocking, - mStatusBarKeyguardViewManager.isGoingToNotificationShade()); + mStatusBarKeyguardViewManager.isGoingToNotificationShade(), + mStatusBarKeyguardViewManager.isKeyguardShowingMedia()); } catch (RemoteException e) { Log.e(TAG, "Error while calling WindowManager", e); } @@ -1497,7 +1744,9 @@ public class KeyguardViewMediator extends SystemUI { // only play "unlock" noises if not on a call (since the incall UI // disables the keyguard) if (TelephonyManager.EXTRA_STATE_IDLE.equals(mPhoneState)) { - playSounds(false); + if (mShowing && mDeviceInteractive) { + playSounds(false); + } } setShowingLocked(false); @@ -1550,7 +1799,7 @@ public class KeyguardViewMediator extends SystemUI { private void handleReset() { synchronized (KeyguardViewMediator.this) { if (DEBUG) Log.d(TAG, "handleReset"); - mStatusBarKeyguardViewManager.reset(); + mStatusBarKeyguardViewManager.reset(false); } } @@ -1655,6 +1904,7 @@ public class KeyguardViewMediator extends SystemUI { FingerprintUnlockController fingerprintUnlockController) { mStatusBarKeyguardViewManager.registerStatusBar(phoneStatusBar, container, statusBarWindowManager, scrimController, fingerprintUnlockController); + mStatusBar = phoneStatusBar; return mStatusBarKeyguardViewManager; } @@ -1725,6 +1975,31 @@ public class KeyguardViewMediator extends SystemUI { } } + public void setKeyguardPanelFocused(boolean focused) { + if (DEBUG) Log.d(TAG, "setSlideOffset " + focused); + mHandler.removeMessages(NOTIFY_KEYGUARD_PANEL_FOCUS_CHANGED); + Message msg = mHandler.obtainMessage(NOTIFY_KEYGUARD_PANEL_FOCUS_CHANGED, + focused ? 1 : 0, 0); + mHandler.sendMessage(msg); + } + + public void notifyKeyguardPanelFocusChanged(boolean focused) { + if (focused != mKeyguardPanelFocused) { + mKeyguardPanelFocused = focused; + int size = mKeyguardStateCallbacks.size(); + for (int i = size - 1; i >= 0; i--) { + try { + mKeyguardStateCallbacks.get(i).onKeyguardPanelFocusChanged(focused); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onShowingStateChanged", e); + if (e instanceof DeadObjectException) { + mKeyguardStateCallbacks.remove(i); + } + } + } + } + } + public void addStateMonitorCallback(IKeyguardStateCallback callback) { synchronized (this) { mKeyguardStateCallbacks.add(callback); @@ -1732,6 +2007,7 @@ public class KeyguardViewMediator extends SystemUI { callback.onSimSecureStateChanged(mUpdateMonitor.isSimPinSecure()); callback.onShowingStateChanged(mShowing); callback.onInputRestrictedStateChanged(mInputRestricted); + callback.onKeyguardPanelFocusChanged(mKeyguardPanelFocused); } catch (RemoteException e) { Slog.w(TAG, "Failed to call onShowingStateChanged or onSimSecureStateChanged or onInputRestrictedStateChanged", e); } diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 9459740..e519e34 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -16,23 +16,30 @@ package com.android.systemui.power; +import android.app.Notification; +import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.ContentObserver; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; import android.os.BatteryManager; import android.os.Handler; import android.os.PowerManager; import android.os.SystemClock; import android.os.UserHandle; +import android.os.Vibrator; import android.provider.Settings; import android.util.Log; import android.util.Slog; import com.android.systemui.SystemUI; import com.android.systemui.statusbar.phone.PhoneStatusBar; +import cyanogenmod.providers.CMSettings; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -57,6 +64,9 @@ public class PowerUI extends SystemUI { private long mScreenOffTime = -1; + // For filtering ACTION_POWER_DISCONNECTED on boot + boolean mIgnoreFirstPowerEvent = true; + public void start() { mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mScreenOffTime = mPowerManager.isScreenOn() ? -1 : SystemClock.elapsedRealtime(); @@ -140,6 +150,8 @@ public class PowerUI extends SystemUI { filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING); filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); + filter.addAction(Intent.ACTION_POWER_CONNECTED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); mContext.registerReceiver(this, filter, null, mHandler); updateSaverMode(); } @@ -165,6 +177,10 @@ public class PowerUI extends SystemUI { final boolean plugged = mPlugType != 0; final boolean oldPlugged = oldPlugType != 0; + if (mIgnoreFirstPowerEvent && plugged) { + mIgnoreFirstPowerEvent = false; + } + int oldBucket = findBatteryLevelBucket(oldBatteryLevel); int bucket = findBatteryLevelBucket(mBatteryLevel); @@ -214,12 +230,44 @@ public class PowerUI extends SystemUI { updateSaverMode(); } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGING.equals(action)) { setSaverMode(intent.getBooleanExtra(PowerManager.EXTRA_POWER_SAVE_MODE, false)); + } else if (Intent.ACTION_POWER_CONNECTED.equals(action) + || Intent.ACTION_POWER_DISCONNECTED.equals(action)) { + final ContentResolver cr = mContext.getContentResolver(); + + if (mIgnoreFirstPowerEvent) { + mIgnoreFirstPowerEvent = false; + } else { + if (Settings.Global.getInt(cr, + Settings.Global.CHARGING_SOUNDS_ENABLED, 0) == 1) { + playPowerNotificationSound(); + } + } } else { Slog.w(TAG, "unknown intent: " + intent); } } }; + void playPowerNotificationSound() { + final ContentResolver cr = mContext.getContentResolver(); + final String soundPath = + CMSettings.Global.getString(cr, CMSettings.Global.POWER_NOTIFICATIONS_RINGTONE); + + if (soundPath != null) { + Ringtone powerRingtone = RingtoneManager.getRingtone(mContext, Uri.parse(soundPath)); + if (powerRingtone != null) { + powerRingtone.play(); + } + } + if (CMSettings.Global.getInt(cr, + CMSettings.Global.POWER_NOTIFICATIONS_VIBRATE, 0) == 1) { + Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + if (vibrator != null) { + vibrator.vibrate(250); + } + } + } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.print("mLowBatteryAlertCloseLevel="); pw.println(mLowBatteryAlertCloseLevel); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSBooleanSettingRow.java b/packages/SystemUI/src/com/android/systemui/qs/QSBooleanSettingRow.java new file mode 100644 index 0000000..50845da --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSBooleanSettingRow.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.qs; + +import android.annotation.Nullable; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.TypedArray; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.Switch; +import android.widget.TextView; + +import com.android.systemui.R; +import cyanogenmod.providers.CMSettings; + +public class QSBooleanSettingRow extends LinearLayout implements View.OnClickListener { + + private static final String TAG = "QSSettingRow"; + + public static final int TABLE_SYSTEM = 0; + public static final int TABLE_GLOBAL = 1; + public static final int TABLE_SECURE = 2; + + public static final int TABLE_CM_SYSTEM = 3; + public static final int TABLE_CM_GLOBAL = 4; + public static final int TABLE_CM_SECURE = 5; + + int mWhichTable; + String mTitle; + String mKey; + private TextView mText; + private Switch mSwitch; + private int mDefaultValue; + private CompoundButton.OnCheckedChangeListener mOnCheckedChangeListener; + + public QSBooleanSettingRow(Context context) { + this(context, null); + } + + public QSBooleanSettingRow(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public QSBooleanSettingRow(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public QSBooleanSettingRow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + View.inflate(context, R.layout.qs_settings_row, this); + + setOrientation(HORIZONTAL); + setClickable(true); + setOnClickListener(this); + + mText = (TextView) findViewById(R.id.title); + mSwitch = (Switch) findViewById(R.id.switcher); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.QuickSettingsRow, + defStyleAttr, defStyleRes); + + mWhichTable = a.getInteger(R.styleable.QuickSettingsRow_table, -1); + + mTitle = a.getString(R.styleable.QuickSettingsRow_android_title); + mKey = a.getString(R.styleable.QuickSettingsRow_android_key); + mDefaultValue = a.getInt(R.styleable.QuickSettingsRow_defaultValue, 0); + + if (mText != null) { + mText.setText(mTitle); + mText.setClickable(false); + mText.setFocusable(false); + } + + if (mSwitch != null) { + mSwitch.setClickable(false); + mSwitch.setFocusable(false); + mSwitch.setChecked(getCurrent()); + mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (false) Log.d(TAG, "onCheckedChanged() called with " + + "buttonView = [" + buttonView + "], isChecked = [" + isChecked + + "] and table: " + mWhichTable + ", and key: " + mKey); + applyChange(isChecked); + if (mOnCheckedChangeListener != null) { + mOnCheckedChangeListener.onCheckedChanged(buttonView, isChecked); + } + } + }); + } + + a.recycle(); + } + + public void setChecked(boolean checked) { + if (mSwitch.isChecked() == checked) { + return; + } + mSwitch.setChecked(checked); + } + + private void applyChange(boolean value) { + ContentResolver cr = getContext().getContentResolver(); + switch (mWhichTable) { + case TABLE_GLOBAL: + Settings.Global.putInt(cr, mKey, value ? 1 : 0); + break; + case TABLE_SECURE: + Settings.Secure.putInt(cr, mKey, value ? 1 : 0); + break; + case TABLE_SYSTEM: + Settings.System.putInt(cr, mKey, value ? 1 : 0); + break; + case TABLE_CM_GLOBAL: + CMSettings.Global.putInt(cr, mKey, value ? 1 : 0); + break; + case TABLE_CM_SECURE: + CMSettings.Secure.putInt(cr, mKey, value ? 1 : 0); + break; + case TABLE_CM_SYSTEM: + CMSettings.System.putInt(cr, mKey, value ? 1 : 0); + break; + } + } + + private boolean getCurrent() { + ContentResolver cr = getContext().getContentResolver(); + int ret = 0; + switch (mWhichTable) { + case TABLE_GLOBAL: + ret = Settings.Global.getInt(cr, mKey, mDefaultValue); + break; + case TABLE_SECURE: + ret = Settings.Secure.getInt(cr, mKey, mDefaultValue); + break; + case TABLE_SYSTEM: + ret = Settings.System.getInt(cr, mKey, mDefaultValue); + break; + case TABLE_CM_GLOBAL: + ret = CMSettings.Global.getInt(cr, mKey, mDefaultValue); + break; + case TABLE_CM_SECURE: + ret = CMSettings.Secure.getInt(cr, mKey, mDefaultValue); + break; + case TABLE_CM_SYSTEM: + ret = CMSettings.System.getInt(cr, mKey, mDefaultValue); + break; + } + return ret == 1; + } + + @Override + public void onClick(View v) { + mSwitch.setChecked(!mSwitch.isChecked()); + } + + public void setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener l) { + mOnCheckedChangeListener = l; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainer.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainer.java index cfe8d07..74f4cbb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainer.java @@ -63,7 +63,7 @@ public class QSContainer extends FrameLayout { */ public int getDesiredHeight() { if (mQSPanel.isClosingDetail()) { - return mQSPanel.getGridHeight() + getPaddingTop() + getPaddingBottom(); + return mQSPanel.getGridHeight() + getPaddingBottom(); } else { return getMeasuredHeight(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java index a318efc..8b89a65 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetailClipper.java @@ -36,6 +36,10 @@ public class QSDetailClipper { mBackground = (TransitionDrawable) detail.getBackground(); } + public boolean isAnimating() { + return mAnimator != null && mAnimator.isRunning(); + } + public void animateCircularClip(int x, int y, boolean in, AnimatorListener listener) { if (mAnimator != null) { mAnimator.cancel(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetailItemsGrid.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItemsGrid.java new file mode 100644 index 0000000..07e01e4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItemsGrid.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2015, The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs; + +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import android.widget.BaseAdapter; + +import cyanogenmod.app.CustomTile; + +import com.android.systemui.qs.tiles.UserDetailItemView; +import com.android.systemui.R; + +/** + * Quick settings common detail grid view with circular items. + */ +public class QSDetailItemsGrid extends PseudoGridView { + private static final String TAG = "QSDetailItemsGrid"; + private QSDetailItemsGridAdapter mDetailItemsGridAdapter; + + public QSDetailItemsGrid(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public static QSDetailItemsGrid inflate(Context context, ViewGroup parent, boolean attach) { + return (QSDetailItemsGrid) LayoutInflater.from(context).inflate( + R.layout.qs_detail_items_grid, parent, attach); + } + + public QSDetailItemsGridAdapter createAndSetAdapter(String externalPackage, + CustomTile.ExpandedItem[] items) { + mDetailItemsGridAdapter = new QSDetailItemsGridAdapter(externalPackage, mContext, items); + ViewGroupAdapterBridge.link(this, mDetailItemsGridAdapter); + return mDetailItemsGridAdapter; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + setOnTouchListener(new OnTouchListener() { + // Setting on Touch Listener for handling the touch inside ScrollView + @Override + public boolean onTouch(View v, MotionEvent event) { + // Disallow the touch request for parent scroll on touch of child view + v.getParent().requestDisallowInterceptTouchEvent(true); + return false; + } + }); + } + + public static class QSDetailItemsGridAdapter extends BaseAdapter implements OnClickListener { + private String mPkg; + private Context mContext; + private CustomTile.ExpandedItem[] mItems; + private OnPseudoGriditemClickListener mOnPseudoGridItemClickListener; + + public interface OnPseudoGriditemClickListener { + void onPsuedoGridItemClick(View view, CustomTile.ExpandedItem item); + } + + public QSDetailItemsGridAdapter(String packageName, + Context context, CustomTile.ExpandedItem[] items) { + mPkg = packageName; + mContext = context; + mItems = items; + } + + public void setOnPseudoGridItemClickListener(OnPseudoGriditemClickListener + onPseudoGridItemClickListener) { + mOnPseudoGridItemClickListener = onPseudoGridItemClickListener; + } + + @Override + public int getCount() { + return mItems.length; + } + + @Override + public CustomTile.ExpandedItem getItem(int position) { + return mItems[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int i, View convertView, ViewGroup parent) { + UserDetailItemView v = UserDetailItemView.convertOrInflate( + mContext, convertView, parent); + CustomTile.ExpandedItem item = getItem(i); + Drawable d = null; + if (item.itemDrawableResourceId != 0 && item.itemBitmapResource == null) { + try { + d = getPackageContext(mPkg, mContext).getResources() + .getDrawable(item.itemDrawableResourceId); + } catch (Throwable t) { + Log.w(TAG, "Error creating package context" + mPkg + + " id=" + item.itemDrawableResourceId, t); + } + } else { + d = new BitmapDrawable(mContext.getResources(), item.itemBitmapResource); + } + if (v != convertView) { + v.setOnClickListener(this); + } + String name = item.itemTitle; + v.setActivated(true); + v.bind(name, d); + v.setTag(item); + return v; + } + + @Override + public void onClick(View view) { + CustomTile.ExpandedItem item = (CustomTile.ExpandedItem) view.getTag(); + mOnPseudoGridItemClickListener.onPsuedoGridItemClick(view, item); + } + } + + private static Context getPackageContext(String pkg, Context context) { + Context packageContext; + try { + packageContext = context.createPackageContext(pkg, 0); + } catch (Throwable t) { + Log.w(TAG, "Error creating package context" + pkg, t); + return null; + } + return packageContext; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetailItemsList.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItemsList.java new file mode 100644 index 0000000..3e0ab8b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItemsList.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs; + +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; + +import android.widget.TextView; +import com.android.systemui.R; + +import cyanogenmod.app.CustomTile; + +import java.util.List; + +/** + * Quick settings common detail list view with line items. + */ +public class QSDetailItemsList extends FrameLayout { + private static final String TAG = "QSDetailItemsList"; + + private ListView mListView; + private View mEmpty; + private TextView mEmptyText; + private ImageView mEmptyIcon; + + public QSDetailItemsList(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mTag = TAG; + } + + public static QSDetailItemsList convertOrInflate(Context context, + View convertView, ViewGroup parent) { + if (convertView instanceof QSDetailItemsList) { + return (QSDetailItemsList) convertView; + } + LayoutInflater inflater = LayoutInflater.from(context); + return (QSDetailItemsList) inflater.inflate(R.layout.qs_detail_items_list, parent, false); + } + + public void setAdapter(ListAdapter adapter) { + mListView.setAdapter(adapter); + } + + public ListView getListView() { + return mListView; + } + + public void setEmptyState(int icon, int text) { + mEmptyIcon.setImageResource(icon); + mEmptyText.setText(text); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mListView = (ListView) findViewById(android.R.id.list); + mListView.setOnTouchListener(new OnTouchListener() { + // Setting on Touch Listener for handling the touch inside ScrollView + @Override + public boolean onTouch(View v, MotionEvent event) { + // Disallow the touch request for parent scroll on touch of child view + v.getParent().requestDisallowInterceptTouchEvent(true); + return false; + } + }); + mEmpty = findViewById(android.R.id.empty); + mEmpty.setVisibility(GONE); + mEmptyText = (TextView) mEmpty.findViewById(android.R.id.title); + mEmptyIcon = (ImageView) mEmpty.findViewById(android.R.id.icon); + mListView.setEmptyView(mEmpty); + } + + public static class QSCustomDetailListAdapter extends ArrayAdapter<CustomTile.ExpandedItem> { + private String mPackage; + + public QSCustomDetailListAdapter(String externalPackage, Context context, + List<CustomTile.ExpandedItem> objects) { + super(context, R.layout.qs_detail_item, objects); + mPackage = externalPackage; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + LinearLayout view = (LinearLayout) inflater.inflate( + R.layout.qs_detail_item, parent, false); + + final CustomTile.ExpandedItem item = getItem(position); + Drawable d = null; + if (item.itemDrawableResourceId != 0 && item.itemBitmapResource == null) { + try { + d = getPackageContext(mPackage, getContext()).getResources() + .getDrawable(item.itemDrawableResourceId); + } catch (Throwable t) { + Log.w(TAG, "Error creating package context" + mPackage + + " id=" + item.itemDrawableResourceId, t); + } + } else { + d = new BitmapDrawable(getContext().getResources(), item.itemBitmapResource); + } + final ImageView iv = (ImageView) view.findViewById(android.R.id.icon); + iv.setImageDrawable(d); + iv.getOverlay().clear(); + //TODO: hide icon for the time being until the API supports granular item manipulation + final ImageView iv2 = (ImageView) view.findViewById(android.R.id.icon2); + iv2.setVisibility(View.GONE); + final TextView title = (TextView) view.findViewById(android.R.id.title); + title.setText(item.itemTitle); + final TextView summary = (TextView) view.findViewById(android.R.id.summary); + final boolean twoLines = !TextUtils.isEmpty(item.itemSummary); + title.setMaxLines(twoLines ? 1 : 2); + summary.setVisibility(twoLines ? VISIBLE : GONE); + summary.setText(twoLines ? item.itemSummary : null); + view.setMinimumHeight(getContext().getResources().getDimensionPixelSize( + twoLines ? R.dimen.qs_detail_item_height_twoline + : R.dimen.qs_detail_item_height)); + return view; + } + } + + private static Context getPackageContext(String pkg, Context context) { + Context packageContext; + try { + packageContext = context.createPackageContext(pkg, 0); + } catch (Throwable t) { + Log.w(TAG, "Error creating package context" + pkg, t); + return null; + } + return packageContext; + } + + public static class QSDetailListAdapter extends ArrayAdapter<QSDetailItems.Item> { + private QSDetailItems.Callback mCallback; + + public QSDetailListAdapter(Context context, List<QSDetailItems.Item> objects) { + super(context, R.layout.qs_detail_item, objects); + } + + public void setCallback(QSDetailItems.Callback cb) { + mCallback = cb; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + LinearLayout view = (LinearLayout) inflater.inflate( + R.layout.qs_detail_item, parent, false); + + view.setClickable(false); // let list view handle this + + final QSDetailItems.Item item = getItem(position); + + final ImageView iv = (ImageView) view.findViewById(android.R.id.icon); + iv.setImageResource(item.icon); + iv.getOverlay().clear(); + if (item.overlay != null) { + item.overlay.setBounds(0, 0, item.overlay.getIntrinsicWidth(), + item.overlay.getIntrinsicHeight()); + iv.getOverlay().add(item.overlay); + } + final TextView title = (TextView) view.findViewById(android.R.id.title); + title.setText(item.line1); + final TextView summary = (TextView) view.findViewById(android.R.id.summary); + final boolean twoLines = !TextUtils.isEmpty(item.line2); + title.setMaxLines(twoLines ? 1 : 2); + summary.setVisibility(twoLines ? VISIBLE : GONE); + summary.setText(twoLines ? item.line2 : null); + view.setMinimumHeight(getContext().getResources().getDimensionPixelSize( + twoLines ? R.dimen.qs_detail_item_height_twoline : R.dimen.qs_detail_item_height)); + + final ImageView disconnect = (ImageView) view.findViewById(android.R.id.icon2); + disconnect.setVisibility(item.canDisconnect ? VISIBLE : GONE); + disconnect.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mCallback != null) { + mCallback.onDetailItemDisconnect(item); + } + } + }); + return view; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDragPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSDragPanel.java new file mode 100644 index 0000000..13f552c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDragPanel.java @@ -0,0 +1,2331 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.app.ActivityManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.PorterDuff; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.UserHandle; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.ArrayMap; +import android.util.AttributeSet; +import android.util.Log; +import android.view.DragEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseExpandableListAdapter; +import android.widget.ExpandableListView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.FontSizeUtils; +import com.android.systemui.R; +import com.android.systemui.cm.UserContentObserver; +import com.android.systemui.qs.tiles.CustomQSTile; +import com.android.systemui.qs.tiles.EditTile; +import com.android.systemui.settings.BrightnessController; +import com.android.systemui.settings.ToggleSlider; +import com.android.systemui.statusbar.phone.NotificationPanelView; +import com.android.systemui.statusbar.phone.QSTileHost; +import com.android.systemui.statusbar.policy.BrightnessMirrorController; +import com.android.systemui.tuner.QsTuner; +import com.viewpagerindicator.CirclePageIndicator; +import cyanogenmod.app.StatusBarPanelCustomTile; +import cyanogenmod.providers.CMSettings; +import org.cyanogenmod.internal.logging.CMMetricsLogger; +import org.cyanogenmod.internal.util.QSUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; + +public class QSDragPanel extends QSPanel implements View.OnDragListener, View.OnLongClickListener { + + private static final String TAG = "QSDragPanel"; + + public static final boolean DEBUG_TILES = false; + public static final boolean DEBUG_DRAG = false; + + private static final int MAX_ROW_COUNT = 3; + + // how long to wait before resetting the page + private static final int PAGE_RESET_DELAY = 10000; + + protected final ArrayList<QSPage> mPages = new ArrayList<>(); + + private NotificationPanelView mPanelView; + protected QSViewPager mViewPager; + protected PagerAdapter mPagerAdapter; + QSPanelTopView mQsPanelTop; + CirclePageIndicator mPageIndicator; + private int mPageIndicatorHeight; + + private TextView mDetailRemoveButton; + private DragTileRecord mDraggingRecord, mLastDragRecord; + private ViewGroup mDetailButtons; + private boolean mEditing; + private boolean mDragging; + private float mLastTouchLocationX, mLastTouchLocationY; + private int mLocationHits; + private int mLastLeftShift = -1; + private int mLastRightShift = -1; + private boolean mRestored; + private boolean mRestoring; + // whether the current view we are dragging in has shifted tiles + private boolean mMovedByLocation = false; + + protected boolean mFirstRowLarge = true; + private SettingsObserver mSettingsObserver; + + List<TileRecord> mCurrentlyAnimating + = Collections.synchronizedList(new ArrayList<TileRecord>()); + + private Runnable mResetPage = new Runnable() { + @Override + public void run() { + if (!mExpanded) { + // only reset when the user isn't interacting at all + mViewPager.setCurrentItem(0); + mPagerAdapter.notifyDataSetChanged(); + } + } + }; + + public QSDragPanel(Context context) { + this(context, null); + } + + public QSDragPanel(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void setupViews() { + updateResources(); + + mDetail = LayoutInflater.from(mContext).inflate(R.layout.qs_detail, this, false); + mDetailButtons = (ViewGroup) mDetail.findViewById(R.id.buttons); + mDetailContent = (ViewGroup) mDetail.findViewById(android.R.id.content); + mDetailRemoveButton = (TextView) mDetail.findViewById(android.R.id.button3); + mDetailSettingsButton = (TextView) mDetail.findViewById(android.R.id.button2); + mDetailDoneButton = (TextView) mDetail.findViewById(android.R.id.button1); + updateDetailText(); + mDetail.setVisibility(GONE); + mDetail.setClickable(true); + + mQsPanelTop = (QSPanelTopView) LayoutInflater.from(mContext).inflate(R.layout.qs_tile_top, + this, false); + + mBrightnessView = mQsPanelTop.getBrightnessView(); + mFooter = new QSFooter(this, mContext); + + // add target click listener + mQsPanelTop.getAddTarget().setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + TilesListAdapter adapter = new TilesListAdapter(mContext, QSDragPanel.this); + showDetailAdapter(true, adapter, v.getLocationOnScreen()); + mDetail.bringToFront(); + } + }); + mViewPager = new QSViewPager(getContext()); + mViewPager.setDragPanel(this); + + mPageIndicator = new CirclePageIndicator(getContext()); + addView(mDetail); + addView(mQsPanelTop); + addView(mViewPager); + addView(mPageIndicator); + addView(mFooter.getView()); + + mClipper = new QSDetailClipper(mDetail); + + mBrightnessController = new BrightnessController(getContext(), + (ImageView) mQsPanelTop.getBrightnessView().findViewById(R.id.brightness_icon), + (ToggleSlider) mQsPanelTop.getBrightnessView().findViewById(R.id.brightness_slider)); + + mDetailDoneButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + announceForAccessibility( + mContext.getString(R.string.accessibility_desc_quick_settings)); + closeDetail(); + } + }); + + mPagerAdapter = new PagerAdapter() { + @Override + public Object instantiateItem(ViewGroup container, int position) { + if (DEBUG_TILES) { + Log.d(TAG, "instantiateItem() called with " + + "container = [" + container + "], position = [" + position + "]"); + } + + if (mEditing && position == 0) { + QSSettings qss = (QSSettings) + View.inflate(container.getContext(), R.layout.qs_settings, null); + qss.setHost(mHost); + container.addView(qss, 0); + return qss; + } else { + final int adjustedPosition = mEditing ? position - 1 : position; + QSPage page = mPages.get(adjustedPosition); + if (!page.isAttachedToWindow()) { + container.addView(page); + } + return page; + } + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (DEBUG_TILES) { + Log.d(TAG, "destroyItem() called with " + "container = [" + + container + "], position = [" + position + "], object = [" + + object + "]"); + } + if (object instanceof View) { + container.removeView((View) object); + } + } + + @Override + public int getItemPosition(Object object) { + if (object instanceof QSPage) { + if (mEditing != ((QSPage) object).getAdapterEditingState()) { + // position of item changes when we set change the editing mode, + // sync it and send the new position + ((QSPage) object).setAdapterEditingState(mEditing); + + // calculate new position + int indexOf = ((QSPage) object).getPageIndex(); + if (mEditing) return indexOf + 1; + else return indexOf; + + } else if (!mPages.contains(object) && !mDragging) { + // only return none if we aren't dragging (object may be removed from + // the records array temporarily and we might think we have less pages, + // we don't want to prematurely remove this page + return POSITION_NONE; + } else { + + return POSITION_UNCHANGED; + } + + } else if (object instanceof QSSettings) { + if (((QSSettings) object).getAdapterEditingState() != mEditing) { + ((QSSettings) object).setAdapterEditingState(mEditing); + if (mEditing) return 0 /* locked at position 0 */; + else return POSITION_NONE; + } else { + return POSITION_UNCHANGED; + } + } + return super.getItemPosition(object); + } + + @Override + public int getCount() { + final int qsPages = Math.max(getCurrentMaxPageCount(), 1); + + if (mPages != null && qsPages > mPages.size()) { + for(int i = mPages.size(); i < qsPages; i++) { + mPages.add(i, new QSPage(mViewPager.getContext(), QSDragPanel.this, i)); + } + } + + if (mEditing) return qsPages + 1; + return qsPages; + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + }; + mViewPager.setAdapter(mPagerAdapter); + + mPageIndicator.setViewPager(mViewPager); + mPageIndicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { + + @Override + public void onPageScrolled(int position, float positionOffset, + int positionOffsetPixels) { + if (DEBUG_DRAG) { + Log.i(TAG, "onPageScrolled() called with " + "position = [" + + position + "], positionOffset = [" + positionOffset + + "], positionOffsetPixels = [" + positionOffsetPixels + "]"); + } + + if (mEditing) { + float targetTranslationX = 0; + + // targetTranslationX = where it's supposed to be - diff + int homeLocation = mViewPager.getMeasuredWidth(); + + // how far away from homeLocation is the scroll? + if (positionOffsetPixels < homeLocation + && position == 0) { + targetTranslationX = homeLocation - positionOffsetPixels; + } + mQsPanelTop.setTranslationX(targetTranslationX); + } + } + + @Override + public void onPageSelected(int position) { + if (mDragging && position != mDraggingRecord.page + && !mViewPager.isFakeDragging() && !mRestoring) { + if (DEBUG_DRAG) { + Log.w(TAG, "moving drag record to page: " + position); + } + + // remove it from the previous page and add it here + final QSPage sourceP = getPage(mDraggingRecord.page); + final QSPage targetP = getPage(position); + + sourceP.removeView(mDraggingRecord.tileView); + mDraggingRecord.page = position; + targetP.addView(mDraggingRecord.tileView); + + // set coords off screen until we're ready to place it + mDraggingRecord.tileView.setX(-mDraggingRecord.tileView.getMeasuredWidth()); + mDraggingRecord.tileView.setY(-mDraggingRecord.tileView.getMeasuredHeight()); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + } + }); + mViewPager.setOverScrollMode(OVER_SCROLL_NEVER); + + setClipChildren(false); + + mSettingsObserver = new SettingsObserver(new Handler()); + + mViewPager.setOnDragListener(QSDragPanel.this); + mQsPanelTop.setOnDragListener(QSDragPanel.this); + mPageIndicator.setOnDragListener(QSDragPanel.this); + setOnDragListener(QSDragPanel.this); + + mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER); + } + + @Override + public boolean hasOverlappingRendering() { + return mClipper.isAnimating() || mEditing || !mCurrentlyAnimating.isEmpty(); + } + + @Override + public void setBrightnessMirror(BrightnessMirrorController c) { + super.onFinishInflate(); + ToggleSlider brightnessSlider = + (ToggleSlider) mQsPanelTop.findViewById(R.id.brightness_slider); + ToggleSlider mirror = (ToggleSlider) c.getMirror().findViewById(R.id.brightness_slider); + brightnessSlider.setMirror(mirror); + brightnessSlider.setMirrorController(c); + } + + protected void drawTile(TileRecord r, QSTile.State state) { + if (mEditing) { + if ((r.tile instanceof CustomQSTile) + && (((CustomQSTile) r.tile).isUserRemoved() + || ((CustomQSTile) r.tile).getTile() == null)) { + // don't modify visibility state if removed, or not yet published + } else { + state.visible = true; + state.enabled = true; + } + } + final int visibility = state.visible ? VISIBLE : GONE; + setTileVisibility(r.tileView, visibility); + setTileEnabled(r.tileView, state.enabled); + r.tileView.onStateChanged(state); + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + for (TileRecord r : mRecords) { + r.tile.setListening(mListening); + } + mFooter.setListening(mListening); + mQsPanelTop.setListening(mListening); + if (mListening) { + refreshAllTiles(); + } + if (mListening) { + mSettingsObserver.observe(); + } else { + mSettingsObserver.unobserve(); + } + + if (isLaidOut() && listening && showBrightnessSlider()) { + mBrightnessController.registerCallbacks(); + } else { + mBrightnessController.unregisterCallbacks(); + } + } + + private void persistRecords() { + // persist the new config. + List<String> newTiles = new ArrayList<>(); + for (TileRecord record : mRecords) { + newTiles.add(mHost.getSpec(record.tile)); + } + mHost.setTiles(newTiles); + } + + public void setEditing(boolean editing) { + if (mEditing == editing) return; + final boolean isOnSettings = isOnSettingsPage(); + + mQsPanelTop.setEditing(editing, isOnSettings); + if (!editing) { + persistRecords(); + + refreshAllTiles(); + + mQsPanelTop.setTranslationX(0); + if (isOnSettings) { + mViewPager.setCurrentItem(1, true); + } + } + mEditing = editing; + mPagerAdapter.notifyDataSetChanged(); + + mPageIndicator.setEditing(editing); + mViewPager.setOffscreenPageLimit(mEditing ? getCurrentMaxPageCount() + 1 : 1); + mPagerAdapter.notifyDataSetChanged(); + + // clear the record state + for (TileRecord record : mRecords) { + setupRecord(record); + drawTile(record, record.tile.getState()); + } + + requestLayout(); + } + + protected void onStartDrag() { + mQsPanelTop.onStartDrag(); + } + + protected void onStopDrag() { + mDraggingRecord.tileView.setAlpha(1f); + + mLastDragRecord = mDraggingRecord; + mDraggingRecord = null; + mDragging = false; + mRestored = false; + + mLastLeftShift = -1; + mLastRightShift = -1; + + mQsPanelTop.onStopDrag(); + } + + protected View getDropTarget() { + return mQsPanelTop.getDropTarget(); + } + + public View getBrightnessView() { + return mQsPanelTop.getBrightnessView(); + } + + public boolean isEditing() { + return mEditing; + } + + protected int getPagesForCount(int tileCount) { + if (tileCount == 0) { + return 1; + } + tileCount = Math.max(0, tileCount - getTilesPerPage(true)); + // first page + rest of tiles + return 1 + (int) Math.ceil(tileCount / (double) getTilesPerPage(false)); + } + + protected int getCurrentMaxPageCount() { + int initialSize = mRecords.size(); + return getPagesForCount(initialSize); + } + + @Override + protected void updateDetailText() { + super.updateDetailText(); + mDetailRemoveButton.setText(R.string.quick_settings_remove); + } + + public void setTiles(final Collection<QSTile<?>> tilesCollection) { + // we try to be as efficient as possible here because this can happen while the user + // is in edit mode, or maybe even while tiles are animating + // step 1: stop all animations + // step 2: remove tiles no longer to be used, cache ones that are still valid + // step 3: remove empty viewpager pages + // step 4: generate new tiles, re-add cached ones + + if (DEBUG_TILES) { + Log.i(TAG, "setTiles() called with tiles = [" + tilesCollection + "]"); + } + if (mLastDragRecord != null && mRecords.indexOf(mLastDragRecord) == -1) { + // the last removed record might be stored in mLastDragRecord if we just shifted + // re-add it to the list so we'll clean it up below + mRecords.add(mLastDragRecord); + mLastDragRecord = null; + } + + // step kinda-1 + if (mDraggingRecord != null) { + // dragging record might be animating back, force it to finished position + mDraggingRecord.tileView.animate().cancel(); + } + + int currentViewPagerPage = mViewPager.getCurrentItem(); + int removedPages = 0; + + Map<QSTile<?>, DragTileRecord> cachedRecords = new ArrayMap<>(); + ListIterator<TileRecord> iterator = mRecords.listIterator(mRecords.size()); + + int recordsRemoved = 0; + // cleanup current records + while (iterator.hasPrevious()) { // mRecords + DragTileRecord dr = (DragTileRecord) iterator.previous(); + + // step 1 + dr.tileView.animate().cancel(); + + // step 2 + if (tilesCollection.contains(dr.tile)) { + if (DEBUG_TILES) { + Log.i(TAG, "caching tile: " + dr.tile); + } + cachedRecords.put(dr.tile, dr); + } else { + if (dr.page >= 0) { + if (DEBUG_TILES) { + Log.w(TAG, "removed dr.tileView: " + dr.tileView + " from page: " + + dr.page + " (dest page: " + dr.destinationPage + ")"); + } + + removeTileView(dr.tileView); + } + if (DEBUG_TILES) { + Log.i(TAG, "removing tile: " + dr.tile); + } + + // remove record + iterator.remove(); + recordsRemoved++; + + dr.page = -1; + dr.destinationPage = -1; + } + } + + // at this point cachedRecords should have all retained tiles, no new or old tiles + int delta = tilesCollection.size() - cachedRecords.size() - recordsRemoved; + if (DEBUG_TILES) { + Log.i(TAG, "record map delta: " + delta); + } + + // step 3 + final Iterator<QSPage> pageIterator = mPages.iterator(); + while (pageIterator.hasNext()) { + final QSPage page = pageIterator.next(); + final int viewpagerIndex = page.getPageIndex() + (mEditing ? 1 : 0); + final int childCount = page.getChildCount(); + + if (DEBUG_TILES) { + Log.d(TAG, "page " + viewpagerIndex + " has " + childCount); + } + if (page.getPageIndex() >= getCurrentMaxPageCount() - 1) { + if (DEBUG_TILES) { + Log.d(TAG, "page : " + page + " has " + childCount + " children"); + } + if (childCount == 0) { + removedPages++; + + page.removeAllViews(); + mPagerAdapter.startUpdate(mViewPager); + mPagerAdapter.destroyItem(mViewPager, viewpagerIndex, page); + mPagerAdapter.finishUpdate(mViewPager); + mPagerAdapter.notifyDataSetChanged(); + } + } + } + + if (removedPages > 0) { + // even though we explicitly destroy old pages, without this call, + // the viewpager doesn't seem to want to pick up the fact that we have less pages + // and allows "empty" scrolls to the right where there is no page. + if (DEBUG_TILES) { + Log.d(TAG, "re-setting adapter, page: " + currentViewPagerPage); + } + mViewPager.setAdapter(mPagerAdapter); + mViewPager.setCurrentItem(Math.min(currentViewPagerPage, mPagerAdapter.getCount()), + false); + mPagerAdapter.notifyDataSetChanged(); + } + + // step 4 + mRecords.ensureCapacity(tilesCollection.size()); + int runningCount = 0; + + final Iterator<QSTile<?>> newTileIterator = tilesCollection.iterator(); + while (newTileIterator.hasNext()) { + QSTile<?> tile = newTileIterator.next(); + if (tile instanceof CustomQSTile) { + if (((CustomQSTile) tile).isUserRemoved() + || ((CustomQSTile) tile).getTile() == null) { + // tile not published yet + continue; + } + } + final int tileDestPage = getPagesForCount(runningCount + 1) - 1; + + if (DEBUG_TILES) { + Log.d(TAG, "tile at : " + runningCount + ": " + tile + + " to dest page: " + tileDestPage); + } + DragTileRecord record; + if (!cachedRecords.containsKey(tile)) { + if (DEBUG_TILES) { + Log.d(TAG, "tile at: " + runningCount + " not cached, adding it to records"); + } + record = makeRecord(tile); + record.destinationPage = tileDestPage; + mRecords.add(runningCount, record); + mPagerAdapter.notifyDataSetChanged(); + } else { + record = cachedRecords.get(tile); + if (DEBUG_TILES) { + Log.d(TAG, "tile at : " + runningCount + ": cached, restoring: " + record); + } + + mPages.get(record.page).removeView(record.tileView); + + record.page = -1; + record.destinationPage = tileDestPage; + + mRecords.remove(record); + mRecords.add(runningCount, record); + mPagerAdapter.notifyDataSetChanged(); + } + if (record.page == -1) { + // add the view + mPages.get(record.destinationPage).addView(record.tileView); + record.page = record.destinationPage; + if (DEBUG_TILES) { + Log.d(TAG, "added view " + record); + } + } + runningCount++; + } + + if (isShowingDetail()) { + mDetail.bringToFront(); + } + mPagerAdapter.notifyDataSetChanged(); + + refreshAllTiles(); + requestLayout(); + } + + private DragTileRecord makeRecord(final QSTile<?> tile) { + if (DEBUG_TILES) { + Log.d(TAG, "+++ makeRecord() called with " + "tile = [" + tile + "]"); + } + final DragTileRecord r = new DragTileRecord(); + + + r.tile = tile; + r.page = -1; + r.destinationPage = -1; + r.tileView = tile.createTileView(mContext); + final QSTile.Callback callback = new QSTile.Callback() { + @Override + public void onStateChanged(QSTile.State state) { + if (!r.openingDetail) { + drawTile(r, state); + } + } + + @Override + public void onShowDetail(boolean show) { + showDetail(show, r); + } + + @Override + public void onToggleStateChanged(boolean state) { + if (mDetailRecord == r) { + fireToggleStateChanged(state); + } + } + + @Override + public void onScanStateChanged(boolean state) { + r.scanState = state; + if (mDetailRecord == r) { + fireScanStateChanged(r.scanState); + } + } + + @Override + public void onAnnouncementRequested(CharSequence announcement) { + announceForAccessibility(announcement); + } + }; + r.tile.setCallback(callback); + final OnClickListener click = new OnClickListener() { + @Override + public void onClick(View v) { + if (!mEditing || r.tile instanceof EditTile) { + r.tile.click(); + } + } + }; + final OnClickListener clickSecondary = new OnClickListener() { + @Override + public void onClick(View v) { + if (!mEditing) { + r.tile.secondaryClick(); + } + } + }; + final OnLongClickListener longClick = new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (!mEditing) { + r.tile.longClick(); + } else { + QSDragPanel.this.onLongClick(r.tileView); + } + return true; + } + }; + r.tileView.init(click, clickSecondary, longClick); + r.tile.setListening(mListening); + r.tile.refreshState(); + r.tileView.setVisibility(mEditing ? View.VISIBLE : View.GONE); + callback.onStateChanged(r.tile.getState()); + + if (DEBUG_TILES) { + Log.d(TAG, "--- makeRecord() called with " + "tile = [" + tile + "]"); + } + return r; + } + + private void removeTileView(QSTileView v) { + for (QSPage page : mPages) { + page.removeView(v); + page.removeTransientView(v); + } + + } + + private void removeDraggingRecord() { + // what spec is this tile? + String spec = mHost.getSpec(mDraggingRecord.tile); + if (DEBUG_TILES) { + Log.w(TAG, "removing tile: " + mDraggingRecord + " with spec: " + spec); + } + onStopDrag(); + mHost.remove(spec); + } + + public int getTilesPerPage(boolean firstPage) { + if ((!mFirstRowLarge && firstPage) || !firstPage) { + return QSTileHost.TILES_PER_PAGE + 1; + } + return QSTileHost.TILES_PER_PAGE; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + + mQsPanelTop.measure(exactly(width), MeasureSpec.UNSPECIFIED); + mViewPager.measure(exactly(width), MeasureSpec.UNSPECIFIED); + mPageIndicator.measure(exactly(width), atMost(mPageIndicatorHeight)); + mFooter.getView().measure(exactly(width), MeasureSpec.UNSPECIFIED); + + int h = getRowTop(getCurrentMaxRow() + 1) + mPanelPaddingBottom; + + if (mFooter.hasFooter()) { + h += mFooter.getView().getMeasuredHeight(); + } + mGridHeight = h; + + mDetail.measure(exactly(width), MeasureSpec.UNSPECIFIED); + + if (mDetail.getMeasuredHeight() < h) { + mDetail.measure(exactly(width), exactly(h)); + } + if (isShowingDetail() && !isClosingDetail() && mExpanded) { + h = mDetail.getMeasuredHeight(); + } + + setMeasuredDimension(width, h); + for (TileRecord record : mRecords) { + setupRecord(record); + } + } + + private void setupRecord(TileRecord record) { + record.tileView.setEditing(mEditing); + record.tileView.setOnDragListener(mEditing ? this : null); + } + + public static int exactly(int size) { + return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); + } + + public static int atMost(int size) { + return MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); + } + + @Override + protected void handleShowDetailTile(TileRecord r, boolean show) { + if (r instanceof DragTileRecord) { + 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 = (int) ((DragTileRecord) r).destination.x + r.tileView.getWidth() / 2; + int y = mViewPager.getTop() + + (int) ((DragTileRecord) r).destination.y + r.tileView.getHeight() / 2; + handleShowDetailImpl(r, show, x, y); + } else { + super.handleShowDetailTile(r, show); + } + mPageIndicator.setVisibility(!show ? View.VISIBLE : View.INVISIBLE); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int w = getWidth(); + + mQsPanelTop.layout(0, 0, w, mQsPanelTop.getMeasuredHeight()); + + int viewPagerBottom = mQsPanelTop.getMeasuredHeight() + mViewPager.getMeasuredHeight(); + // view pager laid out from top of brightness view to bottom to page through settings + mViewPager.layout(0, 0, w, viewPagerBottom); + + mDetail.layout(0, 0, w, mDetail.getMeasuredHeight()); + + if (mFooter.hasFooter()) { + View footer = mFooter.getView(); + footer.layout(0, getMeasuredHeight() - footer.getMeasuredHeight(), + footer.getMeasuredWidth(), getMeasuredHeight()); + } + + if (!isShowingDetail() && !isClosingDetail()) { + mQsPanelTop.bringToFront(); + + } + // layout page indicator inside viewpager inset + mPageIndicator.layout(0, b - mPageIndicatorHeight, w, b); + } + + protected int getRowTop(int row) { + int baseHeight = mQsPanelTop.getMeasuredHeight(); + if (row <= 0) return baseHeight; + return baseHeight + mLargeCellHeight - mDualTileUnderlap + (row - 1) * mCellHeight; + } + + public int getColumnCount() { + return mColumns; + } + + public int getColumnCount(int page, int row, boolean smart) { + int cols = 0; + for (Record record : mRecords) { + if (record instanceof DragTileRecord) { + DragTileRecord dr = (DragTileRecord) record; + if (dr.tileView.getVisibility() == GONE) continue; + if (dr.destinationPage != page) continue; + if (dr.row == row) cols++; + } + } + + if (smart && isEditing() && (isDragging() || mRestoring) && !isDragRecordAttached()) { + // if shifting tiles back, and one moved from previous page + + // if it's the very last row on the last page, we should add an extra column to account + // for where teh dragging lastRecord would go + DragTileRecord lastRecord = (DragTileRecord) mRecords.get(mRecords.size() - 1); + if (lastRecord.destinationPage == page && lastRecord.row == row + && cols < getColumnCount()) { + cols++; + if (DEBUG_DRAG) { + boolean draggingRecordBefore = isBefore(mDraggingRecord, lastRecord); + Log.w(TAG, "adding another col, cols: " + cols + ", last: " + lastRecord + + ", drag: " + mDraggingRecord + + ", and dragging record before last: " + draggingRecordBefore); + } + } + } + return cols; + } + + public int getColumnCount(int page, int row) { + return getColumnCount(page, row, true); + } + + public int getCurrentMaxRow() { + int max = 0; + for (TileRecord record : mRecords) { + if (record.row > max) { + max = record.row; + } + } + return max; + } + + public int getLeft(int page, int row, int col) { + final boolean firstRowLarge = mFirstRowLarge && page == 0 && row == 0; + int cols = firstRowLarge ? 2 : mColumns; + return getLeft(row, col, cols, firstRowLarge); + } + + public int getLeft(int page, int row, int col, int cols) { + final boolean firstRowLarge = mFirstRowLarge && page == 0 && row == 0; + return getLeft(row, col, cols, firstRowLarge); + } + + public int getLeft(int row, int col, int cols, boolean firstRowLarge) { + final int cw = row == 0 && firstRowLarge ? mLargeCellWidth : mCellWidth; + final int extra = (getWidth() - cw * cols) / (cols + 1); + int left = col * cw + (col + 1) * extra; + return left; + } + + public QSPage getCurrentPage() { + return mPages.get(mViewPager.getCurrentItem()); + } + + public QSPage getPage(int pos) { + if (pos >= mPages.size()) { + return null; + } + return mPages.get(pos); + } + + private TileRecord getRecord(View v) { + for (TileRecord record : mRecords) { + if (record.tileView == v) { + return record; + } + } + return null; + } + + @Override + public boolean onDrag(View v, DragEvent event) { + final DragTileRecord targetTile = (DragTileRecord) getRecord(v); + boolean originatingTileEvent = mDraggingRecord != null && v == mDraggingRecord.tileView; + + final int dragRecordIndex = mRecords.indexOf(mDraggingRecord); + boolean dragRecordAttached = dragRecordIndex != -1; + switch (event.getAction()) { + case DragEvent.ACTION_DRAG_STARTED: + if (DEBUG_DRAG) { + Log.v(TAG, "ACTION_DRAG_STARTED on view: " + v); + } + + if (originatingTileEvent) { + if (DEBUG_DRAG) { + Log.v(TAG, "ACTION_DRAG_STARTED on target view."); + } + mRestored = false; + mQsPanelTop.setDropIcon(R.drawable.ic_qs_tile_delete_disable, R.color.qs_tile_trash_normal_tint); + } + + break; + + case DragEvent.ACTION_DRAG_ENTERED: + if (DEBUG_DRAG) { + if (targetTile != null) { + Log.v(TAG, "ACTION_DRAG_ENTERED on view with tile: " + targetTile); + } else { + Log.v(TAG, "ACTION_DRAG_ENTERED on view: " + v); + } + } + mLocationHits = 0; + mMovedByLocation = false; + + if (v == mQsPanelTop) { + int icon, color; + if (mDraggingRecord.tile instanceof EditTile) { + // use a different warning, user can't erase this one + icon = R.drawable.ic_qs_tile_delete_disable_avd; + color = R.color.qs_tile_trash_delete_tint_warning; + } else { + icon = R.drawable.ic_qs_tile_delete_disable; + color = R.color.qs_tile_trash_delete_tint; + } + + mQsPanelTop.setDropIcon(icon, color); + } + + if (!originatingTileEvent && v != getDropTarget() && targetTile != null) { + if (DEBUG_DRAG) { + Log.e(TAG, "entered tile " + targetTile); + } + if (mCurrentlyAnimating.isEmpty() + && !mViewPager.isFakeDragging() + && !dragRecordAttached) { + mMovedByLocation = true; + shiftTiles(targetTile, true); + } else { + if (DEBUG_DRAG) { + Log.w(TAG, "ignoring action enter for animating tiles and fake drags"); + } + } + } + + break; + case DragEvent.ACTION_DRAG_ENDED: + if (DEBUG_DRAG) { + Log.v(TAG, "ACTION_DRAG_ENDED on view: " + v + "(tile: " + + targetTile + "), result: " + event.getResult()); + } + if (originatingTileEvent && !event.getResult()) { + // view pager probably ate the event + restoreDraggingTilePosition(v, null); + } + + break; + + case DragEvent.ACTION_DROP: + if (DEBUG_DRAG) { + Log.v(TAG, "ACTION_DROP, event loc: " + event.getX() + ", " + event.getY() + + " + with tile: " + targetTile + " and view: " + v); + } + mLastTouchLocationX = event.getX(); + mLastTouchLocationY = event.getY(); + + if (isDropTargetEvent(event, v)) { + if (DEBUG_DRAG) { + Log.d(TAG, "dropping on delete target!!"); + } + if (mDraggingRecord.tile instanceof EditTile) { + final QSTileView editTileView = mDraggingRecord.tileView; + + mQsPanelTop.toast(R.string.quick_settings_cannot_delete_edit_tile); + restoreDraggingTilePosition(v, new Runnable() { + @Override + public void run() { + // move edit tile to the back + final TileRecord editTile = getRecord(editTileView); + if (mRecords.remove(editTile)) { + // we depend on mHost.setTiles() placing it on the end + persistRecords(); + } + } + }); + break; + } else if (mDraggingRecord.tile instanceof CustomQSTile) { + ((CustomQSTile) mDraggingRecord.tile).setUserRemoved(true); + final String spec = mHost.getSpec(mDraggingRecord.tile); + restoreDraggingTilePosition(v, new Runnable() { + @Override + public void run() { + // it might get added back later by the app, but that's ok, + // we just want to reset its position after it has been removed. + mHost.remove(spec); + } + }); + } else { + mRestored = true; + removeDraggingRecord(); + } + } else { + restoreDraggingTilePosition(v, null); + } + break; + + case DragEvent.ACTION_DRAG_EXITED: + if (DEBUG_DRAG) { + if (targetTile != null) { + Log.v(TAG, "ACTION_DRAG_EXITED on view with tile: " + targetTile); + } else { + Log.v(TAG, "ACTION_DRAG_EXITED on view: " + v); + } + } + + if (v == mQsPanelTop) { + mQsPanelTop.setDropIcon(R.drawable.ic_qs_tile_delete_disable, R.color.qs_tile_trash_normal_tint); + } + + if (originatingTileEvent + && mCurrentlyAnimating.isEmpty() + && !mViewPager.isFakeDragging() + && dragRecordAttached + && mLastLeftShift == -1) { + + if (DEBUG_DRAG) { + Log.v(TAG, "target: " + targetTile + ", hit mLastRightShift: " + + mLastRightShift + ", mLastLeftShift: " + + mLastLeftShift + ", dragRecordIndex: " + + dragRecordIndex); + } + + // move tiles back + shiftTiles(mDraggingRecord, false); + break; + } + // fall through so exit events can trigger a left shift + case DragEvent.ACTION_DRAG_LOCATION: + mLastTouchLocationX = event.getX(); + mLastTouchLocationY = event.getY(); + + // do nothing if we're animating tiles + if (mCurrentlyAnimating.isEmpty() && !mViewPager.isFakeDragging()) { + if (v == mViewPager) { + // do we need to change pages? + int x = (int) event.getX(); + int width = mViewPager.getWidth(); + int scrollPadding = (int) (width * QSViewPager.SCROLL_PERCENT); + if (x < scrollPadding) { + if (mViewPager.canScrollHorizontally(-1)) { + mViewPager.animatePagerTransition(false); + return true; + } + } else if (x > width - scrollPadding) { + if (mViewPager.canScrollHorizontally(1)) { + mViewPager.animatePagerTransition(true); + return true; + } + } + } + if (DEBUG_DRAG) { + Log.v(TAG, "location hit:// target: " + targetTile + + ", hit mLastRightShift: " + mLastRightShift + + ", mLastLeftShift: " + mLastLeftShift + + ", dragRecordIndex: " + dragRecordIndex + + ", originatingTileEvent: " + originatingTileEvent + + ", mLocationHits: " + mLocationHits + + ", mMovedByLocation: " + mMovedByLocation); + } + + if (v != getDropTarget() && targetTile != null && !dragRecordAttached) { + // dragging around on another tile + if (mLocationHits++ == 30) { + if (DEBUG_DRAG) { + Log.w(TAG, "shifting right due to location hits."); + } + // add dragging tile to current page + shiftTiles(targetTile, true); + mMovedByLocation = true; + } else { + mLocationHits++; + } + } else if (mLastRightShift != -1 // right has shifted recently + && mLastLeftShift == -1 // -1 means its attached + && dragRecordIndex == mLastRightShift + && !originatingTileEvent + && !mMovedByLocation /* helps avoid continuous shifting */) { + // check if the location is on another tile/view + // that is not the last drag index, shift back left to revert back and + // potentially get ready for shifting right + if (DEBUG_DRAG) { + Log.w(TAG, "conditions met to reverse!!!! shifting left. <<<<<<<"); + } + shiftTiles((DragTileRecord) mRecords.get(mLastRightShift), false); + mMovedByLocation = true; + } + + } else { + if (DEBUG_DRAG) { + Log.i(TAG, "ignoring location event because things are animating, size: " + + mCurrentlyAnimating.size()); + } + } + break; + + default: + Log.w(TAG, "unhandled event"); + return false; + } + return true; + } + + private boolean isDropTargetEvent(DragEvent event, View v) { + if (DEBUG_DRAG) { + Log.d(TAG, "isDropTargetEvent() called with " + "event = [" + event + "], v = [" + v + "]"); + } + if (v == getDropTarget() || v == mQsPanelTop) { + if (DEBUG_DRAG) { + Log.d(TAG, "isDropTargetEvent() returns true by view"); + } + return true; + } + + if (v == mViewPager && mLastTouchLocationY <= getRowTop(0)) { + if (DEBUG_DRAG) { + Log.d(TAG, "isDropTargetEvent() returns true by loc"); + } + return true; + } + + return false; + } + + private void restoreDraggingTilePosition(View v, final Runnable onAnimationFinishedRunnable) { + if (mRestored) { + return; + } + mRestored = true; + mRestoring = true; + mCurrentlyAnimating.add(mDraggingRecord); + + if (DEBUG_DRAG) { + Log.i(TAG, "restoreDraggingTilePosition() called with " + + "v = [" + (v.getTag() != null ? v.getTag() : v) + "]"); + } + final boolean dragRecordDetached = mRecords.indexOf(mDraggingRecord) == -1; + + if (DEBUG_DRAG) { + Log.v(TAG, "mLastLeftShift: " + mLastLeftShift + + ", detached: " + dragRecordDetached + ", drag record: " + mDraggingRecord); + } + + final QSPage originalPage = getPage(mDraggingRecord.page); + originalPage.removeView(mDraggingRecord.tileView); + addTransientView(mDraggingRecord.tileView, 0); + mDraggingRecord.tileView.setTransitionVisibility(View.VISIBLE); + + // need to move center of the dragging view to the coords of the event. + final float touchEventBoxLeft = v.getX() + + (mLastTouchLocationX - (mDraggingRecord.tileView.getWidth() / 2)); + final float touchEventBoxTop = v.getY() + + (mLastTouchLocationY - (mDraggingRecord.tileView.getHeight() / 2)); + + mDraggingRecord.tileView.setX(touchEventBoxLeft); + mDraggingRecord.tileView.setY(touchEventBoxTop); + + if (dragRecordDetached) { + setToLastDestination(mDraggingRecord); + if (DEBUG_DRAG) { + Log.d(TAG, "setting drag record view to coords: x:" + touchEventBoxLeft + + ", y:" + touchEventBoxTop); + Log.d(TAG, "animating drag record to: " + mDraggingRecord + ", loc: " + + mDraggingRecord.destination); + } + } else { + mDraggingRecord.destination.x = getLeft(mDraggingRecord.destinationPage, + mDraggingRecord.row, mDraggingRecord.col, + getColumnCount(mDraggingRecord.destinationPage, mDraggingRecord.row)); + + mDraggingRecord.destination.y = getRowTop(mDraggingRecord.row); + } + + // setup x destination to animate to + float destinationX = mDraggingRecord.destination.x; + + // see if we should animate this to the left or right off the page + // the +1's are to account for the edit page + if (mDraggingRecord.destinationPage > mViewPager.getCurrentItem() - 1) { + if (DEBUG_DRAG) { + Log.d(TAG, "adding width to animate out >>>>>"); + } + destinationX += getWidth(); + } else if (mDraggingRecord.destinationPage < mViewPager.getCurrentItem() - 1) { + if (DEBUG_DRAG) { + Log.d(TAG, "removing width to animate out <<<<<"); + } + destinationX -= getWidth(); + } + + // setup y + float destinationY = mDraggingRecord.destination.y + mViewPager.getTop(); + + mDraggingRecord.tileView.animate() + .withLayer() + .x(destinationX) + .y(destinationY) // part of the viewpager now + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mDraggingRecord.tileView.setAlpha(1); + } + + @Override + public void onAnimationCancel(Animator animation) { + mViewPager.requestDisallowInterceptTouchEvent(false); + removeTransientView(mDraggingRecord.tileView); + mCurrentlyAnimating.remove(mDraggingRecord); + mRestoring = false; + mPagerAdapter.notifyDataSetChanged(); + onStopDrag(); + + if (onAnimationFinishedRunnable != null) { + postOnAnimation(onAnimationFinishedRunnable); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + mViewPager.requestDisallowInterceptTouchEvent(false); + + removeTransientView(mDraggingRecord.tileView); + + final QSPage targetP = getPage(mDraggingRecord.destinationPage); + + if (DEBUG_DRAG) { + if (dragRecordDetached) { + Log.i(TAG, "drag record was detached"); + } else { + Log.i(TAG, "drag record was attached"); + } + } + targetP.addView(mDraggingRecord.tileView); + mDraggingRecord.page = mDraggingRecord.destinationPage; + + mDraggingRecord.tileView.setX(mDraggingRecord.destination.x); + // reset this to be in the coords of the page, not viewpager anymore + mDraggingRecord.tileView.setY(mDraggingRecord.destination.y); + + mCurrentlyAnimating.remove(mDraggingRecord); + + mRestoring = false; + + if (dragRecordDetached) { + mRecords.add(mDraggingRecord); + mPagerAdapter.notifyDataSetChanged(); + } + onStopDrag(); + + if (onAnimationFinishedRunnable != null) { + postOnAnimation(onAnimationFinishedRunnable); + } else { + requestLayout(); + } + } + }); + } + + private void setToNextDestination(DragTileRecord tile) { + if (DEBUG_DRAG) { + Log.i(TAG, "+++setToNextDestination() called with " + "tile = [" + tile + "], at: " + + tile.destination); + } + tile.col++; + int maxCols = getColumnCount(); + + if (tile.col >= maxCols) { + tile.col = 0; + tile.row++; + if (DEBUG_DRAG) { + Log.w(TAG, "reached max column count, shifting to next row: " + tile.row); + } + } + + // clamp this value to the max count we want. + int maxRows = Math.min(MAX_ROW_COUNT - 1 /* we are 0 based */, getCurrentMaxRow()); + + if (tile.row > maxRows) { + tile.destinationPage = tile.destinationPage + 1; + tile.row = 0; + tile.col = 0; + + if (DEBUG_DRAG) { + Log.w(TAG, "tile's destination page moved to: " + tile.destinationPage); + } + } + int columnCount = Math.max(1, getColumnCount(tile.destinationPage, tile.row, false)); + if (columnCount < maxCols) { + // if columncount gives us 1 and we're at col 2 + columnCount = Math.max((tile.col + 1), columnCount); + } + if (DEBUG_DRAG) { + Log.w(TAG, "columCount at: " + columnCount); + } + + boolean firstRowLarge = mFirstRowLarge && tile.row == 0 && tile.destinationPage == 0; + + tile.destination.x = getLeft(tile.row, tile.col, columnCount, firstRowLarge); + tile.destination.y = getRowTop(tile.row); + + if (DEBUG_DRAG) { + Log.i(TAG, "---setToNextDestination() called with " + "tile = [" + tile + "], now at: " + + tile.destination); + } + } + + private boolean isBefore(DragTileRecord r1, DragTileRecord r2) { + if (DEBUG_DRAG) { + Log.d(TAG, "isBefore() called with " + "r1 = [" + r1 + "], r2 = [" + r2 + "]"); + } + boolean isBefore = r1.destinationPage <= r2.destinationPage; + if (r1.destinationPage == r2.destinationPage) { + isBefore = r1.row <= r2.row; + if (r1.row == r2.row) { + isBefore = r1.col <= r2.col; + } + } + + if (DEBUG_DRAG) { + Log.d(TAG, "r1 isBefore r2: " + isBefore); + } + return isBefore; + } + + private void setToLastDestination(DragTileRecord record) { + DragTileRecord last = (DragTileRecord) mRecords.get(mRecords.size() - 1); + if (DEBUG_DRAG) { + Log.d(TAG, "setToLastDestination() called with record = [" + + record + "], and last record is: " + last); + } + + if (isBefore(record, last)) { + // if the record is before the last record in the records list, set it to the + // last location, then spoof it one space forward + record.destinationPage = last.destinationPage; + record.row = last.row; + record.col = last.col; + record.destination.x = last.destination.x; + record.destination.y = last.destination.y; + setToNextDestination(record); + } + } + + @Override + public boolean onLongClick(View v) { + final DragTileRecord record = (DragTileRecord) getRecord(v); + if (record == null) { + // TODO couldn't find a matching tag? + Log.e(TAG, "got a null record on touch down."); + return false; + } + + mDraggingRecord = record; + + mDraggingRecord.tileView.setAlpha(0); + mDraggingRecord.tileView.setDual(false, false); + TileShadow mTileShadow = new TileShadow(mDraggingRecord.tileView); + + v.startDrag(null, mTileShadow, null, 0); + + mViewPager.requestDisallowInterceptTouchEvent(true); + + onStartDrag(); + mDragging = true; + return true; + } + + private void shiftTiles(DragTileRecord startingTile, boolean forward) { + if (DEBUG_DRAG) { + Log.i(TAG, "shiftTiles() called with " + "startingTile = [" + startingTile + + "], forward = [" + forward + "]"); + } + + if (forward) { + // startingTile and all after will need to be shifted one to the right + // dragging tile needs room + + final int destP = startingTile.destinationPage; + final int rowF = startingTile.row; + final int colF = startingTile.col; + PointF loc = new PointF(startingTile.destination.x, startingTile.destination.y); + + // the index of the original position of the statingTile before it moved + int startingIndex = mRecords.indexOf(startingTile); + mLastRightShift = startingIndex; + mLastLeftShift = -1; + + shiftAllTilesRight(startingIndex); + mRecords.add(startingIndex, mDraggingRecord); + + mPagerAdapter.notifyDataSetChanged(); + + mDraggingRecord.col = colF; + mDraggingRecord.row = rowF; + mDraggingRecord.destination = loc; + mDraggingRecord.destinationPage = destP; + + mDraggingRecord.tileView.setX(mDraggingRecord.destination.x); + mDraggingRecord.tileView.setY(mDraggingRecord.destination.y); + + } else { + // it is also probably the dragging tile + final int startingIndex = mRecords.indexOf(startingTile); + mLastLeftShift = startingIndex; + mLastRightShift = -1; + + final int draggingIndex = mRecords.indexOf(mDraggingRecord); + + if (startingIndex != draggingIndex) { + if (DEBUG_DRAG) { + Log.e(TAG, "startinIndex: " + startingIndex + ", draggingIndex: " + + draggingIndex + ", and they differ!!!!"); + } + } + + // startingTile should be the "empty" tile that things should start shifting into + shiftAllTilesLeft(startingIndex); + + // remove the dragging record + if (mRecords.remove(mDraggingRecord)) { + mPagerAdapter.notifyDataSetChanged(); + if (DEBUG_DRAG) { + Log.v(TAG, "removed dragging record after moving tiles back"); + } + } + + // set coords off screen until we're ready to place it + mDraggingRecord.tileView.setX(-mDraggingRecord.tileView.getMeasuredWidth()); + mDraggingRecord.tileView.setY(-mDraggingRecord.tileView.getMeasuredHeight()); + } + + mViewPager.getAdapter().notifyDataSetChanged(); + } + + private void shiftAllTilesRight(int startingIndex) { + int desiredColumnCount = -1; + for (int j = startingIndex; j < mRecords.size() - 1; j++) { + final DragTileRecord ti = (DragTileRecord) mRecords.get(j); + final DragTileRecord tnext = (DragTileRecord) mRecords.get(j + 1); + + mCurrentlyAnimating.add(ti); + if (tnext.row != ti.row || desiredColumnCount == -1) { + desiredColumnCount = getColumnCount(tnext.destinationPage, tnext.row); + //Log.w(TAG, "updated desiredColumnCount: " + desiredColumnCount); + } + + if (DEBUG_DRAG) { + Log.v(TAG, "moving " + ti + " to page " + tnext.destinationPage + ", at coords: " + + tnext.row + ", col: " + tnext.col + ", dest: " + tnext.destination); + } + + ti.row = tnext.row; + ti.col = tnext.col; + ti.destination.x = getLeft(tnext.destinationPage, ti.row, ti.col, desiredColumnCount); + ti.destination.y = getRowTop(ti.row); + + if (ti.destinationPage != tnext.destinationPage) { + ti.destinationPage = tnext.destinationPage; + + final QSPage tilePageSource = getPage(ti.page); + final QSPage tilePageTarget = getPage(ti.destinationPage); + tilePageSource.removeView(ti.tileView); + + tilePageSource.addTransientView(ti.tileView, 0); + ti.tileView.animate() + .withLayer() + .x(ti.destination.x + getWidth()) + .y(ti.destination.y) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + tilePageSource.removeTransientView(ti.tileView); + mCurrentlyAnimating.remove(ti); + } + + @Override + public void onAnimationEnd(Animator animation) { + tilePageSource.removeTransientView(ti.tileView); + tilePageTarget.addView(ti.tileView); + ti.page = tilePageTarget.getPageIndex(); + ti.tileView.setX(ti.destination.x); + ti.tileView.setY(ti.destination.y); + + mCurrentlyAnimating.remove(ti); + requestLayout(); + } + }); + + } else { + ti.tileView.animate() + .withLayer() + .x(ti.destination.x) + .y(ti.destination.y) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + mCurrentlyAnimating.remove(ti); + } + + @Override + public void onAnimationEnd(Animator animation) { + mCurrentlyAnimating.remove(ti); + final boolean dual = getPage(ti.destinationPage).dualRecord(ti); + if (ti.tileView.setDual(dual, ti.tile.hasDualTargetsDetails())) { + if (DEBUG_DRAG) { + Log.w(TAG, ti + " changed dual state to : " + + ti.tileView.isDual()); + } + } + requestLayout(); + } + }); + } + } + + // need to do last tile manually + final DragTileRecord last = (DragTileRecord) mRecords.get(mRecords.size() - 1); + mCurrentlyAnimating.add(last); + + if (DEBUG_DRAG) { + Log.i(TAG, "last tile shifting to the right: " + last); + } + setToNextDestination(last); + if (last.page != last.destinationPage) { + final QSPage tilePageSource = getPage(last.page); + final QSPage tilePageTarget = getPage(last.destinationPage); + tilePageSource.removeView(last.tileView); + tilePageSource.addTransientView(last.tileView, 0); + + last.tileView.animate() + .withLayer() + .x(last.destination.x + getWidth()) + .y(last.destination.y) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + tilePageSource.removeTransientView(last.tileView); + mCurrentlyAnimating.remove(last); + } + + @Override + public void onAnimationEnd(Animator animation) { + tilePageSource.removeTransientView(last.tileView); + tilePageTarget.addView(last.tileView); + last.page = tilePageTarget.getPageIndex(); + last.tileView.setX(last.destination.x); + last.tileView.setY(last.destination.y); + + if (DEBUG_DRAG) { + Log.i(TAG, "page shift finished: " + last); + } + + mCurrentlyAnimating.remove(last); + requestLayout(); + } + }); + } else { + last.tileView.animate() + .withLayer() + .x(last.destination.x) + .y(last.destination.y) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + mCurrentlyAnimating.remove(last); + } + + @Override + public void onAnimationEnd(Animator animation) { + if (DEBUG_DRAG) { + Log.i(TAG, "shift finished: " + last); + } + + mCurrentlyAnimating.remove(last); + requestLayout(); + } + }); + } + } + + private void shiftAllTilesLeft(int startingIndex) { + DragTileRecord startingTile = (DragTileRecord) mRecords.get(startingIndex); + + final PointF lastLocation = new PointF(startingTile.destination.x, + startingTile.destination.y); + PointF reallyTempLoc = new PointF(); + int lastRow = startingTile.row, lastCol = startingTile.col, tempRow, + tempCol, lastPage = startingTile.destinationPage, tempPage; + + int desiredColCount = getColumnCount(startingTile.destinationPage, startingTile.row); + for (int j = startingIndex + 1; j < mRecords.size(); j++) { + final DragTileRecord ti = (DragTileRecord) mRecords.get(j); + + mCurrentlyAnimating.add(ti); + + if (DEBUG_DRAG) { + Log.v(TAG, "moving " + ti + " to " + lastPage + ", at coords: " + + lastRow + ", col: " + lastCol); + Log.i(TAG, "and will have desiredColCount: " + desiredColCount); + } + + final int columnCountF = desiredColCount; + + if (ti.row != lastRow) { + desiredColCount = getColumnCount(ti.destinationPage, ti.row); + if (DEBUG_DRAG) { + Log.e(TAG, "updating desired colum count to: " + desiredColCount); + } + } + + // save current tile's loc + reallyTempLoc.x = ti.destination.x; + reallyTempLoc.y = ti.destination.y; + + tempRow = ti.row; + tempCol = ti.col; + tempPage = ti.destinationPage; + + ti.row = lastRow; + ti.col = lastCol; + + ti.destination.x = getLeft(lastRow, lastCol, columnCountF, + lastPage == 0 && lastRow == 0 && mFirstRowLarge); + ti.destination.y = getRowTop(lastRow); + + final boolean dual = getPage(ti.destinationPage).dualRecord(ti); + + if (ti.destinationPage != lastPage) { + ti.destinationPage = lastPage; + + ti.tileView.setX(reallyTempLoc.x + getWidth()); + ti.tileView.setY(reallyTempLoc.y); + + final QSPage originalPage = getPage(ti.page); + final QSPage page = getPage(lastPage); + + originalPage.removeView(ti.tileView); + + ti.tileView.animate() + .withLayer() + .x(ti.destination.x) + .y(ti.destination.y) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + page.addTransientView(ti.tileView, 0); + } + + @Override + public void onAnimationCancel(Animator animation) { + page.removeTransientView(ti.tileView); + mCurrentlyAnimating.remove(ti); + } + + @Override + public void onAnimationEnd(Animator animation) { + page.removeTransientView(ti.tileView); + page.addView(ti.tileView); + ti.page = page.getPageIndex(); + + mCurrentlyAnimating.remove(ti); + requestLayout(); + } + }); + } else { + ti.tileView.animate() + .withLayer() + .x(ti.destination.x) + .y(ti.destination.y) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCurrentlyAnimating.remove(ti); + if (ti.tileView.setDual(dual, ti.tile.hasDualTargetsDetails())) { + if (DEBUG_DRAG) { + Log.w(TAG, ti + " changed dual state to : " + + ti.tileView.isDual()); + } + } + requestLayout(); + } + }); + } + + // update previous location + lastLocation.x = reallyTempLoc.x; + lastLocation.y = reallyTempLoc.y; + + lastRow = tempRow; + lastCol = tempCol; + lastPage = tempPage; + } + } + + @Override + protected void handleShowDetailImpl(Record r, boolean show, int x, int y) { + super.handleShowDetailImpl(r, show, x, y); + if (show) { + final StatusBarPanelCustomTile customTile = r.detailAdapter.getCustomTile(); + mDetailRemoveButton.setVisibility(customTile != null && + !(customTile.getPackage().equals(mContext.getPackageName()) + || customTile.getUid() == android.os.Process.SYSTEM_UID) + ? VISIBLE : GONE); + mDetailRemoveButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mHost.removeCustomTile(customTile); + closeDetail(); + } + }); + } + mPanelView.setDetailRequestedScrollLock(mExpanded && show + && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + FontSizeUtils.updateFontSize(mDetailRemoveButton, R.dimen.qs_detail_button_text_size); + mPanelView.setDetailRequestedScrollLock(mExpanded && isShowingDetail() + && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE); + } + + @Override + public void setExpanded(boolean expanded) { + super.setExpanded(expanded); + // reset the page when inactive for a while + if (expanded) { + removeCallbacks(mResetPage); + } else { + postDelayed(mResetPage, PAGE_RESET_DELAY); + } + if (!expanded) { + if (mEditing) { + mHost.setEditing(false); + } + } + } + + public void updateResources() { + final Resources res = mContext.getResources(); + final int columns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns)); + mCellHeight = res.getDimensionPixelSize(R.dimen.qs_tile_height); + mCellWidth = (int) (mCellHeight * TILE_ASPECT); + mLargeCellHeight = res.getDimensionPixelSize(R.dimen.qs_dual_tile_height); + mLargeCellWidth = (int) (mLargeCellHeight * TILE_ASPECT); + mPanelPaddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); + mDualTileUnderlap = res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical); + mBrightnessPaddingTop = res.getDimensionPixelSize(R.dimen.qs_brightness_padding_top); + mPageIndicatorHeight = res.getDimensionPixelSize(R.dimen.qs_panel_page_indicator_height); + if (mColumns != columns) { + mColumns = columns; + if (isLaidOut()) postInvalidate(); + } + if (isLaidOut()) { + for (TileRecord r : mRecords) { + r.tile.clearState(); + } + updateDetailText(); + mQsPanelTop.updateResources(); + if (mListening) { + refreshAllTiles(); + } + } + } + + public boolean isAnimating(TileRecord t) { + return mCurrentlyAnimating.contains(t); + } + + public void cleanup() { + if (mSettingsObserver != null) { + mSettingsObserver.unobserve(); + } + } + + public void setPanelView(NotificationPanelView panelView) { + this.mPanelView = panelView; + } + + public static class TilesListAdapter extends BaseExpandableListAdapter + implements QSTile.DetailAdapter { + + public static final String PACKAGE_ANDROID = "android"; + + Context mContext; + QSTileHost mHost; + QSDragPanel mPanel; + + ArrayMap<String, List<String>> mPackageTileMap = new ArrayMap<>(); + + public TilesListAdapter(Context context, QSDragPanel panel) { + mContext = context; + mHost = panel.getHost(); + mPanel = panel; + + List<String> currentTileSpec = mHost.getTileSpecs(); + final Collection<String> tiles = QSUtils.getAvailableTiles(mContext); + tiles.removeAll(currentTileSpec); + + // we'll always have a system tiles category + mPackageTileMap.put(PACKAGE_ANDROID, new ArrayList<String>()); + + final Iterator<String> i = tiles.iterator(); + while (i.hasNext()) { + final String spec = i.next(); + if (QSUtils.isStaticQsTile(spec) + || QSUtils.isDynamicQsTile(extractTileTagFromSpec(spec))) { + List<String> packageList = mPackageTileMap.get(PACKAGE_ANDROID); + packageList.add(spec); + } else { + String tilePackage = getCustomTilePackage(spec); + List<String> packageList = mPackageTileMap.get(tilePackage); + if (packageList == null) { + mPackageTileMap.put(tilePackage, packageList = new ArrayList<>()); + } + packageList.add(spec); + } + } + + final Map<String, ?> stringMap = CustomQSTile.getCustomQSTilePrefs(mContext).getAll(); + for (Map.Entry<String, ?> entry : stringMap.entrySet()) { + if (entry.getValue() instanceof Boolean) { + if ((Boolean)entry.getValue()) { + final String key = entry.getKey(); + if (QSUtils.isDynamicQsTile(extractTileTagFromSpec(key))) { + mPackageTileMap.get(PACKAGE_ANDROID).add(key); + } else { + final String customTilePackage = getCustomTilePackage(key); + List<String> packageList = mPackageTileMap.get(customTilePackage); + if (packageList == null) { + mPackageTileMap.put(customTilePackage, + packageList = new ArrayList<>()); + } + packageList.add(key); + + } + } + } + }; + + final List<String> systemTiles = mPackageTileMap.get(PACKAGE_ANDROID); + Collections.sort(systemTiles); + } + + private String getCustomTilePackage(String spec) { + if (mHost.getCustomTileData().get(spec) != null) { + StatusBarPanelCustomTile sbc = mHost.getCustomTileData().get(spec).sbc; + return sbc.getPackage(); + } else { + return extractPackageFromCustomTileSpec(spec); + } + } + + private static String extractPackageFromCustomTileSpec(String spec) { + if (spec != null && !spec.isEmpty()) { + final String[] split = spec.split("\\|"); + if (split != null && split.length > 2) { + return split[1]; + } + return spec; + } + return null; + } + + private static String extractTileTagFromSpec(String spec) { + if (spec != null && !spec.isEmpty()) { + final String[] split = spec.split("\\|"); + if (split != null && split.length == 5) { + /** for {@link cyanogenmod.app.StatusBarPanelCustomTile#key() **/ + return split[3]; + } else if (split != null && split.length == 3) { + /** for {@link cyanogenmod.app.StatusBarPanelCustomTile#persistableKey()} **/ + return split[2]; + } + return spec; + } + return null; + } + + private Drawable getQSTileIcon(String spec) { + if (QSUtils.isDynamicQsTile(extractTileTagFromSpec(spec))) { + return QSTile.ResourceIcon.get(QSUtils.getDynamicQSTileResIconId(mContext, + UserHandle.myUserId(), extractTileTagFromSpec(spec))).getDrawable(mContext); + } else if (QSUtils.isStaticQsTile(spec)) { + final int res = QSTileHost.getIconResource(spec); + if (res != 0) { + return QSTile.ResourceIcon.get(res).getDrawable(mContext); + } else { + return mContext.getPackageManager().getDefaultActivityIcon(); + } + } else { + QSTile<?> tile = mHost.getTile(spec); + if (tile != null) { + QSTile.State state = tile.getState(); + if (state != null && state.icon != null) { + return state.icon.getDrawable(mContext); + } + } + return getPackageDrawable(getCustomTilePackage(spec)); + } + } + + private String getPackageLabel(String packageName) { + try { + return mContext.getPackageManager().getApplicationLabel( + mContext.getPackageManager().getApplicationInfo(packageName, 0)).toString(); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return null; + } + + private Drawable getPackageDrawable(String packageName) { + try { + return mContext.getPackageManager().getApplicationIcon(packageName); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return null; + } + + private String getQSTileLabel(String spec) { + if (QSUtils.isStaticQsTile(spec)) { + int resource = QSTileHost.getLabelResource(spec); + if (resource != 0) { + return mContext.getText(resource).toString(); + } else { + return spec; + } + } else if (QSUtils.isDynamicQsTile(extractTileTagFromSpec(spec))) { + return QSUtils.getDynamicQSTileLabel(mContext, + UserHandle.myUserId(), extractTileTagFromSpec(spec)); + } else { + return getPackageLabel(getCustomTilePackage(spec)); + } + } + + @Override + public int getGroupCount() { + return mPackageTileMap.keySet().size(); + } + + @Override + public int getChildrenCount(int groupPosition) { + return mPackageTileMap.valueAt(groupPosition).size(); + } + + @Override + public String getGroup(int groupPosition) { + return mPackageTileMap.keyAt(groupPosition); + } + + @Override + public String getChild(int groupPosition, int childPosition) { + return mPackageTileMap.valueAt(groupPosition).get(childPosition); + } + + @Override + public long getGroupId(int groupPosition) { + return groupPosition; + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return mPackageTileMap.valueAt(groupPosition).get(childPosition).hashCode(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { + LinearLayout row = (LinearLayout) convertView; + if (row == null) { + row = (LinearLayout) LayoutInflater.from(mContext) + .inflate(R.layout.qs_tile_category_row, parent, false); + } + TextView title = (TextView) row.findViewById(android.R.id.title); + + ImageView systemOrAppIcon = (ImageView) row.findViewById(android.R.id.icon); + ImageView expansionIndicator = (ImageView) row.findViewById(android.R.id.icon2); + + expansionIndicator.setImageResource(isExpanded ? R.drawable.ic_qs_tile_contract + : R.drawable.ic_qs_tile_expand); + // hide indicator when there's only 1 group + final boolean singleGroupMode = getGroupCount() == 1; + expansionIndicator.setVisibility(singleGroupMode ? View.GONE : View.VISIBLE); + + String group = getGroup(groupPosition); + if (group.equals(PACKAGE_ANDROID)) { + group = mContext.getText(R.string.quick_settings_tiles_category_system).toString(); + // special icon + systemOrAppIcon.setImageResource(R.drawable.ic_qs_tile_category_system); + } else { + group = getPackageLabel(group); + systemOrAppIcon.setImageResource(R.drawable.ic_qs_tile_category_other); + } + title.setText(group); + + if (isExpanded) { + expansionIndicator.setColorFilter( + mContext.getColor( + R.color.qs_detailed_expansion_indicator_color), PorterDuff.Mode.SRC_ATOP); + systemOrAppIcon.setColorFilter( + mContext.getColor(R.color.qs_detailed_icon_tint_color), PorterDuff.Mode.SRC_ATOP); + title.setTextColor(mContext.getColor(R.color.qs_detailed_title_text_color)); + } else { + title.setTextColor(mContext.getColor(R.color.qs_detailed_default_text_color)); + systemOrAppIcon.setColorFilter(null); + expansionIndicator.setColorFilter(null); + } + return row; + } + + @Override + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { + LinearLayout child = (LinearLayout) convertView; + if (child == null) { + child = (LinearLayout) LayoutInflater.from(mContext) + .inflate(R.layout.qs_tile_child_row, parent, false); + } + String spec = getChild(groupPosition, childPosition); + + TextView title = (TextView) child.findViewById(android.R.id.title); + title.setText(getQSTileLabel(spec)); + + ImageView icon = (ImageView) child.findViewById(android.R.id.icon); + icon.setImageDrawable(getQSTileIcon(spec)); + + return child; + } + + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + @Override + public int getTitle() { + return R.string.quick_settings_tiles_add_tiles; + } + + @Override + public Boolean getToggleState() { + return null; + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + ExpandableListView lv = (ExpandableListView) convertView; + if (lv == null) { + lv = new ExpandableListView(parent.getContext()); + lv.setOnTouchListener(new OnTouchListener() { + // Setting on Touch Listener for handling the touch inside ScrollView + @Override + public boolean onTouch(View v, MotionEvent event) { + // Disallow the touch request for parent scroll on touch of child view + v.getParent().requestDisallowInterceptTouchEvent(true); + return false; + } + }); + } + lv.setAdapter(this); + lv.expandGroup(mPackageTileMap.indexOfKey(PACKAGE_ANDROID)); + lv.setGroupIndicator(null); + lv.setChildIndicator(null); + lv.setChildDivider(new ColorDrawable(Color.TRANSPARENT)); + lv.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { + @Override + public boolean onChildClick(ExpandableListView parent, View v, + int groupPosition, int childPosition, long id) { + String spec = getChild(groupPosition, childPosition); + + final QSTile<?> tile = mHost.getTile(spec); + if (tile != null && tile instanceof CustomQSTile) { + // already present + ((CustomQSTile) tile).setUserRemoved(false); + mPanel.refreshAllTiles(); + } else { + // reset its state just in case it's not published + CustomQSTile.getCustomQSTilePrefs(mContext) + .edit() + .remove(spec) + .apply(); + mPanel.add(spec); + // TODO notify user the app isn't publishing the tile, but it now can be! + } + mPanel.closeDetail(); + return true; + } + }); + lv.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() { + @Override + public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, + long id) { + if (getGroupCount() == 1) { + // disable contracting/expanding group when there's only 1 + return true; + } + return false; + } + }); + return lv; + } + + @Override + public Intent getSettingsIntent() { + return null; + } + + @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override + public void setToggleState(boolean state) { + + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.DONT_LOG; + } + + 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; + } + } + + public void add(String tile) { + MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_ADD, tile); + List<String> tiles = new ArrayList<>(mHost.getTileSpecs()); + tiles.add(tile); + mHost.setTiles(tiles); + } + + public boolean isDragging() { + return mDragging; + } + + public boolean isDragRecordAttached() { + return mRecords.indexOf(mDraggingRecord) >= 0; + } + + public boolean isOnSettingsPage() { + return mEditing && mViewPager.getCurrentItem() == 0; + } + + public void goToSettingsPage() { + if (mEditing) { + mViewPager.setCurrentItem(0, true); + } + } + + class SettingsObserver extends UserContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + @Override + protected void observe() { + super.observe(); + + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(CMSettings.Secure.getUriFor( + CMSettings.Secure.QS_USE_MAIN_TILES), false, this, UserHandle.USER_ALL); + update(); + } + + @Override + protected void unobserve() { + super.unobserve(); + + ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(this); + } + + @Override + public void update() { + ContentResolver resolver = mContext.getContentResolver(); + int currentUserId = ActivityManager.getCurrentUser(); + boolean firstRowLarge = CMSettings.Secure.getIntForUser(resolver, + CMSettings.Secure.QS_USE_MAIN_TILES, 1, currentUserId) == 1; + if (firstRowLarge != mFirstRowLarge) { + mFirstRowLarge = firstRowLarge; + setTiles(mHost.getTiles()); + mPagerAdapter.notifyDataSetChanged(); + } + } + } + + public static final class DragTileRecord extends TileRecord { + public int page; + public int destinationPage; + public PointF destination = new PointF(); + + @Override + public String toString() { + String label = tile instanceof QsTuner.DraggableTile ? tile.toString() : + tile.getClass().getSimpleName(); + + String p = "at page: " + page; + if (destinationPage != page) { + p += "{-> " + destinationPage + "} "; + } + + return "[" + label + ", coords: (" + row + ", " + col + ") " + p + "]"; + } + } + + private static class TileShadow extends View.DragShadowBuilder { + + public TileShadow(View view) { + super(view); + Drawable shadow = view.getContext().getDrawable(R.drawable.qs_tile_background_drag); + view.setBackground(shadow); + } + + @Override + public void onDrawShadow(Canvas canvas) { + super.onDrawShadow(canvas); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPage.java b/packages/SystemUI/src/com/android/systemui/qs/QSPage.java new file mode 100644 index 0000000..7871a62 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPage.java @@ -0,0 +1,161 @@ +package com.android.systemui.qs; + +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import com.android.systemui.R; + +public class QSPage extends ViewGroup { + + private static final String TAG = "QSPage"; + + static final float TILE_ASPECT = 1.2f; + + private int mCellWidth; + private int mCellHeight; + private int mLargeCellWidth; + private int mLargeCellHeight; + private int mGridHeight; + + private QSDragPanel mPanel; + + private int mPage; + + private boolean mAdapterEditingState; + + public QSPage(Context context, QSDragPanel panel, int page) { + super(context); + mPanel = panel; + mPage = page; + updateResources(); + setClipChildren(false); + setClipToPadding(false); + setClipToOutline(false); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + public int getPageIndex() { + return mPage; + } + + public void updateResources() { + final Resources res = mContext.getResources(); + final int columns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns)); + mCellHeight = res.getDimensionPixelSize(R.dimen.qs_tile_height); + mCellWidth = (int)(mCellHeight * TILE_ASPECT); + mLargeCellHeight = res.getDimensionPixelSize(R.dimen.qs_dual_tile_height); + mLargeCellWidth = (int)(mLargeCellHeight * TILE_ASPECT); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + if (mPanel.mCurrentlyAnimating.isEmpty() && !mPanel.isDragging()) { + int r = -1; + int c = -1; + int rows = 0; + for (QSPanel.TileRecord ts : mPanel.mRecords) { + QSDragPanel.DragTileRecord record = (QSDragPanel.DragTileRecord) ts; + if (record.page != mPage) continue; + if (record.tileView.getVisibility() == GONE) continue; + + if (mPage == 0 && r == 0 && c == 1 && mPanel.mFirstRowLarge) { + r = 1; + c = 0; + } else if (r == -1 || c == (mPanel.getColumnCount() - 1)) { + r++; + c = 0; + } else { + c++; + } + record.row = r; + record.col = c; + rows = r + 1; + } + mGridHeight = mPanel.getRowTop(rows); + } + + View previousView = mPanel.getBrightnessView(); + for (QSPanel.TileRecord ts : mPanel.mRecords) { + QSDragPanel.DragTileRecord record = (QSDragPanel.DragTileRecord) ts; + if (record.page != mPage) continue; + if (record.page != record.destinationPage) continue; + + final boolean dual = dualRecord(record); + if (record.tileView.setDual(dual, record.tile.hasDualTargetsDetails())) { + record.tileView.handleStateChanged(record.tile.getState()); + } + if (record.tileView.getVisibility() == GONE) continue; + final int cw = dual ? mLargeCellWidth : mCellWidth; + final int ch = dual ? mLargeCellHeight : mCellHeight; + record.tileView.measure(exactly(cw), exactly(ch)); + previousView = record.tileView.updateAccessibilityOrder(previousView); + } + setMeasuredDimension(width, mGridHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int w = getWidth(); + boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; + for (QSPanel.TileRecord ts : mPanel.mRecords) { + QSDragPanel.DragTileRecord record = (QSDragPanel.DragTileRecord) ts; + if (record.page != mPage) continue; + if (record.page != record.destinationPage) continue; + if (record.tileView.getVisibility() == GONE) continue; + + final int cols = mPanel.getColumnCount(mPage, record.row); + + int left = mPanel.getLeft(record.row, record.col, cols, dualRecord(record)); + final int top = mPanel.getRowTop(record.row); + int right; + int tileWith = record.tileView.getMeasuredWidth(); + if (isRtl) { + right = w - left; + left = right - tileWith; + } else { + right = left + tileWith; + } + if (mPanel.isAnimating(record)) { + record.tileView.layout(record.tileView.getLeft(), record.tileView.getTop(), + record.tileView.getRight(), record.tileView.getBottom()); + continue; + } + if (false) { + Log.v(TAG + "-" + mPage, "laying out " + record + ", top: " + top + ", left: " + left); + Log.d(TAG, record + " wiping translations: " + + record.tileView.getTranslationX() + + ", " + record.tileView.getTranslationY()); + } + record.tileView.setTranslationX(0); + record.tileView.setTranslationY(0); + + record.destination.x = record.tileView.getX(); + record.destination.y = record.tileView.getY(); + + record.tileView.layout(left, top, right, top + record.tileView.getMeasuredHeight()); + } + } + + public boolean getAdapterEditingState() { + return mAdapterEditingState; + } + + public void setAdapterEditingState(boolean editing) { + this.mAdapterEditingState = editing; + } + + public boolean dualRecord(QSPanel.TileRecord record) { + return mPanel.mFirstRowLarge && record.row == 0 && mPage == 0; + } + + private static int exactly(int size) { + return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 94d5170..77ede93 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -25,6 +25,8 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.os.Handler; import android.os.Message; +import android.os.UserHandle; +import android.provider.Settings; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -45,40 +47,42 @@ import com.android.systemui.statusbar.policy.BrightnessMirrorController; import java.util.ArrayList; import java.util.Collection; +import cyanogenmod.app.StatusBarPanelCustomTile; +import cyanogenmod.providers.CMSettings; + /** View that represents the quick settings tile panel. **/ public class QSPanel extends ViewGroup { - private static final float TILE_ASPECT = 1.2f; - - private final Context mContext; - protected final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>(); - private final View mDetail; - private final ViewGroup mDetailContent; - private final TextView mDetailSettingsButton; - private final TextView mDetailDoneButton; - protected final View mBrightnessView; - private final QSDetailClipper mClipper; + protected static final float TILE_ASPECT = 1.2f; + + protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); + protected View mDetail; + protected ViewGroup mDetailContent; + protected TextView mDetailSettingsButton; + protected TextView mDetailDoneButton; + protected View mBrightnessView; + protected QSDetailClipper mClipper; private final H mHandler = new H(); - private int mColumns; - private int mCellWidth; - private int mCellHeight; - private int mLargeCellWidth; - private int mLargeCellHeight; - private int mPanelPaddingBottom; - private int mDualTileUnderlap; - private int mBrightnessPaddingTop; - private int mGridHeight; - private boolean mExpanded; - private boolean mListening; + protected int mColumns; + protected int mCellWidth; + protected int mCellHeight; + protected int mLargeCellWidth; + protected int mLargeCellHeight; + protected int mPanelPaddingBottom; + protected int mDualTileUnderlap; + protected int mBrightnessPaddingTop; + protected int mGridHeight; + protected boolean mExpanded; + protected boolean mListening; private boolean mClosingDetail; - private Record mDetailRecord; + protected Record mDetailRecord; private Callback mCallback; - private BrightnessController mBrightnessController; - private QSTileHost mHost; + protected BrightnessController mBrightnessController; + protected QSTileHost mHost; - private QSFooter mFooter; - private boolean mGridContentVisible = true; + protected QSFooter mFooter; + protected boolean mGridContentVisible = true; public QSPanel(Context context) { this(context, null); @@ -87,17 +91,23 @@ public class QSPanel extends ViewGroup { public QSPanel(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; + setupViews(); + } - mDetail = LayoutInflater.from(context).inflate(R.layout.qs_detail, this, false); + /** + * THIS IS OVERRIDDEN in QSDragPanel + */ + protected void setupViews() { + mDetail = LayoutInflater.from(mContext).inflate(R.layout.qs_detail, this, false); mDetailContent = (ViewGroup) mDetail.findViewById(android.R.id.content); mDetailSettingsButton = (TextView) mDetail.findViewById(android.R.id.button2); mDetailDoneButton = (TextView) mDetail.findViewById(android.R.id.button1); updateDetailText(); mDetail.setVisibility(GONE); mDetail.setClickable(true); - mBrightnessView = LayoutInflater.from(context).inflate( + mBrightnessView = LayoutInflater.from(mContext).inflate( R.layout.quick_settings_brightness_dialog, this, false); - mFooter = new QSFooter(this, context); + mFooter = new QSFooter(this, mContext); addView(mDetail); addView(mBrightnessView); addView(mFooter.getView()); @@ -118,7 +128,26 @@ public class QSPanel extends ViewGroup { }); } - private void updateDetailText() { + /** + * Enable/disable brightness slider. + */ + protected boolean showBrightnessSlider() { + boolean brightnessSliderEnabled = CMSettings.System.getIntForUser( + mContext.getContentResolver(), CMSettings.System.QS_SHOW_BRIGHTNESS_SLIDER, + 1, UserHandle.USER_CURRENT) == 1; + ToggleSlider brightnessSlider = (ToggleSlider) findViewById(R.id.brightness_slider); + if (brightnessSliderEnabled) { + mBrightnessView.setVisibility(VISIBLE); + brightnessSlider.setVisibility(VISIBLE); + } else { + mBrightnessView.setVisibility(GONE); + brightnessSlider.setVisibility(GONE); + } + updateResources(); + return brightnessSliderEnabled; + } + + protected void updateDetailText() { mDetailDoneButton.setText(R.string.quick_settings_done); mDetailSettingsButton.setText(R.string.quick_settings_more_settings); } @@ -206,7 +235,7 @@ public class QSPanel extends ViewGroup { if (mListening) { refreshAllTiles(); } - if (listening) { + if (listening && showBrightnessSlider()) { mBrightnessController.registerCallbacks(); } else { mBrightnessController.unregisterCallbacks(); @@ -236,11 +265,11 @@ public class QSPanel extends ViewGroup { showDetail(show, r); } - private void showDetail(boolean show, Record r) { + protected void showDetail(boolean show, Record r) { mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget(); } - private void setTileVisibility(View v, int visibility) { + protected void setTileVisibility(View v, int visibility) { mHandler.obtainMessage(H.SET_TILE_VISIBILITY, visibility, 0, v).sendToTarget(); } @@ -252,6 +281,15 @@ public class QSPanel extends ViewGroup { v.setVisibility(visibility); } + protected void setTileEnabled(View v, boolean enabled) { + mHandler.obtainMessage(H.SET_TILE_ENABLED, enabled ? 1 : 0, 0, v).sendToTarget(); + } + + private void handleSetTileEnabled(View v, boolean enabled) { + if (enabled == v.isEnabled()) return; + v.setEnabled(enabled); + } + public void setTiles(Collection<QSTile<?>> tiles) { for (TileRecord record : mRecords) { removeView(record.tileView); @@ -265,7 +303,7 @@ public class QSPanel extends ViewGroup { } } - private void drawTile(TileRecord r, QSTile.State state) { + protected void drawTile(TileRecord r, QSTile.State state) { final int visibility = state.visible ? VISIBLE : GONE; setTileVisibility(r.tileView, visibility); r.tileView.onStateChanged(state); @@ -365,7 +403,7 @@ public class QSPanel extends ViewGroup { } } - private void handleShowDetailTile(TileRecord r, boolean show) { + protected void handleShowDetailTile(TileRecord r, boolean show) { if ((mDetailRecord != null) == show && mDetailRecord == r) return; if (show) { @@ -378,7 +416,7 @@ public class QSPanel extends ViewGroup { handleShowDetailImpl(r, show, x, y); } - private void handleShowDetailImpl(Record r, boolean show, int x, int y) { + protected void handleShowDetailImpl(Record r, boolean show, int x, int y) { boolean visibleDiff = (mDetailRecord != null) != show; if (!visibleDiff && mDetailRecord == r) return; // already in right state DetailAdapter detailAdapter = null; @@ -425,7 +463,7 @@ public class QSPanel extends ViewGroup { } } - private void setGridContentVisibility(boolean visible) { + protected void setGridContentVisibility(boolean visible) { int newVis = visible ? VISIBLE : INVISIBLE; for (int i = 0; i < mRecords.size(); i++) { TileRecord tileRecord = mRecords.get(i); @@ -433,7 +471,7 @@ public class QSPanel extends ViewGroup { tileRecord.tileView.setVisibility(newVis); } } - mBrightnessView.setVisibility(newVis); + mBrightnessView.setVisibility(showBrightnessSlider() ? newVis : GONE); if (mGridContentVisible != visible) { MetricsLogger.visibility(mContext, MetricsLogger.QS_PANEL, newVis); } @@ -458,15 +496,12 @@ public class QSPanel extends ViewGroup { int r = -1; int c = -1; int rows = 0; - boolean rowIsDual = false; for (TileRecord record : mRecords) { if (record.tileView.getVisibility() == GONE) continue; // wrap to next column if we've reached the max # of columns - // also don't allow dual + single tiles on the same row - if (r == -1 || c == (mColumns - 1) || rowIsDual != record.tile.supportsDualTargets()) { + if (r == -1 || c == (mColumns - 1)) { r++; c = 0; - rowIsDual = record.tile.supportsDualTargets(); } else { c++; } @@ -477,7 +512,8 @@ public class QSPanel extends ViewGroup { View previousView = mBrightnessView; for (TileRecord record : mRecords) { - if (record.tileView.setDual(record.tile.supportsDualTargets())) { + final boolean dualTarget = record.tile.hasDualTargetsDetails(); + if (record.tileView.setDual(dualTarget, dualTarget)) { record.tileView.handleStateChanged(record.tile.getState()); } if (record.tileView.getVisibility() == GONE) continue; @@ -535,7 +571,7 @@ public class QSPanel extends ViewGroup { } } - private int getRowTop(int row) { + protected int getRowTop(int row) { if (row <= 0) return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop; return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop + mLargeCellHeight - mDualTileUnderlap + (row - 1) * mCellHeight; @@ -556,13 +592,13 @@ public class QSPanel extends ViewGroup { } } - private void fireToggleStateChanged(boolean state) { + protected void fireToggleStateChanged(boolean state) { if (mCallback != null) { mCallback.onToggleStateChanged(state); } } - private void fireScanStateChanged(boolean state) { + protected void fireScanStateChanged(boolean state) { if (mCallback != null) { mCallback.onScanStateChanged(state); } @@ -579,24 +615,27 @@ public class QSPanel extends ViewGroup { private class H extends Handler { private static final int SHOW_DETAIL = 1; private static final int SET_TILE_VISIBILITY = 2; + private static final int SET_TILE_ENABLED = 3; @Override public void handleMessage(Message msg) { if (msg.what == SHOW_DETAIL) { handleShowDetail((Record)msg.obj, msg.arg1 != 0); } else if (msg.what == SET_TILE_VISIBILITY) { handleSetTileVisibility((View)msg.obj, msg.arg1); + } else if (msg.what == SET_TILE_ENABLED) { + handleSetTileEnabled((View)msg.obj, msg.arg1 == 1); } } } - private static class Record { + protected static class Record { View detailView; DetailAdapter detailAdapter; int x; int y; } - protected static final class TileRecord extends Record { + protected static class TileRecord extends Record { public QSTile<?> tile; public QSTileView tileView; public int row; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelTopView.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelTopView.java new file mode 100644 index 0000000..b00483c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelTopView.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.qs; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.UserHandle; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.systemui.R; +import com.android.systemui.cm.UserContentObserver; +import com.android.systemui.settings.ToggleSlider; + +import cyanogenmod.providers.CMSettings; + +public class QSPanelTopView extends FrameLayout { + + private static final String TAG = "QSPanelTopView"; + + public static final int TOAST_DURATION = 2000; + + protected View mEditTileInstructionView; + protected View mDropTarget; + protected View mBrightnessView; + protected TextView mToastView; + protected View mAddTarget; + protected TextView mEditInstructionText; + + private boolean mEditing = false; + private boolean mDisplayingInstructions = false; + private boolean mDisplayingTrash = false; + private boolean mDisplayingToast = false; + public boolean mHasBrightnessSliderToDisplay = true; + + private AnimatorSet mAnimator; + private ImageView mDropTargetIcon; + + private SettingsObserver mSettingsObserver; + private boolean mListening; + private boolean mSkipAnimations; + + public QSPanelTopView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public QSPanelTopView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public QSPanelTopView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setFocusable(true); + mSettingsObserver = new SettingsObserver(new Handler()); + } + + @Override + public boolean hasOverlappingRendering() { + return mEditing; + } + + public View getDropTarget() { + return mDropTarget; + } + + public ImageView getDropTargetIcon() { + return mDropTargetIcon; + } + + public View getBrightnessView() { + return mBrightnessView; + } + + public View getAddTarget() { + return mAddTarget; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mDropTarget = findViewById(R.id.delete_container); + mDropTargetIcon = (ImageView) findViewById(R.id.delete_target); + mEditTileInstructionView = findViewById(R.id.edit_container); + mBrightnessView = findViewById(R.id.brightness_container); + mToastView = (TextView) findViewById(R.id.qs_toast); + mAddTarget = findViewById(R.id.add_target); + mEditInstructionText = (TextView) findViewById(R.id.edit_text_instruction); + updateResources(); + } + + public void updateResources() { + if (mEditInstructionText != null) { + mEditInstructionText.setText(R.string.qs_tile_edit_header_instruction); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + mBrightnessView.measure(QSDragPanel.exactly(width), MeasureSpec.UNSPECIFIED); + mEditTileInstructionView.measure(QSDragPanel.exactly(width), MeasureSpec.UNSPECIFIED); + mToastView.measure(QSDragPanel.exactly(width), MeasureSpec.UNSPECIFIED); + + // if we are showing a brightness slider, always fit to that, otherwise only + // declare a height when editing. + int dh = mHasBrightnessSliderToDisplay ? mBrightnessView.getMeasuredHeight() + : mEditing ? mEditTileInstructionView.getMeasuredHeight() : 0; + + mDropTarget.measure(QSDragPanel.exactly(width), QSDragPanel.atMost(dh)); + mEditTileInstructionView.measure(QSDragPanel.exactly(width), QSDragPanel.atMost(dh)); + mToastView.measure(QSDragPanel.exactly(width), QSDragPanel.atMost(dh)); + + setMeasuredDimension(width, dh); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + boolean animateToState = !isLaidOut(); + super.onLayout(changed, left, top, right, bottom); + if (animateToState) { + goToState(); + } + } + + public void setEditing(boolean editing, boolean skipAnim) { + mEditing = editing; + if (editing) { + mDisplayingInstructions = true; + mDisplayingTrash = false; + } else { + mDisplayingInstructions = false; + mDisplayingTrash = false; + } + if (skipAnim) { + goToState(); + } else { + animateToState(); + } + } + + public void onStopDrag() { + mDisplayingTrash = false; + animateToState(); + } + + public void onStartDrag() { + mDisplayingTrash = true; + animateToState(); + } + + public void setDropIcon(int resourceId, int colorResourceId) { + mDropTargetIcon.setImageResource(resourceId); + final Drawable drawable = mDropTargetIcon.getDrawable(); + + DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_ATOP); + DrawableCompat.setTint(drawable, mContext.getColor(colorResourceId)); + + if (drawable instanceof Animatable) { + ((Animatable) drawable).start(); + } + } + + public void toast(int textStrResId) { + mDisplayingToast = true; + mToastView.setText(textStrResId); + animateToState(); + } + + private Runnable mAnimateRunnable = new Runnable() { + @Override + public void run() { + if (mAnimator != null) { + mAnimator.cancel(); + } + mAnimator = new AnimatorSet(); + + final boolean showToast = mDisplayingToast; + final boolean showTrash = mDisplayingTrash && !mDisplayingToast; + final boolean showBrightness = !mEditing && !mDisplayingToast; + final boolean showInstructions = mEditing + && mDisplayingInstructions + && !mDisplayingTrash + && !mDisplayingToast; + + /*Log.d(TAG, "animating to state: " + + " showBrightness: " + showBrightness + + " showInstructions: " + showInstructions + + " showTrash: " + showTrash + + " showToast: " + showToast + );*/ + + final Animator brightnessAnimator = showBrightnessSlider(showBrightness); + final Animator instructionAnimator = showInstructions(showInstructions); + final Animator trashAnimator = showTrash(showTrash); + final Animator toastAnimator = showToast(showToast); + + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // if the view is already visible, keep it visible on animation start + // to animate it out, otherwise set it as invisible (to not affect view height) + mEditTileInstructionView.setVisibility( + getVisibilityForAnimation(mEditTileInstructionView, showInstructions)); + mDropTarget.setVisibility( + getVisibilityForAnimation(mDropTarget, showTrash)); + mToastView.setVisibility( + getVisibilityForAnimation(mToastView, showToast)); + if (mHasBrightnessSliderToDisplay) { + mBrightnessView.setVisibility( + getVisibilityForAnimation(mBrightnessView, showBrightness)); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + mToastView.setVisibility(showToast ? View.VISIBLE : View.GONE); + mEditTileInstructionView.setVisibility(showInstructions + ? View.VISIBLE : View.GONE); + mDropTarget.setVisibility(showTrash ? View.VISIBLE : View.GONE); + if (mHasBrightnessSliderToDisplay) { + mBrightnessView.setVisibility(showBrightness ? View.VISIBLE : View.GONE); + } + + mAnimator = null; + + requestLayout(); + + if (showToast) { + mToastView.bringToFront(); + mToastView.postDelayed(new Runnable() { + @Override + public void run() { + mDisplayingToast = false; + animateToState(); + } + }, TOAST_DURATION); + } + } + }); + + mAnimator.setDuration(mSkipAnimations ? 0 : 500); + mAnimator.setInterpolator(new FastOutSlowInInterpolator()); + mAnimator.setStartDelay(mSkipAnimations ? 0 : 100); + mAnimator.playTogether(instructionAnimator, trashAnimator, + brightnessAnimator, toastAnimator); + mAnimator.start(); + } + }; + + private int getVisibilityForAnimation(View view, boolean show) { + if (show || view.getVisibility() != View.GONE) { + return View.VISIBLE; + } + return View.INVISIBLE; + } + + private void animateToState() { + mSkipAnimations = false; + post(mAnimateRunnable); + } + + private void goToState() { + mSkipAnimations = true; + post(mAnimateRunnable); + } + + private Animator animateView(View v, boolean show) { + return ObjectAnimator.ofFloat(v, "translationY", show ? 0 : -getMeasuredHeight()); + } + + private Animator showBrightnessSlider(boolean show) { + return animateView(mBrightnessView, show); + } + + private Animator showInstructions(boolean show) { + return animateView(mEditTileInstructionView, show); + } + + private Animator showTrash(boolean show) { + return animateView(mDropTarget, show); + } + + private Animator showToast(boolean show) { + return animateView(mToastView, show); + } + + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + if (mListening) { + mSettingsObserver.observe(); + } else { + mSettingsObserver.unobserve(); + } + + } + + class SettingsObserver extends UserContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + @Override + protected void observe() { + super.observe(); + + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.QS_SHOW_BRIGHTNESS_SLIDER), false, this, UserHandle.USER_ALL); + update(); + } + + @Override + protected void unobserve() { + super.unobserve(); + + ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(this); + } + + @Override + public void update() { + ContentResolver resolver = mContext.getContentResolver(); + int currentUserId = ActivityManager.getCurrentUser(); + boolean showSlider = CMSettings.System.getIntForUser(resolver, + CMSettings.System.QS_SHOW_BRIGHTNESS_SLIDER, 1, currentUserId) == 1; + if (showSlider != mHasBrightnessSliderToDisplay) { + if (mAnimator != null) { + mAnimator.cancel(); // cancel everything we're animating + mAnimator = null; + } + mHasBrightnessSliderToDisplay = showSlider; + if (mBrightnessView != null) { + mBrightnessView.setVisibility(showSlider ? View.VISIBLE : View.GONE); + + // as per showBrightnessSlider() in QSPanel.java, we look it up on-the-go + ToggleSlider brightnessSlider = (ToggleSlider) findViewById(R.id.brightness_slider); + if (brightnessSlider != null) { + brightnessSlider.setVisibility(showSlider ? View.VISIBLE : View.GONE); + } + + } + getParent().requestLayout(); + animateToState(); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSettings.java b/packages/SystemUI/src/com/android/systemui/qs/QSSettings.java new file mode 100644 index 0000000..0a2b937 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSettings.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.qs; + +import android.Manifest; +import android.annotation.Nullable; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.ResultReceiver; +import android.util.AttributeSet; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.ScrollView; + +import com.android.systemui.R; +import com.android.systemui.statusbar.phone.QSTileHost; +import com.android.systemui.statusbar.phone.SystemUIDialog; + +public class QSSettings extends ScrollView { + + private static final String RESULT_RECEIVER_EXTRA = "result_receiver"; + private static final String LOCK_CLOCK_PACKAGENAME = "com.cyanogenmod.lockclock"; + private static final String LOCK_CLOCK_PERM_CLASS = LOCK_CLOCK_PACKAGENAME + + ".weather.PermissionRequestActivity"; + + private QSTileHost mHost; + + private boolean mAdapterEditingState; + private QSBooleanSettingRow mShowWeather; + private ResultReceiver mResultReceiver; + + public QSSettings(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + setFillViewport(true); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + findViewById(R.id.reset_tiles).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + initiateTileReset(); + } + }); + + mShowWeather = (QSBooleanSettingRow) findViewById(R.id.show_weather); + mShowWeather.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + PackageManager packageManager = getContext().getPackageManager(); + if (packageManager.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION, + LOCK_CLOCK_PACKAGENAME) != PackageManager.PERMISSION_GRANTED) { + mShowWeather.setChecked(false); + requestPermission(); + mHost.collapsePanels(); + } + } + } + }); + } + + public Parcelable getResultReceiverForSending() { + if (mResultReceiver == null) { + mResultReceiver = new ResultReceiver(new Handler()) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + super.onReceiveResult(resultCode, resultData); + if (resultCode == Activity.RESULT_OK) { + mShowWeather.setChecked(true); + } + mResultReceiver = null; + } + }; + } + Parcel parcel = Parcel.obtain(); + mResultReceiver.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel); + parcel.recycle(); + return receiverForSending; + } + + private void requestPermission() { + Intent i = new Intent(); + i.setClassName(LOCK_CLOCK_PACKAGENAME, LOCK_CLOCK_PERM_CLASS); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + i.putExtra(RESULT_RECEIVER_EXTRA, getResultReceiverForSending()); + getContext().startActivity(i); + } + + private void initiateTileReset() { + final AlertDialog d = new AlertDialog.Builder(mContext) + .setMessage(R.string.qs_tiles_reset_confirmation) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(com.android.internal.R.string.reset, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mHost.initiateReset(); + } + }).create(); + SystemUIDialog.makeSystemUIDialog(d); + d.show(); + } + + public void setHost(QSTileHost host) { + mHost = host; + } + + public boolean getAdapterEditingState() { + return mAdapterEditingState; + } + + public void setAdapterEditingState(boolean editing) { + this.mAdapterEditingState = editing; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java index e4a37fb..01a170f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java @@ -20,7 +20,9 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Animatable; +import android.graphics.Bitmap; import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; @@ -30,7 +32,9 @@ import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; +import android.widget.RemoteViews; import com.android.systemui.qs.QSTile.State; +import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.FlashlightController; @@ -41,6 +45,7 @@ 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.ZenModeController; +import cyanogenmod.app.StatusBarPanelCustomTile; import java.util.Collection; import java.util.Objects; @@ -85,7 +90,7 @@ public abstract class QSTile<TState extends State> implements Listenable { mHandler = new H(host.getLooper()); } - public boolean supportsDualTargets() { + public boolean hasDualTargetsDetails() { return false; } @@ -106,6 +111,7 @@ public abstract class QSTile<TState extends State> implements Listenable { Boolean getToggleState(); View createDetailView(Context context, View convertView, ViewGroup parent); Intent getSettingsIntent(); + StatusBarPanelCustomTile getCustomTile(); void setToggleState(boolean state); int getMetricsCategory(); } @@ -320,10 +326,12 @@ public abstract class QSTile<TState extends State> implements Listenable { } public interface Host { + void removeCustomTile(StatusBarPanelCustomTile customTile); void startActivityDismissingKeyguard(Intent intent); void startActivityDismissingKeyguard(PendingIntent intent); void warn(String message, Throwable t); void collapsePanels(); + RemoteViews.OnClickHandler getOnClickHandler(); Looper getLooper(); Context getContext(); Collection<QSTile<?>> getTiles(); @@ -337,9 +345,18 @@ public abstract class QSTile<TState extends State> implements Listenable { CastController getCastController(); FlashlightController getFlashlightController(); KeyguardMonitor getKeyguardMonitor(); + BatteryController getBatteryController(); + boolean isEditing(); + void setEditing(boolean editing); + void resetTiles(); + void goToSettingsPage(); public interface Callback { void onTilesChanged(); + void setEditing(boolean editing); + boolean isEditing(); + void goToSettingsPage(); + void resetTiles(); } } @@ -352,6 +369,42 @@ public abstract class QSTile<TState extends State> implements Listenable { } } + protected class ExternalIcon extends AnimationIcon { + private Context mPackageContext; + private String mPkg; + private int mResId; + + public ExternalIcon(String pkg, int resId) { + super(resId); + mPkg = pkg; + mResId = resId; + } + + @Override + public Drawable getDrawable(Context context) { + // Get the drawable from the package context + Drawable d = null; + try { + d = super.getDrawable(getPackageContext()); + } catch (Throwable t) { + Log.w(TAG, "Error creating package context" + mPkg + " id=" + mResId, t); + } + return d; + } + + private Context getPackageContext() { + if (mPackageContext == null) { + try { + mPackageContext = mContext.createPackageContext(mPkg, 0); + } catch (Throwable t) { + Log.w(TAG, "Error creating package context" + mPkg, t); + return null; + } + } + return mPackageContext; + } + } + public static class ResourceIcon extends Icon { private static final SparseArray<Icon> ICONS = new SparseArray<Icon>(); @@ -390,6 +443,21 @@ public abstract class QSTile<TState extends State> implements Listenable { } } + protected class ExternalBitmapIcon extends Icon { + private Bitmap mBitmap; + + public ExternalBitmapIcon(Bitmap bitmap) { + mBitmap = bitmap; + } + + @Override + public Drawable getDrawable(Context context) { + // This is gross + BitmapDrawable bitmapDrawable = new BitmapDrawable(context.getResources(), mBitmap); + return bitmapDrawable; + } + } + protected class AnimationIcon extends ResourceIcon { private boolean mAllowAnimation; @@ -404,13 +472,14 @@ 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) context.getDrawable(mResId) - .getConstantState().newDrawable(); - d.start(); - if (mAllowAnimation) { - mAllowAnimation = false; - } else { - d.stop(); // skip directly to end state + final Drawable d = super.getDrawable(context).getConstantState().newDrawable(); + if (d instanceof AnimatedVectorDrawable) { + ((AnimatedVectorDrawable)d).start(); + if (mAllowAnimation) { + mAllowAnimation = false; + } else { + ((AnimatedVectorDrawable)d).stop(); // skip directly to end state + } } return d; } @@ -431,6 +500,7 @@ public abstract class QSTile<TState extends State> implements Listenable { public static class State { public boolean visible; + public boolean enabled = true; public Icon icon; public String label; public String contentDescription; @@ -441,6 +511,7 @@ public abstract class QSTile<TState extends State> implements Listenable { if (other == null) throw new IllegalArgumentException(); if (!other.getClass().equals(getClass())) throw new IllegalArgumentException(); final boolean changed = other.visible != visible + || !Objects.equals(other.enabled, enabled) || !Objects.equals(other.icon, icon) || !Objects.equals(other.label, label) || !Objects.equals(other.contentDescription, contentDescription) @@ -448,6 +519,7 @@ public abstract class QSTile<TState extends State> implements Listenable { || !Objects.equals(other.dualLabelContentDescription, dualLabelContentDescription); other.visible = visible; + other.enabled = enabled; other.icon = icon; other.label = label; other.contentDescription = contentDescription; @@ -464,6 +536,7 @@ public abstract class QSTile<TState extends State> implements Listenable { protected StringBuilder toStringBuilder() { final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('['); sb.append("visible=").append(visible); + sb.append(",enabled=").append(enabled); sb.append(",icon=").append(icon); sb.append(",label=").append(label); sb.append(",contentDescription=").append(contentDescription); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java index 6d26a3b..7c63782 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +17,13 @@ package com.android.systemui.qs; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PorterDuff; import android.graphics.Typeface; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; @@ -27,6 +31,7 @@ import android.graphics.drawable.RippleDrawable; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.util.Log; import android.util.MathUtils; import android.util.TypedValue; import android.view.Gravity; @@ -48,6 +53,8 @@ public class QSTileView extends ViewGroup { private static final Typeface CONDENSED = Typeface.create("sans-serif-condensed", Typeface.NORMAL); + private static final String TAG = "QSTileView"; + protected final Context mContext; private final View mIcon; private final View mDivider; @@ -62,6 +69,7 @@ public class QSTileView extends ViewGroup { private TextView mLabel; private QSDualTileLabel mDualLabel; private boolean mDual; + private boolean mDualDetails; private OnClickListener mClickPrimary; private OnClickListener mClickSecondary; private OnLongClickListener mLongClick; @@ -124,31 +132,34 @@ public class QSTileView extends ViewGroup { private void recreateLabel() { CharSequence labelText = null; CharSequence labelDescription = null; - if (mLabel != null) { + if (mLabel != null && mLabel.isAttachedToWindow()) { labelText = mLabel.getText(); removeView(mLabel); - mLabel = null; } - if (mDualLabel != null) { + if (mDualLabel != null && mDualLabel.isAttachedToWindow()) { labelText = mDualLabel.getText(); - labelDescription = mLabel.getContentDescription(); + labelDescription = mDualLabel.getContentDescription(); removeView(mDualLabel); - mDualLabel = null; } final Resources res = mContext.getResources(); if (mDual) { - mDualLabel = new QSDualTileLabel(mContext); - mDualLabel.setId(View.generateViewId()); - mDualLabel.setBackgroundResource(R.drawable.btn_borderless_rect); - 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, - res.getDimensionPixelSize(R.dimen.qs_tile_text_size)); - mDualLabel.setClickable(true); - mDualLabel.setOnClickListener(mClickSecondary); - mDualLabel.setFocusable(true); + if (mDualLabel == null) { + mDualLabel = new QSDualTileLabel(mContext); + mDualLabel.setId(View.generateViewId()); + mDualLabel.setBackgroundResource(R.drawable.btn_borderless_rect); + if (mDualDetails) { + 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, + res.getDimensionPixelSize(R.dimen.qs_tile_text_size)); + mDualLabel.setClickable(true); + mDualLabel.setFocusable(true); + mDualLabel.setOnClickListener(mDualDetails ? mClickSecondary : mClickPrimary); + mDualLabel.setOnLongClickListener(mLongClick); + } if (labelText != null) { mDualLabel.setText(labelText); } @@ -158,15 +169,18 @@ public class QSTileView extends ViewGroup { addView(mDualLabel); mDualLabel.setAccessibilityTraversalAfter(mTopBackgroundView.getId()); } else { - mLabel = new TextView(mContext); - mLabel.setTextColor(mContext.getColor(R.color.qs_tile_text)); - mLabel.setGravity(Gravity.CENTER_HORIZONTAL); - mLabel.setMinLines(2); - mLabel.setPadding(0, 0, 0, 0); - mLabel.setTypeface(CONDENSED); - mLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, - res.getDimensionPixelSize(R.dimen.qs_tile_text_size)); - mLabel.setClickable(false); + if (mLabel == null) { + mLabel = new TextView(mContext); + mLabel.setTextColor(mContext.getColor(R.color.qs_tile_text)); + mLabel.setGravity(Gravity.CENTER_HORIZONTAL); + mLabel.setMinLines(2); + mLabel.setPadding(0, 0, 0, 0); + mLabel.setTypeface(CONDENSED); + mLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, + res.getDimensionPixelSize(R.dimen.qs_tile_text_size)); + mLabel.setClickable(false); + mLabel.setFocusable(false); + } if (labelText != null) { mLabel.setText(labelText); } @@ -174,39 +188,59 @@ public class QSTileView extends ViewGroup { } } - public boolean setDual(boolean dual) { + public boolean isDual() { + return mDual; + } + + public boolean setDual(boolean dual, boolean hasDetails) { final boolean changed = dual != mDual; mDual = dual; + mDualDetails = hasDetails; if (changed) { recreateLabel(); } - if (mTileBackground instanceof RippleDrawable) { - setRipple((RippleDrawable) mTileBackground); - } + if (dual) { mTopBackgroundView.setOnClickListener(mClickPrimary); + mTopBackgroundView.setOnLongClickListener(mLongClick); setOnClickListener(null); - setClickable(false); + setOnLongClickListener(null); setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - mTopBackgroundView.setBackground(mTileBackground); } else { mTopBackgroundView.setOnClickListener(null); - mTopBackgroundView.setClickable(false); + mTopBackgroundView.setOnLongClickListener(null); setOnClickListener(mClickPrimary); setOnLongClickListener(mLongClick); setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - setBackground(mTileBackground); } + setTileBackground(); + mTopBackgroundView.setClickable(dual); mTopBackgroundView.setFocusable(dual); + setClickable(!dual); setFocusable(!dual); mDivider.setVisibility(dual ? VISIBLE : GONE); + mTopBackgroundView.setVisibility(dual ? VISIBLE : GONE); + + if (changed) { + getParent().requestLayout(); + } postInvalidate(); return changed; } + protected void setTileBackground() { + if (mTileBackground instanceof RippleDrawable) { + setRipple((RippleDrawable) mTileBackground); + } else { + setRipple(null); + } + mTopBackgroundView.setBackground(mDual ? mTileBackground : null); + setBackground(!mDual ? mTileBackground : null); + } + private void setRipple(RippleDrawable tileBackground) { mRipple = tileBackground; - if (getWidth() != 0) { + if (getWidth() != 0 && mRipple != null) { updateRippleSize(getWidth(), getHeight()); } } @@ -225,7 +259,7 @@ public class QSTileView extends ViewGroup { return icon; } - private Drawable newTileBackground() { + public Drawable newTileBackground() { final int[] attrs = new int[] { android.R.attr.selectableItemBackgroundBorderless }; final TypedArray ta = mContext.obtainStyledAttributes(attrs); final Drawable d = ta.getDrawable(0); @@ -285,7 +319,7 @@ public class QSTileView extends ViewGroup { private void updateRippleSize(int width, int height) { // center the touch feedback on the center of the icon, and dial it down a bit final int cx = width / 2; - final int cy = mDual ? mIcon.getTop() + mIcon.getHeight() / 2 : height / 2; + final int cy = mDual ? mIcon.getTop() + mIcon.getHeight() : height / 2; final int rad = (int)(mIcon.getHeight() * 1.25f); mRipple.setHotspotBounds(cx - rad, cy - rad, cx + rad, cy + rad); } @@ -302,9 +336,20 @@ public class QSTileView extends ViewGroup { mDualLabel.setText(state.label); mDualLabel.setContentDescription(state.dualLabelContentDescription); mTopBackgroundView.setContentDescription(state.contentDescription); + if (!Objects.equals(state.enabled, mDualLabel.isEnabled())) { + mTopBackgroundView.setEnabled(state.enabled); + mDualLabel.setEnabled(state.enabled); + mDualLabel.setTextColor(mContext.getResources().getColor(state.enabled ? + R.color.qs_tile_text : R.color.qs_tile_text_disabled)); + } } else { mLabel.setText(state.label); setContentDescription(state.contentDescription); + if (!Objects.equals(state.enabled, mLabel.isEnabled())) { + mLabel.setEnabled(state.enabled); + mLabel.setTextColor(mContext.getResources().getColor(state.enabled ? + R.color.qs_tile_text : R.color.qs_tile_text_disabled)); + } } } @@ -323,6 +368,14 @@ public class QSTileView extends ViewGroup { } } } + if (!Objects.equals(state.enabled, iv.isEnabled())) { + iv.setEnabled(state.enabled); + if (state.enabled) { + iv.setColorFilter(null); + } else { + iv.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY); + } + } } public void onStateChanged(QSTile.State state) { @@ -349,6 +402,37 @@ public class QSTileView extends ViewGroup { return lastView; } + public void setEditing(boolean editing) { + if (mDual) { + if (mTopBackgroundView != null) { + mTopBackgroundView.setFocusable(!editing); + mTopBackgroundView.setClickable(!editing); + } + if (mDualLabel != null) { + mDualLabel.setFocusable(!editing); + mDualLabel.setClickable(!editing); + } + setClickable(editing); + setFocusable(editing); + } else { + if (mLabel != null) { + mLabel.setFocusable(!editing); + } + if (mRipple != null) { + mRipple.setVisible(!editing, false); + } + } + + // clean up extra label view if needed + if (!editing) { + if (mDual && mLabel != null) { + mLabel = null; + } else if (!mDual && mDualLabel != null) { + mDualLabel = null; + } + } + } + private class H extends Handler { private static final int STATE_CHANGED = 1; public H() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSViewPager.java b/packages/SystemUI/src/com/android/systemui/qs/QSViewPager.java new file mode 100644 index 0000000..3dc5d27 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSViewPager.java @@ -0,0 +1,105 @@ +package com.android.systemui.qs; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AccelerateInterpolator; + +public class QSViewPager extends ViewPager { + + private static final String TAG = "QSViewPager"; + + protected static final float SCROLL_PERCENT = .10f; + + QSDragPanel mDragPanel; + + public QSViewPager(Context context) { + super(context); + } + + public void setDragPanel(QSDragPanel p) { + mDragPanel = p; + } + + @Override + public boolean hasOverlappingRendering() { + return mDragPanel.isEditing(); + } + + @Override + public boolean canScrollHorizontally(int direction) { + if (direction < 0 + && mDragPanel.isDragging() + && getCurrentItem() == 1) { + // can't scroll left while not editing, OR dragging on the first page + return false; + } + return super.canScrollHorizontally(direction); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int height = 0; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int h = child.getMeasuredHeight(); + if (h > height && !(child instanceof QSSettings)) height = h; + } + heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + public void animatePagerTransition(final boolean forward) { + ValueAnimator animator = ValueAnimator.ofInt(0, getWidth()); + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + if (isFakeDragging()) { + endFakeDrag(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (isFakeDragging()) { + endFakeDrag(); + } + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + + animator.setInterpolator(new AccelerateInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + + private int oldDragPosition = 0; + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (isFakeDragging()) { + int dragPosition = (Integer) animation.getAnimatedValue(); + int dragOffset = dragPosition - oldDragPosition; + oldDragPosition = dragPosition; + fakeDragBy(dragOffset * (forward ? -1 : 1)); + } + } + }); + if (beginFakeDrag()) { + animator.setDuration(500); + animator.start(); + } else { + Log.e(TAG, "can't start fake drag?"); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AdbOverNetworkTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AdbOverNetworkTile.java new file mode 100644 index 0000000..b970a4c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AdbOverNetworkTile.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.net.NetworkUtils; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.systemui.R; +import com.android.systemui.qs.QSTile; + +import java.net.InetAddress; + +import cyanogenmod.providers.CMSettings; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +public class AdbOverNetworkTile extends QSTile<QSTile.BooleanState> { + + private boolean mListening; + + private static final Intent SETTINGS_DEVELOPMENT = + new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS); + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleClick() { + CMSettings.Secure.putIntForUser(mContext.getContentResolver(), + CMSettings.Secure.ADB_PORT, getState().value ? -1 : 5555, + UserHandle.USER_CURRENT); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(SETTINGS_DEVELOPMENT); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.visible = isAdbEnabled(); + if (!state.visible) { + return; + } + state.value = isAdbNetworkEnabled(); + if (state.value) { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + + if (wifiInfo != null) { + // if wifiInfo is not null, set the label to "hostAddress" + InetAddress address = NetworkUtils.intToInetAddress(wifiInfo.getIpAddress()); + state.label = address.getHostAddress(); + } else { + // if wifiInfo is null, set the label without host address + state.label = mContext.getString(R.string.quick_settings_network_adb_label); + } + state.icon = ResourceIcon.get(R.drawable.ic_qs_network_adb_on); + } else { + // Otherwise set the disabled label and icon + state.label = mContext.getString(R.string.quick_settings_network_adb_label); + state.icon = ResourceIcon.get(R.drawable.ic_qs_network_adb_off); + } + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_ADB_OVER_NETWORK; + } + + private boolean isAdbEnabled() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.ADB_ENABLED, 0) > 0; + } + + private boolean isAdbNetworkEnabled() { + return CMSettings.Secure.getInt(mContext.getContentResolver(), + CMSettings.Secure.ADB_PORT, 0) > 0; + } + + public AdbOverNetworkTile(Host host) { + super(host); + } + + private ContentObserver mObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + refreshState(); + } + }; + + @Override + public void setListening(boolean listening) { + if (mListening != listening) { + mListening = listening; + if (listening) { + mContext.getContentResolver().registerContentObserver( + CMSettings.Secure.getUriFor(CMSettings.Secure.ADB_PORT), + false, mObserver); + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(Settings.Global.ADB_ENABLED), + false, mObserver); + } else { + mContext.getContentResolver().unregisterContentObserver(mObserver); + } + } + } +} 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 49f8d1c..bf28dbf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +22,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; +import android.provider.Settings; import android.provider.Settings.Global; import com.android.internal.logging.MetricsLogger; @@ -30,6 +32,9 @@ import com.android.systemui.qs.QSTile; /** Quick settings tile: Airplane mode **/ public class AirplaneModeTile extends QSTile<QSTile.BooleanState> { + + private static final Intent WIRELESS_SETTINGS = new Intent(Settings.ACTION_WIRELESS_SETTINGS); + private final AnimationIcon mEnable = new AnimationIcon(R.drawable.ic_signal_airplane_enable_animation); private final AnimationIcon mDisable = @@ -62,6 +67,11 @@ public class AirplaneModeTile extends QSTile<QSTile.BooleanState> { mDisable.setAllowAnimation(true); } + @Override + public void handleLongClick() { + mHost.startActivityDismissingKeyguard(WIRELESS_SETTINGS); + } + private void setEnabled(boolean enabled) { final ConnectivityManager mgr = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AmbientDisplayTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AmbientDisplayTile.java new file mode 100644 index 0000000..cc08d02 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AmbientDisplayTile.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; +import android.content.Intent; +import android.provider.Settings; +import android.provider.Settings.Secure; + +import com.android.systemui.qs.SecureSetting; +import com.android.systemui.qs.QSTile; +import com.android.systemui.R; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +/** Quick settings tile: Ambient Display **/ +public class AmbientDisplayTile extends QSTile<QSTile.BooleanState> { + + private static final Intent DISPLAY_SETTINGS = new Intent("android.settings.DISPLAY_SETTINGS"); + + private final SecureSetting mSetting; + + public AmbientDisplayTile(Host host) { + super(host); + + mSetting = new SecureSetting(mContext, mHandler, Secure.DOZE_ENABLED) { + @Override + protected void handleValueChanged(int value, boolean observedChange) { + handleRefreshState(value); + } + }; + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleClick() { + setEnabled(!mState.value); + refreshState(); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(DISPLAY_SETTINGS); + } + + private void setEnabled(boolean enabled) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.DOZE_ENABLED, + enabled ? 1 : 0); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + final int value = arg instanceof Integer ? (Integer)arg : mSetting.getValue(); + final boolean enable = value != 0; + state.value = enable; + state.visible = true; + state.label = mContext.getString(R.string.quick_settings_ambient_display_label); + if (enable) { + state.icon = ResourceIcon.get(R.drawable.ic_qs_ambientdisplay_on); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_ambient_display_on); + } else { + state.icon = ResourceIcon.get(R.drawable.ic_qs_ambientdisplay_off); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_ambient_display_off); + } + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_AMBIENT_DISPLAY; + } + + @Override + protected String composeChangeAnnouncement() { + if (mState.value) { + return mContext.getString( + R.string.accessibility_quick_settings_ambient_display_changed_on); + } else { + return mContext.getString( + R.string.accessibility_quick_settings_ambient_display_changed_off); + } + } + + @Override + public void setListening(boolean listening) { + // Do nothing + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java new file mode 100644 index 0000000..1a60fa9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.PowerManager; +import android.provider.Settings; + +import com.android.systemui.qs.QSTile; +import com.android.systemui.R; +import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryStateRegistar; + +import cyanogenmod.power.PerformanceManager; + +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +/** Quick settings tile: Battery saver **/ +public class BatterySaverTile extends QSTile<QSTile.BooleanState> { + + private static final Intent BATTERY_SETTINGS = new Intent(Intent.ACTION_POWER_USAGE_SUMMARY); + + private final PowerManager mPm; + private final boolean mHasPowerProfiles; + + private boolean mListening; + private boolean mPluggedIn; + + public BatterySaverTile(Host host) { + super(host); + mPm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mHasPowerProfiles = PerformanceManager.getInstance(mContext).getNumberOfProfiles() > 0; + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleClick() { + mPm.setPowerSaveMode(!mState.value); + refreshState(!mState.value); + } + + @Override + public void handleLongClick() { + mHost.startActivityDismissingKeyguard(BATTERY_SETTINGS); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.value = arg instanceof Boolean ? (boolean) arg : mPm.isPowerSaveMode(); + state.visible = !mHasPowerProfiles; + state.label = mContext.getString(R.string.quick_settings_battery_saver_label); + if (state.value) { + state.icon = ResourceIcon.get(R.drawable.ic_qs_battery_saver_on); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_battery_saver_on); + } else { + state.icon = ResourceIcon.get(R.drawable.ic_qs_battery_saver_off); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_battery_saver_off); + } + + state.enabled = !mPluggedIn; + if (mPluggedIn) { + state.label = mContext.getString(R.string.quick_settings_battery_saver_label_charging); + } + } + + @Override + protected String composeChangeAnnouncement() { + if (mState.value) { + return mContext.getString( + R.string.accessibility_quick_settings_battery_saver_changed_on); + } else { + return mContext.getString( + R.string.accessibility_quick_settings_battery_saver_changed_off); + } + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_BATTERY_SAVER; + } + + private BatteryStateRegistar.BatteryStateChangeCallback mBatteryState + = new BatteryStateRegistar.BatteryStateChangeCallback() { + @Override + public void onBatteryLevelChanged(boolean present, int level, boolean pluggedIn, + boolean charging) { + mPluggedIn = pluggedIn || charging; + refreshState(); + } + + @Override + public void onPowerSaveChanged() { + refreshState(); + } + + @Override + public void onBatteryStyleChanged(int style, int percentMode) { + // ignore + } + }; + + @Override + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + + if (listening) { + getHost().getBatteryController().addStateChangedCallback(mBatteryState); + } else { + getHost().getBatteryController().removeStateChangedCallback(mBatteryState); + } + } +} 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 abce31f..cdedc26 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,17 +25,25 @@ import android.provider.Settings; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ListView; 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.QSDetailItemsList; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.BluetoothController; +import cyanogenmod.app.StatusBarPanelCustomTile; + +import java.util.ArrayList; import java.util.Collection; -import java.util.Set; +import java.util.List; /** Quick settings tile: Bluetooth **/ public class BluetoothTile extends QSTile<QSTile.BooleanState> { @@ -50,7 +59,7 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { } @Override - public boolean supportsDualTargets() { + public boolean hasDualTargetsDetails() { return true; } @@ -90,6 +99,11 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { } @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(BLUETOOTH_SETTINGS); + } + + @Override protected void handleUpdateState(BooleanState state, Object arg) { final boolean supported = mController.isBluetoothSupported(); final boolean enabled = mController.isBluetoothEnabled(); @@ -165,8 +179,11 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { } }; - private final class BluetoothDetailAdapter implements DetailAdapter, QSDetailItems.Callback { - private QSDetailItems mItems; + private final class BluetoothDetailAdapter implements DetailAdapter, + QSDetailItems.Callback, AdapterView.OnItemClickListener { + private QSDetailItemsList mItemsList; + private QSDetailItemsList.QSDetailListAdapter mAdapter; + private List<Item> mBluetoothItems = new ArrayList<>(); @Override public int getTitle() { @@ -184,6 +201,11 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { } @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override public void setToggleState(boolean state) { MetricsLogger.action(mContext, MetricsLogger.QS_BLUETOOTH_TOGGLE, state); mController.setBluetoothEnabled(state); @@ -197,29 +219,35 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { @Override public View createDetailView(Context context, View convertView, ViewGroup parent) { - mItems = QSDetailItems.convertOrInflate(context, convertView, parent); - mItems.setTagSuffix("Bluetooth"); - mItems.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty, + mItemsList = QSDetailItemsList.convertOrInflate(context, convertView, parent); + ListView listView = mItemsList.getListView(); + listView.setDivider(null); + listView.setOnItemClickListener(this); + listView.setAdapter(mAdapter = + new QSDetailItemsList.QSDetailListAdapter(context, mBluetoothItems)); + mAdapter.setCallback(this); + mItemsList.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty, R.string.quick_settings_bluetooth_detail_empty_text); - mItems.setCallback(this); - mItems.setMinHeightInItems(0); + updateItems(); - setItemsVisible(mState.value); - return mItems; + return mItemsList; } public void setItemsVisible(boolean visible) { - if (mItems == null) return; - mItems.setItemsVisible(visible); + if (mAdapter == null) return; + if (visible) { + updateItems(); + } else { + mBluetoothItems.clear(); + } + mAdapter.notifyDataSetChanged(); } private void updateItems() { - if (mItems == null) return; - Item[] items = null; - final Collection<CachedBluetoothDevice> devices = mController.getDevices(); + if (mAdapter == null) return; + final Collection<CachedBluetoothDevice> devices = mController.getDevices(); if (devices != null) { - items = new Item[getBondedCount(devices)]; - int i = 0; + mBluetoothItems.clear(); for (CachedBluetoothDevice device : devices) { if (device.getBondState() == BluetoothDevice.BOND_NONE) continue; final Item item = new Item(); @@ -235,10 +263,10 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { item.line2 = mContext.getString(R.string.quick_settings_connecting); } item.tag = device; - items[i++] = item; + mBluetoothItems.add(item); } } - mItems.setItems(items); + mAdapter.notifyDataSetChanged(); } private int getBondedCount(Collection<CachedBluetoothDevice> devices) { @@ -253,12 +281,7 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { @Override public void onDetailItemClick(Item item) { - if (item == null || item.tag == null) return; - final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; - if (device != null && device.getMaxConnectionState() - == BluetoothProfile.STATE_DISCONNECTED) { - mController.connect(device); - } + // noop } @Override @@ -269,5 +292,16 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { mController.disconnect(device); } } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + Item item = (Item) parent.getItemAtPosition(position); + if (item == null || item.tag == null) return; + final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; + if (device != null && device.getMaxConnectionState() + == BluetoothProfile.STATE_DISCONNECTED) { + mController.connect(device); + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CaffeineTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CaffeineTile.java new file mode 100644 index 0000000..fb6ec42 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CaffeineTile.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.database.ContentObserver; +import android.net.Uri; +import android.os.CountDownTimer; +import android.os.PowerManager; +import android.os.SystemClock; +import android.provider.Settings; + +import com.android.systemui.qs.QSTile; +import com.android.systemui.R; + +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +/** Quick settings tile: Caffeine **/ +public class CaffeineTile extends QSTile<QSTile.BooleanState> { + + private final PowerManager.WakeLock mWakeLock; + private int mSecondsRemaining; + private int mDuration; + private static int[] DURATIONS = new int[] { + 5 * 60, // 5 min + 10 * 60, // 10 min + 30 * 60, // 30 min + -1, // infinity + }; + private CountDownTimer mCountdownTimer = null; + public long mLastClickTime = -1; + private final Receiver mReceiver = new Receiver(); + private boolean mListening; + + public CaffeineTile(Host host) { + super(host); + mWakeLock = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE)).newWakeLock( + PowerManager.FULL_WAKE_LOCK, "CaffeineTile"); + mReceiver.init(); + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleDestroy() { + super.handleDestroy(); + stopCountDown(); + mReceiver.destroy(); + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + } + + @Override + public void setListening(boolean listening) { + } + + @Override + public void handleClick() { + // If last user clicks < 5 seconds + // we cycle different duration + // otherwise toggle on/off + if (mWakeLock.isHeld() && (mLastClickTime != -1) && + (SystemClock.elapsedRealtime() - mLastClickTime < 5000)) { + // cycle duration + mDuration++; + if (mDuration >= DURATIONS.length) { + // all durations cycled, turn if off + mDuration = -1; + stopCountDown(); + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + } else { + // change duration + startCountDown(DURATIONS[mDuration]); + if (!mWakeLock.isHeld()) { + mWakeLock.acquire(); + } + } + } else { + // toggle + if (mWakeLock.isHeld()) { + mWakeLock.release(); + stopCountDown(); + } else { + mWakeLock.acquire(); + mDuration = 0; + startCountDown(DURATIONS[mDuration]); + } + } + mLastClickTime = SystemClock.elapsedRealtime(); + refreshState(); + } + + private void startCountDown(long duration) { + stopCountDown(); + mSecondsRemaining = (int)duration; + if (duration == -1) { + // infinity timing, no need to start timer + return; + } + mCountdownTimer = new CountDownTimer(duration * 1000, 1000) { + + @Override + public void onTick(long millisUntilFinished) { + mSecondsRemaining = (int) (millisUntilFinished / 1000); + refreshState(); + } + + @Override + public void onFinish() { + if (mWakeLock.isHeld()) + mWakeLock.release(); + refreshState(); + } + + }.start(); + } + + private void stopCountDown() { + if (mCountdownTimer != null) { + mCountdownTimer.cancel(); + mCountdownTimer = null; + } + } + + private String formatValueWithRemainingTime() { + if (mSecondsRemaining == -1) { + return "\u221E"; // infinity + } + return String.format("%02d:%02d", + mSecondsRemaining / 60 % 60, mSecondsRemaining % 60); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.value = mWakeLock.isHeld(); + state.visible = true; + if (state.value) { + state.label = formatValueWithRemainingTime(); + state.icon = ResourceIcon.get(R.drawable.ic_qs_caffeine_on); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_caffeine_on); + } else { + state.label = mContext.getString(R.string.quick_settings_caffeine_label); + state.icon = ResourceIcon.get(R.drawable.ic_qs_caffeine_off); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_caffeine_off); + } + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_CAFFEINE; + } + + private final class Receiver extends BroadcastReceiver { + public void init() { + // Register for Intent broadcasts for... + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + mContext.registerReceiver(this, filter, null, mHandler); + } + + public void destroy() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_SCREEN_OFF.equals(action)) { + // disable caffeine if user force off (power button) + stopCountDown(); + if (mWakeLock.isHeld()) + mWakeLock.release(); + refreshState(); + } + } + } +} 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 61695b2..cd608d6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +33,7 @@ import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.CastController.CastDevice; import com.android.systemui.statusbar.policy.KeyguardMonitor; +import cyanogenmod.app.StatusBarPanelCustomTile; import java.util.LinkedHashMap; import java.util.Set; @@ -91,6 +93,16 @@ public class CastTile extends QSTile<QSTile.BooleanState> { } @Override + protected void handleSecondaryClick() { + handleClick(); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(CAST_SETTINGS); + } + + @Override protected void handleUpdateState(BooleanState state, Object arg) { state.visible = !mKeyguard.isSecure() || !mKeyguard.isShowing() || mKeyguard.canSkipBouncer(); @@ -129,6 +141,11 @@ public class CastTile extends QSTile<QSTile.BooleanState> { return null; } + @Override + public boolean hasDualTargetsDetails() { + return true; + } + private String getDeviceName(CastDevice device) { return device.name != null ? device.name : mContext.getString(R.string.quick_settings_cast_device_default_name); @@ -167,6 +184,11 @@ public class CastTile extends QSTile<QSTile.BooleanState> { } @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override public void setToggleState(boolean state) { // noop } @@ -186,12 +208,14 @@ public class CastTile extends QSTile<QSTile.BooleanState> { @Override public void onViewAttachedToWindow(View v) { if (DEBUG) Log.d(TAG, "onViewAttachedToWindow"); + mController.setDiscovering(true); } @Override public void onViewDetachedFromWindow(View v) { if (DEBUG) Log.d(TAG, "onViewDetachedFromWindow"); mVisibleOrder.clear(); + mController.setDiscovering(false); } }); } @@ -199,7 +223,6 @@ public class CastTile extends QSTile<QSTile.BooleanState> { R.string.quick_settings_cast_detail_empty_text); mItems.setCallback(this); updateItems(mController.getCastDevices()); - mController.setDiscovering(true); return mItems; } 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 f3ad9d8..038fa5e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +17,13 @@ package com.android.systemui.qs.tiles; +import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.os.UserHandle; +import android.telephony.TelephonyManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -35,15 +39,23 @@ import com.android.systemui.statusbar.policy.NetworkController.MobileDataControl import com.android.systemui.statusbar.policy.NetworkController.MobileDataController.DataUsageInfo; import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; import com.android.systemui.statusbar.policy.SignalCallbackAdapter; +import cyanogenmod.app.StatusBarPanelCustomTile; /** Quick settings tile: Cellular **/ public class CellularTile extends QSTile<QSTile.SignalState> { - private static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName( + + private static final Intent DATA_USAGE_SETTINGS = new Intent().setComponent(new ComponentName( "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity")); + private static final Intent MOBILE_NETWORK_SETTINGS = new Intent(Intent.ACTION_MAIN) + .setComponent(new ComponentName("com.android.phone", + "com.android.phone.MobileNetworkSettings")); + private static final Intent MOBILE_NETWORK_SETTINGS_MSIM + = new Intent("com.android.settings.sim.SIM_SUB_INFO_SETTINGS"); private final NetworkController mController; private final MobileDataController mDataController; private final CellularDetailAdapter mDetailAdapter; + private final TelephonyManager mTelephonyManager; private final CellSignalCallback mSignalCallback = new CellSignalCallback(); @@ -52,6 +64,7 @@ public class CellularTile extends QSTile<QSTile.SignalState> { mController = host.getNetworkController(); mDataController = mController.getMobileDataController(); mDetailAdapter = new CellularDetailAdapter(); + mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); } @Override @@ -79,18 +92,40 @@ public class CellularTile extends QSTile<QSTile.SignalState> { } @Override + protected void handleUserSwitch(int newUserId) { + if (newUserId != UserHandle.USER_OWNER) { + refreshState(); + } + } + + @Override protected void handleClick() { MetricsLogger.action(mContext, getMetricsCategory()); if (mDataController.isMobileDataSupported()) { showDetail(true); } else { - mHost.startActivityDismissingKeyguard(CELLULAR_SETTINGS); + mHost.startActivityDismissingKeyguard(DATA_USAGE_SETTINGS); + } + } + + @Override + protected void handleSecondaryClick() { + handleClick(); + } + + @Override + protected void handleLongClick() { + if (mTelephonyManager.getDefault().getPhoneCount() > 1) { + mHost.startActivityDismissingKeyguard(MOBILE_NETWORK_SETTINGS_MSIM); + } else { + mHost.startActivityDismissingKeyguard(MOBILE_NETWORK_SETTINGS); } } @Override protected void handleUpdateState(SignalState state, Object arg) { - state.visible = mController.hasMobileDataFeature(); + state.visible = mController.hasMobileDataFeature() + && (ActivityManager.getCurrentUser() == UserHandle.USER_OWNER); if (!state.visible) return; CallbackInfo cb = (CallbackInfo) arg; if (cb == null) { @@ -112,7 +147,9 @@ public class CellularTile extends QSTile<QSTile.SignalState> { state.label = cb.enabled ? removeTrailingPeriod(cb.enabledDesc) - : r.getString(R.string.quick_settings_rssi_emergency_only); + : mDataController.isMobileDataSupported() ? + r.getString(R.string.data_sim_not_configured) : + r.getString(R.string.quick_settings_rssi_emergency_only); final String signalContentDesc = cb.enabled && (cb.mobileSignalIconId > 0) ? cb.signalContentDescription @@ -131,6 +168,11 @@ public class CellularTile extends QSTile<QSTile.SignalState> { return MetricsLogger.QS_CELLULAR; } + @Override + public boolean hasDualTargetsDetails() { + return true; + } + // Remove the period from the network name public static String removeTrailingPeriod(String string) { if (string == null) return null; @@ -168,7 +210,7 @@ public class CellularTile extends QSTile<QSTile.SignalState> { @Override public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType, int qsType, boolean activityIn, boolean activityOut, String typeContentDescription, - String description, boolean isWide, int subId) { + String description, boolean isWide, boolean showSeparateRoaming, int subId) { if (qsIcon == null) { // Not data sim, don't display. return; @@ -192,10 +234,14 @@ public class CellularTile extends QSTile<QSTile.SignalState> { // Make sure signal gets cleared out when no sims. mInfo.mobileSignalIconId = 0; mInfo.dataTypeIconId = 0; - // Show a No SIMs description to avoid emergency calls message. + // Show a No SIMs description if we're incapable of supporting mobile data + // to avoid showing an emergency mode description. If we're still capable of + // supporting mobile data, notify the user that the data sim is not configured + // only relevant in MSIM scenario: CYNGNOS-2211 mInfo.enabled = true; - mInfo.enabledDesc = mContext.getString( - R.string.keyguard_missing_sim_message_short); + mInfo.enabledDesc = mDataController.isMobileDataSupported() ? + mContext.getString(R.string.data_sim_not_configured) + : mContext.getString(R.string.keyguard_missing_sim_message_short); mInfo.signalContentDescription = mInfo.enabledDesc; } refreshState(mInfo); @@ -214,6 +260,10 @@ public class CellularTile extends QSTile<QSTile.SignalState> { }; private final class CellularDetailAdapter implements DetailAdapter { + @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } @Override public int getTitle() { @@ -229,7 +279,7 @@ public class CellularTile extends QSTile<QSTile.SignalState> { @Override public Intent getSettingsIntent() { - return CELLULAR_SETTINGS; + return DATA_USAGE_SETTINGS; } @Override 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 c6fc6ff..f49d97e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +17,8 @@ package com.android.systemui.qs.tiles; +import android.content.Intent; +import android.provider.Settings; import android.provider.Settings.Secure; import com.android.internal.logging.MetricsLogger; @@ -28,12 +31,14 @@ import com.android.systemui.qs.UsageTracker; /** Quick settings tile: Invert colors **/ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { + private static final Intent ACCESSIBILITY_SETTINGS = new Intent( + Settings.ACTION_ACCESSIBILITY_SETTINGS); + private final AnimationIcon mEnable = new AnimationIcon(R.drawable.ic_invert_colors_enable_animation); private final AnimationIcon mDisable = new AnimationIcon(R.drawable.ic_invert_colors_disable_animation); private final SecureSetting mSetting; - private final UsageTracker mUsageTracker; private boolean mListening; @@ -44,29 +49,11 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) { @Override protected void handleValueChanged(int value, boolean observedChange) { - if (value != 0 || observedChange) { - mUsageTracker.trackUsage(); - } if (mListening) { handleRefreshState(value); } } }; - 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(); - } - mUsageTracker.setListening(true); - mSetting.setListening(true); - } - - @Override - protected void handleDestroy() { - super.handleDestroy(); - mUsageTracker.setListening(false); - mSetting.setListening(false); } @Override @@ -76,7 +63,11 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { @Override public void setListening(boolean listening) { + if (mListening == listening) { + return; + } mListening = listening; + mSetting.setListening(mListening); } @Override @@ -95,22 +86,14 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> { @Override protected void handleLongClick() { - if (mState.value) return; // don't allow usage reset if inversion is active - final String title = mContext.getString(R.string.quick_settings_reset_confirmation_title, - mState.label); - mUsageTracker.showResetConfirmation(title, new Runnable() { - @Override - public void run() { - refreshState(); - } - }); + mHost.startActivityDismissingKeyguard(ACCESSIBILITY_SETTINGS); } @Override protected void handleUpdateState(BooleanState state, Object arg) { final int value = arg instanceof Integer ? (Integer) arg : mSetting.getValue(); final boolean enabled = value != 0; - state.visible = enabled || mUsageTracker.isRecentlyUsed(); + state.visible = true; state.value = enabled; state.label = mContext.getString(R.string.quick_settings_inversion_label); state.icon = enabled ? mEnable : mDisable; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java new file mode 100644 index 0000000..85790d1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.widget.ImageView; + +import com.android.systemui.R; +import com.android.systemui.qs.QSTile; +import com.android.systemui.qs.QSTileView; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +public class CompassTile extends QSTile<QSTile.BooleanState> implements SensorEventListener { + private final static float ALPHA = 0.97f; + + private boolean mActive = false; + private boolean mListening = false; + + private SensorManager mSensorManager; + private Sensor mAccelerationSensor; + private Sensor mGeomagneticFieldSensor; + + private float[] mAcceleration; + private float[] mGeomagnetic; + + private ImageView mImage; + private boolean mListeningSensors; + + public CompassTile(Host host) { + super(host); + mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + mAccelerationSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mGeomagneticFieldSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleDestroy() { + super.handleDestroy(); + setListeningSensors(false); + mSensorManager = null; + mImage = null; + } + + @Override + public QSTileView createTileView(Context context) { + QSTileView tileView = super.createTileView(context); + mImage = (ImageView) tileView.findViewById(android.R.id.icon); + return tileView; + } + + @Override + protected void handleClick() { + mActive = !mActive; + refreshState(); + setListeningSensors(mActive); + } + + private void setListeningSensors(boolean listening) { + if (listening == mListeningSensors) return; + mListeningSensors = listening; + if (mListeningSensors) { + mSensorManager.registerListener( + this, mAccelerationSensor, SensorManager.SENSOR_DELAY_GAME); + mSensorManager.registerListener( + this, mGeomagneticFieldSensor, SensorManager.SENSOR_DELAY_GAME); + } else { + mSensorManager.unregisterListener(this); + } + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + Float degrees = arg == null ? 0 :(float) arg; + + state.visible = true; + state.value = mActive && mListening; + + if (state.value) { + state.icon = ResourceIcon.get(R.drawable.ic_qs_compass_on); + if (arg != null) { + state.label = formatValueWithCardinalDirection(degrees); + + float target = 360 - degrees; + float relative = target - mImage.getRotation(); + if (relative > 180) relative -= 360; + + mImage.setRotation(mImage.getRotation() + relative / 2); + + } else { + state.label = mContext.getString(R.string.quick_settings_compass_init); + mImage.setRotation(0); + } + } else { + state.icon = ResourceIcon.get(R.drawable.ic_qs_compass_off); + state.label = mContext.getString(R.string.quick_settings_compass_label); + mImage.setRotation(0); + } + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_COMPASS; + } + + @Override + public void setListening(boolean listening) { + // setListening might get called multiple times with the same value, we check for it + // in setListeningSensors + mListening = listening; + setListeningSensors(mListening && mActive); + } + + private String formatValueWithCardinalDirection(float degree) { + int cardinalDirectionIndex = (int) (Math.floor(((degree - 22.5) % 360) / 45) + 1) % 8; + String[] cardinalDirections = mContext.getResources().getStringArray( + R.array.cardinal_directions); + + return mContext.getString(R.string.quick_settings_compass_value, degree, + cardinalDirections[cardinalDirectionIndex]); + } + + @Override + public void onSensorChanged(SensorEvent event) { + float[] values; + if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + if (mAcceleration == null) { + mAcceleration = event.values.clone(); + } + + values = mAcceleration; + } else { + // Magnetic field sensor + if (mGeomagnetic == null) { + mGeomagnetic = event.values.clone(); + } + + values = mGeomagnetic; + } + + for (int i = 0; i < 3; i++) { + values[i] = ALPHA * values[i] + (1 - ALPHA) * event.values[i]; + } + + if (!mActive || !mListeningSensors || mAcceleration == null || mGeomagnetic == null) { + // Nothing to do at this moment + return; + } + + float R[] = new float[9]; + float I[] = new float[9]; + if (!SensorManager.getRotationMatrix(R, I, mAcceleration, mGeomagnetic)) { + // Rotation matrix couldn't be calculated + return; + } + + // Get the current orientation + float[] orientation = new float[3]; + SensorManager.getOrientation(R, orientation); + + // Convert azimuth to degrees + Float newDegree = Float.valueOf((float) Math.toDegrees(orientation[0])); + newDegree = (newDegree + 360) % 360; + + refreshState(newDegree); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // noop + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CustomQSTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CustomQSTile.java new file mode 100644 index 0000000..40c7184 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CustomQSTile.java @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.ThemeConfig; +import android.net.Uri; +import android.os.Process; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.RemoteViews; +import android.widget.TextView; + +import com.android.systemui.qs.QSDetailItemsGrid; +import com.android.systemui.qs.QSDetailItemsList; +import cyanogenmod.app.CustomTile; +import cyanogenmod.app.StatusBarPanelCustomTile; + +import com.android.systemui.R; +import com.android.systemui.qs.QSTile; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +import java.util.Arrays; + +public class CustomQSTile extends QSTile<QSTile.State> { + + private static final String HIDDEN_TILES_PREF_NAME = "user_hidden_qs_tiles"; + + private CustomTile.ExpandedStyle mExpandedStyle; + private PendingIntent mOnClick; + private PendingIntent mOnLongClick; + private Uri mOnClickUri; + private int mCurrentUserId; + private StatusBarPanelCustomTile mTile; + private CustomQSDetailAdapter mDetailAdapter; + private boolean mCollapsePanel; + private boolean mUserRemoved; + private String mPersistedPlaceHolderKey; + + public CustomQSTile(Host host, String persistedSpec) { + super(host); + mTile = null; + mPersistedPlaceHolderKey = persistedSpec; + } + + public CustomQSTile(Host host, StatusBarPanelCustomTile tile) { + super(host); + mTile = tile; + mUserRemoved = getIsUserRemovedPersisted(); + } + + private String getPersistableKey() { + if (mPersistedPlaceHolderKey != null) { + return mPersistedPlaceHolderKey; + } else { + return getTile().persistableKey(); + } + } + + private boolean getIsUserRemovedPersisted() { + return getCustomQSTilePrefs(mContext).getBoolean(getPersistableKey(), false); + } + + public boolean isUserRemoved() { + return mUserRemoved; + } + + public void setUserRemoved(boolean removed) { + if (mUserRemoved != removed) { + if (removed) { + getCustomQSTilePrefs(mContext).edit().putBoolean(getPersistableKey(), true).apply(); + } else { + getCustomQSTilePrefs(mContext).edit().remove(getPersistableKey()).apply(); + } + mUserRemoved = removed; + refreshState(); + } + } + + public static SharedPreferences getCustomQSTilePrefs(Context context) { + return context.getSharedPreferences(HIDDEN_TILES_PREF_NAME, Context.MODE_PRIVATE); + } + + @Override + public DetailAdapter getDetailAdapter() { + return mDetailAdapter; + } + + @Override + public void setListening(boolean listening) { + } + + @Override + protected State newTileState() { + return new State(); + } + + @Override + protected void handleUserSwitch(int newUserId) { + super.handleUserSwitch(newUserId); + mCurrentUserId = newUserId; + } + + public void update(StatusBarPanelCustomTile customTile) { + refreshState(customTile); + } + + @Override + protected void handleLongClick() { + if (mOnLongClick != null) { + if (mOnLongClick.isActivity()) { + getHost().collapsePanels(); + } + try { + mOnLongClick.send(); + } catch (Throwable e) { + Log.w(TAG, "Error sending long click intent", e); + } + } else if (mExpandedStyle == null) { + showDetail(true); + } + } + + @Override + protected void handleClick() { + try { + if (mExpandedStyle != null && + mExpandedStyle.getStyle() != CustomTile.ExpandedStyle.NO_STYLE) { + showDetail(true); + return; + } + if (mCollapsePanel) { + mHost.collapsePanels(); + } + if (mOnClick != null) { + mOnClick.send(); + } else if (mOnClickUri != null) { + mHost.collapsePanels(); + final Intent intent = new Intent().setData(mOnClickUri); + mContext.sendBroadcastAsUser(intent, new UserHandle(mCurrentUserId)); + } + } catch (Throwable t) { + Log.w(TAG, "Error sending click intent", t); + } + } + + public StatusBarPanelCustomTile getTile() { + return mTile; + } + + @Override + protected void handleUpdateState(State state, Object arg) { + if (arg instanceof StatusBarPanelCustomTile) { + mTile = (StatusBarPanelCustomTile) arg; + mPersistedPlaceHolderKey = null; + mUserRemoved = getIsUserRemovedPersisted(); + } + if (mTile == null) { + state.visible = false; + // nothing to show, it's a place holder for now + return; + } + final CustomTile customTile = mTile.getCustomTile(); + state.contentDescription = customTile.contentDescription; + state.label = customTile.label; + state.visible = !mUserRemoved; + final int iconId = customTile.icon; + if (iconId != 0 && (customTile.remoteIcon == null)) { + final String iconPackage = mTile.getResPkg(); + if (!TextUtils.isEmpty(iconPackage)) { + state.icon = new ExternalIcon(iconPackage, iconId); + } + } else { + state.icon = new ExternalBitmapIcon(customTile.remoteIcon); + } + mOnClick = customTile.onClick; + mOnLongClick = customTile.onLongClick; + mOnClickUri = customTile.onClickUri; + mExpandedStyle = customTile.expandedStyle; + mCollapsePanel = customTile.collapsePanel; + mDetailAdapter = new CustomQSDetailAdapter(); + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_CUSTOM_QS; + } + + private boolean isDynamicTile() { + return mTile.getPackage().equals(mContext.getPackageName()) + || mTile.getUid() == Process.SYSTEM_UID; + } + + private class CustomQSDetailAdapter implements DetailAdapter, AdapterView.OnItemClickListener, + QSDetailItemsGrid.QSDetailItemsGridAdapter.OnPseudoGriditemClickListener { + private QSDetailItemsList.QSCustomDetailListAdapter mListAdapter; + private QSDetailItemsGrid.QSDetailItemsGridAdapter mGridAdapter; + + public int getTitle() { + if (isDynamicTile()) { + return mContext.getResources().getIdentifier( + String.format("dynamic_qs_tile_%s_label", mTile.getTag()), + "string", mContext.getPackageName()); + } + return R.string.quick_settings_custom_tile_detail_title; + } + + @Override + public Boolean getToggleState() { + return null; + } + + + @Override + public Intent getSettingsIntent() { + return mTile.getCustomTile().onSettingsClick; + } + + @Override + public StatusBarPanelCustomTile getCustomTile() { + return mTile; + } + + @Override + public void setToggleState(boolean state) { + // noop + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_CUSTOM_QS_DETAIL; + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + View rootView = null; + if (mExpandedStyle == null) { + rootView = (LinearLayout) LayoutInflater.from(context) + .inflate(R.layout.qs_custom_detail, parent, false); + ImageView imageView = (ImageView) + rootView.findViewById(R.id.custom_qs_tile_icon); + TextView customTileTitle = (TextView) + rootView.findViewById(R.id.custom_qs_tile_title); + TextView customTilePkg = (TextView) rootView + .findViewById(R.id.custom_qs_tile_package); + TextView customTileContentDesc = (TextView) rootView + .findViewById(R.id.custom_qs_tile_content_description); + // icon is cached in state, fetch it + imageView.setImageDrawable(getState().icon.getDrawable(mContext)); + customTileTitle.setText(mTile.getCustomTile().label); + if (isDynamicTile()) { + customTilePkg.setText(R.string.quick_settings_dynamic_tile_detail_title); + } else { + customTilePkg.setText(mTile.getPackage()); + customTileContentDesc.setText(mTile.getCustomTile().contentDescription); + } + } else { + switch (mExpandedStyle.getStyle()) { + case CustomTile.ExpandedStyle.GRID_STYLE: + rootView = QSDetailItemsGrid.inflate(context, parent, false); + mGridAdapter = ((QSDetailItemsGrid) rootView) + .createAndSetAdapter(mTile.getPackage(), + mExpandedStyle.getExpandedItems()); + mGridAdapter.setOnPseudoGridItemClickListener(this); + break; + case CustomTile.ExpandedStyle.REMOTE_STYLE: + rootView = (LinearLayout) LayoutInflater.from(context) + .inflate(R.layout.qs_custom_detail_remote, parent, false); + RemoteViews remoteViews = mExpandedStyle.getContentViews(); + if (remoteViews != null) { + View localView = mTile.getCustomTile().expandedStyle.getContentViews() + .apply(context, (ViewGroup) rootView, + mHost.getOnClickHandler(), getThemePackageName()); + ((LinearLayout) rootView).addView(localView); + } else { + Log.d(TAG, "Unable to add null remoteview for " + mTile.getOpPkg()); + } + break; + case CustomTile.ExpandedStyle.LIST_STYLE: + default: + rootView = QSDetailItemsList.convertOrInflate(context, convertView, parent); + ListView listView = ((QSDetailItemsList) rootView).getListView(); + listView.setDivider(null); + listView.setOnItemClickListener(this); + listView.setAdapter(mListAdapter = + new QSDetailItemsList.QSCustomDetailListAdapter(mTile.getPackage(), + context, Arrays.asList(mExpandedStyle.getExpandedItems()))); + break; + } + } + return rootView; + } + + @Override + public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) { + CustomTile.ExpandedItem item = mListAdapter.getItem(position); + sendEvent(item.onClickPendingIntent); + } + + @Override + public void onPsuedoGridItemClick(View view, CustomTile.ExpandedItem item) { + sendEvent(item.onClickPendingIntent); + } + + private void sendEvent(PendingIntent intent) { + try { + if (intent.isActivity()) { + mHost.collapsePanels(); + } + intent.send(); + } catch (PendingIntent.CanceledException e) { + // + } + } + + private String getThemePackageName() { + final Configuration config = mContext.getResources().getConfiguration(); + final ThemeConfig themeConfig = config != null ? config.themeConfig : null; + return themeConfig != null ? themeConfig.getOverlayForStatusBar() : null; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java index 781ab1c..0d43f8c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +38,7 @@ import com.android.systemui.SysUIToast; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.volume.ZenModePanel; +import cyanogenmod.app.StatusBarPanelCustomTile; /** Quick settings tile: Do not disturb **/ public class DndTile extends QSTile<QSTile.BooleanState> { @@ -121,6 +123,16 @@ public class DndTile extends QSTile<QSTile.BooleanState> { } @Override + protected void handleSecondaryClick() { + handleClick(); + } + + @Override + public void handleLongClick() { + mHost.startActivityDismissingKeyguard(ZEN_SETTINGS); + } + + @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; @@ -129,7 +141,7 @@ public class DndTile extends QSTile<QSTile.BooleanState> { state.visible = isVisible(mContext); switch (zen) { case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: - state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on); + state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on_priority); state.label = mContext.getString(R.string.quick_settings_dnd_priority_label); state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_dnd_priority_on); @@ -188,6 +200,11 @@ public class DndTile extends QSTile<QSTile.BooleanState> { } } + @Override + public boolean hasDualTargetsDetails() { + return true; + } + private final OnSharedPreferenceChangeListener mPrefListener = new OnSharedPreferenceChangeListener() { @Override @@ -233,6 +250,11 @@ public class DndTile extends QSTile<QSTile.BooleanState> { } @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override public void setToggleState(boolean state) { MetricsLogger.action(mContext, MetricsLogger.QS_DND_TOGGLE, state); if (!state) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/EditTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/EditTile.java new file mode 100644 index 0000000..7173786 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/EditTile.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.android.systemui.R; +import com.android.systemui.qs.QSTile; +import com.android.systemui.statusbar.policy.KeyguardMonitor; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +public class EditTile extends QSTile<QSTile.BooleanState> implements KeyguardMonitor.Callback { + + private boolean mListening; + + public EditTile(Host host) { + super(host); + refreshState(); + } + + @Override + protected void handleDestroy() { + super.handleDestroy(); + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleClick() { + getHost().setEditing(!mState.value); + refreshState(!mState.value); + } + + @Override + protected void handleLongClick() { + getHost().goToSettingsPage(); + refreshState(true); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + final boolean showing = getHost().getKeyguardMonitor().isShowing(); + final boolean secure = getHost().getKeyguardMonitor().isSecure(); + state.visible = !showing || !secure; + state.enabled = !showing; + state.label = mContext.getString(R.string.quick_settings_edit_label); + + if (arg instanceof Boolean) { + state.value = (boolean) arg; + } else { + state.value = getHost().isEditing(); + } + state.icon = ResourceIcon.get(R.drawable.ic_qs_edit_tiles); + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_EDIT; + } + + @Override + protected String composeChangeAnnouncement() { + // TODO + return null; + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + if (mListening) { + mHost.getKeyguardMonitor().addCallback(this); + } else { + mHost.getKeyguardMonitor().removeCallback(this); + } + refreshState(); + } + + @Override + public void onKeyguardChanged() { + refreshState(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HeadsUpTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HeadsUpTile.java new file mode 100644 index 0000000..d30bd91 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HeadsUpTile.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; +import android.content.Intent; +import android.provider.Settings; +import android.provider.Settings.Global; + +import com.android.systemui.qs.GlobalSetting; +import com.android.systemui.qs.QSTile; +import com.android.systemui.R; + +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +/** Quick settings tile: Heads up **/ +public class HeadsUpTile extends QSTile<QSTile.BooleanState> { + + private static final Intent NOTIFICATION_SETTINGS = + new Intent("android.settings.NOTIFICATION_MANAGER"); + + private final GlobalSetting mSetting; + + public HeadsUpTile(Host host) { + super(host); + + mSetting = new GlobalSetting(mContext, mHandler, Global.HEADS_UP_NOTIFICATIONS_ENABLED) { + @Override + protected void handleValueChanged(int value) { + handleRefreshState(value); + } + }; + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleClick() { + setEnabled(!mState.value); + refreshState(); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(NOTIFICATION_SETTINGS); + } + + private void setEnabled(boolean enabled) { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, + enabled ? 1 : 0); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + final int value = arg instanceof Integer ? (Integer)arg : mSetting.getValue(); + final boolean headsUp = value != 0; + state.value = headsUp; + state.visible = true; + state.label = mContext.getString(R.string.quick_settings_heads_up_label); + if (headsUp) { + state.icon = ResourceIcon.get(R.drawable.ic_qs_heads_up_on); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_heads_up_on); + } else { + state.icon = ResourceIcon.get(R.drawable.ic_qs_heads_up_off); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_heads_up_off); + } + } + + @Override + protected String composeChangeAnnouncement() { + if (mState.value) { + return mContext.getString(R.string.accessibility_quick_settings_heads_up_changed_on); + } else { + return mContext.getString(R.string.accessibility_quick_settings_heads_up_changed_off); + } + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_HEADS_UP; + } + + @Override + public void setListening(boolean listening) { + // Do nothing + } +} 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 7b83e6a..6080358 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,37 +18,44 @@ package com.android.systemui.qs.tiles; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.wifi.WifiDevice; +import android.provider.Settings; 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.UsageTracker; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.HotspotController; +import java.util.List; + /** Quick settings tile: Hotspot **/ public class HotspotTile extends QSTile<QSTile.BooleanState> { + + private static final Intent TETHER_SETTINGS = new Intent().setComponent(new ComponentName( + "com.android.settings", "com.android.settings.TetherSettings")); + private final AnimationIcon mEnable = new AnimationIcon(R.drawable.ic_hotspot_enable_animation); private final AnimationIcon mDisable = new AnimationIcon(R.drawable.ic_hotspot_disable_animation); private final HotspotController mController; private final Callback mCallback = new Callback(); - private final UsageTracker mUsageTracker; + private final ConnectivityManager mConnectivityManager; + private boolean mListening; + private int mNumConnectedClients = 0; public HotspotTile(Host host) { super(host); mController = host.getHotspotController(); - mUsageTracker = newUsageTracker(host.getContext()); - mUsageTracker.setListening(true); - } - - @Override - protected void handleDestroy() { - super.handleDestroy(); - mUsageTracker.setListening(false); + mConnectivityManager = host.getContext().getSystemService(ConnectivityManager.class); } @Override @@ -57,15 +65,31 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { @Override public void setListening(boolean listening) { + if (mListening == listening) return; if (listening) { mController.addCallback(mCallback); + mContext.registerReceiver(mTetherConnectStateChangedReceiver, + new IntentFilter(ConnectivityManager.TETHER_CONNECT_STATE_CHANGED)); } else { mController.removeCallback(mCallback); + mContext.unregisterReceiver(mTetherConnectStateChangedReceiver); } + mListening = listening; } @Override protected void handleClick() { + boolean airplaneMode = (Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 0) == 1); + if (airplaneMode) { + SystemUIDialog d = new SystemUIDialog(mContext); + d.setTitle(R.string.quick_settings_hotspot_label); + d.setMessage(R.string.hotspot_apm_message); + d.setPositiveButton(com.android.internal.R.string.ok, null); + d.setShowForAllUsers(true); + d.show(); + return; + } final boolean isEnabled = (Boolean) mState.value; MetricsLogger.action(mContext, getMetricsCategory(), !isEnabled); mController.setHotspotEnabled(!isEnabled); @@ -75,27 +99,25 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { @Override protected void handleLongClick() { - if (mState.value) return; // don't allow usage reset if hotspot is active - final String title = mContext.getString(R.string.quick_settings_reset_confirmation_title, - mState.label); - mUsageTracker.showResetConfirmation(title, new Runnable() { - @Override - public void run() { - refreshState(); - } - }); + mHost.startActivityDismissingKeyguard(TETHER_SETTINGS); } @Override protected void handleUpdateState(BooleanState state, Object arg) { - state.visible = mController.isHotspotSupported() && mUsageTracker.isRecentlyUsed(); - state.label = mContext.getString(R.string.quick_settings_hotspot_label); + state.visible = mController.isHotspotSupported(); if (arg instanceof Boolean) { state.value = (boolean) arg; } else { state.value = mController.isHotspotEnabled(); } + if (state.visible && state.value) { + state.label = mContext.getResources().getQuantityString( + R.plurals.wifi_hotspot_connected_clients_label, mNumConnectedClients, + mNumConnectedClients); + } else { + state.label = mContext.getString(R.string.quick_settings_hotspot_label); + } state.icon = state.visible && state.value ? mEnable : mDisable; } @@ -113,10 +135,14 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { } } - private static UsageTracker newUsageTracker(Context context) { - return new UsageTracker(context, Prefs.Key.HOTSPOT_TILE_LAST_USED, HotspotTile.class, - R.integer.days_to_show_hotspot_tile); - } + private BroadcastReceiver mTetherConnectStateChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final List<WifiDevice> clients = mConnectivityManager.getTetherConnectedSta(); + mNumConnectedClients = clients != null ? clients.size() : 0; + refreshState(); + } + }; private final class Callback implements HotspotController.Callback { @Override @@ -124,20 +150,4 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> { refreshState(enabled); } }; - - /** - * This will catch broadcasts for changes in hotspot state so we can show - * the hotspot tile for a number of days after use. - */ - public static class APChangedReceiver extends BroadcastReceiver { - private UsageTracker mUsageTracker; - - @Override - public void onReceive(Context context, Intent intent) { - if (mUsageTracker == null) { - mUsageTracker = newUsageTracker(context); - } - mUsageTracker.trackUsage(); - } - } } 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 e6fade4..3598bba 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,32 +17,69 @@ package com.android.systemui.qs.tiles; +import com.android.internal.logging.MetricsConstants; import com.android.internal.logging.MetricsLogger; +import android.content.Context; +import android.content.Intent; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; +import android.widget.ListView; + import com.android.systemui.R; +import com.android.systemui.qs.QSDetailItems; +import com.android.systemui.qs.QSDetailItemsList; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.LocationController; import com.android.systemui.statusbar.policy.LocationController.LocationSettingsChangeCallback; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import cyanogenmod.app.StatusBarPanelCustomTile; + /** Quick settings tile: Location **/ public class LocationTile extends QSTile<QSTile.BooleanState> { + private static final Intent LOCATION_SETTINGS_INTENT + = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + public static final Integer[] LOCATION_SETTINGS = new Integer[]{ + Settings.Secure.LOCATION_MODE_BATTERY_SAVING, + Settings.Secure.LOCATION_MODE_SENSORS_ONLY, + Settings.Secure.LOCATION_MODE_HIGH_ACCURACY + }; + private final AnimationIcon mEnable = new AnimationIcon(R.drawable.ic_signal_location_enable_animation); private final AnimationIcon mDisable = new AnimationIcon(R.drawable.ic_signal_location_disable_animation); private final LocationController mController; + private final LocationDetailAdapter mDetailAdapter; private final KeyguardMonitor mKeyguard; private final Callback mCallback = new Callback(); + private final List<Integer> mLocationList = new ArrayList<Integer>(); public LocationTile(Host host) { super(host); mController = host.getLocationController(); + mDetailAdapter = new LocationDetailAdapter(); mKeyguard = host.getKeyguardMonitor(); } @Override + public DetailAdapter getDetailAdapter() { + return mDetailAdapter; + } + + @Override protected BooleanState newTileState() { return new BooleanState(); } @@ -59,32 +97,74 @@ 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); + if(mController.isAdvancedSettingsEnabled()) { + showDetail(true); + } else { + boolean wasEnabled = mController.isLocationEnabled(); + mController.setLocationEnabled(!wasEnabled); + MetricsLogger.action(mContext, getMetricsCategory(), !wasEnabled); + refreshState(); + } + mEnable.setAllowAnimation(true); mDisable.setAllowAnimation(true); } @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(LOCATION_SETTINGS_INTENT); + } + + @Override protected void handleUpdateState(BooleanState state, Object arg) { - final boolean locationEnabled = mController.isLocationEnabled(); + final int currentState = mController.getLocationCurrentState(); // Work around for bug 15916487: don't show location tile on top of lock screen. After the // bug is fixed, this should be reverted to only hiding it on secure lock screens: // state.visible = !(mKeyguard.isSecure() && mKeyguard.isShowing()); state.visible = !mKeyguard.isShowing(); - state.value = locationEnabled; - if (locationEnabled) { - state.icon = mEnable; - state.label = mContext.getString(R.string.quick_settings_location_label); - state.contentDescription = mContext.getString( - R.string.accessibility_quick_settings_location_on); - } else { - state.icon = mDisable; - state.label = mContext.getString(R.string.quick_settings_location_label); - state.contentDescription = mContext.getString( - R.string.accessibility_quick_settings_location_off); + state.label = mContext.getString(getStateLabelRes(currentState)); + + switch (currentState) { + case Settings.Secure.LOCATION_MODE_OFF: + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_location_off); + state.icon = mDisable; + break; + case Settings.Secure.LOCATION_MODE_BATTERY_SAVING: + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_location_battery_saving); + state.icon = ResourceIcon.get(R.drawable.ic_qs_location_battery_saving); + break; + case Settings.Secure.LOCATION_MODE_SENSORS_ONLY: + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_location_gps_only); + state.icon = mEnable; + break; + case Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_location_high_accuracy); + state.icon = mEnable; + break; + default: + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_location_on); + state.icon = mEnable; + } + } + + private int getStateLabelRes(int currentState) { + switch (currentState) { + case Settings.Secure.LOCATION_MODE_OFF: + return R.string.quick_settings_location_off_label; + case Settings.Secure.LOCATION_MODE_BATTERY_SAVING: + return R.string.quick_settings_location_battery_saving_label; + case Settings.Secure.LOCATION_MODE_SENSORS_ONLY: + return R.string.quick_settings_location_gps_only_label; + case Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: + return R.string.quick_settings_location_high_accuracy_label; + default: + return R.string.quick_settings_location_label; } } @@ -95,10 +175,22 @@ public class LocationTile extends QSTile<QSTile.BooleanState> { @Override protected String composeChangeAnnouncement() { - if (mState.value) { - return mContext.getString(R.string.accessibility_quick_settings_location_changed_on); - } else { - return mContext.getString(R.string.accessibility_quick_settings_location_changed_off); + switch (mController.getLocationCurrentState()) { + case Settings.Secure.LOCATION_MODE_OFF: + return mContext.getString( + R.string.accessibility_quick_settings_location_changed_off); + case Settings.Secure.LOCATION_MODE_BATTERY_SAVING: + return mContext.getString( + R.string.accessibility_quick_settings_location_changed_battery_saving); + case Settings.Secure.LOCATION_MODE_SENSORS_ONLY: + return mContext.getString( + R.string.accessibility_quick_settings_location_changed_gps_only); + case Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: + return mContext.getString( + R.string.accessibility_quick_settings_location_changed_high_accuracy); + default: + return mContext.getString( + R.string.accessibility_quick_settings_location_changed_on); } } @@ -114,4 +206,90 @@ public class LocationTile extends QSTile<QSTile.BooleanState> { refreshState(); } }; + + private class AdvancedLocationAdapter extends ArrayAdapter<Integer> { + public AdvancedLocationAdapter(Context context) { + super(context, android.R.layout.simple_list_item_single_choice, mLocationList); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(mContext); + CheckedTextView label = (CheckedTextView) inflater.inflate( + android.R.layout.simple_list_item_single_choice, parent, false); + label.setText(getStateLabelRes(getItem(position))); + return label; + } + } + + private class LocationDetailAdapter implements DetailAdapter, AdapterView.OnItemClickListener { + + private AdvancedLocationAdapter mAdapter; + private QSDetailItemsList mDetails; + + @Override + public int getTitle() { + return R.string.quick_settings_location_detail_title; + } + + @Override + public Boolean getToggleState() { + boolean state = mController.getLocationCurrentState() + != Settings.Secure.LOCATION_MODE_OFF; + rebuildLocationList(state); + return state; + } + + @Override + public Intent getSettingsIntent() { + return LOCATION_SETTINGS_INTENT; + } + + @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override + public void setToggleState(boolean state) { + mController.setLocationEnabled(state); + rebuildLocationList(state); + fireToggleStateChanged(state); + } + + @Override + public int getMetricsCategory() { + return MetricsLogger.QS_LOCATION; + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + mDetails = QSDetailItemsList.convertOrInflate(context, convertView, parent); + mDetails.setEmptyState(R.drawable.ic_qs_location_off, + R.string.accessibility_quick_settings_location_off); + mAdapter = new LocationTile.AdvancedLocationAdapter(context); + mDetails.setAdapter(mAdapter); + + final ListView list = mDetails.getListView(); + list.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); + list.setOnItemClickListener(this); + + return mDetails; + } + + private void rebuildLocationList(boolean populate) { + mLocationList.clear(); + if (populate) { + mLocationList.addAll(Arrays.asList(LOCATION_SETTINGS)); + mDetails.getListView().setItemChecked(mAdapter.getPosition( + mController.getLocationCurrentState()), true); + } + mAdapter.notifyDataSetChanged(); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + mController.setLocationMode((Integer) parent.getItemAtPosition(position)); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LockscreenToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LockscreenToggleTile.java new file mode 100644 index 0000000..a147d30 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LockscreenToggleTile.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2015-2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Intent; +import android.os.UserHandle; +import com.android.systemui.R; +import com.android.systemui.SystemUIApplication; +import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.qs.QSTile; +import com.android.systemui.statusbar.policy.KeyguardMonitor; + +import cyanogenmod.app.Profile; +import cyanogenmod.app.ProfileManager; +import cyanogenmod.providers.CMSettings; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +public class LockscreenToggleTile extends QSTile<QSTile.BooleanState> + implements KeyguardMonitor.Callback { + + private static final Intent LOCK_SCREEN_SETTINGS = + new Intent("android.settings.LOCK_SCREEN_SETTINGS"); + + private KeyguardMonitor mKeyguard; + private boolean mListening; + + private KeyguardViewMediator.LockscreenEnabledSettingsObserver mSettingsObserver; + + public LockscreenToggleTile(Host host) { + super(host); + + mKeyguard = host.getKeyguardMonitor(); + + mSettingsObserver = new KeyguardViewMediator.LockscreenEnabledSettingsObserver(mContext, + mUiHandler) { + + @Override + public void update() { + refreshState(); + } + }; + + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) { + return; + } + mListening = listening; + if (listening) { + mSettingsObserver.observe(); + mKeyguard.addCallback(this); + refreshState(); + } else { + mSettingsObserver.unobserve(); + mKeyguard.removeCallback(this); + } + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleClick() { + final boolean newState = !getState().value; + setPersistedState(newState); + refreshState(newState); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(LOCK_SCREEN_SETTINGS); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + KeyguardViewMediator mediator = ((SystemUIApplication) + mContext.getApplicationContext()).getComponent(KeyguardViewMediator.class); + + if (mediator == null) { + state.visible = false; + state.value = false; + state.enabled = false; + } else { + final boolean lockscreenEnforced = mediator.lockscreenEnforcedByDevicePolicy(); + final boolean lockscreenEnabled = lockscreenEnforced || + arg != null ? (Boolean) arg : mediator.getKeyguardEnabledInternal(); + + state.visible = mediator.isKeyguardBound(); + + if (mediator.isProfileDisablingKeyguard()) { + state.value = false; + state.enabled = false; + state.label = mContext.getString( + R.string.quick_settings_lockscreen_label_locked_by_profile); + } else if (lockscreenEnforced) { + state.value = true; + state.enabled = false; + state.label = mContext.getString( + R.string.quick_settings_lockscreen_label_enforced); + } else { + state.value = lockscreenEnabled; + state.enabled = !mKeyguard.isShowing() || !mKeyguard.isSecure(); + + state.label = mContext.getString(R.string.quick_settings_lockscreen_label); + } + // update icon + if (lockscreenEnabled) { + state.icon = ResourceIcon.get(R.drawable.ic_qs_lock_screen_on); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_lock_screen_on); + } else { + state.icon = ResourceIcon.get(R.drawable.ic_qs_lock_screen_off); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_lock_screen_off); + } + } + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_LOCKSCREEN_TOGGLE; + } + + @Override + protected String composeChangeAnnouncement() { + if (mState.value) { + return mContext.getString( + R.string.accessibility_quick_settings_lock_screen_changed_on); + } else { + return mContext.getString( + R.string.accessibility_quick_settings_lock_screen_changed_off); + } + } + + @Override + public void onKeyguardChanged() { + refreshState(); + } + + private void setPersistedState(boolean enabled) { + CMSettings.Secure.putIntForUser(mContext.getContentResolver(), + CMSettings.Secure.LOCKSCREEN_INTERNALLY_ENABLED, + enabled ? 1 : 0, UserHandle.USER_CURRENT); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java new file mode 100644 index 0000000..a5ffd23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nfc.NfcAdapter; + +import android.util.Log; +import com.android.systemui.R; +import com.android.systemui.qs.QSTile; +import org.cyanogenmod.internal.logging.CMMetricsLogger; +import org.cyanogenmod.internal.util.QSUtils; + +public class NfcTile extends QSTile<QSTile.BooleanState> { + + private boolean mListening; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + refreshState(); + } + }; + private final boolean mSupportsNfc; + + public NfcTile(Host host) { + super(host); + mSupportsNfc = QSUtils.deviceSupportsNfc(mContext); + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleClick() { + boolean newState = !getState().value; + setState(newState); + refreshState(); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(new Intent("android.settings.NFC_SETTINGS")); + } + + private void setState(boolean on) { + try { + NfcAdapter nfcAdapter = NfcAdapter.getNfcAdapter(mContext); + if (nfcAdapter == null) { + Log.e(TAG, "tried to set NFC state, but no NFC adapter was found"); + return; + } + if (on) { + nfcAdapter.enable(); + } else { + nfcAdapter.disable(); + } + } catch (UnsupportedOperationException e) { + // ignore + } + } + + private int getNfcAdapterState() { + try { + NfcAdapter nfcAdapter = NfcAdapter.getNfcAdapter(mContext); + if (nfcAdapter == null) { + Log.e(TAG, "tried to get NFC state, but no NFC adapter was found"); + return NfcAdapter.STATE_OFF; + } + return nfcAdapter.getAdapterState(); + } catch (UnsupportedOperationException e) { + // ignore + return NfcAdapter.STATE_OFF; + } + } + + /** + * Helper method to encapsulate intermediate states (turning off/on) to help determine whether + * the adapter will be on or off. + * @param nfcState The current NFC adapter state. + * @return boolean representing what state the adapter is/will be in + */ + private static boolean isEnabled(int nfcState) { + switch (nfcState) { + case NfcAdapter.STATE_TURNING_ON: + case NfcAdapter.STATE_ON: + return true; + case NfcAdapter.STATE_TURNING_OFF: + case NfcAdapter.STATE_OFF: + default: + return false; + } + } + + /** + * Helper method to determine intermediate states + * @param nfcState The current NFC adapter state. + * @return boolean representing if the adapter is in an intermediate state + */ + private static boolean isEnablingDisabling(int nfcState) { + switch (nfcState) { + case NfcAdapter.STATE_TURNING_OFF: + case NfcAdapter.STATE_TURNING_ON: + return true; + default: + return false; + } + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.visible = mSupportsNfc; + final int nfcState = getNfcAdapterState(); + state.value = mSupportsNfc && isEnabled(nfcState); + state.enabled = mSupportsNfc && !isEnablingDisabling(nfcState); + + state.icon = ResourceIcon.get(state.value ? + R.drawable.ic_qs_nfc_on : R.drawable.ic_qs_nfc_off); + state.label = mContext.getString(R.string.quick_settings_nfc_label); + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_NFC; + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + if (listening) { + mContext.registerReceiver(mReceiver, + new IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)); + refreshState(); + } else { + mContext.unregisterReceiver(mReceiver); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/PerfProfileTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/PerfProfileTile.java new file mode 100644 index 0000000..4863683 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/PerfProfileTile.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import com.android.systemui.R; +import com.android.systemui.qs.QSDetailItemsList; +import com.android.systemui.qs.QSTile; + +import org.cyanogenmod.internal.logging.CMMetricsLogger; +import org.cyanogenmod.internal.util.QSUtils; + +import cyanogenmod.app.StatusBarPanelCustomTile; +import cyanogenmod.power.PerformanceManager; +import cyanogenmod.providers.CMSettings; + +public class PerfProfileTile extends QSTile<PerfProfileTile.ProfileState> { + + private static final Intent BATTERY_SETTINGS = new Intent(Intent.ACTION_POWER_USAGE_SUMMARY); + + private final String[] mEntries; + private final String[] mDescriptionEntries; + private final String[] mAnnouncementEntries; + private final int[] mPerfProfileValues; + private final int mNumPerfProfiles; + private final Icon mIcon; + + private final PowerManager mPm; + private final PerformanceManager mPerformanceManager; + private boolean mListening; + + private PerformanceProfileObserver mObserver; + + public PerfProfileTile(Host host) { + super(host); + mObserver = new PerformanceProfileObserver(mHandler); + mPm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mPerformanceManager = PerformanceManager.getInstance(mContext); + mNumPerfProfiles = mPerformanceManager.getNumberOfProfiles(); + + mPerfProfileValues = new int[mNumPerfProfiles]; + mEntries = new String[mNumPerfProfiles]; + mDescriptionEntries = new String[mNumPerfProfiles]; + mAnnouncementEntries = new String[mNumPerfProfiles]; + + mIcon = ResourceIcon.get(R.drawable.ic_qs_perf_profile); + + // Filter out unsupported profiles + Resources res = mContext.getResources(); + final int[] perfProfileValues = res.getIntArray( + org.cyanogenmod.platform.internal.R.array.perf_profile_values); + final String[] entries = res.getStringArray( + org.cyanogenmod.platform.internal.R.array.perf_profile_entries); + final String[] descriptionEntries = res.getStringArray( + R.array.perf_profile_description); + final String[] announcementEntries = res.getStringArray( + R.array.perf_profile_announcement); + int i = 0; + + for (int j = 0; j < perfProfileValues.length; j++) { + if (perfProfileValues[j] < mNumPerfProfiles) { + mPerfProfileValues[i] = perfProfileValues[j]; + mEntries[i] = entries[j]; + mDescriptionEntries[i] = descriptionEntries[j]; + mAnnouncementEntries[i] = announcementEntries[j]; + i++; + } + } + } + + @Override + protected ProfileState newTileState() { + return new ProfileState(); + } + + @Override + protected void handleClick() { + showDetail(true); + } + + @Override + public DetailAdapter getDetailAdapter() { + return new PerfProfileDetailAdapter(); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(BATTERY_SETTINGS); + } + + @Override + protected void handleUpdateState(ProfileState state, Object arg) { + state.visible = mPerformanceManager.getNumberOfProfiles() > 0; + state.profile = arg == null ? getCurrentProfileIndex() : (Integer) arg; + state.label = mEntries[state.profile]; + state.icon = mIcon; + state.contentDescription = mDescriptionEntries[state.profile]; + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_PERF_PROFILE; + } + + @Override + protected String composeChangeAnnouncement() { + return mAnnouncementEntries[getCurrentProfileIndex()]; + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + if (listening) { + mObserver.startObserving(); + } else { + mObserver.endObserving(); + } + } + + private class PerformanceProfileObserver extends ContentObserver { + public PerformanceProfileObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + refreshState(getCurrentProfileIndex()); + } + + public void startObserving() { + mContext.getContentResolver().registerContentObserver( + CMSettings.Secure.getUriFor(CMSettings.Secure.PERFORMANCE_PROFILE), + false, this); + } + + public void endObserving() { + mContext.getContentResolver().unregisterContentObserver(this); + } + } + + private int getCurrentProfileIndex() { + int index = 0; + int perfProfile = mPerformanceManager.getPowerProfile(); + + int count = mPerfProfileValues.length; + for (int i = 0; i < count; i++) { + if (mPerfProfileValues[i] == perfProfile) { + index = i; + break; + } + } + + return index; + } + + private void changeToProfile(int profileIndex) { + mPerformanceManager.setPowerProfile(mPerfProfileValues[profileIndex]); // content observer will notify + } + + public static class ProfileState extends QSTile.State { + public int profile; + + @Override + public boolean copyTo(State other) { + final ProfileState o = (ProfileState) other; + final boolean changed = profile != o.profile; + return super.copyTo(other) || changed; + } + + @Override + protected StringBuilder toStringBuilder() { + final StringBuilder rt = super.toStringBuilder(); + rt.insert(rt.length() - 1, ",profile=" + profile); + return rt; + } + } + + private class PerfProfileDetailAdapter implements DetailAdapter, + AdapterView.OnItemClickListener { + private QSDetailItemsList mItems; + + @Override + public int getTitle() { + return R.string.quick_settings_performance_profile_detail_title; + } + + @Override + public Boolean getToggleState() { + return null; + } + + @Override + public Intent getSettingsIntent() { + return BATTERY_SETTINGS; + } + + @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override + public void setToggleState(boolean state) { + // noop + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_PERF_PROFILE_DETAIL; + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + mItems = QSDetailItemsList.convertOrInflate(context, convertView, parent); + ArrayAdapter adapter = new ArrayAdapter<String>(mContext, + android.R.layout.simple_list_item_single_choice, mEntries); + ListView listView = mItems.getListView(); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + listView.setAdapter(adapter); + listView.setOnItemClickListener(this); + listView.setDivider(null); + listView.setItemChecked(getCurrentProfileIndex(), true); + return mItems; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + changeToProfile(position); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ProfilesTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ProfilesTile.java new file mode 100644 index 0000000..6f65f6c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ProfilesTile.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.database.ContentObserver; +import android.media.AudioManager; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; +import android.widget.ListView; + +import com.android.systemui.R; +import com.android.systemui.qs.QSDetailItemsList; +import com.android.systemui.qs.QSTile; +import com.android.systemui.statusbar.policy.KeyguardMonitor; + +import cyanogenmod.app.Profile; +import cyanogenmod.app.ProfileManager; +import cyanogenmod.app.StatusBarPanelCustomTile; +import cyanogenmod.providers.CMSettings; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ProfilesTile extends QSTile<QSTile.State> implements KeyguardMonitor.Callback { + + private static final Intent PROFILES_SETTINGS = + new Intent("android.settings.PROFILES_SETTINGS"); + + private boolean mListening; + private ProfilesObserver mObserver; + private ProfileManager mProfileManager; + private QSDetailItemsList mDetails; + private ProfileAdapter mAdapter; + private KeyguardMonitor mKeyguardMonitor; + + public ProfilesTile(Host host) { + super(host); + mProfileManager = ProfileManager.getInstance(mContext); + mObserver = new ProfilesObserver(mHandler); + mKeyguardMonitor = host.getKeyguardMonitor(); + mKeyguardMonitor.addCallback(this); + } + + @Override + protected void handleDestroy() { + mKeyguardMonitor.removeCallback(this); + } + + @Override + protected State newTileState() { + return new State(); + } + + @Override + protected void handleClick() { + showDetail(true); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(PROFILES_SETTINGS); + } + + @Override + protected void handleUpdateState(State state, Object arg) { + state.visible = true; + + + + state.enabled = !mKeyguardMonitor.isShowing() || !mKeyguardMonitor.isSecure(); + if (profilesEnabled()) { + state.icon = ResourceIcon.get(R.drawable.ic_qs_profiles_on); + state.label = mProfileManager.getActiveProfile().getName(); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_profiles, state.label); + } else { + state.icon = ResourceIcon.get(R.drawable.ic_qs_profiles_off); + state.label = mContext.getString(R.string.quick_settings_profiles_off); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_profiles_off); + } + } + + @Override + protected String composeChangeAnnouncement() { + if (profilesEnabled()) { + return mContext.getString(R.string.accessibility_quick_settings_profiles_changed, + mState.label); + } else { + return mContext.getString(R.string.accessibility_quick_settings_profiles_changed_off); + } + } + + private boolean profilesEnabled() { + return CMSettings.System.getInt(mContext.getContentResolver(), + CMSettings.System.SYSTEM_PROFILES_ENABLED, 1) == 1; + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_PROFILES; + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + if (listening) { + mObserver.startObserving(); + final IntentFilter filter = new IntentFilter(); + filter.addAction(ProfileManager.INTENT_ACTION_PROFILE_SELECTED); + filter.addAction(ProfileManager.INTENT_ACTION_PROFILE_UPDATED); + mContext.registerReceiver(mReceiver, filter); + refreshState(); + } else { + mObserver.endObserving(); + mContext.unregisterReceiver(mReceiver); + } + } + + @Override + public DetailAdapter getDetailAdapter() { + return new ProfileDetailAdapter(); + } + + @Override + public void onKeyguardChanged() { + refreshState(); + } + + private class ProfileAdapter extends ArrayAdapter<Profile> { + public ProfileAdapter(Context context, List<Profile> profiles) { + super(context, android.R.layout.simple_list_item_single_choice, profiles); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(mContext); + CheckedTextView label = (CheckedTextView) inflater.inflate( + android.R.layout.simple_list_item_single_choice, parent, false); + + Profile p = getItem(position); + label.setText(p.getName()); + + return label; + } + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ProfileManager.INTENT_ACTION_PROFILE_SELECTED.equals(intent.getAction()) + || ProfileManager.INTENT_ACTION_PROFILE_UPDATED.equals(intent.getAction())) { + refreshState(); + } + } + }; + + public class ProfileDetailAdapter implements DetailAdapter, AdapterView.OnItemClickListener { + + private List<Profile> mProfilesList; + + @Override + public int getTitle() { + return R.string.quick_settings_profiles; + } + + @Override + public Boolean getToggleState() { + boolean enabled = profilesEnabled(); + rebuildProfilesList(enabled); + return enabled; + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_PROFILES_DETAIL; + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + mDetails = QSDetailItemsList.convertOrInflate(context, convertView, parent); + mProfilesList = new ArrayList<>(); + mDetails.setAdapter(mAdapter = new ProfileAdapter(context, mProfilesList)); + + final ListView list = mDetails.getListView(); + list.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); + list.setOnItemClickListener(this); + + mDetails.setEmptyState(R.drawable.ic_qs_profiles_off, + R.string.quick_settings_profiles_off); + + return mDetails; + } + + private void rebuildProfilesList(boolean populate) { + mProfilesList.clear(); + if (populate) { + int selected = -1; + + final Profile[] profiles = mProfileManager.getProfiles(); + final Profile activeProfile = mProfileManager.getActiveProfile(); + final UUID activeUuid = activeProfile != null ? activeProfile.getUuid() : null; + + for (int i = 0; i < profiles.length; i++) { + mProfilesList.add(profiles[i]); + if (activeUuid != null && activeUuid.equals(profiles[i].getUuid())) { + selected = i; + } + } + mDetails.getListView().setItemChecked(selected, true); + } + mAdapter.notifyDataSetChanged(); + } + + @Override + public Intent getSettingsIntent() { + return PROFILES_SETTINGS; + } + + @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override + public void setToggleState(boolean state) { + CMSettings.System.putInt(mContext.getContentResolver(), + CMSettings.System.SYSTEM_PROFILES_ENABLED, state ? 1 : 0); + fireToggleStateChanged(state); + rebuildProfilesList(state); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + Profile selected = (Profile) parent.getItemAtPosition(position); + mProfileManager.setActiveProfile(selected.getUuid()); + } + } + + private class ProfilesObserver extends ContentObserver { + public ProfilesObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + refreshState(); + } + + public void startObserving() { + mContext.getContentResolver().registerContentObserver( + CMSettings.System.getUriFor(CMSettings.System.SYSTEM_PROFILES_ENABLED), + false, this); + } + + public void endObserving() { + mContext.getContentResolver().unregisterContentObserver(this); + } + } +} 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 1a26a4d..f074d9d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +17,7 @@ package com.android.systemui.qs.tiles; +import android.content.Intent; import android.content.res.Configuration; import com.android.internal.logging.MetricsLogger; @@ -26,6 +28,10 @@ import com.android.systemui.statusbar.policy.RotationLockController.RotationLock /** Quick settings tile: Rotation **/ public class RotationLockTile extends QSTile<QSTile.BooleanState> { + + private static final Intent DISPLAY_ROTATION_SETTINGS = + new Intent("android.settings.DISPLAY_ROTATION_SETTINGS"); + private final AnimationIcon mPortraitToAuto = new AnimationIcon(R.drawable.ic_portrait_to_auto_rotate_animation); private final AnimationIcon mAutoToPortrait @@ -67,6 +73,11 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> { } @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(DISPLAY_ROTATION_SETTINGS); + } + + @Override protected void handleUpdateState(BooleanState state, Object arg) { if (mController == null) return; final boolean rotationLocked = arg != null ? ((UserBoolean) arg).value diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenTimeoutTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenTimeoutTile.java new file mode 100644 index 0000000..e933787 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenTimeoutTile.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.provider.Settings; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import com.android.systemui.R; +import com.android.systemui.qs.QSDetailItemsList; +import com.android.systemui.qs.QSTile; +import cyanogenmod.app.StatusBarPanelCustomTile; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +import java.util.ArrayList; +import java.util.Arrays; + +public class ScreenTimeoutTile extends QSTile<ScreenTimeoutTile.TimeoutState> { + + private static final Intent SETTINGS_INTENT = new Intent(Settings.ACTION_DISPLAY_SETTINGS); + private static final String TIMEOUT_ENTRIES_NAME = "screen_timeout_entries"; + private static final String TIMEOUT_VALUES_NAME = "screen_timeout_values"; + private static final String SETTINGS_PACKAGE_NAME = "com.android.settings"; + + private final AnimationIcon mShort = + new AnimationIcon(R.drawable.ic_qs_screen_timeout_short_avd); + private final AnimationIcon mShortReverse = + new AnimationIcon(R.drawable.ic_qs_screen_timeout_short_reverse_avd); + private final AnimationIcon mMedium = + new AnimationIcon(R.drawable.ic_qs_screen_timeout_med_avd); + private final AnimationIcon mMediumReverse = + new AnimationIcon(R.drawable.ic_qs_screen_timeout_med_reverse_avd); + private final AnimationIcon mLong = + new AnimationIcon(R.drawable.ic_qs_screen_timeout_long_avd); + private final AnimationIcon mLongReverse = + new AnimationIcon(R.drawable.ic_qs_screen_timeout_long_reverse_avd); + + private String[] mEntries, mValues; + + public ScreenTimeoutTile(Host host) { + super(host); + populateList(); + } + + private void populateList() { + try { + Context context = mContext.createPackageContext(SETTINGS_PACKAGE_NAME, 0); + Resources mSettingsResources = context.getResources(); + int id = mSettingsResources.getIdentifier(TIMEOUT_ENTRIES_NAME, + "array", SETTINGS_PACKAGE_NAME); + if (id <= 0) { + return; + } + mEntries = mSettingsResources.getStringArray(id); + id = mSettingsResources.getIdentifier(TIMEOUT_VALUES_NAME, + "array", SETTINGS_PACKAGE_NAME); + if (id <= 0) { + return; + } + mValues = mSettingsResources.getStringArray(id); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + } + + private int getScreenTimeout() { + return Settings.System.getInt(mContext.getContentResolver(), + Settings.System.SCREEN_OFF_TIMEOUT, 0); + } + + @Override + public DetailAdapter getDetailAdapter() { + return new ScreenTimeoutDetailAdapter(); + } + + private ContentObserver mObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + refreshState(); + } + }; + + @Override + public void setListening(boolean listening) { + if (listening) { + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.SCREEN_OFF_TIMEOUT), + false, mObserver); + } else { + mContext.getContentResolver().unregisterContentObserver(mObserver); + } + } + + @Override + protected TimeoutState newTileState() { + return new TimeoutState(); + } + + @Override + protected void handleClick() { + if (mEntries.length > 0) { + showDetail(true); + } + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(SETTINGS_INTENT); + } + + private String makeTimeoutSummaryString(int timeout) { + Resources res = mContext.getResources(); + int resId; + + /* ms -> seconds */ + timeout /= 1000; + + if (timeout >= 60 && timeout % 60 == 0) { + /* seconds -> minutes */ + timeout /= 60; + if (timeout >= 60 && timeout % 60 == 0) { + /* minutes -> hours */ + timeout /= 60; + resId = com.android.internal.R.plurals.duration_hours; + } else { + resId = com.android.internal.R.plurals.duration_minutes; + } + } else { + resId = com.android.internal.R.plurals.duration_seconds; + } + + return res.getQuantityString(resId, timeout, timeout); + } + + public static final class TimeoutState extends QSTile.State { + int previousTimeout; + } + + private enum Bucket { + SMALL(0, 30000), + MEDIUM(60000,300000), + LARGE(600000, 1800000); + private final int start; + private final int stop; + + Bucket(int start, int stop) { + this.start = start; + this.stop = stop; + } + + public static Bucket getBucket(int value) { + for (Bucket item : Bucket.values()) { + if (value >= item.start && value <= item.stop) { + return item; + } + } + return null; + } + + } + @Override + protected void handleUpdateState(final TimeoutState state, Object arg) { + int newTimeout = getScreenTimeout(); + + AnimationIcon d = null; + Bucket nextBucket = Bucket.getBucket(newTimeout); + Bucket previousBucket = Bucket.getBucket(state.previousTimeout); + + if (state.previousTimeout < 60000) { + // Default + d = mMediumReverse; + if (nextBucket == Bucket.MEDIUM) { + // Medium + d = mShort; + } else if (nextBucket == Bucket.LARGE) { + // Large + d = mShortReverse; + } + } else if (state.previousTimeout < 600000) { + // Default + d = mShort; + if (nextBucket == Bucket.SMALL) { + // Small + d = mMediumReverse; + } else if (nextBucket == Bucket.LARGE) { + // Large + d = mMedium; + } + } else { + d = mMedium; + if (nextBucket == Bucket.MEDIUM) { + // Small + d = mLongReverse; + } else if (nextBucket == Bucket.SMALL) { + // Large + d = mLong; + } + } + + if (state.icon == null || previousBucket != nextBucket) { + if (arg instanceof Boolean && (Boolean) arg) { + d.setAllowAnimation(true); + } + state.icon = d; + } + + state.visible = true; + state.label = makeTimeoutSummaryString(newTimeout); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_screen_timeout, state.label); + state.previousTimeout = newTimeout; + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_SCREEN_TIME_OUT; + } + + @Override + protected String composeChangeAnnouncement() { + return mContext.getString(R.string.accessibility_quick_settings_screen_timeout_changed, + mState.label); + } + + private class RadioAdapter extends ArrayAdapter<String> { + + public RadioAdapter(Context context, int resource, String[] objects) { + super(context, resource, objects); + } + + public RadioAdapter(Context context, int resource, + int textViewResourceId, String[] objects) { + super(context, resource, textViewResourceId, objects); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + view = super.getView(position, view, parent); + + view.setMinimumHeight(mContext.getResources() .getDimensionPixelSize( + R.dimen.qs_detail_item_height)); + + return view; + } + + } + private class ScreenTimeoutDetailAdapter implements DetailAdapter, + AdapterView.OnItemClickListener { + private QSDetailItemsList mItems; + + @Override + public int getTitle() { + return R.string.quick_settings_screen_timeout_detail_title; + } + + @Override + public Boolean getToggleState() { + return null; + } + + @Override + public Intent getSettingsIntent() { + return SETTINGS_INTENT; + } + + @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override + public void setToggleState(boolean state) { + // noop + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_SCREEN_TIME_OUT_DETAIL; + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + mItems = QSDetailItemsList.convertOrInflate(context, convertView, parent); + ListView listView = mItems.getListView(); + listView.setOnItemClickListener(this); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + listView.setDivider(null); + RadioAdapter adapter = new RadioAdapter(context, + android.R.layout.simple_list_item_single_choice, mEntries); + int indexOfSelection = Arrays.asList(mValues).indexOf(String.valueOf(getScreenTimeout())); + mItems.setAdapter(adapter); + listView.setItemChecked(indexOfSelection, true); + mItems.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + } + + @Override + public void onViewDetachedFromWindow(View v) { + mUiHandler.postDelayed(new Runnable() { + @Override + public void run() { + refreshState(true); + } + }, 100); + + } + }); + return mItems; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + int selectedTimeout = Integer.valueOf(mValues[position]); + Settings.System.putInt(mContext.getContentResolver(), + Settings.System.SCREEN_OFF_TIMEOUT, selectedTimeout); + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SyncTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SyncTile.java new file mode 100644 index 0000000..7ffebc7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SyncTile.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.ContentResolver; +import android.content.Intent; +import android.content.SyncStatusObserver; + +import com.android.systemui.R; +import com.android.systemui.qs.QSTile; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +/** Quick settings tile: Sync **/ +public class SyncTile extends QSTile<QSTile.BooleanState> { + + private Object mSyncObserverHandle = null; + private boolean mListening; + + public SyncTile(Host host) { + super(host); + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleClick() { + ContentResolver.setMasterSyncAutomatically(!mState.value); + refreshState(); + } + + @Override + public void handleLongClick() { + Intent intent = new Intent("android.settings.SYNC_SETTINGS"); + intent.addCategory(Intent.CATEGORY_DEFAULT); + mHost.startActivityDismissingKeyguard(intent); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.value = ContentResolver.getMasterSyncAutomatically(); + state.visible = true; + state.label = mContext.getString(R.string.quick_settings_sync_label); + if (state.value) { + state.icon = ResourceIcon.get(R.drawable.ic_qs_sync_on); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_sync_on); + } else { + state.icon = ResourceIcon.get(R.drawable.ic_qs_sync_off); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_sync_off); + } + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_SYNC; + } + + @Override + protected String composeChangeAnnouncement() { + if (mState.value) { + return mContext.getString(R.string.accessibility_quick_settings_sync_changed_on); + } else { + return mContext.getString(R.string.accessibility_quick_settings_sync_changed_off); + } + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + + if (listening) { + mSyncObserverHandle = ContentResolver.addStatusChangeListener( + ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncObserver); + } else { + ContentResolver.removeStatusChangeListener(mSyncObserverHandle); + mSyncObserverHandle = null; + } + } + + private SyncStatusObserver mSyncObserver = new SyncStatusObserver() { + public void onStatusChanged(int which) { + mHandler.post(new Runnable() { + @Override + public void run() { + refreshState(); + } + }); + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UsbTetherTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UsbTetherTile.java new file mode 100644 index 0000000..1274195 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UsbTetherTile.java @@ -0,0 +1,119 @@ +/* + * 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.hardware.usb.UsbManager; +import android.provider.Settings; +import android.net.ConnectivityManager; + +import com.android.systemui.R; +import com.android.systemui.qs.QSTile; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +/** + * USB Tether quick settings tile + */ +public class UsbTetherTile extends QSTile<QSTile.BooleanState> { + private static final Intent WIRELESS_SETTINGS = new Intent(Settings.ACTION_WIRELESS_SETTINGS); + + private final ConnectivityManager mConnectivityManager; + + private boolean mListening; + + private boolean mUsbTethered = false; + private boolean mUsbConnected = false; + + public UsbTetherTile(Host host) { + super(host); + mConnectivityManager = (ConnectivityManager) mContext + .getSystemService(Context.CONNECTIVITY_SERVICE); + } + + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) + return; + mListening = listening; + if (listening) { + final IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_STATE); + mContext.registerReceiver(mReceiver, filter); + } else { + mContext.unregisterReceiver(mReceiver); + } + } + + @Override + protected void handleClick() { + mConnectivityManager.setUsbTethering(!mUsbTethered); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(WIRELESS_SETTINGS); + } + + private void updateState() { + String[] tetheredIfaces = mConnectivityManager.getTetheredIfaces(); + String[] usbRegexs = mConnectivityManager.getTetherableUsbRegexs(); + + mUsbTethered = false; + for (String s : tetheredIfaces) { + for (String regex : usbRegexs) { + if (s.matches(regex)) { + mUsbTethered = true; + return; + } + } + } + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mUsbConnected = intent.getBooleanExtra(UsbManager.USB_CONNECTED, false); + if (mUsbConnected && mConnectivityManager.isTetheringSupported()) { + updateState(); + } else { + mUsbTethered = false; + } + refreshState(); + } + }; + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.visible = mUsbConnected && mConnectivityManager.isTetheringSupported(); + state.value = mUsbTethered; + state.label = mContext.getString(R.string.quick_settings_usb_tether_label); + state.icon = mUsbTethered ? ResourceIcon.get(R.drawable.ic_qs_usb_tether_on) + : ResourceIcon.get(R.drawable.ic_qs_usb_tether_off); + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_USB_TETHER; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/VolumeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/VolumeTile.java new file mode 100644 index 0000000..ae29f16 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/VolumeTile.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.provider.Settings; + +import com.android.systemui.R; +import com.android.systemui.qs.QSTile; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + +public class VolumeTile extends QSTile<QSTile.BooleanState> { + + private static final Intent SOUND_SETTINGS = new Intent("android.settings.SOUND_SETTINGS"); + + public VolumeTile(Host host) { + super(host); + } + + @Override + protected void handleClick() { + AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + am.adjustVolume(AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI); + } + + @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(SOUND_SETTINGS); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.visible = true; + state.label = mContext.getString(R.string.quick_settings_volume_panel_label); + state.icon = ResourceIcon.get(R.drawable.ic_qs_volume_panel); // TODO needs own icon + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.TILE_VOLUME; + } + + @Override + protected BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void setListening(boolean listening) { + // Do nothing + } +} 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 e654efd..abc9acd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,16 +20,20 @@ package com.android.systemui.qs.tiles; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.os.Looper; import android.provider.Settings; import android.util.Log; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ListView; 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.QSDetailItemsList; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QSTileView; import com.android.systemui.qs.SignalTileView; @@ -37,6 +42,9 @@ import com.android.systemui.statusbar.policy.NetworkController.AccessPointContro import com.android.systemui.statusbar.policy.NetworkController.IconState; import com.android.systemui.statusbar.policy.SignalCallbackAdapter; +import cyanogenmod.app.StatusBarPanelCustomTile; + +import java.util.ArrayList; import java.util.List; /** Quick settings tile: Wifi **/ @@ -58,7 +66,7 @@ public class WifiTile extends QSTile<QSTile.SignalState> { } @Override - public boolean supportsDualTargets() { + public boolean hasDualTargetsDetails() { return true; } @@ -116,19 +124,36 @@ public class WifiTile extends QSTile<QSTile.SignalState> { } @Override + protected void handleLongClick() { + mHost.startActivityDismissingKeyguard(WIFI_SETTINGS); + } + + @Override protected void handleUpdateState(SignalState state, Object arg) { state.visible = true; if (DEBUG) Log.d(TAG, "handleUpdateState arg=" + arg); - CallbackInfo cb = (CallbackInfo) arg; - if (cb == null) { + final CallbackInfo cb; + if (arg == null) { cb = mSignalCallback.mInfo; + } else { + cb = (CallbackInfo) arg; } boolean wifiConnected = cb.enabled && (cb.wifiSignalIconId > 0) && (cb.enabledDesc != null); boolean wifiNotConnected = (cb.wifiSignalIconId > 0) && (cb.enabledDesc == null); boolean enabledChanging = state.enabled != cb.enabled; if (enabledChanging) { - mDetailAdapter.setItemsVisible(cb.enabled); + if (Looper.myLooper() == Looper.getMainLooper()) { + // on main thread, bypass the handler + mDetailAdapter.setItemsVisible(cb.enabled); + } else { + mUiHandler.post(new Runnable() { + @Override + public void run() { + mDetailAdapter.setItemsVisible(cb.enabled); + } + }); + } fireToggleStateChanged(cb.enabled); } state.enabled = cb.enabled; @@ -235,10 +260,12 @@ public class WifiTile extends QSTile<QSTile.SignalState> { }; private final class WifiDetailAdapter implements DetailAdapter, - NetworkController.AccessPointController.AccessPointCallback, QSDetailItems.Callback { + AccessPointController.AccessPointCallback, AdapterView.OnItemClickListener { - private QSDetailItems mItems; - private AccessPoint[] mAccessPoints; + private QSDetailItemsList mItemsList; + private List<AccessPoint> mAccessPoints; + private List<Item> mDisplayedAccessPoints = new ArrayList<>(); + private QSDetailItemsList.QSDetailListAdapter mAdapter; @Override public int getTitle() { @@ -250,6 +277,11 @@ public class WifiTile extends QSTile<QSTile.SignalState> { } @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override public Boolean getToggleState() { return mState.enabled; } @@ -273,19 +305,21 @@ public class WifiTile extends QSTile<QSTile.SignalState> { mAccessPoints = null; mWifiController.scanForAccessPoints(); fireScanStateChanged(true); - mItems = QSDetailItems.convertOrInflate(context, convertView, parent); - mItems.setTagSuffix("Wifi"); - mItems.setCallback(this); - mItems.setEmptyState(R.drawable.ic_qs_wifi_detail_empty, + mItemsList = QSDetailItemsList.convertOrInflate(context, convertView, parent); + ListView listView = mItemsList.getListView(); + listView.setDivider(null); + listView.setOnItemClickListener(this); + listView.setAdapter(mAdapter = + new QSDetailItemsList.QSDetailListAdapter(context, mDisplayedAccessPoints)); + mItemsList.setEmptyState(R.drawable.ic_qs_wifi_detail_empty, R.string.quick_settings_wifi_detail_empty_text); updateItems(); - setItemsVisible(mState.enabled); - return mItems; + return mItemsList; } @Override public void onAccessPointsChanged(final List<AccessPoint> accessPoints) { - mAccessPoints = accessPoints.toArray(new AccessPoint[accessPoints.size()]); + mAccessPoints = accessPoints; updateItems(); if (accessPoints != null && accessPoints.size() > 0) { fireScanStateChanged(false); @@ -297,35 +331,22 @@ public class WifiTile extends QSTile<QSTile.SignalState> { mHost.startActivityDismissingKeyguard(settingsIntent); } - @Override - public void onDetailItemClick(Item item) { - if (item == null || item.tag == null) return; - final AccessPoint ap = (AccessPoint) item.tag; - if (!ap.isActive()) { - if (mWifiController.connect(ap)) { - mHost.collapsePanels(); - } - } - showDetail(false); - } - - @Override - public void onDetailItemDisconnect(Item item) { - // noop - } - public void setItemsVisible(boolean visible) { - if (mItems == null) return; - mItems.setItemsVisible(visible); + if (mAdapter == null) return; + if (visible) { + updateItems(); + } else { + mDisplayedAccessPoints.clear(); + } + mAdapter.notifyDataSetChanged(); } private void updateItems() { - if (mItems == null) return; - Item[] items = null; + if (mAdapter == null) return; if (mAccessPoints != null) { - items = new Item[mAccessPoints.length]; - for (int i = 0; i < mAccessPoints.length; i++) { - final AccessPoint ap = mAccessPoints[i]; + mDisplayedAccessPoints.clear(); + for (int i = 0; i < mAccessPoints.size(); i++) { + final AccessPoint ap = mAccessPoints.get(i); final Item item = new Item(); item.tag = ap; item.icon = mWifiController.getIcon(ap); @@ -334,10 +355,23 @@ public class WifiTile extends QSTile<QSTile.SignalState> { item.overlay = ap.getSecurity() != AccessPoint.SECURITY_NONE ? mContext.getDrawable(R.drawable.qs_ic_wifi_lock) : null; - items[i] = item; + mDisplayedAccessPoints.add(item); + } + } + mAdapter.notifyDataSetChanged(); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + Item item = (Item) parent.getItemAtPosition(position); + if (item == null || item.tag == null) return; + final AccessPoint ap = (AccessPoint) item.tag; + if (!ap.isActive()) { + if (mWifiController.connect(ap)) { + mHost.collapsePanels(); } } - mItems.setItems(items); + showDetail(false); } }; } diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java index a4acf83..b482a50 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/Constants.java +++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java @@ -38,7 +38,7 @@ public class Constants { // Enables the filtering of tasks according to their grouping public static final boolean EnableTaskFiltering = false; // Enables dismiss-all - public static final boolean EnableDismissAll = false; + public static final boolean EnableDismissAll = true; // Enables debug mode public static final boolean EnableDebugMode = false; // Enables the search bar layout diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java index 3917bab..b3a3dfd 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java +++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java @@ -46,6 +46,7 @@ import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.SystemUI; import com.android.systemui.SystemUIApplication; +import com.android.systemui.cm.UserContentObserver; import com.android.systemui.recents.misc.Console; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.model.RecentsTaskLoadPlan; @@ -61,6 +62,8 @@ import com.android.systemui.statusbar.phone.PhoneStatusBar; import java.util.ArrayList; +import cyanogenmod.providers.CMSettings; + /** * Annotation for a method that is only called from the primary user's SystemUI process and will be * proxied to the current user. @@ -161,6 +164,35 @@ public class Recents extends SystemUI } } + class RecentsSettingsObserver extends UserContentObserver { + + public RecentsSettingsObserver(Handler handler) { + super(handler); + } + + @Override + protected void observe() { + super.observe(); + mContext.getContentResolver().registerContentObserver( + CMSettings.System.getUriFor(CMSettings.System.RECENTS_SHOW_SEARCH_BAR), + false, this); + update(); + } + + @Override + protected void unobserve() { + super.unobserve(); + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + protected void update() { + if (mConfig.updateShowSearch(mContext)) { + reloadHeaderBarLayout(); + } + } + } + static RecentsComponent.Callbacks sRecentsComponentCallbacks; static RecentsTaskLoadPlan sInstanceLoadPlan; static Recents sInstance; @@ -171,6 +203,7 @@ public class Recents extends SystemUI TaskStackListenerImpl mTaskStackListener; RecentsOwnerEventProxyReceiver mProxyBroadcastReceiver; RecentsAppWidgetHost mAppWidgetHost; + RecentsSettingsObserver mSettingsObserver; boolean mBootCompleted; boolean mStartAnimationTriggered; boolean mCanReuseTaskStackViews = true; @@ -259,6 +292,9 @@ public class Recents extends SystemUI // Load the header bar layout reloadHeaderBarLayout(); + mSettingsObserver = new RecentsSettingsObserver(mHandler); + mSettingsObserver.observe(); + // When we start, preload the data associated with the previous recent tasks. // We can use a new plan since the caches will be the same. RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); @@ -549,7 +585,8 @@ public class Recents extends SystemUI // 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) { + if (mConfig.searchBarEnabled && + mSystemServicesProxy.getOrBindSearchAppWidget(mContext, mAppWidgetHost) != null) { mConfig.getSearchBarBounds(mWindowRect.width(), mWindowRect.height(), mStatusBarHeight, searchBarBounds); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java index d0876fa..9e08599 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -48,6 +48,7 @@ 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 cyanogenmod.providers.CMSettings; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -84,6 +85,8 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView // Runnable to be executed after we paused ourselves Runnable mAfterPauseRunnable; + private ReferenceCountedTrigger mExitTrigger; + /** * A common Runnable to finish Recents either by calling finish() (with a custom animation) or * launching Home with some ActivityOptions. Generally we always launch home when we exit @@ -94,6 +97,7 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView class FinishRecentsRunnable implements Runnable { Intent mLaunchIntent; ActivityOptions mLaunchOpts; + boolean mAbort = false; /** * Creates a finish runnable that starts the specified intent, using the given @@ -104,8 +108,15 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView mLaunchOpts = opts; } + public void setAbort(boolean run) { + this.mAbort = run; + } + @Override public void run() { + if (mAbort) { + return; + } // Finish Recents if (mLaunchIntent != null) { try { @@ -207,9 +218,7 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView ArrayList<TaskStack> stacks = plan.getAllTaskStacks(); mConfig.launchedWithNoRecentTasks = !plan.hasTasks(); - if (!mConfig.launchedWithNoRecentTasks) { - mRecentsView.setTaskStacks(stacks); - } + mRecentsView.setTaskStacks(stacks); // Create the home intent runnable Intent homeIntent = new Intent(Intent.ACTION_MAIN, null); @@ -218,10 +227,8 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); mFinishLaunchHomeRunnable = new FinishRecentsRunnable(homeIntent, ActivityOptions.makeCustomAnimation(this, - mConfig.launchedFromSearchHome ? R.anim.recents_to_search_launcher_enter : - R.anim.recents_to_launcher_enter, - mConfig.launchedFromSearchHome ? R.anim.recents_to_search_launcher_exit : - R.anim.recents_to_launcher_exit)); + R.anim.recents_to_search_launcher_enter, + R.anim.recents_to_search_launcher_exit)); // Mark the task that is the launch target int taskStackCount = stacks.size(); @@ -248,15 +255,26 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView mEmptyView = mEmptyViewStub.inflate(); } mEmptyView.setVisibility(View.VISIBLE); + mEmptyView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + dismissRecentsToHome(true); + } + }); mRecentsView.setSearchBarVisibility(View.GONE); } else { if (mEmptyView != null) { mEmptyView.setVisibility(View.GONE); + mEmptyView.setOnClickListener(null); } - if (mRecentsView.hasValidSearchBar()) { - mRecentsView.setSearchBarVisibility(View.VISIBLE); + if (!mConfig.searchBarEnabled) { + mRecentsView.setSearchBarVisibility(View.GONE); } else { - refreshSearchWidgetView(); + if (mRecentsView.hasValidSearchBar()) { + mRecentsView.setSearchBarVisibility(View.VISIBLE); + } else { + refreshSearchWidgetView(); + } } } @@ -309,13 +327,26 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView return false; } + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (!hasFocus && mExitTrigger != null && mExitTrigger.getCount() > 0) { + // we are animating recents out and the window has lost focus during the + // animation. we need to stop everything we're doing now and get out + // without any animations (since we were already animating) + mFinishLaunchHomeRunnable.setAbort(true); + finish(); + overridePendingTransition(0, 0); + } + } + /** Dismisses Recents directly to Home. */ void dismissRecentsToHomeRaw(boolean animated) { if (animated) { - ReferenceCountedTrigger exitTrigger = new ReferenceCountedTrigger(this, + mExitTrigger = new ReferenceCountedTrigger(this, null, mFinishLaunchHomeRunnable, null); mRecentsView.startExitToHomeAnimation( - new ViewAnimation.TaskViewExitContext(exitTrigger)); + new ViewAnimation.TaskViewExitContext(mExitTrigger)); } else { mFinishLaunchHomeRunnable.run(); } @@ -431,6 +462,14 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView } @Override + protected void onResume() { + if (mConfig.searchBarEnabled && mConfig.launchedFromHome) { + overridePendingTransition(0, 0); + } + super.onResume(); + } + + @Override protected void onPause() { super.onPause(); if (mAfterPauseRunnable != null) { @@ -442,6 +481,9 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView @Override protected void onStop() { super.onStop(); + + mExitTrigger = null; + MetricsLogger.hidden(this, MetricsLogger.OVERVIEW_ACTIVITY); RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java index dfe7e96..d7e8b99 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java @@ -31,6 +31,7 @@ import com.android.systemui.R; import com.android.systemui.recents.misc.Console; import com.android.systemui.recents.misc.SystemServicesProxy; +import cyanogenmod.providers.CMSettings; /** A static Recents configuration for the current context * NOTE: We should not hold any references to a Context from a static instance */ @@ -73,6 +74,7 @@ public class RecentsConfiguration { public int maxNumTasksToLoad; /** Search bar */ + public boolean searchBarEnabled = true; public int searchBarSpaceHeightPx; /** Task stack */ @@ -175,6 +177,14 @@ public class RecentsConfiguration { return sInstance; } + /** Returns the current recents configuration or creates and populates it if required */ + public static RecentsConfiguration getInstance(Context context, SystemServicesProxy ssp) { + if (sInstance == null) { + sInstance = reinitialize(context, ssp); + } + return sInstance; + } + /** Updates the state, given the specified context */ void update(Context context) { Resources res = context.getResources(); @@ -271,6 +281,13 @@ public class RecentsConfiguration { svelteLevel = res.getInteger(R.integer.recents_svelte_level); } + public boolean updateShowSearch(Context context) { + boolean wasEnabled = searchBarEnabled; + searchBarEnabled = CMSettings.System.getInt(context.getContentResolver(), + CMSettings.System.RECENTS_SHOW_SEARCH_BAR, 1) == 1; + return wasEnabled != searchBarEnabled; + } + /** Updates the system insets */ public void updateSystemInsets(Rect insets) { systemInsets.set(insets); diff --git a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java index cbf5c05..b1413c9 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java +++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java @@ -30,9 +30,11 @@ import android.graphics.drawable.ColorDrawable; import android.os.RemoteException; import android.util.DisplayMetrics; import android.view.Gravity; +import android.view.IWindowManager; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.view.animation.DecelerateInterpolator; import android.widget.Button; @@ -49,6 +51,7 @@ public class ScreenPinningRequest implements View.OnClickListener { private final AccessibilityManager mAccessibilityService; private final WindowManager mWindowManager; + private final IWindowManager mWindowManagerService; private RequestWindowView mRequestWindow; @@ -58,6 +61,7 @@ public class ScreenPinningRequest implements View.OnClickListener { mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); } public void clearPrompt() { @@ -215,19 +219,33 @@ public class ScreenPinningRequest implements View.OnClickListener { .setVisibility(View.INVISIBLE); } - final int description = mAccessibilityService.isEnabled() + final int description; + if (hasNavigationBar()) { + description = mAccessibilityService.isEnabled() ? R.string.screen_pinning_description_accessible : R.string.screen_pinning_description; + final int backBgVis = + mAccessibilityService.isEnabled() ? View.INVISIBLE : View.VISIBLE; + mLayout.findViewById(R.id.screen_pinning_back_bg).setVisibility(backBgVis); + mLayout.findViewById(R.id.screen_pinning_back_bg_light).setVisibility(backBgVis); + } else { + description = R.string.screen_pinning_description_no_navbar; + ((ViewGroup) buttons.getParent()).removeView(buttons); + } ((TextView) mLayout.findViewById(R.id.screen_pinning_description)) .setText(description); - final int backBgVisibility = - mAccessibilityService.isEnabled() ? View.INVISIBLE : View.VISIBLE; - mLayout.findViewById(R.id.screen_pinning_back_bg).setVisibility(backBgVisibility); - mLayout.findViewById(R.id.screen_pinning_back_bg_light).setVisibility(backBgVisibility); - + addView(mLayout, getRequestLayoutParams(isLandscape)); } + private boolean hasNavigationBar() { + try { + return mWindowManagerService.hasNavigationBar(); + } catch (RemoteException e) { + //ignore + } + return false; + } private void swapChildrenIfRtlAndVertical(View group) { if (mContext.getResources().getConfiguration().getLayoutDirection() != View.LAYOUT_DIRECTION_RTL) { 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 ad25c85..4b3b391 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java @@ -458,7 +458,7 @@ public class RecentsTaskLoader { /** Creates a new plan for loading the recent tasks. */ public RecentsTaskLoadPlan createLoadPlan(Context context) { - RecentsConfiguration config = RecentsConfiguration.getInstance(); + RecentsConfiguration config = RecentsConfiguration.getInstance(context, mSystemServicesProxy); RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(context, config, mSystemServicesProxy); return plan; } 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 947c19c..2f11c56 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -31,6 +31,7 @@ import android.os.UserHandle; import android.provider.Settings; import android.util.AttributeSet; import android.util.Log; +import android.util.EventLog; import android.view.LayoutInflater; import android.view.View; import android.view.WindowInsets; @@ -48,6 +49,8 @@ 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.EventLogTags; + import java.util.ArrayList; import java.util.List; @@ -267,6 +270,8 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV stackView.startEnterRecentsAnimation(ctx); } ctx.postAnimationTrigger.decrement(); + + EventLog.writeEvent(EventLogTags.SYSUI_RECENTS_EVENT, 1 /* opened */); } /** Requests all task stacks to start their exit-recents animation */ @@ -323,7 +328,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV // Get the search bar bounds and measure the search bar layout Rect searchBarSpaceBounds = new Rect(); - if (mSearchBar != null) { + if (mSearchBar != null && mConfig.searchBarEnabled) { mConfig.getSearchBarBounds(width, height, mConfig.systemInsets.top, searchBarSpaceBounds); mSearchBar.measure( MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.width(), MeasureSpec.EXACTLY), @@ -360,7 +365,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // Get the search bar bounds so that we lay it out - if (mSearchBar != null) { + if (mSearchBar != null && mConfig.searchBarEnabled) { Rect searchBarSpaceBounds = new Rect(); mConfig.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(), mConfig.systemInsets.top, searchBarSpaceBounds); @@ -620,6 +625,8 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV launchRunnable.run(); } } + + EventLog.writeEvent(EventLogTags.SYSUI_RECENTS_EVENT, 3 /* chose task */); } @Override @@ -659,6 +666,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV // Keep track of all-deletions MetricsLogger.count(getContext(), "overview_task_all_dismissed", 1); + EventLog.writeEvent(EventLogTags.SYSUI_RECENTS_EVENT, 4 /* closed all */); } /** Final callback after Recents is finally hidden. */ @@ -670,6 +678,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV TaskStackView stackView = stackViews.get(i); stackView.onRecentsHidden(); } + EventLog.writeEvent(EventLogTags.SYSUI_RECENTS_EVENT, 2 /* closed */); } @Override 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 0068f84..43b9a3e 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -17,18 +17,27 @@ package com.android.systemui.recents.views; import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.app.admin.DevicePolicyManager; 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.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageDataObserver; +import android.content.pm.PackageManager; +import android.net.Uri; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; +import android.widget.PopupMenu; import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; @@ -85,6 +94,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal boolean mDismissAllButtonAnimating; int mFocusedTaskIndex = -1; int mPrevAccessibilityFocusedIndex = -1; + + private PopupMenu mPopup; + // Optimizations int mStackViewsAnimationDuration; boolean mStackViewsDirty = true; @@ -1372,6 +1384,85 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } @Override + public void onTaskViewLongClicked(final TaskView tv) { + final PopupMenu popup = new PopupMenu(getContext(), tv.mHeaderView.mApplicationIcon); + mPopup = popup; + popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu()); + + final Task task = tv.getTask(); + final String packageName = task.key.baseIntent.getComponent().getPackageName(); + + try { + PackageManager pm = (PackageManager) getContext().getPackageManager(); + ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); + DevicePolicyManager dpm = (DevicePolicyManager) getContext() + .getSystemService(Context.DEVICE_POLICY_SERVICE); + + boolean hasActiveAdmins = dpm.packageHasActiveAdmins(packageName); + boolean isClearable = (appInfo.flags & + (ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA | ApplicationInfo.FLAG_SYSTEM)) != + ApplicationInfo.FLAG_SYSTEM; + if (!isClearable || hasActiveAdmins) { + popup.getMenu().findItem(R.id.recent_wipe_app).setEnabled(false); + popup.getMenu().findItem(R.id.recent_uninstall).setEnabled(false); + } + } catch (PackageManager.NameNotFoundException ex) { + } + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.recent_remove_item: + onTaskViewDismissed(tv); + break; + case R.id.recent_inspect_item: + onTaskViewAppInfoClicked(tv); + break; + case R.id.recent_force_stop: + { + ActivityManager am = (ActivityManager) getContext() + .getSystemService(Context.ACTIVITY_SERVICE); + am.forceStopPackage(packageName); + onTaskViewDismissed(tv); + break; + } + case R.id.recent_wipe_app: + { + ActivityManager am = (ActivityManager) getContext() + .getSystemService(Context.ACTIVITY_SERVICE); + am.clearApplicationUserData(packageName, new IPackageDataObserver.Stub() { + @Override + public void onRemoveCompleted(String packageName, boolean succeeded) {} + }); + onTaskViewDismissed(tv); + break; + } + case R.id.recent_uninstall: + { + Uri packageUri = Uri.parse("package:" + packageName); + Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); + uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, true); + getContext().startActivity(uninstallIntent); + onTaskViewDismissed(tv); + break; + } + default: + return false; + } + return true; + } + }); + popup.setOnDismissListener(new PopupMenu.OnDismissListener() { + @Override + public void onDismiss(PopupMenu menu) { + mPopup = null; + } + }); + popup.show(); + } + + + @Override public void onTaskViewClicked(TaskView tv, Task task, boolean lockToTask) { // Cancel any doze triggers mUIDozeTrigger.stopDozing(); 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 cbfe842..b7e46f4 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -34,6 +34,7 @@ import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.Task; import com.android.systemui.statusbar.phone.PhoneStatusBar; +import cyanogenmod.providers.CMSettings; /* A task view */ public class TaskView extends FrameLayout implements Task.TaskCallbacks, @@ -43,6 +44,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, interface TaskViewCallbacks { public void onTaskViewAppIconClicked(TaskView tv); public void onTaskViewAppInfoClicked(TaskView tv); + public void onTaskViewLongClicked(TaskView tv); public void onTaskViewClicked(TaskView tv, Task task, boolean lockToTask); public void onTaskViewDismissed(TaskView tv); public void onTaskViewClipStateChanged(TaskView tv); @@ -769,7 +771,14 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, public boolean onLongClick(View v) { if (v == mHeaderView.mApplicationIcon) { if (mCb != null) { - mCb.onTaskViewAppInfoClicked(this); + boolean showDevShortcuts = + CMSettings.Secure.getInt(v.getContext().getContentResolver(), + CMSettings.Secure.DEVELOPMENT_SHORTCUT, 0) != 0; + if (showDevShortcuts) { + mCb.onTaskViewLongClicked(this); + } else { + mCb.onTaskViewAppInfoClicked(this); + } return true; } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 9e3cf37..260f625 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -430,6 +430,7 @@ class GlobalScreenshot { private MediaActionSound mCameraSound; + private final int mSfHwRotation; /** * @param context everything needs a context :( @@ -496,6 +497,9 @@ class GlobalScreenshot { // Setup the Camera shutter sound mCameraSound = new MediaActionSound(); mCameraSound.load(MediaActionSound.SHUTTER_CLICK); + + // Load hardware rotation from prop + mSfHwRotation = android.os.SystemProperties.getInt("ro.sf.hwrotation",0) / 90; } /** @@ -539,7 +543,10 @@ class GlobalScreenshot { // only in the natural orientation of the device :!) mDisplay.getRealMetrics(mDisplayMetrics); float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels}; - float degrees = getDegreesForRotation(mDisplay.getRotation()); + int rot = mDisplay.getRotation(); + // Allow for abnormal hardware orientation + rot = (rot + mSfHwRotation) % 4; + float degrees = getDegreesForRotation(rot); boolean requiresRotation = (degrees > 0); if (requiresRotation) { // Get the dimensions of the device in its native orientation diff --git a/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java index 77c27fa..e74edd0 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java @@ -42,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 = 2048; + public static final float BRIGHTNESS_ADJ_RESOLUTION = 2048; private final int mMinimumBacklight; private final int mMaximumBacklight; diff --git a/packages/SystemUI/src/com/android/systemui/settings/NotificationBrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/NotificationBrightnessController.java new file mode 100644 index 0000000..617ef8f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/NotificationBrightnessController.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2012-2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.systemui.R; + +import java.lang.Exception; +import java.util.ArrayList; + +import cyanogenmod.providers.CMSettings; + +public class NotificationBrightnessController implements ToggleSlider.Listener { + private static final String TAG = "StatusBar.NotificationBrightnessController"; + + public static final int LIGHT_BRIGHTNESS_MINIMUM = 1; + public static final int LIGHT_BRIGHTNESS_MAXIMUM = 255; + + // Minimum delay between LED notification updates + private final static long LED_UPDATE_DELAY_MS = 250; + + private int mCurrentBrightness; + private final int mMinimumBrightness; + private final int mMaximumBrightness; + + private final Context mContext; + private final ToggleSlider mControl; + private final CurrentUserTracker mUserTracker; + private final Handler mHandler; + private final NotificationBrightnessObserver mBrightnessObserver; + + private ArrayList<BrightnessStateChangeCallback> mChangeCallbacks = + new ArrayList<BrightnessStateChangeCallback>(); + + private boolean mListening; + private boolean mExternalChange; + + private boolean mNotificationAllow; + private final Bundle mNotificationBundle; + private final Notification.Builder mNotificationBuilder; + private NotificationManager mNotificationManager; + + public interface BrightnessStateChangeCallback { + public void onBrightnessLevelChanged(); + } + + /** ContentObserver to watch brightness **/ + private class NotificationBrightnessObserver extends ContentObserver { + + private final Uri NOTIFICATION_LIGHT_BRIGHTNESS_LEVEL_URI = + CMSettings.System.getUriFor(CMSettings.System.NOTIFICATION_LIGHT_BRIGHTNESS_LEVEL); + + public NotificationBrightnessObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (selfChange) return; + try { + mExternalChange = true; + updateSlider(); + for (BrightnessStateChangeCallback cb : mChangeCallbacks) { + cb.onBrightnessLevelChanged(); + } + } finally { + mExternalChange = false; + } + } + + public void startObserving() { + final ContentResolver cr = mContext.getContentResolver(); + cr.unregisterContentObserver(this); + cr.registerContentObserver( + NOTIFICATION_LIGHT_BRIGHTNESS_LEVEL_URI, + false, this, UserHandle.USER_ALL); + } + + public void stopObserving() { + final ContentResolver cr = mContext.getContentResolver(); + cr.unregisterContentObserver(this); + } + + } + + public NotificationBrightnessController(Context context, ToggleSlider control) { + mContext = context; + mControl = control; + mHandler = new Handler(); + mUserTracker = new CurrentUserTracker(mContext) { + @Override + public void onUserSwitched(int newUserId) { + updateSlider(); + } + }; + mBrightnessObserver = new NotificationBrightnessObserver(mHandler); + + mMinimumBrightness = LIGHT_BRIGHTNESS_MINIMUM; + mMaximumBrightness = LIGHT_BRIGHTNESS_MAXIMUM; + mCurrentBrightness = LIGHT_BRIGHTNESS_MAXIMUM; + + mNotificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + mNotificationBundle = new Bundle(); + mNotificationBuilder = new Notification.Builder(mContext); + + mNotificationBundle.putBoolean(Notification.EXTRA_FORCE_SHOW_LIGHTS, true); + mNotificationBuilder.setExtras(mNotificationBundle) + .setContentTitle(mContext.getString(R.string.led_notification_title)) + .setContentText(mContext.getString(R.string.led_notification_text)) + .setSmallIcon(R.drawable.ic_settings) + .setOngoing(true); + } + + private Handler mLedHandler = new Handler() { + public void handleMessage(Message msg) { + updateNotification(); + } + }; + + public void addStateChangedCallback(BrightnessStateChangeCallback cb) { + mChangeCallbacks.add(cb); + } + + public boolean removeStateChangedCallback(BrightnessStateChangeCallback cb) { + return mChangeCallbacks.remove(cb); + } + + @Override + public void onInit(ToggleSlider control) { + // Do nothing + } + + public void registerCallbacks() { + if (mListening) { + return; + } + + // Update the slider and mode before attaching the listener so we don't + // receive the onChanged notifications for the initial values. + mNotificationAllow = true; + updateSlider(); + + mBrightnessObserver.startObserving(); + mUserTracker.startTracking(); + + mControl.setOnChangedListener(this); + mListening = true; + } + + /** Unregister all call backs, both to and from the controller */ + public void unregisterCallbacks() { + if (!mListening) { + return; + } + + mNotificationAllow = false; + mBrightnessObserver.stopObserving(); + mUserTracker.stopTracking(); + mControl.setOnChangedListener(null); + mNotificationManager.cancel(1); + mListening = false; + + CMSettings.System.putIntForUser(mContext.getContentResolver(), + CMSettings.System.NOTIFICATION_LIGHT_BRIGHTNESS_LEVEL, + mCurrentBrightness, UserHandle.USER_CURRENT); + } + + @Override + public void onChanged(ToggleSlider view, boolean tracking, boolean automatic, int value, + boolean stopTracking) { + if (mExternalChange) return; + + mCurrentBrightness = value + mMinimumBrightness; + updateNotification(); + + for (BrightnessStateChangeCallback cb : mChangeCallbacks) { + cb.onBrightnessLevelChanged(); + } + } + + /** Fetch the brightness from the system settings and update the slider */ + private void updateSlider() { + mCurrentBrightness = CMSettings.System.getIntForUser(mContext.getContentResolver(), + CMSettings.System.NOTIFICATION_LIGHT_BRIGHTNESS_LEVEL, + mMaximumBrightness, UserHandle.USER_CURRENT); + + CMSettings.System.putIntForUser(mContext.getContentResolver(), + CMSettings.System.NOTIFICATION_LIGHT_BRIGHTNESS_LEVEL, + mMaximumBrightness, UserHandle.USER_CURRENT); + + mControl.setMax(mMaximumBrightness - mMinimumBrightness); + mControl.setValue(mCurrentBrightness - mMinimumBrightness); + updateNotification(); + } + + /** Fetch the brightness from the system settings and update the slider */ + private void updateNotification() { + // Dampen rate of consecutive LED changes + if (mLedHandler.hasMessages(0)) { + return; + } + + if (mNotificationAllow) { + mLedHandler.sendEmptyMessageDelayed(0, LED_UPDATE_DELAY_MS); + + // Instead of canceling the notification, force it to update with the color. + // Use a white light for a better preview of the brightness. + int notificationColor = 0xFFFFFF | (mCurrentBrightness << 24); + mNotificationBuilder.setLights(notificationColor, 1, 0); + mNotificationManager.notify(1, mNotificationBuilder.build()); + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/NotificationBrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/NotificationBrightnessDialog.java new file mode 100644 index 0000000..82c1b3c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/NotificationBrightnessDialog.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012-2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings; + +import android.app.Activity; +import android.os.Bundle; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.Window; +import android.view.WindowManager; + +import com.android.systemui.R; + +/** A dialog that provides controls for adjusting the notifications brightness. */ +public class NotificationBrightnessDialog extends Activity { + + private NotificationBrightnessController mNotificationBrightnessController; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Window window = getWindow(); + + window.setGravity(Gravity.TOP); + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.requestFeature(Window.FEATURE_NO_TITLE); + + setContentView(R.layout.quick_settings_notification_brightness_dialog); + + final ToggleSlider slider = (ToggleSlider) findViewById(R.id.notification_brightness_slider); + mNotificationBrightnessController = new NotificationBrightnessController(this, slider); + } + + @Override + protected void onStart() { + super.onStart(); + mNotificationBrightnessController.registerCallbacks(); + } + + @Override + protected void onStop() { + super.onStop(); + mNotificationBrightnessController.unregisterCallbacks(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) { + finish(); + } + + return super.onKeyDown(keyCode, event); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java index 7f17885..dba74de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java @@ -34,6 +34,7 @@ import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; import com.android.systemui.R; +import com.android.systemui.ViewInvertHelper; /** * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer} @@ -128,6 +129,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private final int mNormalColor; private final int mLowPriorityColor; private boolean mIsBelowSpeedBump; + private ViewInvertHelper mBackgroundNormalInvertHelper; + private ViewInvertHelper mBackgroundDimmedInvertHelper; public ActivatableNotificationView(Context context, AttributeSet attrs) { super(context, attrs); @@ -145,6 +148,9 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mNormalColor = context.getColor(R.color.notification_material_background_color); mLowPriorityColor = context.getColor( R.color.notification_material_background_low_priority_color); + int roundedRectCornerRadius = getResources().getDimensionPixelSize( + R.dimen.notification_material_rounded_rect_radius); + setRoundCornerRadius(roundedRectCornerRadius); // Themes: For drop-shadow rounded corners mTintedRippleColor = context.getColor( R.color.notification_ripple_tinted_color); mLowPriorityRippleColor = context.getColor( @@ -160,6 +166,10 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed); mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg); mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim); + mBackgroundNormalInvertHelper = + new ViewInvertHelper(mBackgroundNormal, DARK_ANIMATION_LENGTH); + mBackgroundDimmedInvertHelper = + new ViewInvertHelper(mBackgroundDimmed, DARK_ANIMATION_LENGTH); updateBackground(); updateBackgroundTint(); } @@ -393,15 +403,15 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView */ private void fadeInFromDark(long delay) { final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal; - background.setAlpha(0f); - background.setPivotX(mBackgroundDimmed.getWidth() / 2f); - background.setPivotY(getActualHeight() / 2f); - background.setScaleX(DARK_EXIT_SCALE_START); - background.setScaleY(DARK_EXIT_SCALE_START); + if (mDimmed) { + mBackgroundDimmedInvertHelper.fade(false, delay); + mBackgroundNormalInvertHelper.update(false); + } else { + mBackgroundDimmedInvertHelper.update(false); + mBackgroundNormalInvertHelper.fade(false, delay); + } background.animate() .alpha(1f) - .scaleX(1f) - .scaleY(1f) .setDuration(DARK_ANIMATION_LENGTH) .setStartDelay(delay) .setInterpolator(mLinearOutSlowInInterpolator) @@ -409,8 +419,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView @Override public void onAnimationCancel(Animator animation) { // Jump state if we are cancelled - background.setScaleX(1f); - background.setScaleY(1f); background.setAlpha(1f); } }) @@ -463,9 +471,11 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private void updateBackground() { cancelFadeAnimations(); + mBackgroundNormalInvertHelper.update(mDark); + mBackgroundDimmedInvertHelper.update(mDark); if (mDark) { mBackgroundDimmed.setVisibility(View.INVISIBLE); - mBackgroundNormal.setVisibility(View.INVISIBLE); + mBackgroundNormal.setVisibility(View.VISIBLE); } else if (mDimmed) { mBackgroundDimmed.setVisibility(View.VISIBLE); mBackgroundNormal.setVisibility(View.INVISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index 9ff86eb..e958ee1 100644..100755 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -29,6 +29,8 @@ import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; @@ -38,10 +40,13 @@ import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.content.res.ThemeConfig; import android.database.ContentObserver; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.media.session.MediaController; +import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; @@ -87,6 +92,9 @@ 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.cm.SpamFilter; +import com.android.internal.util.cm.SpamFilter.SpamContract.NotificationTable; +import com.android.internal.util.cm.SpamFilter.SpamContract.PackageTable; import com.android.internal.util.NotificationColorUtil; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; @@ -97,6 +105,7 @@ 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.cm.SpamMessageProvider; import com.android.systemui.statusbar.NotificationData.Entry; import com.android.systemui.statusbar.phone.NavigationBarView; import com.android.systemui.statusbar.phone.NotificationGroupManager; @@ -109,6 +118,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import cyanogenmod.providers.CMSettings; + import static com.android.keyguard.KeyguardHostView.OnDismissAction; public abstract class BaseStatusBar extends SystemUI implements @@ -146,6 +157,14 @@ public abstract class BaseStatusBar extends SystemUI implements private static final String BANNER_ACTION_SETUP = "com.android.systemui.statusbar.banner_action_setup"; + protected static final int SYSTEM_UI_VISIBILITY_MASK = 0xffffffff; + + private static final Uri SPAM_MESSAGE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SpamMessageProvider.AUTHORITY) + .appendPath("messages") + .build(); + protected CommandQueue mCommandQueue; protected IStatusBarService mBarService; protected H mHandler = createHandler(); @@ -207,6 +226,8 @@ public abstract class BaseStatusBar extends SystemUI implements protected WindowManager mWindowManager; protected IWindowManager mWindowManagerService; + private NotificationManager mNoMan; + protected abstract void refreshLayout(int layoutDirection); protected Display mDisplay; @@ -237,6 +258,10 @@ public abstract class BaseStatusBar extends SystemUI implements protected AssistManager mAssistManager; + // last theme that was applied in order to detect theme change (as opposed + // to some other configuration change). + protected ThemeConfig mCurrentTheme; + @Override // NotificationData.Environment public boolean isDeviceProvisioned() { return mDeviceProvisioned; @@ -245,8 +270,8 @@ public abstract class BaseStatusBar extends SystemUI implements protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { - final boolean provisioned = 0 != Settings.Global.getInt( - mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0); + final boolean provisioned = 0 != CMSettings.Secure.getInt( + mContext.getContentResolver(), CMSettings.Secure.CM_SETUP_WIZARD_COMPLETED, 0); if (provisioned != mDeviceProvisioned) { mDeviceProvisioned = provisioned; updateNotifications(); @@ -270,6 +295,10 @@ public abstract class BaseStatusBar extends SystemUI implements } }; + public RemoteViews.OnClickHandler getOnClickHandler() { + return mOnClickHandler; + } + private RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() { @Override public boolean onClickHandler( @@ -405,9 +434,7 @@ public abstract class BaseStatusBar extends SystemUI implements } } } else if (BANNER_ACTION_CANCEL.equals(action) || BANNER_ACTION_SETUP.equals(action)) { - NotificationManager noMan = (NotificationManager) - mContext.getSystemService(Context.NOTIFICATION_SERVICE); - noMan.cancel(R.id.notification_hidden); + mNoMan.cancel(R.id.notification_hidden); Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0); @@ -536,6 +563,9 @@ public abstract class BaseStatusBar extends SystemUI implements public void start() { mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); + + mNoMan = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + mDisplay = mWindowManager.getDefaultDisplay(); mDevicePolicyManager = (DevicePolicyManager)mContext.getSystemService( Context.DEVICE_POLICY_SERVICE); @@ -552,8 +582,8 @@ public abstract class BaseStatusBar extends SystemUI implements mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), true, - mSettingsObserver); + CMSettings.Secure.getUriFor(CMSettings.Secure.CM_SETUP_WIZARD_COMPLETED), false, + mSettingsObserver, UserHandle.USER_ALL); mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false, mSettingsObserver); @@ -602,7 +632,7 @@ public abstract class BaseStatusBar extends SystemUI implements mSettingsObserver.onChange(false); // set up disable(switches[0], switches[6], false /* animate */); - setSystemUiVisibility(switches[1], 0xffffffff); + setSystemUiVisibility(switches[1], SYSTEM_UI_VISIBILITY_MASK); topAppWindowChanged(switches[2] != 0); // StatusBarManagerService has a back up of IME token and it's restored here. setImeWindowStatus(binders.get(0), switches[3], switches[4], switches[5] != 0); @@ -701,9 +731,7 @@ public abstract class BaseStatusBar extends SystemUI implements mContext.getString(R.string.hidden_notifications_setup), setupIntent); - NotificationManager noMan = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - noMan.notify(R.id.notification_hidden, note.build()); + mNoMan.notify(R.id.notification_hidden, note.build()); } } @@ -747,6 +775,10 @@ public abstract class BaseStatusBar extends SystemUI implements return null; } + protected MediaController getCurrentMediaController() { + return null; + } + @Override public NotificationGroupManager getGroupManager() { return mGroupManager; @@ -915,6 +947,7 @@ public abstract class BaseStatusBar extends SystemUI implements final View settingsButton = guts.findViewById(R.id.notification_inspect_item); final View appSettingsButton = guts.findViewById(R.id.notification_inspect_app_provided_settings); + final View filterButton = guts.findViewById(R.id.notification_inspect_filter_notification); if (appUid >= 0) { final int appUidF = appUid; settingsButton.setOnClickListener(new View.OnClickListener() { @@ -924,6 +957,26 @@ public abstract class BaseStatusBar extends SystemUI implements } }); + Notification notification = sbn.getNotification(); + filterButton.setVisibility(SpamFilter.hasFilterableContent(notification) + ? View.VISIBLE : View.GONE); + filterButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + AsyncTask.execute(new Runnable() { + @Override + public void run() { + ContentValues values = new ContentValues(); + String message = SpamFilter.getNotificationContent( + sbn.getNotification()); + values.put(NotificationTable.MESSAGE_TEXT, message); + values.put(PackageTable.PACKAGE_NAME, pkg); + mContext.getContentResolver().insert(SPAM_MESSAGE_URI, values); + } + }); + removeNotification(sbn.getKey(), null); + } + }); + final Intent appSettingsQueryIntent = new Intent(Intent.ACTION_MAIN) .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) @@ -953,6 +1006,7 @@ public abstract class BaseStatusBar extends SystemUI implements } else { settingsButton.setVisibility(View.GONE); appSettingsButton.setVisibility(View.GONE); + filterButton.setVisibility(View.GONE); } } @@ -972,6 +1026,10 @@ public abstract class BaseStatusBar extends SystemUI implements } ExpandableNotificationRow row = (ExpandableNotificationRow) v; + if (v instanceof MediaExpandableNotificationRow + && !((MediaExpandableNotificationRow) v).inflateGuts()) { + return false; + } bindGuts(row); // Assume we are a status_bar_notification_row @@ -1092,26 +1150,6 @@ public abstract class BaseStatusBar extends SystemUI implements protected abstract View getStatusBarView(); - protected View.OnTouchListener mRecentsPreloadOnTouchListener = new View.OnTouchListener() { - // additional optimization when we have software system buttons - start loading the recent - // tasks on touch down - @Override - public boolean onTouch(View v, MotionEvent event) { - int action = event.getAction() & MotionEvent.ACTION_MASK; - if (action == MotionEvent.ACTION_DOWN) { - preloadRecents(); - } else if (action == MotionEvent.ACTION_CANCEL) { - cancelPreloadingRecents(); - } else if (action == MotionEvent.ACTION_UP) { - if (!v.isPressed()) { - cancelPreloadingRecents(); - } - - } - return false; - } - }; - /** Proxy for RecentsComponent */ protected void showRecents(boolean triggeredFromAltTab) { @@ -1258,6 +1296,20 @@ public abstract class BaseStatusBar extends SystemUI implements } protected boolean inflateViews(Entry entry, ViewGroup parent) { + final StatusBarNotification sbn = entry.notification; + String themePackageName = mCurrentTheme != null + ? mCurrentTheme.getOverlayPkgNameForApp(sbn.getPackageName()) : null; + boolean inflated = inflateViews(entry, parent, true); + if (!inflated && themePackageName != null + && !ThemeConfig.SYSTEM_DEFAULT.equals(themePackageName)) { + Log.w(TAG, "Couldn't expand themed RemoteViews, trying unthemed for: " + sbn); + inflated = inflateViews(entry, mStackScroller, false); + } + + return inflated; + } + + protected boolean inflateViews(Entry entry, ViewGroup parent, boolean isThemeable) { PackageManager pmUser = getPackageManagerForUser( entry.notification.getUser().getIdentifier()); @@ -1298,8 +1350,19 @@ public abstract class BaseStatusBar extends SystemUI implements // create the row view LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); - row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row, - parent, false); + + // cannot use isMediaNotification() + if (sbn.getNotification().category != null + && sbn.getNotification().category.equals(Notification.CATEGORY_TRANSPORT)) { + row = (MediaExpandableNotificationRow) inflater.inflate( + R.layout.status_bar_notification_row_media, parent, false); + ((MediaExpandableNotificationRow)row).setMediaController( + getCurrentMediaController()); + } else { + row = (ExpandableNotificationRow) inflater.inflate( + R.layout.status_bar_notification_row, + parent, false); + } row.setExpansionLogger(this, entry.notification.getKey()); row.setGroupManager(mGroupManager); } @@ -1323,22 +1386,52 @@ public abstract class BaseStatusBar extends SystemUI implements View contentViewLocal = null; View bigContentViewLocal = null; View headsUpContentViewLocal = null; + String themePackageName = (isThemeable && mCurrentTheme != null) + ? mCurrentTheme.getOverlayPkgNameForApp(sbn.getPackageName()) + : ThemeConfig.SYSTEM_DEFAULT; + String statusBarThemePackageName = (isThemeable && mCurrentTheme != null) + ? mCurrentTheme.getOverlayForStatusBar() + : ThemeConfig.SYSTEM_DEFAULT; + try { contentViewLocal = contentView.apply( sbn.getPackageContext(mContext), contentContainer, - mOnClickHandler); + mOnClickHandler, + statusBarThemePackageName); + + final int platformTemplateRootViewId = + com.android.internal.R.id.status_bar_latest_event_content; + final String inflationThemePackageName; + if (themePackageName != null + && !TextUtils.equals(themePackageName, statusBarThemePackageName) + && contentViewLocal.getId() != platformTemplateRootViewId) { + // This notification uses custom RemoteViews, and its app uses a different + // theme than the status bar. Re-inflate the views using the app's theme, + // as the RemoteViews likely will contain resources of the app, not the platform + inflationThemePackageName = themePackageName; + contentViewLocal = contentView.apply( + sbn.getPackageContext(mContext), + contentContainer, + mOnClickHandler, + inflationThemePackageName); + } else { + inflationThemePackageName = statusBarThemePackageName; + } + if (bigContentView != null) { bigContentViewLocal = bigContentView.apply( sbn.getPackageContext(mContext), contentContainer, - mOnClickHandler); + mOnClickHandler, + inflationThemePackageName); } if (headsUpContentView != null) { headsUpContentViewLocal = headsUpContentView.apply( sbn.getPackageContext(mContext), contentContainer, - mOnClickHandler); + mOnClickHandler, + inflationThemePackageName); } } catch (RuntimeException e) { @@ -1389,8 +1482,10 @@ public abstract class BaseStatusBar extends SystemUI implements } if (publicViewLocal == null) { + final Context layoutContext = isThemeable ? mContext + : maybeGetThemedContext(mContext, ThemeConfig.SYSTEM_DEFAULT); // Add a basic notification template - publicViewLocal = LayoutInflater.from(mContext).inflate( + publicViewLocal = LayoutInflater.from(layoutContext).inflate( R.layout.notification_public_default, contentContainerPublic, false); publicViewLocal.setIsRootNamespace(true); @@ -1403,49 +1498,12 @@ public abstract class BaseStatusBar extends SystemUI implements title.setText(entry.notification.getPackageName()); } - final ImageView icon = (ImageView) publicViewLocal.findViewById(R.id.icon); - final ImageView profileBadge = (ImageView) publicViewLocal.findViewById( - R.id.profile_badge_line3); - - final StatusBarIcon ic = new StatusBarIcon( - entry.notification.getUser(), - entry.notification.getPackageName(), - entry.notification.getNotification().getSmallIcon(), - entry.notification.getNotification().iconLevel, - entry.notification.getNotification().number, - entry.notification.getNotification().tickerText); - - Drawable iconDrawable = StatusBarIconView.getIcon(mContext, ic); - icon.setImageDrawable(iconDrawable); - if (entry.targetSdk >= Build.VERSION_CODES.LOLLIPOP - || mNotificationColorUtil.isGrayscaleIcon(iconDrawable)) { - icon.setBackgroundResource( - com.android.internal.R.drawable.notification_icon_legacy_bg); - int padding = mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.notification_large_icon_circle_padding); - icon.setPadding(padding, padding, padding, padding); - if (sbn.getNotification().color != Notification.COLOR_DEFAULT) { - icon.getBackground().setColorFilter( - sbn.getNotification().color, PorterDuff.Mode.SRC_ATOP); - } - } - - if (profileBadge != null) { - Drawable profileDrawable = mContext.getPackageManager().getUserBadgeForDensity( - entry.notification.getUser(), 0); - if (profileDrawable != null) { - profileBadge.setImageDrawable(profileDrawable); - profileBadge.setVisibility(View.VISIBLE); - } else { - profileBadge.setVisibility(View.GONE); - } - } + updatePublicViewProperties(publicViewLocal, entry); final View privateTime = contentViewLocal.findViewById(com.android.internal.R.id.time); final DateTimeView time = (DateTimeView) publicViewLocal.findViewById(R.id.time); if (privateTime != null && privateTime.getVisibility() == View.VISIBLE) { time.setVisibility(View.VISIBLE); - time.setTime(entry.notification.getNotification().when); } final TextView text = (TextView) publicViewLocal.findViewById(R.id.text); @@ -1862,7 +1920,18 @@ public abstract class BaseStatusBar extends SystemUI implements } private boolean shouldShowOnKeyguard(StatusBarNotification sbn) { - return mShowLockscreenNotifications && !mNotificationData.isAmbient(sbn.getKey()); + if (!mShowLockscreenNotifications || mNotificationData.isAmbient(sbn.getKey())) { + return false; + } + final int showOnKeyguard = mNoMan.getShowNotificationForPackageOnKeyguard( + sbn.getPackageName(), sbn.getUid()); + boolean isKeyguardAllowedForApp = + (showOnKeyguard & Notification.SHOW_ALL_NOTI_ON_KEYGUARD) != 0; + if (isKeyguardAllowedForApp && sbn.isOngoing()) { + isKeyguardAllowedForApp = + (showOnKeyguard & Notification.SHOW_NO_ONGOING_NOTI_ON_KEYGUARD) == 0; + } + return isKeyguardAllowedForApp; } protected void setZenMode(int mode) { @@ -1914,8 +1983,9 @@ public abstract class BaseStatusBar extends SystemUI implements boolean shouldInterrupt = shouldInterrupt(entry, notification); boolean alertAgain = alertAgain(entry, n); + final StatusBarNotification oldNotification = entry.notification; entry.notification = notification; - mGroupManager.onEntryUpdated(entry, entry.notification); + mGroupManager.onEntryUpdated(entry, oldNotification); boolean updateSuccessful = false; if (applyInPlace) { @@ -1957,7 +2027,9 @@ public abstract class BaseStatusBar extends SystemUI implements entry.icon.set(ic); inflateViews(entry, mStackScroller); } - updateHeadsUp(key, entry, shouldInterrupt, alertAgain); + if (mUseHeadsUp) { + updateHeadsUp(key, entry, shouldInterrupt, alertAgain); + } mNotificationData.updateRanking(ranking); updateNotifications(); @@ -2051,6 +2123,7 @@ public abstract class BaseStatusBar extends SystemUI implements final Notification publicVersion = notification.getNotification().publicVersion; final RemoteViews publicContentView = publicVersion != null ? publicVersion.contentView : null; + final View publicLocalView = entry.getPublicContentView(); // Reapply the RemoteViews contentView.reapply(mContext, entry.getContentView(), mOnClickHandler); @@ -2064,13 +2137,18 @@ public abstract class BaseStatusBar extends SystemUI implements headsUpContentView.reapply(notification.getPackageContext(mContext), headsUpChild, mOnClickHandler); } - if (publicContentView != null && entry.getPublicContentView() != null) { - publicContentView.reapply(notification.getPackageContext(mContext), - entry.getPublicContentView(), mOnClickHandler); + if (publicLocalView != null) { + if (publicContentView != null) { + publicContentView.reapply(notification.getPackageContext(mContext), + publicLocalView, mOnClickHandler); + } else { + updatePublicViewProperties(publicLocalView, entry); + } } // update the contentIntent mNotificationClicker.register(entry.row, notification); + applyColorsAndBackgrounds(notification, entry); entry.row.setStatusBarNotification(notification); entry.row.notifyContentUpdated(); entry.row.resetHeight(); @@ -2080,6 +2158,55 @@ public abstract class BaseStatusBar extends SystemUI implements maybeEscalateHeadsUp(); } + private void updatePublicViewProperties(View publicView, Entry entry) { + final StatusBarNotification n = entry.notification; + final ImageView icon = (ImageView) publicView.findViewById(R.id.icon); + final ImageView profileBadge = + (ImageView) publicView.findViewById(R.id.profile_badge_line3); + final DateTimeView time = (DateTimeView) publicView.findViewById(R.id.time); + + if (icon != null) { + final StatusBarIcon ic = new StatusBarIcon( + n.getUser(), n.getPackageName(), + n.getNotification().getSmallIcon(), + n.getNotification().iconLevel, + n.getNotification().number, + n.getNotification().tickerText); + + Drawable iconDrawable = StatusBarIconView.getIcon(mContext, ic); + icon.setImageDrawable(iconDrawable); + if (entry.targetSdk >= Build.VERSION_CODES.LOLLIPOP + || mNotificationColorUtil.isGrayscaleIcon(iconDrawable)) { + icon.setBackgroundResource( + com.android.internal.R.drawable.notification_icon_legacy_bg); + int padding = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.notification_large_icon_circle_padding); + icon.setPadding(padding, padding, padding, padding); + if (n.getNotification().color != Notification.COLOR_DEFAULT) { + icon.getBackground().setColorFilter( + n.getNotification().color, PorterDuff.Mode.SRC_ATOP); + } + } else { + icon.setBackgroundDrawable(null); + } + } + + if (time != null) { + time.setTime(entry.notification.getNotification().when); + } + + if (profileBadge != null) { + Drawable profileDrawable = mContext.getPackageManager().getUserBadgeForDensity( + n.getUser(), 0); + if (profileDrawable != null) { + profileBadge.setImageDrawable(profileDrawable); + profileBadge.setVisibility(View.VISIBLE); + } else { + profileBadge.setVisibility(View.GONE); + } + } + } + private boolean alertAgain(Entry oldEntry, Notification newNotification) { return oldEntry == null || !oldEntry.hasInterrupted() || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; @@ -2215,4 +2342,24 @@ public abstract class BaseStatusBar extends SystemUI implements mAssistManager.startAssist(args); } } + + /** + * Returns a context with the given theme applied or the original context if we fail to get a + * themed context. + */ + private Context maybeGetThemedContext(Context context, String themePkg) { + Context themedContext; + try { + ApplicationInfo ai = context.getPackageManager().getApplicationInfo( + context.getPackageName(), 0); + themedContext = context.createApplicationContext(ai, themePkg, + 0); + } catch (PackageManager.NameNotFoundException e) { + themedContext = null; + } + if (themedContext == null) { + themedContext = context; + } + return themedContext; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 897f5e5..83b2fb5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -78,6 +78,7 @@ public class CommandQueue extends IStatusBar.Stub { private StatusBarIconList mList; private Callbacks mCallbacks; private Handler mHandler = new H(); + private boolean mPaused = false; /** * These methods are called back on the main thread. @@ -295,6 +296,14 @@ public class CommandQueue extends IStatusBar.Stub { } } + public void pause() { + mPaused = true; + } + + public void resume() { + mPaused = false; + } + @Override public void onCameraLaunchGestureDetected(int source) { synchronized (mList) { @@ -305,6 +314,10 @@ public class CommandQueue extends IStatusBar.Stub { private final class H extends Handler { public void handleMessage(Message msg) { + if (mPaused) { + this.sendMessageAtFrontOfQueue(Message.obtain(msg)); + return; + } final int what = msg.what & MSG_MASK; switch (what) { case MSG_ICON: { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CustomTileData.java b/packages/SystemUI/src/com/android/systemui/statusbar/CustomTileData.java new file mode 100644 index 0000000..42974ba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CustomTileData.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 cyanogenmod.app.StatusBarPanelCustomTile; + +import android.util.ArrayMap; + +/** + * Custom tile data to keep track of created 3rd party tiles + */ +public class CustomTileData { + public static final class Entry { + public final String key; + public final StatusBarPanelCustomTile sbc; + + public Entry(StatusBarPanelCustomTile sbc) { + this.key = sbc.persistableKey(); + this.sbc = sbc; + } + } + + private final ArrayMap<String, Entry> mEntries = new ArrayMap<>(); + + public ArrayMap<String, Entry> getEntries() { + return mEntries; + } + + public void add(Entry entry) { + mEntries.put(entry.key, entry); + } + + public Entry remove(String key) { + Entry removed = mEntries.remove(key); + if (removed == null) return null; + return removed; + } + + public Entry get(String key) { + return mEntries.get(key); + } + + public Entry get(int i) { + return mEntries.valueAt(i); + } + + public void clear() { + mEntries.clear(); + } + + public int size() { + return mEntries.size(); + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index 56e9af5..be51e57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -64,7 +64,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { private boolean mShowingPublic; private boolean mSensitive; private boolean mShowingPublicInitialized; - private boolean mHideSensitiveForIntrinsicHeight; + protected boolean mHideSensitiveForIntrinsicHeight; /** * Is this notification expanded by the system. The expansion state can be overridden by the @@ -77,10 +77,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { */ private boolean mExpansionDisabled; - private NotificationContentView mPublicLayout; - private NotificationContentView mPrivateLayout; - private int mMaxExpandHeight; - private int mHeadsUpHeight; + protected NotificationContentView mPublicLayout; + protected NotificationContentView mPrivateLayout; + protected int mMaxExpandHeight; + protected int mHeadsUpHeight; private View mVetoButton; private boolean mClearable; private ExpansionLogger mLogger; @@ -101,7 +101,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { private ValueAnimator mChildExpandAnimator; private float mChildrenExpandProgress; private float mExpandButtonStart; - private ViewStub mGutsStub; + protected ViewStub mGutsStub; private boolean mHasExpandAction; private boolean mIsSystemChildExpanded; private boolean mIsPinned; @@ -443,10 +443,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { mVetoButton = findViewById(R.id.veto); } - public void inflateGuts() { + public boolean inflateGuts() { if (mGuts == null) { mGutsStub.inflate(); } + return false; } private void updateChildrenVisibility(boolean animated) { @@ -531,6 +532,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { public void setExpandable(boolean expandable) { mExpandable = expandable; + setClipToOutline(expandable); } /** @@ -644,7 +646,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { } else if (mChildrenExpanded) { maxContentHeight = mChildrenContainer.getIntrinsicHeight(); } else { - maxContentHeight = getMaxExpandHeight(); + maxContentHeight = mMaxExpandHeight; } return maxContentHeight + getBottomDecorHeight(); } @@ -668,7 +670,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { * * @return whether the view state is currently expanded. */ - private boolean isExpanded() { + protected boolean isExpanded() { return !mExpansionDisabled && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded()) || isUserExpanded()); @@ -702,7 +704,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { return super.isChildInvisible(child) || isInvisibleChildContainer; } - private void updateMaxHeights() { + protected void updateMaxHeights() { int intrinsicBefore = getIntrinsicHeight(); View expandedChild = mPrivateLayout.getExpandedChild(); if (expandedChild == null) { @@ -901,10 +903,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { } } - public int getMaxExpandHeight() { - return mMaxExpandHeight; - } - @Override public boolean isContentExpandable() { NotificationContentView showingLayout = getShowingLayout(); @@ -960,7 +958,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView { return mMaxExpandHeight != 0; } - private NotificationContentView getShowingLayout() { + protected NotificationContentView getShowingLayout() { return mShowingPublic ? mPublicLayout : mPrivateLayout; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java index a6fc4bb..cc50e49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java @@ -34,6 +34,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { private final Rect mOutlineRect = new Rect(); protected final int mRoundedRectCornerRadius; private boolean mCustomOutline; + private float mRoundCornerRadius = 0; private float mOutlineAlpha = 1f; public ExpandableOutlineView(Context context, AttributeSet attrs) { @@ -44,12 +45,12 @@ public abstract class ExpandableOutlineView extends ExpandableView { @Override public void getOutline(View view, Outline outline) { if (!mCustomOutline) { - outline.setRect(0, + outline.setRoundRect(0, mClipTopAmount, getWidth(), - Math.max(getActualHeight(), mClipTopAmount)); + Math.max(getActualHeight(), mClipTopAmount), mRoundCornerRadius); } else { - outline.setRoundRect(mOutlineRect, mRoundedRectCornerRadius); + outline.setRoundRect(mOutlineRect, mRoundCornerRadius); } outline.setAlpha(mOutlineAlpha); } @@ -96,4 +97,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { invalidateOutline(); } + protected void setRoundCornerRadius(float roundRadius) { + mRoundCornerRadius = roundRadius; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java index 71baf57..76858ab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java @@ -42,7 +42,7 @@ public abstract class ExpandableView extends FrameLayout { private boolean mDark; private ArrayList<View> mMatchParentViews = new ArrayList<View>(); private int mClipTopOptimization; - private static Rect mClipRect = new Rect(); + private Rect mClipRect = new Rect(); private boolean mWillBeGone; private int mMinClipTopAmount = 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardAffordanceView.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardAffordanceView.java index 8058933..1f7e687 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardAffordanceView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardAffordanceView.java @@ -25,9 +25,13 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.CanvasProperty; import android.graphics.Color; +import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PorterDuff; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.support.v7.graphics.Palette; import android.util.AttributeSet; import android.view.DisplayListCanvas; import android.view.RenderNodeAnimator; @@ -45,7 +49,7 @@ import com.android.systemui.statusbar.phone.PhoneStatusBar; * An ImageView which does not have overlapping renderings commands and therefore does not need a * layer when alpha is changed. */ -public class KeyguardAffordanceView extends ImageView { +public class KeyguardAffordanceView extends ImageView implements Palette.PaletteAsyncListener { private static final long CIRCLE_APPEAR_DURATION = 80; private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200; @@ -81,6 +85,7 @@ public class KeyguardAffordanceView extends ImageView { private boolean mSupportHardware; private boolean mFinishing; private boolean mLaunchingAffordance; + private ColorFilter mDefaultFilter; private CanvasProperty<Float> mHwCircleRadius; private CanvasProperty<Float> mHwCenterX; @@ -162,21 +167,66 @@ public class KeyguardAffordanceView extends ImageView { canvas.restore(); } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + doPaletteIfNecessary(); + } + + private void doPaletteIfNecessary() { + if (mDefaultFilter != null && getDrawable() instanceof BitmapDrawable) { + Palette.generateAsync(((BitmapDrawable) getDrawable()).getBitmap(), this); + } + } + + public void setPreviewView(View v) { View oldPreviewView = mPreviewView; mPreviewView = v; if (mPreviewView != null) { mPreviewView.setVisibility(mLaunchingAffordance ? oldPreviewView.getVisibility() : INVISIBLE); + mPreviewView.setVisibility(GONE); + addOverlay(); + } + } + + private void addOverlay() { + if (mPreviewView != null) { + mPreviewView.getOverlay().clear(); + if (mDefaultFilter != null) { + ColorDrawable d = new ColorDrawable(mCircleColor); + d.setBounds(0, 0, mPreviewView.getWidth(), mPreviewView.getHeight()); + mPreviewView.getOverlay().add(d); + } } } + public void setDefaultFilter(ColorFilter filter) { + mDefaultFilter = filter; + mCircleColor = Color.WHITE; + addOverlay(); + updateIconColor(); + } + private void updateIconColor() { + if (getDrawable() == null) { + return; + } Drawable drawable = getDrawable().mutate(); float alpha = mCircleRadius / mMinBackgroundRadius; alpha = Math.min(1.0f, alpha); int color = (int) mColorInterpolator.evaluate(alpha, mNormalColor, mInverseColor); - drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); + if (mDefaultFilter != null) { + if (alpha == 0) { + drawable.setColorFilter(mDefaultFilter); + } else { + drawable.setColorFilter(color, PorterDuff.Mode.DST_IN); + } + } else { + drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); + } } private void drawBackgroundCircle(Canvas canvas) { @@ -233,7 +283,7 @@ public class KeyguardAffordanceView extends ImageView { }); animatorToRadius.start(); setImageAlpha(0, true); - if (mPreviewView != null) { + if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) { mPreviewView.setVisibility(View.VISIBLE); mPreviewClipper = ViewAnimationUtils.createCircularReveal( mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius, @@ -336,7 +386,7 @@ public class KeyguardAffordanceView extends ImageView { invalidate(); if (nowHidden) { if (mPreviewView != null) { - mPreviewView.setVisibility(View.INVISIBLE); + mPreviewView.setVisibility(View.GONE); } } } else if (!mCircleWillBeHidden) { @@ -375,7 +425,7 @@ public class KeyguardAffordanceView extends ImageView { mPreviewClipper.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mPreviewView.setVisibility(View.INVISIBLE); + mPreviewView.setVisibility(View.GONE); } }); mPreviewClipper.start(); @@ -551,4 +601,10 @@ public class KeyguardAffordanceView extends ImageView { public void setLaunchingAffordance(boolean launchingAffordance) { mLaunchingAffordance = launchingAffordance; } + + @Override + public void onGenerated(Palette palette) { + mCircleColor = palette.getDarkVibrantColor(Color.WHITE); + addOverlay(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index fd84345..e4a0196 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -20,6 +20,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; import android.hardware.fingerprint.FingerprintManager; @@ -74,16 +75,20 @@ public class KeyguardIndicationController { private int mChargingSpeed; private int mChargingCurrent; private String mMessageToShowOnScreenOn; + private IndicationDirection mIndicationDirection; + private boolean mScreenOnHintsEnabled; public KeyguardIndicationController(Context context, KeyguardIndicationTextView textView, LockIcon lockIcon) { mContext = context; mTextView = textView; mLockIcon = lockIcon; + mIndicationDirection = IndicationDirection.NONE; Resources res = context.getResources(); mSlowThreshold = res.getInteger(R.integer.config_chargingSlowlyThreshold); mFastThreshold = res.getInteger(R.integer.config_chargingFastThreshold); + mScreenOnHintsEnabled = res.getBoolean(R.bool.config_showScreenOnLockScreenHints); mBatteryInfo = IBatteryStats.Stub.asInterface( @@ -121,6 +126,20 @@ public class KeyguardIndicationController { /** * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}. */ + public void showTransientIndication(int transientIndication, IndicationDirection direction) { + showTransientIndication(mContext.getResources().getString(transientIndication), direction); + } + + /** + * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}. + */ + public void showTransientIndication(String transientIndication, IndicationDirection direction) { + showTransientIndication(transientIndication, Color.WHITE, direction); + } + + /** + * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}. + */ public void showTransientIndication(int transientIndication) { showTransientIndication(mContext.getResources().getString(transientIndication)); } @@ -129,15 +148,24 @@ public class KeyguardIndicationController { * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}. */ public void showTransientIndication(String transientIndication) { - showTransientIndication(transientIndication, Color.WHITE); + showTransientIndication(transientIndication, Color.WHITE, IndicationDirection.NONE); } /** * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}. */ public void showTransientIndication(String transientIndication, int textColor) { + showTransientIndication(transientIndication, textColor, IndicationDirection.NONE); + } + + /** + * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}. + */ + public void showTransientIndication(String transientIndication, int textColor, + IndicationDirection direction) { mTransientIndication = transientIndication; mTransientTextColor = textColor; + mIndicationDirection = direction; mHandler.removeMessages(MSG_HIDE_TRANSIENT); updateIndication(); } @@ -153,10 +181,38 @@ public class KeyguardIndicationController { } } + public void cleanup() { + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitor); + mContext.unregisterReceiver(mReceiver); + } + private void updateIndication() { if (mVisible) { + final int color = computeColor(); mTextView.switchIndication(computeIndication()); - mTextView.setTextColor(computeColor()); + mTextView.setTextColor(color); + int top = 0, left = 0, right = 0; + // pad the bottom using ic_empty_space to keep text vertically aligned if needed + int bottom = mScreenOnHintsEnabled ? R.drawable.ic_empty_space : 0; + switch (mIndicationDirection) { + case UP: + top = R.drawable.ic_keyboard_arrow_up; + break; + case DOWN: + bottom = R.drawable.ic_keyboard_arrow_down; + break; + case LEFT: + left = R.drawable.ic_keyboard_arrow_left; + break; + case RIGHT: + right = R.drawable.ic_keyboard_arrow_right; + break; + case NONE: + default: + break; + } + mTextView.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom); + mTextView.setCompoundDrawableTintList(ColorStateList.valueOf(color)); } } @@ -171,6 +227,7 @@ public class KeyguardIndicationController { if (!TextUtils.isEmpty(mTransientIndication)) { return mTransientIndication; } + mIndicationDirection = IndicationDirection.NONE; if (mPowerPluggedIn) { String indication = computePowerIndication(); if (DEBUG_CHARGING_CURRENT) { @@ -321,4 +378,12 @@ public class KeyguardIndicationController { StatusBarKeyguardViewManager statusBarKeyguardViewManager) { mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; } + + public enum IndicationDirection { + NONE, + UP, + DOWN, + LEFT, + RIGHT + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/MediaExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/MediaExpandableNotificationRow.java new file mode 100644 index 0000000..c25f146 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/MediaExpandableNotificationRow.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.statusbar; + +import android.content.Context; +import android.content.res.Resources; +import android.media.session.MediaController; +import android.os.Build; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.R; +import com.android.systemui.cm.UserContentObserver; + +import cyanogenmod.providers.CMSettings; + +public class MediaExpandableNotificationRow extends ExpandableNotificationRow { + + private static final String TAG = MediaExpandableNotificationRow.class.getSimpleName(); + public static final boolean DEBUG = false; + + public static final int MAX_QUEUE_ENTRIES = 3; + + private QueueView mQueue; + + private int mMaxQueueHeight; + private int mRowHeight; + private int mShadowHeight; + private int mDisplayedRows; + + private SettingsObserver mSettingsObserver; + private boolean mQueueEnabled; + + public MediaExpandableNotificationRow(Context context, AttributeSet attrs) { + super(context, attrs); + mSettingsObserver = new SettingsObserver(new Handler()); + mQueueEnabled = isQueueEnabled(context); + + Resources res = mContext.getResources(); + + // 3 * queue_row_height + shadow height + mRowHeight = res.getDimensionPixelSize(R.dimen.queue_row_height); + mShadowHeight = res.getDimensionPixelSize(R.dimen.queue_top_shadow); + } + + public boolean inflateGuts() { + if (getGuts() == null) { + mGutsStub.inflate(); + } + if (!mQueueEnabled) { + return true; + } + return !mQueue.isUserSelectingRow(); + } + + @Override + protected void updateMaxHeights() { + // update queue height based on number of rows + int rows = mQueue != null ? mQueue.getCurrentQueueRowCount() : 0; + if (rows != mDisplayedRows) { + mMaxQueueHeight = rows * mRowHeight; + if (mMaxQueueHeight > 0) { + mMaxQueueHeight += mShadowHeight; + } + mDisplayedRows = rows; + } + + int intrinsicBefore = getIntrinsicHeight(); + View expandedChild = mPrivateLayout.getExpandedChild(); + if (expandedChild == null) { + expandedChild = mPrivateLayout.getContractedChild(); + } + mMaxExpandHeight = expandedChild.getHeight() + mMaxQueueHeight; + + View headsUpChild = mPrivateLayout.getHeadsUpChild(); + if (headsUpChild == null) { + headsUpChild = mPrivateLayout.getContractedChild(); + } + mHeadsUpHeight = headsUpChild.getHeight(); + if (intrinsicBefore != getIntrinsicHeight()) { + notifyHeightChanged(false /* needsAnimation */); + } + invalidateOutline(); + } + + @Override + public int getIntrinsicHeight() { + if (getGuts() != null && getGuts().isShown()) { + return getGuts().getActualHeight(); + } + if (!mQueueEnabled) { + return super.getIntrinsicHeight(); + } + if (mHideSensitiveForIntrinsicHeight) { + return getMinHeight(); + } + if (isUserLocked()) { + return getActualHeight(); + } + boolean inExpansionState = isExpanded(); + if (!inExpansionState) { + // not expanded, so we return the collapsed size + return getMinHeight(); + } + return getMaxContentHeight(); + } + + @Override + public int getMaxContentHeight() { + /** + * calls into NotificationContentView.getMaxHeight() + */ + return getShowingLayout().getMaxHeight() + mMaxQueueHeight; + } + + @Override + public void setHeightRange(int rowMinHeight, int rowMaxHeight) { + super.setHeightRange(rowMinHeight, rowMaxHeight); + mMaxViewHeight = Math.max(rowMaxHeight, getMaxContentHeight()); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mQueue = (QueueView) findViewById(R.id.queue_view); + mQueue.setQueueEnabled(mQueueEnabled); + mQueue.setVisibility(mQueueEnabled ? View.VISIBLE : View.GONE); + } + + public void setMediaController(MediaController mediaController) { + if (DEBUG) Log.d(TAG, "setMediaController() called with " + + "mediaController = [" + mediaController + "]"); + if (mQueue.setController(mediaController) && mQueueEnabled) { + notifyHeightChanged(true); + } + } + + @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); + } + return false; + } + + @Override + protected boolean filterMotionEvent(MotionEvent event) { + if (!mQueueEnabled) { + return super.filterMotionEvent(event); + } + if (isExpanded() && mQueue.isUserSelectingRow() + && event.getActionMasked() != MotionEvent.ACTION_DOWN + && event.getActionMasked() != MotionEvent.ACTION_UP + && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { + // this is for hotspot propogation? + return false; + } + return super.filterMotionEvent(event); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mSettingsObserver.observe(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mSettingsObserver.unobserve(); + } + + private class SettingsObserver extends UserContentObserver { + + public SettingsObserver(Handler handler) { + super(handler); + } + + @Override + protected void observe() { + super.observe(); + mContext.getContentResolver().registerContentObserver( + CMSettings.System.getUriFor(CMSettings.System.NOTIFICATION_PLAY_QUEUE), + true, this); + } + + @Override + protected void unobserve() { + super.unobserve(); + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + protected void update() { + mQueueEnabled = isQueueEnabled(mContext); + mQueue.setQueueEnabled(mQueueEnabled); + mQueue.setVisibility(mQueueEnabled ? View.VISIBLE : View.GONE); + requestLayout(); + } + } + + public static boolean isQueueEnabled(Context context) { + return CMSettings.System.getInt(context.getContentResolver(), + CMSettings.System.NOTIFICATION_PLAY_QUEUE, 1) == 1; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/MediaNotificationGuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/MediaNotificationGuts.java new file mode 100644 index 0000000..14ce8e1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/MediaNotificationGuts.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.Switch; +import android.widget.TextView; +import com.android.systemui.R; +import cyanogenmod.providers.CMSettings; + +/** + * The guts of a media notification revealed when performing a long press. + */ +public class MediaNotificationGuts extends NotificationGuts { + + private static final String TAG = MediaNotificationGuts.class.getSimpleName(); + + private ViewGroup mQueueGroup; + private TextView mText; + private Switch mSwitch; + + public MediaNotificationGuts(Context context, AttributeSet attrs) { + super(context, attrs); + + setWillNotDraw(true); + } + + @Override + protected void onDraw(Canvas canvas) { + // do nothing! + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mQueueGroup = (ViewGroup) findViewById(R.id.queue_group); + mSwitch = (Switch) findViewById(R.id.queue_switch); + mSwitch.setChecked(MediaExpandableNotificationRow.isQueueEnabled(getContext())); + mText = (TextView) findViewById(R.id.switch_label); + mText.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mSwitch.toggle(); + } + }); + mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + buttonView.setChecked(isChecked); + CMSettings.System.putInt(getContext().getContentResolver(), + CMSettings.System.NOTIFICATION_PLAY_QUEUE, + isChecked ? 1 : 0); + } + }); + } + + + @Override + public void setActualHeight(int actualHeight) { + super.setActualHeight(actualHeight); + } + + @Override + public int getActualHeight() { + return getHeight(); + } + + @Override + public boolean hasOverlappingRendering() { + + // Prevents this view from creating a layer when alpha is animating. + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java index aedae52..c8c318b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java @@ -191,6 +191,14 @@ public class NotificationData { return mEntries.get(key); } + public Entry get(int i) { + return mEntries.valueAt(i); + } + + public RankingMap getRankingMap() { + return mRankingMap; + } + public void add(Entry entry, RankingMap ranking) { mEntries.put(entry.notification.getKey(), entry); updateRankingAndSort(ranking); @@ -288,6 +296,14 @@ public class NotificationData { return false; } + public void clear() { + mEntries.clear(); + } + + public int size() { + return mEntries.size(); + } + // Q: What kinds of notifications should show during setup? // A: Almost none! Only things coming from the system (package is "android") that also // have special "kind" tags marking them as relevant for setup (see below). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGuts.java index 46e0bf8..23912c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGuts.java @@ -28,9 +28,9 @@ import com.android.systemui.R; */ public class NotificationGuts extends FrameLayout { - private Drawable mBackground; - private int mClipTopAmount; - private int mActualHeight; + protected Drawable mBackground; + protected int mClipTopAmount; + protected int mActualHeight; public NotificationGuts(Context context, AttributeSet attrs) { super(context, attrs); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QueueView.java b/packages/SystemUI/src/com/android/systemui/statusbar/QueueView.java new file mode 100644 index 0000000..1da2e5e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/QueueView.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.statusbar; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Handler; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.ListView; +import com.android.systemui.R; +import com.android.systemui.cm.UserContentObserver; + +import java.util.ArrayList; +import java.util.List; + +public class QueueView extends LinearLayout implements + QueueViewRow.UserRowInteractionListener, AdapterView.OnItemClickListener, + AdapterView.OnItemLongClickListener { + + private static final String TAG = QueueView.class.getSimpleName(); + private static final boolean DEBUG = MediaExpandableNotificationRow.DEBUG; + + private MediaController mController; + + private List<MediaSession.QueueItem> mQueue = new ArrayList<>(getMaxQueueRowCount()); + + private QueueItemAdapter mAdapter; + private ListView mList; + private boolean mQueueEnabled; + + long mLastUserInteraction = -1; + + private MediaController.Callback mCallback = new MediaController.Callback() { + @Override + public void onPlaybackStateChanged(@NonNull PlaybackState state) { + super.onPlaybackStateChanged(state); + + if (getParent() != null && updateQueue(mController.getQueue())) { + getParent().requestLayout(); + } + } + + @Override + public void onSessionDestroyed() { + if (DEBUG) Log.d(TAG, "onSessionDestroyed() called with " + ""); + super.onSessionDestroyed(); + setController(null); + } + }; + + public QueueView(Context context, AttributeSet attrs) { + super(context, attrs); + mAdapter = new QueueItemAdapter(context); + setClipToOutline(false); + setClipToPadding(false); + } + + public void setQueueEnabled(boolean enabled) { + mQueueEnabled = enabled; + mAdapter.notifyDataSetChanged(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mList = (ListView) findViewById(R.id.queue_list); + mList.setItemsCanFocus(true); + mList.setDrawSelectorOnTop(true); + mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + mList.setAdapter(mAdapter); + mList.setOnItemLongClickListener(this); + mList.setOnItemClickListener(this); + mList.setVerticalScrollBarEnabled(false); + } + + private class QueueItemAdapter extends ArrayAdapter<MediaSession.QueueItem> { + + public QueueItemAdapter(Context context) { + super(context, R.layout.queue_adapter_row, mQueue); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public long getItemId(int position) { + if (position > getCount() - 1) { + return -1; + } + return getItem(position).getQueueId(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final MediaSession.QueueItem queueItem = getItem(position); + + if (convertView == null) { + convertView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.queue_adapter_row, parent, false); + } + + QueueViewRow row = (QueueViewRow) convertView; + row.setHotSpotChangeListener(QueueView.this); + + row.setQueueItem(queueItem); + + return convertView; + } + + @Override + public int getCount() { + if (!mQueueEnabled) { + return 0; + } + return super.getCount(); + } + } + + public boolean isUserSelectingRow() { + final long delta = System.currentTimeMillis() - mLastUserInteraction; + if (DEBUG) Log.i(TAG, "isUserSelectingRow() delta=" + delta); + + if (mLastUserInteraction > 0 && delta < 500) { + if (DEBUG) Log.w(TAG, "user selecting row bc of hotspot change."); + return true; + } + + return false; + } + + public int getMaxQueueRowCount() { + return MediaExpandableNotificationRow.MAX_QUEUE_ENTRIES; + } + + public int getCurrentQueueRowCount() { + if (mAdapter == null) { + return 0; + } + return mAdapter.getCount(); + } + + @Override + public void onHotSpotChanged(float x, float y) { + mLastUserInteraction = System.currentTimeMillis(); + } + + @Override + public void onDrawableStateChanged() { + mLastUserInteraction = System.currentTimeMillis(); + } + + /** + * @param queue + * @return whether the queue size has changed + */ + public boolean updateQueue(List<MediaSession.QueueItem> queue) { + int queueSizeBefore = mAdapter.getCount(); + + mQueue.clear(); + + if (queue != null) { + // add everything *after* the currently playing item + boolean foundNowPlaying = false; + + final PlaybackState playbackState = mController.getPlaybackState(); + + long activeQueueId = -1; + if (playbackState != null) { + activeQueueId = playbackState.getActiveQueueItemId(); + } + + for (int i = 0; i < queue.size() && mQueue.size() < getMaxQueueRowCount(); i++) { + final MediaSession.QueueItem item = queue.get(i); + if (!foundNowPlaying + && activeQueueId != -1 + && activeQueueId == item.getQueueId()) { + foundNowPlaying = true; + continue; + } + if (foundNowPlaying) { + mQueue.add(item); + } + } + + // add everything + if (!foundNowPlaying) { + for(int i = 0; i < getMaxQueueRowCount() && i < queue.size(); i++) { + mQueue.add(queue.get(i)); + } + } + } + mAdapter.notifyDataSetChanged(); + + return mAdapter.getCount() != queueSizeBefore; + } + + public boolean setController(MediaController controller) { + if (mController != null) { + mController.unregisterCallback(mCallback); + } + mController = controller; + if (mController != null) { + mController.registerCallback(mCallback); + } + + return updateQueue(mController != null + ? mController.getQueue() : null); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + final MediaSession.QueueItem itemAtPosition = (MediaSession.QueueItem) + parent.getItemAtPosition(position); + if (itemAtPosition != null && mController != null) { + mController.getTransportControls().skipToQueueItem(itemAtPosition.getQueueId()); + } + mAdapter.notifyDataSetChanged(); + } + + @Override + public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { + return true; + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QueueViewRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/QueueViewRow.java new file mode 100644 index 0000000..dab89ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/QueueViewRow.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.statusbar; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.media.MediaDescription; +import android.media.session.MediaSession; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import com.android.systemui.R; + +public class QueueViewRow extends RelativeLayout { + + private static final String TAG = QueueViewRow.class.getSimpleName(); + + private UserRowInteractionListener mHotSpotChangeListener; + + private ImageView mArt; + private TextView mTitle; + private TextView mSummary; + + public QueueViewRow(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mArt = (ImageView) findViewById(R.id.art); + mTitle = (TextView) findViewById(R.id.title); + mSummary = (TextView) findViewById(R.id.summary); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mHotSpotChangeListener != null) { + mHotSpotChangeListener.onDrawableStateChanged(); + } + } + + @Override + public void dispatchDrawableHotspotChanged(float x, float y) { + super.dispatchDrawableHotspotChanged(x, y); + if (mHotSpotChangeListener != null) { + mHotSpotChangeListener.onHotSpotChanged(x, y); + } + } + + public void setHotSpotChangeListener(UserRowInteractionListener listener) { + mHotSpotChangeListener = listener; + } + + public TextView getTitle() { + return mTitle; + } + + public TextView getSummary() { + return mSummary; + } + + public void setQueueItem(MediaSession.QueueItem queueItem) { + setTag(queueItem); + + MediaDescription metadata = queueItem.getDescription(); + + final Bitmap bitmap = metadata.getIconBitmap(); + mArt.setImageBitmap(bitmap); + mArt.setVisibility(bitmap != null ? View.VISIBLE : View.GONE); + + mTitle.setText(metadata.getTitle()); + mSummary.setText(metadata.getSubtitle()); + } + + /* package */ interface UserRowInteractionListener { + public void onHotSpotChanged(float x, float y); + public void onDrawableStateChanged(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java index cc30882..ecaa809 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java @@ -88,8 +88,6 @@ public class SignalClusterView private int mWideTypeIconStartPadding; private int mSecondaryTelephonyPadding; - private int mEndPadding; - private int mEndPaddingNothingVisible; private boolean mBlockAirplane; private boolean mBlockMobile; @@ -138,9 +136,14 @@ public class SignalClusterView public void setSecurityController(SecurityController sc) { if (DEBUG) Log.d(TAG, "SecurityController=" + sc); + if (sc == null && mSC != null) { + mSC.removeCallback(this); + } mSC = sc; - mSC.addCallback(this); - mVpnVisible = mSC.isVpnEnabled(); + if (mSC != null) { + mSC.addCallback(this); + mVpnVisible = mSC.isVpnEnabled(); + } } @Override @@ -150,10 +153,6 @@ public class SignalClusterView R.dimen.wide_type_icon_start_padding); mSecondaryTelephonyPadding = getContext().getResources().getDimensionPixelSize( R.dimen.secondary_telephony_padding); - mEndPadding = getContext().getResources().getDimensionPixelSize( - R.dimen.signal_cluster_battery_padding); - mEndPaddingNothingVisible = getContext().getResources().getDimensionPixelSize( - R.dimen.no_signal_cluster_battery_padding); } @Override @@ -204,8 +203,10 @@ public class SignalClusterView post(new Runnable() { @Override public void run() { - mVpnVisible = mSC.isVpnEnabled(); - apply(); + if (mSC != null) { + mVpnVisible = mSC.isVpnEnabled(); + apply(); + } } }); } @@ -223,7 +224,7 @@ public class SignalClusterView @Override public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType, int qsType, boolean activityIn, boolean activityOut, String typeContentDescription, - String description, boolean isWide, int subId) { + String description, boolean isWide, boolean showRoamingIndicator, int subId) { PhoneState state = getState(subId); if (state == null) { return; @@ -234,6 +235,7 @@ public class SignalClusterView state.mMobileDescription = statusIcon.contentDescription; state.mMobileTypeDescription = typeContentDescription; state.mIsMobileTypeIconWide = statusType != 0 && isWide; + state.mShowRoamingIndicator = showRoamingIndicator; apply(); } @@ -351,10 +353,13 @@ public class SignalClusterView for (PhoneState state : mPhoneStates) { if (state.mMobile != null) { + state.maybeStopAnimatableDrawable(state.mMobile); state.mMobile.setImageDrawable(null); + state.mLastMobileStrengthId = -1; } if (state.mMobileType != null) { state.mMobileType.setImageDrawable(null); + state.mLastMobileTypeId = -1; } } @@ -447,10 +452,6 @@ public class SignalClusterView } mNoSimsCombo.setVisibility(mNoSimsVisible ? View.VISIBLE : View.GONE); - - boolean anythingVisible = mNoSimsVisible || mWifiVisible || mIsAirplaneMode - || anyMobileVisible || mVpnVisible || mEthernetVisible; - setPaddingRelative(0, 0, anythingVisible ? mEndPadding : mEndPaddingNothingVisible, 0); } public void setIconTint(int tint, float darkIntensity) { @@ -486,11 +487,15 @@ public class SignalClusterView private final int mSubId; private boolean mMobileVisible = false; private int mMobileStrengthId = 0, mMobileTypeId = 0; + private int mLastMobileStrengthId = -1; + private int mLastMobileTypeId = -1; private boolean mIsMobileTypeIconWide; private String mMobileDescription, mMobileTypeDescription; + private boolean mShowRoamingIndicator; private ViewGroup mMobileGroup; private ImageView mMobile, mMobileDark, mMobileType; + private ImageView mMobileRoaming; public PhoneState(int subId, Context context) { ViewGroup root = (ViewGroup) LayoutInflater.from(context) @@ -504,32 +509,25 @@ public class SignalClusterView 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); + mMobileRoaming = (ImageView) root.findViewById(R.id.mobile_roaming); } 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(); - } + if (mLastMobileStrengthId != mMobileStrengthId) { + updateAnimatableIcon(mMobile, mMobileStrengthId); + updateAnimatableIcon(mMobileDark, mMobileStrengthId); + mLastMobileStrengthId = mMobileStrengthId; } - mMobileDark.setImageResource(mMobileStrengthId); - Drawable mobileDarkDrawable = mMobileDark.getDrawable(); - if (mobileDarkDrawable instanceof Animatable) { - Animatable ad = (Animatable) mobileDarkDrawable; - if (!ad.isRunning()) { - ad.start(); - } + if (mLastMobileTypeId != mMobileTypeId) { + mMobileType.setImageResource(mMobileTypeId); + mLastMobileTypeId = mMobileTypeId; } - - mMobileType.setImageResource(mMobileTypeId); mMobileGroup.setContentDescription(mMobileTypeDescription + " " + mMobileDescription); mMobileGroup.setVisibility(View.VISIBLE); + mMobileRoaming.setVisibility(mShowRoamingIndicator ? View.VISIBLE : View.GONE); } else { mMobileGroup.setVisibility(View.GONE); } @@ -550,6 +548,32 @@ public class SignalClusterView return mMobileVisible; } + private void updateAnimatableIcon(ImageView view, int resId) { + maybeStopAnimatableDrawable(view); + view.setImageResource(resId); + maybeStartAnimatableDrawable(view); + } + + private void maybeStopAnimatableDrawable(ImageView view) { + Drawable drawable = view.getDrawable(); + if (drawable instanceof Animatable) { + Animatable ad = (Animatable) drawable; + if (ad.isRunning()) { + ad.stop(); + } + } + } + + private void maybeStartAnimatableDrawable(ImageView view) { + Drawable drawable = view.getDrawable(); + if (drawable instanceof Animatable) { + Animatable ad = (Animatable) drawable; + if (!ad.isRunning()) { + ad.start(); + } + } + } + public void populateAccessibilityEvent(AccessibilityEvent event) { if (mMobileVisible && mMobileGroup != null && mMobileGroup.getContentDescription() != null) { @@ -560,6 +584,7 @@ public class SignalClusterView public void setIconTint(int tint, float darkIntensity) { applyDarkIntensity(darkIntensity, mMobile, mMobileDark); setTint(mMobileType, tint); + setTint(mMobileRoaming, tint); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StackScrollerDecorView.java index 2f66c41..9103525 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StackScrollerDecorView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StackScrollerDecorView.java @@ -87,6 +87,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { mAnimating = true; mContent.animate() .alpha(endValue) + .withLayer() .setInterpolator(interpolator) .setDuration(260) .withEndAction(new Runnable() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index baac8ac..4371cce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -19,12 +19,16 @@ package com.android.systemui.statusbar; import android.app.Notification; import android.content.Context; import android.content.res.Resources; +import android.content.res.ThemeConfig; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.graphics.Typeface; +import android.os.Handler; import android.os.UserHandle; +import android.provider.Settings; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; @@ -34,8 +38,12 @@ import android.widget.ImageView; import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.R; +import com.android.systemui.cm.UserContentObserver; import java.text.NumberFormat; +import java.util.ArrayList; + +import cyanogenmod.providers.CMSettings; public class StatusBarIconView extends AnimatedImageView { private static final String TAG = "StatusBarIconView"; @@ -43,12 +51,14 @@ public class StatusBarIconView extends AnimatedImageView { private StatusBarIcon mIcon; @ViewDebug.ExportedProperty private String mSlot; private Drawable mNumberBackground; - private Paint mNumberPain; + private Paint mNumberPaint; private int mNumberX; private int mNumberY; private String mNumberText; private Notification mNotification; private final boolean mBlocked; + private boolean mShowNotificationCount; + private GlobalSettingsObserver mObserver; public StatusBarIconView(Context context, String slot, Notification notification) { this(context, slot, notification, false); @@ -60,12 +70,10 @@ public class StatusBarIconView extends AnimatedImageView { final Resources res = context.getResources(); mBlocked = blocked; mSlot = slot; - mNumberPain = new Paint(); - mNumberPain.setTextAlign(Paint.Align.CENTER); - mNumberPain.setColor(context.getColor(R.drawable.notification_number_text_color)); - mNumberPain.setAntiAlias(true); setNotification(notification); + mObserver = GlobalSettingsObserver.getInstance(context); + // We do not resize and scale system icons (on the right), only notification icons (on the // left). if (notification != null) { @@ -81,6 +89,8 @@ public class StatusBarIconView extends AnimatedImageView { public void setNotification(Notification notification) { mNotification = notification; + mShowNotificationCount = CMSettings.System.getIntForUser(mContext.getContentResolver(), + CMSettings.System.STATUS_BAR_NOTIF_COUNT, 0, UserHandle.USER_CURRENT) == 1; setContentDescription(notification); } @@ -124,6 +134,10 @@ public class StatusBarIconView extends AnimatedImageView { * Returns whether the set succeeded. */ public boolean set(StatusBarIcon icon) { + return set(icon, false); + } + + private boolean set(StatusBarIcon icon, boolean force) { final boolean iconEquals = mIcon != null && equalIcons(mIcon.icon, icon.icon); final boolean levelEquals = iconEquals && mIcon.iconLevel == icon.iconLevel; @@ -133,17 +147,25 @@ public class StatusBarIconView extends AnimatedImageView { && mIcon.number == icon.number; mIcon = icon.clone(); setContentDescription(icon.contentDescription); - if (!iconEquals) { + if (!iconEquals || force) { if (!updateDrawable(false /* no clear */)) return false; } - if (!levelEquals) { + if (!levelEquals || force) { setImageLevel(icon.iconLevel); } - if (!numberEquals) { - if (icon.number > 0 && getContext().getResources().getBoolean( - R.bool.config_statusBarShowNumber)) { + if (!numberEquals || force) { + if (icon.number > 1 && mShowNotificationCount) { if (mNumberBackground == null) { + final Resources res = mContext.getResources(); + final float densityMultiplier = res.getDisplayMetrics().density; + final float scaledPx = 8 * densityMultiplier; + mNumberPaint = new Paint(); + mNumberPaint.setTextAlign(Paint.Align.CENTER); + mNumberPaint.setColor(res.getColor(R.drawable.notification_number_text_color)); + mNumberPaint.setAntiAlias(true); + mNumberPaint.setTypeface(Typeface.DEFAULT_BOLD); + mNumberPaint.setTextSize(scaledPx); mNumberBackground = getContext().getResources().getDrawable( R.drawable.ic_notification_overlay); } @@ -151,10 +173,11 @@ public class StatusBarIconView extends AnimatedImageView { } else { mNumberBackground = null; mNumberText = null; + mNumberPaint = null; } invalidate(); } - if (!visibilityEquals) { + if (!visibilityEquals || force) { setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE); } return true; @@ -211,6 +234,10 @@ public class StatusBarIconView extends AnimatedImageView { } } + public String getStatusBarSlot() { + return mSlot; + } + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); @@ -231,7 +258,25 @@ public class StatusBarIconView extends AnimatedImageView { if (mNumberBackground != null) { mNumberBackground.draw(canvas); - canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain); + canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPaint); + } + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mObserver != null) { + mObserver.attach(this); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mObserver != null) { + mObserver.detach(this); } } @@ -248,7 +293,7 @@ public class StatusBarIconView extends AnimatedImageView { android.R.integer.status_bar_notification_info_maxnum); if (mIcon.number > tooBig) { str = getContext().getResources().getString( - android.R.string.status_bar_notification_info_overflow); + R.string.status_bar_notification_info_overflow); } else { NumberFormat f = NumberFormat.getIntegerInstance(); str = f.format(mIcon.number); @@ -258,7 +303,7 @@ public class StatusBarIconView extends AnimatedImageView { final int w = getWidth(); final int h = getHeight(); final Rect r = new Rect(); - mNumberPain.getTextBounds(str, 0, str.length(), r); + mNumberPaint.getTextBounds(str, 0, str.length(), r); final int tw = r.right - r.left; final int th = r.bottom - r.top; mNumberBackground.getPadding(r); @@ -292,4 +337,63 @@ public class StatusBarIconView extends AnimatedImageView { public String getSlot() { return mSlot; } + + static class GlobalSettingsObserver extends UserContentObserver { + private static GlobalSettingsObserver sInstance; + private ArrayList<StatusBarIconView> mIconViews = new ArrayList<StatusBarIconView>(); + private Context mContext; + + GlobalSettingsObserver(Handler handler, Context context) { + super(handler); + mContext = context.getApplicationContext(); + } + + static GlobalSettingsObserver getInstance(Context context) { + if (sInstance == null) { + sInstance = new GlobalSettingsObserver(new Handler(), context); + } + return sInstance; + } + + void attach(StatusBarIconView sbiv) { + if (mIconViews.isEmpty()) { + observe(); + } + mIconViews.add(sbiv); + } + + void detach(StatusBarIconView sbiv) { + mIconViews.remove(sbiv); + if (mIconViews.isEmpty()) { + unobserve(); + } + } + + @Override + protected void observe() { + super.observe(); + + mContext.getContentResolver().registerContentObserver( + CMSettings.System.getUriFor(CMSettings.System.STATUS_BAR_NOTIF_COUNT), + false, this); + } + + @Override + protected void unobserve() { + super.unobserve(); + + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + public void update() { + boolean showIconCount = CMSettings.System.getIntForUser(mContext.getContentResolver(), + CMSettings.System.STATUS_BAR_NOTIF_COUNT, 0, UserHandle.USER_CURRENT) == 1; + for (StatusBarIconView sbiv : mIconViews) { + sbiv.mShowNotificationCount = showIconCount; + sbiv.set(sbiv.mIcon, true); + } + } + } } + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VisualizerView.java b/packages/SystemUI/src/com/android/systemui/statusbar/VisualizerView.java new file mode 100644 index 0000000..538140c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/VisualizerView.java @@ -0,0 +1,395 @@ +/* +* Copyright (C) 2015 The CyanogenMod Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package com.android.systemui.statusbar; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.media.audiofx.Visualizer; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.UserHandle; +import android.support.v7.graphics.Palette; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.systemui.cm.UserContentObserver; +import cyanogenmod.providers.CMSettings; + +public class VisualizerView extends View implements Palette.PaletteAsyncListener { + + private static final String TAG = VisualizerView.class.getSimpleName(); + private static final boolean DEBUG = false; + + private Paint mPaint; + private Visualizer mVisualizer; + private ObjectAnimator mVisualizerColorAnimator; + + private ValueAnimator[] mValueAnimators; + private float[] mFFTPoints; + + private int mStatusBarState; + private boolean mVisualizerEnabled = false; + private boolean mVisible = false; + private boolean mPlaying = false; + private boolean mPowerSaveMode = false; + private boolean mDisplaying = false; // the state we're animating to + private boolean mDozing = false; + private boolean mOccluded = false; + + private int mColor; + private Bitmap mCurrentBitmap; + + private SettingsObserver mObserver; + + private Visualizer.OnDataCaptureListener mVisualizerListener = + new Visualizer.OnDataCaptureListener() { + byte rfk, ifk; + int dbValue; + float magnitude; + + @Override + public void onWaveFormDataCapture(Visualizer visualizer, byte[] bytes, int samplingRate) { + } + + @Override + public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) { + for (int i = 0; i < 32; i++) { + mValueAnimators[i].cancel(); + rfk = fft[i * 2 + 2]; + ifk = fft[i * 2 + 3]; + magnitude = rfk * rfk + ifk * ifk; + dbValue = magnitude > 0 ? (int) (10 * Math.log10(magnitude)) : 0; + + mValueAnimators[i].setFloatValues(mFFTPoints[i * 4 + 1], + mFFTPoints[3] - (dbValue * 16f)); + mValueAnimators[i].start(); + } + } + }; + + private final Runnable mLinkVisualizer = new Runnable() { + @Override + public void run() { + if (DEBUG) { + Log.w(TAG, "+++ mLinkVisualizer run()"); + } + + try { + mVisualizer = new Visualizer(0); + } catch (Exception e) { + Log.e(TAG, "error initializing visualizer", e); + return; + } + + mVisualizer.setEnabled(false); + mVisualizer.setCaptureSize(66); + mVisualizer.setDataCaptureListener(mVisualizerListener,Visualizer.getMaxCaptureRate(), + false, true); + mVisualizer.setEnabled(true); + + if (DEBUG) { + Log.w(TAG, "--- mLinkVisualizer run()"); + } + } + }; + + private final Runnable mAsyncUnlinkVisualizer = new Runnable() { + @Override + public void run() { + AsyncTask.execute(mUnlinkVisualizer); + } + }; + + private final Runnable mUnlinkVisualizer = new Runnable() { + @Override + public void run() { + if (DEBUG) { + Log.w(TAG, "+++ mUnlinkVisualizer run(), mVisualizer: " + mVisualizer); + } + if (mVisualizer != null) { + mVisualizer.setEnabled(false); + mVisualizer.release(); + mVisualizer = null; + } + if (DEBUG) { + Log.w(TAG, "--- mUninkVisualizer run()"); + } + } + }; + + public VisualizerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mColor = Color.TRANSPARENT; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(mColor); + + mFFTPoints = new float[128]; + mValueAnimators = new ValueAnimator[32]; + for (int i = 0; i < 32; i++) { + final int j = i * 4 + 1; + mValueAnimators[i] = new ValueAnimator(); + mValueAnimators[i].setDuration(128); + mValueAnimators[i].addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mFFTPoints[j] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + } + } + + public VisualizerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public VisualizerView(Context context) { + this(context, null, 0); + } + + private void updateViewVisibility() { + final int curVis = getVisibility(); + final int newVis = mStatusBarState != StatusBarState.SHADE + && mVisualizerEnabled ? View.VISIBLE : View.GONE; + if (curVis != newVis) { + setVisibility(newVis); + checkStateChanged(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mObserver = new SettingsObserver(new Handler()); + mObserver.observe(); + mObserver.update(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mObserver.unobserve(); + mObserver = null; + mCurrentBitmap = null; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + float barUnit = w / 32f; + float barWidth = barUnit * 8f / 9f; + barUnit = barWidth + (barUnit - barWidth) * 32f / 31f; + mPaint.setStrokeWidth(barWidth); + + for (int i = 0; i < 32; i++) { + mFFTPoints[i * 4] = mFFTPoints[i * 4 + 2] = i * barUnit + (barWidth / 2); + mFFTPoints[i * 4 + 1] = h; + mFFTPoints[i * 4 + 3] = h; + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mVisualizer != null) { + canvas.drawLines(mFFTPoints, mPaint); + } + } + + public void setVisible(boolean visible) { + if (mVisible != visible) { + if (DEBUG) { + Log.i(TAG, "setVisible() called with visible = [" + visible + "]"); + } + mVisible = visible; + checkStateChanged(); + } + } + + public void setDozing(boolean dozing) { + if (mDozing != dozing) { + if (DEBUG) { + Log.i(TAG, "setDozing() called with dozing = [" + dozing + "]"); + } + mDozing = dozing; + checkStateChanged(); + } + } + + public void setPlaying(boolean playing) { + if (mPlaying != playing) { + if (DEBUG) { + Log.i(TAG, "setPlaying() called with playing = [" + playing + "]"); + } + mPlaying = playing; + checkStateChanged(); + } + } + + public void setPowerSaveMode(boolean powerSaveMode) { + if (mPowerSaveMode != powerSaveMode) { + if (DEBUG) { + Log.i(TAG, "setPowerSaveMode() called with powerSaveMode = [" + powerSaveMode + "]"); + } + mPowerSaveMode = powerSaveMode; + checkStateChanged(); + } + } + + public void setOccluded(boolean occluded) { + if (mOccluded != occluded) { + if (DEBUG) { + Log.i(TAG, "setOccluded() called with occluded = [" + occluded + "]"); + } + mOccluded = occluded; + checkStateChanged(); + } + } + + public void setStatusBarState(int statusBarState) { + if (mStatusBarState != statusBarState) { + mStatusBarState = statusBarState; + updateViewVisibility(); + } + } + + public void setBitmap(Bitmap bitmap) { + if (mCurrentBitmap == bitmap) { + return; + } + mCurrentBitmap = bitmap; + if (bitmap != null) { + Palette.generateAsync(bitmap, this); + } else { + setColor(Color.TRANSPARENT); + } + } + + @Override + public void onGenerated(Palette palette) { + int color = Color.TRANSPARENT; + + color = palette.getVibrantColor(color); + if (color == Color.TRANSPARENT) { + color = palette.getLightVibrantColor(color); + if (color == Color.TRANSPARENT) { + color = palette.getDarkVibrantColor(color); + } + } + + setColor(color); + } + + private void setColor(int color) { + if (color == Color.TRANSPARENT) { + color = Color.WHITE; + } + + color = Color.argb(140, Color.red(color), Color.green(color), Color.blue(color)); + + if (mColor != color) { + mColor = color; + + if (mVisualizer != null) { + if (mVisualizerColorAnimator != null) { + mVisualizerColorAnimator.cancel(); + } + + mVisualizerColorAnimator = ObjectAnimator.ofArgb(mPaint, "color", + mPaint.getColor(), mColor); + mVisualizerColorAnimator.setStartDelay(600); + mVisualizerColorAnimator.setDuration(1200); + mVisualizerColorAnimator.start(); + } else { + mPaint.setColor(mColor); + } + } + } + + private void checkStateChanged() { + if (getVisibility() == View.VISIBLE && mVisible && mPlaying && !mDozing && !mPowerSaveMode + && mVisualizerEnabled && !mOccluded) { + if (!mDisplaying) { + mDisplaying = true; + AsyncTask.execute(mLinkVisualizer); + animate() + .alpha(1f) + .withEndAction(null) + .setDuration(800); + } + } else { + if (mDisplaying) { + mDisplaying = false; + if (mVisible) { + animate() + .alpha(0f) + .withEndAction(mAsyncUnlinkVisualizer) + .setDuration(600); + } else { + animate(). + alpha(0f) + .withEndAction(mAsyncUnlinkVisualizer) + .setDuration(0); + } + } + } + } + + private class SettingsObserver extends UserContentObserver { + + public SettingsObserver(Handler handler) { + super(handler); + } + + @Override + protected void update() { + mVisualizerEnabled = CMSettings.Secure.getInt(getContext().getContentResolver(), + CMSettings.Secure.LOCKSCREEN_VISUALIZER_ENABLED, 1) != 0; + checkStateChanged(); + updateViewVisibility(); + } + + @Override + protected void observe() { + super.observe(); + getContext().getContentResolver().registerContentObserver( + CMSettings.Secure.getUriFor(CMSettings.Secure.LOCKSCREEN_VISUALIZER_ENABLED), + false, this, UserHandle.USER_CURRENT); + } + + @Override + protected void unobserve() { + super.unobserve(); + getContext().getContentResolver().unregisterContentObserver(this); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BackButtonDrawable.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BackButtonDrawable.java new file mode 100644 index 0000000..c76b5d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BackButtonDrawable.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.systemui.statusbar.phone; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.FloatProperty; +import android.util.Property; + +public class BackButtonDrawable extends Drawable { + private final Drawable mWrappedDrawable; + private float mRotation; + private Animator mCurrentAnimator; + + private static final int ANIMATION_DURATION = 200; + public static final Property<BackButtonDrawable, Float> ROTATION + = new FloatProperty<BackButtonDrawable>("rotation") { + @Override + public void setValue(BackButtonDrawable object, float value) { + object.setRotation(value); + } + + @Override + public Float get(BackButtonDrawable object) { + return object.getRotation(); + } + }; + + public BackButtonDrawable(Drawable wrappedDrawable) { + mWrappedDrawable = wrappedDrawable; + } + + @Override + public void draw(Canvas canvas) { + final Rect bounds = mWrappedDrawable.getBounds(); + final int boundsCenterX = bounds.width() / 2; + final int boundsCenterY = bounds.height() / 2; + + canvas.translate(boundsCenterX, boundsCenterY); + canvas.rotate(mRotation); + canvas.translate(- boundsCenterX, - boundsCenterY); + + mWrappedDrawable.draw(canvas); + } + + @Override + public void setBounds(Rect bounds) { + mWrappedDrawable.setBounds(bounds); + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + mWrappedDrawable.setBounds(left, top, right, bottom); + } + + @Override + protected void onBoundsChange(Rect bounds) { + mWrappedDrawable.setBounds(bounds); + } + + @Override + public void setAlpha(int alpha) { + mWrappedDrawable.setAlpha(alpha); + if (mCurrentAnimator != null) { + mCurrentAnimator.end(); + } + } + + @Override + public int getAlpha() { + return mWrappedDrawable.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + + @Override + public int getOpacity() { + return mWrappedDrawable.getOpacity(); + } + + @Override + public int getIntrinsicWidth() { + return mWrappedDrawable.getIntrinsicWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mWrappedDrawable.getIntrinsicHeight(); + } + + public void setRotation(float rotation) { + mRotation = rotation; + invalidateSelf(); + } + + public float getRotation() { + return mRotation; + } + + public void setImeVisible(boolean ime) { + if (mCurrentAnimator != null) { + mCurrentAnimator.cancel(); + } + + final float nextRotation = ime ? - 90 : 0; + if (mRotation == nextRotation) { + return; + } + + if (isVisible() && ActivityManager.isHighEndGfx()) { + mCurrentAnimator = ObjectAnimator.ofFloat(this, ROTATION, nextRotation) + .setDuration(ANIMATION_DURATION); + mCurrentAnimator.start(); + } else { + setRotation(nextRotation); + } + } +} 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 1601b83..093d18c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java @@ -57,15 +57,28 @@ public class BarTransitions { private int mMode; - public BarTransitions(View view, int gradientResourceId) { + public BarTransitions(View view, int gradientResourceId, int opaqueColorResourceId, + int semiTransparentColorResourceId, int transparentColorResourceId, + int warningColorResourceId) { mTag = "BarTransitions." + view.getClass().getSimpleName(); mView = view; - mBarBackground = new BarBackgroundDrawable(mView.getContext(), gradientResourceId); + mBarBackground = new BarBackgroundDrawable(mView.getContext(), gradientResourceId, + opaqueColorResourceId, semiTransparentColorResourceId, + transparentColorResourceId, warningColorResourceId); if (HIGH_END) { mView.setBackground(mBarBackground); } } + protected void setGradientResourceId(int gradientResourceId) { + mBarBackground.setGradientResourceId(mView.getContext().getResources(), + gradientResourceId); + } + + public void updateResources(Resources res) { + mBarBackground.updateResources(res); + } + public int getMode() { return mMode; } @@ -119,11 +132,11 @@ public class BarTransitions { } private static class BarBackgroundDrawable extends Drawable { - private final int mOpaque; - private final int mSemiTransparent; - private final int mTransparent; - private final int mWarning; - private final Drawable mGradient; + private int mOpaque; + private int mSemiTransparent; + private int mTransparent; + private int mWarning; + private Drawable mGradient; private final TimeInterpolator mInterpolator; private int mMode = -1; @@ -137,7 +150,15 @@ public class BarTransitions { private int mGradientAlphaStart; private int mColorStart; - public BarBackgroundDrawable(Context context, int gradientResourceId) { + private int mGradientResourceId; + private final int mOpaqueColorResourceId; + private final int mSemiTransparentColorResourceId; + private final int mTransparentColorResourceId; + private final int mWarningColorResourceId; + + public BarBackgroundDrawable(Context context, int gradientResourceId, + int opaqueColorResourceId, int semiTransparentColorResourceId, + int transparentColorResourceId, int warningColorResourceId) { final Resources res = context.getResources(); if (DEBUG_COLORS) { mOpaque = 0xff0000ff; @@ -145,13 +166,36 @@ public class BarTransitions { mTransparent = 0x2f0000ff; mWarning = 0xffff0000; } else { - 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); + mOpaque = res.getColor(R.color.system_bar_background_opaque); + mSemiTransparent = res.getColor(R.color.system_bar_background_semi_transparent); + mTransparent = res.getColor(transparentColorResourceId); + mWarning = res.getColor(warningColorResourceId); } mGradient = context.getDrawable(gradientResourceId); mInterpolator = new LinearInterpolator(); + mGradientResourceId = gradientResourceId; + mOpaqueColorResourceId = opaqueColorResourceId; + mSemiTransparentColorResourceId = semiTransparentColorResourceId; + mTransparentColorResourceId = transparentColorResourceId; + mWarningColorResourceId = warningColorResourceId; + } + + public void setGradientResourceId(Resources res, int gradientResourceId) { + mGradient = res.getDrawable(gradientResourceId); + mGradientResourceId = gradientResourceId; + } + + public void updateResources(Resources res) { + mOpaque = res.getColor(mOpaqueColorResourceId); + mSemiTransparent = res.getColor(mSemiTransparentColorResourceId); + mTransparent = res.getColor(mTransparentColorResourceId); + mWarning = res.getColor(mWarningColorResourceId); + // Retrieve the current bounds for mGradient so they can be set to + // the new drawable being loaded, otherwise the bounds will be (0, 0, 0, 0) + // and the gradient will not be drawn. + Rect bounds = mGradient.getBounds(); + mGradient = res.getDrawable(mGradientResourceId); + mGradient.setBounds(bounds); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BlurLayer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BlurLayer.java new file mode 100644 index 0000000..a966409 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BlurLayer.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2014, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.systemui.statusbar.phone; + +import android.graphics.PixelFormat; +import android.util.Slog; +import android.view.SurfaceControl; +import android.view.SurfaceSession; + +import java.io.PrintWriter; + +public class BlurLayer { + private static final String TAG = "BlurLayer"; + private static final boolean DEBUG = true; + private SurfaceControl mBlurSurface; + private int mLayer = -1; + private float mAlpha = 0; + private float mBlur = 0; + private int mX, mY; + private int mW, mH; + private boolean mIsShow; + + public BlurLayer(SurfaceSession mFxSession, int w, int h, String tag) { + this(mFxSession, 0, 0, w, h, tag); + } + + public BlurLayer(SurfaceSession mFxSession, int x, int y, int w, int h, String tag) { + mX = x; + mY = y; + mW = w; + mH = h; + mIsShow = false; + + SurfaceControl.openTransaction(); + try { + mBlurSurface = new SurfaceControl(mFxSession, TAG+"_"+tag, 16, 16, PixelFormat.OPAQUE, + SurfaceControl.FX_SURFACE_BLUR | SurfaceControl.HIDDEN); + mBlurSurface.setLayerStack(0); + mBlurSurface.setPosition(mX, mY); + mBlurSurface.setSize(mW, mH); + } catch (Exception e) { + Slog.e(TAG, "Exception creating BlurLayer surface", e); + } finally { + SurfaceControl.closeTransaction(); + } + } + + public void setSize(int w, int h) { + if (mBlurSurface != null && (mW != w || mH != h) ) { + SurfaceControl.openTransaction(); + try { + mBlurSurface.setSize(w, h); + mW = w; + mH = h; + } catch (RuntimeException e) { + Slog.w(TAG, "Failure setting setSize immediately", e); + } catch (Exception e) { + Slog.e(TAG, "Exception setSize", e); + } finally { + SurfaceControl.closeTransaction(); + } + } + } + + public void setPosition(int x, int y) { + if (mBlurSurface != null && (mX != x || mY != y) ) { + SurfaceControl.openTransaction(); + try { + mBlurSurface.setPosition(x, y); + mX = x; + mY = y; + } catch (RuntimeException e) { + Slog.w(TAG, "Failure setting setPosition immediately", e); + } catch (Exception e) { + Slog.e(TAG, "Exception setPosition", e); + } finally { + SurfaceControl.closeTransaction(); + } + } + } + + public void setLayer(int layer) { + if (mBlurSurface != null && mLayer != layer) { + SurfaceControl.openTransaction(); + try { + mBlurSurface.setLayer(layer); + mLayer = layer; + } catch (RuntimeException e) { + Slog.w(TAG, "Failure setting setLayer immediately", e); + } catch (Exception e) { + Slog.e(TAG, "Exception setLayer", e); + } finally { + SurfaceControl.closeTransaction(); + } + } + } + + public void setAlpha(float alpha){ + if(mBlurSurface != null && mAlpha != alpha){ + SurfaceControl.openTransaction(); + try { + mBlurSurface.setAlpha(alpha); + mAlpha = alpha; + } catch (RuntimeException e) { + Slog.w(TAG, "Failure setting alpha immediately", e); + } catch (Exception e) { + Slog.e(TAG, "Exception setAlpha", e); + } finally { + SurfaceControl.closeTransaction(); + } + } + } + + public void setBlur(float blur){ + if(mBlurSurface != null && mBlur != blur ){ + SurfaceControl.openTransaction(); + try { + mBlurSurface.setBlur(blur); + mBlur = blur; + } catch (RuntimeException e) { + Slog.w(TAG, "Failure setting blur immediately", e); + } catch (Exception e) { + Slog.e(TAG, "Exception setBlur", e); + } finally { + SurfaceControl.closeTransaction(); + } + } + } + + public void show() { + if(mBlurSurface != null && !mIsShow ){ + try { + mBlurSurface.show(); + mIsShow = true; + } catch (RuntimeException e) { + Slog.w(TAG, "Failure show()", e); + } catch (Exception e) { + Slog.e(TAG, "Exception show()", e); + } finally { + SurfaceControl.closeTransaction(); + } + } + } + + public void hide(){ + if(mBlurSurface != null && mIsShow ){ + try { + mBlurSurface.hide(); + mIsShow = false; + } catch (RuntimeException e) { + Slog.w(TAG, "Failure hide()", e); + } catch (Exception e) { + Slog.e(TAG, "Exception hide()", e); + } finally { + SurfaceControl.closeTransaction(); + } + } + } + + public void destroySurface() { + if (DEBUG) Slog.v(TAG, "destroySurface."); + if (mBlurSurface != null) { + mBlurSurface.destroy(); + mBlurSurface = null; + } + } + +} + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ClockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ClockController.java new file mode 100644 index 0000000..84eeb31 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ClockController.java @@ -0,0 +1,149 @@ +package com.android.systemui.statusbar.phone; + +import com.android.systemui.FontSizeUtils; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.graphics.Color; +import android.os.Handler; +import android.os.UserHandle; +import android.view.View; +import com.android.systemui.R; +import com.android.systemui.cm.UserContentObserver; +import com.android.systemui.statusbar.policy.Clock; + +import cyanogenmod.providers.CMSettings; + +/** + * To control your...clock + */ +public class ClockController { + + public static final int STYLE_HIDE_CLOCK = 0; + public static final int STYLE_CLOCK_RIGHT = 1; + public static final int STYLE_CLOCK_CENTER = 2; + public static final int STYLE_CLOCK_LEFT = 3; + + private final IconMerger mNotificationIcons; + private final Context mContext; + private final SettingsObserver mSettingsObserver; + private Clock mRightClock, mCenterClock, mLeftClock, mActiveClock; + + private int mClockLocation; + private int mAmPmStyle; + private int mIconTint = Color.WHITE; + + class SettingsObserver extends UserContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + @Override + protected void observe() { + super.observe(); + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.STATUS_BAR_AM_PM), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.STATUS_BAR_CLOCK), false, this, UserHandle.USER_ALL); + updateSettings(); + } + + @Override + protected void unobserve() { + super.unobserve(); + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + public void update() { + updateSettings(); + } + } + + public ClockController(View statusBar, IconMerger notificationIcons, Handler handler) { + mRightClock = (Clock) statusBar.findViewById(R.id.clock); + mCenterClock = (Clock) statusBar.findViewById(R.id.center_clock); + mLeftClock = (Clock) statusBar.findViewById(R.id.left_clock); + mNotificationIcons = notificationIcons; + mContext = statusBar.getContext(); + + mActiveClock = mRightClock; + mSettingsObserver = new SettingsObserver(handler); + mSettingsObserver.observe(); + } + + private Clock getClockForCurrentLocation() { + Clock clockForAlignment; + switch (mClockLocation) { + case STYLE_CLOCK_CENTER: + clockForAlignment = mCenterClock; + break; + case STYLE_CLOCK_LEFT: + clockForAlignment = mLeftClock; + break; + case STYLE_CLOCK_RIGHT: + case STYLE_HIDE_CLOCK: + default: + clockForAlignment = mRightClock; + break; + } + return clockForAlignment; + } + + private void updateActiveClock() { + mActiveClock.setVisibility(View.GONE); + if (mClockLocation == STYLE_HIDE_CLOCK) { + return; + } + + mActiveClock = getClockForCurrentLocation(); + mActiveClock.setVisibility(View.VISIBLE); + mActiveClock.setAmPmStyle(mAmPmStyle); + + setClockAndDateStatus(); + setTextColor(mIconTint); + updateFontSize(); + } + + private void updateSettings() { + ContentResolver resolver = mContext.getContentResolver(); + mAmPmStyle = CMSettings.System.getIntForUser(resolver, + CMSettings.System.STATUS_BAR_AM_PM, Clock.AM_PM_STYLE_GONE, + UserHandle.USER_CURRENT); + mClockLocation = CMSettings.System.getIntForUser( + resolver, CMSettings.System.STATUS_BAR_CLOCK, STYLE_CLOCK_RIGHT, + UserHandle.USER_CURRENT); + updateActiveClock(); + } + + private void setClockAndDateStatus() { + if (mNotificationIcons != null) { + mNotificationIcons.setClockAndDateStatus(mClockLocation); + } + } + + public void setVisibility(boolean visible) { + if (mActiveClock != null) { + mActiveClock.setVisibility(visible ? View.VISIBLE : View.GONE); + } + } + + public void setTextColor(int iconTint) { + mIconTint = iconTint; + if (mActiveClock != null) { + mActiveClock.setTextColor(iconTint); + } + } + + public void updateFontSize() { + if (mActiveClock != null) { + FontSizeUtils.updateFontSize(mActiveClock, R.dimen.status_bar_clock_size); + } + } + + public void cleanup() { + mSettingsObserver.unobserve(); + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index 1d890d0..9897098 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -22,6 +22,7 @@ import android.text.TextUtils; import android.util.Log; import android.util.MathUtils; +import com.android.systemui.doze.DozeLog; import com.android.systemui.R; import java.io.PrintWriter; @@ -46,17 +47,20 @@ public class DozeParameters { public void dump(PrintWriter pw) { pw.println(" DozeParameters:"); pw.print(" getDisplayStateSupported(): "); pw.println(getDisplayStateSupported()); - pw.print(" getPulseDuration(pickup=false): "); pw.println(getPulseDuration(false)); - pw.print(" getPulseDuration(pickup=true): "); pw.println(getPulseDuration(true)); - pw.print(" getPulseInDuration(pickup=false): "); pw.println(getPulseInDuration(false)); - pw.print(" getPulseInDuration(pickup=true): "); pw.println(getPulseInDuration(true)); + pw.print(" getPulseDuration(notification): "); pw.println(getPulseDuration(DozeLog.PULSE_REASON_NOTIFICATION)); + pw.print(" getPulseDuration(pickup): "); pw.println(getPulseDuration(DozeLog.PULSE_REASON_SENSOR_PICKUP)); + pw.print(" getPulseDuration(intent): "); pw.println(getPulseDuration(DozeLog.PULSE_REASON_INTENT)); + pw.print(" getPulseInDuration(notification): "); pw.println(getPulseInDuration(DozeLog.PULSE_REASON_NOTIFICATION)); + pw.print(" getPulseInDuration(pickup): "); pw.println(getPulseInDuration(DozeLog.PULSE_REASON_SENSOR_PICKUP)); + pw.print(" getPulseInDuration(intent): "); pw.println(getPulseInDuration(DozeLog.PULSE_REASON_INTENT)); pw.print(" getPulseInVisibleDuration(): "); pw.println(getPulseVisibleDuration()); pw.print(" getPulseOutDuration(): "); pw.println(getPulseOutDuration()); pw.print(" getPulseOnSigMotion(): "); pw.println(getPulseOnSigMotion()); pw.print(" getVibrateOnSigMotion(): "); pw.println(getVibrateOnSigMotion()); pw.print(" getPulseOnPickup(): "); pw.println(getPulseOnPickup()); pw.print(" getVibrateOnPickup(): "); pw.println(getVibrateOnPickup()); - pw.print(" getProxCheckBeforePulse(): "); pw.println(getProxCheckBeforePulse()); + pw.print(" getProxCheckBeforePulse(pickup): "); pw.println(getProxCheckBeforePulse(DozeLog.PULSE_REASON_SENSOR_PICKUP)); + pw.print(" getProxCheckBeforePulse(intent): "); pw.println(getProxCheckBeforePulse(DozeLog.PULSE_REASON_INTENT)); pw.print(" getPulseOnNotifications(): "); pw.println(getPulseOnNotifications()); pw.print(" getPulseSchedule(): "); pw.println(getPulseSchedule()); pw.print(" getPulseScheduleResets(): "); pw.println(getPulseScheduleResets()); @@ -68,14 +72,19 @@ public class DozeParameters { return getBoolean("doze.display.supported", R.bool.doze_display_state_supported); } - public int getPulseDuration(boolean pickup) { - return getPulseInDuration(pickup) + getPulseVisibleDuration() + getPulseOutDuration(); + public int getPulseDuration(int reason) { + return getPulseInDuration(reason) + getPulseVisibleDuration() + getPulseOutDuration(); } - public int getPulseInDuration(boolean pickup) { - return pickup - ? getInt("doze.pulse.duration.in.pickup", R.integer.doze_pulse_duration_in_pickup) - : getInt("doze.pulse.duration.in", R.integer.doze_pulse_duration_in); + public int getPulseInDuration(int reason) { + switch(reason) { + case DozeLog.PULSE_REASON_SENSOR_PICKUP: + return getInt("doze.pulse.duration.in.pickup", R.integer.doze_pulse_duration_in_pickup); + case DozeLog.PULSE_REASON_INTENT: + return getInt("doze.pulse.duration.in.intent", R.integer.doze_pulse_duration_in_intent); + default: + return getInt("doze.pulse.duration.in", R.integer.doze_pulse_duration_in); + } } public int getPulseVisibleDuration() { @@ -102,8 +111,15 @@ public class DozeParameters { return SystemProperties.getBoolean("doze.vibrate.pickup", false); } - public boolean getProxCheckBeforePulse() { - return getBoolean("doze.pulse.proxcheck", R.bool.doze_proximity_check_before_pulse); + public boolean getProxCheckBeforePulse(int reason) { + switch(reason) { + case DozeLog.PULSE_REASON_SENSOR_PICKUP: + return getBoolean("doze.pulse.proxcheck.pickup", R.bool.doze_proximity_check_before_pulse); + case DozeLog.PULSE_REASON_INTENT: + return getBoolean("doze.pulse.proxcheck.intent", R.bool.doze_proximity_check_before_pulse_intent); + default: + return getBoolean("doze.pulse.proxcheck", R.bool.doze_proximity_check_before_pulse); + } } public boolean getPickupPerformsProxCheck() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java index 3ff69c9..b3e0104 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java @@ -115,7 +115,7 @@ public class DozeScrimController { if (isPulsing()) { final boolean pickup = mPulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP; startScrimAnimation(true /* inFront */, 0f, - mDozeParameters.getPulseInDuration(pickup), + mDozeParameters.getPulseInDuration(mPulseReason), pickup ? mPulseInInterpolatorPickup : mPulseInInterpolator, mPulseInFinished); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/FingerprintUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FingerprintUnlockController.java index 2912963..7135836 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/FingerprintUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FingerprintUnlockController.java @@ -173,7 +173,7 @@ public class FingerprintUnlockController extends KeyguardUpdateMonitorCallback { if (DEBUG_FP_WAKELOCK) { Log.i(TAG, "fp wakelock: Authenticated, waking up..."); } - mPowerManager.wakeUp(SystemClock.uptimeMillis()); + mPowerManager.wakeUp(SystemClock.uptimeMillis(), "android.policy:FINGERPRINT"); } releaseFingerprintWakeLock(); switch (mMode) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/IconMerger.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/IconMerger.java index 50ead3d..5750372 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/IconMerger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/IconMerger.java @@ -22,12 +22,14 @@ import android.view.View; import android.widget.LinearLayout; import com.android.systemui.R; +import com.android.systemui.statusbar.policy.Clock; public class IconMerger extends LinearLayout { private static final String TAG = "IconMerger"; private static final boolean DEBUG = false; private int mIconSize; + private int mClockLocation; private View mMoreView; public IconMerger(Context context, AttributeSet attrs) { @@ -50,6 +52,10 @@ public class IconMerger extends LinearLayout { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // we need to constrain this to an integral multiple of our children int width = getMeasuredWidth(); + if (mClockLocation == ClockController.STYLE_CLOCK_CENTER) { + int totalWidth = getResources().getDisplayMetrics().widthPixels; + width = totalWidth / 2 - mIconSize * 2; + } setMeasuredDimension(width - (width % mIconSize), getMeasuredHeight()); } @@ -69,7 +75,14 @@ public class IconMerger extends LinearLayout { } final boolean overflowShown = (mMoreView.getVisibility() == View.VISIBLE); // let's assume we have one more slot if the more icon is already showing - if (overflowShown) visibleChildren --; + if (overflowShown) { + int totalWidth = getResources().getDisplayMetrics().widthPixels; + if ((mClockLocation != ClockController.STYLE_CLOCK_CENTER && + mClockLocation != ClockController.STYLE_CLOCK_LEFT) || + (visibleChildren > (totalWidth / mIconSize / 2 + 1))) { + visibleChildren--; + } + } final boolean moreRequired = visibleChildren * mIconSize > width; if (moreRequired != overflowShown) { post(new Runnable() { @@ -80,4 +93,9 @@ public class IconMerger extends LinearLayout { }); } } + + public void setClockAndDateStatus(int mode) { + mClockLocation = mode; + + } } 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 60ebfdf..e1a345f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java @@ -49,7 +49,7 @@ public class KeyguardAffordanceHelper { private VelocityTracker mVelocityTracker; private boolean mSwipingInProgress; private float mInitialTouchX; - private float mInitialTouchY; + private float mInitialTouchYRaw; private float mTranslation; private float mTranslationOnDown; private int mTouchSlop; @@ -128,7 +128,7 @@ public class KeyguardAffordanceHelper { if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) { return false; } - final float y = event.getY(); + final float y = event.getRawY(); final float x = event.getX(); boolean isUp = false; @@ -146,7 +146,7 @@ public class KeyguardAffordanceHelper { } startSwiping(targetView); mInitialTouchX = x; - mInitialTouchY = y; + mInitialTouchYRaw = y; mTranslationOnDown = mTranslation; initVelocityTracker(); trackMovement(event); @@ -159,7 +159,7 @@ public class KeyguardAffordanceHelper { case MotionEvent.ACTION_MOVE: trackMovement(event); float xDist = x - mInitialTouchX; - float yDist = y - mInitialTouchY; + float yDist = y - mInitialTouchYRaw; float distance = (float) Math.hypot(xDist, yDist); if (!mTouchSlopExeeded && distance > mTouchSlop) { mTouchSlopExeeded = true; @@ -211,8 +211,9 @@ public class KeyguardAffordanceHelper { } private boolean isOnIcon(View icon, float x, float y) { + int[] location = icon.getLocationOnScreen(); float iconX = icon.getX() + icon.getWidth() / 2.0f; - float iconY = icon.getY() + icon.getHeight() / 2.0f; + float iconY = location[1] + icon.getHeight() / 2.0f; double distance = Math.hypot(x - iconX, y - iconY); return distance <= mTouchTargetSize / 2; } @@ -241,6 +242,13 @@ public class KeyguardAffordanceHelper { return false; } + public boolean isOnLockIcon(MotionEvent event) { + final float x = event.getX(); + final float y = event.getRawY(); + + return isOnIcon(mCenterIcon, x, y); + } + public void startHintAnimation(boolean right, Runnable onFinishedListener) { cancelAnimation(); @@ -488,7 +496,7 @@ public class KeyguardAffordanceHelper { float aX = mVelocityTracker.getXVelocity(); float aY = mVelocityTracker.getYVelocity(); float bX = lastX - mInitialTouchX; - float bY = lastY - mInitialTouchY; + float bY = lastY - mInitialTouchYRaw; float bLen = (float) Math.hypot(bX, bY); // Project the velocity onto the distance vector: a * b / |b| float projectedVelocity = (aX * bX + aY * bY) / bLen; 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 14176a6..b244e26 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -29,6 +29,10 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; @@ -42,14 +46,16 @@ import android.telecom.TelecomManager; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.TextView; - import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; @@ -57,6 +63,8 @@ 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.cm.LockscreenShortcutsHelper; +import com.android.systemui.cm.LockscreenShortcutsHelper.Shortcuts; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.KeyguardAffordanceView; import com.android.systemui.statusbar.KeyguardIndicationController; @@ -64,6 +72,8 @@ import com.android.systemui.statusbar.policy.AccessibilityController; import com.android.systemui.statusbar.policy.FlashlightController; import com.android.systemui.statusbar.policy.PreviewInflater; +import java.util.Objects; + import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; @@ -72,7 +82,7 @@ import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityActi * text. */ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickListener, - UnlockMethodCache.OnUnlockMethodChangedListener, + UnlockMethodCache.OnUnlockMethodChangedListener, LockscreenShortcutsHelper.OnChangeListener, AccessibilityController.AccessibilityStateChangedCallback, View.OnLongClickListener { final static String TAG = "PhoneStatusBar/KeyguardBottomAreaView"; @@ -80,6 +90,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL public static final String CAMERA_LAUNCH_SOURCE_AFFORDANCE = "lockscreen_affordance"; public static final String CAMERA_LAUNCH_SOURCE_WIGGLE = "wiggle_gesture"; public static final String CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP = "power_double_tap"; + public static final String CAMERA_LAUNCH_SOURCE_SCREEN_GESTURE = "screen_gesture"; public static final String EXTRA_CAMERA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source"; @@ -110,11 +121,20 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private KeyguardIndicationController mIndicationController; private AccessibilityController mAccessibilityController; private PhoneStatusBar mPhoneStatusBar; + private LockscreenShortcutsHelper mShortcutHelper; + private final ColorMatrixColorFilter mGrayScaleFilter; private final Interpolator mLinearOutSlowInInterpolator; private boolean mUserSetupComplete; private boolean mPrewarmBound; private Messenger mPrewarmMessenger; + private final WindowManager mWindowManager; + private boolean mBottomAreaAttached; + private final WindowManager.LayoutParams mWindowLayoutParams; + private OnInterceptTouchEventListener mInterceptTouchListener; + private BroadcastReceiver mDevicePolicyReceiver; + private Intent mLastCameraIntent; + private final ServiceConnection mPrewarmConnection = new ServiceConnection() { @Override @@ -128,7 +148,48 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } }; - private boolean mLeftIsVoiceAssist; + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (!mBottomAreaAttached) { + addKeyguardBottomArea(false); + } + } else if (mBottomAreaAttached) { + removeKeyguardBottomArea(); + } + super.setVisibility(visibility); + } + + public void expand(boolean expand) { + addKeyguardBottomArea(expand); + } + + private void addKeyguardBottomArea(boolean fullyExpand) { + mWindowLayoutParams.height = fullyExpand ? WindowManager.LayoutParams.MATCH_PARENT : + WindowManager.LayoutParams.WRAP_CONTENT; + if (!mBottomAreaAttached) { + try { + mWindowManager.addView(this, mWindowLayoutParams); + } catch (IllegalStateException e) { + Log.e(TAG, e.getMessage()); + } + mBottomAreaAttached = true; + } else { + mWindowManager.updateViewLayout(this, mWindowLayoutParams); + } + } + + private void removeKeyguardBottomArea() { + if (mBottomAreaAttached) { + try { + mWindowManager.removeView(this); + } catch (IllegalArgumentException e) { + Log.e(TAG, e.getMessage()); + } + mBottomAreaAttached = false; + } + } + private AssistManager mAssistManager; public KeyguardBottomAreaView(Context context) { @@ -148,6 +209,23 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL super(context, attrs, defStyleAttr, defStyleRes); mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); + ColorMatrix cm = new ColorMatrix(); + cm.setSaturation(0); + mGrayScaleFilter = new ColorMatrixColorFilter(cm); + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + mWindowLayoutParams = new WindowManager.LayoutParams(); + mWindowLayoutParams.type = WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL; + mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mWindowLayoutParams.privateFlags = + WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + mWindowLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT; + mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + mWindowLayoutParams.format = PixelFormat.TRANSPARENT; + mWindowLayoutParams.setTitle("KeyguardBottomArea"); + mWindowLayoutParams.gravity = Gravity.BOTTOM; } private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { @@ -158,12 +236,20 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL if (host == mLockIcon) { label = getResources().getString(R.string.unlock_label); } else if (host == mCameraImageView) { - label = getResources().getString(R.string.camera_label); + if (isTargetCustom(Shortcuts.RIGHT_SHORTCUT)) { + label = mShortcutHelper.getFriendlyNameForUri(Shortcuts.RIGHT_SHORTCUT); + } else { + label = getResources().getString(R.string.camera_label); + } } else if (host == mLeftAffordanceView) { - if (mLeftIsVoiceAssist) { - label = getResources().getString(R.string.voice_assist_label); + if (isTargetCustom(Shortcuts.LEFT_SHORTCUT)) { + label = mShortcutHelper.getFriendlyNameForUri(Shortcuts.LEFT_SHORTCUT); } else { - label = getResources().getString(R.string.phone_label); + if (isLeftVoiceAssist()) { + label = getResources().getString(R.string.voice_assist_label); + } else { + label = getResources().getString(R.string.phone_label); + } } } info.addAction(new AccessibilityAction(ACTION_CLICK, label)); @@ -197,20 +283,47 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL mLeftAffordanceView = (KeyguardAffordanceView) findViewById(R.id.left_button); mLockIcon = (LockIcon) findViewById(R.id.lock_icon); mIndicationText = (TextView) findViewById(R.id.keyguard_indication_text); + mShortcutHelper = new LockscreenShortcutsHelper(mContext, this); watchForCameraPolicyChanges(); updateCameraVisibility(); + updateLeftButtonVisibility(); mUnlockMethodCache = UnlockMethodCache.getInstance(getContext()); mUnlockMethodCache.addListener(this); mLockIcon.update(); setClipChildren(false); setClipToPadding(false); mPreviewInflater = new PreviewInflater(mContext, new LockPatternUtils(mContext)); - inflateCameraPreview(); mLockIcon.setOnClickListener(this); mLockIcon.setOnLongClickListener(this); mCameraImageView.setOnClickListener(this); mLeftAffordanceView.setOnClickListener(this); initAccessibility(); + updateCustomShortcuts(); + } + + private void updateCustomShortcuts() { + updateLeftAffordanceIcon(); + updateRightAffordanceIcon(); + inflateCameraPreview(); + } + + private void updateRightAffordanceIcon() { + Drawable drawable; + String contentDescription; + boolean shouldGrayScale = false; + if (isTargetCustom(Shortcuts.RIGHT_SHORTCUT)) { + drawable = mShortcutHelper.getDrawableForTarget(Shortcuts.RIGHT_SHORTCUT); + shouldGrayScale = true; + contentDescription = mShortcutHelper.getFriendlyNameForUri(Shortcuts.RIGHT_SHORTCUT); + } else { + drawable = mContext.getDrawable(R.drawable.ic_camera_alt_24dp); + contentDescription = mContext.getString(R.string.accessibility_camera_button); + } + mCameraImageView.setImageDrawable(drawable); + mCameraImageView.setContentDescription(contentDescription); + mCameraImageView.setDefaultFilter(shouldGrayScale ? mGrayScaleFilter : null); + updateCameraVisibility(); + updateLeftButtonVisibility(); } private void initAccessibility() { @@ -253,11 +366,13 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL public void setPhoneStatusBar(PhoneStatusBar phoneStatusBar) { mPhoneStatusBar = phoneStatusBar; updateCameraVisibility(); // in case onFinishInflate() was called too early + updateLeftButtonVisibility(); } public void setUserSetupComplete(boolean userSetupComplete) { mUserSetupComplete = userSetupComplete; updateCameraVisibility(); + updateLeftButtonVisibility(); updateLeftAffordanceIcon(); } @@ -278,38 +393,66 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL KeyguardUpdateMonitor.getCurrentUser()); } + private void updateLeftButtonVisibility() { + if (mLeftAffordanceView == null) { + return; + } + boolean visible = mUserSetupComplete; + if (visible) { + if (isTargetCustom(Shortcuts.LEFT_SHORTCUT)) { + visible = !mShortcutHelper.isTargetEmpty(Shortcuts.LEFT_SHORTCUT); + } else { + // Display left shortcut + } + } + mLeftAffordanceView.setVisibility(visible ? View.VISIBLE : View.GONE); + } + private void updateCameraVisibility() { if (mCameraImageView == null) { // Things are not set up yet; reply hazy, ask again later return; } - ResolveInfo resolved = resolveCameraIntent(); - boolean visible = !isCameraDisabledByDpm() && resolved != null - && getResources().getBoolean(R.bool.config_keyguardShowCameraAffordance) - && mUserSetupComplete; + boolean visible = mUserSetupComplete; + if (visible) { + if (isTargetCustom(Shortcuts.RIGHT_SHORTCUT)) { + visible = !mShortcutHelper.isTargetEmpty(Shortcuts.RIGHT_SHORTCUT); + } else { + ResolveInfo resolved = resolveCameraIntent(); + visible = !isCameraDisabledByDpm() && resolved != null + && getResources().getBoolean(R.bool.config_keyguardShowCameraAffordance); + } + } mCameraImageView.setVisibility(visible ? View.VISIBLE : View.GONE); } private void updateLeftAffordanceIcon() { - mLeftIsVoiceAssist = canLaunchVoiceAssist(); - int drawableId; - int contentDescription; + Drawable drawable; + String contentDescription; + boolean shouldGrayScale = false; boolean visible = mUserSetupComplete; - if (mLeftIsVoiceAssist) { - drawableId = R.drawable.ic_mic_26dp; - contentDescription = R.string.accessibility_voice_assist_button; + if (mShortcutHelper.isTargetCustom(Shortcuts.LEFT_SHORTCUT)) { + drawable = mShortcutHelper.getDrawableForTarget(Shortcuts.LEFT_SHORTCUT); + shouldGrayScale = true; + contentDescription = mShortcutHelper.getFriendlyNameForUri(Shortcuts.LEFT_SHORTCUT); + visible |= !mShortcutHelper.isTargetEmpty(Shortcuts.LEFT_SHORTCUT); + } else if (canLaunchVoiceAssist()) { + drawable = mContext.getDrawable(R.drawable.ic_mic_26dp); + contentDescription = mContext.getString(R.string.accessibility_voice_assist_button); } else { visible &= isPhoneVisible(); - drawableId = R.drawable.ic_phone_24dp; - contentDescription = R.string.accessibility_phone_button; + drawable = mContext.getDrawable(R.drawable.ic_phone_24dp); + contentDescription = mContext.getString(R.string.accessibility_phone_button); } mLeftAffordanceView.setVisibility(visible ? View.VISIBLE : View.GONE); - mLeftAffordanceView.setImageDrawable(mContext.getDrawable(drawableId)); - mLeftAffordanceView.setContentDescription(mContext.getString(contentDescription)); + mLeftAffordanceView.setImageDrawable(drawable); + mLeftAffordanceView.setContentDescription(contentDescription); + mLeftAffordanceView.setDefaultFilter(shouldGrayScale ? mGrayScaleFilter : null); + updateLeftButtonVisibility(); } public boolean isLeftVoiceAssist() { - return mLeftIsVoiceAssist; + return !isTargetCustom(Shortcuts.LEFT_SHORTCUT) && canLaunchVoiceAssist(); } private boolean isPhoneVisible() { @@ -339,6 +482,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private void watchForCameraPolicyChanges() { final IntentFilter filter = new IntentFilter(); filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); + mDevicePolicyReceiver = new DevicePolicyBroadcastReceiver(); getContext().registerReceiverAsUser(mDevicePolicyReceiver, UserHandle.ALL, filter, null, null); KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallback); @@ -425,8 +569,15 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } public void launchCamera(String source) { - final Intent intent = getCameraIntent(); - intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source); + final Intent intent; + if (source.equals(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) || + source.equals(CAMERA_LAUNCH_SOURCE_SCREEN_GESTURE) || + !mShortcutHelper.isTargetCustom(LockscreenShortcutsHelper.Shortcuts.RIGHT_SHORTCUT)) { + intent = getCameraIntent(); + } else { + intent = mShortcutHelper.getIntent(LockscreenShortcutsHelper.Shortcuts.RIGHT_SHORTCUT); + intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source); + } boolean wouldLaunchResolverActivity = PreviewInflater.wouldLaunchResolverActivity( mContext, intent, KeyguardUpdateMonitor.getCurrentUser()); if (intent == SECURE_CAMERA_INTENT && !wouldLaunchResolverActivity) { @@ -475,7 +626,10 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } public void launchLeftAffordance() { - if (mLeftIsVoiceAssist) { + if (mShortcutHelper.isTargetCustom(Shortcuts.LEFT_SHORTCUT)) { + Intent intent = mShortcutHelper.getIntent(Shortcuts.LEFT_SHORTCUT); + mActivityStarter.startActivity(intent, false /* dismissShade */); + } else if (isLeftVoiceAssist()) { launchVoiceAssist(); } else { launchPhone(); @@ -499,6 +653,9 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } private boolean canLaunchVoiceAssist() { + if (mAssistManager == null) { + return false; + } return mAssistManager.canVoiceAssistBeLaunchedFromKeyguard(); } @@ -523,6 +680,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL if (changedView == this && visibility == VISIBLE) { mLockIcon.update(); updateCameraVisibility(); + updateLeftButtonVisibility(); } } @@ -559,13 +717,27 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL public void onUnlockMethodStateChanged() { mLockIcon.update(); updateCameraVisibility(); + updateLeftButtonVisibility(); } private void inflateCameraPreview() { - mCameraPreview = mPreviewInflater.inflatePreview(getCameraIntent()); - if (mCameraPreview != null) { - mPreviewContainer.addView(mCameraPreview); - mCameraPreview.setVisibility(View.INVISIBLE); + if (isTargetCustom(Shortcuts.RIGHT_SHORTCUT)) { + mPreviewContainer.removeView(mCameraPreview); + } else { + Intent cameraIntent = getCameraIntent(); + if (!Objects.equals(cameraIntent, mLastCameraIntent)) { + if (mCameraPreview != null) { + mPreviewContainer.removeView(mCameraPreview); + } + mCameraPreview = mPreviewInflater.inflatePreview(cameraIntent); + if (mCameraPreview != null) { + mPreviewContainer.addView(mCameraPreview); + } + } + mLastCameraIntent = cameraIntent; + if (mCameraPreview != null) { + mCameraPreview.setVisibility(View.GONE); + } } } @@ -574,7 +746,11 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL if (previewBefore != null) { mPreviewContainer.removeView(previewBefore); } - if (mLeftIsVoiceAssist) { + if (isTargetCustom(Shortcuts.LEFT_SHORTCUT)) { + // Custom shortcuts don't support previews + return; + } + if (isLeftVoiceAssist()) { mLeftPreview = mPreviewInflater.inflatePreviewFromService( mAssistManager.getVoiceInteractorComponentName()); } else { @@ -582,7 +758,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } if (mLeftPreview != null) { mPreviewContainer.addView(mLeftPreview); - mLeftPreview.setVisibility(View.INVISIBLE); + mLeftPreview.setVisibility(View.GONE); } } @@ -615,13 +791,18 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL .setDuration(DOZE_ANIMATION_ELEMENT_DURATION); } - private final BroadcastReceiver mDevicePolicyReceiver = new BroadcastReceiver() { + public void cleanup() { + removeKeyguardBottomArea(); + } + + private final class DevicePolicyBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { post(new Runnable() { @Override public void run() { updateCameraVisibility(); + updateLeftButtonVisibility(); } }); } @@ -632,6 +813,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL @Override public void onUserSwitchComplete(int userId) { updateCameraVisibility(); + updateLeftButtonVisibility(); } @Override @@ -684,4 +866,92 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL updateLeftAffordanceIcon(); updateLeftPreview(); } + + private String getIndexHint(LockscreenShortcutsHelper.Shortcuts shortcut) { + if (mShortcutHelper.isTargetCustom(shortcut)) { + boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; + String label = mShortcutHelper.getFriendlyNameForUri(shortcut); + int resId = 0; + switch (shortcut) { + case LEFT_SHORTCUT: + resId = isRtl ? R.string.right_shortcut_hint : R.string.left_shortcut_hint; + break; + case RIGHT_SHORTCUT: + resId = isRtl ? R.string.left_shortcut_hint : R.string.right_shortcut_hint; + break; + } + return mContext.getString(resId, label); + } else { + return null; + } + } + + public String getLeftHint() { + String label = getIndexHint(LockscreenShortcutsHelper.Shortcuts.LEFT_SHORTCUT); + if (label == null) { + if (isLeftVoiceAssist()) { + label = mContext.getString(R.string.voice_hint); + } else { + label = mContext.getString(R.string.phone_hint); + } + } + return label; + } + + public String getRightHint() { + String label = getIndexHint(LockscreenShortcutsHelper.Shortcuts.RIGHT_SHORTCUT); + if (label == null) { + label = mContext.getString(R.string.camera_hint); + } + return label; + } + + public boolean isTargetCustom(LockscreenShortcutsHelper.Shortcuts shortcut) { + return mShortcutHelper.isTargetCustom(shortcut); + } + + @Override + public void onChange() { + updateCustomShortcuts(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mAccessibilityController != null) { + mAccessibilityController.addStateChangedCallback(this); + } + mShortcutHelper.registerAndFetchTargets(); + updateCustomShortcuts(); + mUnlockMethodCache.addListener(this); + watchForCameraPolicyChanges(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mAccessibilityController.removeStateChangedCallback(this); + if (mDevicePolicyReceiver != null) { + mContext.unregisterReceiver(mDevicePolicyReceiver); + mDevicePolicyReceiver = null; + } + mShortcutHelper.cleanup(); + mUnlockMethodCache.removeListener(this); + } + + public interface OnInterceptTouchEventListener { + boolean onInterceptTouchEvent(MotionEvent e); + } + + public void setOnInterceptTouchListener(OnInterceptTouchEventListener listener) { + mInterceptTouchListener = listener; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mInterceptTouchListener != null) { + return mInterceptTouchListener.onInterceptTouchEvent(ev); + } + return super.onInterceptTouchEvent(ev); + } } 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 893b352..d992b17 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -32,6 +32,8 @@ import com.android.keyguard.R; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.DejankUtils; +import org.cyanogenmod.internal.util.CmLockPatternUtils; + import static com.android.keyguard.KeyguardHostView.OnDismissAction; import static com.android.keyguard.KeyguardSecurityModel.SecurityMode; @@ -40,15 +42,21 @@ import static com.android.keyguard.KeyguardSecurityModel.SecurityMode; */ public class KeyguardBouncer { + public static final int UNLOCK_SEQUENCE_DEFAULT = 0; + public static final int UNLOCK_SEQUENCE_BOUNCER_FIRST = 1; + public static final int UNLOCK_SEQUENCE_FORCE_BOUNCER = 2; + private Context mContext; private ViewMediatorCallback mCallback; private LockPatternUtils mLockPatternUtils; + private CmLockPatternUtils mCmLockPatternUtils; private ViewGroup mContainer; private StatusBarWindowManager mWindowManager; private KeyguardHostView mKeyguardView; private ViewGroup mRoot; private boolean mShowingSoon; private int mBouncerPromptReason; + private PhoneStatusBar mPhoneStatusBar; private KeyguardUpdateMonitorCallback mUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { @Override @@ -59,12 +67,14 @@ public class KeyguardBouncer { public KeyguardBouncer(Context context, ViewMediatorCallback callback, LockPatternUtils lockPatternUtils, StatusBarWindowManager windowManager, - ViewGroup container) { + ViewGroup container, PhoneStatusBar phoneStatusBar) { mContext = context; mCallback = callback; mLockPatternUtils = lockPatternUtils; mContainer = container; mWindowManager = windowManager; + mCmLockPatternUtils = new CmLockPatternUtils(mContext); + mPhoneStatusBar = phoneStatusBar; KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallback); } @@ -78,7 +88,15 @@ public class KeyguardBouncer { if (mRoot.getVisibility() == View.VISIBLE || mShowingSoon) { return; } - + // ensure external keyguard view does not have focus + mPhoneStatusBar.unfocusKeyguardExternalView(); + mPhoneStatusBar.getScrimController().forceHideScrims(false); + // Don't hide bottom area if we are in the middle of a affordance + // launch transition, since once the animation is finished, NPV + // will take care of setting it invisible. + if (!mPhoneStatusBar.mNotificationPanel.isLaunchTransitionRunning()) { + mPhoneStatusBar.mKeyguardBottomArea.setVisibility(View.GONE); + } // 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()) { @@ -195,7 +213,7 @@ public class KeyguardBouncer { mRoot.setSystemUiVisibility(View.STATUS_BAR_DISABLE_HOME); } - private void removeView() { + void removeView() { if (mRoot != null && mRoot.getParent() == mContainer) { mContainer.removeView(mRoot); mRoot = null; @@ -207,28 +225,42 @@ public class KeyguardBouncer { } /** - * @return True if and only if the security method should be shown before showing the - * notifications on Keyguard, like SIM PIN/PUK. + * @return Whether the bouncer should be shown first, this could be because of SIM PIN/PUK + * or it just could be chosen to be shown first. */ - public boolean needsFullscreenBouncer() { + public int needsFullscreenBouncer() { ensureView(); if (mKeyguardView != null) { SecurityMode mode = mKeyguardView.getSecurityMode(); - return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk; + if (mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk) + return UNLOCK_SEQUENCE_FORCE_BOUNCER; + // "Bouncer first" mode currently only available to some security methods. + else if ((mode == SecurityMode.Pattern || mode == SecurityMode.Password + || mode == SecurityMode.PIN) && (mLockPatternUtils != null && + mCmLockPatternUtils.shouldPassToSecurityView( + KeyguardUpdateMonitor.getCurrentUser()))) + return UNLOCK_SEQUENCE_BOUNCER_FIRST; } - return false; + return UNLOCK_SEQUENCE_DEFAULT; } /** * Like {@link #needsFullscreenBouncer}, but uses the currently visible security method, which * makes this method much faster. */ - public boolean isFullscreenBouncer() { + public int isFullscreenBouncer() { if (mKeyguardView != null) { SecurityMode mode = mKeyguardView.getCurrentSecurityMode(); - return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk; + if (mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk) + return UNLOCK_SEQUENCE_FORCE_BOUNCER; + // "Bouncer first" mode currently only available to some security methods. + else if ((mode == SecurityMode.Pattern || mode == SecurityMode.Password + || mode == SecurityMode.PIN) && (mLockPatternUtils != null && + mCmLockPatternUtils.shouldPassToSecurityView( + KeyguardUpdateMonitor.getCurrentUser()))) + return UNLOCK_SEQUENCE_BOUNCER_FIRST; } - return false; + return UNLOCK_SEQUENCE_DEFAULT; } /** 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 b93fc76..ec307de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project - * + * Copyright (C) 2016 The CyanogenMod Project * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -29,37 +29,37 @@ import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; +import com.android.systemui.BatteryLevelTextView; import com.android.systemui.BatteryMeterView; +import com.android.systemui.DockBatteryMeterView; import com.android.systemui.R; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.DockBatteryController; 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; - /** * The header group on Keyguard. */ -public class KeyguardStatusBarView extends RelativeLayout - implements BatteryController.BatteryStateChangeCallback { +public class KeyguardStatusBarView extends RelativeLayout { - private boolean mBatteryCharging; private boolean mKeyguardUserSwitcherShowing; - private boolean mBatteryListening; private TextView mCarrierLabel; private View mSystemIconsSuperContainer; private MultiUserSwitch mMultiUserSwitch; private ImageView mMultiUserAvatar; - private TextView mBatteryLevel; + private BatteryLevelTextView mBatteryLevel; + private BatteryLevelTextView mDockBatteryLevel; - private BatteryController mBatteryController; private KeyguardUserSwitcher mKeyguardUserSwitcher; private int mSystemIconsSwitcherHiddenExpandedMargin; private Interpolator mFastOutSlowInInterpolator; + private UserInfoController mUserInfoController; + public KeyguardStatusBarView(Context context, AttributeSet attrs) { super(context, attrs); } @@ -70,12 +70,14 @@ public class KeyguardStatusBarView extends RelativeLayout mSystemIconsSuperContainer = findViewById(R.id.system_icons_super_container); mMultiUserSwitch = (MultiUserSwitch) findViewById(R.id.multi_user_switch); mMultiUserAvatar = (ImageView) findViewById(R.id.multi_user_avatar); - mBatteryLevel = (TextView) findViewById(R.id.battery_level); + mBatteryLevel = (BatteryLevelTextView) findViewById(R.id.battery_level_text); + mDockBatteryLevel = (BatteryLevelTextView) findViewById(R.id.dock_battery_level_text); mCarrierLabel = (TextView) findViewById(R.id.keyguard_carrier_text); loadDimens(); mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(), android.R.interpolator.fast_out_slow_in); updateUserSwitcher(); + updateVisibilities(); } @Override @@ -86,8 +88,14 @@ public class KeyguardStatusBarView extends RelativeLayout mCarrierLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize( com.android.internal.R.dimen.text_size_small_material)); - mBatteryLevel.setTextSize(TypedValue.COMPLEX_UNIT_PX, - getResources().getDimensionPixelSize(R.dimen.battery_level_text_size)); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mUserInfoController != null) { + mUserInfoController.removeListener(mUserInfoChangedListener); + } } private void loadDimens() { @@ -104,7 +112,10 @@ public class KeyguardStatusBarView extends RelativeLayout } else if (mMultiUserSwitch.getParent() == this && mKeyguardUserSwitcherShowing) { removeView(mMultiUserSwitch); } - mBatteryLevel.setVisibility(mBatteryCharging ? View.VISIBLE : View.GONE); + mBatteryLevel.setVisibility(View.VISIBLE); + if (mDockBatteryLevel != null) { + mDockBatteryLevel.setVisibility(View.VISIBLE); + } } private void updateSystemIconsLayoutParams() { @@ -117,18 +128,6 @@ public class KeyguardStatusBarView extends RelativeLayout } } - public void setListening(boolean listening) { - if (listening == mBatteryListening) { - return; - } - mBatteryListening = listening; - if (mBatteryListening) { - mBatteryController.addStateChangedCallback(this); - } else { - mBatteryController.removeStateChangedCallback(this); - } - } - private void updateUserSwitcher() { boolean keyguardSwitcherAvailable = mKeyguardUserSwitcher != null; mMultiUserSwitch.setClickable(keyguardSwitcherAvailable); @@ -137,37 +136,43 @@ public class KeyguardStatusBarView extends RelativeLayout } public void setBatteryController(BatteryController batteryController) { - mBatteryController = batteryController; - ((BatteryMeterView) findViewById(R.id.battery)).setBatteryController(batteryController); + BatteryMeterView v = ((BatteryMeterView) findViewById(R.id.battery)); + v.setBatteryStateRegistar(batteryController); + v.setBatteryController(batteryController); + mBatteryLevel.setBatteryStateRegistar(batteryController); } - public void setUserSwitcherController(UserSwitcherController controller) { - mMultiUserSwitch.setUserSwitcherController(controller); + public void setDockBatteryController(DockBatteryController dockBatteryController) { + DockBatteryMeterView v = ((DockBatteryMeterView) findViewById(R.id.dock_battery)); + if (dockBatteryController != null) { + v.setBatteryStateRegistar(dockBatteryController); + mDockBatteryLevel.setBatteryStateRegistar(dockBatteryController); + } else { + if (v != null ) { + removeView(v); + } + if (mDockBatteryLevel != null) { + removeView(mDockBatteryLevel); + mDockBatteryLevel = null; + } + } } - public void setUserInfoController(UserInfoController userInfoController) { - userInfoController.addListener(new UserInfoController.OnUserInfoChangedListener() { - @Override - public void onUserInfoChanged(String name, Drawable picture) { - mMultiUserAvatar.setImageDrawable(picture); - } - }); + public void setUserSwitcherController(UserSwitcherController controller) { + mMultiUserSwitch.setUserSwitcherController(controller); } - @Override - public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { - String percentage = NumberFormat.getPercentInstance().format((double) level / 100.0); - mBatteryLevel.setText(percentage); - boolean changed = mBatteryCharging != charging; - mBatteryCharging = charging; - if (changed) { - updateVisibilities(); + private UserInfoController.OnUserInfoChangedListener mUserInfoChangedListener = + new UserInfoController.OnUserInfoChangedListener() { + @Override + public void onUserInfoChanged(String name, Drawable picture) { + mMultiUserAvatar.setImageDrawable(picture); } - } + }; - @Override - public void onPowerSaveChanged() { - // could not care less + public void setUserInfoController(UserInfoController userInfoController) { + mUserInfoController = userInfoController; + userInfoController.addListener(mUserInfoChangedListener); } public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java index 8e58d14..cf39655 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java @@ -61,7 +61,8 @@ public class LockIcon extends KeyguardAffordanceView { @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); - if (isShown()) { + if (isShown() && + KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive()) { mTrustDrawable.start(); } else { mTrustDrawable.stop(); 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 e70d146..e89cd3f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java @@ -30,6 +30,7 @@ import android.widget.FrameLayout; import com.android.systemui.R; import com.android.systemui.qs.QSPanel; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; +import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.statusbar.policy.UserSwitcherController; /** @@ -37,16 +38,20 @@ import com.android.systemui.statusbar.policy.UserSwitcherController; */ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener { + public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile"; + private QSPanel mQsPanel; private KeyguardUserSwitcher mKeyguardUserSwitcher; private boolean mKeyguardMode; private UserSwitcherController.BaseUserAdapter mUserListener; final UserManager mUserManager; + private ActivityStarter mActivityStarter; private final int[] mTmpInt2 = new int[2]; private UserSwitcherController mUserSwitcherController; + private UserInfoController mUserInfoController; public MultiUserSwitch(Context context, AttributeSet attrs) { super(context, attrs); @@ -101,6 +106,10 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener } } + public void setActivityStarter(ActivityStarter activityStarter) { + mActivityStarter = activityStarter; + } + @Override public void onClick(View v) { if (UserSwitcherController.isUserSwitcherAvailable(mUserManager)) { @@ -120,10 +129,20 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener mTmpInt2); } } else { - Intent intent = ContactsContract.QuickContact.composeQuickContactsIntent( - getContext(), v, ContactsContract.Profile.CONTENT_URI, - ContactsContract.QuickContact.MODE_LARGE, null); - getContext().startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + Intent intent; + if (mUserInfoController == null || mUserInfoController.isProfileSetup()) { + intent = ContactsContract.QuickContact.composeQuickContactsIntent( + getContext(), v, ContactsContract.Profile.CONTENT_URI, + ContactsContract.QuickContact.MODE_LARGE, null); + } else { + intent = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI); + intent.putExtra(INTENT_EXTRA_NEW_LOCAL_PROFILE, true); + } + if (mActivityStarter != null) { + mActivityStarter.startActivity(intent, true /* dismissShade */); + } else { + getContext().startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + } } } @@ -171,4 +190,7 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener return false; } + public void setUserInfoController(UserInfoController userInfoController) { + mUserInfoController = userInfoController; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavBarInsetLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavBarInsetLayout.java new file mode 100644 index 0000000..2028132 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavBarInsetLayout.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.os.Handler; +import android.os.IBinder; +import android.os.UserHandle; +import android.util.AttributeSet; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; +import android.widget.FrameLayout; +import com.android.systemui.R; +import com.android.systemui.cm.UserContentObserver; +import com.android.systemui.statusbar.BaseStatusBar; +import cyanogenmod.providers.CMSettings; + +public class NavBarInsetLayout extends FrameLayout { + public static final String TAG = "NavBarInsetLayout"; + public static final boolean DEBUG = BaseStatusBar.DEBUG; + + boolean mLeftInsetMode = false; + + private int mLeftInset = 0; + private int mRightInset = 0; + + private final Paint mTransparentSrcPaint = new Paint(); + + private SettingsObserver mSettingsObserver; + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mSettingsObserver.observe(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mSettingsObserver.unobserve(); + } + + public NavBarInsetLayout(Context context, AttributeSet attrs) { + super(context, attrs); + setMotionEventSplittingEnabled(false); + mTransparentSrcPaint.setColor(0); + mTransparentSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + + mSettingsObserver = new SettingsObserver(new Handler()); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + if (getFitsSystemWindows()) { + boolean paddingChanged; + + if (mLeftInsetMode) { + paddingChanged = insets.right != getPaddingRight() + || insets.top != getPaddingTop() + || insets.bottom != getPaddingBottom(); + + if (insets.left != mLeftInset) { + mLeftInset = insets.left; + applyMargins(); + } + } else { + paddingChanged = insets.left != getPaddingLeft() + || insets.top != getPaddingTop() + || insets.bottom != getPaddingBottom(); + + if (insets.right != mRightInset) { + mRightInset = insets.right; + applyMargins(); + } + } + + // Drop top inset, apply left inset and pass through bottom inset. + if (paddingChanged) { + setPadding(mLeftInsetMode ? 0 : insets.left, + 0, + mLeftInsetMode ? insets.right : 0, + 0); + } + insets.left = 0; + insets.top = 0; + insets.right = 0; + } else { + boolean applyMargins = false; + if (mLeftInset != 0) { + mLeftInset = 0; + applyMargins = true; + } + if (mRightInset != 0) { + mRightInset = 0; + applyMargins = true; + } + if (applyMargins) { + applyMargins(); + } + boolean changed = getPaddingLeft() != 0 + || getPaddingRight() != 0 + || getPaddingTop() != 0 + || getPaddingBottom() != 0; + if (changed) { + setPadding(0, 0, 0, 0); + } + insets.top = 0; + } + return false; + } + + private void applyMargins() { + final int N = getChildCount(); + for (int i = 0; i < N; i++) { + View child = getChildAt(i); + if (child.getLayoutParams() instanceof InsetLayoutParams) { + InsetLayoutParams lp = (InsetLayoutParams) child.getLayoutParams(); + if (!lp.ignoreRightInset) { + if (mLeftInsetMode && lp.leftMargin != mLeftInset) { + lp.leftMargin = mLeftInset; + if (lp.rightMargin != 0) { + lp.rightMargin = 0; + } + } else if (lp.rightMargin != mRightInset) { + lp.rightMargin = mRightInset; + if (lp.leftMargin != 0) { + lp.leftMargin = 0; + } + } + child.requestLayout(); + } + } + } + } + + @Override + public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new InsetLayoutParams(getContext(), attrs); + } + + @Override + protected FrameLayout.LayoutParams generateDefaultLayoutParams() { + return new InsetLayoutParams(InsetLayoutParams.MATCH_PARENT, + InsetLayoutParams.MATCH_PARENT); + } + + private class SettingsObserver extends UserContentObserver { + + public SettingsObserver(Handler handler) { + super(handler); + } + + @Override + protected void observe() { + super.observe(); + mContext.getContentResolver().registerContentObserver( + CMSettings.System.getUriFor(CMSettings.System.NAVBAR_LEFT_IN_LANDSCAPE), false, + this, UserHandle.USER_CURRENT); + update(); + } + + @Override + protected void unobserve() { + super.unobserve(); + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + protected void update() { + boolean before = mLeftInsetMode; + mLeftInsetMode = CMSettings.System.getIntForUser(mContext.getContentResolver(), + CMSettings.System.NAVBAR_LEFT_IN_LANDSCAPE, 0, UserHandle.USER_CURRENT) == 1; + if (mLeftInsetMode != before) { + applyMargins(); + } + } + } + + public static class InsetLayoutParams extends FrameLayout.LayoutParams { + + public boolean ignoreRightInset; + + public InsetLayoutParams(int width, int height) { + super(width, height); + } + + public InsetLayoutParams(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/NavbarEditor.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavbarEditor.java new file mode 100644 index 0000000..7b4d7f9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavbarEditor.java @@ -0,0 +1,580 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.view.DisplayInfo; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.util.ArrayUtils; +import com.android.systemui.R; +import com.android.systemui.statusbar.policy.KeyButtonView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import cyanogenmod.providers.CMSettings; + +/** + * Handles the editing of the navigation bar + * @author Danesh M + * @hide + */ +public class NavbarEditor implements View.OnTouchListener { + /** + * Holds reference to all assignable button ids + */ + private static final int[] BUTTON_IDS = + { R.id.one, R.id.two, R.id.three, R.id.four, R.id.five, R.id.six }; + + /** + * Subset of BUTTON_IDS, to differentiate small/side buttons + * since they can be assigned additional functionality. + */ + private static final int[] SMALL_BUTTON_IDS = { R.id.one, R.id.six }; + + // holds the button views in the order they currently appear on screen + private final ArrayList<KeyButtonView> mButtonViews; + private final boolean mRtl; + + private Context mContext; + private static Boolean sIsDevicePhone = null; + private boolean mInEditMode = false; + + // Holds reference to the parent/root of the inflated view + private View mParent; + + // Button chooser dialog + private AlertDialog mDialog; + + // true == we're in landscape mode + private boolean mVertical; + // true == we're currently checking for long press + private boolean mLongPressed; + // start point of the current drag operation + private float mDragOrigin; + + // just to avoid reallocations + private static final int[] sLocation = new int[2]; + + private Resources mResources; + + /** + * Longpress runnable to assign buttons in edit mode + */ + private Runnable mCheckLongPress = new Runnable() { + public void run() { + if (mInEditMode) { + mLongPressed = true; + mParent.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + } + } + }; + + //Available buttons + public static final ButtonInfo NAVBAR_EMPTY = new ButtonInfo("empty", + R.string.navbar_empty_button, R.string.accessibility_clear_all, + 0, R.drawable.ic_sysbar_add, + R.drawable.ic_sysbar_add_land, R.drawable.ic_sysbar_add_side); + public static final ButtonInfo NAVBAR_HOME = new ButtonInfo("home", + R.string.navbar_home_button, R.string.accessibility_home, + KeyEvent.KEYCODE_HOME, R.drawable.ic_sysbar_home, + R.drawable.ic_sysbar_home, R.drawable.ic_sysbar_home); + public static final ButtonInfo NAVBAR_BACK = new ButtonInfo("back", + R.string.navbar_back_button, R.string.accessibility_back, + KeyEvent.KEYCODE_BACK, R.drawable.ic_sysbar_back, + R.drawable.ic_sysbar_back, R.drawable.ic_sysbar_back_side); + public static final ButtonInfo NAVBAR_SEARCH = new ButtonInfo("search", + R.string.navbar_search_button, R.string.accessibility_back, + KeyEvent.KEYCODE_SEARCH, R.drawable.ic_sysbar_search, + R.drawable.ic_sysbar_search_land, R.drawable.ic_sysbar_search_side); + public static final ButtonInfo NAVBAR_RECENT = new ButtonInfo("recent", + R.string.navbar_recent_button, R.string.accessibility_recent, + 0, R.drawable.ic_sysbar_recent, + R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_side); + public static final ButtonInfo NAVBAR_CONDITIONAL_MENU = new ButtonInfo("menu0", + R.string.navbar_menu_conditional_button, R.string.accessibility_menu, + KeyEvent.KEYCODE_MENU, R.drawable.ic_sysbar_menu, + R.drawable.ic_sysbar_menu, R.drawable.ic_sysbar_menu); + public static final ButtonInfo NAVBAR_ALWAYS_MENU = new ButtonInfo("menu1", + R.string.navbar_menu_always_button, R.string.accessibility_menu, + KeyEvent.KEYCODE_MENU, R.drawable.ic_sysbar_menu, + R.drawable.ic_sysbar_menu, R.drawable.ic_sysbar_menu); + public static final ButtonInfo NAVBAR_MENU_BIG = new ButtonInfo("menu2", + R.string.navbar_menu_big_button, R.string.accessibility_menu, + KeyEvent.KEYCODE_MENU, R.drawable.ic_sysbar_menu_big, + R.drawable.ic_sysbar_menu_big_land, 0); + public static final ButtonInfo NAVBAR_DPAD_LEFT = new ButtonInfo("dpad_left", + 0, R.string.accessibility_dpad_left, + KeyEvent.KEYCODE_DPAD_LEFT, 0, + 0, R.drawable.ic_sysbar_ime_left); + public static final ButtonInfo NAVBAR_DPAD_RIGHT = new ButtonInfo("dpad_right", + 0, R.string.accessibility_dpad_right, + KeyEvent.KEYCODE_DPAD_RIGHT, 0, + 0, R.drawable.ic_sysbar_ime_right); + + private static final ButtonInfo[] ALL_BUTTONS = new ButtonInfo[] { + NAVBAR_EMPTY, NAVBAR_HOME, NAVBAR_BACK, NAVBAR_SEARCH, + NAVBAR_RECENT, NAVBAR_CONDITIONAL_MENU, NAVBAR_ALWAYS_MENU, NAVBAR_MENU_BIG + }; + + private static final String DEFAULT_SETTING_STRING = "empty|back|home|recent|empty|menu0"; + + public NavbarEditor (View parent, boolean orientation, boolean isRtl, Resources res) { + mContext = parent.getContext(); + mParent = parent; + mVertical = orientation; + mRtl = isRtl; + mResources = res; + + mButtonViews = new ArrayList<KeyButtonView>(); + + KeyButtonView dpadLeft = (KeyButtonView) mParent.findViewById(R.id.dpad_left); + dpadLeft.setInfo(NAVBAR_DPAD_LEFT, orientation, true); + mButtonViews.add(dpadLeft); + + for (int id : BUTTON_IDS) { + mButtonViews.add((KeyButtonView) mParent.findViewById(id)); + } + + KeyButtonView dpadRight = (KeyButtonView) mParent.findViewById(R.id.dpad_right); + dpadRight.setInfo(NAVBAR_DPAD_RIGHT, orientation, true); + mButtonViews.add(dpadRight); + } + + public void setEditMode(boolean editMode) { + mInEditMode = editMode; + for (Integer id : BUTTON_IDS) { + KeyButtonView button = (KeyButtonView) mParent.findViewById(id); + if (button != null) { + button.setEditMode(editMode); + button.setOnTouchListener(editMode ? this : null); + } + } + if (!editMode && mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + } + } + + public static boolean isDevicePhone(Context context) { + if (sIsDevicePhone == null) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayInfo outDisplayInfo = new DisplayInfo(); + + wm.getDefaultDisplay().getDisplayInfo(outDisplayInfo); + + int shortSize = Math.min(outDisplayInfo.logicalHeight, outDisplayInfo.logicalWidth); + int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / outDisplayInfo.logicalDensityDpi; + + // 0-599dp: "phone" UI with a separate status & navigation bar + sIsDevicePhone = shortSizeDp < 600; + } + + return sIsDevicePhone; + } + + /** + * Find intersecting view in mButtonViews + * @param pos - pointer location + * @param v - view being dragged + * @return intersecting view or null + */ + private View findInterceptingView(float pos, View v) { + for (KeyButtonView otherView : mButtonViews) { + if (otherView == v) { + continue; + } + + if (ArrayUtils.contains(SMALL_BUTTON_IDS, otherView.getId())) { + continue; + } + + otherView.getLocationOnScreen(sLocation); + float otherPos = sLocation[mVertical ? 1 : 0]; + float otherDimension = mVertical ? v.getHeight() : v.getWidth(); + + if (pos > (otherPos + otherDimension / 4) && pos < (otherPos + otherDimension)) { + return otherView; + } + } + return null; + } + + @Override + public boolean onTouch(final View view, MotionEvent event) { + if (!mInEditMode || (mDialog != null && mDialog.isShowing())) { + return false; + } + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + view.setPressed(true); + view.getLocationOnScreen(sLocation); + mDragOrigin = sLocation[mVertical ? 1 : 0]; + view.postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + view.setPressed(false); + + if (!mLongPressed || ArrayUtils.contains(SMALL_BUTTON_IDS, view.getId())) { + return false; + } + + ViewGroup viewParent = (ViewGroup) view.getParent(); + float pos = mVertical ? event.getRawY() : event.getRawX(); + float buttonSize = mVertical ? view.getHeight() : view.getWidth(); + float min = mVertical ? viewParent.getTop() : (viewParent.getLeft() - buttonSize / 2); + float max = mVertical ? (viewParent.getTop() + viewParent.getHeight()) + : (viewParent.getLeft() + viewParent.getWidth()); + + // Prevents user from dragging view outside of bounds + if (pos < min || pos > max) { + return false; + } + if (!mVertical) { + view.setX(pos - viewParent.getLeft() - buttonSize / 2); + } else { + view.setY(pos - viewParent.getTop() - buttonSize / 2); + } + View affectedView = findInterceptingView(pos, view); + if (affectedView == null) { + return false; + } + switchId(affectedView, view); + } else if (event.getAction() == MotionEvent.ACTION_UP + || event.getAction() == MotionEvent.ACTION_CANCEL) { + view.setPressed(false); + view.removeCallbacks(mCheckLongPress); + + if (!mLongPressed && !view.getTag().equals(NAVBAR_HOME) && + !view.getTag().equals(NAVBAR_RECENT) && !view.getTag().equals(NAVBAR_BACK)) { + final boolean isSmallButton = ArrayUtils.contains(SMALL_BUTTON_IDS, view.getId()); + final ButtonAdapter list = new ButtonAdapter(mContext, mButtonViews, isSmallButton, + getResources()); + + AlertDialog.Builder builder = new AlertDialog.Builder(mContext) + .setTitle(mContext.getString(R.string.navbar_dialog_title)) + .setAdapter(list, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + KeyButtonView button = (KeyButtonView) view; + ButtonInfo info = list.getItem(which); + + button.setInfo(info, mVertical, isSmallButton); + } + }) + .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + + mDialog = builder.create(); + mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); + mDialog.setCanceledOnTouchOutside(false); + mDialog.show(); + } else { + // Reset the dragged view to its original location + ViewGroup parent = (ViewGroup) view.getParent(); + + if (!mVertical) { + view.setX(mDragOrigin - parent.getLeft()); + } else { + view.setY(mDragOrigin - parent.getTop()); + } + } + mLongPressed = false; + } + return true; + } + + /** + * Switches positions of two views and + * updates their mButtonViews entry + * @param targetView - view to be replaced + * @param view - view being dragged + */ + private void switchId(View targetView, View view) { + ViewGroup parent = (ViewGroup) view.getParent(); + + targetView.getLocationOnScreen(sLocation); + if (!mVertical) { + targetView.setX(mDragOrigin - parent.getLeft()); + mDragOrigin = sLocation[0]; + } else { + targetView.setY(mDragOrigin - parent.getTop()); + mDragOrigin = sLocation[1]; + } + + int targetIndex = mButtonViews.indexOf(targetView); + int draggedIndex = mButtonViews.indexOf(view); + Collections.swap(mButtonViews, draggedIndex, targetIndex); + } + + /** + * Saves the current key arrangement + * to the settings provider + */ + protected void saveKeys() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < BUTTON_IDS.length; i++) { + int idIndex = mVertical && !mRtl ? BUTTON_IDS.length - (i + 1) : i; + ButtonInfo info = (ButtonInfo) mButtonViews.get(idIndex).getTag(); + if (i != 0) sb.append("|"); + sb.append(info.key); + } + CMSettings.System.putStringForUser(mContext.getContentResolver(), + CMSettings.System.NAV_BUTTONS, sb.toString(), UserHandle.USER_CURRENT); + } + + /** + * Updates the buttons according to the + * key arrangement stored in settings provider + */ + protected void updateKeys() { + String saved = CMSettings.System.getStringForUser(mContext.getContentResolver(), + CMSettings.System.NAV_BUTTONS, UserHandle.USER_CURRENT); + if (saved == null) { + saved = DEFAULT_SETTING_STRING; + } + + String[] buttons = saved.split("\\|"); + if (buttons.length < BUTTON_IDS.length) { + buttons = DEFAULT_SETTING_STRING.split("\\|"); + } + + int visibleCount = 0; + + for (int i = 0; i < BUTTON_IDS.length; i++) { + int id = BUTTON_IDS[i]; + int index = mVertical && !mRtl ? BUTTON_IDS.length - i - 1 : i; + String key = index < buttons.length ? buttons[index] : null; + KeyButtonView buttonView = (KeyButtonView) mParent.findViewById(id); + boolean isSmallButton = ArrayUtils.contains(SMALL_BUTTON_IDS, id); + ButtonInfo button = NAVBAR_EMPTY; + + for (ButtonInfo info : ALL_BUTTONS) { + if (info.key.equals(key)) { + button = info; + break; + } + } + + buttonView.setInfo(button, mVertical, isSmallButton, getResources()); + if (button != NAVBAR_EMPTY && !isSmallButton) { + visibleCount++; + } + + buttonView.setTranslationX(0); + mButtonViews.set(i, buttonView); + } + + if (isDevicePhone(mContext)) { + adjustPadding(visibleCount); + } + updateLowLights(visibleCount); + } + + /** + * Accommodates the padding between keys based on + * number of keys in use. + */ + private void adjustPadding(int visibleCount) { + ViewGroup viewParent = (ViewGroup) mParent.findViewById(R.id.mid_nav_buttons); + int totalViews = viewParent.getChildCount(); + + for (int v = 0; v < totalViews; v++) { + View currentKey = viewParent.getChildAt(v); + if (!(currentKey instanceof KeyButtonView)) { + continue; + } + View nextPadding = viewParent.getChildAt(v + 1); + if (nextPadding == null) { + continue; + } + + View nextKey = viewParent.getChildAt(v + 2); + ButtonInfo nextInfo = nextKey == null ? null : (ButtonInfo) nextKey.getTag(); + ButtonInfo currentInfo = (ButtonInfo) currentKey.getTag(); + + if (nextInfo != null && currentInfo != null && currentInfo != NAVBAR_EMPTY) { + if (nextInfo != NAVBAR_EMPTY || visibleCount > 1) { + nextPadding.setVisibility(View.VISIBLE); + } else { + nextPadding.setVisibility(View.GONE); + } + visibleCount--; + } else { + nextPadding.setVisibility(View.GONE); + } + } + } + + protected void updateLowLights(int visibleCount) { + ViewGroup lowLights = (ViewGroup) mParent.findViewById(R.id.lights_out); + int totalViews = lowLights.getChildCount(); + + for (int v = 0;v < totalViews; v++) { + View currentView = lowLights.getChildAt(v); + if (!(currentView instanceof ImageView)) { + continue; + } + + if (visibleCount <= 0) { + currentView.setVisibility(View.GONE); + } else { + currentView.setVisibility(View.VISIBLE); + visibleCount--; + } + + View blank = lowLights.getChildAt(v + 1); + if (blank != null) { + blank.setVisibility(visibleCount > 0 ? View.VISIBLE : View.GONE); + } + } + } + + private Resources getResources() { + return mResources != null ? mResources : mContext.getResources(); + } + + public void updateResources(Resources res) { + mResources = res; + } + + /** + * Class to store info about supported buttons + */ + public static final class ButtonInfo { + private final String key; + public int displayId; + public int contentDescription; + public int keyCode; + public int portResource; + public int landResource; + public int sideResource; + /** + * Constructor for new button type + * @param key - the internal key of the button + * @param rId - resource id of text shown to user in choose dialog + * @param cD - accessibility information regarding button + * @param mC - keyCode to execute on button press + * @param pR - portrait resource used to display button + * @param lR - landscape resource used to display button + * @param sR - smaller scaled resource for side buttons + */ + ButtonInfo (String key, int rId, int cD, int mC, int pR, int lR, int sR) { + this.key = key; + displayId = rId; + contentDescription = cD; + keyCode = mC; + portResource = pR; + landResource = lR; + sideResource = sR; + } + + @Override + public String toString() { + return "ButtonInfo[" + key + "]"; + } + } + + private static class ButtonAdapter extends ArrayAdapter<ButtonInfo> { + private ArrayList<ButtonInfo> mTakenItems; + private Resources mResources; + + public ButtonAdapter(Context context, + ArrayList<KeyButtonView> buttons, boolean smallButtons, Resources resources) { + super(context, R.layout.navigation_bar_edit_menu_item, R.id.key_text, + buildItems(smallButtons)); + + mTakenItems = new ArrayList<ButtonInfo>(); + for (KeyButtonView button : buttons) { + ButtonInfo info = (ButtonInfo) button.getTag(); + if (info != null && info != NAVBAR_EMPTY) { + mTakenItems.add(info); + } + } + mResources = resources; + } + + private static List<ButtonInfo> buildItems(boolean smallButtons) { + List<ButtonInfo> items = new ArrayList<ButtonInfo>(Arrays.asList(ALL_BUTTONS)); + + // Not re-assignable + items.remove(NAVBAR_HOME); + items.remove(NAVBAR_RECENT); + items.remove(NAVBAR_BACK); + // menu buttons can only be assigned to side buttons + if (!smallButtons) { + items.remove(NAVBAR_CONDITIONAL_MENU); + items.remove(NAVBAR_ALWAYS_MENU); + } else { + items.remove(NAVBAR_MENU_BIG); + } + + return items; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + ButtonInfo info = getItem(position); + boolean enabled = isEnabled(position); + + TextView text = (TextView) view.findViewById(R.id.key_text); + text.setText(getContext().getResources().getString(info.displayId)); + text.setEnabled(enabled); + + ImageView icon = (ImageView) view.findViewById(R.id.key_icon); + icon.setImageDrawable(mResources.getDrawable(info.portResource)); + icon.setColorFilter(new PorterDuffColorFilter( + text.getCurrentTextColor(), PorterDuff.Mode.SRC_IN)); + + return view; + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return !mTakenItems.contains(getItem(position)); + } + } +} 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 134c579..50656ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java @@ -35,7 +35,10 @@ public final class NavigationBarTransitions extends BarTransitions { private boolean mLightsOut; public NavigationBarTransitions(NavigationBarView view) { - super(view, R.drawable.nav_background); + super(view, R.drawable.nav_background, R.color.navigation_bar_background_opaque, + R.color.navigation_bar_background_semi_transparent, + R.color.navigation_bar_background_transparent, + com.android.internal.R.color.battery_saver_mode_color); mView = view; mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 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 8046eb5..c10f45b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -23,19 +23,25 @@ import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.app.ActivityManagerNative; import android.app.StatusBarManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; +import android.database.ContentObserver; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Handler; import android.os.Message; import android.os.RemoteException; +import android.os.UserHandle; import android.util.AttributeSet; import android.util.Log; import android.view.Display; -import android.view.Gravity; import android.view.MotionEvent; import android.view.Surface; import android.view.View; @@ -43,7 +49,6 @@ import android.view.ViewGroup; import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -53,7 +58,8 @@ import com.android.systemui.statusbar.policy.KeyButtonView; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.ArrayList; + +import cyanogenmod.providers.CMSettings; public class NavigationBarView extends LinearLayout { final static boolean DEBUG = false; @@ -69,26 +75,56 @@ public class NavigationBarView extends LinearLayout { int mBarSize; boolean mVertical; boolean mScreenOn; + boolean mLeftInLandscape; boolean mShowMenu; int mDisabledFlags = 0; int mNavigationIconHints = 0; - private Drawable mBackIcon, mBackLandIcon, mBackAltIcon, mBackAltLandIcon; + private BackButtonDrawable mBackIcon, mBackLandIcon; private Drawable mRecentIcon; private Drawable mRecentLandIcon; + private Drawable mHomeIcon, mHomeLandIcon; private NavigationBarViewTaskSwitchHelper mTaskSwitchHelper; private DeadZone mDeadZone; private final NavigationBarTransitions mBarTransitions; + /** + * Tracks the current visibilities of the far left (R.id.one) and right (R.id.six) buttons + * while dpad arrow keys are visible. + * + * We keep track of the orientations separately because they can get in different states, + * We can be showing dpad arrow keys on vertical, but on portrait that may not be so. + */ + public int[][] mSideButtonVisibilities = new int[][] { + {-1, -1} /* portrait */, {-1, -1} /* vertical */ + }; + + // workaround for LayoutTransitions leaving the nav buttons in a weird state (bug 5549288) final static boolean WORKAROUND_INVALID_LAYOUT = true; final static int MSG_CHECK_INVALID_LAYOUT = 8686; + final static String NAVBAR_EDIT_ACTION = "android.intent.action.NAVBAR_EDIT"; + + private boolean mInEditMode; + private NavbarEditor mEditBar; + private NavBarReceiver mNavBarReceiver; + private OnClickListener mRecentsClickListener; + private OnTouchListener mRecentsPreloadListener; + private OnTouchListener mHomeSearchActionListener; + private OnLongClickListener mRecentsBackListener; + private OnLongClickListener mLongPressHomeListener; + + private SettingsObserver mSettingsObserver; + private boolean mShowDpadArrowKeys; + // performs manual animation in sync with layout transitions private final NavTransitionListener mTransitionListener = new NavTransitionListener(); + private Resources mThemedResources; + private OnVerticalChangedListener mOnVerticalChangedListener; private boolean mIsLayoutRtl; private boolean mLayoutTransitionsEnabled = true; @@ -104,9 +140,9 @@ public class NavigationBarView extends LinearLayout { @Override public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { - if (view.getId() == R.id.back) { + if (NavbarEditor.NAVBAR_BACK.equals(view.getTag())) { mBackTransitioning = true; - } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) { + } else if (NavbarEditor.NAVBAR_HOME.equals(view.getTag()) && transitionType == LayoutTransition.APPEARING) { mHomeAppearing = true; mStartDelay = transition.getStartDelay(transitionType); mDuration = transition.getDuration(transitionType); @@ -117,9 +153,9 @@ public class NavigationBarView extends LinearLayout { @Override public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { - if (view.getId() == R.id.back) { + if (NavbarEditor.NAVBAR_BACK.equals(view.getTag())) { mBackTransitioning = false; - } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) { + } else if (NavbarEditor.NAVBAR_HOME.equals(view.getTag()) && transitionType == LayoutTransition.APPEARING) { mHomeAppearing = false; } } @@ -176,7 +212,7 @@ public class NavigationBarView extends LinearLayout { mDisplay = ((WindowManager)context.getSystemService( Context.WINDOW_SERVICE)).getDefaultDisplay(); - final Resources res = getContext().getResources(); + final Resources res = getResources(); mBarSize = res.getDimensionPixelSize(R.dimen.navigation_bar_size); mVertical = false; mShowMenu = false; @@ -185,6 +221,11 @@ public class NavigationBarView extends LinearLayout { getIcons(res); mBarTransitions = new NavigationBarTransitions(this); + + mNavBarReceiver = new NavBarReceiver(); + getContext().registerReceiverAsUser(mNavBarReceiver, UserHandle.ALL, + new IntentFilter(NAVBAR_EDIT_ACTION), null, null); + mSettingsObserver = new SettingsObserver(new Handler()); } @Override @@ -194,6 +235,13 @@ public class NavigationBarView extends LinearLayout { if (root != null) { root.setDrawDuringWindowsAnimating(true); } + mSettingsObserver.observe(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mSettingsObserver.unobserve(); } public BarTransitions getBarTransitions() { @@ -211,7 +259,7 @@ public class NavigationBarView extends LinearLayout { @Override public boolean onTouchEvent(MotionEvent event) { - if (mTaskSwitchHelper.onTouchEvent(event)) { + if (!mInEditMode && mTaskSwitchHelper.onTouchEvent(event)) { return true; } if (mDeadZone != null && event.getAction() == MotionEvent.ACTION_OUTSIDE) { @@ -222,7 +270,7 @@ public class NavigationBarView extends LinearLayout { @Override public boolean onInterceptTouchEvent(MotionEvent event) { - return mTaskSwitchHelper.onInterceptTouchEvent(event); + return !mInEditMode && mTaskSwitchHelper.onInterceptTouchEvent(event); } public void abortCurrentGesture() { @@ -236,19 +284,19 @@ public class NavigationBarView extends LinearLayout { } public View getRecentsButton() { - return mCurrentView.findViewById(R.id.recent_apps); + return mCurrentView.findViewWithTag(NavbarEditor.NAVBAR_RECENT); } public View getMenuButton() { - return mCurrentView.findViewById(R.id.menu); + return mCurrentView.findViewWithTag(NavbarEditor.NAVBAR_CONDITIONAL_MENU); } public View getBackButton() { - return mCurrentView.findViewById(R.id.back); + return mCurrentView.findViewWithTag(NavbarEditor.NAVBAR_BACK); } public KeyButtonView getHomeButton() { - return (KeyButtonView) mCurrentView.findViewById(R.id.home); + return (KeyButtonView) mCurrentView.findViewWithTag(NavbarEditor.NAVBAR_HOME); } public View getImeSwitchButton() { @@ -256,17 +304,51 @@ public class NavigationBarView extends LinearLayout { } private void getIcons(Resources res) { - mBackIcon = res.getDrawable(R.drawable.ic_sysbar_back); - mBackLandIcon = mBackIcon; - mBackAltIcon = res.getDrawable(R.drawable.ic_sysbar_back_ime); - mBackAltLandIcon = mBackAltIcon; + mBackIcon = new BackButtonDrawable(res.getDrawable(R.drawable.ic_sysbar_back)); + mBackLandIcon = mBackIcon; mRecentIcon = res.getDrawable(R.drawable.ic_sysbar_recent); mRecentLandIcon = mRecentIcon; + mHomeIcon = res.getDrawable(R.drawable.ic_sysbar_home); + mHomeLandIcon = mHomeIcon; + } + + public void updateResources(Resources res) { + mThemedResources = res; + getIcons(mThemedResources); + mBarTransitions.updateResources(res); + for (int i = 0; i < mRotatedViews.length; i++) { + ViewGroup container = (ViewGroup) mRotatedViews[i]; + if (container != null) { + updateLightsOutResources(container); + } + } + if (mEditBar != null) { + mEditBar.updateResources(res); + } + } + + private void updateLightsOutResources(ViewGroup container) { + ViewGroup lightsOut = (ViewGroup) container.findViewById(R.id.lights_out); + if (lightsOut != null) { + final int nChildren = lightsOut.getChildCount(); + for (int i = 0; i < nChildren; i++) { + final View child = lightsOut.getChildAt(i); + if (child instanceof ImageView) { + final ImageView iv = (ImageView) child; + // clear out the existing drawable, this is required since the + // ImageView keeps track of the resource ID and if it is the same + // it will not update the drawable. + iv.setImageDrawable(null); + iv.setImageDrawable(mThemedResources.getDrawable( + R.drawable.ic_sysbar_lights_out_dot_large)); + } + } + } } @Override public void setLayoutDirection(int layoutDirection) { - getIcons(getContext().getResources()); + getIcons(getResources()); super.setLayoutDirection(layoutDirection); } @@ -294,19 +376,74 @@ public class NavigationBarView extends LinearLayout { mNavigationIconHints = hints; - ((ImageView)getBackButton()).setImageDrawable(backAlt - ? (mVertical ? mBackAltLandIcon : mBackAltIcon) - : (mVertical ? mBackLandIcon : mBackIcon)); + ((ImageView)getBackButton()).setImageDrawable(null); + ((ImageView)getBackButton()).setImageDrawable(mVertical ? mBackLandIcon : mBackIcon); + mBackLandIcon.setImeVisible(backAlt); + mBackIcon.setImeVisible(backAlt); ((ImageView)getRecentsButton()).setImageDrawable(mVertical ? mRecentLandIcon : mRecentIcon); + ((ImageView)getHomeButton()).setImageDrawable(mVertical ? mHomeLandIcon : mHomeIcon); - final boolean showImeButton = ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0); + final boolean showImeButton = ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0) + && !mShowDpadArrowKeys; getImeSwitchButton().setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE); + + setDisabledFlags(mDisabledFlags, true); + // Update menu button in case the IME state has changed. setMenuVisibility(mShowMenu, true); + if (mShowDpadArrowKeys) { // overrides IME button + final boolean showingIme = ((mNavigationIconHints + & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0); - setDisabledFlags(mDisabledFlags, true); + setVisibleOrGone(getCurrentView().findViewById(R.id.dpad_left), showingIme); + setVisibleOrGone(getCurrentView().findViewById(R.id.dpad_right), showingIme); + + View one = getCurrentView().findViewById(mVertical ? R.id.six : R.id.one); + View six = getCurrentView().findViewById(mVertical ? R.id.one : R.id.six); + if (showingIme) { + if (one.getVisibility() != View.GONE) { + setSideButtonVisibility(true, one.getVisibility()); + setVisibleOrGone(one, false); + } + + if (six.getVisibility() != View.GONE) { + setSideButtonVisibility(false, six.getVisibility()); + setVisibleOrGone(six, false); + } + } else { + if (getSideButtonVisibility(true) != -1) { + one.setVisibility(getSideButtonVisibility(true)); + setSideButtonVisibility(true, - 1); + } + if (getSideButtonVisibility(false) != -1) { + six.setVisibility(getSideButtonVisibility(false)); + setSideButtonVisibility(false, -1); + } + } + } else { + setVisibleOrGone(getCurrentView().findViewById(R.id.dpad_left), false); + setVisibleOrGone(getCurrentView().findViewById(R.id.dpad_right), false); + View one = getCurrentView().findViewById(mVertical ? R.id.six : R.id.one); + View six = getCurrentView().findViewById(mVertical ? R.id.one : R.id.six); + if (getSideButtonVisibility(true) != -1) { + one.setVisibility(getSideButtonVisibility(true)); + setSideButtonVisibility(true, - 1); + } + if (getSideButtonVisibility(false) != -1) { + six.setVisibility(getSideButtonVisibility(false)); + setSideButtonVisibility(false, -1); + } + } + } + + private int getSideButtonVisibility(boolean left) { + return mSideButtonVisibilities[mVertical ? 1 : 0][left ? 0 : 1]; + } + + private void setSideButtonVisibility(boolean left, int vis) { + mSideButtonVisibilities[mVertical ? 1 : 0][left ? 0 : 1] = vis; } public void setDisabledFlags(int disabledFlags) { @@ -343,9 +480,10 @@ public class NavigationBarView extends LinearLayout { disableRecent = false; } - getBackButton() .setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE); - getHomeButton() .setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); - getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); + setButtonWithTagVisibility(NavbarEditor.NAVBAR_BACK, !disableBack); + setButtonWithTagVisibility(NavbarEditor.NAVBAR_HOME, !disableHome); + setButtonWithTagVisibility(NavbarEditor.NAVBAR_RECENT, !disableRecent); + setButtonWithTagVisibility(NavbarEditor.NAVBAR_SEARCH, !disableSearch); } private boolean inLockTask() { @@ -436,18 +574,19 @@ public class NavigationBarView extends LinearLayout { // Only show Menu if IME switcher not shown. final boolean shouldShow = mShowMenu && ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) == 0); - getMenuButton().setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE); + final boolean shouldShowAlwaysMenu = (mNavigationIconHints & + StatusBarManager.NAVIGATION_HINT_IME_SHOWN) == 0; + setButtonWithTagVisibility(NavbarEditor.NAVBAR_ALWAYS_MENU, shouldShowAlwaysMenu); + setButtonWithTagVisibility(NavbarEditor.NAVBAR_CONDITIONAL_MENU, shouldShow); + setButtonWithTagVisibility(NavbarEditor.NAVBAR_SEARCH, shouldShowAlwaysMenu); } @Override public void onFinishInflate() { mRotatedViews[Surface.ROTATION_0] = - mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0); - + mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0); mRotatedViews[Surface.ROTATION_90] = findViewById(R.id.rot90); - mRotatedViews[Surface.ROTATION_270] = mRotatedViews[Surface.ROTATION_90]; - mCurrentView = mRotatedViews[Surface.ROTATION_0]; getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener); @@ -459,6 +598,11 @@ public class NavigationBarView extends LinearLayout { return mVertical; } + public void setLeftInLandscape(boolean leftInLandscape) { + mLeftInLandscape = leftInLandscape; + mDeadZone.setStartFromRight(leftInLandscape); + } + public void reorient() { final int rot = mDisplay.getRotation(); for (int i=0; i<4; i++) { @@ -466,11 +610,22 @@ public class NavigationBarView extends LinearLayout { } mCurrentView = mRotatedViews[rot]; mCurrentView.setVisibility(View.VISIBLE); + updateLayoutTransitionsEnabled(); + if (NavbarEditor.isDevicePhone(getContext())) { + int rotation = mDisplay.getRotation(); + mVertical = rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270; + } else { + mVertical = getWidth() > 0 && getHeight() > getWidth(); + } + mEditBar = new NavbarEditor(mCurrentView, mVertical, mIsLayoutRtl, getResources()); + updateSettings(); + getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener); mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone); + mDeadZone.setStartFromRight(mLeftInLandscape); // force the low profile & disabled states into compliance mBarTransitions.init(); @@ -529,55 +684,8 @@ public class NavigationBarView extends LinearLayout { boolean isLayoutRtl = getResources().getConfiguration() .getLayoutDirection() == LAYOUT_DIRECTION_RTL; if (mIsLayoutRtl != isLayoutRtl) { - - // We swap all children of the 90 and 270 degree layouts, since they are vertical - View rotation90 = mRotatedViews[Surface.ROTATION_90]; - swapChildrenOrderIfVertical(rotation90.findViewById(R.id.nav_buttons)); - adjustExtraKeyGravity(rotation90, isLayoutRtl); - - View rotation270 = mRotatedViews[Surface.ROTATION_270]; - if (rotation90 != rotation270) { - swapChildrenOrderIfVertical(rotation270.findViewById(R.id.nav_buttons)); - adjustExtraKeyGravity(rotation270, isLayoutRtl); - } mIsLayoutRtl = isLayoutRtl; - } - } - - private void adjustExtraKeyGravity(View navBar, boolean isLayoutRtl) { - View menu = navBar.findViewById(R.id.menu); - View imeSwitcher = navBar.findViewById(R.id.ime_switcher); - if (menu != null) { - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) menu.getLayoutParams(); - lp.gravity = isLayoutRtl ? Gravity.BOTTOM : Gravity.TOP; - menu.setLayoutParams(lp); - } - if (imeSwitcher != null) { - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) imeSwitcher.getLayoutParams(); - lp.gravity = isLayoutRtl ? Gravity.BOTTOM : Gravity.TOP; - imeSwitcher.setLayoutParams(lp); - } - } - - /** - * Swaps the children order of a LinearLayout if it's orientation is Vertical - * - * @param group The LinearLayout to swap the children from. - */ - private void swapChildrenOrderIfVertical(View group) { - if (group instanceof LinearLayout) { - LinearLayout linearLayout = (LinearLayout) group; - if (linearLayout.getOrientation() == VERTICAL) { - int childCount = linearLayout.getChildCount(); - ArrayList<View> childList = new ArrayList<>(childCount); - for (int i = 0; i < childCount; i++) { - childList.add(linearLayout.getChildAt(i)); - } - linearLayout.removeAllViews(); - for (int i = childCount - 1; i >= 0; i--) { - linearLayout.addView(childList.get(i)); - } - } + reorient(); } } @@ -605,7 +713,7 @@ public class NavigationBarView extends LinearLayout { private String getResourceName(int resId) { if (resId != 0) { - final android.content.res.Resources res = getContext().getResources(); + final android.content.res.Resources res = getResources(); try { return res.getResourceName(resId); } catch (android.content.res.Resources.NotFoundException ex) { @@ -671,9 +779,9 @@ public class NavigationBarView extends LinearLayout { pw.print("null"); } else { pw.print(PhoneStatusBar.viewInfo(button) - + " " + visibilityToString(button.getVisibility()) - + " alpha=" + button.getAlpha() - ); + + " " + visibilityToString(button.getVisibility()) + + " alpha=" + button.getAlpha() + ); } pw.println(); } @@ -681,4 +789,140 @@ public class NavigationBarView extends LinearLayout { public interface OnVerticalChangedListener { void onVerticalChanged(boolean isVertical); } + + void setListeners(OnClickListener recentsClickListener, OnTouchListener recentsPreloadListener, + OnLongClickListener recentsBackListener, OnTouchListener homeSearchActionListener, + OnLongClickListener longPressHomeListener) { + mRecentsClickListener = recentsClickListener; + mRecentsPreloadListener = recentsPreloadListener; + mHomeSearchActionListener = homeSearchActionListener; + mRecentsBackListener = recentsBackListener; + mLongPressHomeListener = longPressHomeListener; + updateButtonListeners(); + } + + private void removeButtonListeners() { + ViewGroup container = (ViewGroup) mCurrentView.findViewById(R.id.container); + int viewCount = container.getChildCount(); + for (int i = 0; i < viewCount; i++) { + View button = container.getChildAt(i); + if (button instanceof KeyButtonView) { + button.setOnClickListener(null); + button.setOnTouchListener(null); + button.setLongClickable(false); + button.setOnLongClickListener(null); + } + } + } + + protected void updateButtonListeners() { + View recentView = mCurrentView.findViewWithTag(NavbarEditor.NAVBAR_RECENT); + if (recentView != null) { + recentView.setOnClickListener(mRecentsClickListener); + recentView.setOnTouchListener(mRecentsPreloadListener); + recentView.setLongClickable(true); + recentView.setOnLongClickListener(mRecentsBackListener); + } + View backView = mCurrentView.findViewWithTag(NavbarEditor.NAVBAR_BACK); + if (backView != null) { + backView.setLongClickable(true); + backView.setOnLongClickListener(mRecentsBackListener); + } + View homeView = mCurrentView.findViewWithTag(NavbarEditor.NAVBAR_HOME); + if (homeView != null) { + homeView.setOnTouchListener(mHomeSearchActionListener); + homeView.setLongClickable(true); + homeView.setOnLongClickListener(mLongPressHomeListener); + } + } + + public boolean isInEditMode() { + return mInEditMode; + } + + private void setButtonWithTagVisibility(Object tag, boolean visible) { + View findView = mCurrentView.findViewWithTag(tag); + if (findView == null) { + return; + } + int visibility = visible ? View.VISIBLE : View.INVISIBLE; + // if we're showing dpad arrow keys (e.g. the side button visibility where it's shown != -1) + // then don't actually update that buttons visibility, but update the stored value + if (getSideButtonVisibility(true) != -1 + && findView.getId() == (mVertical ? R.id.six : R.id.one)) { + setSideButtonVisibility(true, visibility); + } else if (getSideButtonVisibility(false) != -1 + && findView.getId() == (mVertical ? R.id.one : R.id.six)) { + setSideButtonVisibility(false, visibility); + } else { + findView.setVisibility(visibility); + } + } + + @Override + public Resources getResources() { + return mThemedResources != null ? mThemedResources : getContext().getResources(); + } + + public class NavBarReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + boolean edit = intent.getBooleanExtra("edit", false); + boolean save = intent.getBooleanExtra("save", false); + if (edit != mInEditMode) { + mInEditMode = edit; + if (edit) { + removeButtonListeners(); + mEditBar.setEditMode(true); + } else { + if (save) { + mEditBar.saveKeys(); + } + mEditBar.setEditMode(false); + updateSettings(); + } + } + } + } + + public void updateSettings() { + mEditBar.updateKeys(); + removeButtonListeners(); + updateButtonListeners(); + updateShowDpadKeys(); + setMenuVisibility(mShowMenu, true); + } + + private class SettingsObserver extends ContentObserver { + + SettingsObserver(Handler handler) { + super(handler); + } + + public void observe() { + ContentResolver resolver = getContext().getContentResolver(); + resolver.registerContentObserver( + CMSettings.System.getUriFor(CMSettings.System.NAVIGATION_BAR_MENU_ARROW_KEYS), + false, this); + + // intialize mModlockDisabled + onChange(false); + } + + public void unobserve() { + getContext().getContentResolver().unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + updateShowDpadKeys(); + } + } + + private void updateShowDpadKeys() { + mShowDpadArrowKeys = CMSettings.System.getIntForUser(getContext().getContentResolver(), + CMSettings.System.NAVIGATION_BAR_MENU_ARROW_KEYS, 0, UserHandle.USER_CURRENT) != 0; + setNavigationIconHints(mNavigationIconHints, true); + } } 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 bdd2c73..e4e02a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -18,25 +18,41 @@ package com.android.systemui.statusbar.phone; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; +import android.content.ContentResolver; import android.app.ActivityManager; import android.app.StatusBarManager; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.ResolveInfo; import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.ContentObserver; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Point; import android.graphics.Rect; +import android.net.Uri; +import android.os.Handler; +import android.os.PowerManager; +import android.os.RemoteException; +import android.preference.PreferenceManager; import android.util.AttributeSet; import android.util.MathUtils; +import android.view.Display; +import android.view.GestureDetector; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewTreeObserver; import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; @@ -50,8 +66,9 @@ import com.android.systemui.DejankUtils; import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; import com.android.systemui.R; +import com.android.systemui.SwipeHelper; import com.android.systemui.qs.QSContainer; -import com.android.systemui.qs.QSPanel; +import com.android.systemui.qs.QSDragPanel; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.FlingAnimationUtils; @@ -61,16 +78,22 @@ 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.policy.LiveLockScreenController; +import com.android.systemui.statusbar.policy.WeatherController; +import com.android.systemui.statusbar.policy.WeatherControllerImpl; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.stack.StackStateAnimator; +import cyanogenmod.providers.CMSettings; +import cyanogenmod.weather.util.WeatherUtils; + import java.util.List; public class NotificationPanelView extends PanelView implements ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener, View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener, KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener, - HeadsUpManager.OnHeadsUpChangedListener { + HeadsUpManager.OnHeadsUpChangedListener, WeatherController.Callback { private static final boolean DEBUG = false; @@ -88,14 +111,26 @@ public class NotificationPanelView extends PanelView implements private static final Rect mDummyDirtyRect = new Rect(0, 0, 1, 1); + private static final long SLIDE_PANEL_IN_ANIMATION_DURATION = 300; + + private static final String KEY_USER_EXPANDED_NOTIFICATIONS_IN_KEYGUARD = + "user_expanded_notifications_in_keyguard"; + private static final String KEY_USER_INTERACTED_WITH_LLS = + "user_interacted_with_lls"; + private static final String KEY_USER_UNLOCKED = + "user_unlocked"; + private static final String KEY_USER_RETURNED_FROM_LLS = + "user_returned_from_lls"; + public static final long DOZE_ANIMATION_DURATION = 700; + private KeyguardAffordanceHelper mAfforanceHelper; private StatusBarHeaderView mHeader; private KeyguardUserSwitcher mKeyguardUserSwitcher; private KeyguardStatusBarView mKeyguardStatusBar; private QSContainer mQsContainer; - private QSPanel mQsPanel; + private QSDragPanel mQsPanel; private KeyguardStatusView mKeyguardStatusView; private ObservableScrollView mScrollView; private TextView mClockView; @@ -217,15 +252,201 @@ public class NotificationPanelView extends PanelView implements private final Interpolator mTouchResponseInterpolator = new PathInterpolator(0.3f, 0f, 0.1f, 1f); + private Handler mHandler = new Handler(); + private SettingsObserver mSettingsObserver; + + private int mOneFingerQuickSettingsIntercept; + private boolean mDoubleTapToSleepEnabled; + private int mStatusBarHeaderHeight; + private GestureDetector mDoubleTapGesture; + + // Used to identify whether showUnlock() can dismiss the keyguard + // or not. + // TODO - add a new state to make it easier to identify keyguard vs + // LiveLockscreen + public boolean mCanDismissKeyguard; + + // Used to track which direction the user is currently + // interacting with and ensure they don't alternate back + // and forth. Reset every MOTION_UP/MOTION_CANCEL + private SwipeLockedDirection mLockedDirection; + + private SwipeHelper mSwipeHelper; + private final int mMinimumFlingVelocity; + private final int mScreenHeight; + private LiveLockScreenController mLiveLockscreenController; + private final GestureDetector mGestureDetector; + private ViewLinker mViewLinker; + private final UnlockMethodCache mUnlockMethodCache; + private boolean mDetailScrollLock; + + private boolean mKeyguardWeatherEnabled; + private TextView mKeyguardWeatherInfo; + private WeatherControllerImpl mWeatherController; + + // Keep track of common user interactions on the lock screen + private boolean mUserUnlocked; + private boolean mUserExpandedNotifications; + private boolean mUserInteractedWithLiveLockScreen; + private boolean mUserReturnedFromLiveLockScreen; + + private boolean mScreenOnHintsEnabled; + + private enum SwipeLockedDirection { + UNKNOWN, + HORIZONTAL, + VERTICAL + } + + // Handles swiping to the LiveLockscreen from keyguard + SwipeHelper.SimpleCallback mSwipeCallback = new SwipeHelper.SimpleCallback() { + @Override + public View getChildAtPosition(MotionEvent ev) { + return mViewLinker.getParent(); + } + + @Override + public View getChildContentView(View v) { + return mViewLinker.getParent(); + } + + @Override + public boolean canChildBeDismissed(View v) { + return true; + } + + @Override + public void onChildDismissed(View v) { + mCanDismissKeyguard = false; + mStatusBar.focusKeyguardExternalView(); + mLiveLockscreenController.onLiveLockScreenFocusChanged(true /* hasFocus */); + if (!mUserInteractedWithLiveLockScreen) { + mUserInteractedWithLiveLockScreen = true; + saveUserInteractedWithLls(true); + } + if (!mUserReturnedFromLiveLockScreen) { + startShowNotificationsHintAnimation(); + } + resetAlphaTranslation(); + // Enables the left edge gesture to allow user + // to return to keyguard + try { + WindowManagerGlobal.getWindowManagerService() + .setLiveLockscreenEdgeDetector(true); + } catch (RemoteException e){ + e.printStackTrace(); + } + } + + @Override + public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { + // Let live lockscreen know of swipe progress to allow + // them to translate content in. + mLiveLockscreenController.getLiveLockScreenView() + .onLockscreenSlideOffsetChanged(swipeProgress); + + // Fade out scrim background + float alpha = ScrimController.SCRIM_BEHIND_ALPHA_KEYGUARD - (1f - swipeProgress); + alpha = Math.max(0, alpha); + mStatusBar.getScrimController().setScrimBehindColor(alpha); + return false; + } + + private void resetAlphaTranslation() { + mNotificationStackScroller.setTranslationX(0); + mNotificationStackScroller.setAlpha(1f); + + mKeyguardStatusView.setTranslationX(0); + mKeyguardStatusView.setAlpha(1f); + } + }; + public NotificationPanelView(Context context, AttributeSet attrs) { super(context, attrs); setWillNotDraw(!DEBUG); + + mSettingsObserver = new SettingsObserver(mHandler); + mDoubleTapGesture = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onDoubleTap(MotionEvent e) { + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + if(pm != null) + pm.goToSleep(e.getEventTime()); + return true; + } + }); + + Resources res = getContext().getResources(); + final int gradientStart = res.getColor(R.color.live_lockscreen_gradient_start); + final int gradientEnd = res.getColor(R.color.live_lockscreen_gradient_end); + mGestureDetector = new GestureDetector(getContext(), + new GestureDetector.SimpleOnGestureListener() { + private float mDown; + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + // Ensure we only capture swipes in the up direction + if (velocityY > 0 || Math.abs(velocityY) <= mMinimumFlingVelocity) { + return false; + } + mCanDismissKeyguard = true; + mStatusBar.showBouncer(); + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + float delta = mDown - e2.getRawY(); + delta = Math.max(0, delta); + float screenHeightHalf = (float) mScreenHeight / 2f; + int color = (Integer) ArgbEvaluator.getInstance() + .evaluate(delta / screenHeightHalf, gradientStart, gradientEnd); + mKeyguardBottomArea.setBackgroundColor(color); + return super.onScroll(e1, e2, distanceX, distanceY); + } + + @Override + public boolean onDown(MotionEvent e) { + mDown = e.getRawY(); + mKeyguardBottomArea.expand(true); + return true; + } + }); + + mSwipeHelper = new SwipeHelper(SwipeHelper.X, + SwipeHelper.SWIPE_ZONE_LEFT, mSwipeCallback, mContext); + mSwipeHelper.setSwipeProgressFadeEnd(1.0f); + mMinimumFlingVelocity = ViewConfiguration.get(getContext()) + .getScaledMinimumFlingVelocity(); + + WindowManager windowManager = (WindowManager) mContext + .getSystemService(Context.WINDOW_SERVICE); + Display display = windowManager.getDefaultDisplay(); + Point point = new Point(); + display.getSize(point); + mScreenHeight = point.y; + mUnlockMethodCache = UnlockMethodCache.getInstance(context); + + mScreenOnHintsEnabled = res.getBoolean(R.bool.config_showScreenOnLockScreenHints); + mUserUnlocked = getUserUnlocked(); + mUserExpandedNotifications = getUserExpandedNotificationsInKeyguard(); + mUserInteractedWithLiveLockScreen = getUserInteractedWithLls(); + mUserReturnedFromLiveLockScreen = getUserReturnedFromLls(); } public void setStatusBar(PhoneStatusBar bar) { mStatusBar = bar; } + public void setLiveController(LiveLockScreenController liveController) { + mLiveLockscreenController = liveController; + } + + public void setWeatherController(WeatherControllerImpl weatherController) { + mWeatherController = weatherController; + mWeatherController.addCallback(this); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -234,10 +455,10 @@ public class NotificationPanelView extends PanelView implements mKeyguardStatusBar = (KeyguardStatusBarView) findViewById(R.id.keyguard_header); mKeyguardStatusView = (KeyguardStatusView) findViewById(R.id.keyguard_status_view); mQsContainer = (QSContainer) findViewById(R.id.quick_settings_container); - mQsPanel = (QSPanel) findViewById(R.id.quick_settings_panel); + mQsPanel = (QSDragPanel) findViewById(R.id.quick_settings_panel); + mQsPanel.setPanelView(this); mClockView = (TextView) findViewById(R.id.clock_view); mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view); - mScrollView.setListener(this); mScrollView.setFocusable(false); mReserveNotificationSpace = findViewById(R.id.reserve_notification_space); mNotificationContainerParent = (NotificationsQuickSettingsContainer) @@ -254,7 +475,64 @@ public class NotificationPanelView extends PanelView implements android.R.interpolator.fast_out_linear_in); mDozeAnimationInterpolator = AnimationUtils.loadInterpolator(getContext(), android.R.interpolator.linear_out_slow_in); - mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area); + + mViewLinker = new ViewLinker<NotificationStackScrollLayout>(mNotificationStackScroller, + new ViewLinker.LinkInfo(mKeyguardStatusBar, ViewLinker.LINK_ALPHA), + new ViewLinker.LinkInfo(mKeyguardStatusView, ViewLinker.LINK_ALPHA + | ViewLinker.LINK_TRANSLATION)); + + mKeyguardBottomArea = (KeyguardBottomAreaView) View.inflate(getContext(), + R.layout.keyguard_bottom_area, null); + /** Keyguard bottom area lives in a separate window, and as such, + * we must redirect its touch events through the proper flow + */ + mKeyguardBottomArea.setOnInterceptTouchListener(new KeyguardBottomAreaView.OnInterceptTouchEventListener() { + @Override + public boolean onInterceptTouchEvent(MotionEvent e) { + boolean intercept = false; + if (mLiveLockscreenController.getLiveLockScreenHasFocus()) { + // Handles swipe up to fade/dismiss when showing + // live lock screen + intercept = mAfforanceHelper.onInterceptTouchEvent(e); + if (!intercept) { + intercept = mGestureDetector.onTouchEvent(e); + } + } else { + intercept = NotificationPanelView.this.onInterceptTouchEvent(e); + } + return intercept; + } + }); + mKeyguardBottomArea.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent e) { + int action = e.getAction(); + + boolean isCancelOrUp = action == MotionEvent.ACTION_UP || + action == MotionEvent.ACTION_CANCEL; + if (isCancelOrUp) { + mKeyguardBottomArea.setBackground(null); + } + + boolean intercept = false; + if (mLiveLockscreenController.getLiveLockScreenHasFocus()) { + intercept = mAfforanceHelper.onTouchEvent(e); + // If the touch did not originate on the affordance helper, + // we must collapse the panel here since we can't rely on + // the swipe callbacks from being invoked. + if (isCancelOrUp && !isAffordanceSwipeInProgress()) { + mKeyguardBottomArea.expand(false); + } + if (!intercept) { + intercept = mGestureDetector.onTouchEvent(e); + } + } else { + intercept = NotificationPanelView.this.onTouchEvent(e); + } + return intercept; + } + }); + mQsNavbarScrim = findViewById(R.id.qs_navbar_scrim); mAfforanceHelper = new KeyguardAffordanceHelper(this, getContext()); mLastOrientation = getResources().getConfiguration().orientation; @@ -271,6 +549,26 @@ public class NotificationPanelView extends PanelView implements } } }); + + mKeyguardWeatherInfo = (TextView) mKeyguardStatusView.findViewById(R.id.weather_info); + } + + public boolean isAffordanceSwipeInProgress() { + return mAfforanceHelper.isSwipingInProgress(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mSettingsObserver.observe(); + mScrollView.setListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mSettingsObserver.unobserve(); + mWeatherController.removeCallback(this); } @Override @@ -292,6 +590,7 @@ public class NotificationPanelView extends PanelView implements R.dimen.qs_falsing_threshold); mPositionMinSideMargin = getResources().getDimensionPixelSize( R.dimen.notification_panel_min_side_margin); + mStatusBarHeaderHeight = getResources().getDimensionPixelSize(R.dimen.status_bar_header_height); } public void updateResources() { @@ -382,6 +681,7 @@ public class NotificationPanelView extends PanelView implements @Override public void onAnimationEnd(Animator animation) { mQsSizeChangeAnimator = null; + mQsContainer.setHeightOverride(-1); } }); mQsSizeChangeAnimator.start(); @@ -548,6 +848,10 @@ public class NotificationPanelView extends PanelView implements @Override public boolean onInterceptTouchEvent(MotionEvent event) { + // Reset locked direction + mLockedDirection = SwipeLockedDirection.UNKNOWN; + mCanDismissKeyguard = true; + if (mBlockTouches) { return false; } @@ -558,10 +862,26 @@ public class NotificationPanelView extends PanelView implements MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_PEEK, 1); return true; } + if (mQsPanel.isOnSettingsPage() && isInQsArea(event.getX(), event.getY(), false) + && mQsExpanded) { + mIntercepting = false; + // we explicitly do not intercept the touch event here to let the qs settings page + // scroll as necessary while not blocking horizontal swipes and allowing the panel + // to be collapsed when grabbed below the qs settings page as well. + return false; + } if (!isFullyCollapsed() && onQsIntercept(event)) { return true; } - return super.onInterceptTouchEvent(event); + + if (isKeyguardInteractiveAndShowing() || mStatusBar.isKeyguardShowingMedia() || + (mUnlockMethodCache.isTrustManaged() && mAfforanceHelper.isOnLockIcon(event))) { + return super.onInterceptTouchEvent(event); + } + + // We want both, we really do + return mSwipeHelper.onInterceptTouchEvent(event) + & super.onInterceptTouchEvent(event); } private boolean onQsIntercept(MotionEvent event) { @@ -716,6 +1036,17 @@ public class NotificationPanelView extends PanelView implements if (mBlockTouches) { return false; } + + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mKeyguardBottomArea.setBackground(null); + } + + if (mDoubleTapToSleepEnabled + && mStatusBarState == StatusBarState.KEYGUARD + && event.getY() < mStatusBarHeaderHeight) { + mDoubleTapGesture.onTouchEvent(event); + } initDownStates(event); if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp() && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { @@ -724,7 +1055,8 @@ public class NotificationPanelView extends PanelView implements } if ((!mIsExpanding || mHintAnimationRunning) && !mQsExpanded - && mStatusBar.getBarState() != StatusBarState.SHADE) { + && (mStatusBar.getBarState() != StatusBarState.SHADE + || mLiveLockscreenController.getLiveLockScreenHasFocus())) { mAfforanceHelper.onTouchEvent(event); } if (mOnlyAffordanceInThisMotion) { @@ -738,10 +1070,39 @@ public class NotificationPanelView extends PanelView implements MetricsLogger.count(mContext, COUNTER_PANEL_OPEN, 1); updateVerticalPanelPosition(event.getX()); } - super.onTouchEvent(event); + + if (isKeyguardInteractiveAndShowing() || mStatusBar.isKeyguardShowingMedia() || + (mUnlockMethodCache.isTrustManaged() && mAfforanceHelper.isOnLockIcon(event))) { + super.onTouchEvent(event); + return true; + } + + if ((!mIsExpanding || mHintAnimationRunning) + && !mQsExpanded + && mLockedDirection != SwipeLockedDirection.VERTICAL + && mStatusBar.getBarState() != StatusBarState.SHADE) { + mSwipeHelper.onTouchEvent(event); + if (mSwipeHelper.isDragging()) { + mLockedDirection = SwipeLockedDirection.HORIZONTAL; + } + if (mLockedDirection == SwipeLockedDirection.HORIZONTAL) { + requestDisallowInterceptTouchEvent(true); + return true; + } + } + + if (super.onTouchEvent(event)) { + mLockedDirection = SwipeLockedDirection.VERTICAL; + } return true; } + private boolean isKeyguardInteractiveAndShowing() { + return mLiveLockscreenController.getLiveLockScreenHasFocus() || + mStatusBar.getBarState() != StatusBarState.KEYGUARD || + !mLiveLockscreenController.isLiveLockScreenInteractive(); + } + private boolean handleQsTouch(MotionEvent event) { final int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f @@ -773,7 +1134,8 @@ public class NotificationPanelView extends PanelView implements mTwoFingerQsExpandPossible = true; } if (mTwoFingerQsExpandPossible && isOpenQsEvent(event) - && event.getY(event.getActionIndex()) < mStatusBarMinHeight) { + && event.getY(event.getActionIndex()) < mStatusBarMinHeight + && mExpandedHeight <= mQsPeekHeight) { MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_QS, 1); mQsExpandImmediate = true; requestPanelHeightUpdate(); @@ -786,9 +1148,14 @@ public class NotificationPanelView extends PanelView implements } private boolean isInQsArea(float x, float y) { + return isInQsArea(x, y, true); + } + + private boolean isInQsArea(float x, float y, boolean includeNotifications) { return (x >= mScrollView.getX() && x <= mScrollView.getX() + mScrollView.getWidth()) && - (y <= mNotificationStackScroller.getBottomMostNotificationBottom() - || y <= mQsContainer.getY() + mQsContainer.getHeight()); + ((includeNotifications + && y <= mNotificationStackScroller.getBottomMostNotificationBottom()) + || y <= mQsContainer.getY() + mQsContainer.getHeight()); } private boolean isOpenQsEvent(MotionEvent event) { @@ -806,12 +1173,27 @@ public class NotificationPanelView extends PanelView implements && (event.isButtonPressed(MotionEvent.BUTTON_SECONDARY) || event.isButtonPressed(MotionEvent.BUTTON_TERTIARY)); - return twoFingerDrag || stylusButtonClickDrag || mouseButtonClickDrag; + final float w = getMeasuredWidth(); + final float x = event.getX(); + float region = (w * (1.f/4.f)); // TODO overlay region fraction? + boolean showQsOverride = false; + + switch (mOneFingerQuickSettingsIntercept) { + case 1: // Right side pulldown + showQsOverride = isLayoutRtl() ? (x < region) : (w - region < x); + break; + case 2: // Left side pulldown + showQsOverride = isLayoutRtl() ? (w - region < x) : (x < region); + break; + } + showQsOverride &= mStatusBarState == StatusBarState.SHADE; + + return twoFingerDrag || showQsOverride || stylusButtonClickDrag || mouseButtonClickDrag; } private void handleQsDown(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN - && shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) { + && shouldQuickSettingsIntercept(event.getX(), event.getRawY(), -1)) { mQsTracking = true; onQsExpansionStarted(); mInitialHeightOnTouch = mQsExpansionHeight; @@ -893,8 +1275,13 @@ public class NotificationPanelView extends PanelView implements mTrackingPointer = -1; trackMovement(event); float fraction = getQsExpansionFraction(); - if ((fraction != 0f || y >= mInitialTouchY) - && (fraction != 1f || y <= mInitialTouchY)) { + final boolean fling = (fraction != 0f || y >= mInitialTouchY) + && (fraction != 1f || y <= mInitialTouchY); + final boolean flingExpand = Math.abs(getCurrentVelocity()) + > mFlingAnimationUtils.getMinVelocityPxPerSecond(); + final boolean detailFling = mDetailScrollLock && mQsExpanded + && flingExpand; + if ((fling && !mDetailScrollLock) || detailFling) { flingQsWithCurrentVelocity(y, event.getActionMasked() == MotionEvent.ACTION_CANCEL); } else { @@ -995,9 +1382,17 @@ public class NotificationPanelView extends PanelView implements mStatusBarState = statusBarState; mKeyguardShowing = keyguardShowing; + if (oldState != statusBarState && statusBarState == StatusBarState.KEYGUARD) { + mCanDismissKeyguard = true; + } - if (goingToFullShade || (oldState == StatusBarState.KEYGUARD - && statusBarState == StatusBarState.SHADE_LOCKED)) { + boolean keyguardToShadeLocked = oldState == StatusBarState.KEYGUARD + && statusBarState == StatusBarState.SHADE_LOCKED; + if (goingToFullShade || keyguardToShadeLocked) { + if (keyguardToShadeLocked && !mUserExpandedNotifications) { + mUserExpandedNotifications = true; + saveUserExpandedNotificationsInKeyguard(true); + } animateKeyguardStatusBarOut(); animateHeaderSlidingIn(); } else if (oldState == StatusBarState.SHADE_LOCKED @@ -1012,7 +1407,13 @@ public class NotificationPanelView extends PanelView implements mAfforanceHelper.updatePreviews(); } } - if (keyguardShowing) { + if (oldState != StatusBarState.SHADE && statusBarState == StatusBarState.SHADE && + !mUserUnlocked) { + mUserUnlocked = true; + saveUserUnlocked(true); + } + if (statusBarState == StatusBarState.KEYGUARD || + statusBarState == StatusBarState.SHADE_LOCKED) { updateDozingVisibilities(false /* animate */); } resetVerticalPanelPosition(); @@ -1289,8 +1690,7 @@ public class NotificationPanelView extends PanelView implements if (mKeyguardShowing) { updateHeaderKeyguard(); } - if (mStatusBarState == StatusBarState.SHADE_LOCKED - || mStatusBarState == StatusBarState.KEYGUARD) { + if (mStatusBarState == StatusBarState.KEYGUARD) { updateKeyguardBottomAreaAlpha(); } if (mStatusBarState == StatusBarState.SHADE && mQsExpanded @@ -1469,8 +1869,10 @@ public class NotificationPanelView extends PanelView implements View header = mKeyguardShowing ? mKeyguardStatusBar : mHeader; 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); + return onHeader || mDetailScrollLock + || (mScrollView.isScrolledToBottom() && yDiff < 0) && isInQsArea(x, y); } else { return onHeader; } @@ -1560,7 +1962,7 @@ public class NotificationPanelView extends PanelView implements */ private int getTempQsMaxExpansion() { int qsTempMaxExpansion = mQsMaxExpansionHeight; - if (mScrollYOverride != -1) { + if (mScrollYOverride != -1 && !mDetailScrollLock) { qsTempMaxExpansion -= mScrollYOverride; } return qsTempMaxExpansion; @@ -1727,6 +2129,9 @@ public class NotificationPanelView extends PanelView implements } private void updateHeaderKeyguardAlpha() { + if (mSwipeHelper.isDragging()) { + return; + } float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2); mKeyguardStatusBar.setAlpha(Math.min(getKeyguardContentsAlpha(), alphaQsExpansion) * mKeyguardStatusBarAnimateAlpha); @@ -1741,6 +2146,9 @@ public class NotificationPanelView extends PanelView implements private void updateKeyguardBottomAreaAlpha() { float alpha = Math.min(getKeyguardContentsAlpha(), 1 - getQsExpansionFraction()); + if (mLiveLockscreenController.getLiveLockScreenHasFocus()) { + alpha = 1f; + } mKeyguardBottomArea.setAlpha(alpha); mKeyguardBottomArea.setImportantForAccessibility(alpha == 0f ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS @@ -1802,7 +2210,6 @@ public class NotificationPanelView extends PanelView implements private void setListening(boolean listening) { mHeader.setListening(listening); - mKeyguardStatusBar.setListening(listening); mQsPanel.setListening(listening); } @@ -1965,10 +2372,12 @@ public class NotificationPanelView extends PanelView implements mLaunchAnimationEndRunnable.run(); mLaunchAnimationEndRunnable = null; } + mKeyguardBottomArea.setVisibility(View.GONE); } @Override protected void startUnlockHintAnimation() { + mKeyguardBottomArea.expand(true); super.startUnlockHintAnimation(); startHighlightIconAnimation(getCenterIcon()); } @@ -2003,11 +2412,13 @@ public class NotificationPanelView extends PanelView implements requestDisallowInterceptTouchEvent(true); mOnlyAffordanceInThisMotion = true; mQsTracking = false; + mKeyguardBottomArea.expand(true); } @Override public void onSwipingAborted() { mKeyguardBottomArea.unbindCameraPrewarmService(false /* launched */); + mKeyguardBottomArea.expand(false); } @Override @@ -2016,6 +2427,8 @@ public class NotificationPanelView extends PanelView implements return; } mHintAnimationRunning = true; + mKeyguardBottomArea.expand(true); + mKeyguardBottomArea.getIndicationView().animate().cancel(); mAfforanceHelper.startHintAnimation(rightIcon, new Runnable() { @Override public void run() { @@ -2025,13 +2438,9 @@ public class NotificationPanelView extends PanelView implements }); rightIcon = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? !rightIcon : rightIcon; if (rightIcon) { - mStatusBar.onCameraHintStarted(); + mStatusBar.onCameraHintStarted(mKeyguardBottomArea.getRightHint()); } else { - if (mKeyguardBottomArea.isLeftVoiceAssist()) { - mStatusBar.onVoiceAssistHintStarted(); - } else { - mStatusBar.onPhoneHintStarted(); - } + mStatusBar.onLeftHintStarted(mKeyguardBottomArea.getLeftHint()); } } @@ -2223,6 +2632,11 @@ public class NotificationPanelView extends PanelView implements public void onScreenTurningOn() { mKeyguardStatusView.refreshTime(); + if (shouldShowScreenOnHints()) { + startScreenOnHintAnimation(mLiveLockscreenController.isLiveLockScreenInteractive() && + !mUserInteractedWithLiveLockScreen, + !mUserUnlocked, !mUserExpandedNotifications); + } } @Override @@ -2342,7 +2756,7 @@ public class NotificationPanelView extends PanelView implements * @param x the x-coordinate the touch event */ private void updateVerticalPanelPosition(float x) { - if (mNotificationStackScroller.getWidth() * 1.75f > getWidth()) { + if (mNotificationStackScroller.getWidth() * 1.75f >= getWidth()) { resetVerticalPanelPosition(); return; } @@ -2384,6 +2798,58 @@ public class NotificationPanelView extends PanelView implements return mHeadsUpManager.hasPinnedHeadsUp() || mHeadsUpAnimatingAway; } + public KeyguardBottomAreaView getKeyguardBottomArea() { + return mKeyguardBottomArea; + } + + class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + void observe() { + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.STATUS_BAR_QUICK_QS_PULLDOWN), false, this); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.DOUBLE_TAP_SLEEP_GESTURE), false, this); + resolver.registerContentObserver(CMSettings.Secure.getUriFor( + CMSettings.Secure.LOCK_SCREEN_WEATHER_ENABLED), false, this); + update(); + } + + void unobserve() { + ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + update(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + update(); + } + + public void update() { + ContentResolver resolver = mContext.getContentResolver(); + mOneFingerQuickSettingsIntercept = CMSettings.System.getInt( + resolver, CMSettings.System.STATUS_BAR_QUICK_QS_PULLDOWN, 1); + mDoubleTapToSleepEnabled = CMSettings.System.getInt( + resolver, CMSettings.System.DOUBLE_TAP_SLEEP_GESTURE, 1) == 1; + + boolean wasKeyguardWeatherEnabled = mKeyguardWeatherEnabled; + mKeyguardWeatherEnabled = CMSettings.Secure.getInt( + resolver, CMSettings.Secure.LOCK_SCREEN_WEATHER_ENABLED, 0) == 1; + if (mWeatherController != null + && wasKeyguardWeatherEnabled != mKeyguardWeatherEnabled) { + onWeatherChanged(mWeatherController.getWeatherInfo()); + } + } + } + @Override public boolean hasOverlappingRendering() { return !mDozing; @@ -2394,6 +2860,8 @@ public class NotificationPanelView extends PanelView implements mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP; } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE) { mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_WIGGLE; + } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_SCREEN_GESTURE) { + mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_SCREEN_GESTURE; } else { // Default. @@ -2403,7 +2871,8 @@ public class NotificationPanelView extends PanelView implements // If we are launching it when we are occluded already we don't want it to animate, // nor setting these flags, since the occluded state doesn't change anymore, hence it's // never reset. - if (!isFullyCollapsed()) { + if (!isFullyCollapsed() && mLastCameraLaunchSource == + KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE) { mLaunchingAffordance = true; setLaunchingAffordance(true); } else { @@ -2451,4 +2920,159 @@ public class NotificationPanelView extends PanelView implements List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1); return !tasks.isEmpty() && pkgName.equals(tasks.get(0).topActivity.getPackageName()); } + + public void setDetailRequestedScrollLock(boolean detailScrollFlag) { + if (mDetailScrollLock != detailScrollFlag) { + if (mStatusBarState != StatusBarState.SHADE) { + mDetailScrollLock = false; + } else { + mDetailScrollLock = detailScrollFlag; + } + if (!detailScrollFlag && getQsExpansionFraction() > 0.3f) { + flingSettings(getCurrentVelocity(), true, new Runnable() { + @Override + public void run() { + mStackScrollerOverscrolling = false; + mQsExpansionFromOverscroll = false; + updateQsState(); + updateHeader(); + updateMaxHeadsUpTranslation(); + updatePanelExpanded(); + requestLayout(); + } + }, false); + } else { + requestLayout(); + } + } + } + + @Override + public void onWeatherChanged(WeatherController.WeatherInfo info) { + if (!mKeyguardWeatherEnabled || Double.isNaN(info.temp) || info.condition == null) { + mKeyguardWeatherInfo.setVisibility(GONE); + } else { + mKeyguardWeatherInfo.setText(mContext.getString( + R.string.keyguard_status_view_weather_format, + WeatherUtils.formatTemperature(info.temp, info.tempUnit), + info.condition)); + mKeyguardWeatherInfo.setVisibility(VISIBLE); + } + } + + private class SlideInAnimationListener implements ValueAnimator.AnimatorUpdateListener, + ValueAnimator.AnimatorListener { + @Override + public void onAnimationStart(Animator animator) {} + + @Override + public void onAnimationEnd(Animator animator) { + animationFinished(animator); + } + + @Override + public void onAnimationCancel(Animator animator) { + animationFinished(animator); + } + + @Override + public void onAnimationRepeat(Animator animator) {} + + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + View statusBarView = mStatusBar.getStatusBarWindow(); + if (valueAnimator.getAnimatedFraction() > 0 && + statusBarView.getVisibility() != View.VISIBLE) { + statusBarView.setVisibility(View.VISIBLE); + } + float translationX = (Float) valueAnimator.getAnimatedValue(); + float alpha = valueAnimator.getAnimatedFraction(); + + mViewLinker.getParent().setTranslationX(translationX); + mViewLinker.getParent().setAlpha(alpha); + + float alpha1 = ScrimController.SCRIM_BEHIND_ALPHA_KEYGUARD * alpha; + alpha1 = Math.max(0, alpha1); + mStatusBar.getScrimController().setScrimBehindColor(alpha1); + mLiveLockscreenController.getLiveLockScreenView() + .onLockscreenSlideOffsetChanged(alpha); + } + + private void animationFinished(Animator animator) { + mLiveLockscreenController.onLiveLockScreenFocusChanged(false); + } + } + + private SlideInAnimationListener mSlideInAnimationListener = new SlideInAnimationListener(); + + public void slideLockScreenIn() { + mNotificationStackScroller.setVisibility(View.VISIBLE); + mNotificationStackScroller.setAlpha(0f); + mNotificationStackScroller.setTranslationX(-mNotificationStackScroller.getWidth()); + mKeyguardStatusView.setVisibility(View.VISIBLE); + mKeyguardStatusView.setAlpha(0f); + mKeyguardStatusView.setTranslationX(mNotificationStackScroller.getTranslationX()); + mKeyguardStatusBar.setAlpha(0f); + + mStatusBar.getScrimController().setScrimBehindColor(0f); + ValueAnimator animator = ValueAnimator.ofFloat( + mNotificationStackScroller.getTranslationX(), + 0f); + animator.setDuration(SLIDE_PANEL_IN_ANIMATION_DURATION); + animator.addUpdateListener(mSlideInAnimationListener); + animator.addListener(mSlideInAnimationListener); + animator.start(); + + if (!mUserReturnedFromLiveLockScreen) { + mUserReturnedFromLiveLockScreen = true; + saveUserReturnedFromLls(true); + } + } + + private void saveBooleanSharedPreference(String key, boolean value) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + prefs.edit().putBoolean(key, value).apply(); + } + + private boolean getSharedPreferenceBoolean(String key, boolean defValue) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + return prefs.getBoolean(key, defValue); + } + + private void saveUserExpandedNotificationsInKeyguard(boolean expanded) { + saveBooleanSharedPreference(KEY_USER_EXPANDED_NOTIFICATIONS_IN_KEYGUARD, expanded); + } + + private boolean getUserExpandedNotificationsInKeyguard() { + return getSharedPreferenceBoolean(KEY_USER_EXPANDED_NOTIFICATIONS_IN_KEYGUARD, false); + } + + private void saveUserInteractedWithLls(boolean interacted) { + saveBooleanSharedPreference(KEY_USER_INTERACTED_WITH_LLS, interacted); + } + + private boolean getUserInteractedWithLls() { + return getSharedPreferenceBoolean(KEY_USER_INTERACTED_WITH_LLS, false); + } + + private void saveUserUnlocked(boolean unlocked) { + saveBooleanSharedPreference(KEY_USER_UNLOCKED, unlocked); + } + + private boolean getUserUnlocked() { + return getSharedPreferenceBoolean(KEY_USER_UNLOCKED, false); + } + + private void saveUserReturnedFromLls(boolean revealed) { + saveBooleanSharedPreference(KEY_USER_RETURNED_FROM_LLS, revealed); + } + + private boolean getUserReturnedFromLls() { + return getSharedPreferenceBoolean(KEY_USER_RETURNED_FROM_LLS, false); + } + + public boolean shouldShowScreenOnHints() { + return mScreenOnHintsEnabled && mStatusBar.isDeviceProvisioned() && + mStatusBarState == StatusBarState.KEYGUARD; + } } 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 e03bcfb..3a2c84c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Configuration; @@ -26,6 +27,7 @@ import android.content.res.Resources; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; +import android.view.View; import android.view.ViewConfiguration; import android.view.ViewTreeObserver; import android.view.animation.AnimationUtils; @@ -47,6 +49,9 @@ public abstract class PanelView extends FrameLayout { public static final boolean DEBUG = PanelBar.DEBUG; public static final String TAG = PanelView.class.getSimpleName(); + private static final long ANIMATION_FADE_DURATION = 1000L; + private static final long HINT_DELAY_DURATION = 1500L; + private final void logf(String fmt, Object... args) { Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); } @@ -86,6 +91,20 @@ public abstract class PanelView extends FrameLayout { private VelocityTrackerInterface mVelocityTracker; private FlingAnimationUtils mFlingAnimationUtils; + private boolean mUpdateExpandOnLayout; + private View.OnLayoutChangeListener mLayoutChangeListener = new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + // update expand height + if (mHeightAnimator != null && mExpanding && mUpdateExpandOnLayout && !mJustPeeked) { + final int maxPanelHeight = getMaxPanelHeight(); + final PropertyValuesHolder[] values = mHeightAnimator.getValues(); + values[0].setFloatValues(maxPanelHeight); + } + } + }; + /** * Whether an instant expand request is currently pending and we are just waiting for layout. */ @@ -105,6 +124,9 @@ public abstract class PanelView extends FrameLayout { private boolean mPeekPending; private boolean mCollapseAfterPeek; + private boolean mShowExpandHint; + private boolean mShowUnlockHint; + private boolean mScreenOnHintAnimationRunning; /** * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. @@ -122,6 +144,18 @@ public abstract class PanelView extends FrameLayout { } }; + private ViewTreeObserver.OnGlobalLayoutListener mInstantExpandLayoutListener = + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (mStatusBar.getStatusBarWindow().getHeight() != mStatusBar.getStatusBarHeight()) { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + setExpandedFraction(1f); + mInstantExpanding = false; + } + } + }; + protected void onExpandingFinished() { mBar.onExpandingFinished(); } @@ -623,7 +657,7 @@ public abstract class PanelView extends FrameLayout { flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); } - protected void flingToHeight(float vel, boolean expand, float target, + protected void flingToHeight(float vel, final 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 @@ -644,8 +678,9 @@ public abstract class PanelView extends FrameLayout { if (expandBecauseOfFalsing) { vel = 0; } + mUpdateExpandOnLayout = isFullyCollapsed(); mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight()); - if (expandBecauseOfFalsing) { + if (expandBecauseOfFalsing && vel == 0) { animator.setDuration(350); } } else { @@ -659,16 +694,23 @@ public abstract class PanelView extends FrameLayout { / collapseSpeedUpFactor)); } } + animator.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; @Override + public void onAnimationStart(Animator animation) { + if (expand) PanelView.this.addOnLayoutChangeListener(mLayoutChangeListener); + } + + @Override public void onAnimationCancel(Animator animation) { mCancelled = true; } @Override public void onAnimationEnd(Animator animation) { + if (expand) PanelView.this.removeOnLayoutChangeListener(mLayoutChangeListener); if (clearAllExpandHack && !mCancelled) { setExpandedHeightInternal(getMaxPanelHeight()); } @@ -874,18 +916,7 @@ public abstract class PanelView extends FrameLayout { // Wait for window manager to pickup the change, so we know the maximum height of the panel // then. - getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - if (mStatusBar.getStatusBarWindow().getHeight() - != mStatusBar.getStatusBarHeight()) { - getViewTreeObserver().removeOnGlobalLayoutListener(this); - setExpandedFraction(1f); - mInstantExpanding = false; - } - } - }); + getViewTreeObserver().addOnGlobalLayoutListener(mInstantExpandLayoutListener); // Make sure a layout really happens. requestLayout(); @@ -894,6 +925,7 @@ public abstract class PanelView extends FrameLayout { public void instantCollapse() { abortAnimations(); setExpandedFraction(0f); + getViewTreeObserver().removeOnGlobalLayoutListener(mInstantExpandLayoutListener); if (mExpanding) { notifyExpandingFinished(); } @@ -902,6 +934,7 @@ public abstract class PanelView extends FrameLayout { private void abortAnimations() { cancelPeek(); cancelHeightAnimator(); + mKeyguardBottomArea.getIndicationView().animate().cancel(); removeCallbacks(mPostCollapseRunnable); removeCallbacks(mFlingCollapseRunnable); } @@ -920,6 +953,8 @@ public abstract class PanelView extends FrameLayout { } cancelPeek(); notifyExpandingStarted(); + mKeyguardBottomArea.getIndicationView().animate().cancel(); + mStatusBar.onUnlockHintStarted(); startUnlockHintAnimationPhase1(new Runnable() { @Override public void run() { @@ -928,8 +963,8 @@ public abstract class PanelView extends FrameLayout { mHintAnimationRunning = false; } }); - mStatusBar.onUnlockHintStarted(); mHintAnimationRunning = true; + mShowExpandHint = false; } /** @@ -963,6 +998,7 @@ public abstract class PanelView extends FrameLayout { mKeyguardBottomArea.getIndicationView().animate() .translationY(-mHintDistance) .setDuration(250) + .setStartDelay(0) .setInterpolator(mFastOutSlowInInterpolator) .withEndAction(new Runnable() { @Override @@ -985,17 +1021,213 @@ public abstract class PanelView extends FrameLayout { animator.setDuration(450); animator.setInterpolator(mBounceInterpolator); animator.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + @Override public void onAnimationEnd(Animator animation) { mHeightAnimator = null; - onAnimationFinished.run(); - notifyBarPanelExpansionChanged(); + if (mCancelled) { + onAnimationFinished.run(); + } else { + if (mShowExpandHint) { + startUnlockHintFadeOutAnimationPhase(onAnimationFinished); + } else { + onAnimationFinished.run(); + notifyBarPanelExpansionChanged(); + } + } } }); animator.start(); mHeightAnimator = animator; } + /** + * Fade in unlock hint + */ + private void startUnlockHintFadeInAnimationPhase(final Runnable onAnimationFinished) { + mStatusBar.onUnlockHintStarted(); + mKeyguardBottomArea.getIndicationView().animate() + .alpha(1) + .setDuration(ANIMATION_FADE_DURATION) + .setStartDelay(0) + .setInterpolator(null) + .withEndAction(new Runnable() { + @Override + public void run() { + if (mShowExpandHint) { + startUnlockHintFadeOutAnimationPhase(onAnimationFinished); + } else { + onAnimationFinished.run(); + notifyBarPanelExpansionChanged(); + } + } + }) + .start(); + } + + /** + * Fade out unlock hint + */ + private void startUnlockHintFadeOutAnimationPhase(final Runnable onAnimationFinished) { + mKeyguardBottomArea.getIndicationView().animate() + .alpha(0) + .setDuration(ANIMATION_FADE_DURATION) + .setStartDelay(HINT_DELAY_DURATION) + .setInterpolator(null) + .withEndAction(new Runnable() { + @Override + public void run() { + startExpandHintAnimation(onAnimationFinished); + } + }) + .start(); + } + + /** + * Fade in Lls hint + */ + private void startLlsHintFadeInAnimationPhase(final Runnable onAnimationFinished) { + mKeyguardBottomArea.getIndicationView().setAlpha(0); + mKeyguardBottomArea.getIndicationView().animate() + .alpha(1) + .setDuration(ANIMATION_FADE_DURATION) + .setStartDelay(0) + .setInterpolator(null) + .withEndAction(new Runnable() { + @Override + public void run() { + if (mShowUnlockHint || mShowExpandHint) { + startLlsHintFadeOutAnimationPhase(onAnimationFinished); + } else { + onAnimationFinished.run();; + } + } + }) + .start(); + } + + /** + * Fade out Lls hint + */ + private void startLlsHintFadeOutAnimationPhase(final Runnable onAnimationFinished) { + mKeyguardBottomArea.getIndicationView().animate() + .alpha(0) + .setDuration(ANIMATION_FADE_DURATION) + .setStartDelay(HINT_DELAY_DURATION) + .setInterpolator(null) + .withEndAction(new Runnable() { + @Override + public void run() { + if (mShowUnlockHint) { + startUnlockHintFadeInAnimationPhase(onAnimationFinished); + } else if (mShowExpandHint) { + startExpandHintAnimation(onAnimationFinished); + } else { + onAnimationFinished.run(); + } + } + }) + .start(); + } + + /** + * Fade in expand hint + */ + private void startExpandHintAnimation(final Runnable onAnimationFinished) { + mStatusBar.onExpandHintStarted(); + mKeyguardBottomArea.getIndicationView().animate() + .alpha(1) + .setDuration(ANIMATION_FADE_DURATION) + .setStartDelay(0) + .setInterpolator(null) + .withEndAction(new Runnable() { + @Override + public void run() { + onAnimationFinished.run(); + } + }) + .start(); + } + + /** + * Show notifications hint (swipe right hint) + */ + protected void startShowNotificationsHintAnimation() { + cancelPeek(); + mStatusBar.onNotificationsHintStarted(); + mHintAnimationRunning = true; + mKeyguardBottomArea.getIndicationView().setAlpha(0); + mKeyguardBottomArea.getIndicationView().animate() + .alpha(1) + .setDuration(ANIMATION_FADE_DURATION) + .setInterpolator(null) + .withEndAction(new Runnable() { + @Override + public void run() { + mStatusBar.onHintFinished(); + mHintAnimationRunning = false; + } + }) + .start(); + } + + protected void startScreenOnHintAnimation(boolean showSwipeLeftHint, boolean showUnlockHint, + boolean showExpandHint) { + // We don't need to hint the user if an animation is already running or the user is changing + // the expansion. + if (mHintAnimationRunning || mScreenOnHintAnimationRunning) return; + + final View indicationView = mKeyguardBottomArea.getIndicationView(); + indicationView.animate().cancel(); + indicationView.animate().setListener( + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { } + + @Override + public void onAnimationEnd(Animator animation) { } + + @Override + public void onAnimationCancel(Animator animation) { + mScreenOnHintAnimationRunning = false; + indicationView.setAlpha(1f); + } + + @Override + public void onAnimationRepeat(Animator animation) { } + }); + + Runnable r = new Runnable() { + @Override + public void run() { + mStatusBar.onHintFinished(); + mScreenOnHintAnimationRunning = false; + } + }; + if (showSwipeLeftHint) { + mStatusBar.onLlsHintStarted(); + startLlsHintFadeInAnimationPhase(r); + } else if (showUnlockHint) { + mStatusBar.onUnlockHintStarted(); + startUnlockHintFadeInAnimationPhase(r); + } else if (showExpandHint) { + mStatusBar.onExpandHintStarted(); + startExpandHintAnimation(r); + } else { + return; + } + indicationView.setAlpha(0); + mShowUnlockHint = showUnlockHint; + mShowExpandHint = showExpandHint; + mScreenOnHintAnimationRunning = true; + } + private ValueAnimator createHeightAnimator(float targetHeight) { ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 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 73361bd..7df8346 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -26,13 +26,20 @@ import android.app.IActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.app.StatusBarManager; +import android.app.WallpaperManager; +import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentCallbacks2; +import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.res.Configuration; +import android.content.res.ThemeConfig; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Bitmap; @@ -54,10 +61,12 @@ import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.AsyncTask; +import android.os.BatteryManager; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.os.IPowerManager; import android.os.Message; import android.os.PowerManager; import android.os.Process; @@ -75,44 +84,55 @@ import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; +import android.util.Pair; import android.view.Display; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.ThreadedRenderer; import android.view.VelocityTracker; import android.view.View; +import android.view.WindowManagerGlobal; +import android.view.ViewConfiguration; +import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; 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.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; +import android.widget.AdapterView; +import android.widget.FrameLayout; import android.widget.ImageView; 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.internal.util.cm.ActionUtils; import com.android.keyguard.KeyguardHostView.OnDismissAction; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.BatteryMeterView; +import com.android.systemui.BatteryLevelTextView; import com.android.systemui.DemoMode; +import com.android.systemui.DockBatteryMeterView; import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.assist.AssistManager; +import com.android.systemui.cm.UserContentObserver; 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.qs.QSDragPanel; import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.statusbar.ActivatableNotificationView; import com.android.systemui.statusbar.BackDropView; @@ -124,40 +144,51 @@ import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.GestureRecorder; import com.android.systemui.statusbar.KeyguardIndicationController; +import com.android.systemui.statusbar.MediaExpandableNotificationRow; import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.NotificationData.Entry; 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.VisualizerView; import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener; import com.android.systemui.statusbar.policy.AccessibilityController; import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; +import com.android.systemui.statusbar.policy.BatteryStateRegistar.BatteryStateChangeCallback; 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.DockBatteryController; import com.android.systemui.statusbar.policy.FlashlightController; 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; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; +import com.android.systemui.statusbar.policy.LiveLockScreenController; import com.android.systemui.statusbar.policy.LocationControllerImpl; +import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.NetworkControllerImpl; import com.android.systemui.statusbar.policy.NextAlarmController; import com.android.systemui.statusbar.policy.PreviewInflater; import com.android.systemui.statusbar.policy.RotationLockControllerImpl; import com.android.systemui.statusbar.policy.SecurityControllerImpl; +import com.android.systemui.statusbar.policy.SuControllerImpl; import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.statusbar.policy.UserSwitcherController; +import com.android.systemui.statusbar.policy.WeatherControllerImpl; 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.StackStateAnimator; import com.android.systemui.statusbar.stack.StackViewState; import com.android.systemui.volume.VolumeComponent; +import cyanogenmod.app.CMContextConstants; +import cyanogenmod.app.CustomTileListenerService; +import cyanogenmod.app.StatusBarPanelCustomTile; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -183,6 +214,9 @@ import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCE import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING; +import cyanogenmod.providers.CMSettings; +import cyanogenmod.themes.IThemeService; + public class PhoneStatusBar extends BaseStatusBar implements DemoMode, DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener, HeadsUpManager.OnHeadsUpChangedListener { @@ -231,6 +265,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .build(); + private static final float BRIGHTNESS_CONTROL_PADDING = 0.15f; + private static final int BRIGHTNESS_CONTROL_LONG_PRESS_TIMEOUT = 750; // ms + private static final int BRIGHTNESS_CONTROL_LINGER_THRESHOLD = 20; + public static final int FADE_KEYGUARD_START_DELAY = 100; public static final int FADE_KEYGUARD_DURATION = 300; public static final int FADE_KEYGUARD_DURATION_PULSING = 96; @@ -258,7 +296,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // These are no longer handled by the policy, because we need custom strategies for them BluetoothControllerImpl mBluetoothController; SecurityControllerImpl mSecurityController; + BatteryManager mBatteryManager; BatteryController mBatteryController; + DockBatteryController mDockBatteryController; LocationControllerImpl mLocationController; NetworkControllerImpl mNetworkController; HotspotControllerImpl mHotspotController; @@ -274,7 +314,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, KeyguardMonitor mKeyguardMonitor; BrightnessMirrorController mBrightnessMirrorController; AccessibilityController mAccessibilityController; + WeatherControllerImpl mWeatherController; + SuControllerImpl mSuController; FingerprintUnlockController mFingerprintUnlockController; + LiveLockScreenController mLiveLockScreenController; int mNaturalBarHeight = -1; @@ -282,7 +325,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, Point mCurrentDisplaySize = new Point(); StatusBarWindowView mStatusBarWindow; - PhoneStatusBarView mStatusBarView; + FrameLayout mStatusBarWindowContent; + private PhoneStatusBarView mStatusBarView; private int mStatusBarWindowState = WINDOW_STATE_SHOWING; private StatusBarWindowManager mStatusBarWindowManager; private UnlockMethodCache mUnlockMethodCache; @@ -290,6 +334,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private boolean mWakeUpComingFromTouch; private PointF mWakeUpTouchLocation; private boolean mScreenTurningOn; + private BatteryMeterView mBatteryView; + private BatteryLevelTextView mBatteryTextView; int mPixelFormat; Object mQueueLock = new Object(); @@ -302,7 +348,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, TextView mNotificationPanelDebugText; // settings - private QSPanel mQSPanel; + private QSDragPanel mQSPanel; + private QSTileHost mQSTileHost; + private DevForceNavbarObserver mDevForceNavbarObserver; // top bar StatusBarHeaderView mHeader; @@ -316,15 +364,20 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private boolean mKeyguardGoingAway; // Keyguard is actually fading away now. private boolean mKeyguardFadingAway; + private boolean mKeyguardShowingMedia; private long mKeyguardFadingAwayDelay; private long mKeyguardFadingAwayDuration; + private Bitmap mKeyguardWallpaper; + int mKeyguardMaxNotificationCount; boolean mExpandedVisible; private int mNavigationBarWindowState = WINDOW_STATE_SHOWING; + private int mStatusBarHeaderHeight; + // the tracker view int mTrackingPosition; // the position of the top of the tracking view. @@ -335,6 +388,18 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, int[] mAbsPos = new int[2]; ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>(); + private boolean mAutomaticBrightness; + private boolean mBrightnessControl; + private boolean mBrightnessChanged; + private float mScreenWidth; + private int mMinBrightness; + private boolean mJustPeeked; + int mLinger; + int mInitialTouchX; + int mInitialTouchY; + + private boolean mRecreating = false; + // for disabling the status bar int mDisabled1 = 0; int mDisabled2 = 0; @@ -357,6 +422,127 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private int mNavigationIconHints = 0; private HandlerThread mHandlerThread; + private IThemeService mThemeService; + private long mLastThemeChangeTime = 0; + + Runnable mLongPressBrightnessChange = new Runnable() { + @Override + public void run() { + mStatusBarView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + adjustBrightness(mInitialTouchX); + mLinger = BRIGHTNESS_CONTROL_LINGER_THRESHOLD + 1; + } + }; + + // Custom Recents Long Press + // - Tracks Event state for custom (user-configurable) Long Presses. + private boolean mCustomRecentsLongPressed = false; + // - The ArrayList is updated when packages are added and removed. + private List<ComponentName> mCustomRecentsLongPressHandlerCandidates = new ArrayList<>(); + // - The custom Recents Long Press, if selected. When null, use default (switch last app). + private ComponentName mCustomRecentsLongPressHandler = null; + + class SettingsObserver extends UserContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + @Override + protected void observe() { + super.observe(); + + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.STATUS_BAR_BRIGHTNESS_CONTROL), false, this, + UserHandle.USER_ALL); + resolver.registerContentObserver(Settings.System.getUriFor( + Settings.System.SCREEN_BRIGHTNESS_MODE), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.NAVBAR_LEFT_IN_LANDSCAPE), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.Secure.RECENTS_LONG_PRESS_ACTIVITY), false, this); + update(); + } + + @Override + protected void unobserve() { + super.unobserve(); + ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(this); + } + + @Override + public void update() { + ContentResolver resolver = mContext.getContentResolver(); + int mode = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS_MODE, + Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, + UserHandle.USER_CURRENT); + mAutomaticBrightness = mode != Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL; + mBrightnessControl = CMSettings.System.getIntForUser( + resolver, CMSettings.System.STATUS_BAR_BRIGHTNESS_CONTROL, 0, + UserHandle.USER_CURRENT) == 1; + + if (mNavigationBarView != null) { + boolean navLeftInLandscape = CMSettings.System.getIntForUser(resolver, + CMSettings.System.NAVBAR_LEFT_IN_LANDSCAPE, 0, UserHandle.USER_CURRENT) == 1; + mNavigationBarView.setLeftInLandscape(navLeftInLandscape); + } + + // This method reads CMSettings.Secure.RECENTS_LONG_PRESS_ACTIVITY + updateCustomRecentsLongPressHandler(false); + } + } + + public void setStatusBarViewVisibility(boolean visible) { + mStatusBarView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + } + + class DevForceNavbarObserver extends UserContentObserver { + DevForceNavbarObserver(Handler handler) { + super(handler); + } + + @Override + protected void observe() { + super.observe(); + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(CMSettings.Global.getUriFor( + CMSettings.Global.DEV_FORCE_SHOW_NAVBAR), false, this, UserHandle.USER_ALL); + } + + @Override + public void update() { + boolean visible = CMSettings.Global.getIntForUser(mContext.getContentResolver(), + CMSettings.Global.DEV_FORCE_SHOW_NAVBAR, 0, UserHandle.USER_CURRENT) == 1; + + if (visible) { + forceAddNavigationBar(); + } else { + removeNavigationBar(); + } + + // Send a broadcast to Settings to update Key disabling when user changes + Intent intent = new Intent("com.cyanogenmod.action.UserChanged"); + intent.setPackage("com.android.settings"); + mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + } + } + + private void forceAddNavigationBar() { + // If we have no Navbar view and we should have one, create it + if (mNavigationBarView != null) { + return; + } + + mNavigationBarView = + (NavigationBarView) View.inflate(mContext, R.layout.navigation_bar, null); + + mNavigationBarView.setDisabledFlags(mDisabled1); + mNavigationBarView.setBar(this); + addNavigationBar(true); // dynamically adding nav bar, reset System UI visibility! + } + // ensure quick settings is disabled until the current user makes it through the setup wizard private boolean mUserSetup = false; private ContentObserver mUserSetupObserver = new ContentObserver(new Handler()) { @@ -438,6 +624,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private PorterDuffXfermode mSrcXferMode = new PorterDuffXfermode(PorterDuff.Mode.SRC); private PorterDuffXfermode mSrcOverXferMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER); + private VisualizerView mVisualizerView; + private boolean mScreenOn; + private MediaSessionManager mMediaSessionManager; private MediaController mMediaController; private String mMediaNotificationKey; @@ -453,6 +642,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, clearCurrentMediaNotification(); updateMediaMetaData(true); } + mVisualizerView.setPlaying(state.getState() == PlaybackState.STATE_PLAYING); } } @@ -603,6 +793,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private RankingMap mLatestRankingMap; private boolean mNoAnimationOnNextBarModeChange; + public ScrimController getScrimController() { + return mScrimController; + } + @Override public void start() { mDisplay = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) @@ -611,6 +805,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mScrimSrcModeEnabled = mContext.getResources().getBoolean( R.bool.config_status_bar_scrim_behind_use_src); + ThemeConfig currentTheme = mContext.getResources().getConfiguration().themeConfig; + if (currentTheme != null) { + mCurrentTheme = (ThemeConfig)currentTheme.clone(); + } else { + mCurrentTheme = ThemeConfig.getBootTheme(mContext.getContentResolver()); + } + + mStatusBarWindow = new StatusBarWindowView(mContext, null); + mStatusBarWindow.setService(this); + super.start(); // calls createAndAddWindows() mMediaSessionManager @@ -618,11 +822,25 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // TODO: use MediaSessionManager.SessionListener to hook us up to future updates // in session state - addNavigationBar(); + addNavigationBar(false); + + // Developer options - Force Navigation bar + try { + boolean needsNav = mWindowManagerService.needsNavigationBar(); + if (!needsNav) { + mDevForceNavbarObserver = new DevForceNavbarObserver(mHandler); + mDevForceNavbarObserver.observe(); + } + } catch (RemoteException ex) { + // no window manager? good luck with that + } + + SettingsObserver observer = new SettingsObserver(mHandler); + observer.observe(); // Lastly, call to the icon policy to install/update all the icons. mIconPolicy = new PhoneStatusBarPolicy(mContext, mCastController, mHotspotController, - mUserInfoController, mBluetoothController); + mUserInfoController, mBluetoothController, mSuController); mIconPolicy.setCurrentUserSetup(mUserSetup); mSettingsObserver.onChange(false); // set up @@ -635,6 +853,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true, mHeadsUpObserver); } + + WallpaperManager wallpaperManager = (WallpaperManager) mContext.getSystemService( + Context.WALLPAPER_SERVICE); + mKeyguardWallpaper = wallpaperManager.getKeyguardBitmap(); + mUnlockMethodCache = UnlockMethodCache.getInstance(mContext); mUnlockMethodCache.addListener(this); startKeyguard(); @@ -649,6 +872,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, notifyUserAboutHiddenNotifications(); mScreenPinningRequest = new ScreenPinningRequest(mContext); + + updateCustomRecentsLongPressHandler(true); + + mThemeService = IThemeService.Stub.asInterface(ServiceManager.getService( + CMContextConstants.CM_THEME_SERVICE)); } // ================================================================================ @@ -659,13 +887,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, Resources res = context.getResources(); + mScreenWidth = (float) res.getDisplayMetrics().widthPixels; + mMinBrightness = res.getInteger(com.android.internal.R.integer.config_screenBrightnessDim); + updateDisplaySize(); // populates mDisplayMetrics - updateResources(); + updateResources(null); - mStatusBarWindow = (StatusBarWindowView) View.inflate(context, + mStatusBarWindowContent = (FrameLayout) View.inflate(context, R.layout.super_status_bar, null); mStatusBarWindow.setService(this); - mStatusBarWindow.setOnTouchListener(new View.OnTouchListener() { + mStatusBarWindowContent.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { checkUserAutohide(v, event); @@ -674,17 +905,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, animateCollapsePanels(); } } - return mStatusBarWindow.onTouchEvent(event); + return mStatusBarWindowContent.onTouchEvent(event); } }); - mStatusBarView = (PhoneStatusBarView) mStatusBarWindow.findViewById(R.id.status_bar); + mStatusBarView = (PhoneStatusBarView) mStatusBarWindowContent.findViewById(R.id.status_bar); mStatusBarView.setBar(this); - PanelHolder holder = (PanelHolder) mStatusBarWindow.findViewById(R.id.panel_holder); + PanelHolder holder = (PanelHolder) mStatusBarWindowContent.findViewById(R.id.panel_holder); mStatusBarView.setPanelHolder(holder); - mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById( + mNotificationPanel = (NotificationPanelView) mStatusBarWindowContent.findViewById( R.id.notification_panel); mNotificationPanel.setStatusBar(this); @@ -693,8 +924,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mNotificationPanel.setBackground(new FastColorDrawable(context.getColor( R.color.notification_panel_solid_background))); } + mLiveLockScreenController = new LiveLockScreenController(mContext, this, + mNotificationPanel); + mNotificationPanel.setLiveController(mLiveLockScreenController); + if (mStatusBarWindowManager != null) { + mStatusBarWindowManager.setLiveLockscreenController(mLiveLockScreenController); + } - mHeadsUpManager = new HeadsUpManager(context, mStatusBarWindow); + if (mHeadsUpManager == null) { + mHeadsUpManager = new HeadsUpManager(context, mStatusBarWindow); + } mHeadsUpManager.setBar(this); mHeadsUpManager.addListener(this); mHeadsUpManager.addListener(mNotificationPanel); @@ -710,9 +949,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav); - if (showNav) { + if (showNav && !mRecreating) { mNavigationBarView = (NavigationBarView) View.inflate(context, R.layout.navigation_bar, null); + mNavigationBarView.updateResources(getNavbarThemedResources()); mNavigationBarView.setDisabledFlags(mDisabled1); mNavigationBarView.setBar(this); @@ -737,12 +977,17 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // no window manager? good luck with that } - mAssistManager = new AssistManager(this, context); + if (mAssistManager == null) { + mAssistManager = new AssistManager(this, context); + } + if (mNavigationBarView == null) { + mAssistManager.onConfigurationChanged(); + } // figure out which pixel-format to use for the status bar. mPixelFormat = PixelFormat.OPAQUE; - mStackScroller = (NotificationStackScrollLayout) mStatusBarWindow.findViewById( + mStackScroller = (NotificationStackScrollLayout) mStatusBarWindowContent.findViewById( R.id.notification_stack_scroller); mStackScroller.setLongPressListener(getNotificationLongClicker()); mStackScroller.setPhoneStatusBar(this); @@ -775,13 +1020,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mStackScroller.setDismissView(mDismissView); mExpandedContents = mStackScroller; - mBackdrop = (BackDropView) mStatusBarWindow.findViewById(R.id.backdrop); + mBackdrop = (BackDropView) mStatusBarWindowContent.findViewById(R.id.backdrop); mBackdropFront = (ImageView) mBackdrop.findViewById(R.id.backdrop_front); mBackdropBack = (ImageView) mBackdrop.findViewById(R.id.backdrop_back); - ScrimView scrimBehind = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_behind); - ScrimView scrimInFront = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_in_front); - View headsUpScrim = mStatusBarWindow.findViewById(R.id.heads_up_scrim); + FrameLayout scrimView = (FrameLayout) mStatusBarWindowContent.findViewById(R.id.scrimview); + ScrimView scrimBehind = (ScrimView) scrimView.findViewById(R.id.scrim_behind); + ScrimView scrimInFront = + (ScrimView) mStatusBarWindowContent.findViewById(R.id.scrim_in_front); + View headsUpScrim = mStatusBarWindowContent.findViewById(R.id.heads_up_scrim); mScrimController = new ScrimController(scrimBehind, scrimInFront, headsUpScrim, mScrimSrcModeEnabled); mHeadsUpManager.addListener(mScrimController); @@ -789,18 +1036,19 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mScrimController.setBackDropView(mBackdrop); mStatusBarView.setScrimController(mScrimController); mDozeScrimController = new DozeScrimController(mScrimController, context); + mVisualizerView = (VisualizerView) scrimView.findViewById(R.id.visualizerview); - mHeader = (StatusBarHeaderView) mStatusBarWindow.findViewById(R.id.header); + mHeader = (StatusBarHeaderView) mStatusBarWindowContent.findViewById(R.id.header); mHeader.setActivityStarter(this); - mKeyguardStatusBar = (KeyguardStatusBarView) mStatusBarWindow.findViewById(R.id.keyguard_header); - mKeyguardStatusView = mStatusBarWindow.findViewById(R.id.keyguard_status_view); - mKeyguardBottomArea = - (KeyguardBottomAreaView) mStatusBarWindow.findViewById(R.id.keyguard_bottom_area); + mKeyguardStatusBar = (KeyguardStatusBarView) mStatusBarWindowContent.findViewById(R.id.keyguard_header); + mKeyguardStatusView = mStatusBarWindowContent.findViewById(R.id.keyguard_status_view); + mKeyguardBottomArea = mNotificationPanel.getKeyguardBottomArea(); + mKeyguardBottomArea.setActivityStarter(this); mKeyguardBottomArea.setAssistManager(mAssistManager); mKeyguardIndicationController = new KeyguardIndicationController(mContext, - (KeyguardIndicationTextView) mStatusBarWindow.findViewById( - R.id.keyguard_indication_text), + (KeyguardIndicationTextView) mKeyguardBottomArea.findViewById( + R.id.keyguard_indication_text), mKeyguardBottomArea.getLockIcon()); mKeyguardBottomArea.setKeyguardIndicationController(mKeyguardIndicationController); @@ -815,35 +1063,74 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mHandlerThread.start(); // Other icons - mLocationController = new LocationControllerImpl(mContext, - mHandlerThread.getLooper()); // will post a notification - mBatteryController = new BatteryController(mContext); - mBatteryController.addStateChangedCallback(new BatteryStateChangeCallback() { - @Override - public void onPowerSaveChanged() { - mHandler.post(mCheckBarModes); - if (mDozeServiceHost != null) { - mDozeServiceHost.firePowerSaveChanged(mBatteryController.isPowerSave()); + if (mLocationController == null) { + mLocationController = new LocationControllerImpl(mContext, + mHandlerThread.getLooper()); // will post a notification + } + if (mBatteryManager == null) { + mBatteryManager = (BatteryManager) mContext.getSystemService(Context.BATTERY_SERVICE); + } + if (mBatteryController == null) { + mBatteryController = new BatteryController(mContext, mHandler); + mBatteryController.addStateChangedCallback(new BatteryStateChangeCallback() { + @Override + public void onPowerSaveChanged() { + mHandler.post(mCheckBarModes); + if (mDozeServiceHost != null) { + mDozeServiceHost.firePowerSaveChanged(mBatteryController.isPowerSave()); + } } + + @Override + public void onBatteryLevelChanged(boolean present, int level, + boolean pluggedIn, boolean charging) { + // noop + } + + @Override + public void onBatteryStyleChanged(int style, int percentMode) { + // noop + } + }); + } + if (mBatteryManager.isDockBatterySupported()) { + if (mDockBatteryController == null) { + mDockBatteryController = new DockBatteryController(mContext, mHandler); } - @Override - public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { - // noop - } - }); - mNetworkController = new NetworkControllerImpl(mContext, mHandlerThread.getLooper()); - mHotspotController = new HotspotControllerImpl(mContext); - mBluetoothController = new BluetoothControllerImpl(mContext, mHandlerThread.getLooper()); - mSecurityController = new SecurityControllerImpl(mContext); + } + if (mNetworkController == null) { + mNetworkController = new NetworkControllerImpl(mContext, mHandlerThread.getLooper()); + } + if (mHotspotController == null) { + mHotspotController = new HotspotControllerImpl(mContext); + } + if (mBluetoothController == null) { + mBluetoothController = new BluetoothControllerImpl(mContext, + mHandlerThread.getLooper()); + } + if (mSecurityController == null) { + mSecurityController = new SecurityControllerImpl(mContext); + } if (mContext.getResources().getBoolean(R.bool.config_showRotationLock)) { - mRotationLockController = new RotationLockControllerImpl(mContext); + if (mRotationLockController == null) { + mRotationLockController = new RotationLockControllerImpl(mContext); + } + } + if (mUserInfoController == null) { + mUserInfoController = new UserInfoController(mContext); } - mUserInfoController = new UserInfoController(mContext); mVolumeComponent = getComponent(VolumeComponent.class); if (mVolumeComponent != null) { - mZenModeController = mVolumeComponent.getZenController(); + if (mZenModeController == null) { + mZenModeController = mVolumeComponent.getZenController(); + } + } + if (mCastController == null) { + mCastController = new CastControllerImpl(mContext); + } + if (mSuController == null) { + mSuController = new SuControllerImpl(mContext); } - mCastController = new CastControllerImpl(mContext); final SignalClusterView signalCluster = (SignalClusterView) mStatusBarView.findViewById(R.id.signal_cluster); final SignalClusterView signalClusterKeyguard = @@ -864,45 +1151,143 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mNetworkController.addEmergencyListener(mHeader); } - mFlashlightController = new FlashlightController(mContext); + if (mFlashlightController == null) { + mFlashlightController = new FlashlightController(mContext); + } mKeyguardBottomArea.setFlashlightController(mFlashlightController); mKeyguardBottomArea.setPhoneStatusBar(this); mKeyguardBottomArea.setUserSetupComplete(mUserSetup); - mAccessibilityController = new AccessibilityController(mContext); + if (mAccessibilityController == null) { + mAccessibilityController = new AccessibilityController(mContext); + } mKeyguardBottomArea.setAccessibilityController(mAccessibilityController); - mNextAlarmController = new NextAlarmController(mContext); - mKeyguardMonitor = new KeyguardMonitor(mContext); + if (mNextAlarmController == null) { + mNextAlarmController = new NextAlarmController(mContext); + } + if (mKeyguardMonitor == null) { + mKeyguardMonitor = new KeyguardMonitor(mContext); + } if (UserSwitcherController.isUserSwitcherAvailable(UserManager.get(mContext))) { - mUserSwitcherController = new UserSwitcherController(mContext, mKeyguardMonitor, - mHandler); + if (mUserSwitcherController == null) { + mUserSwitcherController = new UserSwitcherController(mContext, mKeyguardMonitor, + mHandler); + } + } + if (mWeatherController == null) { + mWeatherController = new WeatherControllerImpl(mContext); } + mKeyguardUserSwitcher = new KeyguardUserSwitcher(mContext, - (ViewStub) mStatusBarWindow.findViewById(R.id.keyguard_user_switcher), + (ViewStub) mStatusBarWindowContent.findViewById(R.id.keyguard_user_switcher), mKeyguardStatusBar, mNotificationPanel, mUserSwitcherController); // Set up the quick settings tile panel - mQSPanel = (QSPanel) mStatusBarWindow.findViewById(R.id.quick_settings_panel); + mQSPanel = (QSDragPanel) mStatusBarWindowContent.findViewById(R.id.quick_settings_panel); if (mQSPanel != null) { - final QSTileHost qsh = new QSTileHost(mContext, this, - mBluetoothController, mLocationController, mRotationLockController, - mNetworkController, mZenModeController, mHotspotController, - mCastController, mFlashlightController, - mUserSwitcherController, mKeyguardMonitor, - mSecurityController); - mQSPanel.setHost(qsh); - mQSPanel.setTiles(qsh.getTiles()); - mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow); + if (mQSTileHost == null) { + mQSTileHost = new QSTileHost(mContext, this, + mBluetoothController, mLocationController, mRotationLockController, + mNetworkController, mZenModeController, mHotspotController, + mCastController, mFlashlightController, + mUserSwitcherController, mKeyguardMonitor, + mSecurityController, mBatteryController); + } + mQSPanel.setHost(mQSTileHost); + if (mBrightnessMirrorController == null) { + mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindowContent); + } mQSPanel.setBrightnessMirror(mBrightnessMirrorController); + mQSPanel.setTiles(mQSTileHost.getTiles()); mHeader.setQSPanel(mQSPanel); - qsh.setCallback(new QSTileHost.Callback() { + mQSTileHost.setCallback(new QSTileHost.Callback() { @Override public void onTilesChanged() { - mQSPanel.setTiles(qsh.getTiles()); + mHandler.post(new Runnable() { + @Override + public void run() { + mQSPanel.setTiles(mQSTileHost.getTiles()); + } + }); + } + + @Override + public void setEditing(final boolean editing) { + if (mState != StatusBarState.SHADE) { + return; + } + mHandler.post(new Runnable() { + @Override + public void run() { + mQSPanel.setEditing(editing); + mHeader.setEditing(editing); + } + }); + } + + @Override + public boolean isEditing() { + return mQSPanel.isEditing(); + } + + @Override + public void goToSettingsPage() { + if (mState != StatusBarState.SHADE) { + return; + } + setEditing(true); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mQSPanel.goToSettingsPage(); + } + }, 500); + } + + @Override + public void resetTiles() { + mHandler.post(new Runnable() { + @Override + public void run() { + mQSPanel.setEditing(false); + mHeader.setEditing(false); + + // unregister custom tile service while we reset to not get + // callbacks from custom tiles + try { + mCustomTileListenerService.unregisterAsSystemService(); + } catch (RemoteException e) { + Log.e(TAG, "Unable to unregister custom tile listener", e); + } + + mQSTileHost.resetTiles(); + + // reregister service + try { + mCustomTileListenerService.registerAsSystemService(mContext, + new ComponentName(mContext.getPackageName(), + PhoneStatusBar.this.getClass().getCanonicalName()), + UserHandle.USER_ALL); + } catch (RemoteException e) { + Log.e(TAG, "Unable to register custom tile listener", e); + } + } + }); } }); } + // Set up the initial custom tile listener state. + try { + mCustomTileListenerService.registerAsSystemService(mContext, + new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()), + UserHandle.USER_ALL); + } catch (RemoteException e) { + Log.e(TAG, "Unable to register custom tile listener", e); + } + + mQSPanel.getHost().setCustomTileListenerService(mCustomTileListenerService); + // User info. Trigger first load. mHeader.setUserInfoController(mUserInfoController); mKeyguardStatusBar.setUserInfoController(mUserInfoController); @@ -910,10 +1295,40 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mUserInfoController.reloadUserInfo(); mHeader.setBatteryController(mBatteryController); - ((BatteryMeterView) mStatusBarView.findViewById(R.id.battery)).setBatteryController( - mBatteryController); + + BatteryMeterView batteryMeterView = + ((BatteryMeterView) mStatusBarView.findViewById(R.id.battery)); + batteryMeterView.setBatteryStateRegistar(mBatteryController); + batteryMeterView.setBatteryController(mBatteryController); + batteryMeterView.setAnimationsEnabled(false); + ((BatteryLevelTextView) mStatusBarView.findViewById(R.id.battery_level_text)) + .setBatteryStateRegistar(mBatteryController); mKeyguardStatusBar.setBatteryController(mBatteryController); + mHeader.setDockBatteryController(mDockBatteryController); + mKeyguardStatusBar.setDockBatteryController(mDockBatteryController); + if (mDockBatteryController != null) { + DockBatteryMeterView dockBatteryMeterView = + ((DockBatteryMeterView) mStatusBarView.findViewById(R.id.dock_battery)); + dockBatteryMeterView.setBatteryStateRegistar(mDockBatteryController); + ((BatteryLevelTextView) mStatusBarView.findViewById(R.id.dock_battery_level_text)) + .setBatteryStateRegistar(mDockBatteryController); + } else { + DockBatteryMeterView dockBatteryMeterView = + (DockBatteryMeterView) mStatusBarView.findViewById(R.id.dock_battery); + if (dockBatteryMeterView != null) { + mStatusBarView.removeView(dockBatteryMeterView); + } + BatteryLevelTextView dockBatteryLevel = + (BatteryLevelTextView) mStatusBarView.findViewById(R.id.dock_battery_level_text); + if (dockBatteryLevel != null) { + mStatusBarView.removeView(dockBatteryLevel); + } + } + mHeader.setNextAlarmController(mNextAlarmController); + mHeader.setWeatherController(mWeatherController); + + mNotificationPanel.setWeatherController(mWeatherController); PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mBroadcastReceiver.onReceive(mContext, @@ -927,6 +1342,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_KEYGUARD_WALLPAPER_CHANGED); + filter.addAction(cyanogenmod.content.Intent.ACTION_SCREEN_CAMERA_GESTURE); context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null); IntentFilter demoFilter = new IntentFilter(); @@ -937,6 +1354,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, context.registerReceiverAsUser(mDemoReceiver, UserHandle.ALL, demoFilter, android.Manifest.permission.DUMP, null); + // receive broadcasts for packages + IntentFilter packageFilter = new IntentFilter(); + packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + packageFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); + packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + packageFilter.addDataScheme("package"); + context.registerReceiver(mPackageBroadcastReceiver, packageFilter); + // listen for USER_SETUP_COMPLETE setting (per-user) resetUserSetupObserver(); @@ -1062,6 +1488,41 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mNaturalBarHeight; } + private final CustomTileListenerService mCustomTileListenerService = + new CustomTileListenerService() { + @Override + public void onListenerConnected() { + //Connected + } + @Override + public void onCustomTilePosted(final StatusBarPanelCustomTile sbc) { + if (DEBUG) Log.d(TAG, "onCustomTilePosted: " + sbc.getCustomTile()); + mHandler.post(new Runnable() { + @Override + public void run() { + boolean isUpdate = mQSPanel.getHost().getCustomTileData() + .get(sbc.persistableKey()) != null; + if (isUpdate) { + mQSPanel.getHost().updateCustomTile(sbc); + } else { + mQSPanel.getHost().addCustomTile(sbc); + } + } + }); + } + + @Override + public void onCustomTileRemoved(final StatusBarPanelCustomTile sbc) { + if (DEBUG) Log.d(TAG, "onCustomTileRemoved: " + sbc.getCustomTile()); + mHandler.post(new Runnable() { + @Override + public void run() { + mQSPanel.getHost().removeCustomTileSysUi(sbc.persistableKey()); + } + }); + } + }; + private View.OnClickListener mRecentsClickListener = new View.OnClickListener() { public void onClick(View v) { awakenDreams(); @@ -1118,34 +1579,53 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } - private void prepareNavigationBarView() { + private void prepareNavigationBarView(boolean forceReset) { mNavigationBarView.reorient(); - mNavigationBarView.getRecentsButton().setOnClickListener(mRecentsClickListener); - mNavigationBarView.getRecentsButton().setOnTouchListener(mRecentsPreloadOnTouchListener); - mNavigationBarView.getRecentsButton().setLongClickable(true); - mNavigationBarView.getRecentsButton().setOnLongClickListener(mLongPressBackRecentsListener); - mNavigationBarView.getBackButton().setLongClickable(true); - mNavigationBarView.getBackButton().setOnLongClickListener(mLongPressBackRecentsListener); - mNavigationBarView.getHomeButton().setOnTouchListener(mHomeActionListener); - mNavigationBarView.getHomeButton().setOnLongClickListener(mLongPressHomeListener); + mNavigationBarView.setListeners(mRecentsClickListener, mRecentsPreloadOnTouchListener, + mLongPressBackRecentsListener, mHomeActionListener, mLongPressHomeListener); mAssistManager.onConfigurationChanged(); + if (forceReset) { + // Nav Bar was added dynamically - we need to reset the mSystemUiVisibility and call + // setSystemUiVisibility so that mNavigationBarMode is set to the correct value + int newVal = mSystemUiVisibility; + mSystemUiVisibility = 0; + setSystemUiVisibility(newVal, SYSTEM_UI_VISIBILITY_MASK); + checkBarMode(mNavigationBarMode, + mNavigationBarWindowState, mNavigationBarView.getBarTransitions(), + mNoAnimationOnNextBarModeChange); + } } // For small-screen devices (read: phones) that lack hardware navigation buttons - private void addNavigationBar() { + private void addNavigationBar(boolean forceReset) { if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + mNavigationBarView); if (mNavigationBarView == null) return; - prepareNavigationBarView(); + ThemeConfig newTheme = mContext.getResources().getConfiguration().themeConfig; + if (newTheme != null && + (mCurrentTheme == null || !mCurrentTheme.equals(newTheme))) { + // Nevermind, this will be re-created + return; + } + + prepareNavigationBarView(forceReset); mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams()); } + private void removeNavigationBar() { + if (DEBUG) Log.d(TAG, "removeNavigationBar: about to remove " + mNavigationBarView); + if (mNavigationBarView == null) return; + + mWindowManager.removeView(mNavigationBarView); + mNavigationBarView = null; + } + private void repositionNavigationBar() { if (mNavigationBarView == null || !mNavigationBarView.isAttachedToWindow()) return; - prepareNavigationBarView(); + prepareNavigationBarView(false); mWindowManager.updateViewLayout(mNavigationBarView, getNavigationBarLayoutParams()); } @@ -1176,6 +1656,18 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return lp; } + private Resources getNavbarThemedResources() { + String pkgName = mCurrentTheme.getOverlayForNavBar(); + Resources res = null; + try { + res = mContext.getPackageManager().getThemedResourcesForApplication( + mContext.getPackageName(), pkgName); + } catch (PackageManager.NameNotFoundException e) { + res = mContext.getResources(); + } + return res; + } + public void addIcon(String slot, int index, int viewIndex, StatusBarIcon icon) { mIconController.addSystemIcon(slot, index, viewIndex, icon); } @@ -1272,6 +1764,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mNavigationBarView != null) { mNavigationBarView.setLayoutDirection(layoutDirection); } + mIconController.refreshAllStatusBarIcons(); } private void updateNotificationShade() { @@ -1494,7 +1987,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } @Override - protected void updateRowStates() { + public void updateRowStates() { super.updateRowStates(); mNotificationPanel.notifyVisibleChildrenChanged(); } @@ -1608,6 +2101,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, Log.v(TAG, "DEBUG_MEDIA: insert listener, receive metadata: " + mMediaMetadata); } + if (mediaNotification != null + && mediaNotification.row != null + && mediaNotification.row instanceof MediaExpandableNotificationRow) { + ((MediaExpandableNotificationRow) mediaNotification.row) + .setMediaController(controller); + } if (mediaNotification != null) { mMediaNotificationKey = mediaNotification.notification.getKey(); @@ -1699,18 +2198,46 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, + " state=" + mState); } - Bitmap artworkBitmap = null; + Bitmap backdropBitmap = null; + + // apply any album artwork first if (mMediaMetadata != null) { - artworkBitmap = mMediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); - if (artworkBitmap == null) { - artworkBitmap = mMediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); + backdropBitmap = mMediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); + if (backdropBitmap == null) { + backdropBitmap = mMediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); // might still be null } } - final boolean hasArtwork = artworkBitmap != null; + // HACK: Consider keyguard as visible if showing sim pin security screen + KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext); + boolean keyguardVisible = mState != StatusBarState.SHADE || updateMonitor.isSimPinSecure(); + + if (!mKeyguardFadingAway && keyguardVisible && backdropBitmap != null && mScreenOn) { + // if there's album art, ensure visualizer is visible + mVisualizerView.setPlaying(mMediaController != null + && mMediaController.getPlaybackState() != null + && mMediaController.getPlaybackState().getState() + == PlaybackState.STATE_PLAYING); + } + + // apply user lockscreen image + if (backdropBitmap == null && !mLiveLockScreenController.isShowingLiveLockScreenView()) { + backdropBitmap = mKeyguardWallpaper; + } + + if (keyguardVisible) { + // always use current backdrop to color eq + mVisualizerView.setBitmap(backdropBitmap); + } + + final boolean hasBackdrop = backdropBitmap != null; + mKeyguardShowingMedia = hasBackdrop; + if (mStatusBarWindowManager != null) { + mStatusBarWindowManager.setShowingMedia(mKeyguardShowingMedia); + } - if ((hasArtwork || DEBUG_MEDIA_FAKE_ARTWORK) + if ((hasBackdrop || DEBUG_MEDIA_FAKE_ARTWORK) && (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) && mFingerprintUnlockController.getMode() != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING) { @@ -1743,7 +2270,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mBackdropBack.setBackgroundColor(0xFFFFFFFF); mBackdropBack.setImageDrawable(new ColorDrawable(c)); } else { - mBackdropBack.setImageBitmap(artworkBitmap); + mBackdropBack.setImageBitmap(backdropBitmap); } if (mScrimSrcModeEnabled) { mBackdropBack.getDrawable().mutate().setXfermode(mSrcXferMode); @@ -1947,6 +2474,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mLeaveOpenOnKeyguardHide; } + public boolean isKeyguardShowingMedia() { + return mKeyguardShowingMedia; + } + public boolean isQsExpanded() { return mNotificationPanel.isQsExpanded(); } @@ -1955,6 +2486,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mWakeUpComingFromTouch; } + void setBlur(float b){ + mStatusBarWindowManager.setBlur(b); + } + public boolean isFalsingThresholdNeeded() { return getBarState() == StatusBarState.KEYGUARD; } @@ -1968,6 +2503,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mMediaNotificationKey; } + @Override + protected MediaController getCurrentMediaController() { + return mMediaController; + } + public boolean isScrimSrcModeEnabled() { return mScrimSrcModeEnabled; } @@ -2295,6 +2835,98 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } + private void adjustBrightness(int x) { + mBrightnessChanged = true; + float raw = ((float) x) / mScreenWidth; + + // Add a padding to the brightness control on both sides to + // make it easier to reach min/max brightness + float padded = Math.min(1.0f - BRIGHTNESS_CONTROL_PADDING, + Math.max(BRIGHTNESS_CONTROL_PADDING, raw)); + float value = (padded - BRIGHTNESS_CONTROL_PADDING) / + (1 - (2.0f * BRIGHTNESS_CONTROL_PADDING)); + try { + IPowerManager power = IPowerManager.Stub.asInterface( + ServiceManager.getService("power")); + if (power != null) { + if (mAutomaticBrightness) { + float adj = (2 * value) - 1; + adj = Math.max(adj, -1); + adj = Math.min(adj, 1); + final float val = adj; + power.setTemporaryScreenAutoBrightnessAdjustmentSettingOverride(val); + AsyncTask.execute(new Runnable() { + public void run() { + Settings.System.putFloatForUser(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, val, + UserHandle.USER_CURRENT); + } + }); + } else { + int newBrightness = mMinBrightness + (int) Math.round(value * + (android.os.PowerManager.BRIGHTNESS_ON - mMinBrightness)); + newBrightness = Math.min(newBrightness, android.os.PowerManager.BRIGHTNESS_ON); + newBrightness = Math.max(newBrightness, mMinBrightness); + final int val = newBrightness; + power.setTemporaryScreenBrightnessSettingOverride(val); + AsyncTask.execute(new Runnable() { + @Override + public void run() { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS, val, + UserHandle.USER_CURRENT); + } + }); + } + + + } + } catch (RemoteException e) { + Log.w(TAG, "Setting Brightness failed: " + e); + } + } + + private void brightnessControl(MotionEvent event) { + final int action = event.getAction(); + final int x = (int) event.getRawX(); + final int y = (int) event.getRawY(); + if (action == MotionEvent.ACTION_DOWN) { + if (y < mStatusBarHeaderHeight) { + mLinger = 0; + mInitialTouchX = x; + mInitialTouchY = y; + mJustPeeked = true; + mHandler.removeCallbacks(mLongPressBrightnessChange); + mHandler.postDelayed(mLongPressBrightnessChange, + BRIGHTNESS_CONTROL_LONG_PRESS_TIMEOUT); + } + } else if (action == MotionEvent.ACTION_MOVE) { + if (y < mStatusBarHeaderHeight && mJustPeeked) { + if (mLinger > BRIGHTNESS_CONTROL_LINGER_THRESHOLD) { + adjustBrightness(x); + } else { + final int xDiff = Math.abs(x - mInitialTouchX); + final int yDiff = Math.abs(y - mInitialTouchY); + final int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + if (xDiff > yDiff) { + mLinger++; + } + if (xDiff > touchSlop || yDiff > touchSlop) { + mHandler.removeCallbacks(mLongPressBrightnessChange); + } + } + } else { + if (y > mStatusBarHeaderHeight) { + mJustPeeked = false; + } + mHandler.removeCallbacks(mLongPressBrightnessChange); + } + } else if (action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_CANCEL) { + mHandler.removeCallbacks(mLongPressBrightnessChange); + } + } + public boolean interceptTouchEvent(MotionEvent event) { if (DEBUG_GESTURES) { if (event.getActionMasked() != MotionEvent.ACTION_MOVE) { @@ -2321,16 +2953,29 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mGestureRec.add(event); } + if (mBrightnessControl) { + brightnessControl(event); + if ((mDisabled1 & StatusBarManager.DISABLE_EXPAND) != 0) { + return true; + } + } + + final boolean upOrCancel = + event.getAction() == MotionEvent.ACTION_UP || + event.getAction() == MotionEvent.ACTION_CANCEL; if (mStatusBarWindowState == WINDOW_STATE_SHOWING) { - final boolean upOrCancel = - event.getAction() == MotionEvent.ACTION_UP || - event.getAction() == MotionEvent.ACTION_CANCEL; if (upOrCancel && !mExpandedVisible) { setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false); } else { setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true); } } + if (mBrightnessChanged && upOrCancel && !isQsExpanded()) { + mBrightnessChanged = false; + if (mJustPeeked && mExpandedVisible) { + mNotificationPanel.fling(10, false); + } + } return false; } @@ -2771,6 +3416,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mBatteryController != null) { mBatteryController.dump(fd, pw, args); } + if (mDockBatteryController != null) { + mDockBatteryController.dump(fd, pw, args); + } if (mNextAlarmController != null) { mNextAlarmController.dump(fd, pw, args); } @@ -2813,8 +3461,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private void addStatusBarWindow() { makeStatusBarView(); - mStatusBarWindowManager = new StatusBarWindowManager(mContext); + mStatusBarWindow.addContent(mStatusBarWindowContent); + mStatusBarWindowManager = new StatusBarWindowManager(mContext, mKeyguardMonitor); + mStatusBarWindowManager.setShowingMedia(mKeyguardShowingMedia); mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight()); + if (mLiveLockScreenController != null) { + mStatusBarWindowManager.setLiveLockscreenController(mLiveLockScreenController); + } } // called by makeStatusbar and also by PhoneStatusBarView @@ -2838,10 +3491,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, 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); + startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, afterKeyguardGone, + callback); + } + + public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned, + final boolean dismissShade, final boolean afterKeyguardGone, final Callback callback) { + if (onlyProvisioned && !isDeviceProvisioned()) return; + final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing(); Runnable runnable = new Runnable() { public void run() { @@ -2924,13 +3583,33 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { + mScreenOn = false; notifyNavigationBarScreenOn(false); notifyHeadsUpScreenOff(); finishBarAnimations(); resetUserExpandedStates(); } else if (Intent.ACTION_SCREEN_ON.equals(action)) { + mScreenOn = true; notifyNavigationBarScreenOn(true); + } else if (Intent.ACTION_KEYGUARD_WALLPAPER_CHANGED.equals(action)) { + WallpaperManager wm = (WallpaperManager) mContext.getSystemService( + Context.WALLPAPER_SERVICE); + mKeyguardWallpaper = wm.getKeyguardBitmap(); + updateMediaMetaData(true); + } else if (cyanogenmod.content.Intent.ACTION_SCREEN_CAMERA_GESTURE.equals(action)) { + boolean userSetupComplete = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, 0) != 0; + if (!userSetupComplete || !isDeviceProvisioned()) { + if (DEBUG) { + Log.d(TAG, String.format("userSetupComplete = $1%s, " + + "deviceProvisioned = $2%s, ignoring camera launch gesture.", + userSetupComplete, isDeviceProvisioned())); + } + return; + } + + onCameraLaunchGestureDetected(StatusBarManager.CAMERA_LAUNCH_SOURCE_SCREEN_GESTURE); } } }; @@ -2955,6 +3634,24 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (DEBUG_MEDIA_FAKE_ARTWORK) { updateMediaMetaData(true); } + } else if (Intent.ACTION_KEYGUARD_WALLPAPER_CHANGED.equals(action)) { + WallpaperManager wm = (WallpaperManager) mContext.getSystemService( + Context.WALLPAPER_SERVICE); + mKeyguardWallpaper = wm.getKeyguardBitmap(); + updateMediaMetaData(true); + } + } + }; + + private BroadcastReceiver mPackageBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (DEBUG) Log.v(TAG, "onReceive: " + intent); + String action = intent.getAction(); + if (Intent.ACTION_PACKAGE_ADDED.equals(action) || + Intent.ACTION_PACKAGE_CHANGED.equals(action) || + Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action) || + Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + updateCustomRecentsLongPressHandler(true); } } }; @@ -2995,24 +3692,42 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } updateDisplaySize(); // populates mDisplayMetrics - updateResources(); + updateResources(newConfig); repositionNavigationBar(); updateRowStates(); mIconController.updateResources(); mScreenPinningRequest.onConfigurationChanged(); mNetworkController.onConfigurationChanged(); + mStatusBarWindowManager.onConfigurationChanged(newConfig); } @Override public void userSwitched(int newUserId) { super.userSwitched(newUserId); if (MULTIUSER_DEBUG) mNotificationPanelDebugText.setText("USER " + newUserId); + WallpaperManager wm = (WallpaperManager) + mContext.getSystemService(Context.WALLPAPER_SERVICE); + mKeyguardWallpaper = null; + wm.forgetLoadedKeyguardWallpaper(); + animateCollapsePanels(); updatePublicMode(); updateNotifications(); resetUserSetupObserver(); setControllerUsers(); mAssistManager.onUserSwitched(newUserId); + + mKeyguardWallpaper = wm.getKeyguardBitmap(); + updateMediaMetaData(true); + if (mNavigationBarView != null) { + mNavigationBarView.updateSettings(); + } + } + + public void hideHeadsUp() { + if (mUseHeadsUp && mHeadsUpManager != null) { + mHeadsUpManager.releaseAllImmediately(); + } } private void setControllerUsers() { @@ -3022,6 +3737,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mSecurityController != null) { mSecurityController.onUserSwitched(mCurrentUserId); } + if (mBatteryController != null) { + mBatteryController.setUserId(mCurrentUserId); + } + if (mDockBatteryController != null) { + mDockBatteryController.setUserId(mCurrentUserId); + } } private void resetUserSetupObserver() { @@ -3032,6 +3753,164 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mUserSetupObserver, mCurrentUserId); } + private static void copyNotifications(ArrayList<Pair<String, StatusBarNotification>> dest, + NotificationData source) { + int N = source.size(); + for (int i = 0; i < N; i++) { + NotificationData.Entry entry = source.get(i); + dest.add(Pair.create(entry.key, entry.notification)); + } + } + + private void removeSignalCallbacks(NetworkController networkController) { + 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); + networkController.removeSignalCallback(signalCluster); + networkController.removeSignalCallback(signalClusterKeyguard); + networkController.removeSignalCallback(signalClusterQs); + + if (signalCluster != null) signalCluster.setSecurityController(null); + if (signalClusterKeyguard != null) signalClusterKeyguard.setSecurityController(null); + if (signalClusterQs != null) signalClusterQs.setSecurityController(null); + } + + private void recreateStatusBar() { + mRecreating = true; + + if (mNetworkController != null) { + removeSignalCallbacks(mNetworkController); + if (mNetworkController.hasVoiceCallingFeature()) { + mNetworkController.removeEmergencyListener(mHeader); + } + } + if (mHeadsUpManager != null) { + mHeadsUpManager.removeListener(mNotificationPanel); + mHeadsUpManager.removeListener(mScrimController); + } + if (mIconController != null) { + mIconController.cleanup(); + } + if (mKeyguardIndicationController != null) { + mKeyguardIndicationController.cleanup(); + } + if (mLiveLockScreenController != null) { + mLiveLockScreenController.cleanup(); + } + + mKeyguardBottomArea.cleanup(); + mStatusBarWindow.removeContent(mStatusBarWindowContent); + mStatusBarWindow.clearDisappearingChildren(); + + // extract icons from the soon-to-be recreated viewgroup. + ViewGroup statusIcons = mIconController.getStatusIcons(); + int nIcons = statusIcons != null ? statusIcons.getChildCount() : 0; + ArrayList<StatusBarIcon> icons = new ArrayList<StatusBarIcon>(nIcons); + ArrayList<String> iconSlots = new ArrayList<String>(nIcons); + for (int i = 0; i < nIcons; i++) { + StatusBarIconView iconView = (StatusBarIconView) statusIcons.getChildAt(i); + icons.add(iconView.getStatusBarIcon()); + iconSlots.add(iconView.getStatusBarSlot()); + } + + removeAllViews(mStatusBarWindowContent); + + // extract notifications. + RankingMap rankingMap = mNotificationData.getRankingMap(); + int nNotifs = mNotificationData.size(); + ArrayList<Pair<String, StatusBarNotification>> notifications = new ArrayList<>(nNotifs); + copyNotifications(notifications, mNotificationData); + // now remove all the notifications since we'll be re-creating these with the copied data + mNotificationData.clear(); + + if (mCustomTileListenerService != null) { + try { + mCustomTileListenerService.unregisterAsSystemService(); + } catch (RemoteException e) { + Log.e(TAG, "Unable to unregister custom tile listener", e); + } + } + + mQSPanel.getHost().setCustomTileListenerService(null); + mQSPanel.setListening(false); + + makeStatusBarView(); + repositionNavigationBar(); + + // re-add status icons + for (int i = 0; i < nIcons; i++) { + StatusBarIcon icon = icons.get(i); + String slot = iconSlots.get(i); + addIcon(slot, i, i, icon); + } + + // recreate notifications. + for (int i = 0; i < nNotifs; i++) { + Pair<String, StatusBarNotification> notifData = notifications.get(i); + addNotificationViews(createNotificationViews(notifData.second), rankingMap); + } + mNotificationData.filterAndSort(); + + setAreThereNotifications(); + + mStatusBarWindow.addContent(mStatusBarWindowContent); + + checkBarModes(); + + // Stop the command queue until the new status bar container settles and has a layout pass + mCommandQueue.pause(); + + // fix notification panel being shifted to the left by calling + // instantCollapseNotificationPanel() + instantCollapseNotificationPanel(); + + mStatusBarWindow.requestLayout(); + mStatusBarWindow.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + mStatusBarWindow.getViewTreeObserver().removeOnGlobalLayoutListener(this); + mCommandQueue.resume(); + mRecreating = false; + } + }); + // restart the keyguard so it picks up the newly created ScrimController + startKeyguard(); + + // if the keyguard was showing while this change occurred we'll need to do some extra work + if (mState == StatusBarState.KEYGUARD) { + // this will make sure the keyguard is showing + showKeyguard(); + // make sure to hide the notification icon area and system iconography + // to avoid overlap (CYNGNOS-2253) + mIconController.hideNotificationIconArea(false); + mIconController.hideSystemIconArea(false); + } + + // update mLastThemeChangeTime + try { + mLastThemeChangeTime = mThemeService.getLastThemeChangeTime(); + } catch (RemoteException e) { + /* ignore */ + } + } + + private void removeAllViews(ViewGroup parent) { + int N = parent.getChildCount(); + for (int i = 0; i < N; i++) { + View child = parent.getChildAt(i); + if (child instanceof ViewGroup) { + removeAllViews((ViewGroup) child); + } + } + + // AdapterView does not support removeAllViews so check before calling + if (!(parent instanceof AdapterView)) parent.removeAllViews(); + } + /** * Reload some of our resources when the configuration changes. * @@ -3039,7 +3918,18 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, * should, but getting that smooth is tough. Someday we'll fix that. In the * meantime, just update the things that we know change. */ - void updateResources() { + void updateResources(Configuration newConfig) { + // detect theme change. + ThemeConfig newTheme = newConfig != null ? newConfig.themeConfig : null; + final boolean updateStatusBar = shouldUpdateStatusbar(mCurrentTheme, newTheme); + final boolean updateNavBar = shouldUpdateNavbar(mCurrentTheme, newTheme); + if (newTheme != null) mCurrentTheme = (ThemeConfig) newTheme.clone(); + if (updateStatusBar) { + recreateStatusBar(); + } else { + loadDimens(); + } + // Update the quick setting tiles if (mQSPanel != null) { mQSPanel.updateResources(); @@ -3053,6 +3943,66 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mBrightnessMirrorController != null) { mBrightnessMirrorController.updateResources(); } + + if (mNavigationBarView != null && updateNavBar) { + mNavigationBarView.updateResources(getNavbarThemedResources()); + } + } + + /** + * Determines if we need to recreate the status bar due to a theme change. We currently + * check if the overlay for the status bar, fonts, or icons, or last theme change time is + * greater than mLastThemeChangeTime + * + * @param oldTheme + * @param newTheme + * @return True if we should recreate the status bar + */ + private boolean shouldUpdateStatusbar(ThemeConfig oldTheme, ThemeConfig newTheme) { + // no newTheme, so no need to update status bar + if (newTheme == null) return false; + + final String overlay = newTheme.getOverlayForStatusBar(); + final String icons = newTheme.getIconPackPkgName(); + final String fonts = newTheme.getFontPkgName(); + boolean isNewThemeChange = false; + try { + isNewThemeChange = mLastThemeChangeTime < mThemeService.getLastThemeChangeTime(); + } catch (RemoteException e) { + /* ignore */ + } + + return oldTheme == null || + (overlay != null && !overlay.equals(oldTheme.getOverlayForStatusBar()) || + (fonts != null && !fonts.equals(oldTheme.getFontPkgName())) || + (icons != null && !icons.equals(oldTheme.getIconPackPkgName())) || + isNewThemeChange); + } + + /** + * Determines if we need to update the navbar resources due to a theme change. We currently + * check if the overlay for the navbar, or last theme change time is greater than + * mLastThemeChangeTime + * + * @param oldTheme + * @param newTheme + * @return True if we should update the navbar + */ + private boolean shouldUpdateNavbar(ThemeConfig oldTheme, ThemeConfig newTheme) { + // no newTheme, so no need to update navbar + if (newTheme == null) return false; + + final String overlay = newTheme.getOverlayForNavBar(); + boolean isNewThemeChange = false; + try { + isNewThemeChange = mLastThemeChangeTime < mThemeService.getLastThemeChangeTime(); + } catch (RemoteException e) { + /* ignore */ + } + + return oldTheme == null || + (overlay != null && !overlay.equals(oldTheme.getOverlayForNavBar()) || + isNewThemeChange); } protected void loadDimens() { @@ -3066,6 +4016,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mKeyguardMaxNotificationCount = res.getInteger(R.integer.keyguard_max_notification_count); + mStatusBarHeaderHeight = res.getDimensionPixelSize(R.dimen.status_bar_header_height); + if (DEBUG) Log.v(TAG, "updateResources"); } @@ -3207,7 +4159,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public boolean shouldDisableNavbarGestures() { - return !isDeviceProvisioned() || (mDisabled1 & StatusBarManager.DISABLE_SEARCH) != 0; + return !isDeviceProvisioned() || (mDisabled1 & StatusBarManager.DISABLE_SEARCH) != 0 + || (mNavigationBarView != null && mNavigationBarView.isInEditMode()); } public void postStartActivityDismissingKeyguard(final PendingIntent intent) { @@ -3327,6 +4280,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } if (modeChange || command.equals(COMMAND_BATTERY)) { dispatchDemoCommandToView(command, args, R.id.battery); + dispatchDemoCommandToView(command, args, R.id.dock_battery); } if (modeChange || command.equals(COMMAND_STATUS)) { mIconController.dispatchDemoCommand(command, args); @@ -3407,6 +4361,18 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mDraggedDownRow = null; } mAssistManager.onLockscreenShown(); + mKeyguardBottomArea.requestFocus(); + try { + WindowManagerGlobal.getWindowManagerService() + .setLiveLockscreenEdgeDetector(false); + } catch (RemoteException e){ + e.printStackTrace(); + } + if (mLiveLockScreenController.isShowingLiveLockScreenView()) { + mLiveLockScreenController.onLiveLockScreenFocusChanged(false); + mLiveLockScreenController.getLiveLockScreenView().onKeyguardShowing( + mStatusBarKeyguardViewManager.isScreenTurnedOn()); + } } private void onLaunchTransitionFadingEnded() { @@ -3557,6 +4523,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mNotificationPanel.onAffordanceLaunchEnded(); mNotificationPanel.animate().cancel(); mNotificationPanel.setAlpha(1f); + if (mLiveLockScreenController.isShowingLiveLockScreenView()) { + mLiveLockScreenController.getLiveLockScreenView().onKeyguardDismissed(); + } return staying; } @@ -3566,6 +4535,14 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } + boolean isSecure() { + return mStatusBarKeyguardViewManager != null && mStatusBarKeyguardViewManager.isSecure(); + } + + public boolean isKeyguardInputRestricted() { + return mStatusBarKeyguardViewManager != null && mStatusBarKeyguardViewManager.isInputRestricted(); + } + public long calculateGoingToFullShadeDelay() { return mKeyguardFadingAwayDelay + mKeyguardFadingAwayDuration; } @@ -3641,6 +4618,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mIconPolicy.setKeyguardShowing(false); } mNotificationPanel.setBarState(mState, mKeyguardFadingAway, goingToFullShade); + mLiveLockScreenController.setBarState(mState); updateDozingState(); updatePublicMode(); updateStackScrollerState(goingToFullShade); @@ -3662,12 +4640,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mDozeScrimController.setDozing(mDozing && mFingerprintUnlockController.getMode() != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING, animate); + mVisualizerView.setDozing(mDozing); } public void updateStackScrollerState(boolean goingToFullShade) { if (mStackScroller == null) return; boolean onKeyguard = mState == StatusBarState.KEYGUARD; - mStackScroller.setHideSensitive(isLockscreenPublicMode(), goingToFullShade); + mStackScroller.setHideSensitive(isLockscreenPublicMode() + || (!userAllowsPrivateNotificationsInPublic(mCurrentUserId) && onKeyguard), + goingToFullShade); mStackScroller.setDimmed(onKeyguard, false /* animate */); mStackScroller.setExpandingEnabled(!onKeyguard); ActivatableNotificationView activatedChild = mStackScroller.getActivatedChild(); @@ -3726,13 +4707,34 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return false; } - private void showBouncer() { - if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) { + public void showBouncer() { + if (!mRecreating && mNotificationPanel.mCanDismissKeyguard + && (mState != StatusBarState.SHADE || mLiveLockScreenController.getLiveLockScreenHasFocus())) { mWaitingForKeyguardExit = mStatusBarKeyguardViewManager.isShowing(); mStatusBarKeyguardViewManager.dismiss(); } } + protected void showBouncerOrFocusKeyguardExternalView() { + if (mLiveLockScreenController.isShowingLiveLockScreenView() && !isKeyguardShowingMedia() && + mLiveLockScreenController.isLiveLockScreenInteractive()) { + focusKeyguardExternalView(); + } else { + showBouncer(); + } + } + + protected void unfocusKeyguardExternalView() { + mStatusBarKeyguardViewManager.setKeyguardExternalViewFocus(false); + } + + public void focusKeyguardExternalView() { + mStatusBarView.collapseAllPanels(/*animate=*/ false, false /* delayed*/, + 1.0f /* speedUpFactor */); + mStatusBarKeyguardViewManager.setKeyguardExternalViewFocus(true); + setBarState(StatusBarState.SHADE); + } + private void instantExpandNotificationsPanel() { // Make our window larger and the panel expanded. @@ -3770,6 +4772,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, clearNotificationEffects(); } mState = state; + mVisualizerView.setStatusBarState(state); mGroupManager.setStatusBarState(state); mStatusBarWindowManager.setStatusBarState(state); updateDozing(); @@ -3792,24 +4795,41 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } public void onUnlockHintStarted() { - mKeyguardIndicationController.showTransientIndication(R.string.keyguard_unlock); + mKeyguardIndicationController.showTransientIndication(R.string.keyguard_unlock, + mNotificationPanel.shouldShowScreenOnHints() ? + KeyguardIndicationController.IndicationDirection.UP : + KeyguardIndicationController.IndicationDirection.NONE); + } + + public void onLlsHintStarted() { + String llsName = mLiveLockScreenController.getLiveLockScreenName(); + mKeyguardIndicationController.showTransientIndication( + mContext.getString(R.string.swipe_left_hint, llsName), + KeyguardIndicationController.IndicationDirection.LEFT); + } + + public void onExpandHintStarted() { + mKeyguardIndicationController.showTransientIndication(R.string.expand_hint, + KeyguardIndicationController.IndicationDirection.DOWN); + } + + public void onNotificationsHintStarted() { + mKeyguardIndicationController.showTransientIndication(R.string.swipe_right_hint, + KeyguardIndicationController.IndicationDirection.RIGHT); } public void onHintFinished() { // Delay the reset a bit so the user can read the text. mKeyguardIndicationController.hideTransientIndicationDelayed(HINT_RESET_DELAY_MS); + mKeyguardBottomArea.expand(false); } - public void onCameraHintStarted() { - mKeyguardIndicationController.showTransientIndication(R.string.camera_hint); - } - - public void onVoiceAssistHintStarted() { - mKeyguardIndicationController.showTransientIndication(R.string.voice_hint); + public void onCameraHintStarted(String hint) { + mKeyguardIndicationController.showTransientIndication(hint); } - public void onPhoneHintStarted() { - mKeyguardIndicationController.showTransientIndication(R.string.phone_hint); + public void onLeftHintStarted(String hint) { + mKeyguardIndicationController.showTransientIndication(hint); } public void onTrackingStopped(boolean expand) { @@ -3822,7 +4842,14 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override protected int getMaxKeyguardNotifications() { - return mKeyguardMaxNotificationCount; + int max = mKeyguardMaxNotificationCount; + // When an interactive live lockscreen is showing + // we want to limit the number of maximum notifications + // by 1 so there is additional space for the user to dismiss keygard + if (mLiveLockScreenController.isLiveLockScreenInteractive()) { + max--; + } + return max; } public NavigationBarView getNavigationBarView() { @@ -3938,6 +4965,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mWakeUpTouchLocation = null; mStackScroller.setAnimationsEnabled(false); updateVisibleToUser(); + if (mQSTileHost.isEditing()) { + mQSTileHost.setEditing(false); + } if (mLaunchCameraOnFinishedGoingToSleep) { mLaunchCameraOnFinishedGoingToSleep = false; @@ -3970,12 +5000,23 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private void vibrateForCameraGesture() { // Make sure to pass -1 for repeat so VibratorService doesn't stop us when going to sleep. - mVibrator.vibrate(new long[] { 0, 750L }, -1 /* repeat */); + mVibrator.vibrate(new long[] { 0, 250L }, -1 /* repeat */); } public void onScreenTurnedOn() { mScreenTurningOn = false; mDozeScrimController.onScreenTurnedOn(); + mVisualizerView.setVisible(true); + if (mLiveLockScreenController.isShowingLiveLockScreenView()) { + mLiveLockScreenController.onScreenTurnedOn(); + } + } + + public void onScreenTurnedOff() { + mVisualizerView.setVisible(false); + if (mLiveLockScreenController.isShowingLiveLockScreenView()) { + mLiveLockScreenController.onScreenTurnedOff(); + } } /** @@ -3994,6 +5035,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private void handleLongPressBackRecents(View v) { try { boolean sendBackLongPress = false; + boolean hijackRecentsLongPress = false; IActivityManager activityManager = ActivityManagerNative.getDefault(); boolean isAccessiblityEnabled = mAccessibilityManager.isEnabled(); if (activityManager.isInLockTaskMode() && !isAccessiblityEnabled) { @@ -4004,17 +5046,23 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, activityManager.stopLockTaskModeOnCurrent(); // When exiting refresh disabled flags. mNavigationBarView.setDisabledFlags(mDisabled1, true); - } else if ((v.getId() == R.id.back) + } else if (NavbarEditor.NAVBAR_BACK.equals(v.getTag()) && !mNavigationBarView.getRecentsButton().isPressed()) { // If we aren't pressing recents right now then they presses // won't be together, so send the standard long-press action. sendBackLongPress = true; + } else if (NavbarEditor.NAVBAR_RECENT.equals(v.getTag()) + && !activityManager.isInLockTaskMode()) { + hijackRecentsLongPress = true; } mLastLockToAppLongPress = time; } else { // If this is back still need to handle sending the long-press event. - if (v.getId() == R.id.back) { + if (NavbarEditor.NAVBAR_BACK.equals(v.getTag())) { sendBackLongPress = true; + } else if (NavbarEditor.NAVBAR_RECENT.equals(v.getTag()) + && !activityManager.isInLockTaskMode()) { + hijackRecentsLongPress = true; } else if (isAccessiblityEnabled && activityManager.isInLockTaskMode()) { // When in accessibility mode a long press that is recents (not back) // should stop lock task. @@ -4028,11 +5076,201 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, keyButtonView.sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); keyButtonView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); } + + if (hijackRecentsLongPress) { + // If there is a user-selected, registered handler for the + // recents long press, start the Intent. Otherwise, + // perform the default action, which is last app switching. + + // Copy it so the value doesn't change between now and when the activity is started. + ComponentName customRecentsLongPressHandler = mCustomRecentsLongPressHandler; + if (customRecentsLongPressHandler != null) { + startCustomRecentsLongPressActivity(customRecentsLongPressHandler); + } else { + ActionUtils.switchToLastApp(mContext, mCurrentUserId); + } + } } catch (RemoteException e) { Log.d(TAG, "Unable to reach activity manager", e); } } + protected View.OnTouchListener mRecentsPreloadOnTouchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + int action = event.getAction() & MotionEvent.ACTION_MASK; + + // Handle Document switcher + // Additional optimization when we have software system buttons - start loading the recent + // tasks on touch down + if (action == MotionEvent.ACTION_DOWN) { + preloadRecents(); + } else if (action == MotionEvent.ACTION_CANCEL) { + cancelPreloadingRecents(); + } else if (action == MotionEvent.ACTION_UP) { + if (!v.isPressed()) { + cancelPreloadingRecents(); + } + } + + // Handle custom recents long press + if (action == MotionEvent.ACTION_CANCEL || + action == MotionEvent.ACTION_UP) { + cleanupCustomRecentsLongPressHandler(); + } + return false; + } + }; + + /** + * If a custom Recents Long Press activity was dispatched, then the certain + * handlers need to be cleaned up after the event ends. + */ + private void cleanupCustomRecentsLongPressHandler() { + if (mCustomRecentsLongPressed) { + mNavigationBarView.setSlippery(false); + } + mCustomRecentsLongPressed = false; + } + + /** + * An ACTION_RECENTS_LONG_PRESS intent was received, and a custom handler is + * set and points to a valid app. Start this activity. + */ + private void startCustomRecentsLongPressActivity(ComponentName customComponentName) { + Intent intent = new Intent(cyanogenmod.content.Intent.ACTION_RECENTS_LONG_PRESS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + // Include the package name of the app currently in the foreground + IActivityManager am = ActivityManagerNative.getDefault(); + List<ActivityManager.RecentTaskInfo> recentTasks = null; + try { + recentTasks = am.getRecentTasks( + 1, ActivityManager.RECENT_WITH_EXCLUDED, UserHandle.myUserId()); + } catch (RemoteException e) { + Log.e(TAG, "Cannot get recent tasks", e); + } + if (recentTasks != null && recentTasks.size() > 0) { + String pkgName = recentTasks.get(0).baseIntent.getComponent().getPackageName(); + intent.putExtra(Intent.EXTRA_CURRENT_PACKAGE_NAME, pkgName); + } + + intent.setComponent(customComponentName); + try { + // Allow the touch event to continue into the new activity. + mNavigationBarView.setSlippery(true); + mCustomRecentsLongPressed = true; + + mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Cannot start activity", e); + + // If the activity failed to launch, clean up + cleanupCustomRecentsLongPressHandler(); + } + } + + /** + * Get component name for the recent long press setting. Null means default switch to last app. + * + * Note: every time packages changed, the setting must be re-evaluated. This is to check that the + * component was not uninstalled or disabled. + */ + private void updateCustomRecentsLongPressHandler(boolean scanPackages) { + // scanPackages should be true when the PhoneStatusBar is starting for + // the first time, and when any package activity occurred. + if (scanPackages) { + updateCustomRecentsLongPressCandidates(); + } + + String componentString = CMSettings.Secure.getString(mContext.getContentResolver(), + CMSettings.Secure.RECENTS_LONG_PRESS_ACTIVITY); + if (componentString == null) { + mCustomRecentsLongPressHandler = null; + return; + } + + ComponentName customComponentName = ComponentName.unflattenFromString(componentString); + synchronized (mCustomRecentsLongPressHandlerCandidates) { + for (ComponentName candidate : mCustomRecentsLongPressHandlerCandidates) { + if (candidate.equals(customComponentName)) { + // Found match + mCustomRecentsLongPressHandler = customComponentName; + + return; + } + } + + // Did not find match, probably because the selected application has + // now been uninstalled for some reason. Since user-selected app is + // still saved inside Settings, PhoneStatusBar should fall back to + // the default for now. (We will update this either when the + // package is reinstalled or when the user selects another Setting.) + mCustomRecentsLongPressHandler = null; + } + } + + /** + * Updates the cache of Recents Long Press applications. + * + * These applications must: + * - handle the cyanogenmod.contentIntent.ACTION_RECENTS_LONG_PRESS + * (which is permissions protected); and + * - not be disabled by the user or the system. + * + * More than one handler can be a candidate. When the action is invoked, + * the user setting (stored in CMSettings.Secure) is consulted. + */ + private void updateCustomRecentsLongPressCandidates() { + synchronized (mCustomRecentsLongPressHandlerCandidates) { + mCustomRecentsLongPressHandlerCandidates.clear(); + + PackageManager pm = mContext.getPackageManager(); + Intent intent = new Intent(cyanogenmod.content.Intent.ACTION_RECENTS_LONG_PRESS); + + // Search for all apps that can handle ACTION_RECENTS_LONG_PRESS + List<ResolveInfo> activities = pm.queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo info : activities) { + // Only cache packages that are not disabled + int packageState = mContext.getPackageManager().getApplicationEnabledSetting( + info.activityInfo.packageName); + + if (packageState != PackageManager.COMPONENT_ENABLED_STATE_DISABLED && + packageState != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { + + mCustomRecentsLongPressHandlerCandidates.add( + new ComponentName(info.activityInfo.packageName, info.activityInfo.name)); + } + + } + } + } + + private ActivityManager.RunningTaskInfo getLastTask(final ActivityManager am) { + final String defaultHomePackage = resolveCurrentLauncherPackage(); + List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(5); + + for (int i = 1; i < tasks.size(); i++) { + String packageName = tasks.get(i).topActivity.getPackageName(); + if (!packageName.equals(defaultHomePackage) + && !packageName.equals(mContext.getPackageName())) { + return tasks.get(i); + } + } + + return null; + } + + private String resolveCurrentLauncherPackage() { + final Intent launcherIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME); + final PackageManager pm = mContext.getPackageManager(); + final ResolveInfo launcherInfo = pm.resolveActivity(launcherIntent, 0); + return launcherInfo.activityInfo.packageName; + } + // Recents @Override @@ -4143,7 +5381,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, pm.wakeUp(SystemClock.uptimeMillis(), "com.android.systemui:CAMERA_GESTURE"); mStatusBarKeyguardViewManager.notifyDeviceWakeUpRequested(); } - vibrateForCameraGesture(); + if (source != StatusBarManager.CAMERA_LAUNCH_SOURCE_SCREEN_GESTURE) { + vibrateForCameraGesture(); + } if (!mStatusBarKeyguardViewManager.isShowing()) { startActivity(KeyguardBottomAreaView.INSECURE_CAMERA_INTENT, true /* dismissShade */); @@ -4178,6 +5418,18 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateDozingState(); } + public VisualizerView getVisualizer() { + return mVisualizerView; + } + + public boolean isShowingLiveLockScreenView() { + return mLiveLockScreenController.isShowingLiveLockScreenView(); + } + + public void slideNotificationPanelIn() { + mNotificationPanel.slideLockScreenIn(); + } + private final class ShadeUpdates { private final ArraySet<String> mVisibleNotifications = new ArraySet<String>(); private final ArraySet<String> mNewVisibleNotifications = new ArraySet<String>(); @@ -4330,4 +5582,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } } + + public boolean isAffordanceSwipeInProgress() { + return mNotificationPanel.isAffordanceSwipeInProgress(); + } } 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 83edc96..1395ff6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -20,18 +20,29 @@ import android.app.ActivityManagerNative; import android.app.AlarmManager; import android.app.AlarmManager.AlarmClockInfo; import android.app.IUserSwitchObserver; +import android.app.PendingIntent; import android.app.StatusBarManager; +import android.bluetooth.BluetoothAssignedNumbers; +import android.bluetooth.BluetoothHeadset; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.UserInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; +import android.graphics.Bitmap; import android.media.AudioManager; +import android.net.Uri; +import android.os.Binder; import android.os.Handler; import android.os.IRemoteCallback; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.provider.Settings.Global; import android.telecom.TelecomManager; import android.util.Log; @@ -46,6 +57,17 @@ import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.CastController.CastDevice; import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.statusbar.policy.SuController; + +import cyanogenmod.app.CMStatusBarManager; +import cyanogenmod.app.CustomTile; +import cyanogenmod.providers.CMSettings; + +import org.cyanogenmod.internal.util.QSUtils; +import org.cyanogenmod.internal.util.QSUtils.OnQSChanged; +import org.cyanogenmod.internal.util.QSConstants; + +import java.util.ArrayList; /** * This class contains all of the policy about which icons are installed in the status @@ -64,6 +86,7 @@ public class PhoneStatusBarPolicy implements Callback { private static final String SLOT_VOLUME = "volume"; private static final String SLOT_ALARM_CLOCK = "alarm_clock"; private static final String SLOT_MANAGED_PROFILE = "managed_profile"; + private static final String SLOT_SU = "su"; private final Context mContext; private final StatusBarManager mService; @@ -72,6 +95,8 @@ public class PhoneStatusBarPolicy implements Callback { private final HotspotController mHotspot; private final AlarmManager mAlarmManager; private final UserInfoController mUserInfoController; + private boolean mAlarmIconVisible; + private final SuController mSuController; // Assume it's all good unless we hear otherwise. We don't always seem // to get broadcasts that it *is* there. @@ -80,6 +105,7 @@ public class PhoneStatusBarPolicy implements Callback { private boolean mZenVisible; private boolean mVolumeVisible; private boolean mCurrentUserSetup; + private Float mBluetoothBatteryLevel = null; private int mZen; @@ -106,6 +132,9 @@ public class PhoneStatusBarPolicy implements Callback { else if (action.equals(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED)) { updateTTY(intent); } + else if (action.equals(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT)) { + updateBluetoothBattery(intent); + } } }; @@ -117,8 +146,15 @@ public class PhoneStatusBarPolicy implements Callback { } }; + private final OnQSChanged mQSListener = new OnQSChanged() { + @Override + public void onQSChanged() { + processQSChangedLocked(); + } + }; + public PhoneStatusBarPolicy(Context context, CastController cast, HotspotController hotspot, - UserInfoController userInfoController, BluetoothController bluetooth) { + UserInfoController userInfoController, BluetoothController bluetooth, SuController su) { mContext = context; mCast = cast; mHotspot = hotspot; @@ -127,6 +163,7 @@ public class PhoneStatusBarPolicy implements Callback { mService = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE); mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mUserInfoController = userInfoController; + mSuController = su; // listen for broadcasts IntentFilter filter = new IntentFilter(); @@ -135,6 +172,9 @@ public class PhoneStatusBarPolicy implements Callback { filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); filter.addAction(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED); + filter.addAction(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT); + filter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + + "." + Integer.toString(BluetoothAssignedNumbers.APPLE)); mContext.registerReceiver(mIntentReceiver, filter, null, mHandler); // listen for user / profile change. @@ -151,9 +191,16 @@ public class PhoneStatusBarPolicy implements Callback { // bluetooth status updateBluetooth(); + //Update initial tty mode + updateTTYMode(); + // Alarm clock mService.setIcon(SLOT_ALARM_CLOCK, R.drawable.stat_sys_alarm, 0, null); mService.setIconVisibility(SLOT_ALARM_CLOCK, false); + mAlarmIconObserver.onChange(true); + mContext.getContentResolver().registerContentObserver( + CMSettings.System.getUriFor(CMSettings.System.SHOW_ALARM_ICON), + false, mAlarmIconObserver); // zen mService.setIcon(SLOT_ZEN, R.drawable.stat_sys_zen_important, 0, null); @@ -175,12 +222,33 @@ public class PhoneStatusBarPolicy implements Callback { mService.setIconVisibility(SLOT_HOTSPOT, mHotspot.isHotspotEnabled()); mHotspot.addCallback(mHotspotCallback); + // su + mService.setIcon(SLOT_SU, R.drawable.stat_sys_su, 0, null); + mService.setIconVisibility(SLOT_SU, false); + mSuController.addCallback(mSuCallback); + // 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); + + QSUtils.registerObserverForQSChanges(mContext, mQSListener); } + private ContentObserver mAlarmIconObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange, Uri uri) { + mAlarmIconVisible = CMSettings.System.getInt(mContext.getContentResolver(), + CMSettings.System.SHOW_ALARM_ICON, 1) == 1; + updateAlarm(); + } + + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } + }; + public void setZenMode(int zen) { mZen = zen; updateVolumeZen(); @@ -192,7 +260,7 @@ public class PhoneStatusBarPolicy implements Callback { 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); + mService.setIconVisibility(SLOT_ALARM_CLOCK, mCurrentUserSetup && hasAlarm && mAlarmIconVisible); } private final void updateSimState(Intent intent) { @@ -236,8 +304,17 @@ public class PhoneStatusBarPolicy implements Callback { 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; + switch(mZen) { + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + zenIconId = R.drawable.stat_sys_dnd_priority; + break; + case Global.ZEN_MODE_NO_INTERRUPTIONS: + zenIconId = R.drawable.stat_sys_dnd_total_silence; + break; + default: + zenIconId = R.drawable.stat_sys_dnd; + break; + } zenDescription = mContext.getString(R.string.quick_settings_dnd_label); } else if (mZen == Global.ZEN_MODE_NO_INTERRUPTIONS) { zenVisible = true; @@ -289,6 +366,27 @@ public class PhoneStatusBarPolicy implements Callback { updateBluetooth(); } + private void updateBluetoothBattery(Intent intent) { + if (intent.hasExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD)) { + String command = intent.getStringExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD); + if ("+IPHONEACCEV".equals(command)) { + Object[] args = (Object[]) intent.getSerializableExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS); + if (args.length >= 3 && args[0] instanceof Integer && ((Integer)args[0])*2+1<=args.length) { + for (int i=0;i<((Integer)args[0]);i++) { + if (!(args[i*2+1] instanceof Integer) || !(args[i*2+2] instanceof Integer)) { + continue; + } + if (args[i*2+1].equals(1)) { + mBluetoothBatteryLevel = (((Integer)args[i*2+2])+1)/10.0f; + updateBluetooth(); + break; + } + } + } + } + } + } + private final void updateBluetooth() { int iconId = R.drawable.stat_sys_data_bluetooth; String contentDescription = @@ -297,8 +395,24 @@ public class PhoneStatusBarPolicy implements Callback { if (mBluetooth != null) { bluetoothEnabled = mBluetooth.isBluetoothEnabled(); if (mBluetooth.isBluetoothConnected()) { - iconId = R.drawable.stat_sys_data_bluetooth_connected; + if (mBluetoothBatteryLevel == null) { + iconId = R.drawable.stat_sys_data_bluetooth_connected; + } else { + if (mBluetoothBatteryLevel<=0.15f) { + iconId = R.drawable.stat_sys_data_bluetooth_connected_battery_1; + } else if (mBluetoothBatteryLevel<=0.375f) { + iconId = R.drawable.stat_sys_data_bluetooth_connected_battery_2; + } else if (mBluetoothBatteryLevel<=0.625f) { + iconId = R.drawable.stat_sys_data_bluetooth_connected_battery_3; + } else if (mBluetoothBatteryLevel<=0.85f) { + iconId = R.drawable.stat_sys_data_bluetooth_connected_battery_4; + } else { + iconId = R.drawable.stat_sys_data_bluetooth_connected_battery_5; + } + } contentDescription = mContext.getString(R.string.accessibility_bluetooth_connected); + } else { + mBluetoothBatteryLevel = null; } } @@ -326,6 +440,29 @@ public class PhoneStatusBarPolicy implements Callback { } } + private boolean isWiredHeadsetOn() { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + return audioManager.isWiredHeadsetOn(); + } + + private final void updateTTYMode() { + int ttyMode = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.PREFERRED_TTY_MODE, TelecomManager.TTY_MODE_OFF); + boolean enabled = ttyMode != TelecomManager.TTY_MODE_OFF; + if (DEBUG) Log.v(TAG, "updateTTYMode: enabled: " + enabled); + if (enabled && isWiredHeadsetOn()) { + // TTY is on + if (DEBUG) Log.v(TAG, "updateTTYMode: set TTY on"); + mService.setIcon(SLOT_TTY, R.drawable.stat_sys_tty_mode, 0, + mContext.getString(R.string.accessibility_tty_enabled)); + mService.setIconVisibility(SLOT_TTY, true); + } else { + // TTY is off + if (DEBUG) Log.v(TAG, "updateTTYMode: set TTY off"); + mService.setIconVisibility(SLOT_TTY, false); + } + } + private void updateCast() { boolean isCasting = false; for (CastDevice device : mCast.getCastDevices()) { @@ -410,6 +547,16 @@ public class PhoneStatusBarPolicy implements Callback { } }; + private void updateSu() { + mService.setIconVisibility(SLOT_SU, mSuController.hasActiveSessions()); + final int userId = UserHandle.myUserId(); + if (isSuEnabledForUser(userId)) { + publishSuCustomTile(); + } else { + unpublishSuCustomTile(); + } + } + private final CastController.Callback mCastCallback = new CastController.Callback() { @Override public void onCastDevicesChanged() { @@ -431,4 +578,118 @@ public class PhoneStatusBarPolicy implements Callback { mCurrentUserSetup = userSetup; updateAlarm(); } + + private final SuController.Callback mSuCallback = new SuController.Callback() { + @Override + public void onSuSessionsChanged() { + updateSu(); + } + }; + + private void publishSuCustomTile() { + // This action should be performed as system + final int userId = UserHandle.myUserId(); + long token = Binder.clearCallingIdentity(); + try { + final UserHandle user = new UserHandle(userId); + final int icon = QSUtils.getDynamicQSTileResIconId(mContext, userId, + QSConstants.DYNAMIC_TILE_SU); + final String contentDesc = QSUtils.getDynamicQSTileLabel(mContext, userId, + QSConstants.DYNAMIC_TILE_SU); + final Context resourceContext = QSUtils.getQSTileContext(mContext, userId); + + CustomTile.ListExpandedStyle style = new CustomTile.ListExpandedStyle(); + ArrayList<CustomTile.ExpandedListItem> items = new ArrayList<>(); + for (String pkg : mSuController.getPackageNamesWithActiveSuSessions()) { + CustomTile.ExpandedListItem item = new CustomTile.ExpandedListItem(); + int appIconIdentifier = getActiveSuApkDrawableId(pkg); + if (appIconIdentifier != -1) { + item.setExpandedListItemDrawable(appIconIdentifier); + } else { + item.setExpandedListItemDrawable(icon); + } + item.setExpandedListItemTitle(getActiveSuApkLabel(pkg)); + item.setExpandedListItemSummary(pkg); + item.setExpandedListItemOnClickIntent(getCustomTilePendingIntent(pkg)); + items.add(item); + } + style.setListItems(items); + + CMStatusBarManager statusBarManager = CMStatusBarManager.getInstance(mContext); + CustomTile tile = new CustomTile.Builder(resourceContext) + .setLabel(contentDesc) + .setContentDescription(contentDesc) + .setIcon(icon) + .setOnSettingsClickIntent(getCustomTileSettingsIntent()) + .setExpandedStyle(style) + .build(); + statusBarManager.publishTileAsUser(QSConstants.DYNAMIC_TILE_SU, + PhoneStatusBarPolicy.class.hashCode(), tile, user); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void unpublishSuCustomTile() { + // This action should be performed as system + final int userId = UserHandle.myUserId(); + long token = Binder.clearCallingIdentity(); + try { + CMStatusBarManager statusBarManager = CMStatusBarManager.getInstance(mContext); + statusBarManager.removeTileAsUser(QSConstants.DYNAMIC_TILE_SU, + PhoneStatusBarPolicy.class.hashCode(), new UserHandle(userId)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private PendingIntent getCustomTilePendingIntent(String pkg) { + Intent i = new Intent(Intent.ACTION_MAIN); + i.setPackage(pkg); + i.addCategory(Intent.CATEGORY_LAUNCHER); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return PendingIntent.getActivity(mContext, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private Intent getCustomTileSettingsIntent() { + Intent i = new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return i; + } + + private String getActiveSuApkLabel(String pkg) { + final PackageManager pm = mContext.getPackageManager(); + ApplicationInfo ai = null; + try { + ai = pm.getApplicationInfo(pkg, 0); + } catch (final NameNotFoundException e) { + // Ignore + } + return (String) (ai != null ? pm.getApplicationLabel(ai) : pkg); + } + + private int getActiveSuApkDrawableId(String pkg) { + final PackageManager pm = mContext.getPackageManager(); + ApplicationInfo ai; + try { + ai = pm.getApplicationInfo(pkg, 0); + } catch (final NameNotFoundException e) { + return -1; + } + return ai.icon; + } + + private boolean isSuEnabledForUser(int userId) { + final boolean hasSuAccess = mSuController.hasActiveSessions(); + return (userId == UserHandle.USER_OWNER) && hasSuAccess; + } + + private void processQSChangedLocked() { + final int userId = UserHandle.myUserId(); + if (isSuEnabledForUser(userId)) { + publishSuCustomTile(); + } else { + unpublishSuCustomTile(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java index fb1addf..bb3095e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java @@ -36,7 +36,10 @@ public final class PhoneStatusBarTransitions extends BarTransitions { private Animator mCurrentAnimation; public PhoneStatusBarTransitions(PhoneStatusBarView view) { - super(view, R.drawable.status_background); + super(view, R.drawable.status_background, R.color.status_bar_background_opaque, + R.color.status_bar_background_semi_transparent, + R.color.status_bar_background_transparent, + com.android.internal.R.color.battery_saver_mode_color); mView = view; final Resources res = mView.getContext().getResources(); mIconAlphaWhenOpaque = res.getFraction(R.dimen.status_bar_icon_drawing_alpha, 1, 1); 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 c0887ca..8c9daee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -111,6 +111,7 @@ public class PhoneStatusBarView extends PanelBar { @Override public void onPanelPeeked() { super.onPanelPeeked(); + removePendingHideExpandedRunnables(); mBar.makeExpandedVisible(false); } @@ -193,6 +194,7 @@ public class PhoneStatusBarView extends PanelBar { super.panelExpansionChanged(panel, frac, expanded); mPanelFraction = frac; updateScrimFraction(); + mBar.setBlur(frac); } private void updateScrimFraction() { 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 e66c63b..e88ed73 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java @@ -16,29 +16,57 @@ package com.android.systemui.statusbar.phone; +import android.app.ActivityManager; import android.app.PendingIntent; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Process; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; +import com.android.internal.logging.MetricsLogger; +import android.widget.RemoteViews; import com.android.systemui.R; import com.android.systemui.qs.QSTile; +import com.android.systemui.qs.tiles.AdbOverNetworkTile; import com.android.systemui.qs.tiles.AirplaneModeTile; +import com.android.systemui.qs.tiles.AmbientDisplayTile; +import com.android.systemui.qs.tiles.BatterySaverTile; import com.android.systemui.qs.tiles.BluetoothTile; +import com.android.systemui.qs.tiles.CaffeineTile; 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.CompassTile; +import com.android.systemui.qs.tiles.CustomQSTile; import com.android.systemui.qs.tiles.DndTile; +import com.android.systemui.qs.tiles.EditTile; import com.android.systemui.qs.tiles.FlashlightTile; +import com.android.systemui.qs.tiles.HeadsUpTile; 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.LockscreenToggleTile; +import com.android.systemui.qs.tiles.NfcTile; +import com.android.systemui.qs.tiles.PerfProfileTile; +import com.android.systemui.qs.tiles.ProfilesTile; import com.android.systemui.qs.tiles.RotationLockTile; +import com.android.systemui.qs.tiles.ScreenTimeoutTile; +import com.android.systemui.qs.tiles.SyncTile; +import com.android.systemui.qs.tiles.UsbTetherTile; +import com.android.systemui.qs.tiles.VolumeTile; import com.android.systemui.qs.tiles.WifiTile; +import com.android.systemui.statusbar.CustomTileData; +import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.FlashlightController; @@ -53,6 +81,10 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; +import cyanogenmod.app.CustomTileListenerService; +import cyanogenmod.app.StatusBarPanelCustomTile; +import cyanogenmod.providers.CMSettings; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -65,7 +97,7 @@ public class QSTileHost implements QSTile.Host, Tunable { private static final String TAG = "QSTileHost"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - protected static final String TILES_SETTING = "sysui_qs_tiles"; + public static final int TILES_PER_PAGE = 8; private final Context mContext; private final PhoneStatusBar mStatusBar; @@ -83,6 +115,10 @@ public class QSTileHost implements QSTile.Host, Tunable { private final UserSwitcherController mUserSwitcherController; private final KeyguardMonitor mKeyguard; private final SecurityController mSecurity; + private final BatteryController mBattery; + + private CustomTileData mCustomTileData; + private CustomTileListenerService mCustomTileListenerService; private Callback mCallback; @@ -92,7 +128,7 @@ public class QSTileHost implements QSTile.Host, Tunable { ZenModeController zen, HotspotController hotspot, CastController cast, FlashlightController flashlight, UserSwitcherController userSwitcher, KeyguardMonitor keyguard, - SecurityController security) { + SecurityController security, BatteryController battery) { mContext = context; mStatusBar = statusBar; mBluetooth = bluetooth; @@ -106,19 +142,36 @@ public class QSTileHost implements QSTile.Host, Tunable { mUserSwitcherController = userSwitcher; mKeyguard = keyguard; mSecurity = security; + mBattery = battery; + mCustomTileData = new CustomTileData(); final HandlerThread ht = new HandlerThread(QSTileHost.class.getSimpleName(), Process.THREAD_PRIORITY_BACKGROUND); ht.start(); mLooper = ht.getLooper(); - TunerService.get(mContext).addTunable(this, TILES_SETTING); + TunerService.get(mContext).addTunableByProvider(this, CMSettings.Secure.QS_TILES, true); } public void destroy() { TunerService.get(mContext).removeTunable(this); } + public boolean isEditing() { + if (mCallback != null) { + return mCallback.isEditing(); + } + return false; + } + + public void setEditing(boolean editing) { + mCallback.setEditing(editing); + } + + void setCustomTileListenerService(CustomTileListenerService customTileListenerService) { + mCustomTileListenerService = customTileListenerService; + } + @Override public void setCallback(Callback callback) { mCallback = callback; @@ -129,12 +182,33 @@ public class QSTileHost implements QSTile.Host, Tunable { return mTiles.values(); } + public List<String> getTileSpecs() { + return mTileSpecs; + } + + public String getSpec(QSTile<?> tile) { + for (Map.Entry<String, QSTile<?>> entry : mTiles.entrySet()) { + if (entry.getValue() == tile) { + return entry.getKey(); + } + } + return null; + } + @Override public void startActivityDismissingKeyguard(final Intent intent) { mStatusBar.postStartActivityDismissingKeyguard(intent, 0); } @Override + public void removeCustomTile(StatusBarPanelCustomTile customTile) { + if (mCustomTileListenerService != null) { + mCustomTileListenerService.removeCustomTile(customTile.getPackage(), + customTile.getTag(), customTile.getId()); + } + } + + @Override public void startActivityDismissingKeyguard(PendingIntent intent) { mStatusBar.postStartActivityDismissingKeyguard(intent); } @@ -150,6 +224,11 @@ public class QSTileHost implements QSTile.Host, Tunable { } @Override + public RemoteViews.OnClickHandler getOnClickHandler() { + return mStatusBar.getOnClickHandler(); + } + + @Override public Looper getLooper() { return mLooper; } @@ -204,6 +283,11 @@ public class QSTileHost implements QSTile.Host, Tunable { return mKeyguard; } + @Override + public BatteryController getBatteryController() { + return mBattery; + } + public UserSwitcherController getUserSwitcherController() { return mUserSwitcherController; } @@ -214,14 +298,14 @@ public class QSTileHost implements QSTile.Host, Tunable { @Override public void onTuningChanged(String key, String newValue) { - if (!TILES_SETTING.equals(key)) { + if (!CMSettings.Secure.QS_TILES.equals(key)) { return; } if (DEBUG) Log.d(TAG, "Recreating tiles"); 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 (!tileSpecs.contains(tile.getKey()) && mCustomTileData.get(tile.getKey()) == null) { if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey()); tile.getValue().destroy(); } @@ -233,7 +317,14 @@ public class QSTileHost implements QSTile.Host, Tunable { } else { if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec); try { - newTiles.put(tileSpec, createTile(tileSpec)); + if (mCustomTileData.get(tileSpec) != null) { + final CustomQSTile value = new CustomQSTile(this, + mCustomTileData.get(tileSpec).sbc); + newTiles.put(tileSpec, value); + } else { + final QSTile<?> tile = createTile(tileSpec); + newTiles.put(tileSpec, tile); + } } catch (Throwable t) { Log.w(TAG, "Error creating tile for spec: " + tileSpec, t); } @@ -248,7 +339,14 @@ public class QSTileHost implements QSTile.Host, Tunable { } } - protected QSTile<?> createTile(String tileSpec) { + @Override + public void goToSettingsPage() { + if (mCallback != null) { + mCallback.goToSettingsPage(); + } + } + + public 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); @@ -260,13 +358,34 @@ public class QSTileHost implements QSTile.Host, Tunable { else if (tileSpec.equals("location")) return new LocationTile(this); else if (tileSpec.equals("cast")) return new CastTile(this); else if (tileSpec.equals("hotspot")) return new HotspotTile(this); + else if (tileSpec.equals("edit")) return new EditTile(this); + else if (tileSpec.equals("adb_network")) return new AdbOverNetworkTile(this); + else if (tileSpec.equals("compass")) return new CompassTile(this); + else if (tileSpec.equals("nfc")) return new NfcTile(this); + else if (tileSpec.equals("profiles")) return new ProfilesTile(this); + else if (tileSpec.equals("sync")) return new SyncTile(this); + else if (tileSpec.equals("volume_panel")) return new VolumeTile(this); + else if (tileSpec.equals("usb_tether")) return new UsbTetherTile(this); + else if (tileSpec.equals("screen_timeout")) return new ScreenTimeoutTile(this); + else if (tileSpec.equals("performance")) return new PerfProfileTile(this); + else if (tileSpec.equals("lockscreen")) return new LockscreenToggleTile(this); + else if (tileSpec.equals("ambient_display")) return new AmbientDisplayTile(this); + else if (tileSpec.equals("heads_up")) return new HeadsUpTile(this); + else if (tileSpec.equals("battery_saver")) return new BatterySaverTile(this); + else if (tileSpec.equals("caffeine")) return new CaffeineTile(this); else if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(this,tileSpec); - else throw new IllegalArgumentException("Bad tile spec: " + tileSpec); + else if (TextUtils.split(tileSpec, "\\|").length == 3) { + /** restores placeholder for + * {@link cyanogenmod.app.StatusBarPanelCustomTile#persistableKey()} **/ + return new CustomQSTile(this, tileSpec); + } else + throw new IllegalArgumentException("Bad tile spec: " + tileSpec); } protected List<String> loadTileSpecs(String tileList) { final Resources res = mContext.getResources(); - final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); + final String defaultTileList = res.getString(org.cyanogenmod.platform.internal. + R.string.config_defaultQuickSettingsTiles); if (tileList == null) { tileList = res.getString(R.string.quick_settings_tiles); if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList); @@ -287,6 +406,140 @@ public class QSTileHost implements QSTile.Host, Tunable { tiles.add(tile); } } + // ensure edit tile is present, default placement should be handled in the default + // tile list. + if (!tiles.contains("edit")) { + tiles.add("edit"); + } return tiles; } + + public void remove(String tile) { + MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_REMOVE, tile); + List<String> tiles = new ArrayList<>(mTileSpecs); + tiles.remove(tile); + setTiles(tiles); + } + + public void setTiles(List<String> tiles) { + CMSettings.Secure.putStringForUser(getContext().getContentResolver(), + CMSettings.Secure.QS_TILES, + TextUtils.join(",", tiles), ActivityManager.getCurrentUser()); + } + + public void initiateReset() { + if (mCallback != null) { + mCallback.resetTiles(); + } + } + + @Override + public void resetTiles() { + CMSettings.Secure.putStringForUser(getContext().getContentResolver(), + CMSettings.Secure.QS_TILES, "default", ActivityManager.getCurrentUser()); + } + + public QSTile<?> getTile(String spec) { + return mTiles.get(spec); + } + + public 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; + else if (spec.equals("edit")) return R.string.quick_settings_edit_label; + else if (spec.equals("adb_network")) return R.string.quick_settings_network_adb_label; + else if (spec.equals("compass")) return R.string.quick_settings_compass_label; + else if (spec.equals("nfc")) return R.string.quick_settings_nfc_label; + else if (spec.equals("profiles")) return R.string.quick_settings_profiles; + else if (spec.equals("sync")) return R.string.quick_settings_sync_label; + else if (spec.equals("volume_panel")) return R.string.quick_settings_volume_panel_label; + else if (spec.equals("usb_tether")) return R.string.quick_settings_usb_tether_label; + else if (spec.equals("screen_timeout")) return R.string.quick_settings_screen_timeout_detail_title; + else if (spec.equals("performance")) return R.string.qs_tile_performance; + else if (spec.equals("lockscreen")) return R.string.quick_settings_lockscreen_label; + else if (spec.equals("ambient_display")) return R.string.quick_settings_ambient_display_label; + else if (spec.equals("heads_up")) return R.string.quick_settings_heads_up_label; + else if (spec.equals("battery_saver")) return R.string.quick_settings_battery_saver_label; + else if (spec.equals("caffeine")) return R.string.quick_settings_caffeine_label; + return 0; + } + + public static int getIconResource(String spec) { + if (spec.equals("wifi")) return R.drawable.ic_qs_wifi_full_4; + else if (spec.equals("bt")) return R.drawable.ic_qs_bluetooth_on; + else if (spec.equals("inversion")) return R.drawable.ic_invert_colors_enable_animation; + else if (spec.equals("cell")) return R.drawable.ic_qs_signal_full_4; + else if (spec.equals("airplane")) return R.drawable.ic_signal_airplane_enable; + else if (spec.equals("dnd")) return R.drawable.ic_dnd; + else if (spec.equals("rotation")) return R.drawable.ic_portrait_from_auto_rotate; + else if (spec.equals("flashlight")) return R.drawable.ic_signal_flashlight_enable; + else if (spec.equals("location")) return R.drawable.ic_signal_location_enable; + else if (spec.equals("cast")) return R.drawable.ic_qs_cast_on; + else if (spec.equals("hotspot")) return R.drawable.ic_hotspot_enable; + else if (spec.equals("edit")) return R.drawable.ic_qs_edit_tiles; + else if (spec.equals("adb_network")) return R.drawable.ic_qs_network_adb_on; + else if (spec.equals("compass")) return R.drawable.ic_qs_compass_on; + else if (spec.equals("nfc")) return R.drawable.ic_qs_nfc_on; + else if (spec.equals("profiles")) return R.drawable.ic_qs_profiles_on; + else if (spec.equals("sync")) return R.drawable.ic_qs_sync_on; + else if (spec.equals("volume_panel")) return R.drawable.ic_qs_volume_panel; + else if (spec.equals("usb_tether")) return R.drawable.ic_qs_usb_tether_on; + else if (spec.equals("screen_timeout")) return R.drawable.ic_qs_screen_timeout_short_avd; + else if (spec.equals("performance")) return R.drawable.ic_qs_perf_profile; + else if (spec.equals("lockscreen")) return R.drawable.ic_qs_lock_screen_on; + else if (spec.equals("ambient_display")) return R.drawable.ic_qs_ambientdisplay_on; + else if (spec.equals("heads_up")) return R.drawable.ic_qs_heads_up_on; + else if (spec.equals("battery_saver")) return R.drawable.ic_qs_battery_saver_on; + else if (spec.equals("caffeine")) return R.drawable.ic_qs_caffeine_on; + return 0; + } + + void updateCustomTile(StatusBarPanelCustomTile sbc) { + synchronized (mTiles) { + if (mTiles.containsKey(sbc.persistableKey())) { + QSTile<?> tile = mTiles.get(sbc.persistableKey()); + if (tile instanceof CustomQSTile) { + CustomQSTile qsTile = (CustomQSTile) tile; + qsTile.update(sbc); + } + } + } + } + + void addCustomTile(StatusBarPanelCustomTile sbc) { + synchronized (mTiles) { + mCustomTileData.add(new CustomTileData.Entry(sbc)); + mTileSpecs.add(sbc.persistableKey()); + mTiles.put(sbc.persistableKey(), new CustomQSTile(this, sbc)); + if (mCallback != null) { + mCallback.onTilesChanged(); + } + } + } + + void removeCustomTileSysUi(String key) { + synchronized (mTiles) { + if (mTiles.containsKey(key)) { + mTileSpecs.remove(key); + mTiles.remove(key); + mCustomTileData.remove(key); + if (mCallback != null) { + mCallback.onTilesChanged(); + } + } + } + } + + public CustomTileData getCustomTileData() { + return mCustomTileData; + } } 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 b9e9292..975cb77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -45,9 +45,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, public static final long ANIMATION_DURATION = 220; public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR = new PathInterpolator(0f, 0, 0.7f, 1f); + public static final float SCRIM_BEHIND_ALPHA_KEYGUARD = 0.45f; private static final float SCRIM_BEHIND_ALPHA = 0.62f; - 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; @@ -255,7 +255,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, } } - private void setScrimBehindColor(float alpha) { + public void setScrimBehindColor(float alpha) { setScrimColor(mScrimBehind, alpha); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SettingsButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SettingsButton.java index 18db5b8..3e0aa18 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SettingsButton.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SettingsButton.java @@ -17,18 +17,18 @@ package com.android.systemui.statusbar.phone; import android.animation.Animator; import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; -import android.graphics.drawable.RippleDrawable; -import android.os.Handler; -import android.os.Message; import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; import com.android.keyguard.AlphaOptimizedImageButton; public class SettingsButton extends AlphaOptimizedImageButton { @@ -56,6 +56,10 @@ public class SettingsButton extends AlphaOptimizedImageButton { return mUpToSpeed; } + public void consumeClick() { + mUpToSpeed = false; + } + @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { @@ -78,6 +82,7 @@ public class SettingsButton extends AlphaOptimizedImageButton { if ((x < -mSlop) || (y < -mSlop) || (x > getWidth() + mSlop) || (y > getHeight() + mSlop)) { cancelLongClick(); + startExitAnimation(); } break; } @@ -99,32 +104,11 @@ public class SettingsButton extends AlphaOptimizedImageButton { } private void startExitAnimation() { + cancelAnimation(); animate() - .translationX(((View) getParent().getParent()).getWidth() - getX()) - .alpha(0) - .setDuration(RUN_DURATION) - .setInterpolator(AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.accelerate_cubic)) - .setListener(new AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - setAlpha(1f); - setTranslationX(0); - cancelLongClick(); - } - - @Override - public void onAnimationCancel(Animator animation) { - } - }) + .rotation(0) + .setDuration(ACCEL_LENGTH) + .setInterpolator(new DecelerateInterpolator(4)) .start(); } @@ -164,6 +148,8 @@ public class SettingsButton extends AlphaOptimizedImageButton { mAnimator.setDuration(FULL_SPEED_LENGTH); mAnimator.setRepeatCount(Animation.INFINITE); mAnimator.start(); + + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } private final Runnable mLongPressCallback = new Runnable() { 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 971978d..f9b1f38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java @@ -16,18 +16,31 @@ package com.android.systemui.statusbar.phone; +import android.app.ActivityManager; import android.app.AlarmManager; import android.app.PendingIntent; +import android.content.ContentUris; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; +import android.database.ContentObserver; import android.graphics.Outline; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; +import android.os.Handler; +import android.os.IRemoteCallback; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.AlarmClock; +import android.provider.CalendarContract; +import android.provider.Settings; +import android.net.Uri; import android.util.AttributeSet; +import android.util.Log; import android.util.MathUtils; import android.util.TypedValue; import android.view.View; @@ -41,30 +54,42 @@ import android.widget.TextView; import android.widget.Toast; import com.android.keyguard.KeyguardStatusView; +import com.android.systemui.BatteryLevelTextView; import com.android.systemui.BatteryMeterView; +import com.android.systemui.DockBatteryMeterView; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.cm.UserContentObserver; +import com.android.systemui.qs.QSDragPanel; 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.DockBatteryController; import com.android.systemui.statusbar.policy.NetworkControllerImpl.EmergencyListener; import com.android.systemui.statusbar.policy.NextAlarmController; import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.statusbar.policy.WeatherController; +import com.android.systemui.statusbar.policy.WeatherControllerImpl; import com.android.systemui.tuner.TunerService; import java.text.NumberFormat; +import cyanogenmod.app.StatusBarPanelCustomTile; +import cyanogenmod.providers.CMSettings; +import cyanogenmod.weather.util.WeatherUtils; +import org.cyanogenmod.internal.logging.CMMetricsLogger; + /** * The view to manage the header area in the expanded status bar. */ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickListener, - BatteryController.BatteryStateChangeCallback, NextAlarmController.NextAlarmChangeCallback, - EmergencyListener { + NextAlarmController.NextAlarmChangeCallback, WeatherController.Callback, EmergencyListener { private boolean mExpanded; private boolean mListening; private ViewGroup mSystemIconsContainer; + private ViewGroup mWeatherContainer; private View mSystemIconsSuperContainer; private View mDateGroup; private View mClock; @@ -83,8 +108,11 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL private Switch mQsDetailHeaderSwitch; private ImageView mQsDetailHeaderProgress; private TextView mEmergencyCallsOnly; - private TextView mBatteryLevel; + private BatteryLevelTextView mBatteryLevel; + private BatteryLevelTextView mDockBatteryLevel; private TextView mAlarmStatus; + private TextView mWeatherLine1, mWeatherLine2; + private TextView mEditTileDoneText; private boolean mShowEmergencyCallsOnly; private boolean mAlarmShowing; @@ -112,9 +140,9 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL private float mAvatarCollapsedScaleFactor; private ActivityStarter mActivityStarter; - private BatteryController mBatteryController; private NextAlarmController mNextAlarmController; - private QSPanel mQSPanel; + private WeatherController mWeatherController; + private QSDragPanel mQSPanel; private final Rect mClipBounds = new Rect(); @@ -127,6 +155,14 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL private float mCurrentT; private boolean mShowingDetail; private boolean mDetailTransitioning; + private SettingsObserver mSettingsObserver; + private boolean mShowWeather; + private boolean mShowBatteryTextExpanded; + + private QSTile.DetailAdapter mEditingDetailAdapter; + private boolean mEditing; + + private UserInfoController mUserInfoController; public StatusBarHeaderView(Context context, AttributeSet attrs) { super(context, attrs); @@ -139,7 +175,9 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL mSystemIconsContainer = (ViewGroup) findViewById(R.id.system_icons_container); mSystemIconsSuperContainer.setOnClickListener(this); mDateGroup = findViewById(R.id.date_group); + mDateGroup.setOnClickListener(this); mClock = findViewById(R.id.clock); + mClock.setOnClickListener(this); mTime = (TextView) findViewById(R.id.time_view); mAmPm = (TextView) findViewById(R.id.am_pm_view); mMultiUserSwitch = (MultiUserSwitch) findViewById(R.id.multi_user_switch); @@ -155,11 +193,18 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL mQsDetailHeaderSwitch = (Switch) mQsDetailHeader.findViewById(android.R.id.toggle); mQsDetailHeaderProgress = (ImageView) findViewById(R.id.qs_detail_header_progress); mEmergencyCallsOnly = (TextView) findViewById(R.id.header_emergency_calls_only); - mBatteryLevel = (TextView) findViewById(R.id.battery_level); + mBatteryLevel = (BatteryLevelTextView) findViewById(R.id.battery_level_text); + mDockBatteryLevel = (BatteryLevelTextView) findViewById(R.id.dock_battery_level_text); mAlarmStatus = (TextView) findViewById(R.id.alarm_status); mAlarmStatus.setOnClickListener(this); mSignalCluster = findViewById(R.id.signal_cluster); mSystemIcons = (LinearLayout) findViewById(R.id.system_icons); + mWeatherContainer = (LinearLayout) findViewById(R.id.weather_container); + mWeatherContainer.setOnClickListener(this); + mWeatherLine1 = (TextView) findViewById(R.id.weather_line_1); + mWeatherLine2 = (TextView) findViewById(R.id.weather_line_2); + mEditTileDoneText = (TextView) findViewById(R.id.done); + mSettingsObserver = new SettingsObserver(new Handler()); loadDimens(); updateVisibilities(); updateClockScale(); @@ -188,9 +233,18 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL // RenderThread is doing more harm than good when touching the header (to expand quick // settings), so disable it for this view - ((RippleDrawable) getBackground()).setForceSoftware(true); - ((RippleDrawable) mSettingsButton.getBackground()).setForceSoftware(true); - ((RippleDrawable) mSystemIconsSuperContainer.getBackground()).setForceSoftware(true); + Drawable d = getBackground(); + if (d instanceof RippleDrawable) { + ((RippleDrawable) d).setForceSoftware(true); + } + d = mSettingsButton.getBackground(); + if (d instanceof RippleDrawable) { + ((RippleDrawable) d).setForceSoftware(true); + } + d = mSystemIconsSuperContainer.getBackground(); + if (d instanceof RippleDrawable) { + ((RippleDrawable) d).setForceSoftware(true); + } } @Override @@ -211,7 +265,6 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - FontSizeUtils.updateFontSize(mBatteryLevel, R.dimen.battery_level_text_size); FontSizeUtils.updateFontSize(mEmergencyCallsOnly, R.dimen.qs_emergency_calls_only_text_size); FontSizeUtils.updateFontSize(mDateCollapsed, R.dimen.qs_date_collapsed_size); @@ -228,10 +281,23 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL mClockExpandedSize = getResources().getDimensionPixelSize(R.dimen.qs_time_expanded_size); mClockCollapsedScaleFactor = (float) mClockCollapsedSize / (float) mClockExpandedSize; + if (mEditTileDoneText != null) { + mEditTileDoneText.setText(R.string.quick_settings_done); + } + updateClockScale(); updateClockCollapsedMargin(); } + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mUserInfoController != null) { + mUserInfoController.removeListener(mUserInfoChangedListener); + } + setListening(false); + } + private void updateClockCollapsedMargin() { Resources res = getResources(); int padding = res.getDimensionPixelSize(R.dimen.clock_collapsed_bottom_margin); @@ -274,17 +340,42 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL public void setActivityStarter(ActivityStarter activityStarter) { mActivityStarter = activityStarter; + if (mMultiUserSwitch != null) { + mMultiUserSwitch.setActivityStarter(activityStarter); + } } public void setBatteryController(BatteryController batteryController) { - mBatteryController = batteryController; - ((BatteryMeterView) findViewById(R.id.battery)).setBatteryController(batteryController); + BatteryMeterView v = ((BatteryMeterView) findViewById(R.id.battery)); + v.setBatteryStateRegistar(batteryController); + v.setBatteryController(batteryController); + mBatteryLevel.setBatteryStateRegistar(batteryController); + } + + public void setDockBatteryController(DockBatteryController dockBatteryController) { + DockBatteryMeterView v = ((DockBatteryMeterView) findViewById(R.id.dock_battery)); + if (dockBatteryController != null) { + v.setBatteryStateRegistar(dockBatteryController); + mDockBatteryLevel.setBatteryStateRegistar(dockBatteryController); + } else { + if (v != null) { + removeView(v); + } + if (mDockBatteryLevel != null) { + removeView(mDockBatteryLevel); + mDockBatteryLevel = null; + } + } } public void setNextAlarmController(NextAlarmController nextAlarmController) { mNextAlarmController = nextAlarmController; } + public void setWeatherController(WeatherController weatherController) { + mWeatherController = weatherController; + } + public int getCollapsedHeight() { return mCollapsedHeight; } @@ -305,6 +396,9 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL boolean changed = expanded != mExpanded; mExpanded = expanded; if (changed) { + if (mShowingDetail && !expanded) { + mQsPanelCallback.onShowingDetail(null); + } updateEverything(); } } @@ -335,14 +429,18 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL mDateExpanded.setVisibility(mExpanded && mAlarmShowing ? View.INVISIBLE : View.VISIBLE); mAlarmStatus.setVisibility(mExpanded && mAlarmShowing ? View.VISIBLE : View.INVISIBLE); mSettingsContainer.setVisibility(mExpanded ? View.VISIBLE : View.INVISIBLE); - mQsDetailHeader.setVisibility(mExpanded && mShowingDetail? View.VISIBLE : View.INVISIBLE); + mWeatherContainer.setVisibility(mExpanded && mShowWeather ? View.VISIBLE : View.GONE); + mQsDetailHeader.setVisibility(mExpanded && mShowingDetail ? View.VISIBLE : View.INVISIBLE); if (mSignalCluster != null) { updateSignalClusterDetachment(); } mEmergencyCallsOnly.setVisibility(mExpanded && mShowEmergencyCallsOnly ? VISIBLE : GONE); - mBatteryLevel.setVisibility(mExpanded ? View.VISIBLE : View.GONE); - mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility( - TunerService.isTunerEnabled(mContext) ? View.VISIBLE : View.INVISIBLE); + mBatteryLevel.setForceShown(mExpanded && mShowBatteryTextExpanded); + mBatteryLevel.setVisibility(View.VISIBLE); + if (mDockBatteryLevel != null) { + mDockBatteryLevel.setForceShown(mExpanded && mShowBatteryTextExpanded); + mDockBatteryLevel.setVisibility(View.VISIBLE); + } } private void updateSignalClusterDetachment() { @@ -375,11 +473,13 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL private void updateListeners() { if (mListening) { - mBatteryController.addStateChangedCallback(this); + mSettingsObserver.observe(); mNextAlarmController.addStateChangedCallback(this); + mWeatherController.addCallback(this); } else { - mBatteryController.removeStateChangedCallback(this); mNextAlarmController.removeStateChangedCallback(this); + mWeatherController.removeCallback(this); + mSettingsObserver.unobserve(); } } @@ -408,17 +508,6 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL } @Override - public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { - String percentage = NumberFormat.getPercentInstance().format((double) level / 100.0); - mBatteryLevel.setText(percentage); - } - - @Override - public void onPowerSaveChanged() { - // could not care less - } - - @Override public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { mNextAlarm = nextAlarm; if (nextAlarm != null) { @@ -429,6 +518,19 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL requestCaptureValues(); } + @Override + public void onWeatherChanged(WeatherController.WeatherInfo info) { + if (Double.isNaN(info.temp) || info.condition == null) { + mWeatherLine1.setText(null); + } else { + mWeatherLine1.setText(mContext.getString( + R.string.status_bar_expanded_header_weather_format, + WeatherUtils.formatTemperature(info.temp, info.tempUnit), + info.condition)); + } + mWeatherLine2.setText(info.city); + } + private void updateClickTargets() { mMultiUserSwitch.setClickable(mExpanded); mMultiUserSwitch.setFocusable(mExpanded); @@ -496,33 +598,31 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL invalidateOutline(); } + private UserInfoController.OnUserInfoChangedListener mUserInfoChangedListener = + new UserInfoController.OnUserInfoChangedListener() { + @Override + public void onUserInfoChanged(String name, Drawable picture) { + mMultiUserAvatar.setImageDrawable(picture); + } + }; + public void setUserInfoController(UserInfoController userInfoController) { - userInfoController.addListener(new UserInfoController.OnUserInfoChangedListener() { - @Override - public void onUserInfoChanged(String name, Drawable picture) { - mMultiUserAvatar.setImageDrawable(picture); - } - }); + mUserInfoController = userInfoController; + userInfoController.addListener(mUserInfoChangedListener); + if (mMultiUserSwitch != null) { + mMultiUserSwitch.setUserInfoController(mUserInfoController); + } } @Override public void onClick(View v) { if (v == mSettingsButton) { if (mSettingsButton.isTunerClick()) { - if (TunerService.isTunerEnabled(mContext)) { - TunerService.showResetRequest(mContext, new Runnable() { - @Override - public void run() { - // Relaunch settings so that the tuner disappears. - startSettingsActivity(); - } - }); - } else { - Toast.makeText(getContext(), R.string.tuner_toast, Toast.LENGTH_LONG).show(); - TunerService.setTunerEnabled(mContext, true); - } + mSettingsButton.consumeClick(); + mQSPanel.getHost().setEditing(!mQSPanel.getHost().isEditing()); + } else { + startSettingsActivity(); } - startSettingsActivity(); } else if (v == mSystemIconsSuperContainer) { startBatteryActivity(); } else if (v == mAlarmStatus && mNextAlarm != null) { @@ -530,6 +630,12 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL if (showIntent != null) { mActivityStarter.startPendingIntentDismissingKeyguard(showIntent); } + } else if (v == mClock) { + startClockActivity(); + } else if (v == mDateGroup) { + startDateActivity(); + } else if (v == mWeatherContainer) { + startForecastActivity(); } } @@ -543,7 +649,27 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL true /* dismissShade */); } - public void setQSPanel(QSPanel qsp) { + private void startClockActivity() { + mActivityStarter.startActivity(new Intent(AlarmClock.ACTION_SHOW_ALARMS), + true /* dismissShade */); + } + + private void startDateActivity() { + Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); + builder.appendPath("time"); + ContentUris.appendId(builder, System.currentTimeMillis()); + Intent intent = new Intent(Intent.ACTION_VIEW).setData(builder.build()); + mActivityStarter.startActivity(intent, true /* dismissShade */); + } + + private void startForecastActivity() { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setComponent(WeatherControllerImpl.COMPONENT_WEATHER_FORECAST); + mActivityStarter.startActivity(intent, true /* dismissShade */); + } + + public void setQSPanel(QSDragPanel qsp) { mQSPanel = qsp; if (mQSPanel != null) { mQSPanel.setCallback(mQsPanelCallback); @@ -585,6 +711,7 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL target.avatarScale = mMultiUserAvatar.getScaleX(); target.avatarX = mMultiUserSwitch.getLeft() + mMultiUserAvatar.getLeft(); target.avatarY = mMultiUserSwitch.getTop() + mMultiUserAvatar.getTop(); + target.weatherY = mClock.getBottom() - mWeatherLine1.getHeight(); if (getLayoutDirection() == LAYOUT_DIRECTION_LTR) { target.batteryX = mSystemIconsSuperContainer.getLeft() + mSystemIconsContainer.getRight(); @@ -623,6 +750,7 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL mTime.setScaleY(values.timeScale); mClock.setY(values.clockY - mClock.getHeight()); mDateGroup.setY(values.dateY); + mWeatherContainer.setY(values.weatherY); mAlarmStatus.setY(values.dateY - mAlarmStatus.getPaddingTop()); mMultiUserAvatar.setScaleX(values.avatarScale); mMultiUserAvatar.setScaleY(values.avatarScale); @@ -662,7 +790,12 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL applyAlpha(mDateCollapsed, values.dateCollapsedAlpha); applyAlpha(mDateExpanded, values.dateExpandedAlpha); applyAlpha(mBatteryLevel, values.batteryLevelAlpha); + if (mDockBatteryLevel != null) { + applyAlpha(mDockBatteryLevel, values.batteryLevelAlpha); + } applyAlpha(mSettingsContainer, values.settingsAlpha); + applyAlpha(mWeatherLine1, values.settingsAlpha); + applyAlpha(mWeatherLine2, values.settingsAlpha); applyAlpha(mSignalCluster, values.signalClusterAlpha); if (!mExpanded) { mTime.setScaleX(1f); @@ -671,6 +804,50 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL updateAmPmTranslation(); } + public void setEditing(boolean editing) { + mEditing = editing; + if (editing && mEditingDetailAdapter == null) { + mEditingDetailAdapter = new QSTile.DetailAdapter() { + @Override + public int getTitle() { + return R.string.quick_settings_edit_label; + } + + @Override + public Boolean getToggleState() { + return null; + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + return null; + } + + @Override + public Intent getSettingsIntent() { + return null; + } + + @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override + public void setToggleState(boolean state) { + + } + + @Override + public int getMetricsCategory() { + return CMMetricsLogger.DONT_LOG; + } + }; + } + mQsPanelCallback.onShowingDetail(mEditing ? mEditingDetailAdapter : null); + updateEverything(); + } + /** * Captures all layout values (position, visibility) for a certain state. This is used for * animations. @@ -690,10 +867,12 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL float batteryX; float batteryY; float batteryLevelAlpha; + float batteryLevelExpandedAlpha; float settingsAlpha; float settingsTranslation; float signalClusterAlpha; float settingsRotation; + float weatherY; public void interpoloate(LayoutValues v1, LayoutValues v2, float t) { timeScale = v1.timeScale * (1 - t) + v2.timeScale * t; @@ -705,6 +884,7 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL batteryX = v1.batteryX * (1 - t) + v2.batteryX * t; batteryY = v1.batteryY * (1 - t) + v2.batteryY * t; settingsTranslation = v1.settingsTranslation * (1 - t) + v2.settingsTranslation * t; + weatherY = v1.weatherY * (1 - t) + v2.weatherY * t; float t1 = Math.max(0, t - 0.5f) * 2; settingsRotation = v1.settingsRotation * (1 - t1) + v2.settingsRotation * t1; @@ -720,6 +900,8 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL dateExpandedAlpha = v1.dateExpandedAlpha * (1 - t3) + v2.dateExpandedAlpha * t3; dateCollapsedAlpha = v1.dateCollapsedAlpha * (1 - t3) + v2.dateCollapsedAlpha * t3; alarmStatusAlpha = v1.alarmStatusAlpha * (1 - t3) + v2.alarmStatusAlpha * t3; + batteryLevelExpandedAlpha = + v1.batteryLevelExpandedAlpha * (1 - t3) + v2.batteryLevelExpandedAlpha * t3; } } @@ -742,7 +924,7 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL post(new Runnable() { @Override public void run() { - handleShowingDetail(detail); + handleShowingDetail(mEditing && detail == null ? mEditingDetailAdapter : detail); } }); } @@ -778,18 +960,33 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL final boolean showingDetail = detail != null; transition(mClock, !showingDetail); transition(mDateGroup, !showingDetail); + if (mShowWeather) { + transition(mWeatherContainer, !showingDetail); + } if (mAlarmShowing) { - transition(mAlarmStatus, !showingDetail); + transition(mAlarmStatus, !showingDetail && !mDetailTransitioning); } transition(mQsDetailHeader, showingDetail); mShowingDetail = showingDetail; if (showingDetail) { mQsDetailHeaderTitle.setText(detail.getTitle()); final Boolean toggleState = detail.getToggleState(); - if (toggleState == null) { + if (detail.getTitle() == R.string.quick_settings_edit_label) { + mEditTileDoneText.setVisibility(View.VISIBLE); mQsDetailHeaderSwitch.setVisibility(INVISIBLE); + mQsDetailHeader.setClickable(true); + mQsDetailHeader.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mQSPanel.getHost().setEditing(false); + } + }); + } else if (toggleState == null) { + mQsDetailHeaderSwitch.setVisibility(INVISIBLE); + mEditTileDoneText.setVisibility(View.GONE); mQsDetailHeader.setClickable(false); } else { + mEditTileDoneText.setVisibility(View.GONE); mQsDetailHeaderSwitch.setVisibility(VISIBLE); mQsDetailHeaderSwitch.setChecked(toggleState); mQsDetailHeader.setClickable(true); @@ -829,4 +1026,58 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL .start(); } }; + + class SettingsObserver extends UserContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + @Override + protected void observe() { + super.observe(); + + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.STATUS_BAR_SHOW_WEATHER), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.STATUS_BAR_BATTERY_STYLE), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.STATUS_BAR_SHOW_BATTERY_PERCENT), false, this, UserHandle.USER_ALL); + update(); + } + + @Override + protected void unobserve() { + super.unobserve(); + + ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(this); + } + + @Override + public void update() { + + ContentResolver resolver = mContext.getContentResolver(); + int currentUserId = ActivityManager.getCurrentUser(); + int batteryStyle = CMSettings.System.getIntForUser(resolver, + CMSettings.System.STATUS_BAR_BATTERY_STYLE, 0, currentUserId); + boolean showExpandedBatteryPercentage = CMSettings.System.getIntForUser(resolver, + CMSettings.System.STATUS_BAR_SHOW_BATTERY_PERCENT, 0, currentUserId) == 0; + + switch (batteryStyle) { + case 4: //BATTERY_METER_GONE + case 6: //BATTERY_METER_TEXT + showExpandedBatteryPercentage = false; + break; + default: + break; + } + + mShowBatteryTextExpanded = showExpandedBatteryPercentage; + mShowWeather = CMSettings.System.getInt( + resolver, CMSettings.System.STATUS_BAR_SHOW_WEATHER, 1) == 1; + updateVisibilities(); + requestCaptureValues(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index 5de1c13..a7fa27d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -24,6 +24,7 @@ import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; +import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; import android.view.View; @@ -36,12 +37,14 @@ import android.widget.TextView; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.NotificationColorUtil; +import com.android.systemui.BatteryLevelTextView; 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.statusbar.policy.Clock; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; @@ -73,8 +76,10 @@ public class StatusBarIconController implements Tunable { private IconMerger mNotificationIcons; private View mNotificationIconArea; private ImageView mMoreIcon; + private BatteryLevelTextView mBatteryLevelTextView; private BatteryMeterView mBatteryMeterView; - private TextView mClock; + private ClockController mClockController; + private View mCenterClockLayout; private int mIconSize; private int mIconHPadding; @@ -117,8 +122,9 @@ public class StatusBarIconController implements Tunable { mMoreIcon = (ImageView) statusBar.findViewById(R.id.moreIcon); mNotificationIcons.setOverflowIndicator(mMoreIcon); mStatusIconsKeyguard = (LinearLayout) keyguardStatusBar.findViewById(R.id.statusIcons); + mBatteryLevelTextView = + (BatteryLevelTextView) statusBar.findViewById(R.id.battery_level_text); 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, @@ -126,6 +132,8 @@ public class StatusBarIconController implements Tunable { 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(); + mClockController = new ClockController(statusBar, mNotificationIcons, mHandler); + mCenterClockLayout = statusBar.findViewById(R.id.center_clock_layout); updateResources(); TunerService.get(mContext).addTunable(this, ICON_BLACKLIST); @@ -158,7 +166,7 @@ public class StatusBarIconController implements Tunable { 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); + mClockController.updateFontSize(); } public void addSystemIcon(String slot, int index, int viewIndex, StatusBarIcon icon) { @@ -247,22 +255,26 @@ public class StatusBarIconController implements Tunable { public void hideSystemIconArea(boolean animate) { animateHide(mSystemIconArea, animate); + animateHide(mCenterClockLayout, animate); } public void showSystemIconArea(boolean animate) { animateShow(mSystemIconArea, animate); + animateShow(mCenterClockLayout, animate); } public void hideNotificationIconArea(boolean animate) { animateHide(mNotificationIconArea, animate); + animateHide(mCenterClockLayout, animate); } public void showNotificationIconArea(boolean animate) { animateShow(mNotificationIconArea, animate); + animateShow(mCenterClockLayout, animate); } public void setClockVisibility(boolean visible) { - mClock.setVisibility(visible ? View.VISIBLE : View.GONE); + mClockController.setVisibility(visible); } public void dump(PrintWriter pw) { @@ -392,8 +404,9 @@ public class StatusBarIconController implements Tunable { } mSignalCluster.setIconTint(mIconTint, mDarkIntensity); mMoreIcon.setImageTintList(ColorStateList.valueOf(mIconTint)); + mBatteryLevelTextView.setTextColor(mIconTint); mBatteryMeterView.setDarkIntensity(mDarkIntensity); - mClock.setTextColor(mIconTint); + mClockController.setTextColor(mIconTint); applyNotificationIconsTint(); } @@ -462,4 +475,32 @@ public class StatusBarIconController implements Tunable { } return ret; } + + public void refreshAllStatusBarIcons() { + refreshAllIconsForLayout(mStatusIcons); + refreshAllIconsForLayout(mStatusIconsKeyguard); + refreshAllIconsForLayout(mNotificationIcons); + } + + public LinearLayout getStatusIcons() { + return mStatusIcons; + } + + public void cleanup() { + TunerService.get(mContext).removeTunable(this); + mClockController.cleanup(); + if (mSignalCluster != null) { + mSignalCluster.setSecurityController(null); + } + } + + 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(); + } + } + } } 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 05f6e57..96cf093 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -18,17 +18,21 @@ package com.android.systemui.statusbar.phone; import android.content.ComponentCallbacks2; import android.content.Context; +import android.graphics.PixelFormat; import android.os.Bundle; import android.os.SystemClock; import android.os.Trace; +import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewRootImpl; +import android.view.WindowManager; import android.view.WindowManagerGlobal; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.R; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.statusbar.CommandQueue; @@ -94,9 +98,10 @@ public class StatusBarKeyguardViewManager { mContainer = container; mStatusBarWindowManager = statusBarWindowManager; mScrimController = scrimController; + if (mBouncer != null) mBouncer.removeView(); mFingerprintUnlockController = fingerprintUnlockController; mBouncer = new KeyguardBouncer(mContext, mViewMediatorCallback, mLockPatternUtils, - mStatusBarWindowManager, container); + mStatusBarWindowManager, container, mPhoneStatusBar); } /** @@ -107,23 +112,38 @@ public class StatusBarKeyguardViewManager { mShowing = true; mStatusBarWindowManager.setKeyguardShowing(true); mScrimController.abortKeyguardFadingOut(); - reset(); + reset(false); } /** * Shows the notification keyguard or the bouncer depending on * {@link KeyguardBouncer#needsFullscreenBouncer()}. */ - private void showBouncerOrKeyguard() { - if (mBouncer.needsFullscreenBouncer()) { - - // The keyguard might be showing (already). So we need to hide it. - mPhoneStatusBar.hideKeyguard(); - mBouncer.show(true /* resetSecuritySelection */); - } else { - mPhoneStatusBar.showKeyguard(); - mBouncer.hide(false /* destroyView */); - mBouncer.prepare(); + private void showBouncerOrKeyguard(boolean isBackPressed) { + switch (mBouncer.needsFullscreenBouncer()) { + case KeyguardBouncer.UNLOCK_SEQUENCE_FORCE_BOUNCER: + // SIM PIN/PUK + // The keyguard might be showing (already). So we need to hide it. + mPhoneStatusBar.hideKeyguard(); + mBouncer.show(true /* resetSecuritySelection */); + break; + case KeyguardBouncer.UNLOCK_SEQUENCE_BOUNCER_FIRST: + // Pattern/PIN/Password with "Directly pass to security view" enabled + if (isBackPressed) { + mPhoneStatusBar.showKeyguard(); + mBouncer.hide(false /* destroyView */); + mBouncer.prepare(); + } else { + // The keyguard might be showing (already). So we need to hide it. + mPhoneStatusBar.hideKeyguard(); + mBouncer.show(true /* resetSecuritySelection */); + } + break; + case KeyguardBouncer.UNLOCK_SEQUENCE_DEFAULT: + mPhoneStatusBar.showKeyguard(); + mBouncer.hide(false /* destroyView */); + mBouncer.prepare(); + break; } } @@ -150,14 +170,14 @@ public class StatusBarKeyguardViewManager { /** * Reset the state of the view. */ - public void reset() { + public void reset(boolean isBackPressed) { if (mShowing) { if (mOccluded) { mPhoneStatusBar.hideKeyguard(); mPhoneStatusBar.stopWaitingForKeyguardExit(); mBouncer.hide(false /* destroyView */); } else { - showBouncerOrKeyguard(); + showBouncerOrKeyguard(isBackPressed); } KeyguardUpdateMonitor.getInstance(mContext).sendKeyguardReset(); updateStates(); @@ -197,10 +217,12 @@ public class StatusBarKeyguardViewManager { updateStates(); } mPhoneStatusBar.onScreenTurnedOn(); + mStatusBarWindowManager.onKeyguardChanged(); } public void onScreenTurnedOff() { mScreenTurnedOn = false; + mPhoneStatusBar.onScreenTurnedOff(); } public void notifyDeviceWakeUpRequested() { @@ -224,7 +246,7 @@ public class StatusBarKeyguardViewManager { @Override public void run() { mStatusBarWindowManager.setKeyguardOccluded(mOccluded); - reset(); + reset(false); } }); return; @@ -232,7 +254,11 @@ public class StatusBarKeyguardViewManager { } mOccluded = occluded; mStatusBarWindowManager.setKeyguardOccluded(occluded); - reset(); + mPhoneStatusBar.getVisualizer().setOccluded(occluded); + if (!occluded) { + mPhoneStatusBar.mKeyguardBottomArea.setVisibility(View.GONE); + } + reset(false); } public boolean isOccluded() { @@ -359,16 +385,14 @@ public class StatusBarKeyguardViewManager { private void executeAfterKeyguardGoneAction() { if (mAfterKeyguardGoneAction != null) { + dismiss(); mAfterKeyguardGoneAction.onDismiss(); mAfterKeyguardGoneAction = null; } } - /** - * Dismisses the keyguard by going to the next screen or making it gone. - */ public void dismiss() { - if (mDeviceInteractive || mDeviceWillWakeUp) { + if ((mDeviceInteractive || mDeviceWillWakeUp)) { showBouncer(); } } @@ -395,7 +419,7 @@ public class StatusBarKeyguardViewManager { public boolean onBackPressed() { if (mBouncer.isShowing()) { mPhoneStatusBar.endAffordanceLaunch(); - reset(); + reset(true); return true; } return false; @@ -428,7 +452,8 @@ public class StatusBarKeyguardViewManager { boolean showing = mShowing; boolean occluded = mOccluded; boolean bouncerShowing = mBouncer.isShowing(); - boolean bouncerDismissible = !mBouncer.isFullscreenBouncer(); + boolean bouncerDismissible = (mBouncer.isFullscreenBouncer() != + KeyguardBouncer.UNLOCK_SEQUENCE_FORCE_BOUNCER); if ((bouncerDismissible || !showing) != (mLastBouncerDismissible || !mLastShowing) || mFirstUpdate) { @@ -503,7 +528,8 @@ public class StatusBarKeyguardViewManager { } public boolean shouldDisableWindowAnimationsForUnlock() { - return mPhoneStatusBar.isInLaunchTransition(); + return mPhoneStatusBar.isInLaunchTransition() || + mPhoneStatusBar.isShowingLiveLockScreenView(); } public boolean isGoingToNotificationShade() { @@ -525,6 +551,10 @@ public class StatusBarKeyguardViewManager { public void animateCollapsePanels(float speedUpFactor) { mPhoneStatusBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */, false /* delayed */, speedUpFactor); + if (mStatusBarWindowManager.keyguardExternalViewHasFocus()) { + mStatusBarWindowManager.setKeyguardExternalViewFocus(false); + dismiss(); + } } /** @@ -542,4 +572,12 @@ public class StatusBarKeyguardViewManager { public ViewRootImpl getViewRootImpl() { return mPhoneStatusBar.getStatusBarView().getViewRootImpl(); } + + public boolean isKeyguardShowingMedia() { + return mPhoneStatusBar.isKeyguardShowingMedia(); + } + + public void setKeyguardExternalViewFocus(boolean hasFocus) { + mStatusBarWindowManager.setKeyguardExternalViewFocus(hasFocus); + } } 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 ccfa0dd..f0d7828 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java @@ -18,19 +18,28 @@ package com.android.systemui.statusbar.phone; import android.content.Context; import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.content.res.Resources; +import android.database.ContentObserver; +import android.graphics.Point; import android.graphics.PixelFormat; +import android.os.Handler; import android.os.SystemProperties; +import android.provider.Settings; import android.view.Gravity; +import android.view.Display; +import android.view.SurfaceSession; import android.view.View; import android.view.ViewGroup; -import android.view.Window; 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 com.android.systemui.statusbar.policy.KeyguardMonitor; +import com.android.systemui.statusbar.policy.LiveLockScreenController; +import cyanogenmod.providers.CMSettings; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -39,7 +48,7 @@ import java.lang.reflect.Field; /** * Encapsulates all logic for the status bar window state management. */ -public class StatusBarWindowManager { +public class StatusBarWindowManager implements KeyguardMonitor.Callback { private final Context mContext; private final WindowManager mWindowManager; @@ -47,22 +56,49 @@ public class StatusBarWindowManager { private WindowManager.LayoutParams mLp; private WindowManager.LayoutParams mLpChanged; private int mBarHeight; - private final boolean mKeyguardScreenRotation; + private boolean mKeyguardScreenRotation; private final float mScreenBrightnessDoze; + private final boolean mBlurSupported; + + private boolean mKeyguardBlurEnabled; + private boolean mShowingMedia; + private BlurLayer mKeyguardBlur; + private final SurfaceSession mFxSession; + + private final KeyguardMonitor mKeyguardMonitor; + private int mCurrentOrientation; + + private static final int TYPE_LAYER_MULTIPLIER = 10000; // refer to WindowManagerService.TYPE_LAYER_MULTIPLIER + private static final int TYPE_LAYER_OFFSET = 1000; // refer to WindowManagerService.TYPE_LAYER_OFFSET + + private static final int STATUS_BAR_LAYER = 16 * TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET; + private final State mCurrentState = new State(); + private LiveLockScreenController mLiveLockScreenController; - public StatusBarWindowManager(Context context) { + public StatusBarWindowManager(Context context, KeyguardMonitor kgm) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mKeyguardScreenRotation = shouldEnableKeyguardScreenRotation(); mScreenBrightnessDoze = mContext.getResources().getInteger( com.android.internal.R.integer.config_screenBrightnessDoze) / 255f; + mBlurSupported = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_ui_blur_enabled); + + mKeyguardMonitor = kgm; + mKeyguardMonitor.addCallback(this); + mFxSession = new SurfaceSession(); } private boolean shouldEnableKeyguardScreenRotation() { Resources res = mContext.getResources(); + boolean enableAccelerometerRotation = Settings.System.getInt(mContext.getContentResolver(), + Settings.System.ACCELEROMETER_ROTATION, 1) != 0; + boolean enableLockScreenRotation = CMSettings.System.getInt(mContext.getContentResolver(), + CMSettings.System.LOCKSCREEN_ROTATION, 0) != 0; return SystemProperties.getBoolean("lockscreen.rot_override", false) - || res.getBoolean(R.bool.config_enableLockScreenRotation); + || (res.getBoolean(R.bool.config_enableLockScreenRotation) + && (enableLockScreenRotation && enableAccelerometerRotation)); } /** @@ -72,7 +108,6 @@ public class StatusBarWindowManager { * @param barHeight The height of the status bar in collapsed state. */ public void add(View statusBarView, int barHeight) { - // Now that the status bar window encompasses the sliding panel and its // translucent backdrop, the entire thing is made TRANSLUCENT and is // hardware-accelerated. @@ -96,15 +131,37 @@ public class StatusBarWindowManager { mWindowManager.addView(mStatusBarView, mLp); mLpChanged = new WindowManager.LayoutParams(); mLpChanged.copyFrom(mLp); + + mKeyguardBlurEnabled = mBlurSupported ? + CMSettings.Secure.getInt(mContext.getContentResolver(), + CMSettings.Secure.LOCK_SCREEN_BLUR_ENABLED, 1) == 1 : false; + if (mBlurSupported) { + Display display = mWindowManager.getDefaultDisplay(); + Point xy = new Point(); + display.getRealSize(xy); + mCurrentOrientation = mContext.getResources().getConfiguration().orientation; + mKeyguardBlur = new BlurLayer(mFxSession, xy.x, xy.y, "KeyGuard"); + if (mKeyguardBlur != null) { + mKeyguardBlur.setLayer(STATUS_BAR_LAYER - 2); + } + } + + SettingsObserver observer = new SettingsObserver(new Handler()); + observer.observe(mContext); } private void applyKeyguardFlags(State state) { if (state.keyguardShowing) { - mLpChanged.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; mLpChanged.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; + if (!mKeyguardBlurEnabled || mShowingMedia) { + mLpChanged.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + } } else { mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; mLpChanged.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; + if (mKeyguardBlurEnabled && mKeyguardBlur != null) { + mKeyguardBlur.hide(); + } } } @@ -217,13 +274,33 @@ public class StatusBarWindowManager { } } + private void applyKeyguardBlurShow(){ + boolean isblur = false; + if (mCurrentState.keyguardShowing && mKeyguardBlurEnabled + && !mCurrentState.keyguardOccluded + && !mShowingMedia) { + isblur = true; + } + if (mKeyguardBlur != null) { + if (isblur) { + mKeyguardBlur.show(); + } else { + mKeyguardBlur.hide(); + } + } + } + public void setKeyguardShowing(boolean showing) { mCurrentState.keyguardShowing = showing; apply(mCurrentState); } public void setKeyguardOccluded(boolean occluded) { + final boolean oldOccluded = mCurrentState.keyguardOccluded; mCurrentState.keyguardOccluded = occluded; + if (oldOccluded != occluded) { + applyKeyguardBlurShow(); + } apply(mCurrentState); } @@ -263,6 +340,39 @@ public class StatusBarWindowManager { apply(mCurrentState); } + void setBlur(float b){ + if (mKeyguardBlurEnabled && mKeyguardBlur != null) { + float minBlur = mKeyguardMonitor.isSecure() ? 1.0f : 0.0f; + if (b < minBlur) { + b = minBlur; + } else if (b > 1.0f) { + b = 1.0f; + } + mKeyguardBlur.setBlur(b); + } + } + + public void setShowingMedia(boolean showingMedia) { + mShowingMedia = showingMedia; + applyKeyguardBlurShow(); + } + + public void setKeyguardExternalViewFocus(boolean hasFocus) { + mLiveLockScreenController.onLiveLockScreenFocusChanged(hasFocus); + // make the keyguard occluded so the external view gets full focus + setKeyguardOccluded(hasFocus); + } + + public void onConfigurationChanged(Configuration newConfig) { + if (mKeyguardBlur != null && newConfig.orientation != mCurrentOrientation) { + Display display = mWindowManager.getDefaultDisplay(); + Point xy = new Point(); + display.getRealSize(xy); + mKeyguardBlur.setSize(xy.x, xy.y); + mCurrentOrientation = newConfig.orientation; + } + } + /** * @param state The {@link StatusBarState} of the status bar. */ @@ -300,11 +410,24 @@ public class StatusBarWindowManager { apply(mCurrentState); } + @Override + public void onKeyguardChanged() { + applyKeyguardBlurShow(); + } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("StatusBarWindowManager state:"); pw.println(mCurrentState); } + public boolean keyguardExternalViewHasFocus() { + return mLiveLockScreenController.getLiveLockScreenHasFocus(); + } + + public void setLiveLockscreenController(LiveLockScreenController liveLockScreenController) { + mLiveLockScreenController = liveLockScreenController; + } + private static class State { boolean keyguardShowing; boolean keyguardOccluded; @@ -355,4 +478,39 @@ public class StatusBarWindowManager { return result.toString(); } } + + private class SettingsObserver extends ContentObserver { + public SettingsObserver(Handler handler) { + super(handler); + } + + public void observe(Context context) { + context.getContentResolver().registerContentObserver( + CMSettings.Secure.getUriFor(CMSettings.Secure.LOCK_SCREEN_BLUR_ENABLED), + false, + this); + context.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), + false, + this); + context.getContentResolver().registerContentObserver( + CMSettings.System.getUriFor(CMSettings.System.LOCKSCREEN_ROTATION), + false, + this); + } + + public void unobserve(Context context) { + context.getContentResolver().unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + mKeyguardBlurEnabled = mBlurSupported ? + CMSettings.Secure.getInt(mContext.getContentResolver(), + CMSettings.Secure.LOCK_SCREEN_BLUR_ENABLED, 1) == 1 : false; + mKeyguardScreenRotation = shouldEnableKeyguardScreenRotation(); + // update the state + apply(mCurrentState); + } + } } 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 0e22aa8..56ced84 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java @@ -17,29 +17,42 @@ package com.android.systemui.statusbar.phone; import android.app.StatusBarManager; +import android.content.ContentResolver; import android.content.Context; import android.content.res.TypedArray; +import android.database.ContentObserver; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.media.session.MediaSessionLegacyHelper; +import android.net.Uri; +import android.os.Handler; import android.os.IBinder; +import android.os.IPowerManager; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.PowerManager; +import android.provider.Settings; import android.util.AttributeSet; +import android.util.Log; +import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.widget.FrameLayout; - import com.android.systemui.R; import com.android.systemui.statusbar.BaseStatusBar; import com.android.systemui.statusbar.DragDownHelper; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; +import cyanogenmod.providers.CMSettings; public class StatusBarWindowView extends FrameLayout { @@ -51,76 +64,24 @@ public class StatusBarWindowView extends FrameLayout { private NotificationPanelView mNotificationPanel; private View mBrightnessMirror; - private int mRightInset = 0; - private PhoneStatusBar mService; private final Paint mTransparentSrcPaint = new Paint(); + private int mStatusBarHeaderHeight; + + private boolean mDoubleTapToSleepEnabled; + private GestureDetector mDoubleTapGesture; + private Handler mHandler = new Handler(); + private SettingsObserver mSettingsObserver; + public StatusBarWindowView(Context context, AttributeSet attrs) { super(context, attrs); setMotionEventSplittingEnabled(false); mTransparentSrcPaint.setColor(0); mTransparentSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); - } - - @Override - protected boolean fitSystemWindows(Rect insets) { - if (getFitsSystemWindows()) { - boolean paddingChanged = insets.left != getPaddingLeft() - || insets.top != getPaddingTop() - || insets.bottom != getPaddingBottom(); - - // 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 - || getPaddingBottom() != 0; - if (changed) { - setPadding(0, 0, 0, 0); - } - insets.top = 0; - } - return false; - } - - private void applyMargins() { - final int N = getChildCount(); - for (int i = 0; i < N; i++) { - View child = getChildAt(i); - if (child.getLayoutParams() instanceof LayoutParams) { - LayoutParams lp = (LayoutParams) child.getLayoutParams(); - if (!lp.ignoreRightInset && lp.rightMargin != mRightInset) { - lp.rightMargin = mRightInset; - child.requestLayout(); - } - } - } - } - - @Override - public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { - return new LayoutParams(getContext(), attrs); - } - - @Override - protected FrameLayout.LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + mStatusBarHeaderHeight = context + .getResources().getDimensionPixelSize(R.dimen.status_bar_header_height); + mSettingsObserver = new SettingsObserver(mHandler); } @Override @@ -141,6 +102,21 @@ public class StatusBarWindowView extends FrameLayout { protected void onAttachedToWindow () { super.onAttachedToWindow(); + mSettingsObserver.observe(); + mDoubleTapGesture = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onDoubleTap(MotionEvent e) { + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + Log.d(TAG, "Gesture!!"); + if(pm != null) + pm.goToSleep(e.getEventTime()); + else + Log.d(TAG, "getSystemService returned null PowerManager"); + + return true; + } + }); + // We really need to be able to animate while window animations are going on // so that activities may be started asynchronously from panel animations final ViewRootImpl root = getViewRootImpl(); @@ -152,11 +128,13 @@ public class StatusBarWindowView extends FrameLayout { // occur if our window is translucent. Since we are drawing the whole window anyway with // the scrim, we don't need the window to be cleared in the beginning. if (mService.isScrimSrcModeEnabled()) { - IBinder windowToken = getWindowToken(); - WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams(); - lp.token = windowToken; - setLayoutParams(lp); - WindowManagerGlobal.getInstance().changeCanvasOpacity(windowToken, true); + if (getLayoutParams() instanceof WindowManager.LayoutParams) { + WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams(); + IBinder windowToken = getWindowToken(); + lp.token = windowToken; + setLayoutParams(lp); + WindowManagerGlobal.getInstance().changeCanvasOpacity(windowToken, true); + } setWillNotDraw(false); } else { setWillNotDraw(!DEBUG); @@ -164,6 +142,22 @@ public class StatusBarWindowView extends FrameLayout { } @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mSettingsObserver.unobserve(); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + insets.bottom = 0; + insets.top = 0; + insets.right = 0; + insets.left = 0; + super.fitSystemWindows(insets); + return false; + } + + @Override public boolean dispatchKeyEvent(KeyEvent event) { boolean down = event.getAction() == KeyEvent.ACTION_DOWN; switch (event.getKeyCode()) { @@ -211,6 +205,11 @@ public class StatusBarWindowView extends FrameLayout { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; + if (mDoubleTapToSleepEnabled + && ev.getY() < mStatusBarHeaderHeight) { + if (DEBUG) Log.w(TAG, "logging double tap gesture"); + mDoubleTapGesture.onTouchEvent(ev); + } if (mNotificationPanel.isFullyExpanded() && mStackScrollLayout.getVisibility() == View.VISIBLE && mService.getBarState() == StatusBarState.KEYGUARD @@ -251,7 +250,7 @@ public class StatusBarWindowView extends FrameLayout { } @Override - public void onDraw(Canvas canvas) { + protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mService.isScrimSrcModeEnabled()) { // We need to ensure that our window is always drawn fully even when we have paddings, @@ -288,21 +287,51 @@ public class StatusBarWindowView extends FrameLayout { } } - public class LayoutParams extends FrameLayout.LayoutParams { + public void addContent(View content) { + addView(content); + mStackScrollLayout = (NotificationStackScrollLayout) content.findViewById( + R.id.notification_stack_scroller); + mNotificationPanel = (NotificationPanelView) content.findViewById(R.id.notification_panel); + mDragDownHelper = new DragDownHelper(getContext(), this, mStackScrollLayout, mService); + mBrightnessMirror = content.findViewById(R.id.brightness_mirror); - public boolean ignoreRightInset; + } - public LayoutParams(int width, int height) { - super(width, height); + public void removeContent(View content) { + removeView(content); + } + + class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); } - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); + void observe() { + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(CMSettings.System.getUriFor( + CMSettings.System.DOUBLE_TAP_SLEEP_GESTURE), false, this); + update(); + } + + void unobserve() { + ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + update(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + update(); + } - TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout); - ignoreRightInset = a.getBoolean( - R.styleable.StatusBarWindowView_Layout_ignoreRightInset, false); - a.recycle(); + public void update() { + ContentResolver resolver = mContext.getContentResolver(); + mDoubleTapToSleepEnabled = CMSettings.System + .getInt(resolver, CMSettings.System.DOUBLE_TAP_SLEEP_GESTURE, 1) == 1; } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index d701b3c..9ebb79f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -33,12 +33,7 @@ public class SystemUIDialog extends AlertDialog { super(context, R.style.Theme_SystemUI_Dialog); mContext = context; - getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - WindowManager.LayoutParams attrs = getWindow().getAttributes(); - attrs.setTitle(getClass().getSimpleName()); - getWindow().setAttributes(attrs); + makeSystemUIDialog(this); } public void setShowForAllUsers(boolean show) { @@ -62,4 +57,13 @@ public class SystemUIDialog extends AlertDialog { public void setNegativeButton(int resId, OnClickListener onClick) { setButton(BUTTON_NEGATIVE, mContext.getString(resId), onClick); } + + public static void makeSystemUIDialog(AlertDialog d) { + d.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL); + d.getWindow().addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + WindowManager.LayoutParams attrs = d.getWindow().getAttributes(); + attrs.setTitle(SystemUIDialog.class.getClass().getSimpleName()); + d.getWindow().setAttributes(attrs); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ViewLinker.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ViewLinker.java new file mode 100644 index 0000000..48457c6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ViewLinker.java @@ -0,0 +1,76 @@ +package com.android.systemui.statusbar.phone; + +import android.view.View; + +/* + Allows mirroring of view states such as alpha, translation...etc + */ +public class ViewLinker<T extends View & ViewLinker.ViewLinkerParent> { + + public static final int LINK_ALPHA = 0x1; + public static final int LINK_TRANSLATION = 0x2; + + private final LinkInfo[] mLinkedViews; + private final T mParent; + + public interface ViewLinkerCallback { + void onAlphaChanged(float alpha); + void onTranslationXChanged(float translationX); + } + + public interface ViewLinkerParent { + void registerLinker(ViewLinkerCallback callback); + } + + public static class LinkInfo { + private View mView; + private int mFlags; + public LinkInfo(View v, int linkFlags) { + mView = v; + mFlags = linkFlags; + } + private boolean supportsFlag(int flag) { + return (mFlags & flag) != 0; + } + } + + private ViewLinkerCallback mCallback = new ViewLinkerCallback() { + @Override + public void onAlphaChanged(float alpha) { + for (LinkInfo v : mLinkedViews) { + if (v.supportsFlag(LINK_ALPHA)) { + v.mView.setAlpha(alpha); + } + } + } + + @Override + public void onTranslationXChanged(float translationX) { + for (LinkInfo v : mLinkedViews) { + if (v.supportsFlag(LINK_TRANSLATION)) { + v.mView.setTranslationX(translationX); + } + } + } + }; + + public ViewLinker(T parent, LinkInfo... viewsToLink) { + mLinkedViews = viewsToLink; + mParent = parent; + ensureParentNotInLink(); + parent.registerLinker(mCallback); + } + + private void ensureParentNotInLink() { + for (LinkInfo v : mLinkedViews) { + if (v.mView == mParent) { + throw new IllegalStateException("Parent cannot be" + + "one of the linked views"); + } + } + } + + public View getParent() { + return mParent; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java index d1b69ab..c59a0d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -17,10 +17,14 @@ package com.android.systemui.statusbar.policy; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.database.ContentObserver; +import android.net.Uri; import android.os.BatteryManager; +import android.os.Handler; import android.os.PowerManager; import android.util.Log; @@ -28,20 +32,38 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -public class BatteryController extends BroadcastReceiver { +import cyanogenmod.providers.CMSettings; + +public class BatteryController extends BroadcastReceiver implements BatteryStateRegistar { private static final String TAG = "BatteryController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + public static final int STYLE_ICON_PORTRAIT = 0; + public static final int STYLE_CIRCLE = 2; + public static final int STYLE_GONE = 4; + public static final int STYLE_ICON_LANDSCAPE = 5; + public static final int STYLE_TEXT = 6; + + public static final int PERCENTAGE_MODE_OFF = 0; + public static final int PERCENTAGE_MODE_INSIDE = 1; + public static final int PERCENTAGE_MODE_OUTSIDE = 2; + private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>(); private final PowerManager mPowerManager; private int mLevel; + private boolean mPresent; private boolean mPluggedIn; private boolean mCharging; private boolean mCharged; private boolean mPowerSave; - public BatteryController(Context context) { + private int mStyle; + private int mPercentMode; + private int mUserId; + private SettingsObserver mObserver; + + public BatteryController(Context context, Handler handler) { mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); IntentFilter filter = new IntentFilter(); @@ -51,22 +73,34 @@ public class BatteryController extends BroadcastReceiver { context.registerReceiver(this, filter); updatePowerSave(); + + mObserver = new SettingsObserver(context, handler); + mObserver.observe(); + } + + public void setUserId(int userId) { + mUserId = userId; + mObserver.observe(); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("BatteryController state:"); pw.print(" mLevel="); pw.println(mLevel); + pw.print(" mPresent="); pw.println(mPresent); pw.print(" mPluggedIn="); pw.println(mPluggedIn); pw.print(" mCharging="); pw.println(mCharging); pw.print(" mCharged="); pw.println(mCharged); pw.print(" mPowerSave="); pw.println(mPowerSave); } + @Override public void addStateChangedCallback(BatteryStateChangeCallback cb) { mChangeCallbacks.add(cb); - cb.onBatteryLevelChanged(mLevel, mPluggedIn, mCharging); + cb.onBatteryLevelChanged(mPresent, mLevel, mPluggedIn, mCharging); + cb.onBatteryStyleChanged(mStyle, mPercentMode); } + @Override public void removeStateChangedCallback(BatteryStateChangeCallback cb) { mChangeCallbacks.remove(cb); } @@ -77,6 +111,7 @@ public class BatteryController extends BroadcastReceiver { mLevel = (int)(100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); + mPresent = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false); mPluggedIn = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; final int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, @@ -110,7 +145,7 @@ public class BatteryController extends BroadcastReceiver { private void fireBatteryLevelChanged() { final int N = mChangeCallbacks.size(); for (int i = 0; i < N; i++) { - mChangeCallbacks.get(i).onBatteryLevelChanged(mLevel, mPluggedIn, mCharging); + mChangeCallbacks.get(i).onBatteryLevelChanged(mPresent, mLevel, mPluggedIn, mCharging); } } @@ -121,8 +156,50 @@ public class BatteryController extends BroadcastReceiver { } } - public interface BatteryStateChangeCallback { - void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging); - void onPowerSaveChanged(); + private void fireSettingsChanged() { + final int N = mChangeCallbacks.size(); + for (int i = 0; i < N; i++) { + mChangeCallbacks.get(i).onBatteryStyleChanged(mStyle, mPercentMode); + } } + + private final class SettingsObserver extends ContentObserver { + private ContentResolver mResolver; + private boolean mRegistered; + + private final Uri STYLE_URI = + CMSettings.System.getUriFor(CMSettings.System.STATUS_BAR_BATTERY_STYLE); + private final Uri PERCENT_URI = + CMSettings.System.getUriFor(CMSettings.System.STATUS_BAR_SHOW_BATTERY_PERCENT); + + public SettingsObserver(Context context, Handler handler) { + super(handler); + mResolver = context.getContentResolver(); + } + + public void observe() { + if (mRegistered) { + mResolver.unregisterContentObserver(this); + } + mResolver.registerContentObserver(STYLE_URI, false, this, mUserId); + mResolver.registerContentObserver(PERCENT_URI, false, this, mUserId); + mRegistered = true; + + update(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + update(); + } + + private void update() { + mStyle = CMSettings.System.getIntForUser(mResolver, + CMSettings.System.STATUS_BAR_BATTERY_STYLE, 0, mUserId); + mPercentMode = CMSettings.System.getIntForUser(mResolver, + CMSettings.System.STATUS_BAR_SHOW_BATTERY_PERCENT, 0, mUserId); + + fireSettingsChanged(); + } + }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateRegistar.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateRegistar.java new file mode 100644 index 0000000..9fe9bb4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateRegistar.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +public interface BatteryStateRegistar { + interface BatteryStateChangeCallback { + void onBatteryLevelChanged(boolean present, int level, boolean pluggedIn, boolean charging); + void onPowerSaveChanged(); + void onBatteryStyleChanged(int style, int percentMode); + } + + public void addStateChangedCallback(BatteryStateChangeCallback cb); + public void removeStateChangedCallback(BatteryStateChangeCallback cb); +} 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 0340984..1ccf80b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java @@ -38,7 +38,7 @@ public class BrightnessMirrorController { private final View mPanelHolder; private final int[] mInt2Cache = new int[2]; - public BrightnessMirrorController(StatusBarWindowView statusBarWindow) { + public BrightnessMirrorController(View statusBarWindow) { mScrimBehind = (ScrimView) statusBarWindow.findViewById(R.id.scrim_behind); mBrightnessMirror = statusBarWindow.findViewById(R.id.brightness_mirror); mPanelHolder = statusBarWindow.findViewById(R.id.panel_holder); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java index e618cb8..e7f65b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java @@ -126,14 +126,15 @@ public class CallbackHandler extends Handler implements EmergencyListener, Signa 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) { + final String description, final boolean isWide, final boolean showSeparateRoaming, + 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); + showSeparateRoaming, subId); } } }); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java index e344954..835c8ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java @@ -115,7 +115,7 @@ public class CastControllerImpl implements CastController { } if (mDiscovering) { mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaCallback, - MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); + MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); mCallbackRegistered = true; } else if (mCallbacks.size() != 0) { mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaCallback, @@ -164,7 +164,7 @@ public class CastControllerImpl implements CastController { @Override public void startCasting(CastDevice device) { - if (device == null || device.tag == null) return; + if (device == null || !(device.tag instanceof RouteInfo)) return; final RouteInfo route = (RouteInfo) device.tag; if (DEBUG) Log.d(TAG, "startCasting: " + routeToString(route)); mMediaRouter.selectRoute(ROUTE_TYPE_REMOTE_DISPLAY, route); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index 61986ad..0cc82dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -34,6 +34,7 @@ import android.widget.TextView; import com.android.systemui.DemoMode; import com.android.systemui.R; +import com.android.systemui.cm.UserContentObserver; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -52,11 +53,11 @@ public class Clock extends TextView implements DemoMode { private SimpleDateFormat mClockFormat; private Locale mLocale; - private static final int AM_PM_STYLE_NORMAL = 0; - private static final int AM_PM_STYLE_SMALL = 1; - private static final int AM_PM_STYLE_GONE = 2; + public static final int AM_PM_STYLE_NORMAL = 0; + public static final int AM_PM_STYLE_SMALL = 1; + public static final int AM_PM_STYLE_GONE = 2; - private final int mAmPmStyle; + private int mAmPmStyle = AM_PM_STYLE_GONE; public Clock(Context context) { this(context, null); @@ -68,15 +69,6 @@ public class Clock extends TextView implements DemoMode { public Clock(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - TypedArray a = context.getTheme().obtainStyledAttributes( - attrs, - R.styleable.Clock, - 0, 0); - try { - mAmPmStyle = a.getInt(R.styleable.Clock_amPmStyle, AM_PM_STYLE_GONE); - } finally { - a.recycle(); - } } @Override @@ -138,7 +130,7 @@ public class Clock extends TextView implements DemoMode { }; final void updateClock() { - if (mDemoMode) return; + if (mDemoMode || mCalendar == null) return; mCalendar.setTimeInMillis(System.currentTimeMillis()); setText(getSmallTime()); } @@ -244,5 +236,11 @@ public class Clock extends TextView implements DemoMode { setText(getSmallTime()); } } + + public void setAmPmStyle(int style) { + mAmPmStyle = style; + mClockFormatString = ""; + updateClock(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java index 186005c..720ab44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java @@ -1,4 +1,7 @@ /* + * Copyright (c) 2013-2014, The Linux Foundation. All rights reserved. + * Not a Contribution. + * * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -105,11 +108,19 @@ public class DateView extends TextView { } mCurrentTime.setTime(System.currentTimeMillis()); - - final String text = mDateFormat.format(mCurrentTime); + final String text = getDateFormat(); if (!text.equals(mLastText)) { setText(text); mLastText = text; } } + + private String getDateFormat() { + if (getContext().getResources().getBoolean( + com.android.internal.R.bool.def_custom_dateformat)) { + return DateFormat.getDateFormat(getContext()).format(mCurrentTime); + } else { + return mDateFormat.format(mCurrentTime); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeadZone.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeadZone.java index 6eb88be..9864a0a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeadZone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeadZone.java @@ -46,6 +46,7 @@ public class DeadZone extends View { // mHold ms, then move back over the course of mDecay ms private int mHold, mDecay; private boolean mVertical; + private boolean mStartFromRight; private long mLastPokeTime; private final Runnable mDebugFlash = new Runnable() { @@ -73,6 +74,7 @@ public class DeadZone extends View { int index = a.getInt(R.styleable.DeadZone_orientation, -1); mVertical = (index == VERTICAL); + mStartFromRight = false; // Assume deadzone is starting from the left side of the zone if (DEBUG) Slog.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold @@ -100,6 +102,7 @@ public class DeadZone extends View { mShouldFlash = dbg; mFlashFrac = 0f; postInvalidate(); + mFlashFrac = dbg ? 1f : 0f; } // I made you a touch event... @@ -117,7 +120,19 @@ public class DeadZone extends View { Slog.v(TAG, this + " ACTION_DOWN: " + event.getX() + "," + event.getY()); } int size = (int) getSize(event.getEventTime()); - if ((mVertical && event.getX() < size) || event.getY() < size) { + boolean isCaptured; + if (mVertical && mStartFromRight) { + // Landscape on the left side of the screen + float pixelsFromRight = getWidth() - event.getX(); + isCaptured = 0 <= pixelsFromRight && pixelsFromRight < size; + } else if (mVertical) { + // Landscape + isCaptured = event.getX() < size; + } else { + // Portrait + isCaptured = event.getY() < size; + } + if (isCaptured) { if (CHATTY) { Slog.v(TAG, "consuming errant click: (" + event.getX() + "," + event.getY() + ")"); } @@ -147,6 +162,11 @@ public class DeadZone extends View { return mFlashFrac; } + public void setStartFromRight(boolean startFromRight) { + mStartFromRight = startFromRight; + if (mShouldFlash) postInvalidate(); + } + @Override public void onDraw(Canvas can) { if (!mShouldFlash || mFlashFrac <= 0f) { @@ -154,7 +174,17 @@ public class DeadZone extends View { } final int size = (int) getSize(SystemClock.uptimeMillis()); - can.clipRect(0, 0, mVertical ? size : can.getWidth(), mVertical ? can.getHeight() : size); + if (mVertical && mStartFromRight) { + // Landscape on the left side of the screen + can.clipRect(can.getWidth() - size, 0, can.getWidth(), can.getHeight()); + } else if (mVertical) { + // Landscape + can.clipRect(0, 0, size, can.getHeight()); + } else { + // Portrait + can.clipRect(0, 0, can.getWidth(), size); + } + final float frac = DEBUG ? (mFlashFrac - 0.5f) + 0.5f : mFlashFrac; can.drawARGB((int) (frac * 0xFF), 0xDD, 0xEE, 0xAA); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DockBatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DockBatteryController.java new file mode 100644 index 0000000..3faf7d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DockBatteryController.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.BatteryManager; +import android.os.Handler; +import android.provider.Settings; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; + +import cyanogenmod.providers.CMSettings; + +public class DockBatteryController extends BroadcastReceiver implements BatteryStateRegistar { + + private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>(); + + private int mLevel; + private boolean mPresent; + private boolean mPluggedIn; + private boolean mCharging; + private boolean mCharged; + private boolean mPowerSave; + + private int mStyle; + private int mPercentMode; + private int mUserId; + private SettingsObserver mObserver; + + public DockBatteryController(Context context, Handler handler) { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + context.registerReceiver(this, filter); + + mObserver = new SettingsObserver(context, handler); + mObserver.observe(); + } + + public void setUserId(int userId) { + mUserId = userId; + mObserver.observe(); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("BatteryController state:"); + pw.print(" mLevel="); pw.println(mLevel); + pw.print(" mPresent="); pw.println(mPresent); + pw.print(" mPluggedIn="); pw.println(mPluggedIn); + pw.print(" mCharging="); pw.println(mCharging); + pw.print(" mCharged="); pw.println(mCharged); + pw.print(" mPowerSave="); pw.println(mPowerSave); + } + + @Override + public void addStateChangedCallback(BatteryStateChangeCallback cb) { + mChangeCallbacks.add(cb); + cb.onBatteryLevelChanged(mPresent, mLevel, mPluggedIn, mCharging); + cb.onBatteryStyleChanged(mStyle, mPercentMode); + } + + @Override + public void removeStateChangedCallback(BatteryStateChangeCallback cb) { + mChangeCallbacks.remove(cb); + } + + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { + mLevel = (int)(100f + * intent.getIntExtra(BatteryManager.EXTRA_DOCK_LEVEL, 0) + / intent.getIntExtra(BatteryManager.EXTRA_DOCK_SCALE, 100)); + mPresent = intent.getBooleanExtra(BatteryManager.EXTRA_DOCK_PRESENT, false); + mPluggedIn = intent.getIntExtra(BatteryManager.EXTRA_DOCK_PLUGGED, 0) != 0; + + final int status = intent.getIntExtra(BatteryManager.EXTRA_DOCK_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN); + mCharged = status == BatteryManager.BATTERY_STATUS_FULL; + mCharging = mPluggedIn && (mCharged || status == BatteryManager.BATTERY_STATUS_CHARGING); + + fireBatteryLevelChanged(); + } + } + + private void fireBatteryLevelChanged() { + final int N = mChangeCallbacks.size(); + for (int i = 0; i < N; i++) { + mChangeCallbacks.get(i).onBatteryLevelChanged(mPresent, mLevel, mPresent, mCharging); + } + } + + private void fireSettingsChanged() { + final int N = mChangeCallbacks.size(); + for (int i = 0; i < N; i++) { + mChangeCallbacks.get(i).onBatteryStyleChanged(mStyle, mPercentMode); + } + } + + private final class SettingsObserver extends ContentObserver { + private ContentResolver mResolver; + private boolean mRegistered; + + private final Uri STYLE_URI = + CMSettings.System.getUriFor(CMSettings.System.STATUS_BAR_BATTERY_STYLE); + private final Uri PERCENT_URI = + CMSettings.System.getUriFor(CMSettings.System.STATUS_BAR_SHOW_BATTERY_PERCENT); + + public SettingsObserver(Context context, Handler handler) { + super(handler); + mResolver = context.getContentResolver(); + } + + public void observe() { + if (mRegistered) { + mResolver.unregisterContentObserver(this); + } + mResolver.registerContentObserver(STYLE_URI, false, this, mUserId); + mResolver.registerContentObserver(PERCENT_URI, false, this, mUserId); + mRegistered = true; + + update(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + update(); + } + + private void update() { + mStyle = CMSettings.System.getIntForUser(mResolver, + CMSettings.System.STATUS_BAR_BATTERY_STYLE, 0, mUserId); + mPercentMode = CMSettings.System.getIntForUser(mResolver, + CMSettings.System.STATUS_BAR_SHOW_BATTERY_PERCENT, 0, mUserId); + + fireSettingsChanged(); + } + }; +} 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 29a8f67..52a2825 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java @@ -16,16 +16,26 @@ package com.android.systemui.statusbar.policy; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; import android.text.TextUtils; import android.util.Log; +import com.android.systemui.R; + import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -41,6 +51,12 @@ public class FlashlightController { private static final int DISPATCH_CHANGED = 1; private static final int DISPATCH_AVAILABILITY_CHANGED = 2; + private static boolean mUseWakeLock; + + private static final String ACTION_TURN_FLASHLIGHT_OFF = + "com.android.systemui.action.TURN_FLASHLIGHT_OFF"; + + private Context mContext; private final CameraManager mCameraManager; /** Call {@link #ensureHandler()} before using */ private Handler mHandler; @@ -54,7 +70,28 @@ public class FlashlightController { private final String mCameraId; private boolean mTorchAvailable; + private WakeLock mWakeLock; + + private Notification mNotification = null; + private boolean mReceiverRegistered; + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_TURN_FLASHLIGHT_OFF.equals(intent.getAction())) { + mHandler.post(new Runnable() { + @Override + public void run() { + setFlashlight(false); + } + }); + } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) { + setNotificationShown(true); + } + } + }; + public FlashlightController(Context mContext) { + this.mContext = mContext; mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); String cameraId = null; @@ -67,6 +104,11 @@ public class FlashlightController { mCameraId = cameraId; } + mUseWakeLock = mContext.getResources().getBoolean(R.bool.flashlight_use_wakelock); + + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + if (mCameraId != null) { ensureHandler(); mCameraManager.registerTorchCallback(mTorchCallback, mHandler); @@ -78,12 +120,25 @@ public class FlashlightController { synchronized (this) { if (mFlashlightEnabled != enabled) { mFlashlightEnabled = enabled; + + if (mUseWakeLock) { + if (enabled) { + if (!mWakeLock.isHeld()) mWakeLock.acquire(); + } else { + if (mWakeLock.isHeld()) mWakeLock.release(); + } + } + try { mCameraManager.setTorchMode(mCameraId, enabled); } catch (CameraAccessException e) { Log.e(TAG, "Couldn't set torch mode", e); mFlashlightEnabled = false; pendingError = true; + + if (mUseWakeLock && mWakeLock.isHeld()) { + mWakeLock.release(); + } } } } @@ -93,6 +148,56 @@ public class FlashlightController { } } + private void setNotificationShown(boolean showNotification) { + NotificationManager nm = (NotificationManager) + mContext.getSystemService(Context.NOTIFICATION_SERVICE); + if (showNotification) { + nm.notify(R.string.quick_settings_tile_flashlight_not_title, buildNotification()); + } else { + nm.cancel(R.string.quick_settings_tile_flashlight_not_title); + mNotification = null; + } + } + + private void setListenForScreenOff(boolean listen) { + if (listen && !mReceiverRegistered) { + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_TURN_FLASHLIGHT_OFF); + filter.addAction(Intent.ACTION_SCREEN_ON); + mContext.registerReceiver(mReceiver, filter); + mReceiverRegistered = true; + } else if (!listen) { + if (mReceiverRegistered) { + mContext.unregisterReceiver(mReceiver); + mReceiverRegistered = false; + } + setNotificationShown(false); + } + } + + private Notification buildNotification() { + if (mNotification == null) { + Intent fireMe = new Intent(ACTION_TURN_FLASHLIGHT_OFF); + fireMe.addFlags(Intent.FLAG_FROM_BACKGROUND); + fireMe.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + fireMe.setPackage(mContext.getPackageName()); + + final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, fireMe, 0); + mNotification = new Notification.Builder(mContext) + .setContentTitle( + mContext.getString(R.string.quick_settings_tile_flashlight_not_title)) + .setContentText( + mContext.getString(R.string.quick_settings_tile_flashlight_not_summary)) + .setAutoCancel(false) + .setOngoing(true) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setSmallIcon(R.drawable.ic_signal_flashlight_disable) + .setContentIntent(pendingIntent) + .build(); + } + return mNotification; + } + public synchronized boolean isEnabled() { return mFlashlightEnabled; } @@ -188,6 +293,7 @@ public class FlashlightController { public void onTorchModeUnavailable(String cameraId) { if (TextUtils.equals(cameraId, mCameraId)) { setCameraAvailable(false); + setListenForScreenOff(false); } } @@ -196,6 +302,7 @@ public class FlashlightController { if (TextUtils.equals(cameraId, mCameraId)) { setCameraAvailable(true); setTorchMode(enabled); + setListenForScreenOff(enabled); } } @@ -204,6 +311,11 @@ public class FlashlightController { synchronized (FlashlightController.this) { changed = mTorchAvailable != available; mTorchAvailable = available; + + if (mUseWakeLock && !available) { + if (mWakeLock.isHeld()) + mWakeLock.release(); + } } if (changed) { if (DEBUG) Log.d(TAG, "dispatchAvailabilityChanged(" + available + ")"); @@ -216,6 +328,11 @@ public class FlashlightController { synchronized (FlashlightController.this) { changed = mFlashlightEnabled != enabled; mFlashlightEnabled = enabled; + + if (mUseWakeLock && !enabled) { + if (mWakeLock.isHeld()) + mWakeLock.release(); + } } if (changed) { if (DEBUG) Log.d(TAG, "dispatchModeChanged(" + enabled + ")"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java index 4a95d3a..3d1212b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -160,6 +160,10 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL mListeners.add(listener); } + public void removeListener(OnHeadsUpChangedListener listener) { + mListeners.remove(listener); + } + public PhoneStatusBar getBar() { return mBar; } @@ -453,7 +457,10 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL mReleaseOnExpandFinish = false; } else { for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) { - removeHeadsUpEntry(entry); + if (isHeadsUp(entry.key)) { + // Maybe the heads-up was removed already + removeHeadsUpEntry(entry); + } } } mEntriesToRemoveAfterExpand.clear(); @@ -572,6 +579,9 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL earliestRemovaltime = currentTime + mMinimumDisplayTime; postTime = Math.max(postTime, currentTime); removeAutoRemovalCallbacks(); + if (mEntriesToRemoveAfterExpand.contains(entry)) { + mEntriesToRemoveAfterExpand.remove(entry); + } if (!hasFullScreenIntent(entry)) { long finishTime = postTime + mHeadsUpNotificationDecay; long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime); 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 3f63b5f..ed41121 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java @@ -66,16 +66,19 @@ public class KeyButtonRipple extends Drawable { private final HashSet<Animator> mRunningAnimations = new HashSet<>(); private final ArrayList<Animator> mTmpArray = new ArrayList<>(); + private int mRippleColor; + public KeyButtonRipple(Context ctx, View targetView) { mMaxWidth = ctx.getResources().getDimensionPixelSize(R.dimen.key_button_ripple_max_width); mTargetView = targetView; + mRippleColor = ctx.getResources().getColor(R.color.navbutton_ripple_color); } private Paint getRipplePaint() { if (mRipplePaint == null) { mRipplePaint = new Paint(); mRipplePaint.setAntiAlias(true); - mRipplePaint.setColor(0xffffffff); + mRipplePaint.setColor(mRippleColor); } return mRipplePaint; } 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 4d268ce..6fef3d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java @@ -18,6 +18,9 @@ package com.android.systemui.statusbar.policy; import android.app.ActivityManager; import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.ThemeConfig; import android.content.res.Configuration; import android.content.res.TypedArray; import android.hardware.input.InputManager; @@ -39,28 +42,46 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import com.android.systemui.R; +import com.android.systemui.statusbar.phone.NavbarEditor; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; +import cyanogenmod.power.PerformanceManager; + public class KeyButtonView extends ImageView { + public static final int CURSOR_REPEAT_FLAGS = KeyEvent.FLAG_SOFT_KEYBOARD + | KeyEvent.FLAG_KEEP_TOUCH_MODE; + private int mContentDescriptionRes; private long mDownTime; private int mCode; + private boolean mIsSmall; private int mTouchSlop; private boolean mSupportsLongpress = true; + private boolean mInEditMode; private AudioManager mAudioManager; private boolean mGestureAborted; + private boolean mPerformedLongClick; + + private PerformanceManager mPerf; private final Runnable mCheckLongPress = new Runnable() { public void run() { if (isPressed()) { // Log.d("KeyButtonView", "longpressed: " + this); - if (isLongClickable()) { + if (mCode == KeyEvent.KEYCODE_DPAD_LEFT || mCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + sendEvent(KeyEvent.ACTION_UP, CURSOR_REPEAT_FLAGS, + System.currentTimeMillis(), false); + sendEvent(KeyEvent.ACTION_DOWN, CURSOR_REPEAT_FLAGS, + System.currentTimeMillis(), false); + postDelayed(mCheckLongPress, ViewConfiguration.getKeyRepeatDelay()); + } else if (isLongClickable()) { // Just an old-fashioned ImageView + mPerformedLongClick = true; performLongClick(); - } else if (mSupportsLongpress) { + } else { sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); } @@ -94,6 +115,7 @@ public class KeyButtonView extends ImageView { mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); setBackground(new KeyButtonRipple(context, this)); + mPerf = PerformanceManager.getInstance(context); } @Override @@ -142,7 +164,83 @@ public class KeyButtonView extends ImageView { return super.performAccessibilityActionInternal(action, arguments); } + @Override + public Resources getResources() { + ThemeConfig themeConfig = mContext.getResources().getConfiguration().themeConfig; + Resources res = null; + if (themeConfig != null) { + try { + final String navbarThemePkgName = themeConfig.getOverlayForNavBar(); + final String sysuiThemePkgName = themeConfig.getOverlayForStatusBar(); + // Check if the same theme is applied for systemui, if so we can skip this + if (navbarThemePkgName != null && !navbarThemePkgName.equals(sysuiThemePkgName)) { + res = mContext.getPackageManager().getThemedResourcesForApplication( + mContext.getPackageName(), navbarThemePkgName); + } + } catch (PackageManager.NameNotFoundException e) { + // don't care since we'll handle res being null below + } + } + + return res != null ? res : super.getResources(); + } + + public void setEditMode(boolean editMode) { + mInEditMode = editMode; + updateVisibility(); + } + + public void setInfo(NavbarEditor.ButtonInfo item, boolean isVertical, boolean isSmall) { + final Resources res = getResources(); + setInfo(item, isVertical, isSmall, res); + } + + public void setInfo(NavbarEditor.ButtonInfo item, boolean isVertical, boolean isSmall, + Resources res) { + final int keyDrawableResId; + + setTag(item); + setContentDescription(res.getString(item.contentDescription)); + mCode = item.keyCode; + mIsSmall = isSmall; + + if (isSmall) { + keyDrawableResId = item.sideResource; + } else if (!isVertical) { + keyDrawableResId = item.portResource; + } else { + keyDrawableResId = item.landResource; + } + // The reason for setImageDrawable vs setImageResource is because setImageResource calls + // relayout() w/o any checks. setImageDrawable performs size checks and only calls relayout + // if necessary. We rely on this because otherwise the setX/setY attributes which are post + // layout cause it to mess up the layout. + setImageDrawable(res.getDrawable(keyDrawableResId)); + updateVisibility(); + } + + private void updateVisibility() { + if (mInEditMode) { + setVisibility(View.VISIBLE); + return; + } + + NavbarEditor.ButtonInfo info = (NavbarEditor.ButtonInfo) getTag(); + if (info == NavbarEditor.NAVBAR_EMPTY) { + setVisibility(mIsSmall ? View.INVISIBLE : View.GONE); + } else if (info == NavbarEditor.NAVBAR_CONDITIONAL_MENU) { + setVisibility(View.INVISIBLE); + } + } + + private boolean supportsLongPress() { + return mSupportsLongpress; + } + public boolean onTouchEvent(MotionEvent ev) { + if (mInEditMode) { + return false; + } final int action = ev.getAction(); int x, y; if (action == MotionEvent.ACTION_DOWN) { @@ -152,22 +250,32 @@ public class KeyButtonView extends ImageView { return false; } + // A lot of stuff is about to happen. Lets get ready. + mPerf.cpuBoost(750000); + switch (action) { case MotionEvent.ACTION_DOWN: mDownTime = SystemClock.uptimeMillis(); setPressed(true); - if (mCode != 0) { + if (mCode == KeyEvent.KEYCODE_DPAD_LEFT || mCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_VIRTUAL_HARD_KEY + | KeyEvent.FLAG_KEEP_TOUCH_MODE, mDownTime, false); + } else if (mCode != 0) { sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime); } else { // Provide the same haptic feedback that the system offers for virtual keys. performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); } - removeCallbacks(mCheckLongPress); - postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); + + if (supportsLongPress()) { + removeCallbacks(mCheckLongPress); + postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); + } + break; case MotionEvent.ACTION_MOVE: - x = (int)ev.getX(); - y = (int)ev.getY(); + x = (int) ev.getX(); + y = (int) ev.getY(); setPressed(x >= -mTouchSlop && x < getWidth() + mTouchSlop && y >= -mTouchSlop @@ -178,7 +286,13 @@ public class KeyButtonView extends ImageView { if (mCode != 0) { sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); } + removeCallbacks(mCheckLongPress); + + if (supportsLongPress()) { + removeCallbacks(mCheckLongPress); + } + break; case MotionEvent.ACTION_UP: final boolean doIt = isPressed(); @@ -193,11 +307,17 @@ public class KeyButtonView extends ImageView { } } else { // no key code, just a regular ImageView - if (doIt) { + if (doIt && !mPerformedLongClick) { performClick(); } } + removeCallbacks(mCheckLongPress); + + if (supportsLongPress()) { + removeCallbacks(mCheckLongPress); + } + mPerformedLongClick = false; break; } @@ -213,10 +333,17 @@ public class KeyButtonView extends ImageView { } void sendEvent(int action, int flags, long when) { + sendEvent(action, flags, when, true); + } + + void sendEvent(int action, int flags, long when, boolean applyDefaultFlags) { final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0; + if (applyDefaultFlags) { + flags |= KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY; + } final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, - flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, + flags, InputDevice.SOURCE_KEYBOARD); InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LiveLockScreenController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LiveLockScreenController.java new file mode 100644 index 0000000..2f290cc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LiveLockScreenController.java @@ -0,0 +1,349 @@ +package com.android.systemui.statusbar.policy; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.EventLog; + +import android.view.View; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.EventLogTags; +import com.android.systemui.SystemUIApplication; +import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.phone.NotificationPanelView; +import com.android.systemui.statusbar.phone.PhoneStatusBar; + +import cyanogenmod.app.CMContextConstants; +import cyanogenmod.app.ILiveLockScreenChangeListener; +import cyanogenmod.app.ILiveLockScreenManager; +import cyanogenmod.app.LiveLockScreenInfo; +import cyanogenmod.externalviews.KeyguardExternalView; + +import java.util.Objects; + +public class LiveLockScreenController { + private static final String TAG = LiveLockScreenController.class.getSimpleName(); + + private ILiveLockScreenManager mLLSM; + private Context mContext; + private PhoneStatusBar mBar; + private NotificationPanelView mPanelView; + private ComponentName mLiveLockScreenComponentName; + private KeyguardExternalView mLiveLockScreenView; + private Handler mHandler; + + private int mStatusBarState; + + private PowerManager mPowerManager; + + private boolean mLlsHasFocus = false; + + private boolean mScreenOnAndInteractive; + + private String mLlsName; + private KeyguardViewMediator mKeyguardViewMediator; + + public LiveLockScreenController(Context context, PhoneStatusBar bar, + NotificationPanelView panelView) { + mContext = context; + mHandler = new Handler(Looper.getMainLooper()); + + mLLSM = ILiveLockScreenManager.Stub.asInterface(ServiceManager.getService( + CMContextConstants.CM_LIVE_LOCK_SCREEN_SERVICE)); + mBar = bar; + mPanelView = panelView; + mPowerManager = context.getSystemService(PowerManager.class); + mKeyguardViewMediator = ((SystemUIApplication) + mContext.getApplicationContext()).getComponent(KeyguardViewMediator.class); + registerListener(); + try { + LiveLockScreenInfo llsInfo = mLLSM.getCurrentLiveLockScreen(); + if (llsInfo != null && llsInfo.component != null) { + updateLiveLockScreenView(llsInfo.component); + } + } catch (RemoteException e) { + /* ignore */ + } + } + + public void cleanup() { + unregisterListener(); + mPanelView = null; + if (mLiveLockScreenView != null) { + mLiveLockScreenView.setProviderComponent(null); + } + mLiveLockScreenView = null; + mLiveLockScreenComponentName = null; + } + + public void setBarState(int statusBarState) { + if (mStatusBarState != StatusBarState.SHADE && statusBarState == StatusBarState.SHADE) { + // going from KEYGUARD or SHADE_LOCKED to SHADE so device has been unlocked + onKeyguardDismissed(); + } + + if (statusBarState == StatusBarState.KEYGUARD) { + mBar.getScrimController().forceHideScrims(false); + } + + mStatusBarState = statusBarState; + if (statusBarState == StatusBarState.KEYGUARD || + statusBarState == StatusBarState.SHADE_LOCKED) { + if (mLiveLockScreenComponentName != null) { + if (mLiveLockScreenView == null) { + mLiveLockScreenView = + getExternalKeyguardView(mLiveLockScreenComponentName); + if (mLiveLockScreenView != null) { + mLiveLockScreenView.registerKeyguardExternalViewCallback( + mExternalKeyguardViewCallbacks); + } + } + if (mLiveLockScreenView != null && !mLiveLockScreenView.isAttachedToWindow()) { + mBar.updateRowStates(); + mPanelView.addView(mLiveLockScreenView, 0); + } + } + } else { + if (isShowingLiveLockScreenView() && !mBar.isKeyguardInputRestricted()) { + mPanelView.removeView(mLiveLockScreenView); + } + mLlsHasFocus = false; + } + } + + private ILiveLockScreenChangeListener mChangeListener = + new ILiveLockScreenChangeListener.Stub() { + @Override + public void onLiveLockScreenChanged(LiveLockScreenInfo llsInfo) throws RemoteException { + if (mPanelView != null) { + updateLiveLockScreenView(llsInfo != null ? llsInfo.component : null); + } + } + }; + + private void registerListener() { + try { + mLLSM.registerChangeListener(mChangeListener); + } catch (RemoteException e) { + /* ignore */ + } + } + + private void unregisterListener() { + try { + mLLSM.unregisterChangeListener(mChangeListener); + } catch (RemoteException e) { + /* ignore */ + } + } + + private KeyguardExternalView getExternalKeyguardView(ComponentName componentName) { + try { + return new KeyguardExternalView(mContext, null, componentName); + } catch (Exception e) { + // just return null below and move on + } + return null; + } + + private KeyguardExternalView.KeyguardExternalViewCallbacks mExternalKeyguardViewCallbacks = + new KeyguardExternalView.KeyguardExternalViewCallbacks() { + @Override + public boolean requestDismiss() { + if (isShowingLiveLockScreenView()) { + mHandler.post(new Runnable() { + @Override + public void run() { + mBar.showKeyguard(); + mBar.showBouncer(); + } + }); + return true; + } + return false; + } + + @Override + public boolean requestDismissAndStartActivity(final Intent intent) { + if (isShowingLiveLockScreenView()) { + mHandler.post(new Runnable() { + @Override + public void run() { + mBar.startActivityDismissingKeyguard(intent, false, true, true, + null); + } + }); + return true; + } + return false; + } + + @Override + public void providerDied() { + mLiveLockScreenView.unregisterKeyguardExternalViewCallback( + mExternalKeyguardViewCallbacks); + mLiveLockScreenView = null; + // make sure we're showing the notification panel if the LLS crashed while it had focus + if (mLlsHasFocus) { + mLlsHasFocus = false; + mHandler.post(new Runnable() { + @Override + public void run() { + mBar.showKeyguard(); + } + }); + } + } + + @Override + public void slideLockscreenIn() { + if (mLlsHasFocus) { + mHandler.post(new Runnable() { + @Override + public void run() { + mBar.showKeyguard(); + } + }); + } + } + }; + + public boolean isShowingLiveLockScreenView() { + return mLiveLockScreenView != null && mLiveLockScreenView.isAttachedToWindow(); + } + + public boolean isLiveLockScreenInteractive() { + return mLiveLockScreenView != null && mLiveLockScreenView.isInteractive(); + } + + public KeyguardExternalView getLiveLockScreenView() { + return mLiveLockScreenView; + } + + public void onScreenTurnedOn() { + mScreenOnAndInteractive = mPowerManager.isInteractive(); + if (mScreenOnAndInteractive) { + if (mLiveLockScreenView != null) mLiveLockScreenView.onScreenTurnedOn(); + EventLog.writeEvent(EventLogTags.SYSUI_LLS_KEYGUARD_SHOWING, 1); + } + } + + public void onScreenTurnedOff() { + if (mScreenOnAndInteractive) { + if (mLiveLockScreenView != null) mLiveLockScreenView.onScreenTurnedOff(); + if (mStatusBarState != StatusBarState.SHADE) { + EventLog.writeEvent(EventLogTags.SYSUI_LLS_KEYGUARD_SHOWING, 0); + } + mScreenOnAndInteractive = false; + } + } + + public void onLiveLockScreenFocusChanged(boolean hasFocus) { + mKeyguardViewMediator.notifyKeyguardPanelFocusChanged(hasFocus); + if (mLiveLockScreenView != null) { + // make sure the LLS knows where the notification panel is + mLiveLockScreenView.onLockscreenSlideOffsetChanged(hasFocus ? 0f : 1f); + } + // don't log focus changes when screen is not interactive + if (hasFocus != mLlsHasFocus && mPowerManager.isInteractive()) { + EventLog.writeEvent(EventLogTags.SYSUI_LLS_NOTIFICATION_PANEL_SHOWN, + hasFocus ? 0 : 1); + } + // Hide statusbar and scrim if live lockscreen + // currently has focus + mBar.setStatusBarViewVisibility(!hasFocus); + mBar.getScrimController().forceHideScrims(hasFocus); + mLlsHasFocus = hasFocus; + } + + public void onKeyguardDismissed() { + if (mLiveLockScreenView != null) mLiveLockScreenView.onKeyguardDismissed(); + EventLog.writeEvent(EventLogTags.SYSUI_LLS_KEYGUARD_DISMISSED, mLlsHasFocus ? 1 : 0); + // Ensure we reset visibility when keyguard is dismissed + mBar.setStatusBarViewVisibility(true); + mBar.getScrimController().forceHideScrims(false); + } + + public boolean getLiveLockScreenHasFocus() { + return mLlsHasFocus; + } + + public String getLiveLockScreenName() { + return mLlsName; + } + + private String getLlsNameFromComponentName(ComponentName cn) { + if (cn == null) return null; + + PackageManager pm = mContext.getPackageManager(); + Intent intent = new Intent(); + intent.setComponent(cn); + ResolveInfo ri = pm.resolveService(intent, 0); + return ri != null ? ri.serviceInfo.loadLabel(pm).toString() : null; + } + + private Runnable mAddNewLiveLockScreenRunnable = new Runnable() { + @Override + public void run() { + if (mLiveLockScreenComponentName != null) { + mLiveLockScreenView = + getExternalKeyguardView(mLiveLockScreenComponentName); + mLiveLockScreenView.registerKeyguardExternalViewCallback( + mExternalKeyguardViewCallbacks); + if (mStatusBarState != StatusBarState.SHADE) { + mPanelView.addView(mLiveLockScreenView); + mLiveLockScreenView.onKeyguardShowing(true); + } + } else { + mLiveLockScreenView = null; + } + } + }; + + private void updateLiveLockScreenView(final ComponentName cn) { + mHandler.post(new Runnable() { + @Override + public void run() { + // If mThirdPartyKeyguardViewComponent differs from cn, go ahead and update + if (!Objects.equals(mLiveLockScreenComponentName, cn)) { + mLiveLockScreenComponentName = cn; + mLlsName = getLlsNameFromComponentName(cn); + if (mLiveLockScreenView != null) { + mLiveLockScreenView.unregisterKeyguardExternalViewCallback( + mExternalKeyguardViewCallbacks); + // setProviderComponent(null) will unbind the existing service + mLiveLockScreenView.setProviderComponent(null); + if (mPanelView.indexOfChild(mLiveLockScreenView) >= 0) { + mLiveLockScreenView.registerOnWindowAttachmentChangedListener( + new KeyguardExternalView.OnWindowAttachmentChangedListener() { + @Override + public void onAttachedToWindow() { + } + + @Override + public void onDetachedFromWindow() { + mLiveLockScreenView + .unregisterOnWindowAttachmentChangedListener( + this); + mHandler.post(mAddNewLiveLockScreenRunnable); + } + } + ); + mPanelView.removeView(mLiveLockScreenView); + } else { + mAddNewLiveLockScreenRunnable.run(); + } + } + } + } + }); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java index 29a8981..a88f22d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java @@ -19,6 +19,9 @@ package com.android.systemui.statusbar.policy; public interface LocationController { boolean isLocationEnabled(); boolean setLocationEnabled(boolean enabled); + boolean setLocationMode(int mode); + int getLocationCurrentState(); + boolean isAdvancedSettingsEnabled(); void addSettingsChangedCallback(LocationSettingsChangeCallback cb); void removeSettingsChangedCallback(LocationSettingsChangeCallback cb); 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 93a8fd8..e7867b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java @@ -37,6 +37,8 @@ import com.android.systemui.R; import java.util.ArrayList; import java.util.List; +import cyanogenmod.providers.CMSettings; + /** * A controller to manage changes of location related states and update the views accordingly. */ @@ -55,6 +57,7 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio private StatusBarManager mStatusBarManager; private boolean mAreActiveLocationRequests; + private int mLastActiveMode; private ArrayList<LocationSettingsChangeCallback> mSettingsChangeCallbacks = new ArrayList<LocationSettingsChangeCallback>(); @@ -63,6 +66,11 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio public LocationControllerImpl(Context context, Looper bgLooper) { mContext = context; + // Initialize last active mode. If state was off use the default high accuracy mode + mLastActiveMode = getLocationCurrentState(); + if(mLastActiveMode == Settings.Secure.LOCATION_MODE_OFF) + mLastActiveMode = Settings.Secure.LOCATION_MODE_HIGH_ACCURACY; + // Register to listen for changes in location settings. IntentFilter filter = new IntentFilter(); filter.addAction(LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION); @@ -107,10 +115,17 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio return false; } final ContentResolver cr = mContext.getContentResolver(); + + // Store last active mode if we are switching off + // so we can restore it at the next enable + if(!enabled) { + mLastActiveMode = getLocationCurrentState(); + } + // When enabling location, a user consent dialog will pop up, and the // setting won't be fully enabled until the user accepts the agreement. int mode = enabled - ? Settings.Secure.LOCATION_MODE_HIGH_ACCURACY : Settings.Secure.LOCATION_MODE_OFF; + ? mLastActiveMode : Settings.Secure.LOCATION_MODE_OFF; // QuickSettings always runs as the owner, so specifically set the settings // for the current foreground user. return Settings.Secure @@ -118,6 +133,44 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio } /** + * Enable or disable location in settings to a specific mode. + * + * <p>This will attempt to enable/disable every type of location setting + * (e.g. high and balanced power). + * + * <p>If enabling, a user consent dialog will pop up prompting the user to accept. + * If the user doesn't accept, network location won't be enabled. + * + * @return true if attempt to change setting was successful. + */ + public boolean setLocationMode(int mode) { + int currentUserId = ActivityManager.getCurrentUser(); + if (isUserLocationRestricted(currentUserId)) { + return false; + } + final ContentResolver cr = mContext.getContentResolver(); + // When enabling location, a user consent dialog will pop up, and the + // setting won't be fully enabled until the user accepts the agreement. + // QuickSettings always runs as the owner, so specifically set the settings + // for the current foreground user. + return Settings.Secure.putIntForUser(cr, Settings.Secure.LOCATION_MODE, + mode, currentUserId); + } + + /** + * Returns int corresponding to current location mode in settings. + */ + public int getLocationCurrentState() { + int currentUserId = ActivityManager.getCurrentUser(); + if (isUserLocationRestricted(currentUserId)) { + return Settings.Secure.LOCATION_MODE_OFF; + } + final ContentResolver cr = mContext.getContentResolver(); + return Settings.Secure.getIntForUser(cr, Settings.Secure.LOCATION_MODE, + Settings.Secure.LOCATION_MODE_OFF, currentUserId); + } + + /** * Returns true if location isn't disabled in settings. */ public boolean isLocationEnabled() { @@ -130,6 +183,14 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio } /** + * Check if advanced location tile is enabled in settings + */ + public boolean isAdvancedSettingsEnabled() { + return CMSettings.Secure.getIntForUser(mContext.getContentResolver(), + CMSettings.Secure.QS_LOCATION_ADVANCED, 0, ActivityManager.getCurrentUser()) == 1; + } + + /** * Returns true if the current user is restricted from using location. */ private boolean isUserLocationRestricted(int userId) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java index b65bf43..f7d6f85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -19,6 +19,7 @@ import android.content.Context; import android.content.Intent; import android.net.NetworkCapabilities; import android.os.Looper; +import android.os.SystemProperties; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.SignalStrength; @@ -159,6 +160,7 @@ public class MobileSignalController extends SignalController< mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyIcons.THREE_G); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyIcons.THREE_G); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UMTS, TelephonyIcons.THREE_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_TD_SCDMA, TelephonyIcons.THREE_G); if (!mConfig.showAtLeast3G) { mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, @@ -187,12 +189,18 @@ public class MobileSignalController extends SignalController< mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSDPA, hGroup); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSUPA, hGroup); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPA, hGroup); + if (mConfig.hspaDataDistinguishable) { + hGroup = TelephonyIcons.HP; + } mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPAP, hGroup); if (mConfig.show4gForLte) { mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.FOUR_G); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA, + TelephonyIcons.FOUR_G_PLUS); } else { mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.LTE); + mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA, TelephonyIcons.LTE); } mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_IWLAN, TelephonyIcons.WFC); } @@ -231,6 +239,7 @@ public class MobileSignalController extends SignalController< int typeIcon = showDataIcon ? icons.mDataType : 0; mCallbackHandler.setMobileDataIndicators(statusIcon, qsIcon, typeIcon, qsTypeIcon, activityIn, activityOut, dataContentDescription, description, icons.mIsWide, + mCurrentState.showSeparateRoaming, mSubscriptionInfo.getSubscriptionId()); } @@ -270,13 +279,15 @@ public class MobileSignalController extends SignalController< } private boolean isRoaming() { - if (isCdma()) { + if (mServiceState == null) { + return false; + } else 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(); + return mServiceState.getRoaming(); } } @@ -371,6 +382,13 @@ public class MobileSignalController extends SignalController< mCurrentState.level = mSignalStrength.getCdmaLevel(); } else { mCurrentState.level = mSignalStrength.getLevel(); + if (mConfig.showRsrpSignalLevelforLTE) { + int dataType = mServiceState.getDataNetworkType(); + if (dataType == TelephonyManager.NETWORK_TYPE_LTE || + dataType == TelephonyManager.NETWORK_TYPE_LTE_CA) { + mCurrentState.level = getAlternateLteLevel(mSignalStrength); + } + } } } if (mNetworkToIconLookup.indexOfKey(mDataNetType) >= 0) { @@ -379,12 +397,18 @@ public class MobileSignalController extends SignalController< mCurrentState.iconGroup = mDefaultIcons; } mCurrentState.dataConnected = mCurrentState.connected - && mDataState == TelephonyManager.DATA_CONNECTED; + && mDataState == TelephonyManager.DATA_CONNECTED + && mCurrentState.dataSim; + mCurrentState.showSeparateRoaming = false; if (isCarrierNetworkChangeActive()) { mCurrentState.iconGroup = TelephonyIcons.CARRIER_NETWORK_CHANGE; } else if (isRoaming()) { - mCurrentState.iconGroup = TelephonyIcons.ROAMING; + if (SystemProperties.getBoolean("ro.config.always_show_roaming", false)) { + mCurrentState.showSeparateRoaming = true; + } else { + mCurrentState.iconGroup = TelephonyIcons.ROAMING; + } } if (isEmergencyOnly() != mCurrentState.isEmergency) { mCurrentState.isEmergency = isEmergencyOnly(); @@ -399,6 +423,21 @@ public class MobileSignalController extends SignalController< notifyListenersIfNecessary(); } + private int getAlternateLteLevel(SignalStrength signalStrength) { + int lteRsrp = signalStrength.getLteDbm(); + int rsrpLevel = SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN; + if (lteRsrp > -44) rsrpLevel = SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN; + else if (lteRsrp >= -97) rsrpLevel = SignalStrength.SIGNAL_STRENGTH_GREAT; + else if (lteRsrp >= -105) rsrpLevel = SignalStrength.SIGNAL_STRENGTH_GOOD; + else if (lteRsrp >= -113) rsrpLevel = SignalStrength.SIGNAL_STRENGTH_MODERATE; + else if (lteRsrp >= -120) rsrpLevel = SignalStrength.SIGNAL_STRENGTH_POOR; + else if (lteRsrp >= -140) rsrpLevel = SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN; + if (DEBUG) { + Log.d(mTag, "getAlternateLteLevel lteRsrp:" + lteRsrp + " rsrpLevel = " + rsrpLevel); + } + return rsrpLevel; + } + @VisibleForTesting void setActivity(int activity) { mCurrentState.activityIn = activity == TelephonyManager.DATA_ACTIVITY_INOUT @@ -440,6 +479,7 @@ public class MobileSignalController extends SignalController< + " dataState=" + state.getDataRegState()); } mServiceState = state; + mDataNetType = state.getDataNetworkType(); updateTelephony(); } @@ -501,6 +541,7 @@ public class MobileSignalController extends SignalController< boolean airplaneMode; boolean carrierNetworkChangeMode; boolean isDefault; + boolean showSeparateRoaming; @Override public void copyFrom(State s) { @@ -514,6 +555,7 @@ public class MobileSignalController extends SignalController< isEmergency = state.isEmergency; airplaneMode = state.airplaneMode; carrierNetworkChangeMode = state.carrierNetworkChangeMode; + showSeparateRoaming = state.showSeparateRoaming; } @Override @@ -528,6 +570,7 @@ public class MobileSignalController extends SignalController< builder.append("isEmergency=").append(isEmergency).append(','); builder.append("airplaneMode=").append(airplaneMode).append(','); builder.append("carrierNetworkChangeMode=").append(carrierNetworkChangeMode); + builder.append("showSeparateRoaming=").append(showSeparateRoaming); } @Override @@ -540,7 +583,8 @@ public class MobileSignalController extends SignalController< && ((MobileState) o).isEmergency == isEmergency && ((MobileState) o).airplaneMode == airplaneMode && ((MobileState) o).carrierNetworkChangeMode == carrierNetworkChangeMode - && ((MobileState) o).isDefault == isDefault; + && ((MobileState) o).isDefault == isDefault + && ((MobileState) o).showSeparateRoaming == showSeparateRoaming; } } } 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 38656ee..57b0dba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java @@ -40,7 +40,7 @@ public interface NetworkController { void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType, int qsType, boolean activityIn, boolean activityOut, String typeContentDescription, - String description, boolean isWide, int subId); + String description, boolean isWide, boolean showSeparateRoaming, int subId); void setSubs(List<SubscriptionInfo> subs); void setNoSims(boolean show); @@ -82,7 +82,7 @@ public interface NetworkController { public interface AccessPointCallback { void onAccessPointsChanged(List<AccessPoint> accessPoints); - void onSettingsActivityTriggered(Intent settingsIntent); + void onSettingsActivityTriggered(Intent intent); } } 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 2996808..7e1bf05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -245,6 +245,10 @@ public class NetworkControllerImpl extends BroadcastReceiver mCallbackHandler.setEmergencyCallsOnly(isEmergencyOnly()); } + public void removeEmergencyListener(EmergencyListener listener) { + mCallbackHandler.setListening(listener, false); + } + public boolean hasMobileDataFeature() { return mHasMobileDataFeature; } @@ -751,9 +755,11 @@ public class NetworkControllerImpl extends BroadcastReceiver datatype.equals("1x") ? TelephonyIcons.ONE_X : datatype.equals("3g") ? TelephonyIcons.THREE_G : datatype.equals("4g") ? TelephonyIcons.FOUR_G : + datatype.equals("4g+") ? TelephonyIcons.FOUR_G_PLUS : datatype.equals("e") ? TelephonyIcons.E : datatype.equals("g") ? TelephonyIcons.G : datatype.equals("h") ? TelephonyIcons.H : + datatype.equals("h+") ? TelephonyIcons.HP : datatype.equals("lte") ? TelephonyIcons.LTE : datatype.equals("roam") ? TelephonyIcons.ROAMING : TelephonyIcons.UNKNOWN; @@ -780,7 +786,7 @@ public class NetworkControllerImpl extends BroadcastReceiver private SubscriptionInfo addSignalController(int id, int simSlotIndex) { SubscriptionInfo info = new SubscriptionInfo(id, "", simSlotIndex, "", "", 0, 0, "", 0, - null, 0, 0, ""); + null, 0, 0, "", 0); mMobileSignalControllers.put(id, new MobileSignalController(mContext, mConfig, mHasMobileDataFeature, mPhone, mCallbackHandler, this, info, mSubDefaults, mReceiverHandler.getLooper())); @@ -825,6 +831,7 @@ public class NetworkControllerImpl extends BroadcastReceiver boolean alwaysShowCdmaRssi = false; boolean show4gForLte = false; boolean hspaDataDistinguishable; + boolean showRsrpSignalLevelforLTE; static Config readConfig(Context context) { Config config = new Config(); @@ -836,6 +843,8 @@ public class NetworkControllerImpl extends BroadcastReceiver config.show4gForLte = res.getBoolean(R.bool.config_show4GForLTE); config.hspaDataDistinguishable = res.getBoolean(R.bool.config_hspa_data_distinguishable); + config.showRsrpSignalLevelforLTE = + res.getBoolean(R.bool.config_showRsrpSignalLevelforLTE); return config; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java index c3bcd94..e54df32 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java @@ -64,7 +64,7 @@ public final class RotationLockControllerImpl implements RotationLockController } public boolean isRotationLockAffordanceVisible() { - return RotationPolicy.isRotationLockToggleVisible(mContext); + return RotationPolicy.isRotationSupported(mContext); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalCallbackAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalCallbackAdapter.java index dce889f..f13ef9f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalCallbackAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalCallbackAdapter.java @@ -37,7 +37,7 @@ public class SignalCallbackAdapter implements SignalCallback { @Override public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType, int qsType, boolean activityIn, boolean activityOut, String typeContentDescription, - String description, boolean isWide, int subId) { + String description, boolean isWide, boolean showSeparateRoaming, int subId) { } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java index 5e9447e..72e3a06 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java @@ -256,6 +256,7 @@ public abstract class SignalController<T extends SignalController.State, IconGroup iconGroup; int inetCondition; int rssi; // Only for logging. + boolean showSeparateRoaming; // Not used for comparison, just used for logging. long time; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SuController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SuController.java new file mode 100644 index 0000000..de67261 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SuController.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 java.util.List; + +public interface SuController { + void addCallback(Callback callback); + void removeCallback(Callback callback); + boolean hasActiveSessions(); + List<String> getPackageNamesWithActiveSuSessions(); + + public interface Callback { + void onSuSessionsChanged(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SuControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SuControllerImpl.java new file mode 100644 index 0000000..c663bab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SuControllerImpl.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import android.app.AppOpsManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.UserHandle; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * A controller to manage changes to superuser-related states and update the views accordingly. + */ +public class SuControllerImpl implements SuController { + private static final String TAG = "SuControllerImpl"; + + private static final int[] mSuOpArray = new int[] {AppOpsManager.OP_SU}; + + private ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); + + private AppOpsManager mAppOpsManager; + + private List<String> mActiveSuSessions = new ArrayList<>(); + + public SuControllerImpl(Context context) { + mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(AppOpsManager.ACTION_SU_SESSION_CHANGED); + context.registerReceiverAsUser(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Got change"); + String action = intent.getAction(); + if (AppOpsManager.ACTION_SU_SESSION_CHANGED.equals(action)) { + updateActiveSuSessions(); + } + } + }, UserHandle.ALL, intentFilter, null, new Handler()); + + updateActiveSuSessions(); + } + + @Override + public void addCallback(Callback callback) { + mCallbacks.add(callback); + fireCallback(callback); + } + + @Override + public void removeCallback(Callback callback) { + mCallbacks.remove(callback); + } + + @Override + public boolean hasActiveSessions() { + synchronized (mActiveSuSessions) { + return mActiveSuSessions.size() > 0; + } + } + + private void fireCallback(Callback callback) { + callback.onSuSessionsChanged(); + } + + private void fireCallbacks() { + for (Callback callback : mCallbacks) { + callback.onSuSessionsChanged(); + } + } + + // Return the list of package names that currently have an active su session + @Override + public List<String> getPackageNamesWithActiveSuSessions() { + List<String> packageNames = new ArrayList<>(); + List<AppOpsManager.PackageOps> packages + = mAppOpsManager.getPackagesForOps(mSuOpArray); + // AppOpsManager can return null when there is no requested data. + if (packages != null) { + final int numPackages = packages.size(); + for (int packageInd = 0; packageInd < numPackages; packageInd++) { + AppOpsManager.PackageOps packageOp = packages.get(packageInd); + List<AppOpsManager.OpEntry> opEntries = packageOp.getOps(); + if (opEntries != null) { + final int numOps = opEntries.size(); + for (int opInd = 0; opInd < numOps; opInd++) { + AppOpsManager.OpEntry opEntry = opEntries.get(opInd); + if (opEntry.getOp() == AppOpsManager.OP_SU) { + if (opEntry.isRunning()) { + packageNames.add(packageOp.getPackageName()); + break; + } + } + } + } + } + } + + return packageNames; + } + + private synchronized void updateActiveSuSessions() { + List<String> newList = getPackageNamesWithActiveSuSessions(); + synchronized (mActiveSuSessions) { + if (!newList.equals(mActiveSuSessions)) { + mActiveSuSessions.clear(); + mActiveSuSessions.addAll(newList); + fireCallbacks(); + } + } + } +} 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 83e0446..d770681 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/TelephonyIcons.java @@ -153,6 +153,20 @@ class TelephonyIcons { static final int QS_DATA_H = R.drawable.ic_qs_signal_h; + //HSPA+ + static final int[][] DATA_HP = { + { R.drawable.stat_sys_data_fully_connected_hp, + R.drawable.stat_sys_data_fully_connected_hp, + R.drawable.stat_sys_data_fully_connected_hp, + R.drawable.stat_sys_data_fully_connected_hp }, + { R.drawable.stat_sys_data_fully_connected_hp, + R.drawable.stat_sys_data_fully_connected_hp, + R.drawable.stat_sys_data_fully_connected_hp, + R.drawable.stat_sys_data_fully_connected_hp } + }; + + static final int QS_DATA_HP = R.drawable.ic_qs_signal_hp; + //CDMA // Use 3G icons for EVDO data and 1x icons for 1XRTT data static final int[][] DATA_1X = { @@ -182,6 +196,19 @@ class TelephonyIcons { static final int QS_DATA_4G = R.drawable.ic_qs_signal_4g; + static final int[][] DATA_4G_PLUS = { + { R.drawable.stat_sys_data_fully_connected_4g_plus, + R.drawable.stat_sys_data_fully_connected_4g_plus, + R.drawable.stat_sys_data_fully_connected_4g_plus, + R.drawable.stat_sys_data_fully_connected_4g_plus }, + { R.drawable.stat_sys_data_fully_connected_4g_plus, + R.drawable.stat_sys_data_fully_connected_4g_plus, + R.drawable.stat_sys_data_fully_connected_4g_plus, + R.drawable.stat_sys_data_fully_connected_4g_plus } + }; + + static final int QS_DATA_4G_PLUS = R.drawable.ic_qs_signal_4g_plus; + // LTE branded "LTE" static final int[][] DATA_LTE = { { R.drawable.stat_sys_data_fully_connected_lte, @@ -202,8 +229,10 @@ class TelephonyIcons { static final int ICON_G = R.drawable.stat_sys_data_fully_connected_g; static final int ICON_E = R.drawable.stat_sys_data_fully_connected_e; static final int ICON_H = R.drawable.stat_sys_data_fully_connected_h; + static final int ICON_HP = R.drawable.stat_sys_data_fully_connected_hp; static final int ICON_3G = R.drawable.stat_sys_data_fully_connected_3g; static final int ICON_4G = R.drawable.stat_sys_data_fully_connected_4g; + static final int ICON_4G_PLUS = R.drawable.stat_sys_data_fully_connected_4g_plus; static final int ICON_1X = R.drawable.stat_sys_data_fully_connected_1x; static final int ICON_CARRIER_NETWORK_CHANGE = R.drawable.stat_sys_signal_carrier_network_change_animation; @@ -211,6 +240,7 @@ class TelephonyIcons { 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_4G_PLUS = R.drawable.ic_qs_signal_4g_plus; 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; @@ -323,12 +353,27 @@ class TelephonyIcons { TelephonyIcons.TELEPHONY_NO_NETWORK, TelephonyIcons.QS_TELEPHONY_NO_NETWORK, AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], - R.string.accessibility_data_connection_3_5g, + R.string.accessibility_data_connection_hspa, TelephonyIcons.ICON_H, false, TelephonyIcons.QS_DATA_H ); + static final MobileIconGroup HP = new MobileIconGroup( + "HP", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0, 0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_hspap, + TelephonyIcons.ICON_HP, + false, + TelephonyIcons.QS_DATA_HP + ); + static final MobileIconGroup FOUR_G = new MobileIconGroup( "4G", TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, @@ -344,6 +389,21 @@ class TelephonyIcons { TelephonyIcons.QS_DATA_4G ); + static final MobileIconGroup FOUR_G_PLUS = new MobileIconGroup( + "4G+", + TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, + TelephonyIcons.QS_TELEPHONY_SIGNAL_STRENGTH, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + 0,0, + TelephonyIcons.TELEPHONY_NO_NETWORK, + TelephonyIcons.QS_TELEPHONY_NO_NETWORK, + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], + R.string.accessibility_data_connection_4g_plus, + TelephonyIcons.ICON_4G_PLUS, + true, + TelephonyIcons.QS_DATA_4G_PLUS + ); + static final MobileIconGroup LTE = new MobileIconGroup( "LTE", TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java index a8d4f13..58168e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java @@ -54,6 +54,7 @@ public final class UserInfoController { private boolean mUseDefaultAvatar; private String mUserName; private Drawable mUserDrawable; + private boolean mProfileSetup; public UserInfoController(Context context) { mContext = context; @@ -73,6 +74,10 @@ public final class UserInfoController { mCallbacks.add(callback); } + public void removeListener (OnUserInfoChangedListener callback) { + mCallbacks.remove(callback); + } + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -158,24 +163,23 @@ public final class UserInfoController { mUseDefaultAvatar = true; } - // If it's a single-user device, get the profile name, since the nickname is not - // usually valid - if (um.getUsers().size() <= 1) { - // Try and read the display name from the local profile - final Cursor cursor = context.getContentResolver().query( - ContactsContract.Profile.CONTENT_URI, new String[] { - ContactsContract.CommonDataKinds.Phone._ID, - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME - }, null, null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - name = cursor.getString(cursor.getColumnIndex( - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); - } - } finally { - cursor.close(); + mProfileSetup = false; + + // Try and read the display name from the local profile + final Cursor cursor = context.getContentResolver().query( + ContactsContract.Profile.CONTENT_URI, new String[] { + ContactsContract.CommonDataKinds.Phone._ID, + ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + }, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + mProfileSetup = true; + name = cursor.getString(cursor.getColumnIndex( + ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); } + } finally { + cursor.close(); } } return new Pair<String, Drawable>(name, avatar); @@ -198,6 +202,10 @@ public final class UserInfoController { } } + public boolean isProfileSetup() { + return mProfileSetup; + } + public interface OnUserInfoChangedListener { public void onUserInfoChanged(String name, Drawable picture); } 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 3e8d4e9..38fb275 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -53,6 +53,7 @@ import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.tiles.UserDetailView; import com.android.systemui.statusbar.phone.SystemUIDialog; +import cyanogenmod.app.StatusBarPanelCustomTile; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -587,6 +588,11 @@ public class UserSwitcherController { } @Override + public StatusBarPanelCustomTile getCustomTile() { + return null; + } + + @Override public Boolean getToggleState() { return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WeatherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WeatherController.java new file mode 100644 index 0000000..0f71bcc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WeatherController.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +public interface WeatherController { + void addCallback(Callback callback); + void removeCallback(Callback callback); + WeatherInfo getWeatherInfo(); + + public interface Callback { + void onWeatherChanged(WeatherInfo temp); + } + public static class WeatherInfo { + public double temp = Double.NaN; + public String city = null; + public String condition = null; + public int tempUnit; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WeatherControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WeatherControllerImpl.java new file mode 100644 index 0000000..1a798f0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WeatherControllerImpl.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.util.Log; +import cyanogenmod.providers.CMSettings; +import cyanogenmod.providers.WeatherContract; +import cyanogenmod.weather.CMWeatherManager; +import cyanogenmod.weather.util.WeatherUtils; + +import java.util.ArrayList; + +import static cyanogenmod.providers.WeatherContract.WeatherColumns.CURRENT_CITY; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.CURRENT_CONDITION; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.CURRENT_TEMPERATURE; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.CURRENT_TEMPERATURE_UNIT; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.TempUnit.CELSIUS; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT; + +public class WeatherControllerImpl implements WeatherController { + + private static final String TAG = WeatherController.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private WeatherContentObserver mWeatherContentObserver; + private Handler mHandler; + private int mWeatherUnit; + private Uri mWeatherTempetarureUri; + + public static final ComponentName COMPONENT_WEATHER_FORECAST = new ComponentName( + "com.cyanogenmod.lockclock", "com.cyanogenmod.lockclock.weather.ForecastActivity"); + public static final String ACTION_FORCE_WEATHER_UPDATE + = "com.cyanogenmod.lockclock.action.FORCE_WEATHER_UPDATE"; + private static final String[] WEATHER_PROJECTION = new String[]{ + CURRENT_TEMPERATURE, + CURRENT_TEMPERATURE_UNIT, + CURRENT_CITY, + CURRENT_CONDITION + }; + + private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); + private final Context mContext; + + private WeatherInfo mCachedInfo = new WeatherInfo(); + + public WeatherControllerImpl(Context context) { + mContext = context; + mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + mHandler = new Handler(); + mWeatherContentObserver = new WeatherContentObserver(mHandler); + mWeatherTempetarureUri + = CMSettings.Global.getUriFor(CMSettings.Global.WEATHER_TEMPERATURE_UNIT); + mContext.getContentResolver().registerContentObserver( + WeatherContract.WeatherColumns.CURRENT_WEATHER_URI,true, mWeatherContentObserver); + mContext.getContentResolver().registerContentObserver(mWeatherTempetarureUri, true, + mWeatherContentObserver); + queryWeatherTempUnit(); + queryWeather(); + } + + public void addCallback(Callback callback) { + if (callback == null || mCallbacks.contains(callback)) return; + if (DEBUG) Log.d(TAG, "addCallback " + callback); + mCallbacks.add(callback); + callback.onWeatherChanged(mCachedInfo); // immediately update with current values + } + + public void removeCallback(Callback callback) { + if (callback == null) return; + if (DEBUG) Log.d(TAG, "removeCallback " + callback); + mCallbacks.remove(callback); + } + + @Override + public WeatherInfo getWeatherInfo() { + return mCachedInfo; + } + + private void queryWeather() { + Cursor c = mContext.getContentResolver().query( + WeatherContract.WeatherColumns.CURRENT_WEATHER_URI, WEATHER_PROJECTION, + null, null, null); + if (c == null) { + if(DEBUG) Log.e(TAG, "cursor was null for temperature, forcing weather update"); + //LockClock keeps track of the user settings (temp unit, search by geo location/city) + //so we delegate the responsibility of handling a weather update to LockClock + mContext.sendBroadcast(new Intent(ACTION_FORCE_WEATHER_UPDATE)); + } else { + try { + c.moveToFirst(); + double temp = c.getDouble(0); + int reportedUnit = c.getInt(1); + if (reportedUnit == CELSIUS && mWeatherUnit == FAHRENHEIT) { + temp = WeatherUtils.celsiusToFahrenheit(temp); + } else if (reportedUnit == FAHRENHEIT && mWeatherUnit == CELSIUS) { + temp = WeatherUtils.fahrenheitToCelsius(temp); + } + + mCachedInfo.temp = temp; + mCachedInfo.tempUnit = mWeatherUnit; + mCachedInfo.city = c.getString(2); + mCachedInfo.condition = c.getString(3); + } finally { + c.close(); + } + } + } + + private void fireCallback() { + for (Callback callback : mCallbacks) { + callback.onWeatherChanged(mCachedInfo); + } + } + + private class WeatherContentObserver extends ContentObserver { + + public WeatherContentObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (uri != null) { + if (uri.compareTo(WeatherContract.WeatherColumns.CURRENT_WEATHER_URI) == 0) { + queryWeather(); + fireCallback(); + } else if (uri.compareTo(mWeatherTempetarureUri) == 0) { + queryWeatherTempUnit(); + fixCachedWeatherInfo(); + fireCallback(); + } else { + super.onChange(selfChange, uri); + } + } + } + + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } + } + + private void queryWeatherTempUnit() { + try { + mWeatherUnit = CMSettings.Global.getInt(mContext.getContentResolver(), + CMSettings.Global.WEATHER_TEMPERATURE_UNIT); + } catch (CMSettings.CMSettingNotFoundException e) { + //CMSettingsProvider should have taken care of setting a default value for this setting + //so how is that we ended up here?? We need to set a valid temp unit anyway to keep + //this going + mWeatherUnit = WeatherContract.WeatherColumns.TempUnit.CELSIUS; + } + } + + private void fixCachedWeatherInfo() { + if (mCachedInfo.tempUnit == CELSIUS && mWeatherUnit == FAHRENHEIT) { + mCachedInfo.temp = WeatherUtils.celsiusToFahrenheit(mCachedInfo.temp); + mCachedInfo.tempUnit = FAHRENHEIT; + } else if (mCachedInfo.tempUnit == FAHRENHEIT && mWeatherUnit == CELSIUS) { + mCachedInfo.temp = WeatherUtils.fahrenheitToCelsius(mCachedInfo.temp); + mCachedInfo.tempUnit = CELSIUS; + } + } +} 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 af38f5f..b6e131a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -50,9 +50,12 @@ 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.phone.ViewLinker; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.ScrollAdapter; +import cyanogenmod.power.PerformanceManager; + import java.util.ArrayList; import java.util.HashSet; @@ -61,7 +64,8 @@ import java.util.HashSet; */ public class NotificationStackScrollLayout extends ViewGroup implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter, - ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener { + ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener, + ViewLinker.ViewLinkerParent { private static final String TAG = "NotificationStackScrollLayout"; private static final boolean DEBUG = false; @@ -208,6 +212,7 @@ public class NotificationStackScrollLayout extends ViewGroup private boolean mDelegateToScrollView; private boolean mDisallowScrollingInThisMotion; private long mGoToFullShadeDelay; + private ViewTreeObserver.OnPreDrawListener mChildrenUpdater = new ViewTreeObserver.OnPreDrawListener() { @Override @@ -231,6 +236,7 @@ public class NotificationStackScrollLayout extends ViewGroup private boolean mForceNoOverlappingRendering; private NotificationOverflowContainer mOverflowContainer; private final ArrayList<Pair<ExpandableNotificationRow, Boolean>> mTmpList = new ArrayList<>(); + private ViewLinker.ViewLinkerCallback mLinkerCallback; public NotificationStackScrollLayout(Context context) { this(context, null); @@ -839,6 +845,7 @@ public class NotificationStackScrollLayout extends ViewGroup && !mOnlyScrollingInThisMotion) { horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); } + return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev); } @@ -2875,6 +2882,23 @@ public class NotificationStackScrollLayout extends ViewGroup return !mForceNoOverlappingRendering && super.hasOverlappingRendering(); } + @Override + public void registerLinker(ViewLinker.ViewLinkerCallback callback) { + mLinkerCallback = callback; + } + + @Override + public void setAlpha(float alpha) { + super.setAlpha(alpha); + mLinkerCallback.onAlphaChanged(alpha); + } + + @Override + public void setTranslationX(float translationX) { + super.setTranslationX(translationX); + mLinkerCallback.onTranslationXChanged(translationX); + } + /** * A listener that is notified when some child locations might have changed. */ 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 cf696a1..93995f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java @@ -42,6 +42,8 @@ public class StackScrollAlgorithm { private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3; private static final int MAX_ITEMS_IN_TOP_STACK = 3; + private static final int DEFAULT_CORNER_RADIUS = 2; + public static final float DIMMED_SCALE = 0.95f; private int mPaddingBetweenElements; @@ -72,6 +74,7 @@ public class StackScrollAlgorithm { private int mMaxNotificationHeight; private boolean mScaleDimmed; private HeadsUpManager mHeadsUpManager; + private boolean mPerformClipping; public StackScrollAlgorithm(Context context) { initConstants(context); @@ -128,6 +131,12 @@ public class StackScrollAlgorithm { R.dimen.notification_collapse_second_card_padding); mScaleDimmed = context.getResources().getDisplayMetrics().densityDpi >= DisplayMetrics.DENSITY_420; + + // We don't want to clip the notification if a theme overrides the corner radius with + // a value larger than the default. + mPerformClipping = context.getResources() + .getDimension(R.dimen.notification_material_rounded_rect_radius) <= + DEFAULT_CORNER_RADIUS * context.getResources().getDisplayMetrics().density; } public boolean shouldScaleDimmed() { @@ -228,8 +237,7 @@ public class StackScrollAlgorithm { // In the unlocked shade we have to clip a little bit higher because of the rounded // corners of the notifications, but only if we are not fully overlapped by // the top card. - float clippingCorrection = state.dimmed - ? 0 + float clippingCorrection = state.dimmed ? (mPerformClipping ? 0 : newHeight) : mRoundedRectCornerRadius * state.scale; clipHeight += clippingCorrection; } diff --git a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java index a2b062c..0ae34bf 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java @@ -155,7 +155,7 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference getContext().sendBroadcast(intent); intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_CLOCK); - intent.putExtra("hhmm", "0600"); + intent.putExtra("hhmm", "1230"); getContext().sendBroadcast(intent); intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_NETWORK); diff --git a/packages/SystemUI/src/com/android/systemui/tuner/QsTuner.java b/packages/SystemUI/src/com/android/systemui/tuner/QsTuner.java index 37ac098..53fbef7 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/QsTuner.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/QsTuner.java @@ -15,14 +15,23 @@ */ package com.android.systemui.tuner; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Fragment; import android.content.ClipData; +import android.content.ClipDescription; import android.content.Context; import android.content.DialogInterface; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.provider.Settings.Secure; +import android.support.v4.view.ViewPager; import android.text.TextUtils; import android.util.Log; import android.view.DragEvent; @@ -30,11 +39,9 @@ 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; @@ -42,6 +49,8 @@ import android.widget.ScrollView; import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; +import com.android.systemui.qs.QSDragPanel; +import com.android.systemui.qs.QSPage; import com.android.systemui.qs.QSPanel; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QSTile.Host.Callback; @@ -52,6 +61,7 @@ import com.android.systemui.statusbar.phone.QSTileHost; import com.android.systemui.statusbar.policy.SecurityController; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class QsTuner extends Fragment implements Callback { @@ -59,16 +69,13 @@ public class QsTuner extends Fragment implements Callback { private static final String TAG = "QsTuner"; private static final int MENU_RESET = Menu.FIRST; + private static final int MENU_EDIT = Menu.FIRST + 1; private DraggableQsPanel mQsPanel; private CustomHost mTileHost; - private FrameLayout mDropTarget; - private ScrollView mScrollRoot; - private FrameLayout mAddTarget; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -78,6 +85,7 @@ public class QsTuner extends Fragment implements Callback { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.add(0, MENU_RESET, 0, com.android.internal.R.string.reset); + menu.add(0, MENU_EDIT, 0, "toggle edit"); } public void onResume() { @@ -93,8 +101,11 @@ public class QsTuner extends Fragment implements Callback { @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { + case MENU_EDIT: + mQsPanel.setEditing(!mQsPanel.isEditing()); + break; case MENU_RESET: - mTileHost.reset(); + mTileHost.resetTiles(); break; case android.R.id.home: getFragmentManager().popBackStack(); @@ -105,7 +116,7 @@ public class QsTuner extends Fragment implements Callback { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + Bundle savedInstanceState) { mScrollRoot = (ScrollView) inflater.inflate(R.layout.tuner_qs, container, false); mQsPanel = new DraggableQsPanel(getContext()); @@ -116,10 +127,6 @@ public class QsTuner extends Fragment implements Callback { 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; } @@ -129,89 +136,38 @@ public class QsTuner extends Fragment implements Callback { 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(); - } - }); + @Override + public void onTilesChanged() { + mQsPanel.setTiles(mTileHost.getTiles()); } - public void onStartDrag() { - mDropTarget.post(new Runnable() { - @Override - public void run() { - mDropTarget.setVisibility(View.VISIBLE); - mAddTarget.setVisibility(View.GONE); - } - }); + @Override + public void setEditing(boolean editing) { + mQsPanel.setEditing(editing); } - public void stopDrag() { - mDropTarget.post(new Runnable() { - @Override - public void run() { - mDropTarget.setVisibility(View.GONE); - mAddTarget.setVisibility(View.VISIBLE); - } - }); + @Override + public boolean isEditing() { + return mTileHost.isEditing(); } @Override - public void onTilesChanged() { - mQsPanel.setTiles(mTileHost.getTiles()); + public void goToSettingsPage() { } - 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; + @Override + public void resetTiles() { } 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()); + null, null, new BlankSecurityController(), null); } @Override - protected QSTile<?> createTile(String tileSpec) { + public QSTile<?> createTile(String tileSpec) { return new DraggableTile(this, tileSpec); } @@ -232,97 +188,6 @@ public class QsTuner extends Fragment implements Callback { setTiles(order); } - public void remove(String tile) { - MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_REMOVE, tile); - List<String> tiles = new ArrayList<>(mTileSpecs); - tiles.remove(tile); - setTiles(tiles); - } - - public void add(String tile) { - MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_ADD, 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() { @@ -373,8 +238,7 @@ public class QsTuner extends Fragment implements Callback { } } - private static class DraggableTile extends QSTile<QSTile.State> - implements DropListener { + public static class DraggableTile extends QSTile<QSTile.State> { private String mSpec; private QSTileView mView; @@ -391,7 +255,7 @@ public class QsTuner extends Fragment implements Callback { } @Override - public boolean supportsDualTargets() { + public boolean hasDualTargetsDetails() { return "wifi".equals(mSpec) || "bt".equals(mSpec); } @@ -416,7 +280,7 @@ public class QsTuner extends Fragment implements Callback { } private String getLabel() { - int resource = getLabelResource(mSpec); + int resource = QSTileHost.getLabelResource(mSpec); if (resource != 0) { return mContext.getString(resource); } @@ -443,6 +307,7 @@ public class QsTuner extends Fragment implements Callback { 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; + else if (mSpec.equals("adb_network")) return R.drawable.ic_qs_network_adb_on; return R.drawable.android; } @@ -460,81 +325,19 @@ public class QsTuner extends Fragment implements Callback { } @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 String toString() { + return mSpec; } - } - public interface DropListener { - void onDrop(String sourceText); - } + private class DraggableQsPanel extends QSDragPanel { - 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); - } - } + setEditing(true); } - @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/StatusBarIconBlacklistFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/StatusBarIconBlacklistFragment.java new file mode 100644 index 0000000..c339541 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tuner/StatusBarIconBlacklistFragment.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.annotation.Nullable; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; + +import android.preference.PreferenceGroup; +import com.android.systemui.R; +import com.android.systemui.statusbar.phone.StatusBarIconController; + +public class StatusBarIconBlacklistFragment extends PreferenceFragment { + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.tuner_statusbar_icons); + } + + @Override + public void onResume() { + super.onResume(); + registerPrefs(getPreferenceScreen()); + } + + @Override + public void onPause() { + super.onPause(); + 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((TunerService.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 TunerService.Tunable) { + tunerService.removeTunable((TunerService.Tunable) pref); + } else if (pref instanceof PreferenceGroup) { + registerPrefs((PreferenceGroup) pref); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java index c84f618..401fb0e 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java @@ -16,7 +16,9 @@ package com.android.systemui.tuner; import android.app.Activity; +import android.app.Fragment; import android.os.Bundle; +import android.view.MenuItem; public class TunerActivity extends Activity { @@ -27,4 +29,43 @@ public class TunerActivity extends Activity { .commit(); } + /** + * Base class for direct entry points into + * tuner fragments + */ + private static abstract class FragmentTunerActivityBase extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getActionBar().setDisplayHomeAsUpEnabled(true); + getFragmentManager().beginTransaction().replace(android.R.id.content, + getFragment()).commit(); + } + + protected abstract Fragment getFragment(); + + @Override + public final boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + } + + public static final class DemoModeActivity extends FragmentTunerActivityBase { + @Override + protected Fragment getFragment() { + return new DemoModeFragment(); + } + } + + public static final class StatusBarIconActivity extends FragmentTunerActivityBase { + @Override + protected Fragment getFragment() { + return new StatusBarIconBlacklistFragment(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java index 71b5de5..0bc663b 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java @@ -15,8 +15,6 @@ */ package com.android.systemui.tuner; -import static com.android.systemui.BatteryMeterView.SHOW_PERCENT_SETTING; - import android.app.AlertDialog; import android.app.FragmentTransaction; import android.content.DialogInterface; @@ -46,18 +44,13 @@ public class TunerFragment extends PreferenceFragment { private static final String TAG = "TunerFragment"; - private static final String KEY_QS_TUNER = "qs_tuner"; + private static final String KEY_STATUSBAR_BLACKLIST = "statusbar_icon_blacklist"; private static final String KEY_DEMO_MODE = "demo_mode"; - private static final String KEY_BATTERY_PCT = "battery_pct"; public static final String SETTING_SEEN_TUNER_WARNING = "seen_tuner_warning"; private static final int MENU_REMOVE = Menu.FIRST + 1; - private final SettingObserver mSettingObserver = new SettingObserver(); - - private SwitchPreference mBatteryPct; - public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -65,11 +58,12 @@ public class TunerFragment extends PreferenceFragment { getActivity().getActionBar().setDisplayHomeAsUpEnabled(true); setHasOptionsMenu(true); - findPreference(KEY_QS_TUNER).setOnPreferenceClickListener(new OnPreferenceClickListener() { + findPreference(KEY_STATUSBAR_BLACKLIST).setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { FragmentTransaction ft = getFragmentManager().beginTransaction(); - ft.replace(android.R.id.content, new QsTuner(), "QsTuner"); + ft.replace(android.R.id.content, new StatusBarIconBlacklistFragment(), + "StatusBarBlacklist"); ft.addToBackStack(null); ft.commit(); return true; @@ -85,7 +79,6 @@ public class TunerFragment extends PreferenceFragment { return true; } }); - mBatteryPct = (SwitchPreference) findPreference(KEY_BATTERY_PCT); if (Settings.Secure.getInt(getContext().getContentResolver(), SETTING_SEEN_TUNER_WARNING, 0) == 0) { new AlertDialog.Builder(getContext()) @@ -104,9 +97,6 @@ public class TunerFragment extends PreferenceFragment { @Override public void onResume() { super.onResume(); - updateBatteryPct(); - getContext().getContentResolver().registerContentObserver( - System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver); registerPrefs(getPreferenceScreen()); MetricsLogger.visibility(getContext(), MetricsLogger.TUNER, true); @@ -115,7 +105,6 @@ public class TunerFragment extends PreferenceFragment { @Override public void onPause() { super.onPause(); - getContext().getContentResolver().unregisterContentObserver(mSettingObserver); unregisterPrefs(getPreferenceScreen()); MetricsLogger.visibility(getContext(), MetricsLogger.TUNER, false); @@ -169,33 +158,4 @@ public class TunerFragment extends PreferenceFragment { } 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; - MetricsLogger.action(getContext(), MetricsLogger.TUNER_BATTERY_PERCENTAGE, v); - 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 index 50234b2..3e7477c 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java @@ -31,6 +31,7 @@ import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; import android.util.ArrayMap; import com.android.systemui.BatteryMeterView; @@ -40,6 +41,7 @@ import com.android.systemui.SystemUI; import com.android.systemui.SystemUIApplication; import com.android.systemui.settings.CurrentUserTracker; import com.android.systemui.statusbar.phone.SystemUIDialog; +import cyanogenmod.providers.CMSettings; import java.util.ArrayList; import java.util.HashMap; @@ -84,17 +86,31 @@ public class TunerService extends SystemUI { } private void addTunable(Tunable tunable, String key) { + addTunableByProvider(tunable, key, false); + } + + public void addTunableByProvider(Tunable tunable, String key, boolean cm) { if (!mTunableLookup.containsKey(key)) { mTunableLookup.put(key, new ArrayList<Tunable>()); } mTunableLookup.get(key).add(tunable); - Uri uri = Settings.Secure.getUriFor(key); + Uri uri; + if (!cm) { + uri = Settings.Secure.getUriFor(key); + } else { + uri = CMSettings.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); + String value; + if (cm) { + value = CMSettings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); + } else { + value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); + } tunable.onTuningChanged(key, value); } @@ -116,7 +132,18 @@ public class TunerService extends SystemUI { public void reloadSetting(Uri uri) { String key = mListeningUris.get(uri); - String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); + + // Handle possible null keys + if (TextUtils.isEmpty(key)) { + return; + } + + String value; + if (uri.getAuthority().equals(CMSettings.AUTHORITY)) { + value = CMSettings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); + } else { + value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); + } for (Tunable tunable : mTunableLookup.get(key)) { tunable.onTuningChanged(key, value); } @@ -124,8 +151,14 @@ public class TunerService extends SystemUI { private void reloadAll() { for (String key : mTunableLookup.keySet()) { - String value = Settings.Secure.getStringForUser(mContentResolver, key, - mCurrentUser); + String value; + Uri uri = CMSettings.Secure.getUriFor(key); + if (uri.getAuthority() != null && uri.getAuthority().equals(CMSettings.AUTHORITY)) { + value = CMSettings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); + } else { + value = Settings.Secure.getStringForUser(mContentResolver, key, + mCurrentUser); + } for (Tunable tunable : mTunableLookup.get(key)) { tunable.onTuningChanged(key, value); } @@ -135,13 +168,17 @@ public class TunerService extends SystemUI { public void clearAll() { // A couple special cases. Settings.Global.putString(mContentResolver, DemoMode.DEMO_MODE_ALLOWED, null); - Settings.System.putString(mContentResolver, BatteryMeterView.SHOW_PERCENT_SETTING, null); Intent intent = new Intent(DemoMode.ACTION_DEMO); intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_EXIT); mContext.sendBroadcast(intent); for (String key : mTunableLookup.keySet()) { - Settings.Secure.putString(mContentResolver, key, null); + Uri uri = CMSettings.Secure.getUriFor(key); + if (uri.getAuthority() != null && uri.getAuthority().equals(CMSettings.AUTHORITY)) { + CMSettings.Secure.putString(mContentResolver, key, null); + } else { + Settings.Secure.putString(mContentResolver, key, null); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java index 180d918..baa5321 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java +++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java @@ -256,6 +256,11 @@ public class StorageNotification extends SystemUI { } private void onPublicVolumeStateChangedInternal(VolumeInfo vol) { + // Do not notify for volumes on non-removable disks + if (vol.disk.isNonRemovable()) { + return; + } + Log.d(TAG, "Notifying about public volume: " + vol.toString()); final Notification notif; @@ -322,7 +327,7 @@ public class StorageNotification extends SystemUI { // 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()) { + if (rec == null || (rec.isSnoozed() && disk.isAdoptable())) { return null; } @@ -359,6 +364,11 @@ public class StorageNotification extends SystemUI { .setContentIntent(browseIntent) .setCategory(Notification.CATEGORY_SYSTEM) .setPriority(Notification.PRIORITY_LOW); + // USB disks notification can be persistent + if (disk.isUsb()) { + builder.setOngoing(mContext.getResources().getBoolean( + R.bool.config_persistUsbDriveNotification)); + } // Non-adoptable disks can't be snoozed. if (disk.isAdoptable()) { builder.setDeleteIntent(buildSnoozeIntent(vol.getFsUuid())); diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java index e9f1095..9fda531 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java @@ -31,6 +31,7 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; import android.graphics.PixelFormat; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.ColorDrawable; @@ -42,6 +43,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; +import android.provider.Settings; import android.provider.Settings.Global; import android.util.DisplayMetrics; import android.util.Log; @@ -104,7 +106,6 @@ public class VolumeDialog { private final SpTexts mSpTexts; private final SparseBooleanArray mDynamic = new SparseBooleanArray(); private final KeyguardManager mKeyguard; - private final AudioManager mAudioManager; private final int mExpandButtonAnimationDuration; private final ZenFooter mZenFooter; private final LayoutTransition mLayoutTransition; @@ -128,15 +129,15 @@ public class VolumeDialog { private boolean mPendingStateChanged; private boolean mPendingRecheckAll; private long mCollapseTime; + private int mLastActiveStream; public VolumeDialog(Context context, int windowType, VolumeDialogController controller, - ZenModeController zenModeController, Callback callback) { + ZenModeController zenModeController, Callback callback) { mContext = context; mController = controller; mCallback = callback; mSpTexts = new SpTexts(mContext); mKeyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); - mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mDialog = new CustomDialog(mContext); @@ -163,7 +164,7 @@ public class VolumeDialog { window.setAttributes(lp); window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); - mActiveSliderTint = loadColorStateList(R.color.system_accent_color); + mActiveSliderTint = loadColorStateList(R.color.volume_slider_active); mInactiveSliderTint = loadColorStateList(R.color.volume_slider_inactive); mDialog.setContentView(R.layout.volume_dialog); mDialogView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog); @@ -177,19 +178,19 @@ public class VolumeDialog { 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; - } - } - }); + @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); @@ -272,11 +273,15 @@ public class VolumeDialog { 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; + int oldLeft, int oldTop, int oldRight, int oldBottom) { + final boolean moved = mLastActiveStream != mActiveStream || + 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()); + + "," + mLastActiveStream + + " new=" + new Rect(left,top,right,bottom).toShortString() + + "," + mActiveStream); + mLastActiveStream = mActiveStream; if (moved) { for (int i = 0; i < mDialogContentView.getChildCount(); i++) { final View c = mDialogContentView.getChildAt(i); @@ -362,6 +367,8 @@ public class VolumeDialog { 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.setProgressTintMode(PorterDuff.Mode.SRC_ATOP); + row.slider.setThumbTintMode(PorterDuff.Mode.SRC_ATOP); row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); // forward events above the slider into the slider @@ -597,13 +604,17 @@ public class VolumeDialog { 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); + removeRow(row); } } } + private void removeRow(VolumeRow volumeRow) { + mRows.remove(volumeRow); + mDialogContentView.removeView(volumeRow.view); + mDialogContentView.removeView(volumeRow.space); + } + private void onStateChangedH(State state) { final boolean animating = mMotion.isAnimating(); if (D.BUG) Log.d(TAG, "onStateChangedH animating=" + animating); @@ -624,6 +635,8 @@ public class VolumeDialog { } } + updateNotificationRowH(); + if (mActiveStream != state.activeStream) { mActiveStream = state.activeStream; updateRowsH(); @@ -635,11 +648,23 @@ public class VolumeDialog { updateFooterH(); } + private void updateNotificationRowH() { + VolumeRow notificationRow = findRow(AudioManager.STREAM_NOTIFICATION); + if (notificationRow != null) { + if (mState.linkedNotification) { + removeRow(notificationRow); + } + } else if (!mState.linkedNotification) { + addRow(AudioManager.STREAM_NOTIFICATION, + R.drawable.ic_volume_notification, R.drawable.ic_volume_notification_mute, + true); + } + } + 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 - && mAudioManager.isStreamAffectedByRingerMode(mActiveStream); + final boolean visible = mState.zenMode != Global.ZEN_MODE_OFF; if (wasVisible != visible && !visible) { prepareForCollapse(); } @@ -663,8 +688,9 @@ public class VolumeDialog { 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 isNotificationStream = row.stream == AudioManager.STREAM_NOTIFICATION; + final boolean isVibrate = mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; + final boolean isRingVibrate = isRingStream && isVibrate; final boolean isRingSilent = isRingStream && mState.ringerModeInternal == AudioManager.RINGER_MODE_SILENT; final boolean isZenAlarms = mState.zenMode == Global.ZEN_MODE_ALARMS; @@ -672,8 +698,9 @@ public class VolumeDialog { 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) + final boolean zenMuted = isZenAlarms ? (isRingStream || isSystemStream || isNotificationStream) + : isZenNone ? (isRingStream || isSystemStream || isAlarmStream || isMusicStream || isNotificationStream) + : isVibrate ? (isNotificationStream) : false; // update slider max @@ -708,12 +735,12 @@ public class VolumeDialog { row.icon.setAlpha(iconEnabled ? 1 : 0.5f); final int iconRes = isRingVibrate ? R.drawable.ic_volume_ringer_vibrate - : isRingSilent || zenMuted ? row.cachedIconRes - : ss.routedToBluetooth ? + : 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); + : mAutomute && ss.level == 0 ? row.iconMuteRes + : (ss.muted ? row.iconMuteRes : row.iconRes); if (iconRes != row.cachedIconRes) { if (row.cachedIconRes != 0 && isRingVibrate) { mController.vibrate(); @@ -723,11 +750,11 @@ public class VolumeDialog { } row.iconState = iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE - : (iconRes == R.drawable.ic_volume_media_bt_mute || iconRes == row.iconMuteRes) + : (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) + : (iconRes == R.drawable.ic_volume_media_bt || iconRes == row.iconRes) ? Events.ICON_STATE_UNMUTE - : Events.ICON_STATE_UNKNOWN; + : Events.ICON_STATE_UNKNOWN; row.icon.setContentDescription(ss.name); // update slider @@ -934,6 +961,12 @@ public class VolumeDialog { } }; + public void cleanup() { + mController.removeCallback(mControllerCallbackH); + mZenFooter.cleanup(); + mAccessibility.cleanup(); + } + private final class H extends Handler { private static final int SHOW = 1; private static final int DISMISS = 2; @@ -1056,36 +1089,21 @@ public class VolumeDialog { 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(); - } - }); + mDialogView.addOnAttachStateChangeListener(mOnAttachStateChangeListener); updateFeedbackEnabled(); } @Override public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, - AccessibilityEvent event) { + AccessibilityEvent event) { rescheduleTimeoutH(); return super.onRequestSendAccessibilityEvent(host, child, event); } + public void cleanup() { + mDialogView.removeOnAttachStateChangeListener(mOnAttachStateChangeListener); + } + private void updateFeedbackEnabled() { mFeedbackEnabled = computeFeedbackEnabled(); } @@ -1101,6 +1119,21 @@ public class VolumeDialog { } return false; } + + private OnAttachStateChangeListener mOnAttachStateChangeListener = + 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(); + } + }; } private static class VolumeRow { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java index 1083f40..742b18b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java @@ -43,7 +43,7 @@ public class VolumeDialogComponent implements VolumeComponent { private final Context mContext; private final VolumeDialogController mController; private final ZenModeController mZenModeController; - private final VolumeDialog mDialog; + private VolumeDialog mDialog; private final VolumePolicy mVolumePolicy = new VolumePolicy( true, // volumeDownToEnterSilent true, // volumeUpToExitSilent @@ -138,4 +138,11 @@ public class VolumeDialogComponent implements VolumeComponent { } }; + public void recreateDialog() { + if (mDialog != null) mDialog.cleanup(); + + mDialog = new VolumeDialog(mContext, WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY, + mController, mZenModeController, mVolumeDialogCallback); + applyConfiguration(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java index 32d6805..9269c1c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java @@ -29,6 +29,7 @@ import android.database.ContentObserver; import android.media.AudioManager; import android.media.AudioSystem; import android.media.IVolumeController; +import android.media.ToneGenerator; import android.media.VolumePolicy; import android.media.session.MediaController.PlaybackInfo; import android.media.session.MediaSession.Token; @@ -54,6 +55,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +import cyanogenmod.providers.CMSettings; + /** * Source of truth for all state / events related to the volume dialog. No presentation. * @@ -67,6 +70,9 @@ public class VolumeDialogController { private static final int DYNAMIC_STREAM_START_INDEX = 100; private static final int VIBRATE_HINT_DURATION = 50; + private static final int FREE_DELAY = 10000; + private static final int BEEP_DURATION = 150; + private static final int[] STREAMS = { AudioSystem.STREAM_ALARM, AudioSystem.STREAM_BLUETOOTH_SCO, @@ -102,10 +108,13 @@ public class VolumeDialogController { private VolumePolicy mVolumePolicy; private boolean mShowDndTile = true; + private ToneGenerator mToneGenerators[]; + public VolumeDialogController(Context context, ComponentName component) { mContext = context.getApplicationContext(); Events.writeEvent(mContext, Events.EVENT_COLLECTION_STARTED); mComponent = component; + mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; mWorkerThread = new HandlerThread(VolumeDialogController.class.getSimpleName()); mWorkerThread.start(); mWorker = new W(mWorkerThread.getLooper()); @@ -289,6 +298,7 @@ public class VolumeDialogController { 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; + final boolean playSound = (flags & AudioManager.FLAG_PLAY_SOUND) != 0; boolean changed = false; if (showUI) { changed |= updateActiveStreamW(stream); @@ -308,6 +318,19 @@ public class VolumeDialogController { if (showSilentHint) { mCallbacks.onShowSilentHint(); } + if (playSound) { + if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0) { + mWorker.removeMessages(W.PLAY_SOUND); + mWorker.sendMessageDelayed(mWorker.obtainMessage(W.PLAY_SOUND, stream, flags), + AudioSystem.PLAY_SOUND_DELAY); + } + + if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) { + mWorker.removeMessages(W.PLAY_SOUND); + onStopSoundsW(); + } + + } if (changed && fromKey) { Events.writeEvent(mContext, Events.EVENT_KEY, stream, lastAudibleStreamVolume); } @@ -348,6 +371,7 @@ public class VolumeDialogController { updateZenModeW(); updateEffectsSuppressorW(mNoMan.getEffectsSuppressor()); updateZenModeConfigW(); + updateLinkNotificationConfigW(); mCallbacks.onStateChanged(mState); } @@ -400,6 +424,16 @@ public class VolumeDialogController { return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION; } + private boolean updateLinkNotificationConfigW() { + boolean linkNotificationWithVolume = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.VOLUME_LINK_NOTIFICATION, 1) == 1; + if (mState.linkedNotification == linkNotificationWithVolume) { + return false; + } + mState.linkedNotification = linkNotificationWithVolume; + return true; + } + private boolean updateZenModeConfigW() { final ZenModeConfig zenModeConfig = getZenModeConfig(); if (Objects.equals(mState.zenModeConfig, zenModeConfig)) return false; @@ -556,6 +590,9 @@ public class VolumeDialogController { private static final int NOTIFY_VISIBLE = 12; private static final int USER_ACTIVITY = 13; private static final int SHOW_SAFETY_WARNING = 14; + private static final int PLAY_SOUND = 15; + private static final int STOP_SOUNDS = 16; + private static final int FREE_RESOURCES = 17; W(Looper looper) { super(looper); @@ -578,6 +615,9 @@ public class VolumeDialogController { case NOTIFY_VISIBLE: onNotifyVisibleW(msg.arg1 != 0); break; case USER_ACTIVITY: onUserActivityW(); break; case SHOW_SAFETY_WARNING: onShowSafetyWarningW(msg.arg1); break; + case PLAY_SOUND: onPlaySoundW(msg.arg1, msg.arg2); break; + case STOP_SOUNDS: onStopSoundsW(); break; + case FREE_RESOURCES: onFreeResourcesW(); break; } } } @@ -707,6 +747,66 @@ public class VolumeDialogController { } + protected void onPlaySoundW(int streamType, int flags) { + + // If preference is no sound - just exit here + if (CMSettings.System.getInt(mContext.getContentResolver(), + CMSettings.System.VOLUME_ADJUST_SOUNDS_ENABLED, 1) == 0) { + return; + } + + if (mWorker.hasMessages(W.STOP_SOUNDS)) { + mWorker.removeMessages(W.STOP_SOUNDS); + // Force stop right now + onStopSoundsW(); + } + + ToneGenerator toneGen = getOrCreateToneGeneratorW(streamType); + if (toneGen != null) { + toneGen.startTone(ToneGenerator.TONE_PROP_BEEP); + mWorker.sendMessageDelayed(mWorker.obtainMessage(W.STOP_SOUNDS), BEEP_DURATION); + } + + mWorker.removeMessages(W.FREE_RESOURCES); + mWorker.sendMessageDelayed(mWorker.obtainMessage(W.FREE_RESOURCES), FREE_DELAY); + } + + protected void onStopSoundsW() { + int numStreamTypes = AudioSystem.getNumStreamTypes(); + for (int i = numStreamTypes - 1; i >= 0; i--) { + ToneGenerator toneGen = mToneGenerators[i]; + if (toneGen != null) { + toneGen.stopTone(); + } + } + } + + private ToneGenerator getOrCreateToneGeneratorW(int streamType) { + if (mToneGenerators[streamType] == null) { + try { + mToneGenerators[streamType] = new ToneGenerator(streamType, + ToneGenerator.MAX_VOLUME); + } catch (RuntimeException e) { + if (false) { + Log.d(TAG, "ToneGenerator constructor failed with " + + "RuntimeException: " + e); + } + } + } + return mToneGenerators[streamType]; + } + + protected void onFreeResourcesW() { + synchronized (this) { + for (int i = mToneGenerators.length - 1; i >= 0; i--) { + if (mToneGenerators[i] != null) { + mToneGenerators[i].release(); + } + mToneGenerators[i] = null; + } + } + } + private final class SettingObserver extends ContentObserver { private final Uri SERVICE_URI = Settings.Secure.getUriFor( Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT); @@ -714,6 +814,8 @@ public class VolumeDialogController { Settings.Global.getUriFor(Settings.Global.ZEN_MODE); private final Uri ZEN_MODE_CONFIG_URI = Settings.Global.getUriFor(Settings.Global.ZEN_MODE_CONFIG_ETAG); + private final Uri VOLUME_LINK_NOTIFICATION_URI = + Settings.Secure.getUriFor(Settings.Secure.VOLUME_LINK_NOTIFICATION); public SettingObserver(Handler handler) { super(handler); @@ -723,6 +825,8 @@ public class VolumeDialogController { mContext.getContentResolver().registerContentObserver(SERVICE_URI, false, this); mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this); mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_URI, false, this); + mContext.getContentResolver().registerContentObserver(VOLUME_LINK_NOTIFICATION_URI, + false, this); onChange(true, SERVICE_URI); } @@ -750,6 +854,9 @@ public class VolumeDialogController { if (ZEN_MODE_CONFIG_URI.equals(uri)) { changed = updateZenModeConfigW(); } + if (VOLUME_LINK_NOTIFICATION_URI.equals(uri)) { + changed = updateLinkNotificationConfigW(); + } if (changed) { mCallbacks.onStateChanged(mState); } @@ -868,6 +975,13 @@ public class VolumeDialogController { @Override public void onRemoteVolumeChanged(Token token, int flags) { + // If an inactive session changed the remoteVolume, bail + // since we don't have any active streams to update + if (!mRemoteStreams.containsKey(token)) { + Log.i(TAG, "onRemoteVolumeChanged called on inactive" + + "stream. Ignoring"); + return; + } final int stream = mRemoteStreams.get(token); final boolean showUI = (flags & AudioManager.FLAG_SHOW_UI) != 0; boolean changed = updateActiveStreamW(stream); @@ -947,6 +1061,7 @@ public class VolumeDialogController { public String effectsSuppressorName; public ZenModeConfig zenModeConfig; public int activeStream = NO_ACTIVE_STREAM; + public boolean linkedNotification; public State copy() { final State rt = new State(); @@ -960,6 +1075,7 @@ public class VolumeDialogController { rt.effectsSuppressorName = effectsSuppressorName; if (zenModeConfig != null) rt.zenModeConfig = zenModeConfig.copy(); rt.activeStream = activeStream; + rt.linkedNotification = linkedNotification; return rt; } @@ -989,6 +1105,7 @@ public class VolumeDialogController { sep(sb, indent); sb.append("effectsSuppressorName:").append(effectsSuppressorName); sep(sb, indent); sb.append("zenModeConfig:").append(zenModeConfig); sep(sb, indent); sb.append("activeStream:").append(activeStream); + sep(sb, indent); sb.append("linkedNotification:").append(linkedNotification); if (indent > 0) sep(sb, indent); return sb.append('}').toString(); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java index 2688813..fa0de7d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java @@ -27,6 +27,7 @@ import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.media.AudioManager; import android.media.session.MediaSessionManager; @@ -63,6 +64,8 @@ public class VolumeUI extends SystemUI { private VolumeDialogComponent mVolumeComponent; + private Configuration mConfiguration; + @Override public void start() { mEnabled = mContext.getResources().getBoolean(R.bool.enable_volume_ui); @@ -80,6 +83,7 @@ public class VolumeUI extends SystemUI { mContext, Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT, new ServiceMonitorCallbacks()); mVolumeControllerService.start(); + mConfiguration = new Configuration(mContext.getResources().getConfiguration()); } private VolumeComponent getVolumeComponent() { @@ -91,6 +95,20 @@ public class VolumeUI extends SystemUI { super.onConfigurationChanged(newConfig); if (!mEnabled) return; getVolumeComponent().onConfigurationChanged(newConfig); + + if (isThemeChange(newConfig)) { + mContext.recreateTheme(); + mVolumeComponent.recreateDialog(); + } + mConfiguration.setTo(newConfig); + } + + private boolean isThemeChange(Configuration newConfig) { + if (mConfiguration != null) { + int changes = mConfiguration.updateFrom(newConfig); + return (changes & ActivityInfo.CONFIG_THEME_RESOURCE) != 0; + } + return false; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java index af7ee08..0fb80c0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java @@ -69,17 +69,19 @@ public class ZenFooter extends LinearLayout { mSpTexts.add(mEndNowButton); } + private ZenModeController.Callback mZenModeCallback = new ZenModeController.Callback() { + @Override + public void onZenChanged(int zen) { + setZen(zen); + } + @Override + public void onConfigChanged(ZenModeConfig config) { + setConfig(config); + } + }; + 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); - } - }); + controller.addCallback(mZenModeCallback); mEndNowButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -142,4 +144,8 @@ public class ZenFooter extends LinearLayout { mSpTexts.update(); } + public void cleanup() { + mController.removeCallback(mZenModeCallback); + } + } diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java index 07ec843..ddf623a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java @@ -70,9 +70,7 @@ public class ZenModePanel extends LinearLayout { private static final int SECONDS_MS = 1000; private static final int MINUTES_MS = 60 * SECONDS_MS; - private static final int[] MINUTE_BUCKETS = DEBUG - ? new int[] { 0, 1, 2, 5, 15, 30, 45, 60, 120, 180, 240, 480 } - : ZenModeConfig.MINUTE_BUCKETS; + private static final int[] MINUTE_BUCKETS = ZenModeConfig.MINUTE_BUCKETS; private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0]; private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1]; private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60); @@ -421,8 +419,13 @@ public class ZenModePanel extends LinearLayout { mZenIntroductionCustomize.setVisibility(zenImportant ? VISIBLE : GONE); } final String warning = computeAlarmWarningText(zenNone); - mZenAlarmWarning.setVisibility(warning != null ? VISIBLE : GONE); + final int oldVis = mZenAlarmWarning.getVisibility(); + final int newVis = warning != null ? VISIBLE : GONE; + mZenAlarmWarning.setVisibility(newVis); mZenAlarmWarning.setText(warning); + if (newVis != oldVis) { + requestLayout(); + } } private String computeAlarmWarningText(boolean zenNone) { @@ -603,13 +606,6 @@ public class ZenModePanel extends LinearLayout { if (DEBUG) Log.d(mTag, "bind i=" + mZenConditions.indexOfChild(row) + " first=" + first + " condition=" + conditionId); tag.rb.setEnabled(enabled); - final boolean checked = (mSessionExitCondition != null - || mAttachedZen != Global.ZEN_MODE_OFF) - && (sameConditionId(mSessionExitCondition, tag.condition)); - if (checked != tag.rb.isChecked()) { - if (DEBUG) Log.d(mTag, "bind checked=" + checked + " condition=" + conditionId); - tag.rb.setChecked(checked); - } tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { diff --git a/packages/SystemUI/src/com/viewpagerindicator/CirclePageIndicator.java b/packages/SystemUI/src/com/viewpagerindicator/CirclePageIndicator.java new file mode 100644 index 0000000..4286983 --- /dev/null +++ b/packages/SystemUI/src/com/viewpagerindicator/CirclePageIndicator.java @@ -0,0 +1,527 @@ +/* + * Copyright (C) 2011 Patrik Akerfeldt + * Copyright (C) 2011 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.viewpagerindicator; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.drawable.Drawable; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewConfigurationCompat; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import com.android.systemui.R; + +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.widget.LinearLayout.HORIZONTAL; +import static android.widget.LinearLayout.VERTICAL; + +/** + * Draws circles (one for each view). The current view position is filled and + * others are only stroked. + */ +public class CirclePageIndicator extends View implements PageIndicator { + private static final int INVALID_POINTER = -1; + + private float mRadius; + private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG); + private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG); + private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG); + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mListener; + private int mCurrentPage; + private int mSnapPage; + private float mPageOffset; + private int mScrollState; + private int mOrientation; + private boolean mCentered; + private boolean mSnap; + + private int mTouchSlop; + private float mLastMotionX = -1; + private int mActivePointerId = INVALID_POINTER; + private boolean mIsDragging; + + private Bitmap mSettingsIcon; + private boolean mEditing; + + public CirclePageIndicator(Context context) { + this(context, null); + } + + public CirclePageIndicator(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (isInEditMode()) return; + + //Load defaults from resources + final Resources res = getResources(); + final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color); + final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color); + final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation); + final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color); + final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width); + final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius); + final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered); + final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap); + + //Retrieve styles attributes + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0); + + mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered); + mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation); + mPaintPageFill.setStyle(Style.FILL); + mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor)); + mPaintStroke.setStyle(Style.STROKE); + mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor)); + mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth)); + mPaintFill.setStyle(Style.FILL); + mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_indicatorFillColor, defaultFillColor)); + mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius); + mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap); + + Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background); + if (background != null) { + setBackgroundDrawable(background); + } + + a.recycle(); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); + + mSettingsIcon = BitmapFactory.decodeResource(res, R.drawable.ic_mini_settings); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + public void setCentered(boolean centered) { + mCentered = centered; + invalidate(); + } + + public boolean isCentered() { + return mCentered; + } + + public void setPageColor(int pageColor) { + mPaintPageFill.setColor(pageColor); + invalidate(); + } + + public int getPageColor() { + return mPaintPageFill.getColor(); + } + + public void setFillColor(int fillColor) { + mPaintFill.setColor(fillColor); + invalidate(); + } + + public int getFillColor() { + return mPaintFill.getColor(); + } + + public void setOrientation(int orientation) { + switch (orientation) { + case HORIZONTAL: + case VERTICAL: + mOrientation = orientation; + requestLayout(); + break; + + default: + throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL."); + } + } + + public int getOrientation() { + return mOrientation; + } + + public void setStrokeColor(int strokeColor) { + mPaintStroke.setColor(strokeColor); + invalidate(); + } + + public int getStrokeColor() { + return mPaintStroke.getColor(); + } + + public void setStrokeWidth(float strokeWidth) { + mPaintStroke.setStrokeWidth(strokeWidth); + invalidate(); + } + + public float getStrokeWidth() { + return mPaintStroke.getStrokeWidth(); + } + + public void setRadius(float radius) { + mRadius = radius; + invalidate(); + } + + public float getRadius() { + return mRadius; + } + + public void setSnap(boolean snap) { + mSnap = snap; + invalidate(); + } + + public boolean isSnap() { + return mSnap; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mViewPager == null) { + return; + } + final int count = mViewPager.getAdapter().getCount(); + if (count == 0) { + return; + } + + if (mCurrentPage >= count) { + setCurrentItem(count - 1); + return; + } + + int longSize; + int longPaddingBefore; + int longPaddingAfter; + int shortPaddingBefore; + if (mOrientation == HORIZONTAL) { + longSize = getWidth(); + longPaddingBefore = getPaddingLeft(); + longPaddingAfter = getPaddingRight(); + shortPaddingBefore = getPaddingTop(); + } else { + longSize = getHeight(); + longPaddingBefore = getPaddingTop(); + longPaddingAfter = getPaddingBottom(); + shortPaddingBefore = getPaddingLeft(); + } + + final float threeRadius = mRadius * 3; + final float shortOffset = shortPaddingBefore + mRadius; + float longOffset = longPaddingBefore + mRadius; + if (mCentered) { + longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f); + } + + float dX; + float dY; + + float pageFillRadius = mRadius; + if (mPaintStroke.getStrokeWidth() > 0) { + pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f; + } + + //Draw stroked circles + for (int iLoop = 0; iLoop < count; iLoop++) { + float drawLong = longOffset + (iLoop * threeRadius); + if (mOrientation == HORIZONTAL) { + dX = drawLong; + dY = shortOffset; + } else { + dX = shortOffset; + dY = drawLong; + } + // Only paint fill if not completely transparent + if (mPaintPageFill.getAlpha() > 0) { + if (mEditing && iLoop == 0) { + canvas.drawBitmap(mSettingsIcon, + (int) (dX - mRadius), + (int) (dY - mRadius), + mPaintPageFill); + } else { + canvas.drawCircle(dX, dY, (float) (pageFillRadius / 1.5f), mPaintPageFill); + } + } + + // Only paint stroke if a stroke width was non-zero + if (pageFillRadius != mRadius) { + canvas.drawCircle(dX, dY, mRadius, mPaintStroke); + } + } + + //Draw the filled circle according to the current scroll + float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius; + if (!mSnap) { + cx += mPageOffset * threeRadius; + } + if (mOrientation == HORIZONTAL) { + dX = longOffset + cx; + dY = shortOffset; + } else { + dX = shortOffset; + dY = longOffset + cx; + } + canvas.drawCircle(dX, dY, mRadius, mPaintFill); + } + + public boolean onTouchEvent(MotionEvent ev) { + if (super.onTouchEvent(ev)) { + return true; + } + if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) { + return false; + } + + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mLastMotionX = ev.getX(); + break; + + case MotionEvent.ACTION_MOVE: { + final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + final float deltaX = x - mLastMotionX; + + if (!mIsDragging) { + if (Math.abs(deltaX) > mTouchSlop) { + mIsDragging = true; + } + } + + if (mIsDragging) { + mLastMotionX = x; + if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) { + mViewPager.fakeDragBy(deltaX); + } + } + + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (!mIsDragging) { + final int count = mViewPager.getAdapter().getCount(); + final int width = getWidth(); + final float halfWidth = width / 2f; + final float sixthWidth = width / 6f; + + if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage - 1); + } + return true; + } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage + 1); + } + return true; + } + } + + mIsDragging = false; + mActivePointerId = INVALID_POINTER; + if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); + break; + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionX = MotionEventCompat.getX(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + } + mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + + return true; + } + + @Override + public void setViewPager(ViewPager view) { + if (mViewPager == view) { + return; + } + if (mViewPager != null) { + mViewPager.setOnPageChangeListener(null); + } + if (view.getAdapter() == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + mViewPager = view; + mViewPager.setOnPageChangeListener(this); + invalidate(); + } + + @Override + public void setViewPager(ViewPager view, int initialPosition) { + setViewPager(view); + setCurrentItem(initialPosition); + } + + @Override + public void setCurrentItem(int item) { + if (mViewPager == null) { + throw new IllegalStateException("ViewPager has not been bound."); + } + mViewPager.setCurrentItem(item); + mCurrentPage = item; + invalidate(); + } + + @Override + public void notifyDataSetChanged() { + invalidate(); + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mListener != null) { + mListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mCurrentPage = position; + mPageOffset = positionOffset; + invalidate(); + + if (mListener != null) { + mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mCurrentPage = position; + mSnapPage = position; + invalidate(); + } + + if (mListener != null) { + mListener.onPageSelected(position); + } + } + + @Override + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mListener = listener; + } + + /* + * (non-Javadoc) + * + * @see android.view.View#onMeasure(int, int) + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mOrientation == HORIZONTAL) { + setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec)); + } else { + setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec)); + } + } + + /** + * Determines the width of this view + * + * @param measureSpec + * A measureSpec packed into an int + * @return The width of the view, honoring constraints from measureSpec + */ + private int measureLong(int measureSpec) { + int result; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) { + //We were told how big to be + result = specSize; + } else { + //Calculate the width according the views count + final int count = mViewPager.getAdapter().getCount(); + result = (int)(getPaddingLeft() + getPaddingRight() + + (count * 2 * mRadius) + (count - 1) * mRadius + 1); + //Respect AT_MOST value if that was what is called for by measureSpec + if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(result, specSize); + } + } + return result; + } + + /** + * Determines the height of this view + * + * @param measureSpec + * A measureSpec packed into an int + * @return The height of the view, honoring constraints from measureSpec + */ + private int measureShort(int measureSpec) { + int result; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if (specMode == MeasureSpec.EXACTLY) { + //We were told how big to be + result = specSize; + } else { + //Measure the height + result = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1); + //Respect AT_MOST value if that was what is called for by measureSpec + if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(result, specSize); + } + } + return result; + } + + public void setEditing(boolean editing) { + mEditing = editing; + invalidate(); + } +} diff --git a/packages/SystemUI/src/com/viewpagerindicator/PageIndicator.java b/packages/SystemUI/src/com/viewpagerindicator/PageIndicator.java new file mode 100644 index 0000000..c08c00a --- /dev/null +++ b/packages/SystemUI/src/com/viewpagerindicator/PageIndicator.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2011 Patrik Akerfeldt + * Copyright (C) 2011 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.viewpagerindicator; + +import android.support.v4.view.ViewPager; + +/** + * A PageIndicator is responsible to show an visual indicator on the total views + * number and the current visible view. + */ +public interface PageIndicator extends ViewPager.OnPageChangeListener { + /** + * Bind the indicator to a ViewPager. + * + * @param view + */ + void setViewPager(ViewPager view); + + /** + * Bind the indicator to a ViewPager. + * + * @param view + * @param initialPosition + */ + void setViewPager(ViewPager view, int initialPosition); + + /** + * <p>Set the current page of both the ViewPager and indicator.</p> + * + * <p>This <strong>must</strong> be used if you need to set the page before + * the views are drawn on screen (e.g., default start page).</p> + * + * @param item + */ + void setCurrentItem(int item); + + /** + * Set a page change listener which will receive forwarded events. + * + * @param listener + */ + void setOnPageChangeListener(ViewPager.OnPageChangeListener listener); + + /** + * Notify the indicator that the fragment list has changed. + */ + void notifyDataSetChanged(); +} |