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 && widget.isHardwareAccelerated()) {
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 View getContent() {
275        return getChildAt(0);
276    }
277
278    public int getContentAppWidgetId() {
279        View content = getContent();
280        if (content instanceof AppWidgetHostView) {
281            return ((AppWidgetHostView) content).getAppWidgetId();
282        } else if (content instanceof KeyguardStatusView) {
283            return ((KeyguardStatusView) content).getAppWidgetId();
284        } else {
285            return AppWidgetManager.INVALID_APPWIDGET_ID;
286        }
287    }
288
289    public float getBackgroundAlpha() {
290        return mBackgroundAlpha;
291    }
292
293    public void setBackgroundAlphaMultiplier(float multiplier) {
294        if (Float.compare(mBackgroundAlphaMultiplier, multiplier) != 0) {
295            mBackgroundAlphaMultiplier = multiplier;
296            invalidate();
297        }
298    }
299
300    public float getBackgroundAlphaMultiplier() {
301        return mBackgroundAlphaMultiplier;
302    }
303
304    public void setBackgroundAlpha(float alpha) {
305        if (Float.compare(mBackgroundAlpha, alpha) != 0) {
306            mBackgroundAlpha = alpha;
307            invalidate();
308        }
309    }
310
311    public float getContentAlpha() {
312        return mContentAlpha;
313    }
314
315    public void setContentAlpha(float alpha) {
316        mContentAlpha = alpha;
317        View content = getContent();
318        if (content != null) {
319            content.setAlpha(alpha);
320        }
321    }
322
323    /**
324     * Depending on whether the security is up, the widget size needs to change
325     *
326     * @param height The height of the widget, -1 for full height
327     */
328    private void setWidgetHeight(int height) {
329        boolean needLayout = false;
330        View widget = getContent();
331        if (widget != null) {
332            LayoutParams lp = (LayoutParams) widget.getLayoutParams();
333            if (lp.height != height) {
334                needLayout = true;
335                lp.height = height;
336            }
337        }
338        if (needLayout) {
339            requestLayout();
340        }
341    }
342
343    public void setMaxChallengeTop(int top) {
344        boolean dirty = mMaxChallengeTop != top;
345        mMaxChallengeTop = top;
346        mSmallWidgetHeight = top - getPaddingTop();
347        mSmallFrameHeight = top + getPaddingBottom();
348        if (dirty && mIsSmall) {
349            setWidgetHeight(mSmallWidgetHeight);
350            setFrameHeight(mSmallFrameHeight);
351        } else if (dirty && mWidgetLockedSmall) {
352            setWidgetHeight(mSmallWidgetHeight);
353        }
354    }
355
356    public boolean isSmall() {
357        return mIsSmall;
358    }
359
360    public void adjustFrame(int challengeTop) {
361        int frameHeight = challengeTop + getPaddingBottom();
362        setFrameHeight(frameHeight);
363    }
364
365    public void shrinkWidget(boolean alsoShrinkFrame) {
366        mIsSmall = true;
367        setWidgetHeight(mSmallWidgetHeight);
368
369        if (alsoShrinkFrame) {
370            setFrameHeight(mSmallFrameHeight);
371        }
372    }
373
374    public int getSmallFrameHeight() {
375        return mSmallFrameHeight;
376    }
377
378    public void setWidgetLockedSmall(boolean locked) {
379        if (locked) {
380            setWidgetHeight(mSmallWidgetHeight);
381        }
382        mWidgetLockedSmall = locked;
383    }
384
385    public void resetSize() {
386        mIsSmall = false;
387        if (!mWidgetLockedSmall) {
388            setWidgetHeight(LayoutParams.MATCH_PARENT);
389        }
390        setFrameHeight(getMeasuredHeight());
391    }
392
393    public void setFrameHeight(int height) {
394        mFrameHeight = height;
395        mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(mFrameHeight, getMeasuredHeight()));
396        mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment,getMeasuredWidth() -
397                mFrameStrokeAdjustment, Math.min(getMeasuredHeight(), mFrameHeight) -
398                mFrameStrokeAdjustment);
399        updateGradient();
400        invalidate();
401    }
402
403    public void hideFrame(Object caller) {
404        fadeFrame(caller, false, 0f, KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_OUT_DURATION);
405    }
406
407    public void showFrame(Object caller) {
408        fadeFrame(caller, true, OUTLINE_ALPHA_MULTIPLIER,
409                KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_IN_DURATION);
410    }
411
412    public void fadeFrame(Object caller, boolean takeControl, float alpha, int duration) {
413        if (takeControl) {
414            mBgAlphaController = caller;
415        }
416
417        if (mBgAlphaController != caller && mBgAlphaController != null) {
418            return;
419        }
420
421        if (mFrameFade != null) {
422            mFrameFade.cancel();
423            mFrameFade = null;
424        }
425        PropertyValuesHolder bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", alpha);
426        mFrameFade = ObjectAnimator.ofPropertyValuesHolder(this, bgAlpha);
427        mFrameFade.setDuration(duration);
428        mFrameFade.start();
429    }
430
431    private void updateGradient() {
432        float x0 = mLeftToRight ? 0 : mForegroundRect.width();
433        float x1 = mLeftToRight ? mForegroundRect.width(): 0;
434        mLeftToRightGradient = new LinearGradient(x0, 0f, x1, 0f,
435                mGradientColor, 0, Shader.TileMode.CLAMP);
436        mRightToLeftGradient = new LinearGradient(x1, 0f, x0, 0f,
437                mGradientColor, 0, Shader.TileMode.CLAMP);
438    }
439
440    @Override
441    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
442        super.onSizeChanged(w, h, oldw, oldh);
443
444        if (!mIsSmall) {
445            mFrameHeight = h;
446        }
447
448        // mFrameStrokeAdjustment is a cludge to prevent the overlay from drawing outside the
449        // rounded rect background.
450        mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment,
451                w - mFrameStrokeAdjustment, Math.min(h, mFrameHeight) - mFrameStrokeAdjustment);
452
453        mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(h, mFrameHeight));
454        updateGradient();
455        invalidate();
456    }
457
458    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
459        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
460        performAppWidgetSizeCallbacksIfNecessary();
461    }
462
463    private void performAppWidgetSizeCallbacksIfNecessary() {
464        View content = getContent();
465        if (!(content instanceof AppWidgetHostView)) return;
466
467        if (!KeyguardUpdateMonitor.getInstance(mContext).hasBootCompleted()) {
468            mPerformAppWidgetSizeUpdateOnBootComplete = true;
469            return;
470        }
471
472        // TODO: there's no reason to force the AppWidgetHostView to catch duplicate size calls.
473        // We can do that even more cheaply here. It's not an issue right now since we're in the
474        // system process and hence no binder calls.
475        AppWidgetHostView awhv = (AppWidgetHostView) content;
476        float density = getResources().getDisplayMetrics().density;
477
478        int width = (int) (content.getMeasuredWidth() / density);
479        int height = (int) (content.getMeasuredHeight() / density);
480        awhv.updateAppWidgetSize(null, width, height, width, height, true);
481    }
482
483    void setOverScrollAmount(float r, boolean left) {
484        if (Float.compare(mOverScrollAmount, r) != 0) {
485            mOverScrollAmount = r;
486            mForegroundGradient = left ? mLeftToRightGradient : mRightToLeftGradient;
487            mForegroundAlpha = (int) Math.round((0.5f * r * 255));
488
489            // We bump up the alpha of the outline to hide the fact that the overlay is drawing
490            // over the rounded part of the frame.
491            float bgAlpha = Math.min(OUTLINE_ALPHA_MULTIPLIER + r * (1 - OUTLINE_ALPHA_MULTIPLIER),
492                    1f);
493            setBackgroundAlpha(bgAlpha);
494            invalidate();
495        }
496    }
497
498    public void onActive(boolean isActive) {
499        // hook for subclasses
500    }
501
502    public boolean onUserInteraction(MotionEvent event) {
503        // hook for subclasses
504        return false;
505    }
506
507    public void onBouncerShowing(boolean showing) {
508        // hook for subclasses
509    }
510
511    public void setWorkerHandler(Handler workerHandler) {
512        mWorkerHandler = workerHandler;
513    }
514
515    public Handler getWorkerHandler() {
516        return mWorkerHandler;
517    }
518
519}
520