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.content.Context; 20 import android.content.pm.PackageManager.NameNotFoundException; 21 import android.graphics.Color; 22 import android.graphics.Point; 23 import android.graphics.Rect; 24 import android.os.Handler; 25 import android.os.SystemClock; 26 import android.util.Log; 27 import android.view.Gravity; 28 import android.view.LayoutInflater; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.WindowManager; 33 import android.widget.FrameLayout; 34 import android.widget.ImageView; 35 import android.widget.ImageView.ScaleType; 36 37 import com.android.keyguard.KeyguardActivityLauncher.CameraWidgetInfo; 38 39 public class CameraWidgetFrame extends KeyguardWidgetFrame implements View.OnClickListener { 40 private static final String TAG = CameraWidgetFrame.class.getSimpleName(); 41 private static final boolean DEBUG = KeyguardHostView.DEBUG; 42 private static final int WIDGET_ANIMATION_DURATION = 250; // ms 43 private static final int WIDGET_WAIT_DURATION = 400; // ms 44 private static final int RECOVERY_DELAY = 1000; // ms 45 46 interface Callbacks { 47 void onLaunchingCamera(); 48 void onCameraLaunchedSuccessfully(); 49 void onCameraLaunchedUnsuccessfully(); 50 } 51 52 private final Handler mHandler = new Handler(); 53 private final KeyguardActivityLauncher mActivityLauncher; 54 private final Callbacks mCallbacks; 55 private final CameraWidgetInfo mWidgetInfo; 56 private final WindowManager mWindowManager; 57 private final Point mRenderedSize = new Point(); 58 private final int[] mTmpLoc = new int[2]; 59 60 private long mLaunchCameraStart; 61 private boolean mActive; 62 private boolean mTransitioning; 63 private boolean mDown; 64 65 private final Rect mInsets = new Rect(); 66 67 private FixedSizeFrameLayout mPreview; 68 private View mFullscreenPreview; 69 private View mFakeNavBar; 70 private boolean mUseFastTransition; 71 72 private final Runnable mTransitionToCameraRunnable = new Runnable() { 73 @Override 74 public void run() { 75 transitionToCamera(); 76 }}; 77 78 private final Runnable mTransitionToCameraEndAction = new Runnable() { 79 @Override 80 public void run() { 81 if (!mTransitioning) 82 return; 83 Handler worker = getWorkerHandler() != null ? getWorkerHandler() : mHandler; 84 mLaunchCameraStart = SystemClock.uptimeMillis(); 85 if (DEBUG) Log.d(TAG, "Launching camera at " + mLaunchCameraStart); 86 mActivityLauncher.launchCamera(worker, mSecureCameraActivityStartedRunnable); 87 }}; 88 89 private final Runnable mPostTransitionToCameraEndAction = new Runnable() { 90 @Override 91 public void run() { 92 mHandler.post(mTransitionToCameraEndAction); 93 }}; 94 95 private final Runnable mRecoverRunnable = new Runnable() { 96 @Override 97 public void run() { 98 recover(); 99 }}; 100 101 private final Runnable mRenderRunnable = new Runnable() { 102 @Override 103 public void run() { 104 render(); 105 }}; 106 107 private final Runnable mSecureCameraActivityStartedRunnable = new Runnable() { 108 @Override 109 public void run() { 110 onSecureCameraActivityStarted(); 111 } 112 }; 113 114 private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() { 115 private boolean mShowing; 116 void onKeyguardVisibilityChanged(boolean showing) { 117 if (mShowing == showing) 118 return; 119 mShowing = showing; 120 CameraWidgetFrame.this.onKeyguardVisibilityChanged(mShowing); 121 }; 122 }; 123 124 private static final class FixedSizeFrameLayout extends FrameLayout { 125 int width; 126 int height; 127 128 FixedSizeFrameLayout(Context context) { 129 super(context); 130 } 131 132 @Override 133 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 134 measureChildren( 135 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 136 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 137 setMeasuredDimension(width, height); 138 } 139 } 140 141 private CameraWidgetFrame(Context context, Callbacks callbacks, 142 KeyguardActivityLauncher activityLauncher, 143 CameraWidgetInfo widgetInfo, View previewWidget) { 144 super(context); 145 mCallbacks = callbacks; 146 mActivityLauncher = activityLauncher; 147 mWidgetInfo = widgetInfo; 148 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 149 KeyguardUpdateMonitor.getInstance(context).registerCallback(mCallback); 150 151 mPreview = new FixedSizeFrameLayout(context); 152 mPreview.addView(previewWidget); 153 addView(mPreview); 154 155 View clickBlocker = new View(context); 156 clickBlocker.setBackgroundColor(Color.TRANSPARENT); 157 clickBlocker.setOnClickListener(this); 158 addView(clickBlocker); 159 160 setContentDescription(context.getString(R.string.keyguard_accessibility_camera)); 161 if (DEBUG) Log.d(TAG, "new CameraWidgetFrame instance " + instanceId()); 162 } 163 164 public static CameraWidgetFrame create(Context context, Callbacks callbacks, 165 KeyguardActivityLauncher launcher) { 166 if (context == null || callbacks == null || launcher == null) 167 return null; 168 169 CameraWidgetInfo widgetInfo = launcher.getCameraWidgetInfo(); 170 if (widgetInfo == null) 171 return null; 172 View previewWidget = getPreviewWidget(context, widgetInfo); 173 if (previewWidget == null) 174 return null; 175 176 return new CameraWidgetFrame(context, callbacks, launcher, widgetInfo, previewWidget); 177 } 178 179 private static View getPreviewWidget(Context context, CameraWidgetInfo widgetInfo) { 180 return widgetInfo.layoutId > 0 ? 181 inflateWidgetView(context, widgetInfo) : 182 inflateGenericWidgetView(context); 183 } 184 185 private static View inflateWidgetView(Context context, CameraWidgetInfo widgetInfo) { 186 if (DEBUG) Log.d(TAG, "inflateWidgetView: " + widgetInfo.contextPackage); 187 View widgetView = null; 188 Exception exception = null; 189 try { 190 Context cameraContext = context.createPackageContext( 191 widgetInfo.contextPackage, Context.CONTEXT_RESTRICTED); 192 LayoutInflater cameraInflater = (LayoutInflater) 193 cameraContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 194 cameraInflater = cameraInflater.cloneInContext(cameraContext); 195 widgetView = cameraInflater.inflate(widgetInfo.layoutId, null, false); 196 } catch (NameNotFoundException e) { 197 exception = e; 198 } catch (RuntimeException e) { 199 exception = e; 200 } 201 if (exception != null) { 202 Log.w(TAG, "Error creating camera widget view", exception); 203 } 204 return widgetView; 205 } 206 207 private static View inflateGenericWidgetView(Context context) { 208 if (DEBUG) Log.d(TAG, "inflateGenericWidgetView"); 209 ImageView iv = new ImageView(context); 210 iv.setImageResource(R.drawable.ic_lockscreen_camera); 211 iv.setScaleType(ScaleType.CENTER); 212 iv.setBackgroundColor(Color.argb(127, 0, 0, 0)); 213 return iv; 214 } 215 216 private void render() { 217 final View root = getRootView(); 218 final int width = root.getWidth() - mInsets.right; // leave room 219 final int height = root.getHeight() - mInsets.bottom; // for bars 220 if (mRenderedSize.x == width && mRenderedSize.y == height) { 221 if (DEBUG) Log.d(TAG, String.format("Already rendered at size=%sx%s %d%%", 222 width, height, (int)(100*mPreview.getScaleX()))); 223 return; 224 } 225 if (width == 0 || height == 0) { 226 return; 227 } 228 229 mPreview.width = width; 230 mPreview.height = height; 231 mPreview.requestLayout(); 232 233 final int thisWidth = getWidth() - getPaddingLeft() - getPaddingRight(); 234 final int thisHeight = getHeight() - getPaddingTop() - getPaddingBottom(); 235 236 final float pvScaleX = (float) thisWidth / width; 237 final float pvScaleY = (float) thisHeight / height; 238 final float pvScale = Math.min(pvScaleX, pvScaleY); 239 240 final int pvWidth = (int) (pvScale * width); 241 final int pvHeight = (int) (pvScale * height); 242 243 final float pvTransX = pvWidth < thisWidth ? (thisWidth - pvWidth) / 2 : 0; 244 final float pvTransY = pvHeight < thisHeight ? (thisHeight - pvHeight) / 2 : 0; 245 246 final boolean isRtl = mPreview.getLayoutDirection() == LAYOUT_DIRECTION_RTL; 247 mPreview.setPivotX(isRtl ? mPreview.width : 0); 248 mPreview.setPivotY(0); 249 mPreview.setScaleX(pvScale); 250 mPreview.setScaleY(pvScale); 251 mPreview.setTranslationX((isRtl ? -1 : 1) * pvTransX); 252 mPreview.setTranslationY(pvTransY); 253 254 mRenderedSize.set(width, height); 255 if (DEBUG) Log.d(TAG, String.format("Rendered camera widget size=%sx%s %d%% instance=%s", 256 width, height, (int)(100*mPreview.getScaleX()), instanceId())); 257 } 258 259 private void transitionToCamera() { 260 if (mTransitioning || mDown) return; 261 262 mTransitioning = true; 263 264 enableWindowExitAnimation(false); 265 266 final int navHeight = mInsets.bottom; 267 final int navWidth = mInsets.right; 268 269 mPreview.getLocationInWindow(mTmpLoc); 270 final float pvHeight = mPreview.getHeight() * mPreview.getScaleY(); 271 final float pvCenter = mTmpLoc[1] + pvHeight / 2f; 272 273 final ViewGroup root = (ViewGroup) getRootView(); 274 275 if (DEBUG) { 276 Log.d(TAG, "root = " + root.getLeft() + "," + root.getTop() + " " 277 + root.getWidth() + "x" + root.getHeight()); 278 } 279 280 if (mFullscreenPreview == null) { 281 mFullscreenPreview = getPreviewWidget(mContext, mWidgetInfo); 282 mFullscreenPreview.setClickable(false); 283 root.addView(mFullscreenPreview, new FrameLayout.LayoutParams( 284 root.getWidth() - navWidth, 285 root.getHeight() - navHeight)); 286 } 287 288 final float fsHeight = root.getHeight() - navHeight; 289 final float fsCenter = root.getTop() + fsHeight / 2; 290 291 final float fsScaleY = mPreview.getScaleY(); 292 final float fsTransY = pvCenter - fsCenter; 293 final float fsScaleX = fsScaleY; 294 295 mPreview.setVisibility(View.GONE); 296 mFullscreenPreview.setVisibility(View.VISIBLE); 297 mFullscreenPreview.setTranslationY(fsTransY); 298 mFullscreenPreview.setScaleX(fsScaleX); 299 mFullscreenPreview.setScaleY(fsScaleY); 300 mFullscreenPreview 301 .animate() 302 .scaleX(1) 303 .scaleY(1) 304 .translationX(0) 305 .translationY(0) 306 .setDuration(WIDGET_ANIMATION_DURATION) 307 .withEndAction(mPostTransitionToCameraEndAction) 308 .start(); 309 310 if (navHeight > 0 || navWidth > 0) { 311 final boolean atBottom = navHeight > 0; 312 if (mFakeNavBar == null) { 313 mFakeNavBar = new View(mContext); 314 mFakeNavBar.setBackgroundColor(Color.BLACK); 315 root.addView(mFakeNavBar, new FrameLayout.LayoutParams( 316 atBottom ? FrameLayout.LayoutParams.MATCH_PARENT 317 : navWidth, 318 atBottom ? navHeight 319 : FrameLayout.LayoutParams.MATCH_PARENT, 320 atBottom ? Gravity.BOTTOM|Gravity.FILL_HORIZONTAL 321 : Gravity.RIGHT|Gravity.FILL_VERTICAL)); 322 mFakeNavBar.setPivotY(navHeight); 323 mFakeNavBar.setPivotX(navWidth); 324 } 325 mFakeNavBar.setAlpha(0f); 326 if (atBottom) { 327 mFakeNavBar.setScaleY(0.5f); 328 } else { 329 mFakeNavBar.setScaleX(0.5f); 330 } 331 mFakeNavBar.setVisibility(View.VISIBLE); 332 mFakeNavBar.animate() 333 .alpha(1f) 334 .scaleY(1f) 335 .scaleY(1f) 336 .setDuration(WIDGET_ANIMATION_DURATION) 337 .start(); 338 } 339 mCallbacks.onLaunchingCamera(); 340 } 341 342 private void recover() { 343 if (DEBUG) Log.d(TAG, "recovering at " + SystemClock.uptimeMillis()); 344 mCallbacks.onCameraLaunchedUnsuccessfully(); 345 reset(); 346 } 347 348 @Override 349 public void setOnLongClickListener(OnLongClickListener l) { 350 // ignore 351 } 352 353 @Override 354 public void onClick(View v) { 355 if (DEBUG) Log.d(TAG, "clicked"); 356 if (mTransitioning) return; 357 if (mActive) { 358 cancelTransitionToCamera(); 359 transitionToCamera(); 360 } 361 } 362 363 @Override 364 protected void onDetachedFromWindow() { 365 if (DEBUG) Log.d(TAG, "onDetachedFromWindow: instance " + instanceId() 366 + " at " + SystemClock.uptimeMillis()); 367 super.onDetachedFromWindow(); 368 KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mCallback); 369 cancelTransitionToCamera(); 370 mHandler.removeCallbacks(mRecoverRunnable); 371 } 372 373 @Override 374 public void onActive(boolean isActive) { 375 mActive = isActive; 376 if (mActive) { 377 rescheduleTransitionToCamera(); 378 } else { 379 reset(); 380 } 381 } 382 383 @Override 384 public boolean onUserInteraction(MotionEvent event) { 385 if (mTransitioning) { 386 if (DEBUG) Log.d(TAG, "onUserInteraction eaten: mTransitioning"); 387 return true; 388 } 389 390 getLocationOnScreen(mTmpLoc); 391 int rawBottom = mTmpLoc[1] + getHeight(); 392 if (event.getRawY() > rawBottom) { 393 if (DEBUG) Log.d(TAG, "onUserInteraction eaten: below widget"); 394 return true; 395 } 396 397 int action = event.getAction(); 398 mDown = action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE; 399 if (mActive) { 400 rescheduleTransitionToCamera(); 401 } 402 if (DEBUG) Log.d(TAG, "onUserInteraction observed, not eaten"); 403 return false; 404 } 405 406 @Override 407 protected void onFocusLost() { 408 if (DEBUG) Log.d(TAG, "onFocusLost at " + SystemClock.uptimeMillis()); 409 cancelTransitionToCamera(); 410 super.onFocusLost(); 411 } 412 413 public void onScreenTurnedOff() { 414 if (DEBUG) Log.d(TAG, "onScreenTurnedOff"); 415 reset(); 416 } 417 418 private void rescheduleTransitionToCamera() { 419 if (DEBUG) Log.d(TAG, "rescheduleTransitionToCamera at " + SystemClock.uptimeMillis()); 420 mHandler.removeCallbacks(mTransitionToCameraRunnable); 421 final long duration = mUseFastTransition ? 0 : WIDGET_WAIT_DURATION; 422 mHandler.postDelayed(mTransitionToCameraRunnable, duration); 423 } 424 425 private void cancelTransitionToCamera() { 426 if (DEBUG) Log.d(TAG, "cancelTransitionToCamera at " + SystemClock.uptimeMillis()); 427 mHandler.removeCallbacks(mTransitionToCameraRunnable); 428 } 429 430 private void onCameraLaunched() { 431 mCallbacks.onCameraLaunchedSuccessfully(); 432 reset(); 433 } 434 435 private void reset() { 436 if (DEBUG) Log.d(TAG, "reset at " + SystemClock.uptimeMillis()); 437 mLaunchCameraStart = 0; 438 mTransitioning = false; 439 mDown = false; 440 cancelTransitionToCamera(); 441 mHandler.removeCallbacks(mRecoverRunnable); 442 mPreview.setVisibility(View.VISIBLE); 443 if (mFullscreenPreview != null) { 444 mFullscreenPreview.animate().cancel(); 445 mFullscreenPreview.setVisibility(View.GONE); 446 } 447 if (mFakeNavBar != null) { 448 mFakeNavBar.animate().cancel(); 449 mFakeNavBar.setVisibility(View.GONE); 450 } 451 enableWindowExitAnimation(true); 452 } 453 454 @Override 455 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 456 if (DEBUG) Log.d(TAG, String.format("onSizeChanged new=%sx%s old=%sx%s at %s", 457 w, h, oldw, oldh, SystemClock.uptimeMillis())); 458 if ((w != oldw && oldw > 0) || (h != oldh && oldh > 0)) { 459 // we can't trust the old geometry anymore; force a re-render 460 mRenderedSize.x = mRenderedSize.y = -1; 461 } 462 mHandler.post(mRenderRunnable); 463 super.onSizeChanged(w, h, oldw, oldh); 464 } 465 466 @Override 467 public void onBouncerShowing(boolean showing) { 468 if (showing) { 469 mTransitioning = false; 470 mHandler.post(mRecoverRunnable); 471 } 472 } 473 474 private void enableWindowExitAnimation(boolean isEnabled) { 475 View root = getRootView(); 476 ViewGroup.LayoutParams lp = root.getLayoutParams(); 477 if (!(lp instanceof WindowManager.LayoutParams)) 478 return; 479 WindowManager.LayoutParams wlp = (WindowManager.LayoutParams) lp; 480 int newWindowAnimations = isEnabled ? R.style.Animation_LockScreen : 0; 481 if (newWindowAnimations != wlp.windowAnimations) { 482 if (DEBUG) Log.d(TAG, "setting windowAnimations to: " + newWindowAnimations 483 + " at " + SystemClock.uptimeMillis()); 484 wlp.windowAnimations = newWindowAnimations; 485 mWindowManager.updateViewLayout(root, wlp); 486 } 487 } 488 489 private void onKeyguardVisibilityChanged(boolean showing) { 490 if (DEBUG) Log.d(TAG, "onKeyguardVisibilityChanged " + showing 491 + " at " + SystemClock.uptimeMillis()); 492 if (mTransitioning && !showing) { 493 mTransitioning = false; 494 mHandler.removeCallbacks(mRecoverRunnable); 495 if (mLaunchCameraStart > 0) { 496 long launchTime = SystemClock.uptimeMillis() - mLaunchCameraStart; 497 if (DEBUG) Log.d(TAG, String.format("Camera took %sms to launch", launchTime)); 498 mLaunchCameraStart = 0; 499 onCameraLaunched(); 500 } 501 } 502 } 503 504 private void onSecureCameraActivityStarted() { 505 if (DEBUG) Log.d(TAG, "onSecureCameraActivityStarted at " + SystemClock.uptimeMillis()); 506 mHandler.postDelayed(mRecoverRunnable, RECOVERY_DELAY); 507 } 508 509 private String instanceId() { 510 return Integer.toHexString(hashCode()); 511 } 512 513 public void setInsets(Rect insets) { 514 if (DEBUG) Log.d(TAG, "setInsets: " + insets); 515 mInsets.set(insets); 516 } 517 518 public void setUseFastTransition(boolean useFastTransition) { 519 mUseFastTransition = useFastTransition; 520 } 521 } 522