/* * 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.keyguard; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; public class KeyguardWidgetFrame extends FrameLayout { private final static PorterDuffXfermode sAddBlendMode = new PorterDuffXfermode(PorterDuff.Mode.ADD); static final float OUTLINE_ALPHA_MULTIPLIER = 0.6f; static final int HOVER_OVER_DELETE_DROP_TARGET_OVERLAY_COLOR = 0x99FF0000; // Temporarily disable this for the time being until we know why the gfx is messing up static final boolean ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY = true; private int mGradientColor; private LinearGradient mForegroundGradient; private LinearGradient mLeftToRightGradient; private LinearGradient mRightToLeftGradient; private Paint mGradientPaint = new Paint(); boolean mLeftToRight = true; private float mOverScrollAmount = 0f; private final Rect mForegroundRect = new Rect(); private int mForegroundAlpha = 0; private CheckLongPressHelper mLongPressHelper; private Animator mFrameFade; private boolean mIsSmall = false; private Handler mWorkerHandler; private float mBackgroundAlpha; private float mContentAlpha; private float mBackgroundAlphaMultiplier = 1.0f; private Drawable mBackgroundDrawable; private Rect mBackgroundRect = new Rect(); // These variables are all needed in order to size things properly before we're actually // measured. private int mSmallWidgetHeight; private int mSmallFrameHeight; private boolean mWidgetLockedSmall = false; private int mMaxChallengeTop = -1; private int mFrameStrokeAdjustment; private boolean mPerformAppWidgetSizeUpdateOnBootComplete; // This will hold the width value before we've actually been measured private int mFrameHeight; private boolean mIsHoveringOverDeleteDropTarget; // Multiple callers may try and adjust the alpha of the frame. When a caller shows // the outlines, we give that caller control, and nobody else can fade them out. // This prevents animation conflicts. private Object mBgAlphaController; public KeyguardWidgetFrame(Context context) { this(context, null, 0); } public KeyguardWidgetFrame(Context context, AttributeSet attrs) { this(context, attrs, 0); } public KeyguardWidgetFrame(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mLongPressHelper = new CheckLongPressHelper(this); Resources res = context.getResources(); // TODO: this padding should really correspond to the padding embedded in the background // drawable (ie. outlines). float density = res.getDisplayMetrics().density; int padding = (int) (res.getDisplayMetrics().density * 8); setPadding(padding, padding, padding, padding); mFrameStrokeAdjustment = 2 + (int) (2 * density); // This will be overriden on phones based on the current security mode, however on tablets // we need to specify a height. mSmallWidgetHeight = res.getDimensionPixelSize(R.dimen.kg_small_widget_height); mBackgroundDrawable = res.getDrawable(R.drawable.kg_widget_bg_padded); mGradientColor = res.getColor(R.color.kg_widget_pager_gradient); mGradientPaint.setXfermode(sAddBlendMode); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); cancelLongPress(); KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallbacks); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallbacks); } private KeyguardUpdateMonitorCallback mUpdateMonitorCallbacks = new KeyguardUpdateMonitorCallback() { @Override public void onBootCompleted() { if (mPerformAppWidgetSizeUpdateOnBootComplete) { performAppWidgetSizeCallbacksIfNecessary(); mPerformAppWidgetSizeUpdateOnBootComplete = false; } } }; void setIsHoveringOverDeleteDropTarget(boolean isHovering) { if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) { if (mIsHoveringOverDeleteDropTarget != isHovering) { mIsHoveringOverDeleteDropTarget = isHovering; int resId = isHovering ? R.string.keyguard_accessibility_delete_widget_start : R.string.keyguard_accessibility_delete_widget_end; String text = getContext().getResources().getString(resId, getContentDescription()); announceForAccessibility(text); invalidate(); } } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // Watch for longpress events at this level to make sure // users can always pick up this widget switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mLongPressHelper.postCheckForLongPress(ev); break; case MotionEvent.ACTION_MOVE: mLongPressHelper.onMove(ev); break; case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mLongPressHelper.cancelLongPress(); break; } // Otherwise continue letting touch events fall through to children return false; } @Override public boolean onTouchEvent(MotionEvent ev) { // Watch for longpress events at this level to make sure // users can always pick up this widget switch (ev.getAction()) { case MotionEvent.ACTION_MOVE: mLongPressHelper.onMove(ev); break; case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mLongPressHelper.cancelLongPress(); break; } // We return true here to ensure that we will get cancel / up signal // even if none of our children have requested touch. return true; } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { super.requestDisallowInterceptTouchEvent(disallowIntercept); cancelLongPress(); } @Override public void cancelLongPress() { super.cancelLongPress(); mLongPressHelper.cancelLongPress(); } private void drawGradientOverlay(Canvas c) { mGradientPaint.setShader(mForegroundGradient); mGradientPaint.setAlpha(mForegroundAlpha); c.drawRect(mForegroundRect, mGradientPaint); } private void drawHoveringOverDeleteOverlay(Canvas c) { if (mIsHoveringOverDeleteDropTarget) { c.drawColor(HOVER_OVER_DELETE_DROP_TARGET_OVERLAY_COLOR); } } protected void drawBg(Canvas canvas) { if (mBackgroundAlpha > 0.0f) { Drawable bg = mBackgroundDrawable; bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255)); bg.setBounds(mBackgroundRect); bg.draw(canvas); } } @Override protected void dispatchDraw(Canvas canvas) { if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) { canvas.save(); } drawBg(canvas); super.dispatchDraw(canvas); drawGradientOverlay(canvas); if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) { drawHoveringOverDeleteOverlay(canvas); canvas.restore(); } } /** * Because this view has fading outlines, it is essential that we enable hardware * layers on the content (child) so that updating the alpha of the outlines doesn't * result in the content layer being recreated. */ public void enableHardwareLayersForContent() { View widget = getContent(); if (widget != null && widget.isHardwareAccelerated()) { widget.setLayerType(LAYER_TYPE_HARDWARE, null); } } /** * Because this view has fading outlines, it is essential that we enable hardware * layers on the content (child) so that updating the alpha of the outlines doesn't * result in the content layer being recreated. */ public void disableHardwareLayersForContent() { View widget = getContent(); if (widget != null) { widget.setLayerType(LAYER_TYPE_NONE, null); } } public View getContent() { return getChildAt(0); } public int getContentAppWidgetId() { View content = getContent(); if (content instanceof AppWidgetHostView) { return ((AppWidgetHostView) content).getAppWidgetId(); } else if (content instanceof KeyguardStatusView) { return ((KeyguardStatusView) content).getAppWidgetId(); } else { return AppWidgetManager.INVALID_APPWIDGET_ID; } } public float getBackgroundAlpha() { return mBackgroundAlpha; } public void setBackgroundAlphaMultiplier(float multiplier) { if (Float.compare(mBackgroundAlphaMultiplier, multiplier) != 0) { mBackgroundAlphaMultiplier = multiplier; invalidate(); } } public float getBackgroundAlphaMultiplier() { return mBackgroundAlphaMultiplier; } public void setBackgroundAlpha(float alpha) { if (Float.compare(mBackgroundAlpha, alpha) != 0) { mBackgroundAlpha = alpha; invalidate(); } } public float getContentAlpha() { return mContentAlpha; } public void setContentAlpha(float alpha) { mContentAlpha = alpha; View content = getContent(); if (content != null) { content.setAlpha(alpha); } } /** * Depending on whether the security is up, the widget size needs to change * * @param height The height of the widget, -1 for full height */ private void setWidgetHeight(int height) { boolean needLayout = false; View widget = getContent(); if (widget != null) { LayoutParams lp = (LayoutParams) widget.getLayoutParams(); if (lp.height != height) { needLayout = true; lp.height = height; } } if (needLayout) { requestLayout(); } } public void setMaxChallengeTop(int top) { boolean dirty = mMaxChallengeTop != top; mMaxChallengeTop = top; mSmallWidgetHeight = top - getPaddingTop(); mSmallFrameHeight = top + getPaddingBottom(); if (dirty && mIsSmall) { setWidgetHeight(mSmallWidgetHeight); setFrameHeight(mSmallFrameHeight); } else if (dirty && mWidgetLockedSmall) { setWidgetHeight(mSmallWidgetHeight); } } public boolean isSmall() { return mIsSmall; } public void adjustFrame(int challengeTop) { int frameHeight = challengeTop + getPaddingBottom(); setFrameHeight(frameHeight); } public void shrinkWidget(boolean alsoShrinkFrame) { mIsSmall = true; setWidgetHeight(mSmallWidgetHeight); if (alsoShrinkFrame) { setFrameHeight(mSmallFrameHeight); } } public int getSmallFrameHeight() { return mSmallFrameHeight; } public void setWidgetLockedSmall(boolean locked) { if (locked) { setWidgetHeight(mSmallWidgetHeight); } mWidgetLockedSmall = locked; } public void resetSize() { mIsSmall = false; if (!mWidgetLockedSmall) { setWidgetHeight(LayoutParams.MATCH_PARENT); } setFrameHeight(getMeasuredHeight()); } public void setFrameHeight(int height) { mFrameHeight = height; mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(mFrameHeight, getMeasuredHeight())); mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment,getMeasuredWidth() - mFrameStrokeAdjustment, Math.min(getMeasuredHeight(), mFrameHeight) - mFrameStrokeAdjustment); updateGradient(); invalidate(); } public void hideFrame(Object caller) { fadeFrame(caller, false, 0f, KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_OUT_DURATION); } public void showFrame(Object caller) { fadeFrame(caller, true, OUTLINE_ALPHA_MULTIPLIER, KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_IN_DURATION); } public void fadeFrame(Object caller, boolean takeControl, float alpha, int duration) { if (takeControl) { mBgAlphaController = caller; } if (mBgAlphaController != caller && mBgAlphaController != null) { return; } if (mFrameFade != null) { mFrameFade.cancel(); mFrameFade = null; } PropertyValuesHolder bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", alpha); mFrameFade = ObjectAnimator.ofPropertyValuesHolder(this, bgAlpha); mFrameFade.setDuration(duration); mFrameFade.start(); } private void updateGradient() { float x0 = mLeftToRight ? 0 : mForegroundRect.width(); float x1 = mLeftToRight ? mForegroundRect.width(): 0; mLeftToRightGradient = new LinearGradient(x0, 0f, x1, 0f, mGradientColor, 0, Shader.TileMode.CLAMP); mRightToLeftGradient = new LinearGradient(x1, 0f, x0, 0f, mGradientColor, 0, Shader.TileMode.CLAMP); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (!mIsSmall) { mFrameHeight = h; } // mFrameStrokeAdjustment is a cludge to prevent the overlay from drawing outside the // rounded rect background. mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment, w - mFrameStrokeAdjustment, Math.min(h, mFrameHeight) - mFrameStrokeAdjustment); mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(h, mFrameHeight)); updateGradient(); invalidate(); } protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); performAppWidgetSizeCallbacksIfNecessary(); } private void performAppWidgetSizeCallbacksIfNecessary() { View content = getContent(); if (!(content instanceof AppWidgetHostView)) return; if (!KeyguardUpdateMonitor.getInstance(mContext).hasBootCompleted()) { mPerformAppWidgetSizeUpdateOnBootComplete = true; return; } // TODO: there's no reason to force the AppWidgetHostView to catch duplicate size calls. // We can do that even more cheaply here. It's not an issue right now since we're in the // system process and hence no binder calls. AppWidgetHostView awhv = (AppWidgetHostView) content; float density = getResources().getDisplayMetrics().density; int width = (int) (content.getMeasuredWidth() / density); int height = (int) (content.getMeasuredHeight() / density); awhv.updateAppWidgetSize(null, width, height, width, height, true); } void setOverScrollAmount(float r, boolean left) { if (Float.compare(mOverScrollAmount, r) != 0) { mOverScrollAmount = r; mForegroundGradient = left ? mLeftToRightGradient : mRightToLeftGradient; mForegroundAlpha = (int) Math.round((0.5f * r * 255)); // We bump up the alpha of the outline to hide the fact that the overlay is drawing // over the rounded part of the frame. float bgAlpha = Math.min(OUTLINE_ALPHA_MULTIPLIER + r * (1 - OUTLINE_ALPHA_MULTIPLIER), 1f); setBackgroundAlpha(bgAlpha); invalidate(); } } public void onActive(boolean isActive) { // hook for subclasses } public boolean onUserInteraction(MotionEvent event) { // hook for subclasses return false; } public void onBouncerShowing(boolean showing) { // hook for subclasses } public void setWorkerHandler(Handler workerHandler) { mWorkerHandler = workerHandler; } public Handler getWorkerHandler() { return mWorkerHandler; } }