blob: fec6923e4d06ec4a879c801d43601122427bd49e [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.companion.datatransfer.contextsync;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.telecom.Call;
import android.telecom.CallAudioState;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/** Data holder for a telecom call and additional metadata. */
public class CrossDeviceCall {
private static final String TAG = "CrossDeviceCall";
private final String mId;
private final Call mCall;
@VisibleForTesting boolean mIsEnterprise;
private final String mCallingAppPackageName;
private String mCallingAppName;
private byte[] mCallingAppIcon;
private String mCallerDisplayName;
private int mCallerDisplayNamePresentation;
private int mStatus = android.companion.Telecom.Call.UNKNOWN_STATUS;
private String mContactDisplayName;
private Uri mHandle;
private int mHandlePresentation;
private boolean mIsMuted;
private final Set<Integer> mControls = new HashSet<>();
private final boolean mIsCallPlacedByContextSync;
public CrossDeviceCall(Context context, @NonNull Call call,
CallAudioState callAudioState) {
this(context, call, call.getDetails(), callAudioState);
}
CrossDeviceCall(Context context, Call.Details callDetails,
CallAudioState callAudioState) {
this(context, /* call= */ null, callDetails, callAudioState);
}
private CrossDeviceCall(Context context, @Nullable Call call,
Call.Details callDetails, CallAudioState callAudioState) {
mCall = call;
final String predefinedId = callDetails.getIntentExtras() != null
? callDetails.getIntentExtras().getString(CrossDeviceSyncController.EXTRA_CALL_ID)
: null;
final String generatedId = UUID.randomUUID().toString();
mId = predefinedId != null ? (generatedId + predefinedId) : generatedId;
if (call != null) {
call.putExtra(CrossDeviceSyncController.EXTRA_CALL_ID, mId);
}
mIsCallPlacedByContextSync =
new ComponentName(context, CallMetadataSyncConnectionService.class)
.equals(callDetails.getAccountHandle().getComponentName());
mCallingAppPackageName =
callDetails.getAccountHandle().getComponentName().getPackageName();
mIsEnterprise = (callDetails.getCallProperties() & Call.Details.PROPERTY_ENTERPRISE_CALL)
== Call.Details.PROPERTY_ENTERPRISE_CALL;
final PackageManager packageManager = context.getPackageManager();
try {
final ApplicationInfo applicationInfo = packageManager
.getApplicationInfo(mCallingAppPackageName,
PackageManager.ApplicationInfoFlags.of(0));
mCallingAppName = packageManager.getApplicationLabel(applicationInfo).toString();
mCallingAppIcon = BitmapUtils.renderDrawableToByteArray(
packageManager.getApplicationIcon(applicationInfo));
} catch (PackageManager.NameNotFoundException e) {
Slog.e(TAG, "Could not get application info for package " + mCallingAppPackageName, e);
}
mIsMuted = callAudioState != null && callAudioState.isMuted();
updateCallDetails(callDetails);
}
/**
* Update the mute state of this call. No-op if the call is not capable of being muted.
*
* @param isMuted true if the call should be muted, and false if the call should be unmuted.
*/
public void updateMuted(boolean isMuted) {
mIsMuted = isMuted;
updateCallDetails(mCall.getDetails());
}
/**
* Update the state of the call to be ringing silently if it is currently ringing. No-op if the
* call is not
* currently ringing.
*/
public void updateSilencedIfRinging() {
if (mStatus == android.companion.Telecom.Call.RINGING) {
mStatus = android.companion.Telecom.Call.RINGING_SILENCED;
}
mControls.remove(android.companion.Telecom.SILENCE);
}
@VisibleForTesting
void updateCallDetails(Call.Details callDetails) {
mCallerDisplayName = callDetails.getCallerDisplayName();
mCallerDisplayNamePresentation = callDetails.getCallerDisplayNamePresentation();
mContactDisplayName = callDetails.getContactDisplayName();
mHandle = callDetails.getHandle();
mHandlePresentation = callDetails.getHandlePresentation();
mStatus = convertStateToStatus(callDetails.getState());
mControls.clear();
if (mStatus == android.companion.Telecom.Call.RINGING
|| mStatus == android.companion.Telecom.Call.RINGING_SILENCED) {
mControls.add(android.companion.Telecom.ACCEPT);
mControls.add(android.companion.Telecom.REJECT);
if (mStatus == android.companion.Telecom.Call.RINGING) {
mControls.add(android.companion.Telecom.SILENCE);
}
}
if (mStatus == android.companion.Telecom.Call.ONGOING
|| mStatus == android.companion.Telecom.Call.ON_HOLD) {
mControls.add(android.companion.Telecom.END);
if (callDetails.can(Call.Details.CAPABILITY_HOLD)) {
mControls.add(
mStatus == android.companion.Telecom.Call.ON_HOLD
? android.companion.Telecom.TAKE_OFF_HOLD
: android.companion.Telecom.PUT_ON_HOLD);
}
}
if (mStatus == android.companion.Telecom.Call.ONGOING && callDetails.can(
Call.Details.CAPABILITY_MUTE)) {
mControls.add(mIsMuted ? android.companion.Telecom.UNMUTE
: android.companion.Telecom.MUTE);
}
}
/** Converts a Telecom call state to a Context Sync status. */
public static int convertStateToStatus(int callState) {
switch (callState) {
case Call.STATE_HOLDING:
return android.companion.Telecom.Call.ON_HOLD;
case Call.STATE_ACTIVE:
return android.companion.Telecom.Call.ONGOING;
case Call.STATE_RINGING:
return android.companion.Telecom.Call.RINGING;
case Call.STATE_AUDIO_PROCESSING:
return android.companion.Telecom.Call.AUDIO_PROCESSING;
case Call.STATE_SIMULATED_RINGING:
return android.companion.Telecom.Call.RINGING_SIMULATED;
case Call.STATE_DISCONNECTED:
return android.companion.Telecom.Call.DISCONNECTED;
default:
Slog.e(TAG, "Couldn't resolve state to status: " + callState);
return android.companion.Telecom.Call.UNKNOWN_STATUS;
}
}
/**
* Converts a Context Sync status to a Telecom call state. Note that this is lossy for
* and RINGING_SILENCED, as Telecom does not distinguish between RINGING and RINGING_SILENCED.
*/
public static int convertStatusToState(int status) {
switch (status) {
case android.companion.Telecom.Call.ON_HOLD:
return Call.STATE_HOLDING;
case android.companion.Telecom.Call.ONGOING:
return Call.STATE_ACTIVE;
case android.companion.Telecom.Call.RINGING:
case android.companion.Telecom.Call.RINGING_SILENCED:
return Call.STATE_RINGING;
case android.companion.Telecom.Call.AUDIO_PROCESSING:
return Call.STATE_AUDIO_PROCESSING;
case android.companion.Telecom.Call.RINGING_SIMULATED:
return Call.STATE_SIMULATED_RINGING;
case android.companion.Telecom.Call.DISCONNECTED:
return Call.STATE_DISCONNECTED;
case android.companion.Telecom.Call.UNKNOWN_STATUS:
default:
return Call.STATE_NEW;
}
}
public String getId() {
return mId;
}
public Call getCall() {
return mCall;
}
public String getCallingAppName() {
return mCallingAppName;
}
public byte[] getCallingAppIcon() {
return mCallingAppIcon;
}
public String getCallingAppPackageName() {
return mCallingAppPackageName;
}
/**
* Get a human-readable "caller id" to display as the origin of the call.
*
* @param isAdminBlocked whether there is an admin that has blocked contacts over Bluetooth
*/
public String getReadableCallerId(boolean isAdminBlocked) {
if (mIsEnterprise && isAdminBlocked) {
// Cannot use any contact information.
return getNonContactString();
}
return mContactDisplayName != null ? mContactDisplayName : getNonContactString();
}
private String getNonContactString() {
if (mCallerDisplayName != null
&& mCallerDisplayNamePresentation == TelecomManager.PRESENTATION_ALLOWED) {
return mCallerDisplayName;
}
if (mHandle != null && mHandle.getSchemeSpecificPart() != null
&& mHandlePresentation == TelecomManager.PRESENTATION_ALLOWED) {
return mHandle.getSchemeSpecificPart();
}
return null;
}
public int getStatus() {
return mStatus;
}
public Set<Integer> getControls() {
return mControls;
}
public boolean isCallPlacedByContextSync() {
return mIsCallPlacedByContextSync;
}
void doAccept() {
mCall.answer(VideoProfile.STATE_AUDIO_ONLY);
}
void doReject() {
if (mStatus == android.companion.Telecom.Call.RINGING) {
mCall.reject(Call.REJECT_REASON_DECLINED);
}
}
void doEnd() {
mCall.disconnect();
}
void doPutOnHold() {
mCall.hold();
}
void doTakeOffHold() {
mCall.unhold();
}
}