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