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.Point;
23import android.graphics.Rect;
24import android.os.Handler;
25import android.os.SystemClock;
26import android.util.Log;
27import android.view.Gravity;
28import android.view.LayoutInflater;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.WindowManager;
33import android.widget.FrameLayout;
34import android.widget.ImageView;
35import android.widget.ImageView.ScaleType;
36
37import com.android.keyguard.KeyguardActivityLauncher.CameraWidgetInfo;
38
39public 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