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 
17 package com.android.keyguard;
18 
19 import android.animation.Animator;
20 import android.animation.ObjectAnimator;
21 import android.animation.PropertyValuesHolder;
22 import android.appwidget.AppWidgetHostView;
23 import android.appwidget.AppWidgetManager;
24 import android.content.Context;
25 import android.content.res.Resources;
26 import android.graphics.Canvas;
27 import android.graphics.LinearGradient;
28 import android.graphics.Paint;
29 import android.graphics.PorterDuff;
30 import android.graphics.PorterDuffXfermode;
31 import android.graphics.Rect;
32 import android.graphics.Shader;
33 import android.graphics.drawable.Drawable;
34 import android.os.Handler;
35 import android.util.AttributeSet;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.widget.FrameLayout;
39 
40 public 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 
KeyguardWidgetFrame(Context context)90     public KeyguardWidgetFrame(Context context) {
91         this(context, null, 0);
92     }
93 
KeyguardWidgetFrame(Context context, AttributeSet attrs)94     public KeyguardWidgetFrame(Context context, AttributeSet attrs) {
95         this(context, attrs, 0);
96     }
97 
KeyguardWidgetFrame(Context context, AttributeSet attrs, int defStyle)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
onDetachedFromWindow()122     protected void onDetachedFromWindow() {
123         super.onDetachedFromWindow();
124         cancelLongPress();
125         KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallbacks);
126 
127     }
128 
129     @Override
onAttachedToWindow()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 
setIsHoveringOverDeleteDropTarget(boolean isHovering)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
onInterceptTouchEvent(MotionEvent ev)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
onTouchEvent(MotionEvent ev)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
requestDisallowInterceptTouchEvent(boolean disallowIntercept)202     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
203         super.requestDisallowInterceptTouchEvent(disallowIntercept);
204         cancelLongPress();
205     }
206 
207     @Override
cancelLongPress()208     public void cancelLongPress() {
209         super.cancelLongPress();
210         mLongPressHelper.cancelLongPress();
211     }
212 
213 
drawGradientOverlay(Canvas c)214     private void drawGradientOverlay(Canvas c) {
215         mGradientPaint.setShader(mForegroundGradient);
216         mGradientPaint.setAlpha(mForegroundAlpha);
217         c.drawRect(mForegroundRect, mGradientPaint);
218     }
219 
drawHoveringOverDeleteOverlay(Canvas c)220     private void drawHoveringOverDeleteOverlay(Canvas c) {
221         if (mIsHoveringOverDeleteDropTarget) {
222             c.drawColor(HOVER_OVER_DELETE_DROP_TARGET_OVERLAY_COLOR);
223         }
224     }
225 
drawBg(Canvas canvas)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
dispatchDraw(Canvas canvas)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      */
enableHardwareLayersForContent()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      */
disableHardwareLayersForContent()267     public void disableHardwareLayersForContent() {
268         View widget = getContent();
269         if (widget != null) {
270             widget.setLayerType(LAYER_TYPE_NONE, null);
271         }
272     }
273 
getContent()274     public View getContent() {
275         return getChildAt(0);
276     }
277 
getContentAppWidgetId()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 
getBackgroundAlpha()289     public float getBackgroundAlpha() {
290         return mBackgroundAlpha;
291     }
292 
setBackgroundAlphaMultiplier(float multiplier)293     public void setBackgroundAlphaMultiplier(float multiplier) {
294         if (Float.compare(mBackgroundAlphaMultiplier, multiplier) != 0) {
295             mBackgroundAlphaMultiplier = multiplier;
296             invalidate();
297         }
298     }
299 
getBackgroundAlphaMultiplier()300     public float getBackgroundAlphaMultiplier() {
301         return mBackgroundAlphaMultiplier;
302     }
303 
setBackgroundAlpha(float alpha)304     public void setBackgroundAlpha(float alpha) {
305         if (Float.compare(mBackgroundAlpha, alpha) != 0) {
306             mBackgroundAlpha = alpha;
307             invalidate();
308         }
309     }
310 
getContentAlpha()311     public float getContentAlpha() {
312         return mContentAlpha;
313     }
314 
setContentAlpha(float alpha)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      */
setWidgetHeight(int height)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 
setMaxChallengeTop(int top)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 
isSmall()356     public boolean isSmall() {
357         return mIsSmall;
358     }
359 
adjustFrame(int challengeTop)360     public void adjustFrame(int challengeTop) {
361         int frameHeight = challengeTop + getPaddingBottom();
362         setFrameHeight(frameHeight);
363     }
364 
shrinkWidget(boolean alsoShrinkFrame)365     public void shrinkWidget(boolean alsoShrinkFrame) {
366         mIsSmall = true;
367         setWidgetHeight(mSmallWidgetHeight);
368 
369         if (alsoShrinkFrame) {
370             setFrameHeight(mSmallFrameHeight);
371         }
372     }
373 
getSmallFrameHeight()374     public int getSmallFrameHeight() {
375         return mSmallFrameHeight;
376     }
377 
setWidgetLockedSmall(boolean locked)378     public void setWidgetLockedSmall(boolean locked) {
379         if (locked) {
380             setWidgetHeight(mSmallWidgetHeight);
381         }
382         mWidgetLockedSmall = locked;
383     }
384 
resetSize()385     public void resetSize() {
386         mIsSmall = false;
387         if (!mWidgetLockedSmall) {
388             setWidgetHeight(LayoutParams.MATCH_PARENT);
389         }
390         setFrameHeight(getMeasuredHeight());
391     }
392 
setFrameHeight(int height)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 
hideFrame(Object caller)403     public void hideFrame(Object caller) {
404         fadeFrame(caller, false, 0f, KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_OUT_DURATION);
405     }
406 
showFrame(Object caller)407     public void showFrame(Object caller) {
408         fadeFrame(caller, true, OUTLINE_ALPHA_MULTIPLIER,
409                 KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_IN_DURATION);
410     }
411 
fadeFrame(Object caller, boolean takeControl, float alpha, int duration)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 
updateGradient()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
onSizeChanged(int w, int h, int oldw, int oldh)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 
onMeasure(int widthMeasureSpec, int heightMeasureSpec)458     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
459         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
460         performAppWidgetSizeCallbacksIfNecessary();
461     }
462 
performAppWidgetSizeCallbacksIfNecessary()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 
setOverScrollAmount(float r, boolean left)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 
onActive(boolean isActive)498     public void onActive(boolean isActive) {
499         // hook for subclasses
500     }
501 
onUserInteraction(MotionEvent event)502     public boolean onUserInteraction(MotionEvent event) {
503         // hook for subclasses
504         return false;
505     }
506 
onBouncerShowing(boolean showing)507     public void onBouncerShowing(boolean showing) {
508         // hook for subclasses
509     }
510 
setWorkerHandler(Handler workerHandler)511     public void setWorkerHandler(Handler workerHandler) {
512         mWorkerHandler = workerHandler;
513     }
514 
getWorkerHandler()515     public Handler getWorkerHandler() {
516         return mWorkerHandler;
517     }
518 
519 }
520