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 final 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 final 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 // Clean up camera injection sessions (if any). 183 synchronized (mLock) { 184 for (InjectionSessionData sessionData : mPackageToSessionData.values()) { 185 for (CameraInjectionSession session : sessionData.cameraIdToSession.values()) { 186 session.close(); 187 } 188 } 189 mPackageToSessionData.clear(); 190 } 191 mCameraManager.unregisterAvailabilityCallback(this); 192 } 193 194 @Override 195 @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA) onCameraOpened(@onNull String cameraId, @NonNull String packageName)196 public void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) { 197 synchronized (mLock) { 198 InjectionSessionData data = mPackageToSessionData.get(packageName); 199 List<UserInfo> aliveUsers = mUserManager.getAliveUsers(); 200 ArraySet<Integer> packageUids = new ArraySet<>(); 201 for (UserInfo user : aliveUsers) { 202 int userId = user.getUserHandle().getIdentifier(); 203 int appUid = queryUidFromPackageName(userId, packageName); 204 if (mVirtualDeviceManagerInternal != null 205 && mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(appUid)) { 206 if (data == null) { 207 data = new InjectionSessionData(); 208 data.appUid = appUid; 209 mPackageToSessionData.put(packageName, data); 210 } 211 if (data.cameraIdToSession.containsKey(cameraId)) { 212 return; 213 } 214 startBlocking(packageName, cameraId); 215 return; 216 } else { 217 if (appUid != Process.INVALID_UID) { 218 packageUids.add(appUid); 219 } 220 } 221 } 222 OpenCameraInfo openCameraInfo = new OpenCameraInfo(); 223 openCameraInfo.packageName = packageName; 224 openCameraInfo.packageUids = packageUids; 225 mAppsToBlockOnVirtualDevice.put(cameraId, openCameraInfo); 226 CameraInjectionSession existingSession = 227 (data != null) ? data.cameraIdToSession.get(cameraId) : null; 228 if (existingSession != null) { 229 existingSession.close(); 230 data.cameraIdToSession.remove(cameraId); 231 if (data.cameraIdToSession.isEmpty()) { 232 mPackageToSessionData.remove(packageName); 233 } 234 } 235 } 236 } 237 238 @Override onCameraClosed(@onNull String cameraId)239 public void onCameraClosed(@NonNull String cameraId) { 240 synchronized (mLock) { 241 mAppsToBlockOnVirtualDevice.remove(cameraId); 242 for (int i = mPackageToSessionData.size() - 1; i >= 0; i--) { 243 InjectionSessionData data = mPackageToSessionData.valueAt(i); 244 CameraInjectionSession session = data.cameraIdToSession.get(cameraId); 245 if (session != null) { 246 session.close(); 247 data.cameraIdToSession.remove(cameraId); 248 if (data.cameraIdToSession.isEmpty()) { 249 mPackageToSessionData.removeAt(i); 250 } 251 } 252 } 253 } 254 } 255 256 /** 257 * Turns on blocking for a particular camera and package. 258 */ 259 @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA) startBlocking(String packageName, String cameraId)260 private void startBlocking(String packageName, String cameraId) { 261 try { 262 Slog.d( 263 TAG, 264 "startBlocking() cameraId: " + cameraId + " packageName: " + packageName); 265 mCameraManager.injectCamera(packageName, cameraId, /* externalCamId */ "", 266 mContext.getMainExecutor(), 267 new CameraInjectionSession.InjectionStatusCallback() { 268 @Override 269 public void onInjectionSucceeded( 270 @NonNull CameraInjectionSession session) { 271 CameraAccessController.this.onInjectionSucceeded(cameraId, packageName, 272 session); 273 } 274 275 @Override 276 public void onInjectionError(@NonNull int errorCode) { 277 CameraAccessController.this.onInjectionError(cameraId, packageName, 278 errorCode); 279 } 280 }); 281 } catch (CameraAccessException e) { 282 Slog.e(TAG, 283 "Failed to injectCamera for cameraId:" + cameraId + " package:" + packageName, 284 e); 285 } 286 } 287 onInjectionSucceeded(String cameraId, String packageName, @NonNull CameraInjectionSession session)288 private void onInjectionSucceeded(String cameraId, String packageName, 289 @NonNull CameraInjectionSession session) { 290 synchronized (mLock) { 291 InjectionSessionData data = mPackageToSessionData.get(packageName); 292 if (data == null) { 293 Slog.e(TAG, "onInjectionSucceeded didn't find expected entry for package " 294 + packageName); 295 session.close(); 296 return; 297 } 298 CameraInjectionSession existingSession = data.cameraIdToSession.put(cameraId, session); 299 if (existingSession != null) { 300 Slog.e(TAG, "onInjectionSucceeded found unexpected existing session for camera " 301 + cameraId); 302 existingSession.close(); 303 } 304 } 305 } 306 onInjectionError(String cameraId, String packageName, @NonNull int errorCode)307 private void onInjectionError(String cameraId, String packageName, @NonNull int errorCode) { 308 if (errorCode != ERROR_INJECTION_UNSUPPORTED) { 309 // ERROR_INJECTION_UNSUPPORTED means that there wasn't an external camera to map to the 310 // internal camera, which is expected when using the injection interface as we are in 311 // this class to simply block camera access. Any other error is unexpected. 312 Slog.e(TAG, "Unexpected injection error code:" + errorCode + " for camera:" + cameraId 313 + " and package:" + packageName); 314 return; 315 } 316 synchronized (mLock) { 317 InjectionSessionData data = mPackageToSessionData.get(packageName); 318 if (data != null) { 319 mBlockedCallback.onCameraAccessBlocked(data.appUid); 320 } 321 } 322 } 323 queryUidFromPackageName(int userId, String packageName)324 private int queryUidFromPackageName(int userId, String packageName) { 325 try { 326 final ApplicationInfo ainfo = 327 mPackageManager.getApplicationInfoAsUser(packageName, 328 PackageManager.GET_ACTIVITIES, userId); 329 return ainfo.uid; 330 } catch (PackageManager.NameNotFoundException e) { 331 Slog.w(TAG, "queryUidFromPackageName - unknown package " + packageName, e); 332 return Process.INVALID_UID; 333 } 334 } 335 } 336