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