CameraWidgetFrame.java revision 26b7bf6e3cf6be009cd4baf59e083bce37633a4a
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.content.Context;
20import android.content.pm.PackageManager.NameNotFoundException;
21import android.graphics.Color;
22import android.graphics.Paint;
23import android.graphics.Point;
24import android.graphics.Rect;
25import android.os.Handler;
26import android.os.SystemClock;
27import android.util.Log;
28import android.view.Gravity;
29import android.view.LayoutInflater;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.WindowManager;
34import android.widget.FrameLayout;
35import android.widget.ImageView;
36import android.widget.ImageView.ScaleType;
37
38import com.android.keyguard.KeyguardActivityLauncher.CameraWidgetInfo;
39
40public class CameraWidgetFrame extends KeyguardWidgetFrame implements View.OnClickListener {
41    private static final String TAG = CameraWidgetFrame.class.getSimpleName();
42    private static final boolean DEBUG = KeyguardHostView.DEBUG;
43    private static final int WIDGET_ANIMATION_DURATION = 250; // ms
44    private static final int WIDGET_WAIT_DURATION = 650; // ms
45    private static final int RECOVERY_DELAY = 1000; // ms
46
47    interface Callbacks {
48        void onLaunchingCamera();
49        void onCameraLaunchedSuccessfully();
50        void onCameraLaunchedUnsuccessfully();
51    }
52
53    private final Handler mHandler = new Handler();
54    private final KeyguardActivityLauncher mActivityLauncher;
55    private final Callbacks mCallbacks;
56    private final CameraWidgetInfo mWidgetInfo;
57    private final WindowManager mWindowManager;
58    private final Point mRenderedSize = new Point();
59    private final int[] mTmpLoc = new int[2];
60
61    private long mLaunchCameraStart;
62    private boolean mActive;
63    private boolean mTransitioning;
64    private boolean mDown;
65
66    private final Rect mInsets = new Rect();
67
68    private FixedSizeFrameLayout mPreview;
69    private View mFullscreenPreview;
70    private View mFakeNavBar;
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        mPreview.setPivotX(0);
247        mPreview.setPivotY(0);
248        mPreview.setScaleX(pvScale);
249        mPreview.setScaleY(pvScale);
250        mPreview.setTranslationX(pvTransX);
251        mPreview.setTranslationY(pvTransY);
252
253        mRenderedSize.set(width, height);
254        if (DEBUG) Log.d(TAG, String.format("Rendered camera widget size=%sx%s %d%% instance=%s",
255                width, height, (int)(100*mPreview.getScaleX()), instanceId()));
256    }
257
258    private void transitionToCamera() {
259        if (mTransitioning || mDown) return;
260
261        mTransitioning = true;
262
263        enableWindowExitAnimation(false);
264
265        final int navHeight = mInsets.bottom;
266        final int navWidth = mInsets.right;
267
268        mPreview.getLocationInWindow(mTmpLoc);
269        final float pvHeight = mPreview.getHeight() * mPreview.getScaleY();
270        final float pvCenter = mTmpLoc[1] + pvHeight / 2f;
271
272        final ViewGroup root = (ViewGroup) getRootView();
273
274        if (DEBUG) {
275            Log.d(TAG, "root = " + root.getLeft() + "," + root.getTop() + " "
276                    + root.getWidth() + "x" + root.getHeight());
277        }
278
279        if (mFullscreenPreview == null) {
280            mFullscreenPreview = getPreviewWidget(mContext, mWidgetInfo);
281            mFullscreenPreview.setClickable(false);
282            root.addView(mFullscreenPreview, new FrameLayout.LayoutParams(
283                        root.getWidth() - navWidth,
284                        root.getHeight() - navHeight));
285        }
286
287        final float fsHeight = root.getHeight() - navHeight;
288        final float fsCenter = root.getTop() + fsHeight / 2;
289
290        final float fsScaleY = mPreview.getScaleY();
291        final float fsTransY = pvCenter - fsCenter;
292        final float fsScaleX = fsScaleY;
293
294        mPreview.setVisibility(View.GONE);
295        mFullscreenPreview.setVisibility(View.VISIBLE);
296        mFullscreenPreview.setTranslationY(fsTransY);
297        mFullscreenPreview.setScaleX(fsScaleX);
298        mFullscreenPreview.setScaleY(fsScaleY);
299        mFullscreenPreview
300            .animate()
301            .scaleX(1)
302            .scaleY(1)
303            .translationX(0)
304            .translationY(0)
305            .setDuration(WIDGET_ANIMATION_DURATION)
306            .withEndAction(mPostTransitionToCameraEndAction)
307            .start();
308
309        if (navHeight > 0 || navWidth > 0) {
310            final boolean atBottom = navHeight > 0;
311            if (mFakeNavBar == null) {
312                mFakeNavBar = new View(mContext);
313                mFakeNavBar.setBackgroundColor(Color.BLACK);
314                root.addView(mFakeNavBar, new FrameLayout.LayoutParams(
315                            atBottom ? FrameLayout.LayoutParams.MATCH_PARENT
316                                     : navWidth,
317                            atBottom ? navHeight
318                                     : FrameLayout.LayoutParams.MATCH_PARENT,
319                            atBottom ? Gravity.BOTTOM|Gravity.FILL_HORIZONTAL
320                                     : Gravity.RIGHT|Gravity.FILL_VERTICAL));
321                mFakeNavBar.setPivotY(navHeight);
322                mFakeNavBar.setPivotX(navWidth);
323            }
324            mFakeNavBar.setAlpha(0f);
325            if (atBottom) {
326                mFakeNavBar.setScaleY(0.5f);
327            } else {
328                mFakeNavBar.setScaleX(0.5f);
329            }
330            mFakeNavBar.setVisibility(View.VISIBLE);
331            mFakeNavBar.animate()
332                .alpha(1f)
333                .scaleY(1f)
334                .scaleY(1f)
335                .setDuration(WIDGET_ANIMATION_DURATION)
336                .start();
337        }
338        mCallbacks.onLaunchingCamera();
339    }
340
341    private void recover() {
342        if (DEBUG) Log.d(TAG, "recovering at " + SystemClock.uptimeMillis());
343        mCallbacks.onCameraLaunchedUnsuccessfully();
344        reset();
345    }
346
347    @Override
348    public void setOnLongClickListener(OnLongClickListener l) {
349        // ignore
350    }
351
352    @Override
353    public void onClick(View v) {
354        if (DEBUG) Log.d(TAG, "clicked");
355        if (mTransitioning) return;
356        if (mActive) {
357            cancelTransitionToCamera();
358            transitionToCamera();
359        }
360    }
361
362    @Override
363    protected void onDetachedFromWindow() {
364        if (DEBUG) Log.d(TAG, "onDetachedFromWindow: instance " + instanceId()
365                + " at " + SystemClock.uptimeMillis());
366        super.onDetachedFromWindow();
367        KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mCallback);
368        cancelTransitionToCamera();
369        mHandler.removeCallbacks(mRecoverRunnable);
370    }
371
372    @Override
373    public void onActive(boolean isActive) {
374        mActive = isActive;
375        if (mActive) {
376            rescheduleTransitionToCamera();
377        } else {
378            reset();
379        }
380    }
381
382    @Override
383    public boolean onUserInteraction(MotionEvent event) {
384        if (mTransitioning) {
385            if (DEBUG) Log.d(TAG, "onUserInteraction eaten: mTransitioning");
386            return true;
387        }
388
389        getLocationOnScreen(mTmpLoc);
390        int rawBottom = mTmpLoc[1] + getHeight();
391        if (event.getRawY() > rawBottom) {
392            if (DEBUG) Log.d(TAG, "onUserInteraction eaten: below widget");
393            return true;
394        }
395
396        int action = event.getAction();
397        mDown = action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE;
398        if (mActive) {
399            rescheduleTransitionToCamera();
400        }
401        if (DEBUG) Log.d(TAG, "onUserInteraction observed, not eaten");
402        return false;
403    }
404
405    @Override
406    protected void onFocusLost() {
407        if (DEBUG) Log.d(TAG, "onFocusLost at " + SystemClock.uptimeMillis());
408        cancelTransitionToCamera();
409        super.onFocusLost();
410    }
411
412    public void onScreenTurnedOff() {
413        if (DEBUG) Log.d(TAG, "onScreenTurnedOff");
414        reset();
415    }
416
417    private void rescheduleTransitionToCamera() {
418        if (DEBUG) Log.d(TAG, "rescheduleTransitionToCamera at " + SystemClock.uptimeMillis());
419        mHandler.removeCallbacks(mTransitionToCameraRunnable);
420        mHandler.postDelayed(mTransitionToCameraRunnable, WIDGET_WAIT_DURATION);
421    }
422
423    private void cancelTransitionToCamera() {
424        if (DEBUG) Log.d(TAG, "cancelTransitionToCamera at " + SystemClock.uptimeMillis());
425        mHandler.removeCallbacks(mTransitionToCameraRunnable);
426    }
427
428    private void onCameraLaunched() {
429        mCallbacks.onCameraLaunchedSuccessfully();
430        reset();
431    }
432
433    private void reset() {
434        if (DEBUG) Log.d(TAG, "reset at " + SystemClock.uptimeMillis());
435        mLaunchCameraStart = 0;
436        mTransitioning = false;
437        mDown = false;
438        cancelTransitionToCamera();
439        mHandler.removeCallbacks(mRecoverRunnable);
440        mPreview.setVisibility(View.VISIBLE);
441        if (mFullscreenPreview != null) {
442            mFullscreenPreview.animate().cancel();
443            mFullscreenPreview.setVisibility(View.GONE);
444        }
445        if (mFakeNavBar != null) {
446            mFakeNavBar.animate().cancel();
447            mFakeNavBar.setVisibility(View.GONE);
448        }
449        enableWindowExitAnimation(true);
450    }
451
452    @Override
453    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
454        if (DEBUG) Log.d(TAG, String.format("onSizeChanged new=%sx%s old=%sx%s at %s",
455                w, h, oldw, oldh, SystemClock.uptimeMillis()));
456        if ((w != oldw && oldw > 0) || (h != oldh && oldh > 0)) {
457            // we can't trust the old geometry anymore; force a re-render
458            mRenderedSize.x = mRenderedSize.y = -1;
459        }
460        mHandler.post(mRenderRunnable);
461        super.onSizeChanged(w, h, oldw, oldh);
462    }
463
464    @Override
465    public void onBouncerShowing(boolean showing) {
466        if (showing) {
467            mTransitioning = false;
468            mHandler.post(mRecoverRunnable);
469        }
470    }
471
472    private void enableWindowExitAnimation(boolean isEnabled) {
473        View root = getRootView();
474        ViewGroup.LayoutParams lp = root.getLayoutParams();
475        if (!(lp instanceof WindowManager.LayoutParams))
476            return;
477        WindowManager.LayoutParams wlp = (WindowManager.LayoutParams) lp;
478        int newWindowAnimations = isEnabled ? R.style.Animation_LockScreen : 0;
479        if (newWindowAnimations != wlp.windowAnimations) {
480            if (DEBUG) Log.d(TAG, "setting windowAnimations to: " + newWindowAnimations
481                    + " at " + SystemClock.uptimeMillis());
482            wlp.windowAnimations = newWindowAnimations;
483            mWindowManager.updateViewLayout(root, wlp);
484        }
485    }
486
487    private void onKeyguardVisibilityChanged(boolean showing) {
488        if (DEBUG) Log.d(TAG, "onKeyguardVisibilityChanged " + showing
489                + " at " + SystemClock.uptimeMillis());
490        if (mTransitioning && !showing) {
491            mTransitioning = false;
492            mHandler.removeCallbacks(mRecoverRunnable);
493            if (mLaunchCameraStart > 0) {
494                long launchTime = SystemClock.uptimeMillis() - mLaunchCameraStart;
495                if (DEBUG) Log.d(TAG, String.format("Camera took %sms to launch", launchTime));
496                mLaunchCameraStart = 0;
497                onCameraLaunched();
498            }
499        }
500    }
501
502    private void onSecureCameraActivityStarted() {
503        if (DEBUG) Log.d(TAG, "onSecureCameraActivityStarted at " + SystemClock.uptimeMillis());
504        mHandler.postDelayed(mRecoverRunnable, RECOVERY_DELAY);
505    }
506
507    private String instanceId() {
508        return Integer.toHexString(hashCode());
509    }
510
511    public void setInsets(Rect insets) {
512        if (DEBUG) Log.d(TAG, "setInsets: " + insets);
513        mInsets.set(insets);
514    }
515}
516