1 /* 2 * Copyright (C) 2024 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.association; 18 19 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; 20 import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; 21 22 import static com.android.internal.util.CollectionUtils.any; 23 import static com.android.server.companion.utils.RolesUtils.removeRoleHolderForAssociation; 24 25 import static java.util.concurrent.TimeUnit.DAYS; 26 27 import android.annotation.NonNull; 28 import android.annotation.SuppressLint; 29 import android.annotation.UserIdInt; 30 import android.app.ActivityManager; 31 import android.companion.AssociationInfo; 32 import android.content.Context; 33 import android.content.pm.PackageManagerInternal; 34 import android.os.Binder; 35 import android.os.SystemProperties; 36 import android.os.UserHandle; 37 import android.util.Slog; 38 39 import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; 40 import com.android.server.companion.devicepresence.CompanionAppBinder; 41 import com.android.server.companion.devicepresence.DevicePresenceProcessor; 42 import com.android.server.companion.transport.CompanionTransportManager; 43 44 /** 45 * This class responsible for disassociation. 46 */ 47 @SuppressLint("LongLogTag") 48 public class DisassociationProcessor { 49 50 public static final String REASON_REVOKED = "revoked"; 51 public static final String REASON_SELF_IDLE = "self-idle"; 52 public static final String REASON_SHELL = "shell"; 53 public static final String REASON_LEGACY = "legacy"; 54 public static final String REASON_API = "api"; 55 public static final String REASON_PKG_DATA_CLEARED = "pkg-data-cleared"; 56 57 private static final String TAG = "CDM_DisassociationProcessor"; 58 59 private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW = 60 "debug.cdm.cdmservice.removal_time_window"; 61 private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); 62 63 @NonNull 64 private final Context mContext; 65 @NonNull 66 private final AssociationStore mAssociationStore; 67 @NonNull 68 private final PackageManagerInternal mPackageManagerInternal; 69 @NonNull 70 private final DevicePresenceProcessor mDevicePresenceMonitor; 71 @NonNull 72 private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; 73 @NonNull 74 private final CompanionAppBinder mCompanionAppController; 75 @NonNull 76 private final CompanionTransportManager mTransportManager; 77 private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener; 78 private final ActivityManager mActivityManager; 79 DisassociationProcessor(@onNull Context context, @NonNull ActivityManager activityManager, @NonNull AssociationStore associationStore, @NonNull PackageManagerInternal packageManager, @NonNull DevicePresenceProcessor devicePresenceMonitor, @NonNull CompanionAppBinder applicationController, @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, @NonNull CompanionTransportManager companionTransportManager)80 public DisassociationProcessor(@NonNull Context context, 81 @NonNull ActivityManager activityManager, 82 @NonNull AssociationStore associationStore, 83 @NonNull PackageManagerInternal packageManager, 84 @NonNull DevicePresenceProcessor devicePresenceMonitor, 85 @NonNull CompanionAppBinder applicationController, 86 @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, 87 @NonNull CompanionTransportManager companionTransportManager) { 88 mContext = context; 89 mActivityManager = activityManager; 90 mAssociationStore = associationStore; 91 mPackageManagerInternal = packageManager; 92 mOnPackageVisibilityChangeListener = 93 new OnPackageVisibilityChangeListener(); 94 mDevicePresenceMonitor = devicePresenceMonitor; 95 mCompanionAppController = applicationController; 96 mSystemDataTransferRequestStore = systemDataTransferRequestStore; 97 mTransportManager = companionTransportManager; 98 } 99 100 /** 101 * Disassociate an association by id. 102 */ 103 // TODO: also revoke notification access disassociate(int id, String reason)104 public void disassociate(int id, String reason) { 105 Slog.i(TAG, "Disassociating id=[" + id + "]..."); 106 107 final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id); 108 final int userId = association.getUserId(); 109 final String packageName = association.getPackageName(); 110 final String deviceProfile = association.getDeviceProfile(); 111 112 final boolean isRoleInUseByOtherAssociations = deviceProfile != null 113 && any(mAssociationStore.getActiveAssociationsByPackage(userId, packageName), 114 it -> deviceProfile.equals(it.getDeviceProfile()) && id != it.getId()); 115 116 final int packageProcessImportance = getPackageProcessImportance(userId, packageName); 117 if (packageProcessImportance <= IMPORTANCE_FOREGROUND && deviceProfile != null 118 && !isRoleInUseByOtherAssociations) { 119 // Need to remove the app from the list of role holders, but the process is visible 120 // to the user at the moment, so we'll need to do it later. 121 Slog.i(TAG, "Cannot disassociate id=[" + id + "] now - process is visible. " 122 + "Start listening to package importance..."); 123 124 AssociationInfo revokedAssociation = (new AssociationInfo.Builder( 125 association)).setRevoked(true).build(); 126 mAssociationStore.updateAssociation(revokedAssociation); 127 startListening(); 128 return; 129 } 130 131 // Detach transport if exists 132 mTransportManager.detachSystemDataTransport(id); 133 134 // Association cleanup. 135 mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id); 136 mAssociationStore.removeAssociation(association.getId(), reason); 137 138 // If role is not in use by other associations, revoke the role. 139 // Do not need to remove the system role since it was pre-granted by the system. 140 if (!isRoleInUseByOtherAssociations && deviceProfile != null && !deviceProfile.equals( 141 DEVICE_PROFILE_AUTOMOTIVE_PROJECTION)) { 142 removeRoleHolderForAssociation(mContext, association.getUserId(), 143 association.getPackageName(), association.getDeviceProfile()); 144 } 145 146 // Unbind the app if needed. 147 final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(id); 148 if (!wasPresent || !association.isNotifyOnDeviceNearby()) { 149 return; 150 } 151 final boolean shouldStayBound = any( 152 mAssociationStore.getActiveAssociationsByPackage(userId, packageName), 153 it -> it.isNotifyOnDeviceNearby() 154 && mDevicePresenceMonitor.isDevicePresent(it.getId())); 155 if (!shouldStayBound) { 156 mCompanionAppController.unbindCompanionApp(userId, packageName); 157 } 158 } 159 160 /** 161 * @deprecated Use {@link #disassociate(int, String)} instead. 162 */ 163 @Deprecated disassociate(int userId, String packageName, String macAddress)164 public void disassociate(int userId, String packageName, String macAddress) { 165 AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, 166 packageName, macAddress); 167 168 if (association == null) { 169 throw new IllegalArgumentException( 170 "Association for mac address=[" + macAddress + "] doesn't exist"); 171 } 172 173 mAssociationStore.getAssociationWithCallerChecks(association.getId()); 174 175 disassociate(association.getId(), REASON_LEGACY); 176 } 177 178 @SuppressLint("MissingPermission") getPackageProcessImportance(@serIdInt int userId, @NonNull String packageName)179 private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { 180 return Binder.withCleanCallingIdentity(() -> { 181 final int uid = 182 mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); 183 return mActivityManager.getUidImportance(uid); 184 }); 185 } 186 startListening()187 private void startListening() { 188 Slog.i(TAG, "Start listening to uid importance changes..."); 189 try { 190 Binder.withCleanCallingIdentity( 191 () -> mActivityManager.addOnUidImportanceListener( 192 mOnPackageVisibilityChangeListener, 193 ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE)); 194 } catch (IllegalArgumentException e) { 195 Slog.e(TAG, "Failed to start listening to uid importance changes."); 196 } 197 } 198 stopListening()199 private void stopListening() { 200 Slog.i(TAG, "Stop listening to uid importance changes."); 201 try { 202 Binder.withCleanCallingIdentity(() -> mActivityManager.removeOnUidImportanceListener( 203 mOnPackageVisibilityChangeListener)); 204 } catch (IllegalArgumentException e) { 205 Slog.e(TAG, "Failed to stop listening to uid importance changes."); 206 } 207 } 208 209 /** 210 * Remove idle self-managed associations. 211 */ removeIdleSelfManagedAssociations()212 public void removeIdleSelfManagedAssociations() { 213 Slog.i(TAG, "Removing idle self-managed associations."); 214 215 final long currentTime = System.currentTimeMillis(); 216 long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1); 217 if (removalWindow <= 0) { 218 // 0 or negative values indicate that the sysprop was never set or should be ignored. 219 removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT; 220 } 221 222 for (AssociationInfo association : mAssociationStore.getAssociations()) { 223 if (!association.isSelfManaged()) continue; 224 225 final boolean isInactive = 226 currentTime - association.getLastTimeConnectedMs() >= removalWindow; 227 if (!isInactive) continue; 228 229 final int id = association.getId(); 230 231 Slog.i(TAG, "Removing inactive self-managed association=[" + association.toShortString() 232 + "]."); 233 disassociate(id, REASON_SELF_IDLE); 234 } 235 } 236 237 /** 238 * An OnUidImportanceListener class which watches the importance of the packages. 239 * In this class, we ONLY interested in the importance of the running process is greater than 240 * {@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_VISIBLE}. 241 * 242 * Lastly remove the role holder for the revoked associations for the same packages. 243 * 244 * @see #disassociate(int, String) 245 */ 246 private class OnPackageVisibilityChangeListener implements 247 ActivityManager.OnUidImportanceListener { 248 // This method is called when the importance of a uid (app) changes. 249 // We only care about changes where the app is moving to the background. 250 // (e.g., the app currently is not at the top of the screen that the user 251 // is interacting with.) 252 @Override onUidImportance(int uid, int importance)253 public void onUidImportance(int uid, int importance) { 254 // Higher importance values indicate the app is less important. 255 // We are only interested when the process importance level 256 // is greater than IMPORTANCE_FOREGROUND. 257 if (importance <= IMPORTANCE_FOREGROUND) { 258 return; 259 } 260 261 final String packageName = mPackageManagerInternal.getNameForUid(uid); 262 if (packageName == null) { 263 // Not interested in this uid. 264 return; 265 } 266 267 int userId = UserHandle.getUserId(uid); 268 for (AssociationInfo association : mAssociationStore.getRevokedAssociations(userId, 269 packageName)) { 270 disassociate(association.getId(), REASON_REVOKED); 271 } 272 273 if (mAssociationStore.getRevokedAssociations().isEmpty()) { 274 stopListening(); 275 } 276 } 277 } 278 } 279