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