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