• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 
17 package com.android.server.companion.virtual;
18 
19 import static android.hardware.camera2.CameraInjectionSession.InjectionStatusCallback.ERROR_INJECTION_UNSUPPORTED;
20 
21 import android.annotation.NonNull;
22 import android.content.Context;
23 import android.content.pm.ApplicationInfo;
24 import android.content.pm.PackageManager;
25 import android.content.pm.UserInfo;
26 import android.hardware.camera2.CameraAccessException;
27 import android.hardware.camera2.CameraInjectionSession;
28 import android.hardware.camera2.CameraManager;
29 import android.os.Process;
30 import android.os.UserManager;
31 import android.util.ArrayMap;
32 import android.util.ArraySet;
33 import android.util.Slog;
34 
35 import com.android.internal.annotations.GuardedBy;
36 
37 import java.util.List;
38 import java.util.Set;
39 
40 /**
41  * Handles blocking access to the camera for apps running on virtual devices.
42  */
43 class CameraAccessController extends CameraManager.AvailabilityCallback implements AutoCloseable {
44     private static final String TAG = "CameraAccessController";
45 
46     private final Object mLock = new Object();
47 
48     private final Context mContext;
49     private final VirtualDeviceManagerInternal mVirtualDeviceManagerInternal;
50     private final CameraAccessBlockedCallback mBlockedCallback;
51     private final CameraManager mCameraManager;
52     private final PackageManager mPackageManager;
53     private final UserManager mUserManager;
54 
55     @GuardedBy("mLock")
56     private int mObserverCount = 0;
57 
58     @GuardedBy("mLock")
59     private ArrayMap<String, InjectionSessionData> mPackageToSessionData = new ArrayMap<>();
60 
61     /**
62      * Mapping from camera ID to open camera app associations. Key is the camera id, value is the
63      * information of the app's uid and package name.
64      */
65     @GuardedBy("mLock")
66     private ArrayMap<String, OpenCameraInfo> mAppsToBlockOnVirtualDevice = new ArrayMap<>();
67 
68     static class InjectionSessionData {
69         public int appUid;
70         public ArrayMap<String, CameraInjectionSession> cameraIdToSession = new ArrayMap<>();
71     }
72 
73     static class OpenCameraInfo {
74         public String packageName;
75         public Set<Integer> packageUids;
76     }
77 
78     interface CameraAccessBlockedCallback {
79         /**
80          * Called whenever an app was blocked from accessing a camera.
81          * @param appUid uid for the app which was blocked
82          */
onCameraAccessBlocked(int appUid)83         void onCameraAccessBlocked(int appUid);
84     }
85 
CameraAccessController(Context context, VirtualDeviceManagerInternal virtualDeviceManagerInternal, CameraAccessBlockedCallback blockedCallback)86     CameraAccessController(Context context,
87             VirtualDeviceManagerInternal virtualDeviceManagerInternal,
88             CameraAccessBlockedCallback blockedCallback) {
89         mContext = context;
90         mVirtualDeviceManagerInternal = virtualDeviceManagerInternal;
91         mBlockedCallback = blockedCallback;
92         mCameraManager = mContext.getSystemService(CameraManager.class);
93         mPackageManager = mContext.getPackageManager();
94         mUserManager = mContext.getSystemService(UserManager.class);
95     }
96 
97     /**
98      * Starts watching for camera access by uids running on a virtual device, if we were not
99      * already doing so.
100      */
startObservingIfNeeded()101     public void startObservingIfNeeded() {
102         synchronized (mLock) {
103             if (mObserverCount == 0) {
104                 mCameraManager.registerAvailabilityCallback(mContext.getMainExecutor(), this);
105             }
106             mObserverCount++;
107         }
108     }
109 
110     /**
111      * Stop watching for camera access.
112      */
stopObservingIfNeeded()113     public void stopObservingIfNeeded() {
114         synchronized (mLock) {
115             mObserverCount--;
116             if (mObserverCount <= 0) {
117                 close();
118             }
119         }
120     }
121 
122     /**
123      * Need to block camera access for applications running on virtual displays.
124      * <p>
125      * Apps that open the camera on the main display will need to block camera access if moved to a
126      * virtual display.
127      *
128      * @param runningUids uids of the application running on the virtual display
129      */
blockCameraAccessIfNeeded(Set<Integer> runningUids)130     public void blockCameraAccessIfNeeded(Set<Integer> runningUids) {
131         synchronized (mLock) {
132             for (int i = 0; i < mAppsToBlockOnVirtualDevice.size(); i++) {
133                 final String cameraId = mAppsToBlockOnVirtualDevice.keyAt(i);
134                 final OpenCameraInfo openCameraInfo = mAppsToBlockOnVirtualDevice.get(cameraId);
135                 final String packageName = openCameraInfo.packageName;
136                 for (int packageUid : openCameraInfo.packageUids) {
137                     if (runningUids.contains(packageUid)) {
138                         InjectionSessionData data = mPackageToSessionData.get(packageName);
139                         if (data == null) {
140                             data = new InjectionSessionData();
141                             data.appUid = packageUid;
142                             mPackageToSessionData.put(packageName, data);
143                         }
144                         startBlocking(packageName, cameraId);
145                         break;
146                     }
147                 }
148             }
149         }
150     }
151 
152     @Override
close()153     public void close() {
154         synchronized (mLock) {
155             if (mObserverCount < 0) {
156                 Slog.wtf(TAG, "Unexpected negative mObserverCount: " + mObserverCount);
157             } else if (mObserverCount > 0) {
158                 Slog.w(TAG, "Unexpected close with observers remaining: " + mObserverCount);
159             }
160         }
161         mCameraManager.unregisterAvailabilityCallback(this);
162     }
163 
164     @Override
onCameraOpened(@onNull String cameraId, @NonNull String packageName)165     public void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) {
166         synchronized (mLock) {
167             InjectionSessionData data = mPackageToSessionData.get(packageName);
168             List<UserInfo> aliveUsers = mUserManager.getAliveUsers();
169             ArraySet<Integer> packageUids = new ArraySet<>();
170             for (UserInfo user : aliveUsers) {
171                 int userId = user.getUserHandle().getIdentifier();
172                 int appUid = queryUidFromPackageName(userId, packageName);
173                 if (mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(appUid)) {
174                     if (data == null) {
175                         data = new InjectionSessionData();
176                         data.appUid = appUid;
177                         mPackageToSessionData.put(packageName, data);
178                     }
179                     if (data.cameraIdToSession.containsKey(cameraId)) {
180                         return;
181                     }
182                     startBlocking(packageName, cameraId);
183                     return;
184                 } else {
185                     if (appUid != Process.INVALID_UID) {
186                         packageUids.add(appUid);
187                     }
188                 }
189             }
190             OpenCameraInfo openCameraInfo = new OpenCameraInfo();
191             openCameraInfo.packageName = packageName;
192             openCameraInfo.packageUids = packageUids;
193             mAppsToBlockOnVirtualDevice.put(cameraId, openCameraInfo);
194             CameraInjectionSession existingSession =
195                     (data != null) ? data.cameraIdToSession.get(cameraId) : null;
196             if (existingSession != null) {
197                 existingSession.close();
198                 data.cameraIdToSession.remove(cameraId);
199                 if (data.cameraIdToSession.isEmpty()) {
200                     mPackageToSessionData.remove(packageName);
201                 }
202             }
203         }
204     }
205 
206     @Override
onCameraClosed(@onNull String cameraId)207     public void onCameraClosed(@NonNull String cameraId) {
208         synchronized (mLock) {
209             mAppsToBlockOnVirtualDevice.remove(cameraId);
210             for (int i = mPackageToSessionData.size() - 1; i >= 0; i--) {
211                 InjectionSessionData data = mPackageToSessionData.valueAt(i);
212                 CameraInjectionSession session = data.cameraIdToSession.get(cameraId);
213                 if (session != null) {
214                     session.close();
215                     data.cameraIdToSession.remove(cameraId);
216                     if (data.cameraIdToSession.isEmpty()) {
217                         mPackageToSessionData.removeAt(i);
218                     }
219                 }
220             }
221         }
222     }
223 
224     /**
225      * Turns on blocking for a particular camera and package.
226      */
startBlocking(String packageName, String cameraId)227     private void startBlocking(String packageName, String cameraId) {
228         try {
229             Slog.d(
230                     TAG,
231                     "startBlocking() cameraId: " + cameraId + " packageName: " + packageName);
232             mCameraManager.injectCamera(packageName, cameraId, /* externalCamId */ "",
233                     mContext.getMainExecutor(),
234                     new CameraInjectionSession.InjectionStatusCallback() {
235                         @Override
236                         public void onInjectionSucceeded(
237                                 @NonNull CameraInjectionSession session) {
238                             CameraAccessController.this.onInjectionSucceeded(cameraId, packageName,
239                                     session);
240                         }
241 
242                         @Override
243                         public void onInjectionError(@NonNull int errorCode) {
244                             CameraAccessController.this.onInjectionError(cameraId, packageName,
245                                     errorCode);
246                         }
247                     });
248         } catch (CameraAccessException e) {
249             Slog.e(TAG,
250                     "Failed to injectCamera for cameraId:" + cameraId + " package:" + packageName,
251                     e);
252         }
253     }
254 
onInjectionSucceeded(String cameraId, String packageName, @NonNull CameraInjectionSession session)255     private void onInjectionSucceeded(String cameraId, String packageName,
256             @NonNull CameraInjectionSession session) {
257         synchronized (mLock) {
258             InjectionSessionData data = mPackageToSessionData.get(packageName);
259             if (data == null) {
260                 Slog.e(TAG, "onInjectionSucceeded didn't find expected entry for package "
261                         + packageName);
262                 session.close();
263                 return;
264             }
265             CameraInjectionSession existingSession = data.cameraIdToSession.put(cameraId, session);
266             if (existingSession != null) {
267                 Slog.e(TAG, "onInjectionSucceeded found unexpected existing session for camera "
268                         + cameraId);
269                 existingSession.close();
270             }
271         }
272     }
273 
onInjectionError(String cameraId, String packageName, @NonNull int errorCode)274     private void onInjectionError(String cameraId, String packageName, @NonNull int errorCode) {
275         if (errorCode != ERROR_INJECTION_UNSUPPORTED) {
276             // ERROR_INJECTION_UNSUPPORTED means that there wasn't an external camera to map to the
277             // internal camera, which is expected when using the injection interface as we are in
278             // this class to simply block camera access. Any other error is unexpected.
279             Slog.e(TAG, "Unexpected injection error code:" + errorCode + " for camera:" + cameraId
280                     + " and package:" + packageName);
281             return;
282         }
283         synchronized (mLock) {
284             InjectionSessionData data = mPackageToSessionData.get(packageName);
285             if (data != null) {
286                 mBlockedCallback.onCameraAccessBlocked(data.appUid);
287             }
288         }
289     }
290 
queryUidFromPackageName(int userId, String packageName)291     private int queryUidFromPackageName(int userId, String packageName) {
292         try {
293             final ApplicationInfo ainfo =
294                     mPackageManager.getApplicationInfoAsUser(packageName,
295                         PackageManager.GET_ACTIVITIES, userId);
296             return ainfo.uid;
297         } catch (PackageManager.NameNotFoundException e) {
298             Slog.w(TAG, "queryUidFromPackageName - unknown package " + packageName, e);
299             return Process.INVALID_UID;
300         }
301     }
302 }
303