1 /* 2 * Copyright (C) 2023 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.datatransfer.contextsync; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageManager; 25 import android.net.Uri; 26 import android.telecom.Call; 27 import android.telecom.CallAudioState; 28 import android.telecom.PhoneAccountHandle; 29 import android.telecom.TelecomManager; 30 import android.telecom.VideoProfile; 31 import android.text.TextUtils; 32 import android.util.Slog; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 36 import java.util.HashSet; 37 import java.util.Set; 38 import java.util.UUID; 39 40 /** Data holder for a telecom call and additional metadata. */ 41 public class CrossDeviceCall { 42 43 private static final String TAG = "CrossDeviceCall"; 44 private static final String SEPARATOR = "::"; 45 46 private final String mId; 47 private final Call mCall; 48 private final int mUserId; 49 @VisibleForTesting boolean mIsEnterprise; 50 private final String mCallingAppPackageName; 51 private final String mSerializedPhoneAccountHandle; 52 private String mCallingAppName; 53 private byte[] mCallingAppIcon; 54 private String mCallerDisplayName; 55 private int mCallerDisplayNamePresentation; 56 private int mStatus = android.companion.Telecom.Call.UNKNOWN_STATUS; 57 private String mContactDisplayName; 58 private Uri mHandle; 59 private int mHandlePresentation; 60 private int mDirection; 61 private boolean mIsMuted; 62 private final Set<Integer> mControls = new HashSet<>(); 63 private final boolean mIsCallPlacedByContextSync; 64 CrossDeviceCall(Context context, @NonNull Call call, CallAudioState callAudioState)65 public CrossDeviceCall(Context context, @NonNull Call call, 66 CallAudioState callAudioState) { 67 this(context, call, call.getDetails(), callAudioState); 68 } 69 CrossDeviceCall(Context context, Call.Details callDetails, CallAudioState callAudioState)70 CrossDeviceCall(Context context, Call.Details callDetails, 71 CallAudioState callAudioState) { 72 this(context, /* call= */ null, callDetails, callAudioState); 73 } 74 CrossDeviceCall(Context context, @Nullable Call call, Call.Details callDetails, CallAudioState callAudioState)75 private CrossDeviceCall(Context context, @Nullable Call call, 76 Call.Details callDetails, CallAudioState callAudioState) { 77 mCall = call; 78 final String predefinedId = callDetails.getIntentExtras() != null 79 ? callDetails.getIntentExtras().getString(CrossDeviceSyncController.EXTRA_CALL_ID) 80 : null; 81 final String generatedId = UUID.randomUUID().toString(); 82 mId = predefinedId != null ? (generatedId + SEPARATOR + predefinedId) : generatedId; 83 if (call != null) { 84 call.putExtra(CrossDeviceSyncController.EXTRA_CALL_ID, mId); 85 } 86 final PhoneAccountHandle handle = callDetails.getAccountHandle(); 87 mUserId = handle != null ? handle.getUserHandle().getIdentifier() : -1; 88 mIsCallPlacedByContextSync = handle != null 89 && new ComponentName(context, CallMetadataSyncConnectionService.class) 90 .equals(handle.getComponentName()); 91 mCallingAppPackageName = handle != null 92 ? callDetails.getAccountHandle().getComponentName().getPackageName() : ""; 93 mSerializedPhoneAccountHandle = handle != null 94 ? handle.getId() + SEPARATOR + handle.getComponentName().flattenToString() : ""; 95 mIsEnterprise = (callDetails.getCallProperties() & Call.Details.PROPERTY_ENTERPRISE_CALL) 96 == Call.Details.PROPERTY_ENTERPRISE_CALL; 97 final PackageManager packageManager = context.getPackageManager(); 98 try { 99 final ApplicationInfo applicationInfo = packageManager 100 .getApplicationInfoAsUser(mCallingAppPackageName, 101 PackageManager.ApplicationInfoFlags.of(0), mUserId); 102 mCallingAppName = packageManager.getApplicationLabel(applicationInfo).toString(); 103 mCallingAppIcon = BitmapUtils.renderDrawableToByteArray( 104 packageManager.getApplicationIcon(applicationInfo)); 105 } catch (PackageManager.NameNotFoundException e) { 106 Slog.e(TAG, "Could not get application info for package " + mCallingAppPackageName, e); 107 } 108 mIsMuted = callAudioState != null && callAudioState.isMuted(); 109 updateCallDetails(callDetails); 110 } 111 112 /** 113 * Update the mute state of this call. No-op if the call is not capable of being muted. 114 * 115 * @param isMuted true if the call should be muted, and false if the call should be unmuted. 116 */ updateMuted(boolean isMuted)117 public void updateMuted(boolean isMuted) { 118 mIsMuted = isMuted; 119 updateCallDetails(mCall.getDetails()); 120 } 121 122 /** 123 * Update the state of the call to be ringing silently if it is currently ringing. No-op if the 124 * call is not 125 * currently ringing. 126 */ updateSilencedIfRinging()127 public void updateSilencedIfRinging() { 128 if (mStatus == android.companion.Telecom.Call.RINGING) { 129 mStatus = android.companion.Telecom.Call.RINGING_SILENCED; 130 } 131 mControls.remove(android.companion.Telecom.SILENCE); 132 } 133 134 @VisibleForTesting updateCallDetails(Call.Details callDetails)135 void updateCallDetails(Call.Details callDetails) { 136 mCallerDisplayName = callDetails.getCallerDisplayName(); 137 mCallerDisplayNamePresentation = callDetails.getCallerDisplayNamePresentation(); 138 mContactDisplayName = callDetails.getContactDisplayName(); 139 mHandle = callDetails.getHandle(); 140 mHandlePresentation = callDetails.getHandlePresentation(); 141 final int direction = callDetails.getCallDirection(); 142 if (direction == Call.Details.DIRECTION_INCOMING) { 143 mDirection = android.companion.Telecom.Call.INCOMING; 144 } else if (direction == Call.Details.DIRECTION_OUTGOING) { 145 mDirection = android.companion.Telecom.Call.OUTGOING; 146 } else { 147 mDirection = android.companion.Telecom.Call.UNKNOWN_DIRECTION; 148 } 149 mStatus = convertStateToStatus(callDetails.getState()); 150 mControls.clear(); 151 if (mStatus == android.companion.Telecom.Call.DIALING) { 152 mControls.add(android.companion.Telecom.END); 153 } 154 if (mStatus == android.companion.Telecom.Call.RINGING 155 || mStatus == android.companion.Telecom.Call.RINGING_SILENCED) { 156 mControls.add(android.companion.Telecom.ACCEPT); 157 mControls.add(android.companion.Telecom.REJECT); 158 if (mStatus == android.companion.Telecom.Call.RINGING) { 159 mControls.add(android.companion.Telecom.SILENCE); 160 } 161 } 162 if (mStatus == android.companion.Telecom.Call.ONGOING 163 || mStatus == android.companion.Telecom.Call.ON_HOLD) { 164 mControls.add(android.companion.Telecom.END); 165 if (callDetails.can(Call.Details.CAPABILITY_HOLD)) { 166 mControls.add( 167 mStatus == android.companion.Telecom.Call.ON_HOLD 168 ? android.companion.Telecom.TAKE_OFF_HOLD 169 : android.companion.Telecom.PUT_ON_HOLD); 170 } 171 } 172 if (mStatus == android.companion.Telecom.Call.ONGOING && callDetails.can( 173 Call.Details.CAPABILITY_MUTE)) { 174 mControls.add(mIsMuted ? android.companion.Telecom.UNMUTE 175 : android.companion.Telecom.MUTE); 176 } 177 } 178 179 /** Converts a Telecom call state to a Context Sync status. */ convertStateToStatus(int callState)180 public static int convertStateToStatus(int callState) { 181 switch (callState) { 182 case Call.STATE_HOLDING: 183 return android.companion.Telecom.Call.ON_HOLD; 184 case Call.STATE_ACTIVE: 185 return android.companion.Telecom.Call.ONGOING; 186 case Call.STATE_RINGING: 187 return android.companion.Telecom.Call.RINGING; 188 case Call.STATE_AUDIO_PROCESSING: 189 return android.companion.Telecom.Call.AUDIO_PROCESSING; 190 case Call.STATE_SIMULATED_RINGING: 191 return android.companion.Telecom.Call.RINGING_SIMULATED; 192 case Call.STATE_DISCONNECTED: 193 return android.companion.Telecom.Call.DISCONNECTED; 194 case Call.STATE_DIALING: 195 return android.companion.Telecom.Call.DIALING; 196 default: 197 Slog.e(TAG, "Couldn't resolve state to status: " + callState); 198 return android.companion.Telecom.Call.UNKNOWN_STATUS; 199 } 200 } 201 202 /** 203 * Converts a Context Sync status to a Telecom call state. Note that this is lossy for 204 * and RINGING_SILENCED, as Telecom does not distinguish between RINGING and RINGING_SILENCED. 205 */ convertStatusToState(int status)206 public static int convertStatusToState(int status) { 207 switch (status) { 208 case android.companion.Telecom.Call.ON_HOLD: 209 return Call.STATE_HOLDING; 210 case android.companion.Telecom.Call.ONGOING: 211 return Call.STATE_ACTIVE; 212 case android.companion.Telecom.Call.RINGING: 213 case android.companion.Telecom.Call.RINGING_SILENCED: 214 return Call.STATE_RINGING; 215 case android.companion.Telecom.Call.AUDIO_PROCESSING: 216 return Call.STATE_AUDIO_PROCESSING; 217 case android.companion.Telecom.Call.RINGING_SIMULATED: 218 return Call.STATE_SIMULATED_RINGING; 219 case android.companion.Telecom.Call.DISCONNECTED: 220 return Call.STATE_DISCONNECTED; 221 case android.companion.Telecom.Call.DIALING: 222 return Call.STATE_DIALING; 223 case android.companion.Telecom.Call.UNKNOWN_STATUS: 224 default: 225 return Call.STATE_NEW; 226 } 227 } 228 getId()229 public String getId() { 230 return mId; 231 } 232 getCall()233 public Call getCall() { 234 return mCall; 235 } 236 getUserId()237 public int getUserId() { 238 return mUserId; 239 } 240 getCallingAppName()241 public String getCallingAppName() { 242 return mCallingAppName; 243 } 244 getCallingAppIcon()245 public byte[] getCallingAppIcon() { 246 return mCallingAppIcon; 247 } 248 getCallingAppPackageName()249 public String getCallingAppPackageName() { 250 return mCallingAppPackageName; 251 } 252 getSerializedPhoneAccountHandle()253 public String getSerializedPhoneAccountHandle() { 254 return mSerializedPhoneAccountHandle; 255 } 256 257 /** 258 * Get a human-readable "caller id" to display as the origin of the call. 259 * 260 * @param isAdminBlocked whether there is an admin that has blocked contacts over Bluetooth 261 */ getReadableCallerId(boolean isAdminBlocked)262 public String getReadableCallerId(boolean isAdminBlocked) { 263 if (mIsEnterprise && isAdminBlocked) { 264 // Cannot use any contact information. 265 return getNonContactString(); 266 } 267 return TextUtils.isEmpty(mContactDisplayName) ? getNonContactString() : mContactDisplayName; 268 } 269 getNonContactString()270 private String getNonContactString() { 271 if (!TextUtils.isEmpty(mCallerDisplayName) 272 && mCallerDisplayNamePresentation == TelecomManager.PRESENTATION_ALLOWED) { 273 return mCallerDisplayName; 274 } 275 if (mHandle != null && mHandle.getSchemeSpecificPart() != null 276 && mHandlePresentation == TelecomManager.PRESENTATION_ALLOWED) { 277 return mHandle.getSchemeSpecificPart(); 278 } 279 return null; 280 } 281 getStatus()282 public int getStatus() { 283 return mStatus; 284 } 285 getDirection()286 public int getDirection() { 287 return mDirection; 288 } 289 getControls()290 public Set<Integer> getControls() { 291 return mControls; 292 } 293 isCallPlacedByContextSync()294 public boolean isCallPlacedByContextSync() { 295 return mIsCallPlacedByContextSync; 296 } 297 doAccept()298 void doAccept() { 299 mCall.answer(VideoProfile.STATE_AUDIO_ONLY); 300 } 301 doReject()302 void doReject() { 303 if (mStatus == android.companion.Telecom.Call.RINGING) { 304 mCall.reject(Call.REJECT_REASON_DECLINED); 305 } 306 } 307 doEnd()308 void doEnd() { 309 mCall.disconnect(); 310 } 311 doPutOnHold()312 void doPutOnHold() { 313 mCall.hold(); 314 } 315 doTakeOffHold()316 void doTakeOffHold() { 317 mCall.unhold(); 318 } 319 } 320