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