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