diff options
Diffstat (limited to 'packages/SystemUI/src')
39 files changed, 4609 insertions, 1396 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java index 2e95498..292c9c2 100755 --- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java @@ -16,6 +16,7 @@ package com.android.systemui; +import android.animation.ArgbEvaluator; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -80,6 +81,12 @@ public class BatteryMeterView extends View implements DemoMode, private BatteryController mBatteryController; private boolean mPowerSaveEnabled; + private int mDarkModeBackgroundColor; + private int mDarkModeFillColor; + + private int mLightModeBackgroundColor; + private int mLightModeFillColor; + private class BatteryTracker extends BroadcastReceiver { public static final int UNKNOWN_LEVEL = -1; @@ -245,6 +252,13 @@ public class BatteryMeterView extends View implements DemoMode, 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); + mDarkModeFillColor = context.getColor(R.color.dark_mode_icon_color_dual_tone_fill); + mLightModeBackgroundColor = + context.getColor(R.color.light_mode_icon_color_dual_tone_background); + mLightModeFillColor = context.getColor(R.color.light_mode_icon_color_dual_tone_fill); } public void setBatteryController(BatteryController batteryController) { @@ -309,14 +323,30 @@ public class BatteryMeterView extends View implements DemoMode, return color; } - public void setIconTint(int tint) { - mIconTint = tint; - mFramePaint.setColorFilter(new PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_ATOP)); - mBoltPaint.setColor(tint); - mChargeColor = tint; + public void setDarkIntensity(float darkIntensity) { + int backgroundColor = getBackgroundColor(darkIntensity); + int fillColor = getFillColor(darkIntensity); + mIconTint = fillColor; + mFramePaint.setColor(backgroundColor); + mBoltPaint.setColor(backgroundColor); + mChargeColor = fillColor; invalidate(); } + private int getBackgroundColor(float darkIntensity) { + return getColorForDarkIntensity( + darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor); + } + + private int getFillColor(float darkIntensity) { + return getColorForDarkIntensity( + darkIntensity, mLightModeFillColor, mDarkModeFillColor); + } + + private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) { + return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor); + } + @Override public void draw(Canvas c) { BatteryTracker tracker = mDemoMode ? mDemoTracker : mTracker; diff --git a/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java b/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java deleted file mode 100644 index f33e2b8..0000000 --- a/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java +++ /dev/null @@ -1,592 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Outline; -import android.graphics.Paint; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewOutlineProvider; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.view.animation.LinearInterpolator; -import android.widget.FrameLayout; -import android.widget.ImageView; -import com.android.systemui.statusbar.phone.PhoneStatusBar; - -import java.util.ArrayList; - -public class SearchPanelCircleView extends FrameLayout { - - private final int mCircleMinSize; - private final int mBaseMargin; - private final int mStaticOffset; - private final Paint mBackgroundPaint = new Paint(); - private final Paint mRipplePaint = new Paint(); - private final Rect mCircleRect = new Rect(); - private final Rect mStaticRect = new Rect(); - private final Interpolator mFastOutSlowInInterpolator; - private final Interpolator mAppearInterpolator; - private final Interpolator mDisappearInterpolator; - - private boolean mClipToOutline; - private final int mMaxElevation; - private boolean mAnimatingOut; - private float mOutlineAlpha; - private float mOffset; - private float mCircleSize; - private boolean mHorizontal; - private boolean mCircleHidden; - private ImageView mLogo; - private boolean mDraggedFarEnough; - private boolean mOffsetAnimatingIn; - private float mCircleAnimationEndValue; - private ArrayList<Ripple> mRipples = new ArrayList<Ripple>(); - - private ValueAnimator mOffsetAnimator; - private ValueAnimator mCircleAnimator; - private ValueAnimator mFadeOutAnimator; - private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener - = new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - applyCircleSize((float) animation.getAnimatedValue()); - updateElevation(); - } - }; - private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mCircleAnimator = null; - } - }; - private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener - = new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - setOffset((float) animation.getAnimatedValue()); - } - }; - - - public SearchPanelCircleView(Context context) { - this(context, null); - } - - public SearchPanelCircleView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - if (mCircleSize > 0.0f) { - outline.setOval(mCircleRect); - } else { - outline.setEmpty(); - } - outline.setAlpha(mOutlineAlpha); - } - }); - setWillNotDraw(false); - mCircleMinSize = context.getResources().getDimensionPixelSize( - R.dimen.search_panel_circle_size); - mBaseMargin = context.getResources().getDimensionPixelSize( - R.dimen.search_panel_circle_base_margin); - mStaticOffset = context.getResources().getDimensionPixelSize( - R.dimen.search_panel_circle_travel_distance); - mMaxElevation = context.getResources().getDimensionPixelSize( - R.dimen.search_panel_circle_elevation); - mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.linear_out_slow_in); - mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.fast_out_slow_in); - mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.fast_out_linear_in); - mBackgroundPaint.setAntiAlias(true); - mBackgroundPaint.setColor(context.getColor(R.color.search_panel_circle_color)); - mRipplePaint.setColor(context.getColor(R.color.search_panel_ripple_color)); - mRipplePaint.setAntiAlias(true); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - drawBackground(canvas); - drawRipples(canvas); - } - - private void drawRipples(Canvas canvas) { - for (int i = 0; i < mRipples.size(); i++) { - Ripple ripple = mRipples.get(i); - ripple.draw(canvas); - } - } - - private void drawBackground(Canvas canvas) { - canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2, - mBackgroundPaint); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mLogo = (ImageView) findViewById(R.id.search_logo); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight()); - if (changed) { - updateCircleRect(mStaticRect, mStaticOffset, true); - } - } - - public void setCircleSize(float circleSize) { - setCircleSize(circleSize, false, null, 0, null); - } - - public void setCircleSize(float circleSize, boolean animated, final Runnable endRunnable, - int startDelay, Interpolator interpolator) { - boolean isAnimating = mCircleAnimator != null; - boolean animationPending = isAnimating && !mCircleAnimator.isRunning(); - boolean animatingOut = isAnimating && mCircleAnimationEndValue == 0; - if (animated || animationPending || animatingOut) { - if (isAnimating) { - if (circleSize == mCircleAnimationEndValue) { - return; - } - mCircleAnimator.cancel(); - } - mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize); - mCircleAnimator.addUpdateListener(mCircleUpdateListener); - mCircleAnimator.addListener(mClearAnimatorListener); - mCircleAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (endRunnable != null) { - endRunnable.run(); - } - } - }); - Interpolator desiredInterpolator = interpolator != null ? interpolator - : circleSize == 0 ? mDisappearInterpolator : mAppearInterpolator; - mCircleAnimator.setInterpolator(desiredInterpolator); - mCircleAnimator.setDuration(300); - mCircleAnimator.setStartDelay(startDelay); - mCircleAnimator.start(); - mCircleAnimationEndValue = circleSize; - } else { - if (isAnimating) { - float diff = circleSize - mCircleAnimationEndValue; - PropertyValuesHolder[] values = mCircleAnimator.getValues(); - values[0].setFloatValues(diff, circleSize); - mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime()); - mCircleAnimationEndValue = circleSize; - } else { - applyCircleSize(circleSize); - updateElevation(); - } - } - } - - private void applyCircleSize(float circleSize) { - mCircleSize = circleSize; - updateLayout(); - } - - private void updateElevation() { - float t = (mStaticOffset - mOffset) / (float) mStaticOffset; - t = 1.0f - Math.max(t, 0.0f); - float offset = t * mMaxElevation; - setElevation(offset); - } - - /** - * Sets the offset to the edge of the screen. By default this not not animated. - * - * @param offset The offset to apply. - */ - public void setOffset(float offset) { - setOffset(offset, false, 0, null, null); - } - - /** - * Sets the offset to the edge of the screen. - * - * @param offset The offset to apply. - * @param animate Whether an animation should be performed. - * @param startDelay The desired start delay if animated. - * @param interpolator The desired interpolator if animated. If null, - * a default interpolator will be taken designed for appearing or - * disappearing. - * @param endRunnable The end runnable which should be executed when the animation is finished. - */ - private void setOffset(float offset, boolean animate, int startDelay, - Interpolator interpolator, final Runnable endRunnable) { - if (!animate) { - mOffset = offset; - updateLayout(); - if (endRunnable != null) { - endRunnable.run(); - } - } else { - if (mOffsetAnimator != null) { - mOffsetAnimator.removeAllListeners(); - mOffsetAnimator.cancel(); - } - mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset); - mOffsetAnimator.addUpdateListener(mOffsetUpdateListener); - mOffsetAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mOffsetAnimator = null; - if (endRunnable != null) { - endRunnable.run(); - } - } - }); - Interpolator desiredInterpolator = interpolator != null ? - interpolator : offset == 0 ? mDisappearInterpolator : mAppearInterpolator; - mOffsetAnimator.setInterpolator(desiredInterpolator); - mOffsetAnimator.setStartDelay(startDelay); - mOffsetAnimator.setDuration(300); - mOffsetAnimator.start(); - mOffsetAnimatingIn = offset != 0; - } - } - - private void updateLayout() { - updateCircleRect(); - updateLogo(); - invalidateOutline(); - invalidate(); - updateClipping(); - } - - private void updateClipping() { - boolean clip = mCircleSize < mCircleMinSize || !mRipples.isEmpty(); - if (clip != mClipToOutline) { - setClipToOutline(clip); - mClipToOutline = clip; - } - } - - private void updateLogo() { - boolean exitAnimationRunning = mFadeOutAnimator != null; - Rect rect = exitAnimationRunning ? mCircleRect : mStaticRect; - float translationX = (rect.left + rect.right) / 2.0f - mLogo.getWidth() / 2.0f; - float translationY = (rect.top + rect.bottom) / 2.0f - mLogo.getHeight() / 2.0f; - float t = (mStaticOffset - mOffset) / (float) mStaticOffset; - if (!exitAnimationRunning) { - if (mHorizontal) { - translationX += t * mStaticOffset * 0.3f; - } else { - translationY += t * mStaticOffset * 0.3f; - } - float alpha = 1.0f-t; - alpha = Math.max((alpha - 0.5f) * 2.0f, 0); - mLogo.setAlpha(alpha); - } else { - translationY += (mOffset - mStaticOffset) / 2; - } - mLogo.setTranslationX(translationX); - mLogo.setTranslationY(translationY); - } - - private void updateCircleRect() { - updateCircleRect(mCircleRect, mOffset, false); - } - - private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) { - int left, top; - float circleSize = useStaticSize ? mCircleMinSize : mCircleSize; - if (mHorizontal) { - left = (int) (getWidth() - circleSize / 2 - mBaseMargin - offset); - top = (int) ((getHeight() - circleSize) / 2); - } else { - left = (int) (getWidth() - circleSize) / 2; - top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset); - } - rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize)); - } - - public void setHorizontal(boolean horizontal) { - mHorizontal = horizontal; - updateCircleRect(mStaticRect, mStaticOffset, true); - updateLayout(); - } - - public void setDragDistance(float distance) { - if (!mAnimatingOut && (!mCircleHidden || mDraggedFarEnough)) { - float circleSize = mCircleMinSize + rubberband(distance); - setCircleSize(circleSize); - } - - } - - private float rubberband(float diff) { - return (float) Math.pow(Math.abs(diff), 0.6f); - } - - public void startAbortAnimation(Runnable endRunnable) { - if (mAnimatingOut) { - if (endRunnable != null) { - endRunnable.run(); - } - return; - } - setCircleSize(0, true, null, 0, null); - setOffset(0, true, 0, null, endRunnable); - mCircleHidden = true; - } - - public void startEnterAnimation() { - if (mAnimatingOut) { - return; - } - applyCircleSize(0); - setOffset(0); - setCircleSize(mCircleMinSize, true, null, 50, null); - setOffset(mStaticOffset, true, 50, null, null); - mCircleHidden = false; - } - - - public void startExitAnimation(final Runnable endRunnable) { - if (!mHorizontal) { - float offset = getHeight() / 2.0f; - setOffset(offset - mBaseMargin, true, 50, mFastOutSlowInInterpolator, null); - float xMax = getWidth() / 2; - float yMax = getHeight() / 2; - float maxRadius = (float) Math.ceil(Math.hypot(xMax, yMax) * 2); - setCircleSize(maxRadius, true, null, 50, mFastOutSlowInInterpolator); - performExitFadeOutAnimation(50, 300, endRunnable); - } else { - - // when in landscape, we don't wan't the animation as it interferes with the general - // rotation animation to the homescreen. - endRunnable.run(); - } - } - - private void performExitFadeOutAnimation(int startDelay, int duration, - final Runnable endRunnable) { - mFadeOutAnimator = ValueAnimator.ofFloat(mBackgroundPaint.getAlpha() / 255.0f, 0.0f); - - // Linear since we are animating multiple values - mFadeOutAnimator.setInterpolator(new LinearInterpolator()); - mFadeOutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - float animatedFraction = animation.getAnimatedFraction(); - float logoValue = animatedFraction > 0.5f ? 1.0f : animatedFraction / 0.5f; - logoValue = PhoneStatusBar.ALPHA_OUT.getInterpolation(1.0f - logoValue); - float backgroundValue = animatedFraction < 0.2f ? 0.0f : - PhoneStatusBar.ALPHA_OUT.getInterpolation((animatedFraction - 0.2f) / 0.8f); - backgroundValue = 1.0f - backgroundValue; - mBackgroundPaint.setAlpha((int) (backgroundValue * 255)); - mOutlineAlpha = backgroundValue; - mLogo.setAlpha(logoValue); - invalidateOutline(); - invalidate(); - } - }); - mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (endRunnable != null) { - endRunnable.run(); - } - mLogo.setAlpha(1.0f); - mBackgroundPaint.setAlpha(255); - mOutlineAlpha = 1.0f; - mFadeOutAnimator = null; - } - }); - mFadeOutAnimator.setStartDelay(startDelay); - mFadeOutAnimator.setDuration(duration); - mFadeOutAnimator.start(); - } - - public void setDraggedFarEnough(boolean farEnough) { - if (farEnough != mDraggedFarEnough) { - if (farEnough) { - if (mCircleHidden) { - startEnterAnimation(); - } - if (mOffsetAnimator == null) { - addRipple(); - } else { - postDelayed(new Runnable() { - @Override - public void run() { - addRipple(); - } - }, 100); - } - } else { - startAbortAnimation(null); - } - mDraggedFarEnough = farEnough; - } - - } - - private void addRipple() { - if (mRipples.size() > 1) { - // we only want 2 ripples at the time - return; - } - float xInterpolation, yInterpolation; - if (mHorizontal) { - xInterpolation = 0.75f; - yInterpolation = 0.5f; - } else { - xInterpolation = 0.5f; - yInterpolation = 0.75f; - } - float circleCenterX = mStaticRect.left * (1.0f - xInterpolation) - + mStaticRect.right * xInterpolation; - float circleCenterY = mStaticRect.top * (1.0f - yInterpolation) - + mStaticRect.bottom * yInterpolation; - float radius = Math.max(mCircleSize, mCircleMinSize * 1.25f) * 0.75f; - Ripple ripple = new Ripple(circleCenterX, circleCenterY, radius); - ripple.start(); - } - - public void reset() { - mDraggedFarEnough = false; - mAnimatingOut = false; - mCircleHidden = true; - mClipToOutline = false; - if (mFadeOutAnimator != null) { - mFadeOutAnimator.cancel(); - } - mBackgroundPaint.setAlpha(255); - mOutlineAlpha = 1.0f; - } - - /** - * Check if an animation is currently running - * - * @param enterAnimation Is the animating queried the enter animation. - */ - public boolean isAnimationRunning(boolean enterAnimation) { - return mOffsetAnimator != null && (enterAnimation == mOffsetAnimatingIn); - } - - public void performOnAnimationFinished(final Runnable runnable) { - if (mOffsetAnimator != null) { - mOffsetAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (runnable != null) { - runnable.run(); - } - } - }); - } else { - if (runnable != null) { - runnable.run(); - } - } - } - - public void setAnimatingOut(boolean animatingOut) { - mAnimatingOut = animatingOut; - } - - /** - * @return Whether the circle is currently launching to the search activity or aborting the - * interaction - */ - public boolean isAnimatingOut() { - return mAnimatingOut; - } - - @Override - public boolean hasOverlappingRendering() { - // not really true but it's ok during an animation, as it's never permanent - return false; - } - - private class Ripple { - float x; - float y; - float radius; - float endRadius; - float alpha; - - Ripple(float x, float y, float endRadius) { - this.x = x; - this.y = y; - this.endRadius = endRadius; - } - - void start() { - ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f); - - // Linear since we are animating multiple values - animator.setInterpolator(new LinearInterpolator()); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - alpha = 1.0f - animation.getAnimatedFraction(); - alpha = mDisappearInterpolator.getInterpolation(alpha); - radius = mAppearInterpolator.getInterpolation(animation.getAnimatedFraction()); - radius *= endRadius; - invalidate(); - } - }); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mRipples.remove(Ripple.this); - updateClipping(); - } - - public void onAnimationStart(Animator animation) { - mRipples.add(Ripple.this); - updateClipping(); - } - }); - animator.setDuration(400); - animator.start(); - } - - public void draw(Canvas canvas) { - mRipplePaint.setAlpha((int) (alpha * 255)); - canvas.drawCircle(x, y, radius, mRipplePaint); - } - } - -} diff --git a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java deleted file mode 100644 index 445b499..0000000 --- a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui; - -import android.app.ActivityOptions; -import android.app.SearchManager; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.media.AudioAttributes; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.UserHandle; -import android.os.Vibrator; -import android.provider.Settings; -import android.util.AttributeSet; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; - -import com.android.systemui.statusbar.BaseStatusBar; -import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.StatusBarPanel; -import com.android.systemui.statusbar.phone.PhoneStatusBar; - -public class SearchPanelView extends FrameLayout implements StatusBarPanel { - - private static final String TAG = "SearchPanelView"; - private static final String ASSIST_ICON_METADATA_NAME = - "com.android.systemui.action_assist_icon"; - - private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .build(); - - private final Context mContext; - private BaseStatusBar mBar; - - private SearchPanelCircleView mCircle; - private ImageView mLogo; - private View mScrim; - - private int mThreshold; - private boolean mHorizontal; - - private boolean mLaunching; - private boolean mDragging; - private boolean mDraggedFarEnough; - private float mStartTouch; - private float mStartDrag; - private boolean mLaunchPending; - - public SearchPanelView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SearchPanelView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mContext = context; - mThreshold = context.getResources().getDimensionPixelSize(R.dimen.search_panel_threshold); - } - - private void startAssistActivity() { - if (!mBar.isDeviceProvisioned()) return; - - // Close Recent Apps if needed - mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL); - - final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) - .getAssistIntent(mContext, true, UserHandle.USER_CURRENT); - if (intent == null) return; - - try { - final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, - R.anim.search_launch_enter, R.anim.search_launch_exit); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - AsyncTask.execute(new Runnable() { - @Override - public void run() { - mContext.startActivityAsUser(intent, opts.toBundle(), - new UserHandle(UserHandle.USER_CURRENT)); - } - }); - } catch (ActivityNotFoundException e) { - Log.w(TAG, "Activity not found for " + intent.getAction()); - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mCircle = (SearchPanelCircleView) findViewById(R.id.search_panel_circle); - mLogo = (ImageView) findViewById(R.id.search_logo); - mScrim = findViewById(R.id.search_panel_scrim); - } - - private void maybeSwapSearchIcon() { - Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) - .getAssistIntent(mContext, false, UserHandle.USER_CURRENT); - if (intent != null) { - ComponentName component = intent.getComponent(); - replaceDrawable(mLogo, component, ASSIST_ICON_METADATA_NAME); - } else { - mLogo.setImageDrawable(null); - } - } - - public void replaceDrawable(ImageView v, ComponentName component, String name) { - if (component != null) { - try { - PackageManager packageManager = mContext.getPackageManager(); - // Look for the search icon specified in the activity meta-data - Bundle metaData = packageManager.getActivityInfo( - component, PackageManager.GET_META_DATA).metaData; - if (metaData != null) { - int iconResId = metaData.getInt(name); - if (iconResId != 0) { - Resources res = packageManager.getResourcesForActivity(component); - v.setImageDrawable(res.getDrawable(iconResId)); - return; - } - } - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Failed to swap drawable; " - + component.flattenToShortString() + " not found", e); - } catch (Resources.NotFoundException nfe) { - Log.w(TAG, "Failed to swap drawable from " - + component.flattenToShortString(), nfe); - } - } - v.setImageDrawable(null); - } - - @Override - public boolean isInContentArea(int x, int y) { - return true; - } - - private void vibrate() { - Context context = getContext(); - if (Settings.System.getIntForUser(context.getContentResolver(), - Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0) { - Resources res = context.getResources(); - Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); - vibrator.vibrate(res.getInteger(R.integer.config_search_panel_view_vibration_duration), - VIBRATION_ATTRIBUTES); - } - } - - public void show(final boolean show, boolean animate) { - if (show) { - maybeSwapSearchIcon(); - if (getVisibility() != View.VISIBLE) { - setVisibility(View.VISIBLE); - vibrate(); - if (animate) { - startEnterAnimation(); - } else { - mScrim.setAlpha(1f); - } - } - setFocusable(true); - setFocusableInTouchMode(true); - requestFocus(); - } else { - if (animate) { - startAbortAnimation(); - } else { - setVisibility(View.INVISIBLE); - } - } - } - - private void startEnterAnimation() { - mCircle.startEnterAnimation(); - mScrim.setAlpha(0f); - mScrim.animate() - .alpha(1f) - .setDuration(300) - .setStartDelay(50) - .setInterpolator(PhoneStatusBar.ALPHA_IN) - .start(); - - } - - private void startAbortAnimation() { - mCircle.startAbortAnimation(new Runnable() { - @Override - public void run() { - mCircle.setAnimatingOut(false); - setVisibility(View.INVISIBLE); - } - }); - mCircle.setAnimatingOut(true); - mScrim.animate() - .alpha(0f) - .setDuration(300) - .setStartDelay(0) - .setInterpolator(PhoneStatusBar.ALPHA_OUT); - } - - public void hide(boolean animate) { - if (mBar != null) { - // This will indirectly cause show(false, ...) to get called - mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); - } else { - if (animate) { - startAbortAnimation(); - } else { - setVisibility(View.INVISIBLE); - } - } - } - - @Override - public boolean dispatchHoverEvent(MotionEvent event) { - // Ignore hover events outside of this panel bounds since such events - // generate spurious accessibility events with the panel content when - // tapping outside of it, thus confusing the user. - final int x = (int) event.getX(); - final int y = (int) event.getY(); - if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { - return super.dispatchHoverEvent(event); - } - return true; - } - - /** - * Whether the panel is showing, or, if it's animating, whether it will be - * when the animation is done. - */ - public boolean isShowing() { - return getVisibility() == View.VISIBLE && !mCircle.isAnimatingOut(); - } - - public void setBar(BaseStatusBar bar) { - mBar = bar; - } - - public boolean isAssistantAvailable() { - return ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) - .getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (mLaunching || mLaunchPending) { - return false; - } - int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - mStartTouch = mHorizontal ? event.getX() : event.getY(); - mDragging = false; - mDraggedFarEnough = false; - mCircle.reset(); - break; - case MotionEvent.ACTION_MOVE: - float currentTouch = mHorizontal ? event.getX() : event.getY(); - if (getVisibility() == View.VISIBLE && !mDragging && - (!mCircle.isAnimationRunning(true /* enterAnimation */) - || Math.abs(mStartTouch - currentTouch) > mThreshold)) { - mStartDrag = currentTouch; - mDragging = true; - } - if (mDragging) { - float offset = Math.max(mStartDrag - currentTouch, 0.0f); - mCircle.setDragDistance(offset); - mDraggedFarEnough = Math.abs(mStartTouch - currentTouch) > mThreshold; - mCircle.setDraggedFarEnough(mDraggedFarEnough); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (mDraggedFarEnough) { - if (mCircle.isAnimationRunning(true /* enterAnimation */)) { - mLaunchPending = true; - mCircle.setAnimatingOut(true); - mCircle.performOnAnimationFinished(new Runnable() { - @Override - public void run() { - startExitAnimation(); - } - }); - } else { - startExitAnimation(); - } - } else { - startAbortAnimation(); - } - break; - } - return true; - } - - private void startExitAnimation() { - mLaunchPending = false; - if (mLaunching || getVisibility() != View.VISIBLE) { - return; - } - mLaunching = true; - startAssistActivity(); - vibrate(); - mCircle.setAnimatingOut(true); - mCircle.startExitAnimation(new Runnable() { - @Override - public void run() { - mLaunching = false; - mCircle.setAnimatingOut(false); - setVisibility(View.INVISIBLE); - } - }); - mScrim.animate() - .alpha(0f) - .setDuration(300) - .setStartDelay(0) - .setInterpolator(PhoneStatusBar.ALPHA_OUT); - } - - public void setHorizontal(boolean horizontal) { - mHorizontal = horizontal; - mCircle.setHorizontal(horizontal); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistGestureManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistGestureManager.java new file mode 100644 index 0000000..36be355 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistGestureManager.java @@ -0,0 +1,292 @@ +package com.android.systemui.assist; + +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.SearchManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.media.AudioAttributes; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.os.Vibrator; +import android.provider.Settings; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ImageView; + +import com.android.internal.app.IVoiceInteractionManagerService; +import com.android.internal.app.IVoiceInteractionSessionShowCallback; +import com.android.systemui.R; +import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.phone.PhoneStatusBar; + +/** + * Class to manage everything around the assist gesture. + */ +public class AssistGestureManager { + + private static final String TAG = "AssistGestureManager"; + private static final String ASSIST_ICON_METADATA_NAME = + "com.android.systemui.action_assist_icon"; + + private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) + .build(); + + private static final long TIMEOUT_SERVICE = 2500; + private static final long TIMEOUT_ACTIVITY = 1000; + + private final Context mContext; + private final WindowManager mWindowManager; + private AssistOrbContainer mView; + private final PhoneStatusBar mBar; + private final IVoiceInteractionManagerService mVoiceInteractionManagerService; + + private IVoiceInteractionSessionShowCallback mShowCallback = + new IVoiceInteractionSessionShowCallback.Stub() { + + @Override + public void onFailed() throws RemoteException { + mView.post(mHideRunnable); + } + + @Override + public void onShown() throws RemoteException { + mView.post(mHideRunnable); + } + }; + + private Runnable mHideRunnable = new Runnable() { + @Override + public void run() { + mView.removeCallbacks(this); + mView.show(false /* show */, true /* animate */); + } + }; + + public AssistGestureManager(PhoneStatusBar bar, Context context) { + mContext = context; + mBar = bar; + mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + mVoiceInteractionManagerService = IVoiceInteractionManagerService.Stub.asInterface( + ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); + } + + public void onConfigurationChanged() { + boolean visible = false; + if (mView != null) { + visible = mView.isShowing(); + mWindowManager.removeView(mView); + } + + mView = (AssistOrbContainer) LayoutInflater.from(mContext).inflate( + R.layout.assist_orb, null); + mView.setVisibility(View.GONE); + mView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + WindowManager.LayoutParams lp = getLayoutParams(); + mWindowManager.addView(mView, lp); + mBar.getNavigationBarView().setDelegateView(mView); + if (visible) { + mView.show(true /* show */, false /* animate */); + } + } + + public void onGestureInvoked(boolean vibrate) { + boolean isVoiceInteractorActive = getVoiceInteractorSupportsAssistGesture(); + if (!isVoiceInteractorActive && !isAssistantIntentAvailable()) { + return; + } + if (vibrate) { + vibrate(); + } + if (!isVoiceInteractorActive || !isVoiceSessionRunning()) { + showOrb(); + mView.postDelayed(mHideRunnable, isVoiceInteractorActive + ? TIMEOUT_SERVICE + : TIMEOUT_ACTIVITY); + } + startAssist(); + } + + private WindowManager.LayoutParams getLayoutParams() { + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + mContext.getResources().getDimensionPixelSize(R.dimen.assist_orb_scrim_height), + WindowManager.LayoutParams.TYPE_VOICE_INTERACTION_STARTING, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + if (ActivityManager.isHighEndGfx()) { + lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + } + lp.gravity = Gravity.BOTTOM | Gravity.START; + lp.setTitle("AssistPreviewPanel"); + lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; + return lp; + } + + private void showOrb() { + maybeSwapSearchIcon(); + mView.show(true /* show */, true /* animate */); + } + + private void startAssist() { + if (getVoiceInteractorSupportsAssistGesture()) { + startVoiceInteractor(); + } else { + startAssistActivity(); + } + } + + private void startAssistActivity() { + if (!mBar.isDeviceProvisioned()) { + return; + } + + // Close Recent Apps if needed + mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL | + CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL); + + final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) + .getAssistIntent(mContext, true, UserHandle.USER_CURRENT); + if (intent == null) { + return; + } + + try { + final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, + R.anim.search_launch_enter, R.anim.search_launch_exit); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + AsyncTask.execute(new Runnable() { + @Override + public void run() { + mContext.startActivityAsUser(intent, opts.toBundle(), + new UserHandle(UserHandle.USER_CURRENT)); + } + }); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Activity not found for " + intent.getAction()); + } + } + + private void startVoiceInteractor() { + try { + mVoiceInteractionManagerService.showSessionForActiveService(mShowCallback); + } catch (RemoteException e) { + Log.w(TAG, "Failed to call showSessionForActiveService", e); + } + } + + private boolean getVoiceInteractorSupportsAssistGesture() { + try { + return mVoiceInteractionManagerService.activeServiceSupportsAssistGesture(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to call activeServiceSupportsAssistGesture", e); + return false; + } + } + + private ComponentName getVoiceInteractorComponentName() { + try { + return mVoiceInteractionManagerService.getActiveServiceComponentName(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to call getActiveServiceComponentName", e); + return null; + } + } + + private boolean isVoiceSessionRunning() { + try { + return mVoiceInteractionManagerService.isSessionRunning(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to call isSessionRunning", e); + return false; + } + } + + public void destroy() { + mWindowManager.removeViewImmediate(mView); + } + + private void maybeSwapSearchIcon() { + Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) + .getAssistIntent(mContext, false, UserHandle.USER_CURRENT); + ComponentName component = null; + boolean isService = false; + if (getVoiceInteractorSupportsAssistGesture()) { + component = getVoiceInteractorComponentName(); + isService = true; + } else if (intent != null) { + component = intent.getComponent(); + } + if (component != null) { + replaceDrawable(mView.getOrb().getLogo(), component, ASSIST_ICON_METADATA_NAME, + isService); + } else { + mView.getOrb().getLogo().setImageDrawable(null); + } + } + + public void replaceDrawable(ImageView v, ComponentName component, String name, + boolean isService) { + if (component != null) { + try { + PackageManager packageManager = mContext.getPackageManager(); + // Look for the search icon specified in the activity meta-data + Bundle metaData = isService + ? packageManager.getServiceInfo( + component, PackageManager.GET_META_DATA).metaData + : packageManager.getActivityInfo( + component, PackageManager.GET_META_DATA).metaData; + if (metaData != null) { + int iconResId = metaData.getInt(name); + if (iconResId != 0) { + Resources res = packageManager.getResourcesForApplication( + component.getPackageName()); + v.setImageDrawable(res.getDrawable(iconResId)); + return; + } + } + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Failed to swap drawable; " + + component.flattenToShortString() + " not found", e); + } catch (Resources.NotFoundException nfe) { + Log.w(TAG, "Failed to swap drawable from " + + component.flattenToShortString(), nfe); + } + } + v.setImageDrawable(null); + } + + private void vibrate() { + if (Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0) { + Resources res = mContext.getResources(); + Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + vibrator.vibrate(res.getInteger(R.integer.config_search_panel_view_vibration_duration), + VIBRATION_ATTRIBUTES); + } + } + + public boolean isAssistantIntentAvailable() { + return ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) + .getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistOrbContainer.java b/packages/SystemUI/src/com/android/systemui/assist/AssistOrbContainer.java new file mode 100644 index 0000000..67017db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistOrbContainer.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.assist; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; + +import com.android.systemui.R; + +public class AssistOrbContainer extends FrameLayout { + + private static final long EXIT_START_DELAY = 150; + + private final Interpolator mLinearOutSlowInInterpolator; + private final Interpolator mFastOutLinearInInterpolator; + + private View mScrim; + private View mNavbarScrim; + private AssistOrbView mOrb; + + private boolean mAnimatingOut; + + public AssistOrbContainer(Context context) { + this(context, null); + } + + public AssistOrbContainer(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public AssistOrbContainer(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, + android.R.interpolator.linear_out_slow_in); + mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, + android.R.interpolator.fast_out_slow_in); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mScrim = findViewById(R.id.assist_orb_scrim); + mNavbarScrim = findViewById(R.id.assist_orb_navbar_scrim); + mOrb = (AssistOrbView) findViewById(R.id.assist_orb); + } + + public void show(final boolean show, boolean animate) { + if (show) { + if (getVisibility() != View.VISIBLE) { + setVisibility(View.VISIBLE); + if (animate) { + startEnterAnimation(); + } else { + reset(); + } + } + } else { + if (animate) { + startExitAnimation(new Runnable() { + @Override + public void run() { + mAnimatingOut = false; + setVisibility(View.GONE); + } + }); + } else { + setVisibility(View.GONE); + } + } + } + + private void reset() { + mAnimatingOut = false; + mOrb.reset(); + mScrim.setAlpha(1f); + mNavbarScrim.setAlpha(1f); + } + + private void startEnterAnimation() { + if (mAnimatingOut) { + return; + } + mOrb.startEnterAnimation(); + mScrim.setAlpha(0f); + mNavbarScrim.setAlpha(0f); + post(new Runnable() { + @Override + public void run() { + mScrim.animate() + .alpha(1f) + .setDuration(300) + .setStartDelay(0) + .setInterpolator(mLinearOutSlowInInterpolator); + mNavbarScrim.animate() + .alpha(1f) + .setDuration(300) + .setStartDelay(0) + .setInterpolator(mLinearOutSlowInInterpolator); + } + }); + } + + private void startExitAnimation(final Runnable endRunnable) { + if (mAnimatingOut) { + if (endRunnable != null) { + endRunnable.run(); + } + return; + } + mAnimatingOut = true; + mOrb.startExitAnimation(EXIT_START_DELAY); + mScrim.animate() + .alpha(0f) + .setDuration(250) + .setStartDelay(EXIT_START_DELAY) + .setInterpolator(mFastOutLinearInInterpolator); + mNavbarScrim.animate() + .alpha(0f) + .setDuration(250) + .setStartDelay(EXIT_START_DELAY) + .setInterpolator(mFastOutLinearInInterpolator) + .withEndAction(endRunnable); + } + + /** + * Whether the panel is showing, or, if it's animating, whether it will be + * when the animation is done. + */ + public boolean isShowing() { + return getVisibility() == View.VISIBLE && !mAnimatingOut; + } + + public AssistOrbView getOrb() { + return mOrb; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistOrbView.java b/packages/SystemUI/src/com/android/systemui/assist/AssistOrbView.java new file mode 100644 index 0000000..a3372a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistOrbView.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.assist; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.systemui.R; + +public class AssistOrbView extends FrameLayout { + + private final int mCircleMinSize; + private final int mBaseMargin; + private final int mStaticOffset; + private final Paint mBackgroundPaint = new Paint(); + private final Rect mCircleRect = new Rect(); + private final Rect mStaticRect = new Rect(); + private final Interpolator mAppearInterpolator; + private final Interpolator mDisappearInterpolator; + private final Interpolator mOvershootInterpolator = new OvershootInterpolator(); + + private boolean mClipToOutline; + private final int mMaxElevation; + private float mOutlineAlpha; + private float mOffset; + private float mCircleSize; + private ImageView mLogo; + private float mCircleAnimationEndValue; + + private ValueAnimator mOffsetAnimator; + private ValueAnimator mCircleAnimator; + + private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + applyCircleSize((float) animation.getAnimatedValue()); + updateElevation(); + } + }; + private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCircleAnimator = null; + } + }; + private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mOffset = (float) animation.getAnimatedValue(); + updateLayout(); + } + }; + + + public AssistOrbView(Context context) { + this(context, null); + } + + public AssistOrbView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + if (mCircleSize > 0.0f) { + outline.setOval(mCircleRect); + } else { + outline.setEmpty(); + } + outline.setAlpha(mOutlineAlpha); + } + }); + setWillNotDraw(false); + mCircleMinSize = context.getResources().getDimensionPixelSize( + R.dimen.assist_orb_size); + mBaseMargin = context.getResources().getDimensionPixelSize( + R.dimen.assist_orb_base_margin); + mStaticOffset = context.getResources().getDimensionPixelSize( + R.dimen.assist_orb_travel_distance); + mMaxElevation = context.getResources().getDimensionPixelSize( + R.dimen.assist_orb_elevation); + mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.linear_out_slow_in); + mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.fast_out_linear_in); + mBackgroundPaint.setAntiAlias(true); + mBackgroundPaint.setColor(getResources().getColor(R.color.assist_orb_color)); + } + + public ImageView getLogo() { + return mLogo; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + drawBackground(canvas); + } + + private void drawBackground(Canvas canvas) { + canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2, + mBackgroundPaint); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mLogo = (ImageView) findViewById(R.id.search_logo); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight()); + if (changed) { + updateCircleRect(mStaticRect, mStaticOffset, true); + } + } + + public void animateCircleSize(float circleSize, long duration, + long startDelay, Interpolator interpolator) { + if (circleSize == mCircleAnimationEndValue) { + return; + } + if (mCircleAnimator != null) { + mCircleAnimator.cancel(); + } + mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize); + mCircleAnimator.addUpdateListener(mCircleUpdateListener); + mCircleAnimator.addListener(mClearAnimatorListener); + mCircleAnimator.setInterpolator(interpolator); + mCircleAnimator.setDuration(duration); + mCircleAnimator.setStartDelay(startDelay); + mCircleAnimator.start(); + mCircleAnimationEndValue = circleSize; + } + + private void applyCircleSize(float circleSize) { + mCircleSize = circleSize; + updateLayout(); + } + + private void updateElevation() { + float t = (mStaticOffset - mOffset) / (float) mStaticOffset; + t = 1.0f - Math.max(t, 0.0f); + float offset = t * mMaxElevation; + setElevation(offset); + } + + /** + * Animates the offset to the edge of the screen. + * + * @param offset The offset to apply. + * @param startDelay The desired start delay if animated. + * + * @param interpolator The desired interpolator if animated. If null, + * a default interpolator will be taken designed for appearing or + * disappearing. + */ + private void animateOffset(float offset, long duration, long startDelay, + Interpolator interpolator) { + if (mOffsetAnimator != null) { + mOffsetAnimator.removeAllListeners(); + mOffsetAnimator.cancel(); + } + mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset); + mOffsetAnimator.addUpdateListener(mOffsetUpdateListener); + mOffsetAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mOffsetAnimator = null; + } + }); + mOffsetAnimator.setInterpolator(interpolator); + mOffsetAnimator.setStartDelay(startDelay); + mOffsetAnimator.setDuration(duration); + mOffsetAnimator.start(); + } + + private void updateLayout() { + updateCircleRect(); + updateLogo(); + invalidateOutline(); + invalidate(); + updateClipping(); + } + + private void updateClipping() { + boolean clip = mCircleSize < mCircleMinSize; + if (clip != mClipToOutline) { + setClipToOutline(clip); + mClipToOutline = clip; + } + } + + private void updateLogo() { + float translationX = (mCircleRect.left + mCircleRect.right) / 2.0f - mLogo.getWidth() / 2.0f; + float translationY = (mCircleRect.top + mCircleRect.bottom) / 2.0f + - mLogo.getHeight() / 2.0f - mCircleMinSize / 7f; + float t = (mStaticOffset - mOffset) / (float) mStaticOffset; + translationY += t * mStaticOffset * 0.1f; + float alpha = 1.0f-t; + alpha = Math.max((alpha - 0.5f) * 2.0f, 0); + mLogo.setImageAlpha((int) (alpha * 255)); + mLogo.setTranslationX(translationX); + mLogo.setTranslationY(translationY); + } + + private void updateCircleRect() { + updateCircleRect(mCircleRect, mOffset, false); + } + + private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) { + int left, top; + float circleSize = useStaticSize ? mCircleMinSize : mCircleSize; + left = (int) (getWidth() - circleSize) / 2; + top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset); + rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize)); + } + + public void startExitAnimation(long delay) { + animateCircleSize(0, 200, delay, mDisappearInterpolator); + animateOffset(0, 200, delay, mDisappearInterpolator); + } + + public void startEnterAnimation() { + applyCircleSize(0); + post(new Runnable() { + @Override + public void run() { + animateCircleSize(mCircleMinSize, 300, 0 /* delay */, mOvershootInterpolator); + animateOffset(mStaticOffset, 400, 0 /* delay */, mAppearInterpolator); + } + }); + } + + public void reset() { + mClipToOutline = false; + mBackgroundPaint.setAlpha(255); + mOutlineAlpha = 1.0f; + } + + @Override + public boolean hasOverlappingRendering() { + // not really true but it's ok during an animation, as it's never permanent + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java index 79600f5..ff6a45a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java @@ -40,6 +40,7 @@ public class DndTile extends QSTile<QSTile.BooleanState> { private static final String ACTION_SET_VISIBLE = "com.android.systemui.dndtile.SET_VISIBLE"; private static final String EXTRA_VISIBLE = "visible"; private static final String PREF_KEY_VISIBLE = "DndTileVisible"; + private static final String PREF_KEY_COMBINED_ICON = "DndTileCombinedIcon"; private final ZenModeController mController; private final DndDetailAdapter mDetailAdapter; @@ -52,7 +53,7 @@ public class DndTile extends QSTile<QSTile.BooleanState> { super(host); mController = host.getZenModeController(); mDetailAdapter = new DndDetailAdapter(); - mVisible = getSharedPrefs(mContext).getBoolean(PREF_KEY_VISIBLE, false); + mVisible = isVisible(host.getContext()); mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_SET_VISIBLE)); } @@ -65,6 +66,14 @@ public class DndTile extends QSTile<QSTile.BooleanState> { return getSharedPrefs(context).getBoolean(PREF_KEY_VISIBLE, false); } + public static void setCombinedIcon(Context context, boolean combined) { + getSharedPrefs(context).edit().putBoolean(PREF_KEY_COMBINED_ICON, combined).commit(); + } + + public static boolean isCombinedIcon(Context context) { + return getSharedPrefs(context).getBoolean(PREF_KEY_COMBINED_ICON, false); + } + @Override public DetailAdapter getDetailAdapter() { return mDetailAdapter; @@ -103,6 +112,12 @@ public class DndTile extends QSTile<QSTile.BooleanState> { state.contentDescription = mContext.getString( R.string.accessibility_quick_settings_dnd_none_on); break; + case Global.ZEN_MODE_ALARMS: + state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on); + state.label = mContext.getString(R.string.quick_settings_dnd_alarms_label); + state.contentDescription = mContext.getString( + R.string.accessibility_quick_settings_dnd_alarms_on); + break; default: state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_off); state.label = mContext.getString(R.string.quick_settings_dnd_label); diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java index fc8f8a5..011c02e 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -19,7 +19,6 @@ package com.android.systemui.recents; import android.app.Activity; import android.app.ActivityOptions; import android.app.SearchManager; -import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; @@ -35,7 +34,6 @@ import android.view.KeyEvent; import android.view.View; import android.view.ViewStub; import android.widget.Toast; - import com.android.systemui.R; import com.android.systemui.recents.misc.DebugTrigger; import com.android.systemui.recents.misc.ReferenceCountedTrigger; @@ -49,8 +47,6 @@ import com.android.systemui.recents.views.DebugOverlayView; import com.android.systemui.recents.views.RecentsView; import com.android.systemui.recents.views.SystemBarScrimViews; import com.android.systemui.recents.views.ViewAnimation; -import com.android.systemui.statusbar.phone.PhoneStatusBar; -import com.android.systemui.SystemUIApplication; import java.lang.ref.WeakReference; import java.lang.reflect.InvocationTargetException; @@ -78,9 +74,9 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView RecentsResizeTaskDialog mResizeTaskDebugDialog; // Search AppWidget - RecentsAppWidgetHost mAppWidgetHost; AppWidgetProviderInfo mSearchAppWidgetInfo; - AppWidgetHostView mSearchAppWidgetHostView; + RecentsAppWidgetHost mAppWidgetHost; + RecentsAppWidgetHostView mSearchAppWidgetHostView; // Runnables to finish the Recents activity FinishRecentsRunnable mFinishLaunchHomeRunnable; @@ -245,7 +241,7 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView if (mEmptyView != null) { mEmptyView.setVisibility(View.GONE); } - if (mRecentsView.hasSearchBar()) { + if (mRecentsView.hasValidSearchBar()) { mRecentsView.setSearchBarVisibility(View.VISIBLE); } else { addSearchBarAppWidgetView(); @@ -295,8 +291,8 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView if (Constants.DebugFlags.App.EnableSearchLayout) { int appWidgetId = mConfig.searchBarAppWidgetId; if (appWidgetId >= 0) { - mSearchAppWidgetHostView = mAppWidgetHost.createView(this, appWidgetId, - mSearchAppWidgetInfo); + mSearchAppWidgetHostView = (RecentsAppWidgetHostView) mAppWidgetHost.createView( + this, appWidgetId, mSearchAppWidgetInfo); Bundle opts = new Bundle(); opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java index 5bae37a..02a7b94 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java @@ -17,6 +17,7 @@ package com.android.systemui.recents; import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import com.android.systemui.recents.misc.SystemServicesProxy; @@ -61,6 +62,12 @@ public class RecentsAppWidgetHost extends AppWidgetHost { } @Override + protected AppWidgetHostView onCreateView(Context context, int appWidgetId, + AppWidgetProviderInfo appWidget) { + return new RecentsAppWidgetHostView(context); + } + + @Override protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidgetInfo) { if (mCb == null) return; diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHostView.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHostView.java new file mode 100644 index 0000000..1ed755a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHostView.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.appwidget.AppWidgetHostView; +import android.content.Context; +import android.widget.RemoteViews; + +public class RecentsAppWidgetHostView extends AppWidgetHostView { + + private Context mContext; + private int mPreviousOrientation; + + public RecentsAppWidgetHostView(Context context) { + super(context); + mContext = context; + } + + @Override + public void updateAppWidget(RemoteViews remoteViews) { + // Store the orientation in which the widget was inflated + updateLastInflationOrientation(); + super.updateAppWidget(remoteViews); + } + + /** + * Updates the last orientation that this widget was inflated. + */ + private void updateLastInflationOrientation() { + mPreviousOrientation = mContext.getResources().getConfiguration().orientation; + } + + /** + * @return whether the search widget was updated while Recents was in a different orientation + * in the background. + */ + public boolean isReinflateRequired() { + // Re-inflate is required if the orientation has changed since last inflated. + int orientation = mContext.getResources().getConfiguration().orientation; + if (mPreviousOrientation != orientation) { + return true; + } + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java index abed7a5..1377975 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -34,6 +34,7 @@ import android.view.WindowInsets; import android.widget.FrameLayout; import com.android.systemui.R; import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsAppWidgetHostView; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.model.RecentsPackageMonitor; @@ -69,7 +70,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV ArrayList<TaskStack> mStacks; List<TaskStackView> mTaskStackViews = new ArrayList<>(); - View mSearchBar; + RecentsAppWidgetHostView mSearchBar; RecentsViewCallbacks mCb; public RecentsView(Context context) { @@ -278,7 +279,7 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV } /** Adds the search bar */ - public void setSearchBar(View searchBar) { + public void setSearchBar(RecentsAppWidgetHostView searchBar) { // Create the search bar (and hide it if we have no recent tasks) if (Constants.DebugFlags.App.EnableSearchLayout) { // Remove the previous search bar if one exists @@ -294,8 +295,8 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV } /** Returns whether there is currently a search bar */ - public boolean hasSearchBar() { - return mSearchBar != null; + public boolean hasValidSearchBar() { + return mSearchBar != null && !mSearchBar.isReinflateRequired(); } /** Sets the visibility of the search bar */ diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 9d349ab..105bf0f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -122,7 +122,7 @@ class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Voi // Prepare all the output metadata mImageTime = System.currentTimeMillis(); - String imageDate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date(mImageTime)); + String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime)); mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate); mScreenshotDir = new File(Environment.getExternalStoragePublicDirectory( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index 55bdcac..f75dd73 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -71,7 +71,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; import android.view.WindowManager; import android.view.WindowManagerGlobal; @@ -79,7 +78,6 @@ import android.view.accessibility.AccessibilityManager; import android.view.animation.AnimationUtils; import android.widget.DateTimeView; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.RemoteViews; import android.widget.TextView; @@ -90,7 +88,6 @@ import com.android.internal.util.NotificationColorUtil; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.R; import com.android.systemui.RecentsComponent; -import com.android.systemui.SearchPanelView; import com.android.systemui.SwipeHelper; import com.android.systemui.SystemUI; import com.android.systemui.recents.Recents; @@ -132,7 +129,6 @@ public abstract class BaseStatusBar extends SystemUI implements protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023; protected static final int MSG_SHOW_NEXT_AFFILIATED_TASK = 1024; protected static final int MSG_SHOW_PREV_AFFILIATED_TASK = 1025; - protected static final int MSG_CLOSE_SEARCH_PANEL = 1027; protected static final int MSG_SHOW_HEADS_UP = 1028; protected static final int MSG_HIDE_HEADS_UP = 1029; protected static final int MSG_ESCALATE_HEADS_UP = 1030; @@ -164,9 +160,6 @@ public abstract class BaseStatusBar extends SystemUI implements protected HeadsUpNotificationView mHeadsUpNotificationView; protected int mHeadsUpNotificationDecay; - // Search panel - protected SearchPanelView mSearchPanelView; - protected int mCurrentUserId = 0; final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>(); @@ -1043,50 +1036,6 @@ public abstract class BaseStatusBar extends SystemUI implements mHandler.sendEmptyMessage(msg); } - @Override - public void showSearchPanel() { - if (mSearchPanelView != null && mSearchPanelView.isAssistantAvailable()) { - mSearchPanelView.show(true, true); - } - } - - @Override - public void hideSearchPanel() { - int msg = MSG_CLOSE_SEARCH_PANEL; - mHandler.removeMessages(msg); - mHandler.sendEmptyMessage(msg); - } - - protected abstract WindowManager.LayoutParams getSearchLayoutParams( - LayoutParams layoutParams); - - protected void updateSearchPanel() { - // Search Panel - boolean visible = false; - if (mSearchPanelView != null) { - visible = mSearchPanelView.isShowing(); - mWindowManager.removeView(mSearchPanelView); - } - - // Provide SearchPanel with a temporary parent to allow layout params to work. - LinearLayout tmpRoot = new LinearLayout(mContext); - mSearchPanelView = (SearchPanelView) LayoutInflater.from(mContext).inflate( - R.layout.status_bar_search_panel, tmpRoot, false); - mSearchPanelView.setOnTouchListener( - new TouchOutsideListener(MSG_CLOSE_SEARCH_PANEL, mSearchPanelView)); - mSearchPanelView.setVisibility(View.GONE); - boolean vertical = mNavigationBarView != null && mNavigationBarView.isVertical(); - mSearchPanelView.setHorizontal(vertical); - - WindowManager.LayoutParams lp = getSearchLayoutParams(mSearchPanelView.getLayoutParams()); - - mWindowManager.addView(mSearchPanelView, lp); - mSearchPanelView.setBar(this); - if (visible) { - mSearchPanelView.show(true, false); - } - } - protected H createHandler() { return new H(); } @@ -1263,35 +1212,7 @@ public abstract class BaseStatusBar extends SystemUI implements case MSG_SHOW_PREV_AFFILIATED_TASK: showRecentsPreviousAffiliatedTask(); break; - case MSG_CLOSE_SEARCH_PANEL: - if (DEBUG) Log.d(TAG, "closing search panel"); - if (mSearchPanelView != null && mSearchPanelView.isShowing()) { - mSearchPanelView.show(false, true); - } - break; - } - } - } - - public class TouchOutsideListener implements View.OnTouchListener { - private int mMsg; - private StatusBarPanel mPanel; - - public TouchOutsideListener(int msg, StatusBarPanel panel) { - mMsg = msg; - mPanel = panel; - } - - public boolean onTouch(View v, MotionEvent ev) { - final int action = ev.getAction(); - if (action == MotionEvent.ACTION_OUTSIDE - || (action == MotionEvent.ACTION_DOWN - && !mPanel.isInContentArea((int)ev.getX(), (int)ev.getY()))) { - mHandler.removeMessages(mMsg); - mHandler.sendEmptyMessage(mMsg); - return true; } - return false; } } @@ -1935,7 +1856,7 @@ public abstract class BaseStatusBar extends SystemUI implements protected abstract void setAreThereNotifications(); protected abstract void updateNotifications(); - protected abstract boolean shouldDisableNavbarGestures(); + public abstract boolean shouldDisableNavbarGestures(); public abstract void addNotification(StatusBarNotification notification, RankingMap ranking, Entry oldEntry); @@ -2241,9 +2162,6 @@ public abstract class BaseStatusBar extends SystemUI implements } public void destroy() { - if (mSearchPanelView != null) { - mWindowManager.removeViewImmediate(mSearchPanelView); - } mContext.unregisterReceiver(mBroadcastReceiver); try { mNotificationListener.unregisterAsSystemService(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 8f88e73..7aa9a90 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -96,8 +96,6 @@ public class CommandQueue extends IStatusBar.Stub { public void toggleRecentApps(); public void preloadRecentApps(); public void cancelPreloadRecentApps(); - public void showSearchPanel(); - public void hideSearchPanel(); public void setWindowState(int window, int state); public void buzzBeepBlinked(); public void notificationLightOff(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DelegateViewHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/DelegateViewHelper.java index 7ae6764..9e2207e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/DelegateViewHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/DelegateViewHelper.java @@ -22,11 +22,12 @@ import android.graphics.RectF; import android.view.MotionEvent; import android.view.View; import com.android.systemui.R; +import com.android.systemui.statusbar.phone.PhoneStatusBar; public class DelegateViewHelper { private View mDelegateView; private View mSourceView; - private BaseStatusBar mBar; + private PhoneStatusBar mBar; private int[] mTempPoint = new int[2]; private float[] mDownPoint = new float[2]; private float mTriggerThreshhold; @@ -45,7 +46,7 @@ public class DelegateViewHelper { mDelegateView = view; } - public void setBar(BaseStatusBar phoneStatusBar) { + public void setBar(PhoneStatusBar phoneStatusBar) { mBar = phoneStatusBar; } @@ -79,7 +80,7 @@ public class DelegateViewHelper { float y = k < historySize ? event.getHistoricalY(k) : event.getY(); final float distance = mSwapXY ? (mDownPoint[0] - x) : (mDownPoint[1] - y); if (distance > mTriggerThreshhold) { - mBar.showSearchPanel(); + mBar.invokeAssistGesture(false /* vibrate */); mPanelShowing = true; break; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java index 7286907..a82afcf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java @@ -59,9 +59,10 @@ public class SignalClusterView private String mWifiDescription; private ArrayList<PhoneState> mPhoneStates = new ArrayList<PhoneState>(); private int mIconTint = Color.WHITE; + private float mDarkIntensity; ViewGroup mWifiGroup; - ImageView mVpn, mWifi, mAirplane, mNoSims; + ImageView mVpn, mWifi, mAirplane, mNoSims, mWifiDark, mNoSimsDark; View mWifiAirplaneSpacer; View mWifiSignalSpacer; LinearLayout mMobileSignalGroup; @@ -115,8 +116,10 @@ public class SignalClusterView mVpn = (ImageView) findViewById(R.id.vpn); mWifiGroup = (ViewGroup) findViewById(R.id.wifi_combo); mWifi = (ImageView) findViewById(R.id.wifi_signal); + mWifiDark = (ImageView) findViewById(R.id.wifi_signal_dark); mAirplane = (ImageView) findViewById(R.id.airplane); mNoSims = (ImageView) findViewById(R.id.no_sims); + mNoSimsDark = (ImageView) findViewById(R.id.no_sims_dark); mWifiAirplaneSpacer = findViewById(R.id.wifi_airplane_spacer); mWifiSignalSpacer = findViewById(R.id.wifi_signal_spacer); mMobileSignalGroup = (LinearLayout) findViewById(R.id.mobile_signal_group); @@ -273,6 +276,7 @@ public class SignalClusterView if (DEBUG) Log.d(TAG, String.format("vpn: %s", mVpnVisible ? "VISIBLE" : "GONE")); if (mWifiVisible) { mWifi.setImageResource(mWifiStrengthId); + mWifiDark.setImageResource(mWifiStrengthId); mWifiGroup.setContentDescription(mWifiDescription); mWifiGroup.setVisibility(View.VISIBLE); } else { @@ -317,15 +321,17 @@ public class SignalClusterView } mNoSims.setVisibility(mNoSimsVisible ? View.VISIBLE : View.GONE); + mNoSimsDark.setVisibility(mNoSimsVisible ? View.VISIBLE : View.GONE); boolean anythingVisible = mNoSimsVisible || mWifiVisible || mIsAirplaneMode || anyMobileVisible || mVpnVisible; setPaddingRelative(0, 0, anythingVisible ? mEndPadding : mEndPaddingNothingVisible, 0); } - public void setIconTint(int tint) { - boolean changed = tint != mIconTint; + public void setIconTint(int tint, float darkIntensity) { + boolean changed = tint != mIconTint || darkIntensity != mDarkIntensity; mIconTint = tint; + mDarkIntensity = darkIntensity; if (changed && isAttachedToWindow()) { applyIconTint(); } @@ -333,14 +339,19 @@ public class SignalClusterView private void applyIconTint() { setTint(mVpn, mIconTint); - setTint(mWifi, mIconTint); - setTint(mNoSims, mIconTint); setTint(mAirplane, mIconTint); + applyDarkIntensity(mDarkIntensity, mNoSims, mNoSimsDark); + applyDarkIntensity(mDarkIntensity, mWifi, mWifiDark); for (int i = 0; i < mPhoneStates.size(); i++) { - mPhoneStates.get(i).setIconTint(mIconTint); + mPhoneStates.get(i).setIconTint(mIconTint, mDarkIntensity); } } + private void applyDarkIntensity(float darkIntensity, View lightIcon, View darkIcon) { + lightIcon.setAlpha(1 - darkIntensity); + darkIcon.setAlpha(darkIntensity); + } + private void setTint(ImageView v, int tint) { v.setImageTintMode(PorterDuff.Mode.SRC_ATOP); v.setImageTintList(ColorStateList.valueOf(tint)); @@ -354,7 +365,7 @@ public class SignalClusterView private String mMobileDescription, mMobileTypeDescription; private ViewGroup mMobileGroup; - private ImageView mMobile, mMobileType; + private ImageView mMobile, mMobileDark, mMobileType; public PhoneState(int subId, Context context) { ViewGroup root = (ViewGroup) LayoutInflater.from(context) @@ -366,12 +377,14 @@ public class SignalClusterView public void setViews(ViewGroup root) { mMobileGroup = root; mMobile = (ImageView) root.findViewById(R.id.mobile_signal); + mMobileDark = (ImageView) root.findViewById(R.id.mobile_signal_dark); mMobileType = (ImageView) root.findViewById(R.id.mobile_type); } public boolean apply(boolean isSecondaryIcon) { if (mMobileVisible && !mIsAirplaneMode) { mMobile.setImageResource(mMobileStrengthId); + mMobileDark.setImageResource(mMobileStrengthId); mMobileType.setImageResource(mMobileTypeId); mMobileGroup.setContentDescription(mMobileTypeDescription + " " + mMobileDescription); @@ -385,6 +398,8 @@ public class SignalClusterView 0, 0, 0); mMobile.setPaddingRelative(mIsMobileTypeIconWide ? mWideTypeIconStartPadding : 0, 0, 0, 0); + mMobileDark.setPaddingRelative(mIsMobileTypeIconWide ? mWideTypeIconStartPadding : 0, + 0, 0, 0); if (DEBUG) Log.d(TAG, String.format("mobile: %s sig=%d typ=%d", (mMobileVisible ? "VISIBLE" : "GONE"), mMobileStrengthId, mMobileTypeId)); @@ -401,8 +416,8 @@ public class SignalClusterView } } - public void setIconTint(int tint) { - setTint(mMobile, tint); + public void setIconTint(int tint, float darkIntensity) { + applyDarkIntensity(darkIntensity, mMobile, mMobileDark); setTint(mMobileType, tint); } } 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 12ff399..c62ad66 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -50,6 +50,8 @@ import com.android.systemui.R; import com.android.systemui.statusbar.BaseStatusBar; import com.android.systemui.statusbar.DelegateViewHelper; import com.android.systemui.statusbar.policy.DeadZone; +import com.android.systemui.statusbar.policy.KeyButtonView; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -195,7 +197,7 @@ public class NavigationBarView extends LinearLayout { mDelegateHelper.setDelegateView(view); } - public void setBar(BaseStatusBar phoneStatusBar) { + public void setBar(PhoneStatusBar phoneStatusBar) { mTaskSwitchHelper.setBar(phoneStatusBar); mDelegateHelper.setBar(phoneStatusBar); } @@ -261,8 +263,8 @@ public class NavigationBarView extends LinearLayout { return mCurrentView.findViewById(R.id.back); } - public View getHomeButton() { - return mCurrentView.findViewById(R.id.home); + public KeyButtonView getHomeButton() { + return (KeyButtonView) mCurrentView.findViewById(R.id.home); } public View getImeSwitchButton() { 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 2c389fb..ad78f6a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -49,7 +49,6 @@ import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.PixelFormat; import android.graphics.Point; @@ -121,6 +120,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.qs.QSPanel; import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.statusbar.ActivatableNotificationView; +import com.android.systemui.assist.AssistGestureManager; import com.android.systemui.statusbar.BackDropView; import com.android.systemui.statusbar.BaseStatusBar; import com.android.systemui.statusbar.CommandQueue; @@ -225,8 +225,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, /** Allow some time inbetween the long press for back and recents. */ private static final int LOCK_TO_APP_GESTURE_TOLERENCE = 200; - private int mLightModeIconColor; - PhoneStatusBarPolicy mIconPolicy; // These are no longer handled by the policy, because we need custom strategies for them @@ -323,6 +321,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private int mNavigationIconHints = 0; private HandlerThread mHandlerThread; + private AssistGestureManager mAssistGestureManager; + // 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()) { @@ -535,7 +535,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateDisplaySize(); mScrimSrcModeEnabled = mContext.getResources().getBoolean( R.bool.config_status_bar_scrim_behind_use_src); - mLightModeIconColor = mContext.getColor(R.color.light_mode_icon_color); super.start(); // calls createAndAddWindows() @@ -642,8 +641,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, new NavigationBarView.OnVerticalChangedListener() { @Override public void onVerticalChanged(boolean isVertical) { - if (mSearchPanelView != null) { - mSearchPanelView.setHorizontal(isVertical); + if (mAssistGestureManager != null) { + mAssistGestureManager.onConfigurationChanged(); } mNotificationPanel.setQsScrimEnabled(!isVertical); } @@ -834,6 +833,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mBroadcastReceiver.onReceive(mContext, new Intent(pm.isScreenOn() ? Intent.ACTION_SCREEN_ON : Intent.ACTION_SCREEN_OFF)); + mAssistGestureManager = new AssistGestureManager(this, context); + // receive broadcasts IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); @@ -953,60 +954,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mStatusBarWindow; } - @Override - protected WindowManager.LayoutParams getSearchLayoutParams(LayoutParams layoutParams) { - boolean opaque = false; - WindowManager.LayoutParams lp = new WindowManager.LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, - WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM - | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, - (opaque ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT)); - if (ActivityManager.isHighEndGfx()) { - lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; - } - lp.gravity = Gravity.BOTTOM | Gravity.START; - lp.setTitle("SearchPanel"); - lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED - | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; - return lp; - } - - @Override - protected void updateSearchPanel() { - super.updateSearchPanel(); - if (mNavigationBarView != null) { - mNavigationBarView.setDelegateView(mSearchPanelView); - } - } - - @Override - public void showSearchPanel() { - super.showSearchPanel(); - mHandler.removeCallbacks(mShowSearchPanel); - - // we want to freeze the sysui state wherever it is - mSearchPanelView.setSystemUiVisibility(mSystemUiVisibility); - - if (mNavigationBarView != null) { - WindowManager.LayoutParams lp = - (android.view.WindowManager.LayoutParams) mNavigationBarView.getLayoutParams(); - lp.flags &= ~WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; - mWindowManager.updateViewLayout(mNavigationBarView, lp); - } - } - - @Override - public void hideSearchPanel() { - super.hideSearchPanel(); - if (mNavigationBarView != null) { - WindowManager.LayoutParams lp = - (android.view.WindowManager.LayoutParams) mNavigationBarView.getLayoutParams(); - lp.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; - mWindowManager.updateViewLayout(mNavigationBarView, lp); - } + public void invokeAssistGesture(boolean vibrate) { + mHandler.removeCallbacks(mInvokeAssist); + mAssistGestureManager.onGestureInvoked(vibrate); } public int getStatusBarHeight() { @@ -1036,30 +986,33 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, }; private int mShowSearchHoldoff = 0; - private Runnable mShowSearchPanel = new Runnable() { + private Runnable mInvokeAssist = new Runnable() { public void run() { - showSearchPanel(); + invokeAssistGesture(true /* vibrate */); awakenDreams(); + if (mNavigationBarView != null) { + mNavigationBarView.getHomeButton().abortCurrentGesture(); + } } }; View.OnTouchListener mHomeActionListener = new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { - switch(event.getAction()) { + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - if (!shouldDisableNavbarGestures()) { - mHandler.removeCallbacks(mShowSearchPanel); - mHandler.postDelayed(mShowSearchPanel, mShowSearchHoldoff); - } - break; + if (!shouldDisableNavbarGestures()) { + mHandler.removeCallbacks(mInvokeAssist); + mHandler.postDelayed(mInvokeAssist, mShowSearchHoldoff); + } + break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mHandler.removeCallbacks(mShowSearchPanel); - awakenDreams(); - break; - } - return false; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mHandler.removeCallbacks(mInvokeAssist); + awakenDreams(); + break; + } + return false; } }; @@ -1083,7 +1036,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mNavigationBarView.getBackButton().setLongClickable(true); mNavigationBarView.getBackButton().setOnLongClickListener(mLongPressBackRecentsListener); mNavigationBarView.getHomeButton().setOnTouchListener(mHomeActionListener); - updateSearchPanel(); + mAssistGestureManager.onConfigurationChanged(); } // For small-screen devices (read: phones) that lack hardware navigation buttons @@ -2060,11 +2013,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } } - if ((flags & CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL) == 0) { - mHandler.removeMessages(MSG_CLOSE_SEARCH_PANEL); - mHandler.sendEmptyMessage(MSG_CLOSE_SEARCH_PANEL); - } - if (mStatusBarWindow != null) { // release focus immediately to kick off focus change transition mStatusBarWindowManager.setStatusBarFocusable(false); @@ -2324,9 +2272,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, boolean allowLight = isTransparentBar && !mBatteryController.isPowerSave(); boolean light = (vis & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0; - mIconController.setIconTint( - (allowLight && light) ? mLightModeIconColor : Color.WHITE); - + mIconController.setIconsDark(allowLight && light); } // restore the recents bit if (wasRecentsVisible) { @@ -2989,7 +2935,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, }; @Override - protected boolean shouldDisableNavbarGestures() { + public boolean shouldDisableNavbarGestures() { return !isDeviceProvisioned() || mExpandedVisible || (mDisabled & StatusBarManager.DISABLE_SEARCH) != 0; @@ -3058,6 +3004,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mHandlerThread = null; } mContext.unregisterReceiver(mBroadcastReceiver); + mAssistGestureManager.destroy(); } private boolean mDemoModeAllowed; 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 2236aae..ac93ced 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -199,7 +199,7 @@ public class PhoneStatusBarPolicy { int volumeIconId = 0; String volumeDescription = null; - if (DndTile.isVisible(mContext)) { + if (DndTile.isVisible(mContext) || DndTile.isCombinedIcon(mContext)) { zenVisible = mZen != Global.ZEN_MODE_OFF; zenIconId = R.drawable.stat_sys_dnd; zenDescription = mContext.getString(R.string.quick_settings_dnd_label); @@ -218,7 +218,7 @@ public class PhoneStatusBarPolicy { volumeVisible = true; volumeIconId = R.drawable.stat_sys_ringer_silent; volumeDescription = mContext.getString(R.string.accessibility_ringer_silent); - } else if (mZen != Global.ZEN_MODE_NO_INTERRUPTIONS && + } else if (mZen != Global.ZEN_MODE_NO_INTERRUPTIONS && mZen != Global.ZEN_MODE_ALARMS && audioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) { volumeVisible = true; volumeIconId = R.drawable.stat_sys_ringer_vibrate; 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 c49f620..45da297 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.phone; +import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; -import android.graphics.PorterDuff; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; @@ -74,12 +74,16 @@ public class StatusBarIconController { private int mIconHPadding; private int mIconTint = Color.WHITE; + private float mDarkIntensity; private boolean mTransitionPending; private boolean mTintChangePending; - private int mPendingIconTint; + private float mPendingDarkIntensity; private ValueAnimator mTintAnimator; + private int mDarkModeIconColorSingleTone; + private int mLightModeIconColorSingleTone; + private final Handler mHandler; private boolean mTransitionDeferring; private long mTransitionDeferringStartTime; @@ -111,6 +115,8 @@ public class StatusBarIconController { android.R.interpolator.linear_out_slow_in); mFastOutSlowIn = AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); + mDarkModeIconColorSingleTone = context.getColor(R.color.dark_mode_icon_color_single_tone); + mLightModeIconColorSingleTone = context.getColor(R.color.light_mode_icon_color_single_tone); mHandler = new Handler(); updateResources(); } @@ -296,30 +302,31 @@ public class StatusBarIconController { } } - public void setIconTint(int tint) { + public void setIconsDark(boolean dark) { if (mTransitionPending) { - deferIconTintChange(tint); + deferIconTintChange(dark ? 1.0f : 0.0f); } else if (mTransitionDeferring) { - animateIconTint(tint, + animateIconTint(dark ? 1.0f : 0.0f, Math.max(0, mTransitionDeferringStartTime - SystemClock.uptimeMillis()), mTransitionDeferringDuration); } else { - animateIconTint(tint, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION); + animateIconTint(dark ? 1.0f : 0.0f, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION); } } - private void animateIconTint(int targetTint, long delay, long duration) { + private void animateIconTint(float targetDarkIntensity, long delay, + long duration) { if (mTintAnimator != null) { mTintAnimator.cancel(); } - if (mIconTint == targetTint) { + if (mDarkIntensity == targetDarkIntensity) { return; } - mTintAnimator = ValueAnimator.ofArgb(mIconTint, targetTint); + mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity); mTintAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { - setIconTintInternal((Integer) animation.getAnimatedValue()); + setIconTintInternal((Float) animation.getAnimatedValue()); } }); mTintAnimator.setDuration(duration); @@ -327,17 +334,20 @@ public class StatusBarIconController { mTintAnimator.setInterpolator(mFastOutSlowIn); mTintAnimator.start(); } - private void setIconTintInternal(int tint) { - mIconTint = tint; + + private void setIconTintInternal(float darkIntensity) { + mDarkIntensity = darkIntensity; + mIconTint = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, + mLightModeIconColorSingleTone, mDarkModeIconColorSingleTone); applyIconTint(); } - private void deferIconTintChange(int tint) { - if (mTintChangePending && tint == mPendingIconTint) { + private void deferIconTintChange(float darkIntensity) { + if (mTintChangePending && darkIntensity == mPendingDarkIntensity) { return; } mTintChangePending = true; - mPendingIconTint = tint; + mPendingDarkIntensity = darkIntensity; } private void applyIconTint() { @@ -345,9 +355,9 @@ public class StatusBarIconController { StatusBarIconView v = (StatusBarIconView) mStatusIcons.getChildAt(i); v.setImageTintList(ColorStateList.valueOf(mIconTint)); } - mSignalCluster.setIconTint(mIconTint); + mSignalCluster.setIconTint(mIconTint, mDarkIntensity); mMoreIcon.setImageTintList(ColorStateList.valueOf(mIconTint)); - mBatteryMeterView.setIconTint(mIconTint); + mBatteryMeterView.setDarkIntensity(mDarkIntensity); mClock.setTextColor(mIconTint); applyNotificationIconsTint(); } @@ -358,7 +368,6 @@ public class StatusBarIconController { boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L)); boolean colorize = !isPreL || isGrayscale(v); if (colorize) { - v.setImageTintMode(PorterDuff.Mode.SRC_ATOP); v.setImageTintList(ColorStateList.valueOf(mIconTint)); } } @@ -381,7 +390,7 @@ public class StatusBarIconController { public void appTransitionCancelled() { if (mTransitionPending && mTintChangePending) { mTintChangePending = false; - animateIconTint(mPendingIconTint, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION); + animateIconTint(mPendingDarkIntensity, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION); } mTransitionPending = false; } @@ -389,7 +398,7 @@ public class StatusBarIconController { public void appTransitionStarting(long startTime, long duration) { if (mTransitionPending && mTintChangePending) { mTintChangePending = false; - animateIconTint(mPendingIconTint, + animateIconTint(mPendingDarkIntensity, Math.max(0, startTime - SystemClock.uptimeMillis()), duration); 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 a18daed..6bc51fa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java @@ -48,6 +48,7 @@ public class KeyButtonView extends ImageView { private int mTouchSlop; private boolean mSupportsLongpress = true; private AudioManager mAudioManager; + private boolean mGestureAborted; private final Runnable mCheckLongPress = new Runnable() { public void run() { @@ -126,10 +127,15 @@ public class KeyButtonView extends ImageView { public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); int x, y; + if (action == MotionEvent.ACTION_DOWN) { + mGestureAborted = false; + } + if (mGestureAborted) { + return false; + } switch (action) { case MotionEvent.ACTION_DOWN: - //Log.d("KeyButtonView", "press"); mDownTime = SystemClock.uptimeMillis(); setPressed(true); if (mCode != 0) { @@ -203,6 +209,11 @@ public class KeyButtonView extends ImageView { InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } + + public void abortCurrentGesture() { + setPressed(false); + mGestureAborted = true; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java index d1e1b71..c272e48 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java @@ -106,12 +106,6 @@ public class TvStatusBar extends BaseStatusBar { } @Override - protected WindowManager.LayoutParams getSearchLayoutParams( - LayoutParams layoutParams) { - return null; - } - - @Override protected void setAreThereNotifications() { } @@ -120,7 +114,7 @@ public class TvStatusBar extends BaseStatusBar { } @Override - protected boolean shouldDisableNavbarGestures() { + public boolean shouldDisableNavbarGestures() { return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarPanel.java b/packages/SystemUI/src/com/android/systemui/volume/D.java index 272c321..db7c853 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarPanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/D.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,10 @@ * limitations under the License. */ -package com.android.systemui.statusbar; +package com.android.systemui.volume; -public interface StatusBarPanel { - public boolean isInContentArea(int x, int y); +import android.util.Log; + +class D { + public static boolean BUG = Log.isLoggable("volume", Log.DEBUG); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/Events.java b/packages/SystemUI/src/com/android/systemui/volume/Events.java new file mode 100644 index 0000000..12dca94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/Events.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.media.AudioManager; +import android.media.AudioSystem; +import android.provider.Settings.Global; +import android.util.Log; + +import com.android.systemui.volume.VolumeDialogController.State; + +import java.util.Arrays; + +/** + * Interesting events related to the volume. + */ +public class Events { + private static final String TAG = Util.logTag(Events.class); + + public static final int EVENT_SHOW_DIALOG = 0; // (reason|int) (keyguard|bool) + public static final int EVENT_DISMISS_DIALOG = 1; // (reason|int) + public static final int EVENT_ACTIVE_STREAM_CHANGED = 2; // (stream|int) + public static final int EVENT_EXPAND = 3; // (expand|bool) + public static final int EVENT_KEY = 4; + public static final int EVENT_COLLECTION_STARTED = 5; + public static final int EVENT_COLLECTION_STOPPED = 6; + public static final int EVENT_ICON_CLICK = 7; // (stream|int) (icon_state|int) + public static final int EVENT_SETTINGS_CLICK = 8; + public static final int EVENT_TOUCH_LEVEL_CHANGED = 9; // (stream|int) (level|int) + public static final int EVENT_LEVEL_CHANGED = 10; // (stream|int) (level|int) + public static final int EVENT_INTERNAL_RINGER_MODE_CHANGED = 11; // (mode|int) + public static final int EVENT_EXTERNAL_RINGER_MODE_CHANGED = 12; // (mode|int) + public static final int EVENT_ZEN_MODE_CHANGED = 13; // (mode|int) + public static final int EVENT_SUPPRESSOR_CHANGED = 14; // (component|string) (name|string) + public static final int EVENT_MUTE_CHANGED = 15; // (stream|int) (muted|bool) + + private static final String[] EVENT_TAGS = { + "show_dialog", + "dismiss_dialog", + "active_stream_changed", + "expand", + "key", + "collection_started", + "collection_stopped", + "icon_click", + "settings_click", + "touch_level_changed", + "level_changed", + "internal_ringer_mode_changed", + "external_ringer_mode_changed", + "zen_mode_changed", + "suppressor_changed", + "mute_changed", + }; + + public static final int DISMISS_REASON_UNKNOWN = 0; + public static final int DISMISS_REASON_TOUCH_OUTSIDE = 1; + public static final int DISMISS_REASON_VOLUME_CONTROLLER = 2; + public static final int DISMISS_REASON_TIMEOUT = 3; + public static final int DISMISS_REASON_SCREEN_OFF = 4; + public static final int DISMISS_REASON_SETTINGS_CLICKED = 5; + public static final int DISMISS_REASON_DONE_CLICKED = 6; + public static final String[] DISMISS_REASONS = { + "unknown", + "touch_outside", + "volume_controller", + "timeout", + "screen_off", + "settings_clicked", + "done_clicked", + }; + + public static final int SHOW_REASON_UNKNOWN = 0; + public static final int SHOW_REASON_VOLUME_CHANGED = 1; + public static final int SHOW_REASON_REMOTE_VOLUME_CHANGED = 2; + public static final String[] SHOW_REASONS = { + "unknown", + "volume_changed", + "remote_volume_changed" + }; + + public static final int ICON_STATE_UNKNOWN = 0; + public static final int ICON_STATE_UNMUTE = 1; + public static final int ICON_STATE_MUTE = 2; + public static final int ICON_STATE_VIBRATE = 3; + + public static Callback sCallback; + + public static void writeEvent(int tag, Object... list) { + final long time = System.currentTimeMillis(); + final StringBuilder sb = new StringBuilder("writeEvent ").append(EVENT_TAGS[tag]); + if (list != null && list.length > 0) { + sb.append(" "); + switch (tag) { + case EVENT_SHOW_DIALOG: + sb.append(SHOW_REASONS[(Integer) list[0]]).append(" keyguard=").append(list[1]); + break; + case EVENT_EXPAND: + sb.append(list[0]); + break; + case EVENT_DISMISS_DIALOG: + sb.append(DISMISS_REASONS[(Integer) list[0]]); + break; + case EVENT_ACTIVE_STREAM_CHANGED: + sb.append(AudioSystem.streamToString((Integer) list[0])); + break; + case EVENT_ICON_CLICK: + sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ') + .append(iconStateToString((Integer) list[1])); + break; + case EVENT_TOUCH_LEVEL_CHANGED: + case EVENT_LEVEL_CHANGED: + case EVENT_MUTE_CHANGED: + sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ') + .append(list[1]); + break; + case EVENT_INTERNAL_RINGER_MODE_CHANGED: + case EVENT_EXTERNAL_RINGER_MODE_CHANGED: + sb.append(ringerModeToString((Integer) list[0])); + break; + case EVENT_ZEN_MODE_CHANGED: + sb.append(zenModeToString((Integer) list[0])); + break; + case EVENT_SUPPRESSOR_CHANGED: + sb.append(list[0]).append(' ').append(list[1]); + break; + default: + sb.append(Arrays.asList(list)); + break; + } + } + Log.i(TAG, sb.toString()); + if (sCallback != null) { + sCallback.writeEvent(time, tag, list); + } + } + + public static void writeState(long time, State state) { + if (sCallback != null) { + sCallback.writeState(time, state); + } + } + + private static String iconStateToString(int iconState) { + switch (iconState) { + case ICON_STATE_UNMUTE: return "unmute"; + case ICON_STATE_MUTE: return "mute"; + case ICON_STATE_VIBRATE: return "vibrate"; + default: return "unknown_state_" + iconState; + } + } + + private static String ringerModeToString(int ringerMode) { + switch (ringerMode) { + case AudioManager.RINGER_MODE_SILENT: return "silent"; + case AudioManager.RINGER_MODE_VIBRATE: return "vibrate"; + case AudioManager.RINGER_MODE_NORMAL: return "normal"; + default: return "unknown"; + } + } + + private static String zenModeToString(int zenMode) { + switch (zenMode) { + case Global.ZEN_MODE_OFF: return "off"; + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: return "important_interruptions"; + case Global.ZEN_MODE_ALARMS: return "alarms"; + case Global.ZEN_MODE_NO_INTERRUPTIONS: return "no_interruptions"; + default: return "unknown"; + } + } + + public interface Callback { + void writeEvent(long time, int tag, Object[] list); + void writeState(long time, State state); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/MediaSessions.java b/packages/SystemUI/src/com/android/systemui/volume/MediaSessions.java new file mode 100644 index 0000000..712ea27 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/MediaSessions.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.media.IRemoteVolumeController; +import android.media.MediaMetadata; +import android.media.session.ISessionController; +import android.media.session.MediaController; +import android.media.session.MediaController.PlaybackInfo; +import android.media.session.MediaSession.QueueItem; +import android.media.session.MediaSession.Token; +import android.media.session.MediaSessionManager; +import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Convenience client for all media session updates. Provides a callback interface for events + * related to remote media sessions. + */ +public class MediaSessions { + private static final String TAG = Util.logTag(MediaSessions.class); + + private static final boolean USE_SERVICE_LABEL = false; + + private final Context mContext; + private final H mHandler; + private final MediaSessionManager mMgr; + private final Map<Token, MediaControllerRecord> mRecords = new HashMap<>(); + private final Callbacks mCallbacks; + + private boolean mInit; + + public MediaSessions(Context context, Looper looper, Callbacks callbacks) { + mContext = context; + mHandler = new H(looper); + mMgr = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); + mCallbacks = callbacks; + } + + public void dump(PrintWriter writer) { + writer.println(getClass().getSimpleName() + " state:"); + writer.print(" mInit: "); writer.println(mInit); + writer.print(" mRecords.size: "); writer.println(mRecords.size()); + int i = 0; + for (MediaControllerRecord r : mRecords.values()) { + dump(++i, writer, r.controller); + } + } + + public void init() { + if (D.BUG) Log.d(TAG, "init"); + // will throw if no permission + mMgr.addOnActiveSessionsChangedListener(mSessionsListener, null, mHandler); + mInit = true; + postUpdateSessions(); + mMgr.setRemoteVolumeController(mRvc); + } + + protected void postUpdateSessions() { + if (!mInit) return; + mHandler.sendEmptyMessage(H.UPDATE_SESSIONS); + } + + public void destroy() { + if (D.BUG) Log.d(TAG, "destroy"); + mInit = false; + mMgr.removeOnActiveSessionsChangedListener(mSessionsListener); + } + + public void setVolume(Token token, int level) { + final MediaControllerRecord r = mRecords.get(token); + if (r == null) { + Log.w(TAG, "setVolume: No record found for token " + token); + return; + } + if (D.BUG) Log.d(TAG, "Setting level to " + level); + r.controller.setVolumeTo(level, 0); + } + + private void onRemoteVolumeChangedH(ISessionController session, int flags) { + final MediaController controller = new MediaController(mContext, session); + if (D.BUG) Log.d(TAG, "remoteVolumeChangedH " + controller.getPackageName() + " " + + Util.audioManagerFlagsToString(flags)); + final Token token = controller.getSessionToken(); + mCallbacks.onRemoteVolumeChanged(token, flags); + } + + private void onUpdateRemoteControllerH(ISessionController session) { + final MediaController controller = session != null ? new MediaController(mContext, session) + : null; + final String pkg = controller != null ? controller.getPackageName() : null; + if (D.BUG) Log.d(TAG, "updateRemoteControllerH " + pkg); + // this may be our only indication that a remote session is changed, refresh + postUpdateSessions(); + } + + protected void onActiveSessionsUpdatedH(List<MediaController> controllers) { + if (D.BUG) Log.d(TAG, "onActiveSessionsUpdatedH n=" + controllers.size()); + final Set<Token> toRemove = new HashSet<Token>(mRecords.keySet()); + for (MediaController controller : controllers) { + final Token token = controller.getSessionToken(); + final PlaybackInfo pi = controller.getPlaybackInfo(); + toRemove.remove(token); + if (!mRecords.containsKey(token)) { + final MediaControllerRecord r = new MediaControllerRecord(controller); + r.name = getControllerName(controller); + mRecords.put(token, r); + controller.registerCallback(r, mHandler); + } + final MediaControllerRecord r = mRecords.get(token); + final boolean remote = isRemote(pi); + if (remote) { + updateRemoteH(token, r.name, pi); + r.sentRemote = true; + } + } + for (Token t : toRemove) { + final MediaControllerRecord r = mRecords.get(t); + r.controller.unregisterCallback(r); + mRecords.remove(t); + if (D.BUG) Log.d(TAG, "Removing " + r.name + " sentRemote=" + r.sentRemote); + if (r.sentRemote) { + mCallbacks.onRemoteRemoved(t); + r.sentRemote = false; + } + } + } + + private static boolean isRemote(PlaybackInfo pi) { + return pi != null && pi.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE; + } + + protected String getControllerName(MediaController controller) { + final PackageManager pm = mContext.getPackageManager(); + final String pkg = controller.getPackageName(); + try { + if (USE_SERVICE_LABEL) { + final List<ResolveInfo> ris = pm.queryIntentServices( + new Intent("android.media.MediaRouteProviderService").setPackage(pkg), 0); + if (ris != null) { + for (ResolveInfo ri : ris) { + if (ri.serviceInfo == null) continue; + if (pkg.equals(ri.serviceInfo.packageName)) { + final String serviceLabel = + Objects.toString(ri.serviceInfo.loadLabel(pm), "").trim(); + if (serviceLabel.length() > 0) { + return serviceLabel; + } + } + } + } + } + final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0); + final String appLabel = Objects.toString(ai.loadLabel(pm), "").trim(); + if (appLabel.length() > 0) { + return appLabel; + } + } catch (NameNotFoundException e) { } + return pkg; + } + + private void updateRemoteH(Token token, String name, PlaybackInfo pi) { + if (mCallbacks != null) { + mCallbacks.onRemoteUpdate(token, name, pi); + } + } + + private static void dump(int n, PrintWriter writer, MediaController c) { + writer.println(" Controller " + n + ": " + c.getPackageName()); + final Bundle extras = c.getExtras(); + final long flags = c.getFlags(); + final MediaMetadata mm = c.getMetadata(); + final PlaybackInfo pi = c.getPlaybackInfo(); + final PlaybackState playbackState = c.getPlaybackState(); + final List<QueueItem> queue = c.getQueue(); + final CharSequence queueTitle = c.getQueueTitle(); + final int ratingType = c.getRatingType(); + final PendingIntent sessionActivity = c.getSessionActivity(); + + writer.println(" PlaybackState: " + Util.playbackStateToString(playbackState)); + writer.println(" PlaybackInfo: " + Util.playbackInfoToString(pi)); + if (mm != null) { + writer.println(" MediaMetadata.desc=" + mm.getDescription()); + } + writer.println(" RatingType: " + ratingType); + writer.println(" Flags: " + flags); + if (extras != null) { + writer.println(" Extras:"); + for (String key : extras.keySet()) { + writer.println(" " + key + "=" + extras.get(key)); + } + } + if (queueTitle != null) { + writer.println(" QueueTitle: " + queueTitle); + } + if (queue != null && !queue.isEmpty()) { + writer.println(" Queue:"); + for (QueueItem qi : queue) { + writer.println(" " + qi); + } + } + if (pi != null) { + writer.println(" sessionActivity: " + sessionActivity); + } + } + + public static void dumpMediaSessions(Context context) { + final MediaSessionManager mgr = (MediaSessionManager) context + .getSystemService(Context.MEDIA_SESSION_SERVICE); + try { + final List<MediaController> controllers = mgr.getActiveSessions(null); + final int N = controllers.size(); + if (D.BUG) Log.d(TAG, N + " controllers"); + for (int i = 0; i < N; i++) { + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw, true); + dump(i + 1, pw, controllers.get(i)); + if (D.BUG) Log.d(TAG, sw.toString()); + } + } catch (SecurityException e) { + Log.w(TAG, "Not allowed to get sessions", e); + } + } + + private final class MediaControllerRecord extends MediaController.Callback { + private final MediaController controller; + + private boolean sentRemote; + private String name; + + private MediaControllerRecord(MediaController controller) { + this.controller = controller; + } + + private String cb(String method) { + return method + " " + controller.getPackageName() + " "; + } + + @Override + public void onAudioInfoChanged(PlaybackInfo info) { + if (D.BUG) Log.d(TAG, cb("onAudioInfoChanged") + Util.playbackInfoToString(info) + + " sentRemote=" + sentRemote); + final boolean remote = isRemote(info); + if (!remote && sentRemote) { + mCallbacks.onRemoteRemoved(controller.getSessionToken()); + sentRemote = false; + } else if (remote) { + updateRemoteH(controller.getSessionToken(), name, info); + sentRemote = true; + } + } + + @Override + public void onExtrasChanged(Bundle extras) { + if (D.BUG) Log.d(TAG, cb("onExtrasChanged") + extras); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + if (D.BUG) Log.d(TAG, cb("onMetadataChanged") + Util.mediaMetadataToString(metadata)); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (D.BUG) Log.d(TAG, cb("onPlaybackStateChanged") + Util.playbackStateToString(state)); + } + + @Override + public void onQueueChanged(List<QueueItem> queue) { + if (D.BUG) Log.d(TAG, cb("onQueueChanged") + queue); + } + + @Override + public void onQueueTitleChanged(CharSequence title) { + if (D.BUG) Log.d(TAG, cb("onQueueTitleChanged") + title); + } + + @Override + public void onSessionDestroyed() { + if (D.BUG) Log.d(TAG, cb("onSessionDestroyed")); + } + + @Override + public void onSessionEvent(String event, Bundle extras) { + if (D.BUG) Log.d(TAG, cb("onSessionEvent") + "event=" + event + " extras=" + extras); + } + } + + private final OnActiveSessionsChangedListener mSessionsListener = + new OnActiveSessionsChangedListener() { + @Override + public void onActiveSessionsChanged(List<MediaController> controllers) { + onActiveSessionsUpdatedH(controllers); + } + }; + + private final IRemoteVolumeController mRvc = new IRemoteVolumeController.Stub() { + @Override + public void remoteVolumeChanged(ISessionController session, int flags) + throws RemoteException { + mHandler.obtainMessage(H.REMOTE_VOLUME_CHANGED, flags, 0, session).sendToTarget(); + } + + @Override + public void updateRemoteController(final ISessionController session) + throws RemoteException { + mHandler.obtainMessage(H.UPDATE_REMOTE_CONTROLLER, session).sendToTarget(); + } + }; + + private final class H extends Handler { + private static final int UPDATE_SESSIONS = 1; + private static final int REMOTE_VOLUME_CHANGED = 2; + private static final int UPDATE_REMOTE_CONTROLLER = 3; + + private H(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_SESSIONS: + onActiveSessionsUpdatedH(mMgr.getActiveSessions(null)); + break; + case REMOTE_VOLUME_CHANGED: + onRemoteVolumeChangedH((ISessionController) msg.obj, msg.arg1); + break; + case UPDATE_REMOTE_CONTROLLER: + onUpdateRemoteControllerH((ISessionController) msg.obj); + break; + } + } + } + + public interface Callbacks { + void onRemoteUpdate(Token token, String name, PlaybackInfo pi); + void onRemoteRemoved(Token t); + void onRemoteVolumeChanged(Token token, int flags); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/Prefs.java b/packages/SystemUI/src/com/android/systemui/volume/Prefs.java new file mode 100644 index 0000000..58bc9f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/Prefs.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.preference.PreferenceManager; + +/** + * Configuration for the volume dialog + related policy. + */ +public class Prefs { + + public static final String PREF_ENABLE_PROTOTYPE = "pref_enable_prototype"; // not persistent + public static final String PREF_SHOW_ALARMS = "pref_show_alarms"; + public static final String PREF_SHOW_SYSTEM = "pref_show_system"; + public static final String PREF_SHOW_HEADERS = "pref_show_headers"; + public static final String PREF_SHOW_FAKE_REMOTE_1 = "pref_show_fake_remote_1"; + public static final String PREF_SHOW_FAKE_REMOTE_2 = "pref_show_fake_remote_2"; + public static final String PREF_SHOW_FOOTER = "pref_show_footer"; + public static final String PREF_ZEN_FOOTER = "pref_zen_footer"; + public static final String PREF_ENABLE_AUTOMUTE = "pref_enable_automute"; + public static final String PREF_ENABLE_SILENT_MODE = "pref_enable_silent_mode"; + public static final String PREF_DEBUG_LOGGING = "pref_debug_logging"; + public static final String PREF_SEND_LOGS = "pref_send_logs"; + public static final String PREF_ADJUST_SYSTEM = "pref_adjust_system"; + public static final String PREF_ADJUST_VOICE_CALLS = "pref_adjust_voice_calls"; + public static final String PREF_ADJUST_BLUETOOTH_SCO = "pref_adjust_bluetooth_sco"; + public static final String PREF_ADJUST_MEDIA = "pref_adjust_media"; + public static final String PREF_ADJUST_ALARMS = "pref_adjust_alarms"; + public static final String PREF_ADJUST_NOTIFICATION = "pref_adjust_notification"; + + public static final boolean DEFAULT_SHOW_HEADERS = true; + public static final boolean DEFAULT_SHOW_FOOTER = true; + public static final boolean DEFAULT_ENABLE_AUTOMUTE = true; + public static final boolean DEFAULT_ENABLE_SILENT_MODE = true; + public static final boolean DEFAULT_ZEN_FOOTER = true; + + public static void unregisterCallbacks(Context c, OnSharedPreferenceChangeListener listener) { + prefs(c).unregisterOnSharedPreferenceChangeListener(listener); + } + + public static void registerCallbacks(Context c, OnSharedPreferenceChangeListener listener) { + prefs(c).registerOnSharedPreferenceChangeListener(listener); + } + + private static SharedPreferences prefs(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); + } + + public static boolean get(Context context, String key, boolean def) { + return prefs(context).getBoolean(key, def); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/SafetyWarningDialog.java b/packages/SystemUI/src/com/android/systemui/volume/SafetyWarningDialog.java new file mode 100644 index 0000000..04640a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/SafetyWarningDialog.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.util.Log; +import android.view.KeyEvent; +import android.view.WindowManager; + +import com.android.systemui.statusbar.phone.SystemUIDialog; + +abstract public class SafetyWarningDialog extends SystemUIDialog + implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener { + + private static final String TAG = Util.logTag(SafetyWarningDialog.class); + + private static final int KEY_CONFIRM_ALLOWED_AFTER = 1000; // milliseconds + + private final Context mContext; + private final AudioManager mAudioManager; + + private long mShowTime; + private boolean mNewVolumeUp; + + public SafetyWarningDialog(Context context, AudioManager audioManager) { + super(context); + mContext = context; + mAudioManager = audioManager; + + getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR); + setMessage(mContext.getString(com.android.internal.R.string.safe_media_volume_warning)); + setButton(DialogInterface.BUTTON_POSITIVE, + mContext.getString(com.android.internal.R.string.yes), this); + setButton(DialogInterface.BUTTON_NEGATIVE, + mContext.getString(com.android.internal.R.string.no), (OnClickListener) null); + setOnDismissListener(this); + + final IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + context.registerReceiver(mReceiver, filter); + } + + abstract protected void cleanUp(); + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && event.getRepeatCount() == 0) { + mNewVolumeUp = true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mNewVolumeUp + && (System.currentTimeMillis() - mShowTime) > KEY_CONFIRM_ALLOWED_AFTER) { + if (D.BUG) Log.d(TAG, "Confirmed warning via VOLUME_UP"); + mAudioManager.disableSafeMediaVolume(); + dismiss(); + } + return super.onKeyUp(keyCode, event); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + mAudioManager.disableSafeMediaVolume(); + } + + @Override + protected void onStart() { + super.onStart(); + mShowTime = System.currentTimeMillis(); + } + + @Override + public void onDismiss(DialogInterface unused) { + mContext.unregisterReceiver(mReceiver); + cleanUp(); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { + if (D.BUG) Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS"); + cancel(); + cleanUp(); + } + } + }; +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java b/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java index 5f5b881..4f20ac7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java +++ b/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java @@ -60,17 +60,14 @@ public class SegmentedButtons extends LinearLayout { final Object tag = c.getTag(); final boolean selected = Objects.equals(mSelectedValue, tag); c.setSelected(selected); - c.getCompoundDrawables()[1].setTint(mContext.getColor(selected - ? R.color.segmented_button_selected : R.color.segmented_button_unselected)); } fireOnSelected(); } - public void addButton(int labelResId, int iconResId, Object value) { + public void addButton(int labelResId, Object value) { final Button b = (Button) mInflater.inflate(R.layout.segmented_button, this, false); b.setTag(LABEL_RES_KEY, labelResId); b.setText(labelResId); - b.setCompoundDrawablesWithIntrinsicBounds(0, iconResId, 0, 0); final LayoutParams lp = (LayoutParams) b.getLayoutParams(); if (getChildCount() == 0) { lp.leftMargin = lp.rightMargin = 0; // first button has no margin diff --git a/packages/SystemUI/src/com/android/systemui/volume/SpTexts.java b/packages/SystemUI/src/com/android/systemui/volume/SpTexts.java new file mode 100644 index 0000000..d8e53db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/SpTexts.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.Context; +import android.content.res.Resources; +import android.util.ArrayMap; +import android.util.TypedValue; +import android.view.View; +import android.view.View.OnAttachStateChangeListener; +import android.widget.TextView; + +/** + * Capture initial sp values for registered textviews, and update properly when configuration + * changes. + */ +public class SpTexts { + + private final Context mContext; + private final ArrayMap<TextView, Integer> mTexts = new ArrayMap<>(); + + public SpTexts(Context context) { + mContext = context; + } + + public int add(final TextView text) { + if (text == null) return 0; + final Resources res = mContext.getResources(); + final float fontScale = res.getConfiguration().fontScale; + final float density = res.getDisplayMetrics().density; + final float px = text.getTextSize(); + final int sp = (int)(px / fontScale / density); + mTexts.put(text, sp); + text.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { + @Override + public void onViewDetachedFromWindow(View v) { + } + + @Override + public void onViewAttachedToWindow(View v) { + setTextSizeH(text, sp); + } + }); + return sp; + } + + public void update() { + if (mTexts.isEmpty()) return; + mTexts.keyAt(0).post(mUpdateAll); + } + + private void setTextSizeH(TextView text, int sp) { + text.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp); + } + + private final Runnable mUpdateAll = new Runnable() { + @Override + public void run() { + for (int i = 0; i < mTexts.size(); i++) { + setTextSizeH(mTexts.keyAt(i), mTexts.valueAt(i)); + } + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/Util.java b/packages/SystemUI/src/com/android/systemui/volume/Util.java new file mode 100644 index 0000000..78baf67 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/Util.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.VolumeProvider; +import android.media.session.MediaController.PlaybackInfo; +import android.media.session.PlaybackState; +import android.service.notification.ZenModeConfig.DowntimeInfo; +import android.view.View; +import android.widget.TextView; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Objects; + +/** + * Static helpers for the volume dialog. + */ +class Util { + + // Note: currently not shown (only used in the text footer) + private static final SimpleDateFormat HMMAA = new SimpleDateFormat("h:mm aa", Locale.US); + + private static int[] AUDIO_MANAGER_FLAGS = new int[] { + AudioManager.FLAG_SHOW_UI, + AudioManager.FLAG_VIBRATE, + AudioManager.FLAG_PLAY_SOUND, + AudioManager.FLAG_ALLOW_RINGER_MODES, + AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE, + AudioManager.FLAG_SHOW_VIBRATE_HINT, + AudioManager.FLAG_SHOW_SILENT_HINT, + AudioManager.FLAG_FROM_KEY, + AudioManager.FLAG_SHOW_UI_WARNINGS, + }; + + private static String[] AUDIO_MANAGER_FLAG_NAMES = new String[] { + "SHOW_UI", + "VIBRATE", + "PLAY_SOUND", + "ALLOW_RINGER_MODES", + "REMOVE_SOUND_AND_VIBRATE", + "SHOW_VIBRATE_HINT", + "SHOW_SILENT_HINT", + "FROM_KEY", + "SHOW_UI_WARNINGS", + }; + + public static String logTag(Class<?> c) { + final String tag = "vol." + c.getSimpleName(); + return tag.length() < 23 ? tag : tag.substring(0, 23); + } + + public static String ringerModeToString(int ringerMode) { + switch (ringerMode) { + case AudioManager.RINGER_MODE_SILENT: return "RINGER_MODE_SILENT"; + case AudioManager.RINGER_MODE_VIBRATE: return "RINGER_MODE_VIBRATE"; + case AudioManager.RINGER_MODE_NORMAL: return "RINGER_MODE_NORMAL"; + default: return "RINGER_MODE_UNKNOWN_" + ringerMode; + } + } + + public static String mediaMetadataToString(MediaMetadata metadata) { + return metadata.getDescription().toString(); + } + + public static String playbackInfoToString(PlaybackInfo info) { + if (info == null) return null; + final String type = playbackInfoTypeToString(info.getPlaybackType()); + final String vc = volumeProviderControlToString(info.getVolumeControl()); + return String.format("PlaybackInfo[vol=%s,max=%s,type=%s,vc=%s],atts=%s", + info.getCurrentVolume(), info.getMaxVolume(), type, vc, info.getAudioAttributes()); + } + + public static String playbackInfoTypeToString(int type) { + switch (type) { + case PlaybackInfo.PLAYBACK_TYPE_LOCAL: return "LOCAL"; + case PlaybackInfo.PLAYBACK_TYPE_REMOTE: return "REMOTE"; + default: return "UNKNOWN_" + type; + } + } + + public static String playbackStateStateToString(int state) { + switch (state) { + case PlaybackState.STATE_NONE: return "STATE_NONE"; + case PlaybackState.STATE_STOPPED: return "STATE_STOPPED"; + case PlaybackState.STATE_PAUSED: return "STATE_PAUSED"; + case PlaybackState.STATE_PLAYING: return "STATE_PLAYING"; + default: return "UNKNOWN_" + state; + } + } + + public static String volumeProviderControlToString(int control) { + switch (control) { + case VolumeProvider.VOLUME_CONTROL_ABSOLUTE: return "VOLUME_CONTROL_ABSOLUTE"; + case VolumeProvider.VOLUME_CONTROL_FIXED: return "VOLUME_CONTROL_FIXED"; + case VolumeProvider.VOLUME_CONTROL_RELATIVE: return "VOLUME_CONTROL_RELATIVE"; + default: return "VOLUME_CONTROL_UNKNOWN_" + control; + } + } + + public static String playbackStateToString(PlaybackState playbackState) { + if (playbackState == null) return null; + return playbackStateStateToString(playbackState.getState()) + " " + playbackState; + } + + public static String audioManagerFlagsToString(int value) { + return bitFieldToString(value, AUDIO_MANAGER_FLAGS, AUDIO_MANAGER_FLAG_NAMES); + } + + private static String bitFieldToString(int value, int[] values, String[] names) { + if (value == 0) return ""; + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + if ((value & values[i]) != 0) { + if (sb.length() > 0) sb.append(','); + sb.append(names[i]); + } + value &= ~values[i]; + } + if (value != 0) { + if (sb.length() > 0) sb.append(','); + sb.append("UNKNOWN_").append(value); + } + return sb.toString(); + } + + public static String getShortTime(long millis) { + return HMMAA.format(new Date(millis)); + } + + public static String getShortTime(DowntimeInfo info) { + return ((info.endHour + 1) % 12) + ":" + (info.endMinute < 10 ? " " : "") + info.endMinute; + } + + public static void setText(TextView tv, CharSequence text) { + if (Objects.equals(tv.getText(), text)) return; + tv.setText(text); + } + + public static final void setVisOrGone(View v, boolean vis) { + if (v == null || (v.getVisibility() == View.VISIBLE) == vis) return; + v.setVisibility(vis ? View.VISIBLE : View.GONE); + } + + public static final void setVisOrInvis(View v, boolean vis) { + if (v == null || (v.getVisibility() == View.VISIBLE) == vis) return; + v.setVisibility(vis ? View.VISIBLE : View.INVISIBLE); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java index e3f8f3d..1f0ee57 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java @@ -16,10 +16,18 @@ package com.android.systemui.volume; +import android.content.res.Configuration; + import com.android.systemui.DemoMode; import com.android.systemui.statusbar.policy.ZenModeController; +import java.io.FileDescriptor; +import java.io.PrintWriter; + public interface VolumeComponent extends DemoMode { ZenModeController getZenController(); void dismissNow(); + void onConfigurationChanged(Configuration newConfig); + void dump(FileDescriptor fd, PrintWriter pw, String[] args); + void register(); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java new file mode 100644 index 0000000..d8b3965 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java @@ -0,0 +1,1069 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.animation.LayoutTransition; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.provider.Settings.Global; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.DowntimeInfo; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLayoutChangeListener; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.DecelerateInterpolator; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; + +import com.android.systemui.R; +import com.android.systemui.statusbar.policy.ZenModeController; +import com.android.systemui.volume.VolumeDialogController.State; +import com.android.systemui.volume.VolumeDialogController.StreamState; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Visual presentation of the volume dialog. + * + * A client of VolumeDialogController and its state model. + * + * Methods ending in "H" must be called on the (ui) handler. + */ +public class VolumeDialog { + private static final String TAG = Util.logTag(VolumeDialog.class); + + private static final long USER_ATTEMPT_GRACE_PERIOD = 1000; + private static final int WAIT_FOR_RIPPLE = 200; + private static final int UPDATE_ANIMATION_DURATION = 80; + + private final Context mContext; + private final H mHandler = new H(); + private final VolumeDialogController mController; + + private final CustomDialog mDialog; + private final ViewGroup mDialogView; + private final ViewGroup mDialogContentView; + private final ImageButton mExpandButton; + private final TextView mFootlineText; + private final Button mFootlineAction; + private final View mSettingsButton; + private final View mFooter; + private final List<VolumeRow> mRows = new ArrayList<VolumeRow>(); + private final SpTexts mSpTexts; + private final SparseBooleanArray mDynamic = new SparseBooleanArray(); + private final KeyguardManager mKeyguard; + private final int mExpandButtonAnimationDuration; + private final View mTextFooter; + private final ZenFooter mZenFooter; + private final LayoutTransition mLayoutTransition; + private final Object mSafetyWarningLock = new Object(); + + private boolean mShowing; + private boolean mExpanded; + private int mActiveStream; + private boolean mShowHeaders = Prefs.DEFAULT_SHOW_HEADERS; + private boolean mShowFooter = Prefs.DEFAULT_SHOW_FOOTER; + private boolean mShowZenFooter = Prefs.DEFAULT_ZEN_FOOTER; + private boolean mAutomute = Prefs.DEFAULT_ENABLE_AUTOMUTE; + private boolean mSilentMode = Prefs.DEFAULT_ENABLE_SILENT_MODE; + private State mState; + private int mExpandButtonRes; + private boolean mExpanding; + private SafetyWarningDialog mSafetyWarning; + + public VolumeDialog(Context context, VolumeDialogController controller, + ZenModeController zenModeController) { + mContext = context; + mController = controller; + mSpTexts = new SpTexts(mContext); + mKeyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + + mDialog = new CustomDialog(mContext); + + final Window window = mDialog.getWindow(); + window.requestFeature(Window.FEATURE_NO_TITLE); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); + mDialog.setCanceledOnTouchOutside(true); + final Resources res = mContext.getResources(); + final WindowManager.LayoutParams lp = window.getAttributes(); + lp.type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; + lp.format = PixelFormat.TRANSLUCENT; + lp.setTitle(VolumeDialog.class.getSimpleName()); + lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + lp.windowAnimations = R.style.VolumeDialogAnimations; + lp.y = res.getDimensionPixelSize(R.dimen.volume_offset_top); + lp.gravity = Gravity.TOP; + window.setAttributes(lp); + + mDialog.setContentView(R.layout.volume_dialog); + mDialogView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog); + mDialogContentView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog_content); + mExpandButton = (ImageButton) mDialogView.findViewById(R.id.volume_expand_button); + mExpandButton.setOnClickListener(mClickExpand); + updateWindowWidthH(); + updateExpandButtonH(); + mLayoutTransition = new LayoutTransition(); + mLayoutTransition.setDuration(new ValueAnimator().getDuration() / 2); + mLayoutTransition.disableTransitionType(LayoutTransition.DISAPPEARING); + mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); + mDialogContentView.setLayoutTransition(mLayoutTransition); + + addRow(AudioManager.STREAM_RING, + R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true); + addRow(AudioManager.STREAM_MUSIC, + R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true); + addRow(AudioManager.STREAM_ALARM, + R.drawable.ic_volume_alarm, R.drawable.ic_volume_alarm_mute, false); + addRow(AudioManager.STREAM_VOICE_CALL, + R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, false); + addRow(AudioManager.STREAM_BLUETOOTH_SCO, + R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false); + addRow(AudioManager.STREAM_SYSTEM, + R.drawable.ic_volume_system, R.drawable.ic_volume_system_mute, false); + + mTextFooter = mDialog.findViewById(R.id.volume_text_footer); + mFootlineText = (TextView) mDialog.findViewById(R.id.volume_footline_text); + mSpTexts.add(mFootlineText); + mFootlineAction = (Button) mDialog.findViewById(R.id.volume_footline_action_button); + mSpTexts.add(mFootlineAction); + mFooter = mDialog.findViewById(R.id.volume_footer); + mSettingsButton = mDialog.findViewById(R.id.volume_settings_button); + mSettingsButton.setOnClickListener(mClickSettings); + mExpandButtonAnimationDuration = res.getInteger(R.integer.volume_expand_animation_duration); + mZenFooter = (ZenFooter) mDialog.findViewById(R.id.volume_zen_footer); + mZenFooter.init(zenModeController, mZenFooterCallback); + + controller.addCallback(mControllerCallbackH, mHandler); + controller.getState(); + } + + private void updateWindowWidthH() { + final ViewGroup.LayoutParams lp = mDialogView.getLayoutParams(); + final DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); + if (D.BUG) Log.d(TAG, "updateWindowWidth dm.w=" + dm.widthPixels); + int w = dm.widthPixels; + final int max = mContext.getResources() + .getDimensionPixelSize(R.dimen.standard_notification_panel_width); + if (w > max) { + w = max; + } + w -= mContext.getResources().getDimensionPixelSize(R.dimen.notification_side_padding) * 2; + lp.width = w; + mDialogView.setLayoutParams(lp); + } + + public void setStreamImportant(int stream, boolean important) { + mHandler.obtainMessage(H.SET_STREAM_IMPORTANT, stream, important ? 1 : 0).sendToTarget(); + } + + public void setShowHeaders(boolean showHeaders) { + if (showHeaders == mShowHeaders) return; + mShowHeaders = showHeaders; + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + public void setShowFooter(boolean show) { + if (mShowFooter == show) return; + mShowFooter = show; + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + public void setZenFooter(boolean zen) { + if (mShowZenFooter == zen) return; + mShowZenFooter = zen; + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + public void setAutomute(boolean automute) { + if (mAutomute == automute) return; + mAutomute = automute; + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + public void setSilentMode(boolean silentMode) { + if (mSilentMode == silentMode) return; + mSilentMode = silentMode; + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + private void addRow(int stream, int iconRes, int iconMuteRes, boolean important) { + final VolumeRow row = initRow(stream, iconRes, iconMuteRes, important); + if (!mRows.isEmpty()) { + final View v = new View(mContext); + final int h = mContext.getResources() + .getDimensionPixelSize(R.dimen.volume_slider_interspacing); + final LinearLayout.LayoutParams lp = + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, h); + mDialogContentView.addView(v, mDialogContentView.getChildCount() - 1, lp); + row.space = v; + } + row.settingsButton.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (D.BUG) Log.d(TAG, "onLayoutChange" + + " old=" + new Rect(oldLeft, oldTop, oldRight, oldBottom).toShortString() + + " new=" + new Rect(left,top,right,bottom).toShortString()); + if (oldLeft != left || oldTop != top) { + for (int i = 0; i < mDialogContentView.getChildCount(); i++) { + final View c = mDialogContentView.getChildAt(i); + if (!c.isShown()) continue; + if (c == row.view) { + repositionExpandAnim(row); + } + return; + } + } + } + }); + // add new row just before the footer + mDialogContentView.addView(row.view, mDialogContentView.getChildCount() - 1); + mRows.add(row); + } + + private boolean isAttached() { + return mDialogContentView != null && mDialogContentView.isAttachedToWindow(); + } + + private VolumeRow getActiveRow() { + for (VolumeRow row : mRows) { + if (row.stream == mActiveStream) { + return row; + } + } + return mRows.get(0); + } + + private VolumeRow findRow(int stream) { + for (VolumeRow row : mRows) { + if (row.stream == stream) return row; + } + return null; + } + + private void repositionExpandAnim(VolumeRow row) { + final int[] loc = new int[2]; + row.settingsButton.getLocationInWindow(loc); + final MarginLayoutParams mlp = (MarginLayoutParams) mDialogView.getLayoutParams(); + final int x = loc[0] - mlp.leftMargin; + final int y = loc[1] - mlp.topMargin; + if (D.BUG) Log.d(TAG, "repositionExpandAnim x=" + x + " y=" + y); + mExpandButton.setTranslationX(x); + mExpandButton.setTranslationY(y); + } + + public void dump(PrintWriter writer) { + writer.println(VolumeDialog.class.getSimpleName() + " state:"); + writer.print(" mShowing: "); writer.println(mShowing); + writer.print(" mExpanded: "); writer.println(mExpanded); + writer.print(" mExpanding: "); writer.println(mExpanding); + writer.print(" mActiveStream: "); writer.println(mActiveStream); + writer.print(" mDynamic: "); writer.println(mDynamic); + writer.print(" mShowHeaders: "); writer.println(mShowHeaders); + writer.print(" mShowFooter: "); writer.println(mShowFooter); + writer.print(" mAutomute: "); writer.println(mAutomute); + writer.print(" mSilentMode: "); writer.println(mSilentMode); + } + + private static int getImpliedLevel(SeekBar seekBar, int progress) { + final int m = seekBar.getMax(); + final int n = m / 100 - 1; + final int level = progress == 0 ? 0 + : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n)); + return level; + } + + @SuppressLint("InflateParams") + private VolumeRow initRow(final int stream, int iconRes, int iconMuteRes, boolean important) { + final VolumeRow row = new VolumeRow(); + row.stream = stream; + row.iconRes = iconRes; + row.iconMuteRes = iconMuteRes; + row.important = important; + row.view = mDialog.getLayoutInflater().inflate(R.layout.volume_dialog_row, null); + row.view.setTag(row); + row.header = (TextView) row.view.findViewById(R.id.volume_row_header); + mSpTexts.add(row.header); + row.slider = (SeekBar) row.view.findViewById(R.id.volume_row_slider); + row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); + + // forward events above the slider into the slider + row.view.setOnTouchListener(new OnTouchListener() { + private final Rect mSliderHitRect = new Rect(); + private boolean mDragging; + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + row.slider.getHitRect(mSliderHitRect); + if (!mDragging && event.getActionMasked() == MotionEvent.ACTION_DOWN + && event.getY() < mSliderHitRect.top) { + mDragging = true; + } + if (mDragging) { + event.offsetLocation(-mSliderHitRect.left, -mSliderHitRect.top); + row.slider.dispatchTouchEvent(event); + if (event.getActionMasked() == MotionEvent.ACTION_UP + || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { + mDragging = false; + } + return true; + } + return false; + } + }); + row.icon = (ImageButton) row.view.findViewById(R.id.volume_row_icon); + row.icon.setImageResource(iconRes); + row.icon.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Events.writeEvent(Events.EVENT_ICON_CLICK, row.stream, row.iconState); + mController.setActiveStream(row.stream); + if (row.stream == AudioManager.STREAM_RING) { + final boolean hasVibrator = mController.hasVibrator(); + if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) { + if (hasVibrator) { + mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false); + } else { + final boolean wasZero = row.ss.level == 0; + mController.setStreamVolume(stream, wasZero ? row.lastAudibleLevel : 0); + } + } else { + mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); + if (row.ss.level == 0) { + mController.setStreamVolume(stream, 1); + } + } + } else { + if (mAutomute && !row.ss.muteSupported) { + final boolean vmute = row.ss.level == 0; + mController.setStreamVolume(stream, vmute ? row.lastAudibleLevel : 0); + } else { + final boolean mute = !row.ss.muted; + mController.setStreamMute(stream, mute); + if (mAutomute) { + if (!mute && row.ss.level == 0) { + mController.setStreamVolume(stream, 1); + } + } + } + } + row.userAttempt = 0; // reset the grace period, slider should update immediately + } + }); + row.settingsButton = (ImageButton) row.view.findViewById(R.id.volume_settings_button); + row.settingsButton.setOnClickListener(mClickSettings); + return row; + } + + public void destroy() { + mController.removeCallback(mControllerCallbackH); + } + + public void show(int reason) { + mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget(); + } + + public void dismiss(int reason) { + mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget(); + } + + protected void onSettingsClickedH() { + // hook for subclasses + } + + protected void onZenSettingsClickedH() { + // hook for subclasses + } + + private void showH(int reason) { + mHandler.removeMessages(H.SHOW); + mHandler.removeMessages(H.DISMISS); + rescheduleTimeoutH(); + if (mShowing) return; + mShowing = true; + mDialog.show(); + Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked()); + mController.notifyVisible(true); + } + + protected void rescheduleTimeoutH() { + mHandler.removeMessages(H.DISMISS); + final int timeout = computeTimeoutH(); + if (D.BUG) Log.d(TAG, "rescheduleTimeout " + timeout); + mHandler.sendMessageDelayed(mHandler + .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout); + mController.userActivity(); + } + + private int computeTimeoutH() { + if (mZenFooter != null && mZenFooter.isFooterExpanded()) return 10000; + if (mSafetyWarning != null) return 5000; + if (mExpanded || mExpanding) return 5000; + if (mActiveStream == AudioManager.STREAM_MUSIC) return 1500; + return 3000; + } + + protected void dismissH(int reason) { + mHandler.removeMessages(H.DISMISS); + mHandler.removeMessages(H.SHOW); + if (!mShowing) return; + mShowing = false; + mDialog.dismiss(); + Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason); + setExpandedH(false); + mController.notifyVisible(false); + synchronized (mSafetyWarningLock) { + if (mSafetyWarning != null) { + if (D.BUG) Log.d(TAG, "SafetyWarning dismissed"); + mSafetyWarning.dismiss(); + } + } + } + + private void setExpandedH(boolean expanded) { + if (mExpanded == expanded) return; + mExpanded = expanded; + mExpanding = isAttached(); + if (D.BUG) Log.d(TAG, "setExpandedH " + expanded); + updateRowsH(); + if (mExpanding) { + final Drawable d = mExpandButton.getDrawable(); + if (d instanceof AnimatedVectorDrawable) { + // workaround to reset drawable + final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) d.getConstantState() + .newDrawable(); + mExpandButton.setImageDrawable(avd); + avd.start(); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mExpanding = false; + updateExpandButtonH(); + rescheduleTimeoutH(); + } + }, mExpandButtonAnimationDuration); + } + } + rescheduleTimeoutH(); + } + + private void updateExpandButtonH() { + mExpandButton.setClickable(!mExpanding); + if (mExpanding && isAttached()) return; + final int res = mExpanded ? R.drawable.ic_volume_collapse_animation + : R.drawable.ic_volume_expand_animation; + if (res == mExpandButtonRes) return; + mExpandButtonRes = res; + mExpandButton.setImageResource(res); + } + + private boolean isVisibleH(VolumeRow row, boolean isActive) { + return mExpanded && row.view.getVisibility() == View.VISIBLE + || (mExpanded && (row.important || isActive)) + || !mExpanded && isActive; + } + + private void updateRowsH() { + final VolumeRow activeRow = getActiveRow(); + updateFooterH(); + updateExpandButtonH(); + final boolean footerVisible = mFooter.getVisibility() == View.VISIBLE; + if (!mShowing) { + trimObsoleteH(); + } + // first, find the last visible row + VolumeRow lastVisible = null; + for (VolumeRow row : mRows) { + final boolean isActive = row == activeRow; + if (isVisibleH(row, isActive)) { + lastVisible = row; + } + } + // apply changes to all rows + for (VolumeRow row : mRows) { + final boolean isActive = row == activeRow; + final boolean visible = isVisibleH(row, isActive); + Util.setVisOrGone(row.view, visible); + Util.setVisOrGone(row.space, visible && mExpanded); + final int expandButtonRes = mExpanded ? R.drawable.ic_volume_settings : 0; + if (expandButtonRes != row.cachedExpandButtonRes) { + row.cachedExpandButtonRes = expandButtonRes; + if (expandButtonRes == 0) { + row.settingsButton.setImageDrawable(null); + } else { + row.settingsButton.setImageResource(expandButtonRes); + } + } + Util.setVisOrInvis(row.settingsButton, + mExpanded && (!footerVisible && row == lastVisible)); + row.header.setAlpha(mExpanded && isActive ? 1 : 0.5f); + } + } + + private void trimObsoleteH() { + for (int i = mRows.size() -1; i >= 0; i--) { + final VolumeRow row = mRows.get(i); + if (row.ss == null || !row.ss.dynamic) continue; + if (!mDynamic.get(row.stream)) { + mRows.remove(i); + mDialogContentView.removeView(row.view); + mDialogContentView.removeView(row.space); + } + } + } + + private void onStateChangedH(State state) { + mState = state; + mDynamic.clear(); + // add any new dynamic rows + for (int i = 0; i < state.states.size(); i++) { + final int stream = state.states.keyAt(i); + final StreamState ss = state.states.valueAt(i); + if (!ss.dynamic) continue; + mDynamic.put(stream, true); + if (findRow(stream) == null) { + addRow(stream, R.drawable.ic_volume_remote, R.drawable.ic_volume_remote_mute, true); + } + } + + if (mActiveStream != state.activeStream) { + mActiveStream = state.activeStream; + updateRowsH(); + rescheduleTimeoutH(); + } + for (VolumeRow row : mRows) { + updateVolumeRowH(row); + } + updateFooterH(); + } + + private void updateTextFooterH() { + final boolean zen = mState.zenMode != Global.ZEN_MODE_OFF; + final boolean wasVisible = mFooter.getVisibility() == View.VISIBLE; + Util.setVisOrGone(mTextFooter, mExpanded && mShowFooter && (zen || mShowing && wasVisible)); + if (mTextFooter.getVisibility() == View.VISIBLE) { + String text = null; + String action = null; + if (mState.exitCondition != null) { + final long countdown = ZenModeConfig.tryParseCountdownConditionId(mState + .exitCondition.id); + if (countdown != 0) { + text = mContext.getString(R.string.volume_dnd_ends_at, + Util.getShortTime(countdown)); + action = mContext.getString(R.string.volume_end_now); + } else { + final DowntimeInfo info = ZenModeConfig.tryParseDowntimeConditionId(mState. + exitCondition.id); + if (info != null) { + text = mContext.getString(R.string.volume_dnd_ends_at, + Util.getShortTime(info)); + action = mContext.getString(R.string.volume_end_now); + } + } + } + if (text == null) { + text = mContext.getString(R.string.volume_dnd_is_on); + } + if (action == null) { + action = mContext.getString(R.string.volume_turn_off); + } + Util.setText(mFootlineText, text); + Util.setText(mFootlineAction, action); + mFootlineAction.setOnClickListener(mTurnOffDnd); + } + Util.setVisOrGone(mFootlineText, zen); + Util.setVisOrGone(mFootlineAction, zen); + } + + private void updateFooterH() { + if (!mShowFooter) { + Util.setVisOrGone(mFooter, false); + return; + } + if (mShowZenFooter) { + Util.setVisOrGone(mTextFooter, false); + final boolean ringActive = mActiveStream == AudioManager.STREAM_RING; + Util.setVisOrGone(mZenFooter, mZenFooter.isZen() && ringActive + || mShowing && (mExpanded || mZenFooter.getVisibility() == View.VISIBLE)); + mZenFooter.update(); + } else { + Util.setVisOrGone(mZenFooter, false); + updateTextFooterH(); + } + } + + private void updateVolumeRowH(VolumeRow row) { + if (mState == null) return; + final StreamState ss = mState.states.get(row.stream); + if (ss == null) return; + row.ss = ss; + if (ss.level > 0) { + row.lastAudibleLevel = ss.level; + } + final boolean isRingStream = row.stream == AudioManager.STREAM_RING; + final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM; + final boolean isRingVibrate = isRingStream + && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; + final boolean isNoned = (isRingStream || isSystemStream) + && mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS; + final boolean isLimited = isRingStream + && mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + + // update slider max + final int max = ss.levelMax * 100; + if (max != row.slider.getMax()) { + row.slider.setMax(max); + } + + // update header visible + if (row.cachedShowHeaders != mShowHeaders) { + row.cachedShowHeaders = mShowHeaders; + Util.setVisOrGone(row.header, mShowHeaders); + } + + // update header text + final String text; + if (isNoned) { + text = mContext.getString(R.string.volume_stream_muted_dnd, ss.name); + } else if (isRingVibrate && isLimited) { + text = mContext.getString(R.string.volume_stream_vibrate_dnd, ss.name); + } else if (isRingVibrate) { + text = mContext.getString(R.string.volume_stream_vibrate, ss.name); + } else if (ss.muted || mAutomute && ss.level == 0) { + text = mContext.getString(R.string.volume_stream_muted, ss.name); + } else if (isLimited) { + text = mContext.getString(R.string.volume_stream_limited_dnd, ss.name); + } else { + text = ss.name; + } + Util.setText(row.header, text); + + // update icon + final boolean iconEnabled = mAutomute || ss.muteSupported; + row.icon.setEnabled(iconEnabled); + row.icon.setAlpha(iconEnabled ? 1 : 0.5f); + final int iconRes = + isRingVibrate ? R.drawable.ic_volume_ringer_vibrate + : ss.routedToBluetooth ? + (ss.muted ? R.drawable.ic_volume_bt_mute : R.drawable.ic_volume_bt) + : mAutomute && ss.level == 0 ? row.iconMuteRes + : (ss.muted ? row.iconMuteRes : row.iconRes); + if (iconRes != row.cachedIconRes) { + if (row.cachedIconRes != 0 && isRingVibrate) { + mController.vibrate(); + } + row.cachedIconRes = iconRes; + row.icon.setImageResource(iconRes); + } + row.iconState = + iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE + : (iconRes == R.drawable.ic_volume_bt_mute || iconRes == row.iconMuteRes) + ? Events.ICON_STATE_MUTE + : (iconRes == R.drawable.ic_volume_bt || iconRes == row.iconRes) + ? Events.ICON_STATE_UNMUTE + : Events.ICON_STATE_UNKNOWN; + + // update slider + updateVolumeRowSliderH(row); + } + + private void updateVolumeRowSliderH(VolumeRow row) { + if (row.tracking) { + return; // don't update if user is sliding + } + final int progress = row.slider.getProgress(); + final int level = getImpliedLevel(row.slider, progress); + final boolean rowVisible = row.view.getVisibility() == View.VISIBLE; + final boolean inGracePeriod = (SystemClock.uptimeMillis() - row.userAttempt) + < USER_ATTEMPT_GRACE_PERIOD; + mHandler.removeMessages(H.RECHECK, row); + if (mShowing && rowVisible && inGracePeriod) { + if (D.BUG) Log.d(TAG, "inGracePeriod"); + mHandler.sendMessageAtTime(mHandler.obtainMessage(H.RECHECK, row), + row.userAttempt + USER_ATTEMPT_GRACE_PERIOD); + return; // don't update if visible and in grace period + } + final int vlevel = row.ss.muted ? 0 : row.ss.level; + if (vlevel == level) { + if (mShowing && rowVisible) { + return; // don't clamp if visible + } + } + final int newProgress = vlevel * 100; + if (progress != newProgress) { + if (mShowing && rowVisible) { + // animate! + if (row.anim != null && row.anim.isRunning() + && row.animTargetProgress == newProgress) { + return; // already animating to the target progress + } + // start/update animation + if (row.anim == null) { + row.anim = ObjectAnimator.ofInt(row.slider, "progress", progress, newProgress); + row.anim.setInterpolator(new DecelerateInterpolator()); + } else { + row.anim.cancel(); + row.anim.setIntValues(progress, newProgress); + } + row.animTargetProgress = newProgress; + row.anim.setDuration(UPDATE_ANIMATION_DURATION); + row.anim.start(); + } else { + // update slider directly to clamped value + if (row.anim != null) { + row.anim.cancel(); + } + row.slider.setProgress(newProgress); + } + if (mAutomute) { + if (vlevel == 0 && !row.ss.muted && row.stream == AudioManager.STREAM_MUSIC) { + mController.setStreamMute(row.stream, true); + } + } + } + } + + private void recheckH(VolumeRow row) { + if (row == null) { + if (D.BUG) Log.d(TAG, "recheckH ALL"); + trimObsoleteH(); + for (VolumeRow r : mRows) { + updateVolumeRowH(r); + } + } else { + if (D.BUG) Log.d(TAG, "recheckH " + row.stream); + updateVolumeRowH(row); + } + } + + private void setStreamImportantH(int stream, boolean important) { + for (VolumeRow row : mRows) { + if (row.stream == stream) { + row.important = important; + return; + } + } + } + + private void showSafetyWarningH(int flags) { + if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0 + || mShowing) { + synchronized (mSafetyWarningLock) { + if (mSafetyWarning != null) { + return; + } + mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) { + @Override + protected void cleanUp() { + synchronized (mSafetyWarningLock) { + mSafetyWarning = null; + } + recheckH(null); + } + }; + mSafetyWarning.show(); + } + recheckH(null); + } + rescheduleTimeoutH(); + } + + private final VolumeDialogController.Callbacks mControllerCallbackH + = new VolumeDialogController.Callbacks() { + @Override + public void onShowRequested(int reason) { + showH(reason); + } + + @Override + public void onDismissRequested(int reason) { + dismissH(reason); + } + + @Override + public void onScreenOff() { + dismissH(Events.DISMISS_REASON_SCREEN_OFF); + } + + @Override + public void onStateChanged(State state) { + onStateChangedH(state); + } + + @Override + public void onLayoutDirectionChanged(int layoutDirection) { + mDialogView.setLayoutDirection(layoutDirection); + } + + @Override + public void onConfigurationChanged() { + updateWindowWidthH(); + mSpTexts.update(); + } + + @Override + public void onShowVibrateHint() { + if (mSilentMode) { + mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false); + } + } + + @Override + public void onShowSilentHint() { + if (mSilentMode) { + mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); + } + } + + @Override + public void onShowSafetyWarning(int flags) { + showSafetyWarningH(flags); + } + }; + + private final OnClickListener mClickExpand = new OnClickListener() { + @Override + public void onClick(View v) { + if (mExpanding) return; + final boolean newExpand = !mExpanded; + Events.writeEvent(Events.EVENT_EXPAND, v); + setExpandedH(newExpand); + } + }; + + private final OnClickListener mClickSettings = new OnClickListener() { + @Override + public void onClick(View v) { + mSettingsButton.postDelayed(new Runnable() { + @Override + public void run() { + Events.writeEvent(Events.EVENT_SETTINGS_CLICK); + onSettingsClickedH(); + } + }, WAIT_FOR_RIPPLE); + } + }; + + private final View.OnClickListener mTurnOffDnd = new View.OnClickListener() { + @Override + public void onClick(View v) { + mSettingsButton.postDelayed(new Runnable() { + @Override + public void run() { + mController.setZenMode(Global.ZEN_MODE_OFF); + } + }, WAIT_FOR_RIPPLE); + } + }; + + private final ZenFooter.Callback mZenFooterCallback = new ZenFooter.Callback() { + @Override + public void onFooterExpanded() { + mHandler.sendEmptyMessage(H.RESCHEDULE_TIMEOUT); + } + + @Override + public void onSettingsClicked() { + dismiss(Events.DISMISS_REASON_SETTINGS_CLICKED); + onZenSettingsClickedH(); + } + + @Override + public void onDoneClicked() { + dismiss(Events.DISMISS_REASON_DONE_CLICKED); + } + }; + + private final class H extends Handler { + private static final int SHOW = 1; + private static final int DISMISS = 2; + private static final int RECHECK = 3; + private static final int RECHECK_ALL = 4; + private static final int SET_STREAM_IMPORTANT = 5; + private static final int RESCHEDULE_TIMEOUT = 6; + + public H() { + super(Looper.getMainLooper()); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SHOW: showH(msg.arg1); break; + case DISMISS: dismissH(msg.arg1); break; + case RECHECK: recheckH((VolumeRow) msg.obj); break; + case RECHECK_ALL: recheckH(null); break; + case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break; + case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break; + } + } + } + + private final class CustomDialog extends Dialog { + public CustomDialog(Context context) { + super(context); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + rescheduleTimeoutH(); + return super.dispatchTouchEvent(ev); + } + + @Override + protected void onStop() { + super.onStop(); + mHandler.sendEmptyMessage(H.RECHECK_ALL); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (isShowing()) { + if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { + dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE); + return true; + } + } + return false; + } + } + + private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener { + private final VolumeRow mRow; + + private VolumeSeekBarChangeListener(VolumeRow row) { + mRow = row; + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (mRow.ss == null) return; + if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream) + + " onProgressChanged " + progress + " fromUser=" + fromUser); + if (!fromUser) return; + if (mRow.ss.levelMin > 0) { + final int minProgress = mRow.ss.levelMin * 100; + if (progress < minProgress) { + seekBar.setProgress(minProgress); + } + } + final int userLevel = getImpliedLevel(seekBar, progress); + if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) { + mRow.userAttempt = SystemClock.uptimeMillis(); + if (mAutomute) { + if (mRow.stream != AudioManager.STREAM_RING) { + if (userLevel > 0 && mRow.ss.muted) { + mController.setStreamMute(mRow.stream, false); + } + if (userLevel == 0 && mRow.ss.muteSupported && !mRow.ss.muted) { + mController.setStreamMute(mRow.stream, true); + } + } + } + if (mRow.requestedLevel != userLevel) { + mController.setStreamVolume(mRow.stream, userLevel); + mRow.requestedLevel = userLevel; + Events.writeEvent(Events.EVENT_TOUCH_LEVEL_CHANGED, mRow.stream, userLevel); + } + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream); + mController.setActiveStream(mRow.stream); + mRow.tracking = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream); + mRow.tracking = false; + mRow.userAttempt = SystemClock.uptimeMillis(); + int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); + if (mRow.ss.level != userLevel) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow), + USER_ATTEMPT_GRACE_PERIOD); + } + } + } + + private static class VolumeRow { + private View view; + private View space; + private TextView header; + private ImageButton icon; + private SeekBar slider; + private ImageButton settingsButton; + private int stream; + private StreamState ss; + private long userAttempt; // last user-driven slider change + private boolean tracking; // tracking slider touch + private int requestedLevel; + private int iconRes; + private int iconMuteRes; + private boolean important; + private int cachedIconRes; + private int iconState; // from Events + private boolean cachedShowHeaders = Prefs.DEFAULT_SHOW_HEADERS; + private int cachedExpandButtonRes; + private ObjectAnimator anim; // slider progress animation for non-touch-related updates + private int animTargetProgress; + private int lastAudibleLevel = 1; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java new file mode 100644 index 0000000..741e498 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.Context; +import android.content.res.Configuration; +import android.media.AudioManager; +import android.media.VolumePolicy; +import android.os.Bundle; +import android.os.Handler; + +import com.android.systemui.SystemUI; +import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.qs.tiles.DndTile; +import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.statusbar.policy.ZenModeController; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Implementation of VolumeComponent backed by the new volume dialog. + */ +public class VolumeDialogComponent implements VolumeComponent { + private final SystemUI mSysui; + private final Context mContext; + private final VolumeDialogController mController; + private final ZenModeController mZenModeController; + private final VolumeDialog mDialog; + + public VolumeDialogComponent(SystemUI sysui, Context context, Handler handler, + ZenModeController zen) { + mSysui = sysui; + mContext = context; + mController = new VolumeDialogController(context, null) { + @Override + protected void onUserActivityW() { + sendUserActivity(); + } + }; + mZenModeController = zen; + mDialog = new VolumeDialog(context, mController, zen) { + @Override + protected void onZenSettingsClickedH() { + startZenSettings(); + } + }; + applyConfiguration(); + } + + private void sendUserActivity() { + final KeyguardViewMediator kvm = mSysui.getComponent(KeyguardViewMediator.class); + if (kvm != null) { + kvm.userActivity(); + } + } + + private void applyConfiguration() { + mDialog.setStreamImportant(AudioManager.STREAM_ALARM, true); + mDialog.setStreamImportant(AudioManager.STREAM_SYSTEM, false); + mDialog.setShowHeaders(false); + mDialog.setShowFooter(true); + mDialog.setZenFooter(true); + mDialog.setAutomute(true); + mDialog.setSilentMode(false); + mController.setVolumePolicy(VolumePolicy.DEFAULT); + mController.showDndTile(false); + } + + @Override + public ZenModeController getZenController() { + return mZenModeController; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + // noop + } + + @Override + public void dismissNow() { + mController.dismiss(); + } + + @Override + public void dispatchDemoCommand(String command, Bundle args) { + // noop + } + + @Override + public void register() { + mController.register(); + DndTile.setCombinedIcon(mContext, true); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mController.dump(fd, pw, args); + mDialog.dump(pw); + } + + private void startZenSettings() { + mSysui.getComponent(PhoneStatusBar.class).startActivityDismissingKeyguard( + ZenModePanel.ZEN_SETTINGS, true /* onlyProvisioned */, true /* dismissShade */); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java new file mode 100644 index 0000000..a3d9377 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java @@ -0,0 +1,988 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.media.IVolumeController; +import android.media.VolumePolicy; +import android.media.session.MediaController.PlaybackInfo; +import android.media.session.MediaSession.Token; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.Vibrator; +import android.provider.Settings; +import android.service.notification.Condition; +import android.util.Log; +import android.util.SparseArray; + +import com.android.systemui.R; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Source of truth for all state / events related to the volume dialog. No presentation. + * + * All work done on a dedicated background worker thread & associated worker. + * + * Methods ending in "W" must be called on the worker thread. + */ +public class VolumeDialogController { + private static final String TAG = Util.logTag(VolumeDialogController.class); + + private static final int DYNAMIC_STREAM_START_INDEX = 100; + private static final int VIBRATE_HINT_DURATION = 50; + + private static final int[] STREAMS = { + AudioSystem.STREAM_ALARM, + AudioSystem.STREAM_BLUETOOTH_SCO, + AudioSystem.STREAM_DTMF, + AudioSystem.STREAM_MUSIC, + AudioSystem.STREAM_NOTIFICATION, + AudioSystem.STREAM_RING, + AudioSystem.STREAM_SYSTEM, + AudioSystem.STREAM_SYSTEM_ENFORCED, + AudioSystem.STREAM_TTS, + AudioSystem.STREAM_VOICE_CALL, + }; + + private final HandlerThread mWorkerThread; + private final W mWorker; + private final Context mContext; + private final AudioManager mAudio; + private final NotificationManager mNoMan; + private final ComponentName mComponent; + private final SettingObserver mObserver; + private final Receiver mReceiver = new Receiver(); + private final MediaSessions mMediaSessions; + private final VC mVolumeController = new VC(); + private final C mCallbacks = new C(); + private final State mState = new State(); + private final String[] mStreamTitles; + private final MediaSessionsCallbacks mMediaSessionsCallbacksW = new MediaSessionsCallbacks(); + private final Vibrator mVibrator; + private final boolean mHasVibrator; + + private boolean mEnabled; + private boolean mDestroyed; + private VolumePolicy mVolumePolicy = new VolumePolicy(true, true, false, 400); + private boolean mShowDndTile = false; + + public VolumeDialogController(Context context, ComponentName component) { + mContext = context.getApplicationContext(); + Events.writeEvent(Events.EVENT_COLLECTION_STARTED); + mComponent = component; + mWorkerThread = new HandlerThread(VolumeDialogController.class.getSimpleName()); + mWorkerThread.start(); + mWorker = new W(mWorkerThread.getLooper()); + mMediaSessions = createMediaSessions(mContext, mWorkerThread.getLooper(), + mMediaSessionsCallbacksW); + mAudio = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + mNoMan = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + mObserver = new SettingObserver(mWorker); + mObserver.init(); + mReceiver.init(); + mStreamTitles = mContext.getResources().getStringArray(R.array.volume_stream_titles); + mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + mHasVibrator = mVibrator != null && mVibrator.hasVibrator(); + } + + public AudioManager getAudioManager() { + return mAudio; + } + + public void dismiss() { + mCallbacks.onDismissRequested(Events.DISMISS_REASON_VOLUME_CONTROLLER); + } + + public void register() { + try { + mAudio.setVolumeController(mVolumeController); + } catch (SecurityException e) { + Log.w(TAG, "Unable to set the volume controller", e); + return; + } + setVolumePolicy(mVolumePolicy); + showDndTile(mShowDndTile); + try { + mMediaSessions.init(); + } catch (SecurityException e) { + Log.w(TAG, "No access to media sessions", e); + } + } + + public void setVolumePolicy(VolumePolicy policy) { + mVolumePolicy = policy; + try { + mAudio.setVolumePolicy(mVolumePolicy); + } catch (NoSuchMethodError e) { + Log.w(TAG, "No volume policy api"); + } + } + + protected MediaSessions createMediaSessions(Context context, Looper looper, + MediaSessions.Callbacks callbacks) { + return new MediaSessions(context, looper, callbacks); + } + + public void destroy() { + if (D.BUG) Log.d(TAG, "destroy"); + if (mDestroyed) return; + mDestroyed = true; + Events.writeEvent(Events.EVENT_COLLECTION_STOPPED); + mMediaSessions.destroy(); + mObserver.destroy(); + mReceiver.destroy(); + mWorkerThread.quitSafely(); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println(VolumeDialogController.class.getSimpleName() + " state:"); + pw.print(" mEnabled: "); pw.println(mEnabled); + pw.print(" mDestroyed: "); pw.println(mDestroyed); + pw.print(" mVolumePolicy: "); pw.println(mVolumePolicy); + pw.print(" mEnabled: "); pw.println(mEnabled); + pw.print(" mShowDndTile: "); pw.println(mShowDndTile); + pw.print(" mHasVibrator: "); pw.println(mHasVibrator); + pw.print(" mRemoteStreams: "); pw.println(mMediaSessionsCallbacksW.mRemoteStreams + .values()); + pw.println(); + mMediaSessions.dump(pw); + } + + public void addCallback(Callbacks callback, Handler handler) { + mCallbacks.add(callback, handler); + } + + public void removeCallback(Callbacks callback) { + mCallbacks.remove(callback); + } + + public void getState() { + if (mDestroyed) return; + mWorker.sendEmptyMessage(W.GET_STATE); + } + + public void notifyVisible(boolean visible) { + if (mDestroyed) return; + mWorker.obtainMessage(W.NOTIFY_VISIBLE, visible ? 1 : 0, 0).sendToTarget(); + } + + public void userActivity() { + if (mDestroyed) return; + mWorker.removeMessages(W.USER_ACTIVITY); + mWorker.sendEmptyMessage(W.USER_ACTIVITY); + } + + public void setRingerMode(int value, boolean external) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_RINGER_MODE, value, external ? 1 : 0).sendToTarget(); + } + + public void setZenMode(int value) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_ZEN_MODE, value, 0).sendToTarget(); + } + + public void setExitCondition(Condition condition) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_EXIT_CONDITION, condition).sendToTarget(); + } + + public void setStreamMute(int stream, boolean mute) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_STREAM_MUTE, stream, mute ? 1 : 0).sendToTarget(); + } + + public void setStreamVolume(int stream, int level) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_STREAM_VOLUME, stream, level).sendToTarget(); + } + + public void setActiveStream(int stream) { + if (mDestroyed) return; + mWorker.obtainMessage(W.SET_ACTIVE_STREAM, stream, 0).sendToTarget(); + } + + public void vibrate() { + if (mHasVibrator) { + mVibrator.vibrate(VIBRATE_HINT_DURATION); + } + } + + public boolean hasVibrator() { + return mHasVibrator; + } + + private void onNotifyVisibleW(boolean visible) { + if (mDestroyed) return; + mAudio.notifyVolumeControllerVisible(mVolumeController, visible); + if (!visible) { + if (updateActiveStreamW(-1)) { + mCallbacks.onStateChanged(mState); + } + } + } + + protected void onUserActivityW() { + // hook for subclasses + } + + private void onShowSafetyWarningW(int flags) { + mCallbacks.onShowSafetyWarning(flags); + } + + private boolean checkRoutedToBluetoothW(int stream) { + boolean changed = false; + if (stream == AudioManager.STREAM_MUSIC) { + final boolean routedToBluetooth = + (mAudio.getDevicesForStream(AudioManager.STREAM_MUSIC) & + (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP | + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0; + changed |= updateStreamRoutedToBluetoothW(stream, routedToBluetooth); + } + return changed; + } + + private void onVolumeChangedW(int stream, int flags) { + final boolean showUI = (flags & AudioManager.FLAG_SHOW_UI) != 0; + final boolean fromKey = (flags & AudioManager.FLAG_FROM_KEY) != 0; + final boolean showVibrateHint = (flags & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0; + final boolean showSilentHint = (flags & AudioManager.FLAG_SHOW_SILENT_HINT) != 0; + boolean changed = false; + if (showUI) { + changed |= updateActiveStreamW(stream); + } + changed |= updateStreamLevelW(stream, mAudio.getLastAudibleStreamVolume(stream)); + changed |= checkRoutedToBluetoothW(showUI ? AudioManager.STREAM_MUSIC : stream); + if (changed) { + mCallbacks.onStateChanged(mState); + } + if (showUI) { + mCallbacks.onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED); + } + if (showVibrateHint) { + mCallbacks.onShowVibrateHint(); + } + if (showSilentHint) { + mCallbacks.onShowSilentHint(); + } + if (changed && fromKey) { + Events.writeEvent(Events.EVENT_KEY); + } + } + + private boolean updateActiveStreamW(int activeStream) { + if (activeStream == mState.activeStream) return false; + mState.activeStream = activeStream; + Events.writeEvent(Events.EVENT_ACTIVE_STREAM_CHANGED, activeStream); + if (D.BUG) Log.d(TAG, "updateActiveStreamW " + activeStream); + final int s = activeStream < DYNAMIC_STREAM_START_INDEX ? activeStream : -1; + if (D.BUG) Log.d(TAG, "forceVolumeControlStream " + s); + mAudio.forceVolumeControlStream(s); + return true; + } + + private StreamState streamStateW(int stream) { + StreamState ss = mState.states.get(stream); + if (ss == null) { + ss = new StreamState(); + mState.states.put(stream, ss); + } + return ss; + } + + private void onGetStateW() { + for (int stream : STREAMS) { + updateStreamLevelW(stream, mAudio.getLastAudibleStreamVolume(stream)); + streamStateW(stream).levelMin = mAudio.getStreamMinVolume(stream); + streamStateW(stream).levelMax = mAudio.getStreamMaxVolume(stream); + updateStreamMuteW(stream, mAudio.isStreamMute(stream)); + final StreamState ss = streamStateW(stream); + ss.muteSupported = mAudio.isStreamAffectedByMute(stream); + ss.name = mStreamTitles[stream]; + checkRoutedToBluetoothW(stream); + } + updateRingerModeExternalW(mAudio.getRingerMode()); + updateZenModeW(); + updateEffectsSuppressorW(mNoMan.getEffectsSuppressor()); + updateExitConditionW(); + mCallbacks.onStateChanged(mState); + } + + private boolean updateStreamRoutedToBluetoothW(int stream, boolean routedToBluetooth) { + final StreamState ss = streamStateW(stream); + if (ss.routedToBluetooth == routedToBluetooth) return false; + ss.routedToBluetooth = routedToBluetooth; + if (D.BUG) Log.d(TAG, "updateStreamRoutedToBluetoothW stream=" + stream + + " routedToBluetooth=" + routedToBluetooth); + return true; + } + + private boolean updateStreamLevelW(int stream, int level) { + final StreamState ss = streamStateW(stream); + if (ss.level == level) return false; + ss.level = level; + if (isLogWorthy(stream)) { + Events.writeEvent(Events.EVENT_LEVEL_CHANGED, stream, level); + } + return true; + } + + private static boolean isLogWorthy(int stream) { + switch (stream) { + case AudioSystem.STREAM_ALARM: + case AudioSystem.STREAM_BLUETOOTH_SCO: + case AudioSystem.STREAM_MUSIC: + case AudioSystem.STREAM_RING: + case AudioSystem.STREAM_SYSTEM: + case AudioSystem.STREAM_VOICE_CALL: + return true; + } + return false; + } + + private boolean updateStreamMuteW(int stream, boolean muted) { + final StreamState ss = streamStateW(stream); + if (ss.muted == muted) return false; + ss.muted = muted; + if (isLogWorthy(stream)) { + Events.writeEvent(Events.EVENT_MUTE_CHANGED, stream, muted); + } + if (muted && isRinger(stream)) { + updateRingerModeInternalW(mAudio.getRingerModeInternal()); + } + return true; + } + + private static boolean isRinger(int stream) { + return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION; + } + + private boolean updateExitConditionW() { + final Condition exitCondition = mNoMan.getZenModeCondition(); + if (Objects.equals(mState.exitCondition, exitCondition)) return false; + mState.exitCondition = exitCondition; + return true; + } + + private boolean updateEffectsSuppressorW(ComponentName effectsSuppressor) { + if (Objects.equals(mState.effectsSuppressor, effectsSuppressor)) return false; + mState.effectsSuppressor = effectsSuppressor; + mState.effectsSuppressorName = getApplicationName(mContext, mState.effectsSuppressor); + Events.writeEvent(Events.EVENT_SUPPRESSOR_CHANGED, mState.effectsSuppressor, + mState.effectsSuppressorName); + return true; + } + + private static String getApplicationName(Context context, ComponentName component) { + if (component == null) return null; + final PackageManager pm = context.getPackageManager(); + final String pkg = component.getPackageName(); + try { + final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0); + final String rt = Objects.toString(ai.loadLabel(pm), "").trim(); + if (rt.length() > 0) { + return rt; + } + } catch (NameNotFoundException e) {} + return pkg; + } + + private boolean updateZenModeW() { + final int zen = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF); + if (mState.zenMode == zen) return false; + mState.zenMode = zen; + Events.writeEvent(Events.EVENT_ZEN_MODE_CHANGED, zen); + return true; + } + + private boolean updateRingerModeExternalW(int rm) { + if (rm == mState.ringerModeExternal) return false; + mState.ringerModeExternal = rm; + Events.writeEvent(Events.EVENT_EXTERNAL_RINGER_MODE_CHANGED, rm); + return true; + } + + private boolean updateRingerModeInternalW(int rm) { + if (rm == mState.ringerModeInternal) return false; + mState.ringerModeInternal = rm; + Events.writeEvent(Events.EVENT_INTERNAL_RINGER_MODE_CHANGED, rm); + return true; + } + + private void onSetRingerModeW(int mode, boolean external) { + if (external) { + mAudio.setRingerMode(mode); + } else { + mAudio.setRingerModeInternal(mode); + } + } + + private void onSetStreamMuteW(int stream, boolean mute) { + mAudio.adjustStreamVolume(stream, mute ? AudioManager.ADJUST_MUTE + : AudioManager.ADJUST_UNMUTE, 0); + } + + private void onSetStreamVolumeW(int stream, int level) { + if (D.BUG) Log.d(TAG, "onSetStreamVolume " + stream + " level=" + level); + if (stream >= DYNAMIC_STREAM_START_INDEX) { + mMediaSessionsCallbacksW.setStreamVolume(stream, level); + return; + } + mAudio.setStreamVolume(stream, level, 0); + } + + private void onSetActiveStreamW(int stream) { + boolean changed = updateActiveStreamW(stream); + if (changed) { + mCallbacks.onStateChanged(mState); + } + } + + private void onSetExitConditionW(Condition condition) { + mNoMan.setZenModeCondition(condition); + } + + private void onSetZenModeW(int mode) { + if (D.BUG) Log.d(TAG, "onSetZenModeW " + mode); + mNoMan.setZenMode(mode); + } + + private void onDismissRequestedW(int reason) { + mCallbacks.onDismissRequested(reason); + } + + public void showDndTile(boolean visible) { + if (D.BUG) Log.d(TAG, "showDndTile"); + mContext.sendBroadcast(new Intent("com.android.systemui.dndtile.SET_VISIBLE") + .putExtra("visible", visible)); + } + + private final class VC extends IVolumeController.Stub { + private final String TAG = VolumeDialogController.TAG + ".VC"; + + @Override + public void displaySafeVolumeWarning(int flags) throws RemoteException { + if (D.BUG) Log.d(TAG, "displaySafeVolumeWarning " + + Util.audioManagerFlagsToString(flags)); + if (mDestroyed) return; + mWorker.obtainMessage(W.SHOW_SAFETY_WARNING, flags, 0).sendToTarget(); + } + + @Override + public void volumeChanged(int streamType, int flags) throws RemoteException { + if (D.BUG) Log.d(TAG, "volumeChanged " + AudioSystem.streamToString(streamType) + + " " + Util.audioManagerFlagsToString(flags)); + if (mDestroyed) return; + mWorker.obtainMessage(W.VOLUME_CHANGED, streamType, flags).sendToTarget(); + } + + @Override + public void masterMuteChanged(int flags) throws RemoteException { + if (D.BUG) Log.d(TAG, "masterMuteChanged"); + } + + @Override + public void setLayoutDirection(int layoutDirection) throws RemoteException { + if (D.BUG) Log.d(TAG, "setLayoutDirection"); + if (mDestroyed) return; + mWorker.obtainMessage(W.LAYOUT_DIRECTION_CHANGED, layoutDirection, 0).sendToTarget(); + } + + @Override + public void dismiss() throws RemoteException { + if (D.BUG) Log.d(TAG, "dismiss requested"); + if (mDestroyed) return; + mWorker.obtainMessage(W.DISMISS_REQUESTED, Events.DISMISS_REASON_VOLUME_CONTROLLER, 0) + .sendToTarget(); + mWorker.sendEmptyMessage(W.DISMISS_REQUESTED); + } + } + + private final class W extends Handler { + private static final int VOLUME_CHANGED = 1; + private static final int DISMISS_REQUESTED = 2; + private static final int GET_STATE = 3; + private static final int SET_RINGER_MODE = 4; + private static final int SET_ZEN_MODE = 5; + private static final int SET_EXIT_CONDITION = 6; + private static final int SET_STREAM_MUTE = 7; + private static final int LAYOUT_DIRECTION_CHANGED = 8; + private static final int CONFIGURATION_CHANGED = 9; + private static final int SET_STREAM_VOLUME = 10; + private static final int SET_ACTIVE_STREAM = 11; + private static final int NOTIFY_VISIBLE = 12; + private static final int USER_ACTIVITY = 13; + private static final int SHOW_SAFETY_WARNING = 14; + + W(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case VOLUME_CHANGED: onVolumeChangedW(msg.arg1, msg.arg2); break; + case DISMISS_REQUESTED: onDismissRequestedW(msg.arg1); break; + case GET_STATE: onGetStateW(); break; + case SET_RINGER_MODE: onSetRingerModeW(msg.arg1, msg.arg2 != 0); break; + case SET_ZEN_MODE: onSetZenModeW(msg.arg1); break; + case SET_EXIT_CONDITION: onSetExitConditionW((Condition) msg.obj); break; + case SET_STREAM_MUTE: onSetStreamMuteW(msg.arg1, msg.arg2 != 0); break; + case LAYOUT_DIRECTION_CHANGED: mCallbacks.onLayoutDirectionChanged(msg.arg1); break; + case CONFIGURATION_CHANGED: mCallbacks.onConfigurationChanged(); break; + case SET_STREAM_VOLUME: onSetStreamVolumeW(msg.arg1, msg.arg2); break; + case SET_ACTIVE_STREAM: onSetActiveStreamW(msg.arg1); break; + case NOTIFY_VISIBLE: onNotifyVisibleW(msg.arg1 != 0); break; + case USER_ACTIVITY: onUserActivityW(); break; + case SHOW_SAFETY_WARNING: onShowSafetyWarningW(msg.arg1); break; + } + } + } + + private final class C implements Callbacks { + private final HashMap<Callbacks, Handler> mCallbackMap = new HashMap<>(); + + public void add(Callbacks callback, Handler handler) { + if (callback == null || handler == null) throw new IllegalArgumentException(); + mCallbackMap.put(callback, handler); + } + + public void remove(Callbacks callback) { + mCallbackMap.remove(callback); + } + + @Override + public void onShowRequested(final int reason) { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onShowRequested(reason); + } + }); + } + } + + @Override + public void onDismissRequested(final int reason) { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onDismissRequested(reason); + } + }); + } + } + + @Override + public void onStateChanged(final State state) { + final long time = System.currentTimeMillis(); + final State copy = state.copy(); + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onStateChanged(copy); + } + }); + } + Events.writeState(time, copy); + } + + @Override + public void onLayoutDirectionChanged(final int layoutDirection) { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onLayoutDirectionChanged(layoutDirection); + } + }); + } + } + + @Override + public void onConfigurationChanged() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onConfigurationChanged(); + } + }); + } + } + + @Override + public void onShowVibrateHint() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onShowVibrateHint(); + } + }); + } + } + + @Override + public void onShowSilentHint() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onShowSilentHint(); + } + }); + } + } + + @Override + public void onScreenOff() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onScreenOff(); + } + }); + } + } + + @Override + public void onShowSafetyWarning(final int flags) { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onShowSafetyWarning(flags); + } + }); + } + } + } + + + private final class SettingObserver extends ContentObserver { + private final Uri SERVICE_URI = Settings.Secure.getUriFor( + Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT); + private final Uri ZEN_MODE_URI = + Settings.Global.getUriFor(Settings.Global.ZEN_MODE); + private final Uri ZEN_MODE_CONFIG_URI = + Settings.Global.getUriFor(Settings.Global.ZEN_MODE_CONFIG_ETAG); + + public SettingObserver(Handler handler) { + super(handler); + } + + public void init() { + mContext.getContentResolver().registerContentObserver(SERVICE_URI, false, this); + mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this); + mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_URI, false, this); + onChange(true, SERVICE_URI); + } + + public void destroy() { + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + boolean changed = false; + if (SERVICE_URI.equals(uri)) { + final String setting = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT); + final boolean enabled = setting != null && mComponent != null + && mComponent.equals(ComponentName.unflattenFromString(setting)); + if (enabled == mEnabled) return; + if (enabled) { + register(); + } + mEnabled = enabled; + } + if (ZEN_MODE_URI.equals(uri)) { + changed = updateZenModeW(); + } + if (ZEN_MODE_CONFIG_URI.equals(uri)) { + changed = updateExitConditionW(); + } + if (changed) { + mCallbacks.onStateChanged(mState); + } + } + } + + private final class Receiver extends BroadcastReceiver { + + public void init() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(AudioManager.VOLUME_CHANGED_ACTION); + filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION); + filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); + filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); + filter.addAction(AudioManager.STREAM_MUTE_CHANGED_ACTION); + filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + filter.addAction(Intent.ACTION_SCREEN_OFF); + mContext.registerReceiver(this, filter, null, mWorker); + } + + public void destroy() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + boolean changed = false; + if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { + final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final int level = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1); + final int oldLevel = intent + .getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, -1); + if (D.BUG) Log.d(TAG, "onReceive VOLUME_CHANGED_ACTION stream=" + stream + + " level=" + level + " oldLevel=" + oldLevel); + changed = updateStreamLevelW(stream, level); + } else if (action.equals(AudioManager.STREAM_DEVICES_CHANGED_ACTION)) { + final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final int devices = intent + .getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_DEVICES, -1); + final int oldDevices = intent + .getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_DEVICES, -1); + if (D.BUG) Log.d(TAG, "onReceive STREAM_DEVICES_CHANGED_ACTION stream=" + + stream + " devices=" + devices + " oldDevices=" + oldDevices); + changed = checkRoutedToBluetoothW(stream); + } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { + final int rm = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1); + if (D.BUG) Log.d(TAG, "onReceive RINGER_MODE_CHANGED_ACTION rm=" + + Util.ringerModeToString(rm)); + changed = updateRingerModeExternalW(rm); + } else if (action.equals(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)) { + final int rm = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1); + if (D.BUG) Log.d(TAG, "onReceive INTERNAL_RINGER_MODE_CHANGED_ACTION rm=" + + Util.ringerModeToString(rm)); + changed = updateRingerModeInternalW(rm); + } else if (action.equals(AudioManager.STREAM_MUTE_CHANGED_ACTION)) { + final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final boolean muted = intent + .getBooleanExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, false); + if (D.BUG) Log.d(TAG, "onReceive STREAM_MUTE_CHANGED_ACTION stream=" + stream + + " muted=" + muted); + changed = updateStreamMuteW(stream, muted); + } else if (action.equals(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED)) { + if (D.BUG) Log.d(TAG, "onReceive ACTION_EFFECTS_SUPPRESSOR_CHANGED"); + changed = updateEffectsSuppressorW(mNoMan.getEffectsSuppressor()); + } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) { + if (D.BUG) Log.d(TAG, "onReceive ACTION_CONFIGURATION_CHANGED"); + mCallbacks.onConfigurationChanged(); + } else if (action.equals(Intent.ACTION_SCREEN_OFF)) { + if (D.BUG) Log.d(TAG, "onReceive ACTION_SCREEN_OFF"); + mCallbacks.onScreenOff(); + } + if (changed) { + mCallbacks.onStateChanged(mState); + } + } + } + + private final class MediaSessionsCallbacks implements MediaSessions.Callbacks { + private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); + + private int mNextStream = DYNAMIC_STREAM_START_INDEX; + + @Override + public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) { + if (!mRemoteStreams.containsKey(token)) { + mRemoteStreams.put(token, mNextStream); + if (D.BUG) Log.d(TAG, "onRemoteUpdate: " + name + " is stream " + mNextStream); + mNextStream++; + } + final int stream = mRemoteStreams.get(token); + boolean changed = mState.states.indexOfKey(stream) < 0; + final StreamState ss = streamStateW(stream); + ss.dynamic = true; + ss.levelMin = 0; + ss.levelMax = pi.getMaxVolume(); + if (ss.level != pi.getCurrentVolume()) { + ss.level = pi.getCurrentVolume(); + changed = true; + } + if (!Objects.equals(ss.name, name)) { + ss.name = name; + changed = true; + } + if (changed) { + if (D.BUG) Log.d(TAG, "onRemoteUpdate: " + name + ": " + ss.level + + " of " + ss.levelMax); + mCallbacks.onStateChanged(mState); + } + } + + @Override + public void onRemoteVolumeChanged(Token token, int flags) { + final int stream = mRemoteStreams.get(token); + final boolean showUI = (flags & AudioManager.FLAG_SHOW_UI) != 0; + boolean changed = updateActiveStreamW(stream); + if (showUI) { + changed |= checkRoutedToBluetoothW(AudioManager.STREAM_MUSIC); + } + if (changed) { + mCallbacks.onStateChanged(mState); + } + if (showUI) { + mCallbacks.onShowRequested(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED); + } + } + + @Override + public void onRemoteRemoved(Token token) { + final int stream = mRemoteStreams.get(token); + mState.states.remove(stream); + if (mState.activeStream == stream) { + updateActiveStreamW(-1); + } + mCallbacks.onStateChanged(mState); + } + + public void setStreamVolume(int stream, int level) { + final Token t = findToken(stream); + if (t == null) { + Log.w(TAG, "setStreamVolume: No token found for stream: " + stream); + return; + } + mMediaSessions.setVolume(t, level); + } + + private Token findToken(int stream) { + for (Map.Entry<Token, Integer> entry : mRemoteStreams.entrySet()) { + if (entry.getValue().equals(stream)) { + return entry.getKey(); + } + } + return null; + } + } + + public static final class StreamState { + public boolean dynamic; + public int level; + public int levelMin; + public int levelMax; + public boolean muted; + public boolean muteSupported; + public String name; + public boolean routedToBluetooth; + + public StreamState copy() { + final StreamState rt = new StreamState(); + rt.dynamic = dynamic; + rt.level = level; + rt.levelMin = levelMin; + rt.levelMax = levelMax; + rt.muted = muted; + rt.muteSupported = muteSupported; + rt.name = name; + rt.routedToBluetooth = routedToBluetooth; + return rt; + } + } + + public static final class State { + public static int NO_ACTIVE_STREAM = -1; + + public final SparseArray<StreamState> states = new SparseArray<StreamState>(); + + public int ringerModeInternal; + public int ringerModeExternal; + public int zenMode; + public ComponentName effectsSuppressor; + public String effectsSuppressorName; + public Condition exitCondition; + public int activeStream = NO_ACTIVE_STREAM; + + public State copy() { + final State rt = new State(); + for (int i = 0; i < states.size(); i++) { + rt.states.put(states.keyAt(i), states.valueAt(i).copy()); + } + rt.ringerModeExternal = ringerModeExternal; + rt.ringerModeInternal = ringerModeInternal; + rt.zenMode = zenMode; + if (effectsSuppressor != null) rt.effectsSuppressor = effectsSuppressor.clone(); + rt.effectsSuppressorName = effectsSuppressorName; + if (exitCondition != null) rt.exitCondition = exitCondition.copy(); + rt.activeStream = activeStream; + return rt; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + for (int i = 0; i < states.size(); i++) { + if (i > 0) sb.append(','); + final int stream = states.keyAt(i); + final StreamState ss = states.valueAt(i); + sb.append(AudioSystem.streamToString(stream)).append(":").append(ss.level) + .append("[").append(ss.levelMin).append("..").append(ss.levelMax); + if (ss.muted) sb.append(" [MUTED]"); + } + sb.append(",ringerModeExternal:").append(ringerModeExternal); + sb.append(",ringerModeInternal:").append(ringerModeInternal); + sb.append(",zenMode:").append(zenMode); + sb.append(",effectsSuppressor:").append(effectsSuppressor); + sb.append(",effectsSuppressorName:").append(effectsSuppressorName); + sb.append(",exitCondition:").append(exitCondition); + sb.append(",activeStream:").append(activeStream); + return sb.append('}').toString(); + } + } + + public interface Callbacks { + void onShowRequested(int reason); + void onDismissRequested(int reason); + void onStateChanged(State state); + void onLayoutDirectionChanged(int layoutDirection); + void onConfigurationChanged(); + void onShowVibrateHint(); + void onShowSilentHint(); + void onScreenOff(); + void onShowSafetyWarning(int flags); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java index d16b818..f16e9d2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java @@ -52,7 +52,6 @@ import android.os.Message; import android.os.Vibrator; import android.util.Log; import android.util.SparseArray; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -72,7 +71,6 @@ import android.widget.TextView; import com.android.internal.R; import com.android.systemui.DemoMode; -import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.ZenModeController; import java.io.FileDescriptor; @@ -264,80 +262,6 @@ public class VolumePanel extends Handler implements DemoMode { private static AlertDialog sSafetyWarning; private static Object sSafetyWarningLock = new Object(); - private static class SafetyWarning extends SystemUIDialog - implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener { - private final Context mContext; - private final VolumePanel mVolumePanel; - private final AudioManager mAudioManager; - - private boolean mNewVolumeUp; - - SafetyWarning(Context context, VolumePanel volumePanel, AudioManager audioManager) { - super(context); - mContext = context; - mVolumePanel = volumePanel; - mAudioManager = audioManager; - - setMessage(mContext.getString(com.android.internal.R.string.safe_media_volume_warning)); - setButton(DialogInterface.BUTTON_POSITIVE, - mContext.getString(com.android.internal.R.string.yes), this); - setButton(DialogInterface.BUTTON_NEGATIVE, - mContext.getString(com.android.internal.R.string.no), (OnClickListener) null); - setOnDismissListener(this); - - IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - context.registerReceiver(mReceiver, filter); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && event.getRepeatCount() == 0) { - mNewVolumeUp = true; - } - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mNewVolumeUp) { - if (LOGD) Log.d(TAG, "Confirmed warning via VOLUME_UP"); - mAudioManager.disableSafeMediaVolume(); - dismiss(); - } - return super.onKeyUp(keyCode, event); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - mAudioManager.disableSafeMediaVolume(); - } - - @Override - public void onDismiss(DialogInterface unused) { - mContext.unregisterReceiver(mReceiver); - cleanUp(); - } - - private void cleanUp() { - synchronized (sSafetyWarningLock) { - sSafetyWarning = null; - } - mVolumePanel.forceTimeout(0); - mVolumePanel.updateStates(); - } - - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { - if (LOGD) Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS"); - cancel(); - cleanUp(); - } - } - }; - } - protected LayoutParams getDialogLayoutParams(Window window, Resources res) { final LayoutParams lp = window.getAttributes(); lp.token = null; @@ -384,7 +308,7 @@ public class VolumePanel extends Handler implements DemoMode { final Window window = mDialog.getWindow(); window.requestFeature(Window.FEATURE_NO_TITLE); mDialog.setCanceledOnTouchOutside(true); - mDialog.setContentView(com.android.systemui.R.layout.volume_dialog); + mDialog.setContentView(com.android.systemui.R.layout.volume_panel_dialog); mDialog.setOnDismissListener(new OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { @@ -1283,7 +1207,16 @@ public class VolumePanel extends Handler implements DemoMode { if (sSafetyWarning != null) { return; } - sSafetyWarning = new SafetyWarning(mContext, this, mAudioManager); + sSafetyWarning = new SafetyWarningDialog(mContext, mAudioManager) { + @Override + protected void cleanUp() { + synchronized (sSafetyWarningLock) { + sSafetyWarning = null; + } + forceTimeout(0); + updateStates(); + } + }; sSafetyWarning.show(); } updateStates(); diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelComponent.java new file mode 100644 index 0000000..fa6ea9e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelComponent.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume; + +import android.content.Context; +import android.content.res.Configuration; +import android.media.AudioManager; +import android.media.IRemoteVolumeController; +import android.media.IVolumeController; +import android.media.VolumePolicy; +import android.media.session.ISessionController; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; + +import com.android.systemui.R; +import com.android.systemui.SystemUI; +import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.qs.tiles.DndTile; +import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.statusbar.policy.ZenModeController; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Implementation of VolumeComponent backed by the old volume panel. + */ +public class VolumePanelComponent implements VolumeComponent { + + private final SystemUI mSysui; + private final Context mContext; + private final Handler mHandler; + private final VolumeController mVolumeController; + private final RemoteVolumeController mRemoteVolumeController; + private final AudioManager mAudioManager; + private final MediaSessionManager mMediaSessionManager; + + private VolumePanel mPanel; + private int mDismissDelay; + + public VolumePanelComponent(SystemUI sysui, Context context, Handler handler, + ZenModeController controller) { + mSysui = sysui; + mContext = context; + mHandler = handler; + mAudioManager = context.getSystemService(AudioManager.class); + mMediaSessionManager = context.getSystemService(MediaSessionManager.class); + mVolumeController = new VolumeController(); + mRemoteVolumeController = new RemoteVolumeController(); + mDismissDelay = mContext.getResources().getInteger(R.integer.volume_panel_dismiss_delay); + mPanel = new VolumePanel(mContext, controller); + mPanel.setCallback(new VolumePanel.Callback() { + @Override + public void onZenSettings() { + mHandler.removeCallbacks(mStartZenSettings); + mHandler.post(mStartZenSettings); + } + + @Override + public void onInteraction() { + final KeyguardViewMediator kvm = mSysui.getComponent(KeyguardViewMediator.class); + if (kvm != null) { + kvm.userActivity(); + } + } + + @Override + public void onVisible(boolean visible) { + if (mAudioManager != null && mVolumeController != null) { + mAudioManager.notifyVolumeControllerVisible(mVolumeController, visible); + } + } + }); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mPanel != null) { + mPanel.dump(fd, pw, args); + } + } + + public void register() { + mAudioManager.setVolumeController(mVolumeController); + mAudioManager.setVolumePolicy(VolumePolicy.DEFAULT); + mMediaSessionManager.setRemoteVolumeController(mRemoteVolumeController); + DndTile.setVisible(mContext, false); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + if (mPanel != null) { + mPanel.onConfigurationChanged(newConfig); + } + } + + @Override + public ZenModeController getZenController() { + return mPanel.getZenController(); + } + + @Override + public void dispatchDemoCommand(String command, Bundle args) { + mPanel.dispatchDemoCommand(command, args); + } + + @Override + public void dismissNow() { + mPanel.postDismiss(0); + } + + private final Runnable mStartZenSettings = new Runnable() { + @Override + public void run() { + mSysui.getComponent(PhoneStatusBar.class).startActivityDismissingKeyguard( + ZenModePanel.ZEN_SETTINGS, true /* onlyProvisioned */, true /* dismissShade */); + mPanel.postDismiss(mDismissDelay); + } + }; + + private final class RemoteVolumeController extends IRemoteVolumeController.Stub { + @Override + public void remoteVolumeChanged(ISessionController binder, int flags) + throws RemoteException { + MediaController controller = new MediaController(mContext, binder); + mPanel.postRemoteVolumeChanged(controller, flags); + } + + @Override + public void updateRemoteController(ISessionController session) throws RemoteException { + mPanel.postRemoteSliderVisibility(session != null); + // TODO stash default session in case the slider can be opened other + // than by remoteVolumeChanged. + } + } + + /** For now, simply host an unmodified base volume panel in this process. */ + private final class VolumeController extends IVolumeController.Stub { + + @Override + public void displaySafeVolumeWarning(int flags) throws RemoteException { + mPanel.postDisplaySafeVolumeWarning(flags); + } + + @Override + public void volumeChanged(int streamType, int flags) + throws RemoteException { + mPanel.postVolumeChanged(streamType, flags); + } + + @Override + public void masterMuteChanged(int flags) throws RemoteException { + // no-op + } + + @Override + public void setLayoutDirection(int layoutDirection) + throws RemoteException { + mPanel.postLayoutDirection(layoutDirection); + } + + @Override + public void dismiss() throws RemoteException { + dismissNow(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java index ac08904..387aed0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java @@ -29,25 +29,17 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.media.AudioManager; -import android.media.IRemoteVolumeController; -import android.media.IVolumeController; -import android.media.VolumePolicy; -import android.media.session.ISessionController; -import android.media.session.MediaController; import android.media.session.MediaSessionManager; -import android.os.Bundle; import android.os.Handler; -import android.os.RemoteException; +import android.os.SystemProperties; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import com.android.systemui.R; import com.android.systemui.SystemUI; -import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.statusbar.ServiceMonitor; -import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.ZenModeControllerImpl; @@ -59,6 +51,8 @@ public class VolumeUI extends SystemUI { private static final String TAG = "VolumeUI"; private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); + private static final boolean USE_OLD_VOLUME = SystemProperties.getBoolean("volume.old", false); + private final Handler mHandler = new Handler(); private final Receiver mReceiver = new Receiver(); private final RestorationNotification mRestorationNotification = new RestorationNotification(); @@ -67,12 +61,10 @@ public class VolumeUI extends SystemUI { private AudioManager mAudioManager; private NotificationManager mNotificationManager; private MediaSessionManager mMediaSessionManager; - private VolumeController mVolumeController; - private RemoteVolumeController mRemoteVolumeController; private ServiceMonitor mVolumeControllerService; - private VolumePanel mPanel; - private int mDismissDelay; + private VolumePanelComponent mOldVolume; + private VolumeDialogComponent mNewVolume; @Override public void start() { @@ -83,10 +75,10 @@ public class VolumeUI extends SystemUI { (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); mMediaSessionManager = (MediaSessionManager) mContext .getSystemService(Context.MEDIA_SESSION_SERVICE); - initPanel(); - mVolumeController = new VolumeController(); - mRemoteVolumeController = new RemoteVolumeController(); - putComponent(VolumeComponent.class, mVolumeController); + final ZenModeController zenController = new ZenModeControllerImpl(mContext, mHandler); + mOldVolume = new VolumePanelComponent(this, mContext, mHandler, zenController); + mNewVolume = new VolumeDialogComponent(this, mContext, null, zenController); + putComponent(VolumeComponent.class, getVolumeComponent()); mReceiver.start(); mVolumeControllerService = new ServiceMonitor(TAG, LOGD, mContext, Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT, @@ -94,30 +86,30 @@ public class VolumeUI extends SystemUI { mVolumeControllerService.start(); } + private VolumeComponent getVolumeComponent() { + return USE_OLD_VOLUME ? mOldVolume : mNewVolume; + } + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - if (mPanel != null) { - mPanel.onConfigurationChanged(newConfig); - } + if (!mEnabled) return; + getVolumeComponent().onConfigurationChanged(newConfig); } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.print("mEnabled="); pw.println(mEnabled); + if (!mEnabled) return; pw.print("mVolumeControllerService="); pw.println(mVolumeControllerService.getComponent()); - if (mPanel != null) { - mPanel.dump(fd, pw, args); - } + getVolumeComponent().dump(fd, pw, args); } - private void setVolumeController(boolean register) { + private void setDefaultVolumeController(boolean register) { if (register) { - if (LOGD) Log.d(TAG, "Registering default volume controller"); - mAudioManager.setVolumeController(mVolumeController); - mAudioManager.setVolumePolicy(VolumePolicy.DEFAULT); - mMediaSessionManager.setRemoteVolumeController(mRemoteVolumeController); DndTile.setVisible(mContext, false); + if (LOGD) Log.d(TAG, "Registering default volume controller"); + getVolumeComponent().register(); } else { if (LOGD) Log.d(TAG, "Unregistering default volume controller"); mAudioManager.setVolumeController(null); @@ -125,33 +117,6 @@ public class VolumeUI extends SystemUI { } } - private void initPanel() { - mDismissDelay = mContext.getResources().getInteger(R.integer.volume_panel_dismiss_delay); - mPanel = new VolumePanel(mContext, new ZenModeControllerImpl(mContext, mHandler)); - mPanel.setCallback(new VolumePanel.Callback() { - @Override - public void onZenSettings() { - mHandler.removeCallbacks(mStartZenSettings); - mHandler.post(mStartZenSettings); - } - - @Override - public void onInteraction() { - final KeyguardViewMediator kvm = getComponent(KeyguardViewMediator.class); - if (kvm != null) { - kvm.userActivity(); - } - } - - @Override - public void onVisible(boolean visible) { - if (mAudioManager != null && mVolumeController != null) { - mAudioManager.notifyVolumeControllerVisible(mVolumeController, visible); - } - } - }); - } - private String getAppLabel(ComponentName component) { final String pkg = component.getPackageName(); try { @@ -179,83 +144,11 @@ public class VolumeUI extends SystemUI { d.show(); } - private final Runnable mStartZenSettings = new Runnable() { - @Override - public void run() { - getComponent(PhoneStatusBar.class).startActivityDismissingKeyguard( - ZenModePanel.ZEN_SETTINGS, true /* onlyProvisioned */, true /* dismissShade */); - mPanel.postDismiss(mDismissDelay); - } - }; - - /** For now, simply host an unmodified base volume panel in this process. */ - private final class VolumeController extends IVolumeController.Stub implements VolumeComponent { - - @Override - public void displaySafeVolumeWarning(int flags) throws RemoteException { - mPanel.postDisplaySafeVolumeWarning(flags); - } - - @Override - public void volumeChanged(int streamType, int flags) - throws RemoteException { - mPanel.postVolumeChanged(streamType, flags); - } - - @Override - public void masterMuteChanged(int flags) throws RemoteException { - // no-op - } - - @Override - public void setLayoutDirection(int layoutDirection) - throws RemoteException { - mPanel.postLayoutDirection(layoutDirection); - } - - @Override - public void dismiss() throws RemoteException { - dismissNow(); - } - - @Override - public ZenModeController getZenController() { - return mPanel.getZenController(); - } - - @Override - public void dispatchDemoCommand(String command, Bundle args) { - mPanel.dispatchDemoCommand(command, args); - } - - @Override - public void dismissNow() { - mPanel.postDismiss(0); - } - } - - private final class RemoteVolumeController extends IRemoteVolumeController.Stub { - - @Override - public void remoteVolumeChanged(ISessionController binder, int flags) - throws RemoteException { - MediaController controller = new MediaController(mContext, binder); - mPanel.postRemoteVolumeChanged(controller, flags); - } - - @Override - public void updateRemoteController(ISessionController session) throws RemoteException { - mPanel.postRemoteSliderVisibility(session != null); - // TODO stash default session in case the slider can be opened other - // than by remoteVolumeChanged. - } - } - private final class ServiceMonitorCallbacks implements ServiceMonitor.Callbacks { @Override public void onNoService() { if (LOGD) Log.d(TAG, "onNoService"); - setVolumeController(true); + setDefaultVolumeController(true); mRestorationNotification.hide(); if (!mVolumeControllerService.isPackageAvailable()) { mVolumeControllerService.setComponent(null); @@ -267,8 +160,8 @@ public class VolumeUI extends SystemUI { if (LOGD) Log.d(TAG, "onServiceStartAttempt"); // poke the setting to update the uid mVolumeControllerService.setComponent(mVolumeControllerService.getComponent()); - setVolumeController(false); - mVolumeController.dismissNow(); + setDefaultVolumeController(false); + getVolumeComponent().dismissNow(); mRestorationNotification.show(); return 0; } diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java new file mode 100644 index 0000000..f99eb6d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.volume; + +import android.animation.LayoutTransition; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.provider.Settings.Global; +import android.service.notification.Condition; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.LinearLayout; +import android.widget.Switch; +import android.widget.TextView; + +import com.android.systemui.R; +import com.android.systemui.statusbar.policy.ZenModeController; + +/** + * Switch bar + zen mode panel (conditions) attached to the bottom of the volume dialog. + */ +public class ZenFooter extends LinearLayout { + private static final String TAG = Util.logTag(ZenFooter.class); + + private final Context mContext; + private final float mSecondaryAlpha; + private final LayoutTransition mLayoutTransition; + + private ZenModeController mController; + private Switch mSwitch; + private ZenModePanel mZenModePanel; + private View mZenModePanelButtons; + private View mZenModePanelMoreButton; + private View mZenModePanelDoneButton; + private View mSwitchBar; + private View mSwitchBarIcon; + private View mSummary; + private TextView mSummaryLine1; + private TextView mSummaryLine2; + private boolean mFooterExpanded; + private int mZen = -1; + private Callback mCallback; + + public ZenFooter(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mSecondaryAlpha = getFloat(context.getResources(), R.dimen.volume_secondary_alpha); + mLayoutTransition = new LayoutTransition(); + mLayoutTransition.setDuration(new ValueAnimator().getDuration() / 2); + mLayoutTransition.disableTransitionType(LayoutTransition.DISAPPEARING); + mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); + } + + private static float getFloat(Resources r, int resId) { + final TypedValue tv = new TypedValue(); + r.getValue(resId, tv, true); + return tv.getFloat(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSwitchBar = findViewById(R.id.volume_zen_switch_bar); + mSwitchBarIcon = findViewById(R.id.volume_zen_switch_bar_icon); + mSwitch = (Switch) findViewById(R.id.volume_zen_switch); + mZenModePanel = (ZenModePanel) findViewById(R.id.zen_mode_panel); + mZenModePanelButtons = findViewById(R.id.volume_zen_mode_panel_buttons); + mZenModePanelMoreButton = findViewById(R.id.volume_zen_mode_panel_more); + mZenModePanelDoneButton = findViewById(R.id.volume_zen_mode_panel_done); + mSummary = findViewById(R.id.volume_zen_panel_summary); + mSummaryLine1 = (TextView) findViewById(R.id.volume_zen_panel_summary_line_1); + mSummaryLine2 = (TextView) findViewById(R.id.volume_zen_panel_summary_line_2); + } + + public void init(ZenModeController controller, Callback callback) { + mCallback = callback; + mController = controller; + mZenModePanel.init(controller); + mZenModePanel.setEmbedded(true); + mSwitch.setOnCheckedChangeListener(mCheckedListener); + mController.addCallback(new ZenModeController.Callback() { + @Override + public void onZenChanged(int zen) { + setZen(zen); + } + @Override + public void onExitConditionChanged(Condition exitCondition) { + update(); + } + }); + mSwitchBar.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mSwitch.setChecked(!mSwitch.isChecked()); + } + }); + mZenModePanelMoreButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mCallback != null) { + mCallback.onSettingsClicked(); + } + } + }); + mZenModePanelDoneButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mCallback != null) { + mCallback.onDoneClicked(); + } + } + }); + mZen = mController.getZen(); + update(); + } + + private void setZen(int zen) { + if (mZen == zen) return; + mZen = zen; + update(); + } + + public boolean isZen() { + return isZenPriority() || isZenAlarms() || isZenNone(); + } + + private boolean isZenPriority() { + return mZen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + } + + private boolean isZenAlarms() { + return mZen == Global.ZEN_MODE_ALARMS; + } + + private boolean isZenNone() { + return mZen == Global.ZEN_MODE_NO_INTERRUPTIONS; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + setLayoutTransition(null); + setFooterExpanded(false); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + setLayoutTransition(mLayoutTransition); + } + + private boolean setFooterExpanded(boolean expanded) { + if (mFooterExpanded == expanded) return false; + mFooterExpanded = expanded; + update(); + if (mCallback != null) { + mCallback.onFooterExpanded(); + } + return true; + } + + public boolean isFooterExpanded() { + return mFooterExpanded; + } + + public void update() { + final boolean isZen = isZen(); + mSwitch.setOnCheckedChangeListener(null); + mSwitch.setChecked(isZen); + mSwitch.setOnCheckedChangeListener(mCheckedListener); + Util.setVisOrGone(mZenModePanel, isZen && mFooterExpanded); + Util.setVisOrGone(mZenModePanelButtons, isZen && mFooterExpanded); + Util.setVisOrGone(mSummary, isZen && !mFooterExpanded); + mSwitchBarIcon.setAlpha(isZen ? 1 : mSecondaryAlpha); + final String line1 = + isZenPriority() ? mContext.getString(R.string.interruption_level_priority) + : isZenAlarms() ? mContext.getString(R.string.interruption_level_alarms) + : isZenNone() ? mContext.getString(R.string.interruption_level_none) + : null; + Util.setText(mSummaryLine1, line1); + Util.setText(mSummaryLine2, mZenModePanel.getExitConditionText()); + } + + private final OnCheckedChangeListener mCheckedListener = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (D.BUG) Log.d(TAG, "onCheckedChanged " + isChecked); + if (isChecked != isZen()) { + final int newZen = isChecked ? Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS + : Global.ZEN_MODE_OFF; + mZen = newZen; // this one's optimistic + setFooterExpanded(isChecked); + mController.setZen(newZen); + } + } + }; + + public interface Callback { + void onFooterExpanded(); + void onSettingsClicked(); + void onDoneClicked(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java index 878ab712..cb6c29f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java @@ -23,7 +23,6 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.res.Resources; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; @@ -150,12 +149,13 @@ public class ZenModePanel extends LinearLayout { if (mEmbedded == embedded) return; mEmbedded = embedded; mZenButtonsContainer.setLayoutTransition(mEmbedded ? null : newLayoutTransition(null)); + setLayoutTransition(mEmbedded ? null : newLayoutTransition(null)); if (mEmbedded) { mZenButtonsContainer.setBackground(null); } else { mZenButtonsContainer.setBackgroundResource(R.drawable.qs_background_secondary); } - mZenButtons.getChildAt(2).setVisibility(mEmbedded ? GONE : VISIBLE); + mZenButtons.getChildAt(3).setVisibility(mEmbedded ? GONE : VISIBLE); mZenEmbeddedDivider.setVisibility(mEmbedded ? VISIBLE : GONE); setExpanded(mEmbedded); updateWidgets(); @@ -166,12 +166,13 @@ public class ZenModePanel extends LinearLayout { super.onFinishInflate(); mZenButtons = (SegmentedButtons) findViewById(R.id.zen_buttons); - mZenButtons.addButton(R.string.interruption_level_none, R.drawable.ic_zen_none, + mZenButtons.addButton(R.string.interruption_level_none_twoline, Global.ZEN_MODE_NO_INTERRUPTIONS); - mZenButtons.addButton(R.string.interruption_level_priority, R.drawable.ic_zen_important, + mZenButtons.addButton(R.string.interruption_level_alarms_twoline, + Global.ZEN_MODE_ALARMS); + mZenButtons.addButton(R.string.interruption_level_priority_twoline, Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); - mZenButtons.addButton(R.string.interruption_level_all, R.drawable.ic_zen_all, - Global.ZEN_MODE_OFF); + mZenButtons.addButton(R.string.interruption_level_all, Global.ZEN_MODE_OFF); mZenButtons.setCallback(mZenButtonsCallback); mZenButtonsContainer = (ViewGroup) findViewById(R.id.zen_buttons_container); @@ -275,6 +276,7 @@ public class ZenModePanel extends LinearLayout { private void setExpanded(boolean expanded) { if (expanded == mExpanded) return; + if (DEBUG) Log.d(mTag, "setExpanded " + expanded); mExpanded = expanded; if (mExpanded) { ensureSelection(); @@ -358,6 +360,10 @@ public class ZenModePanel extends LinearLayout { return condition == null ? null : condition.copy(); } + public String getExitConditionText() { + return mExitConditionText; + } + private void refreshExitConditionText() { if (mExitCondition == null) { mExitConditionText = foreverSummary(); @@ -428,7 +434,7 @@ public class ZenModePanel extends LinearLayout { mZenSubheadExpanded.setVisibility(expanded ? VISIBLE : GONE); mZenSubheadCollapsed.setVisibility(!expanded ? VISIBLE : GONE); mMoreSettings.setVisibility(zenImportant && expanded ? VISIBLE : GONE); - mZenConditions.setVisibility(!zenOff && expanded ? VISIBLE : GONE); + mZenConditions.setVisibility(mEmbedded || !zenOff && expanded ? VISIBLE : GONE); if (zenNone) { mZenSubheadExpanded.setText(R.string.zen_no_interruptions_with_warning); @@ -715,7 +721,10 @@ public class ZenModePanel extends LinearLayout { case Global.ZEN_MODE_NO_INTERRUPTIONS: modeText = mContext.getString(R.string.zen_no_interruptions); break; - default: + case Global.ZEN_MODE_ALARMS: + modeText = mContext.getString(R.string.zen_alarms); + break; + default: return; } announceForAccessibility(mContext.getString(R.string.zen_mode_and_condition, modeText, |
