summaryrefslogtreecommitdiffstats
path: root/packages/SystemUI/src/com/android/systemui/statusbar/phone/SecureCameraLaunchManager.java
blob: 4a43c4784af580a2e39a9d60a2a6ca934f45d7de (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.phone;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.hardware.camera2.CameraManager;
import android.os.AsyncTask;
import android.os.Handler;
import android.provider.MediaStore;
import android.util.Log;

import com.android.internal.widget.LockPatternUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Handles launching the secure camera properly even when other applications may be using the camera
 * hardware.
 *
 * When other applications (e.g., Face Unlock) are using the camera, they must close the camera to
 * allow the secure camera to open it.  Since we want to minimize the delay when opening the secure
 * camera, other apps should close the camera at the first possible opportunity (i.e., as soon as
 * the user begins swiping to go to the secure camera).
 *
 * If the camera is unavailable when the user begins to swipe, the SecureCameraLaunchManager sends a
 * broadcast to tell other apps to close the camera.  When and if the user completes their swipe to
 * launch the secure camera, the SecureCameraLaunchManager delays launching the secure camera until
 * a callback indicates that the camera has become available.  If it doesn't receive that callback
 * within a specified timeout period, the secure camera is launched anyway.
 *
 * Ideally, the secure camera would handle waiting for the camera to become available.  This allows
 * some of the time necessary to close the camera to happen in parallel with starting the secure
 * camera app.  We can't rely on all third-party camera apps to handle this.  However, an app can
 * put com.android.systemui.statusbar.phone.will_wait_for_camera_available in its meta-data to
 * indicate that it will be responsible for waiting for the camera to become available.
 *
 * It is assumed that the functions in this class, including the constructor, will be called from
 * the UI thread.
 */
public class SecureCameraLaunchManager {
    private static final boolean DEBUG = false;
    private static final String TAG = "SecureCameraLaunchManager";

    // Action sent as a broadcast to tell other apps to stop using the camera.  Other apps that use
    // the camera from keyguard (e.g., Face Unlock) should listen for this broadcast and close the
    // camera as soon as possible after receiving it.
    private static final String CLOSE_CAMERA_ACTION_NAME =
            "com.android.systemui.statusbar.phone.CLOSE_CAMERA";

    // Apps should put this field in their meta-data to indicate that they will take on the
    // responsibility of waiting for the camera to become available.  If this field is present, the
    // SecureCameraLaunchManager launches the secure camera even if the camera hardware has not
    // become available.  Having the secure camera app do the waiting is the optimal approach, but
    // without this field, the SecureCameraLaunchManager doesn't launch the secure camera until the
    // camera hardware is available.
    private static final String META_DATA_WILL_WAIT_FOR_CAMERA_AVAILABLE =
            "com.android.systemui.statusbar.phone.will_wait_for_camera_available";

    // If the camera hardware hasn't become available after this period of time, the
    // SecureCameraLaunchManager launches the secure camera anyway.
    private static final int CAMERA_AVAILABILITY_TIMEOUT_MS = 1000;

    private Context mContext;
    private Handler mHandler;
    private LockPatternUtils mLockPatternUtils;
    private KeyguardBottomAreaView mKeyguardBottomArea;

    private CameraManager mCameraManager;
    private CameraAvailabilityCallback mCameraAvailabilityCallback;
    private Map<String, Boolean> mCameraAvailabilityMap;
    private boolean mWaitingToLaunchSecureCamera;
    private Runnable mLaunchCameraRunnable;

    private class CameraAvailabilityCallback extends CameraManager.AvailabilityCallback {
        @Override
        public void onCameraUnavailable(String cameraId) {
            if (DEBUG) Log.d(TAG, "onCameraUnavailble(" + cameraId + ")");
            mCameraAvailabilityMap.put(cameraId, false);
        }

        @Override
        public void onCameraAvailable(String cameraId) {
            if (DEBUG) Log.d(TAG, "onCameraAvailable(" + cameraId + ")");
            mCameraAvailabilityMap.put(cameraId, true);

            // If we were waiting for the camera hardware to become available to launch the
            // secure camera, we can launch it now if all cameras are available.  If one or more
            // cameras are still not available, we will get this callback again for those
            // cameras.
            if (mWaitingToLaunchSecureCamera && areAllCamerasAvailable()) {
                mKeyguardBottomArea.launchCamera();
                mWaitingToLaunchSecureCamera = false;

                // We no longer need to launch the camera after the timeout hits.
                mHandler.removeCallbacks(mLaunchCameraRunnable);
            }
        }
    }

    public SecureCameraLaunchManager(Context context, KeyguardBottomAreaView keyguardBottomArea) {
        mContext = context;
        mHandler = new Handler();
        mLockPatternUtils = new LockPatternUtils(context);
        mKeyguardBottomArea = keyguardBottomArea;

        mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
        mCameraAvailabilityCallback = new CameraAvailabilityCallback();

        // An onCameraAvailable() or onCameraUnavailable() callback will be received for each camera
        // when the availability callback is registered, thus initializing the map.
        //
        // Keeping track of the state of all cameras using the onCameraAvailable() and
        // onCameraUnavailable() callbacks can get messy when dealing with hot-pluggable cameras.
        // However, we have a timeout in place such that we will never hang waiting for cameras.
        mCameraAvailabilityMap = new HashMap<String, Boolean>();

        mWaitingToLaunchSecureCamera = false;
        mLaunchCameraRunnable = new Runnable() {
                @Override
                public void run() {
                    if (mWaitingToLaunchSecureCamera) {
                        Log.w(TAG, "Timeout waiting for camera availability");
                        mKeyguardBottomArea.launchCamera();
                        mWaitingToLaunchSecureCamera = false;
                    }
                }
            };
    }

    /**
     * Initializes the SecureCameraManager and starts listening for camera availability.
     */
    public void create() {
        mCameraManager.registerAvailabilityCallback(mCameraAvailabilityCallback, mHandler);
    }

    /**
     * Stops listening for camera availability and cleans up the SecureCameraManager.
     */
    public void destroy() {
        mCameraManager.unregisterAvailabilityCallback(mCameraAvailabilityCallback);
    }

    /**
     * Called when the user is starting to swipe horizontally, possibly to start the secure camera.
     * Although this swipe ultimately may not result in the secure camera opening, we need to stop
     * all other camera usage (e.g., Face Unlock) as soon as possible.  We send out a broadcast to
     * notify other apps that they should close the camera immediately.  The broadcast is sent even
     * if the camera appears to be available, because there could be an app that is about to open
     * the camera.
     */
    public void onSwipingStarted() {
        if (DEBUG) Log.d(TAG, "onSwipingStarted");
        AsyncTask.execute(new Runnable() {
                @Override
                public void run() {
                    Intent intent = new Intent();
                    intent.setAction(CLOSE_CAMERA_ACTION_NAME);
                    intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
                    mContext.sendBroadcast(intent);
                }
            });
    }

    /**
     * Called when the secure camera should be started.  If the camera is available or the secure
     * camera app has indicated that it will wait for camera availability, the secure camera app is
     * launched immediately.  Otherwise, we wait for the camera to become available (or timeout)
     * before launching the secure camera.
     */
    public void startSecureCameraLaunch() {
        if (DEBUG) Log.d(TAG, "startSecureCameraLunch");
        if (areAllCamerasAvailable() || targetWillWaitForCameraAvailable()) {
            mKeyguardBottomArea.launchCamera();
        } else {
            mWaitingToLaunchSecureCamera = true;
            mHandler.postDelayed(mLaunchCameraRunnable, CAMERA_AVAILABILITY_TIMEOUT_MS);
        }
    }

    /**
     * Returns true if all of the cameras we are tracking are currently available.
     */
    private boolean areAllCamerasAvailable() {
        for (boolean cameraAvailable: mCameraAvailabilityMap.values()) {
            if (!cameraAvailable) {
                return false;
            }
        }
        return true;
    }

    /**
     * Determines if the secure camera app will wait for the camera hardware to become available
     * before trying to open the camera.  If so, we can fire off an intent to start the secure
     * camera app before the camera is available.  Otherwise, it is our responsibility to wait for
     * the camera hardware to become available before firing off the intent to start the secure
     * camera.
     *
     * Ideally we are able to fire off the secure camera intent as early as possibly so that, if the
     * camera is closing, it can continue to close while the secure camera app is opening.  This
     * improves secure camera startup time.
     */
    private boolean targetWillWaitForCameraAvailable() {
        // Create intent that would launch the secure camera.
        Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE)
                .addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
        PackageManager packageManager = mContext.getPackageManager();

        // Get the list of applications that can handle the intent.
        final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
                intent, PackageManager.MATCH_DEFAULT_ONLY, mLockPatternUtils.getCurrentUser());
        if (appList.size() == 0) {
            if (DEBUG) Log.d(TAG, "No targets found for secure camera intent");
            return false;
        }

        // Get the application that the intent resolves to.
        ResolveInfo resolved = packageManager.resolveActivityAsUser(intent,
                PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA,
                mLockPatternUtils.getCurrentUser());

        if (resolved == null || resolved.activityInfo == null) {
            return false;
        }

        // If we would need to launch the resolver activity, then we can't assume that the target
        // is one that would wait for the camera.
        if (wouldLaunchResolverActivity(resolved, appList)) {
            if (DEBUG) Log.d(TAG, "Secure camera intent would launch resolver");
            return false;
        }

        // If the target doesn't have meta-data we must assume it won't wait for the camera.
        if (resolved.activityInfo.metaData == null || resolved.activityInfo.metaData.isEmpty()) {
            if (DEBUG) Log.d(TAG, "No meta-data found for secure camera application");
            return false;
        }

        // Check the secure camera app meta-data to see if it indicates that it will wait for the
        // camera to become available.
        boolean willWaitForCameraAvailability =
                resolved.activityInfo.metaData.getBoolean(META_DATA_WILL_WAIT_FOR_CAMERA_AVAILABLE);

        if (DEBUG) Log.d(TAG, "Target will wait for camera: " + willWaitForCameraAvailability);

        return willWaitForCameraAvailability;
    }

    /**
     * Determines if the activity that would be launched by the intent is the ResolverActivity.
     */
    private boolean wouldLaunchResolverActivity(ResolveInfo resolved, List<ResolveInfo> appList) {
        // If the list contains the resolved activity, then it can't be the ResolverActivity itself.
        for (int i = 0; i < appList.size(); i++) {
            ResolveInfo tmp = appList.get(i);
            if (tmp.activityInfo.name.equals(resolved.activityInfo.name)
                    && tmp.activityInfo.packageName.equals(resolved.activityInfo.packageName)) {
                return false;
            }
        }
        return true;
    }
}