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