KeyguardWidgetFrame.java revision 1962e264660b37b7e386eb2ac84fc915dd7c75dd
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.keyguard;
18
19import android.animation.Animator;
20import android.animation.ObjectAnimator;
21import android.animation.PropertyValuesHolder;
22import android.appwidget.AppWidgetHostView;
23import android.appwidget.AppWidgetManager;
24import android.content.Context;
25import android.content.res.Resources;
26import android.graphics.Canvas;
27import android.graphics.LinearGradient;
28import android.graphics.Paint;
29import android.graphics.PorterDuff;
30import android.graphics.PorterDuffXfermode;
31import android.graphics.Rect;
32import android.graphics.Shader;
33import android.graphics.drawable.Drawable;
34import android.os.Handler;
35import android.util.AttributeSet;
36import android.view.MotionEvent;
37import android.view.View;
38import android.widget.FrameLayout;
39
40public class KeyguardWidgetFrame extends FrameLayout {
41    private final static PorterDuffXfermode sAddBlendMode =
42            new PorterDuffXfermode(PorterDuff.Mode.ADD);
43
44    static final float OUTLINE_ALPHA_MULTIPLIER = 0.6f;
45    static final int HOVER_OVER_DELETE_DROP_TARGET_OVERLAY_COLOR = 0x99FF0000;
46
47    // Temporarily disable this for the time being until we know why the gfx is messing up
48    static final boolean ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY = true;
49
50    private int mGradientColor;
51    private LinearGradient mForegroundGradient;
52    private LinearGradient mLeftToRightGradient;
53    private LinearGradient mRightToLeftGradient;
54    private Paint mGradientPaint = new Paint();
55    boolean mLeftToRight = true;
56
57    private float mOverScrollAmount = 0f;
58    private final Rect mForegroundRect = new Rect();
59    private int mForegroundAlpha = 0;
60    private CheckLongPressHelper mLongPressHelper;
61    private Animator mFrameFade;
62    private boolean mIsSmall = false;
63    private Handler mWorkerHandler;
64
65    private float mBackgroundAlpha;
66    private float mContentAlpha;
67    private float mBackgroundAlphaMultiplier = 1.0f;
68    private Drawable mBackgroundDrawable;
69    private Rect mBackgroundRect = new Rect();
70
71    // These variables are all needed in order to size things properly before we're actually
72    // measured.
73    private int mSmallWidgetHeight;
74    private int mSmallFrameHeight;
75    private boolean mWidgetLockedSmall = false;
76    private int mMaxChallengeTop = -1;
77    private int mFrameStrokeAdjustment;
78    private boolean mPerformAppWidgetSizeUpdateOnBootComplete;
79
80    // This will hold the width value before we've actually been measured
81    private int mFrameHeight;
82
83    private boolean mIsHoveringOverDeleteDropTarget;
84
85    // Multiple callers may try and adjust the alpha of the frame. When a caller shows
86    // the outlines, we give that caller control, and nobody else can fade them out.
87    // This prevents animation conflicts.
88    private Object mBgAlphaController;
89
90    public KeyguardWidgetFrame(Context context) {
91        this(context, null, 0);
92    }
93
94    public KeyguardWidgetFrame(Context context, AttributeSet attrs) {
95        this(context, attrs, 0);
96    }
97
98    public KeyguardWidgetFrame(Context context, AttributeSet attrs, int defStyle) {
99        super(context, attrs, defStyle);
100
101        mLongPressHelper = new CheckLongPressHelper(this);
102
103        Resources res = context.getResources();
104        // TODO: this padding should really correspond to the padding embedded in the background
105        // drawable (ie. outlines).
106        float density = res.getDisplayMetrics().density;
107        int padding = (int) (res.getDisplayMetrics().density * 8);
108        setPadding(padding, padding, padding, padding);
109
110        mFrameStrokeAdjustment = 2 + (int) (2 * density);
111
112        // This will be overriden on phones based on the current security mode, however on tablets
113        // we need to specify a height.
114        mSmallWidgetHeight =
115                res.getDimensionPixelSize(R.dimen.kg_small_widget_height);
116        mBackgroundDrawable = res.getDrawable(R.drawable.kg_widget_bg_padded);
117        mGradientColor = res.getColor(R.color.kg_widget_pager_gradient);
118        mGradientPaint.setXfermode(sAddBlendMode);
119    }
120
121    @Override
122    protected void onDetachedFromWindow() {
123        super.onDetachedFromWindow();
124        cancelLongPress();
125        KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallbacks);
126
127    }
128
129    @Override
130    protected void onAttachedToWindow() {
131        super.onAttachedToWindow();
132        KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallbacks);
133    }
134
135    private KeyguardUpdateMonitorCallback mUpdateMonitorCallbacks =
136            new KeyguardUpdateMonitorCallback() {
137        @Override
138        public void onBootCompleted() {
139            if (mPerformAppWidgetSizeUpdateOnBootComplete) {
140                performAppWidgetSizeCallbacksIfNecessary();
141                mPerformAppWidgetSizeUpdateOnBootComplete = false;
142            }
143        }
144    };
145
146    void setIsHoveringOverDeleteDropTarget(boolean isHovering) {
147        if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) {
148            if (mIsHoveringOverDeleteDropTarget != isHovering) {
149                mIsHoveringOverDeleteDropTarget = isHovering;
150                int resId = isHovering ? R.string.keyguard_accessibility_delete_widget_start
151                        : R.string.keyguard_accessibility_delete_widget_end;
152                String text = getContext().getResources().getString(resId, getContentDescription());
153                announceForAccessibility(text);
154                invalidate();
155            }
156        }
157    }
158
159    @Override
160    public boolean onInterceptTouchEvent(MotionEvent ev) {
161        // Watch for longpress events at this level to make sure
162        // users can always pick up this widget
163        switch (ev.getAction()) {
164            case MotionEvent.ACTION_DOWN:
165                mLongPressHelper.postCheckForLongPress(ev);
166                break;
167            case MotionEvent.ACTION_MOVE:
168                mLongPressHelper.onMove(ev);
169                break;
170            case MotionEvent.ACTION_POINTER_DOWN:
171            case MotionEvent.ACTION_UP:
172            case MotionEvent.ACTION_CANCEL:
173                mLongPressHelper.cancelLongPress();
174                break;
175        }
176
177        // Otherwise continue letting touch events fall through to children
178        return false;
179    }
180
181    @Override
182    public boolean onTouchEvent(MotionEvent ev) {
183        // Watch for longpress events at this level to make sure
184        // users can always pick up this widget
185        switch (ev.getAction()) {
186            case MotionEvent.ACTION_MOVE:
187                mLongPressHelper.onMove(ev);
188                break;
189            case MotionEvent.ACTION_POINTER_DOWN:
190            case MotionEvent.ACTION_UP:
191            case MotionEvent.ACTION_CANCEL:
192                mLongPressHelper.cancelLongPress();
193                break;
194        }
195
196        // We return true here to ensure that we will get cancel / up signal
197        // even if none of our children have requested touch.
198        return true;
199    }
200
201    @Override
202    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
203        super.requestDisallowInterceptTouchEvent(disallowIntercept);
204        cancelLongPress();
205    }
206
207    @Override
208    public void cancelLongPress() {
209        super.cancelLongPress();
210        mLongPressHelper.cancelLongPress();
211    }
212
213
214    private void drawGradientOverlay(Canvas c) {
215        mGradientPaint.setShader(mForegroundGradient);
216        mGradientPaint.setAlpha(mForegroundAlpha);
217        c.drawRect(mForegroundRect, mGradientPaint);
218    }
219
220    private void drawHoveringOverDeleteOverlay(Canvas c) {
221        if (mIsHoveringOverDeleteDropTarget) {
222            c.drawColor(HOVER_OVER_DELETE_DROP_TARGET_OVERLAY_COLOR);
223        }
224    }
225
226    protected void drawBg(Canvas canvas) {
227        if (mBackgroundAlpha > 0.0f) {
228            Drawable bg = mBackgroundDrawable;
229
230            bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255));
231            bg.setBounds(mBackgroundRect);
232            bg.draw(canvas);
233        }
234    }
235
236    @Override
237    protected void dispatchDraw(Canvas canvas) {
238        if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) {
239            canvas.save();
240        }
241        drawBg(canvas);
242        super.dispatchDraw(canvas);
243        drawGradientOverlay(canvas);
244        if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) {
245            drawHoveringOverDeleteOverlay(canvas);
246            canvas.restore();
247        }
248    }
249
250    /**
251     * Because this view has fading outlines, it is essential that we enable hardware
252     * layers on the content (child) so that updating the alpha of the outlines doesn't
253     * result in the content layer being recreated.
254     */
255    public void enableHardwareLayersForContent() {
256        View widget = getContent();
257        if (widget != null) {
258            widget.setLayerType(LAYER_TYPE_HARDWARE, null);
259        }
260    }
261
262    /**
263     * Because this view has fading outlines, it is essential that we enable hardware
264     * layers on the content (child) so that updating the alpha of the outlines doesn't
265     * result in the content layer being recreated.
266     */
267    public void disableHardwareLayersForContent() {
268        View widget = getContent();
269        if (widget != null) {
270            widget.setLayerType(LAYER_TYPE_NONE, null);
271        }
272    }
273
274    public void enableHardwareLayers() {
275        setLayerType(LAYER_TYPE_HARDWARE, null);
276    }
277
278    public void disableHardwareLayers() {
279        setLayerType(LAYER_TYPE_NONE, null);
280    }
281
282    public View getContent() {
283        return getChildAt(0);
284    }
285
286    public int getContentAppWidgetId() {
287        View content = getContent();
288        if (content instanceof AppWidgetHostView) {
289            return ((AppWidgetHostView) content).getAppWidgetId();
290        } else if (content instanceof KeyguardStatusView) {
291            return ((KeyguardStatusView) content).getAppWidgetId();
292        } else {
293            return AppWidgetManager.INVALID_APPWIDGET_ID;
294        }
295    }
296
297    public float getBackgroundAlpha() {
298        return mBackgroundAlpha;
299    }
300
301    public void setBackgroundAlphaMultiplier(float multiplier) {
302        if (Float.compare(mBackgroundAlphaMultiplier, multiplier) != 0) {
303            mBackgroundAlphaMultiplier = multiplier;
304            invalidate();
305        }
306    }
307
308    public float getBackgroundAlphaMultiplier() {
309        return mBackgroundAlphaMultiplier;
310    }
311
312    public void setBackgroundAlpha(float alpha) {
313        if (Float.compare(mBackgroundAlpha, alpha) != 0) {
314            mBackgroundAlpha = alpha;
315            invalidate();
316        }
317    }
318
319    public float getContentAlpha() {
320        return mContentAlpha;
321    }
322
323    public void setContentAlpha(float alpha) {
324        mContentAlpha = alpha;
325        View content = getContent();
326        if (content != null) {
327            content.setAlpha(alpha);
328        }
329    }
330
331    /**
332     * Depending on whether the security is up, the widget size needs to change
333     *
334     * @param height The height of the widget, -1 for full height
335     */
336    private void setWidgetHeight(int height) {
337        boolean needLayout = false;
338        View widget = getContent();
339        if (widget != null) {
340            LayoutParams lp = (LayoutParams) widget.getLayoutParams();
341            if (lp.height != height) {
342                needLayout = true;
343                lp.height = height;
344            }
345        }
346        if (needLayout) {
347            requestLayout();
348        }
349    }
350
351    public void setMaxChallengeTop(int top) {
352        boolean dirty = mMaxChallengeTop != top;
353        mMaxChallengeTop = top;
354        mSmallWidgetHeight = top - getPaddingTop();
355        mSmallFrameHeight = top + getPaddingBottom();
356        if (dirty && mIsSmall) {
357            setWidgetHeight(mSmallWidgetHeight);
358            setFrameHeight(mSmallFrameHeight);
359        } else if (dirty && mWidgetLockedSmall) {
360            setWidgetHeight(mSmallWidgetHeight);
361        }
362    }
363
364    public boolean isSmall() {
365        return mIsSmall;
366    }
367
368    public void adjustFrame(int challengeTop) {
369        int frameHeight = challengeTop + getPaddingBottom();
370        setFrameHeight(frameHeight);
371    }
372
373    public void shrinkWidget(boolean alsoShrinkFrame) {
374        mIsSmall = true;
375        setWidgetHeight(mSmallWidgetHeight);
376
377        if (alsoShrinkFrame) {
378            setFrameHeight(mSmallFrameHeight);
379        }
380    }
381
382    public int getSmallFrameHeight() {
383        return mSmallFrameHeight;
384    }
385
386    public void shrinkWidget() {
387        shrinkWidget(true);
388    }
389
390    public void setWidgetLockedSmall(boolean locked) {
391        if (locked) {
392            setWidgetHeight(mSmallWidgetHeight);
393        }
394        mWidgetLockedSmall = locked;
395    }
396
397    public void resetSize() {
398        mIsSmall = false;
399        if (!mWidgetLockedSmall) {
400            setWidgetHeight(LayoutParams.MATCH_PARENT);
401        }
402        setFrameHeight(getMeasuredHeight());
403    }
404
405    public void setFrameHeight(int height) {
406        mFrameHeight = height;
407        mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(mFrameHeight, getMeasuredHeight()));
408        mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment,getMeasuredWidth() -
409                mFrameStrokeAdjustment, Math.min(getMeasuredHeight(), mFrameHeight) -
410                mFrameStrokeAdjustment);
411        updateGradient();
412        invalidate();
413    }
414
415    public void hideFrame(Object caller) {
416        fadeFrame(caller, false, 0f, KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_OUT_DURATION);
417    }
418
419    public void showFrame(Object caller) {
420        fadeFrame(caller, true, OUTLINE_ALPHA_MULTIPLIER,
421                KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_IN_DURATION);
422    }
423
424    public void fadeFrame(Object caller, boolean takeControl, float alpha, int duration) {
425        if (takeControl) {
426            mBgAlphaController = caller;
427        }
428
429        if (mBgAlphaController != caller && mBgAlphaController != null) {
430            return;
431        }
432
433        if (mFrameFade != null) {
434            mFrameFade.cancel();
435            mFrameFade = null;
436        }
437        PropertyValuesHolder bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", alpha);
438        mFrameFade = ObjectAnimator.ofPropertyValuesHolder(this, bgAlpha);
439        mFrameFade.setDuration(duration);
440        mFrameFade.start();
441    }
442
443    private void updateGradient() {
444        float x0 = mLeftToRight ? 0 : mForegroundRect.width();
445        float x1 = mLeftToRight ? mForegroundRect.width(): 0;
446        mLeftToRightGradient = new LinearGradient(x0, 0f, x1, 0f,
447                mGradientColor, 0, Shader.TileMode.CLAMP);
448        mRightToLeftGradient = new LinearGradient(x1, 0f, x0, 0f,
449                mGradientColor, 0, Shader.TileMode.CLAMP);
450    }
451
452    @Override
453    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
454        super.onSizeChanged(w, h, oldw, oldh);
455
456        if (!mIsSmall) {
457            mFrameHeight = h;
458        }
459
460        // mFrameStrokeAdjustment is a cludge to prevent the overlay from drawing outside the
461        // rounded rect background.
462        mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment,
463                w - mFrameStrokeAdjustment, Math.min(h, mFrameHeight) - mFrameStrokeAdjustment);
464
465        mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(h, mFrameHeight));
466        updateGradient();
467        invalidate();
468    }
469
470    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
471        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
472        performAppWidgetSizeCallbacksIfNecessary();
473    }
474
475    private void performAppWidgetSizeCallbacksIfNecessary() {
476        View content = getContent();
477        if (!(content instanceof AppWidgetHostView)) return;
478
479        if (!KeyguardUpdateMonitor.getInstance(mContext).hasBootCompleted()) {
480            mPerformAppWidgetSizeUpdateOnBootComplete = true;
481            return;
482        }
483
484        // TODO: there's no reason to force the AppWidgetHostView to catch duplicate size calls.
485        // We can do that even more cheaply here. It's not an issue right now since we're in the
486        // system process and hence no binder calls.
487        AppWidgetHostView awhv = (AppWidgetHostView) content;
488        float density = getResources().getDisplayMetrics().density;
489
490        int width = (int) (content.getMeasuredWidth() / density);
491        int height = (int) (content.getMeasuredHeight() / density);
492        awhv.updateAppWidgetSize(null, width, height, width, height, true);
493    }
494
495    void setOverScrollAmount(float r, boolean left) {
496        if (Float.compare(mOverScrollAmount, r) != 0) {
497            mOverScrollAmount = r;
498            mForegroundGradient = left ? mLeftToRightGradient : mRightToLeftGradient;
499            mForegroundAlpha = (int) Math.round((0.5f * r * 255));
500
501            // We bump up the alpha of the outline to hide the fact that the overlay is drawing
502            // over the rounded part of the frame.
503            float bgAlpha = Math.min(OUTLINE_ALPHA_MULTIPLIER + r * (1 - OUTLINE_ALPHA_MULTIPLIER),
504                    1f);
505            setBackgroundAlpha(bgAlpha);
506            invalidate();
507        }
508    }
509
510    public void onActive(boolean isActive) {
511        // hook for subclasses
512    }
513
514    public boolean onUserInteraction(MotionEvent event) {
515        // hook for subclasses
516        return false;
517    }
518
519    public void onBouncerShowing(boolean showing) {
520        // hook for subclasses
521    }
522
523    public void setWorkerHandler(Handler workerHandler) {
524        mWorkerHandler = workerHandler;
525    }
526
527    public Handler getWorkerHandler() {
528        return mWorkerHandler;
529    }
530
531}
532