diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java new file mode 100644 index 0000000000000..1e4bb9a504bae --- /dev/null +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java @@ -0,0 +1,188 @@ +/* + * 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.companion.ContextSyncMessage; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** A read-only snapshot of an {@link ContextSyncMessage}. */ +class CallMetadataSyncData { + + final Map mCalls = new HashMap<>(); + final List mRequests = new ArrayList<>(); + + public void addCall(CallMetadataSyncData.Call call) { + mCalls.put(call.getId(), call); + } + + public boolean hasCall(long id) { + return mCalls.containsKey(id); + } + + public Collection getCalls() { + return mCalls.values(); + } + + public void addRequest(CallMetadataSyncData.Call call) { + mRequests.add(call); + } + + public List getRequests() { + return mRequests; + } + + public static class Call implements Parcelable { + private long mId; + private String mCallerId; + private byte[] mAppIcon; + private String mAppName; + private String mAppIdentifier; + private int mStatus; + private final Set mControls = new HashSet<>(); + + public static Call fromParcel(Parcel parcel) { + final Call call = new Call(); + call.setId(parcel.readLong()); + call.setCallerId(parcel.readString()); + call.setAppIcon(parcel.readBlob()); + call.setAppName(parcel.readString()); + call.setAppIdentifier(parcel.readString()); + call.setStatus(parcel.readInt()); + final int numberOfControls = parcel.readInt(); + for (int i = 0; i < numberOfControls; i++) { + call.addControl(parcel.readInt()); + } + return call; + } + + @Override + public void writeToParcel(Parcel parcel, int parcelableFlags) { + parcel.writeLong(mId); + parcel.writeString(mCallerId); + parcel.writeBlob(mAppIcon); + parcel.writeString(mAppName); + parcel.writeString(mAppIdentifier); + parcel.writeInt(mStatus); + parcel.writeInt(mControls.size()); + for (int control : mControls) { + parcel.writeInt(control); + } + } + + void setId(long id) { + mId = id; + } + + void setCallerId(String callerId) { + mCallerId = callerId; + } + + void setAppIcon(byte[] appIcon) { + mAppIcon = appIcon; + } + + void setAppName(String appName) { + mAppName = appName; + } + + void setAppIdentifier(String appIdentifier) { + mAppIdentifier = appIdentifier; + } + + void setStatus(int status) { + mStatus = status; + } + + void addControl(int control) { + mControls.add(control); + } + + long getId() { + return mId; + } + + String getCallerId() { + return mCallerId; + } + + byte[] getAppIcon() { + return mAppIcon; + } + + String getAppName() { + return mAppName; + } + + String getAppIdentifier() { + return mAppIdentifier; + } + + int getStatus() { + return mStatus; + } + + Set getControls() { + return mControls; + } + + boolean hasControl(int control) { + return mControls.contains(control); + } + + @Override + public boolean equals(Object other) { + if (other instanceof CallMetadataSyncData.Call) { + return ((Call) other).getId() == getId(); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(mId); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull public static final Parcelable.Creator CREATOR = new Parcelable.Creator<>() { + + @Override + public Call createFromParcel(Parcel source) { + return Call.fromParcel(source); + } + + @Override + public Call[] newArray(int size) { + return new Call[size]; + } + }; + } +} diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java index 077fd2a1157d3..dd0bbf2790eeb 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java @@ -39,6 +39,8 @@ public class CrossDeviceCall { private static final String TAG = "CrossDeviceCall"; + public static final String EXTRA_CALL_ID = + "com.android.companion.datatransfer.contextsync.extra.CALL_ID"; private static final int APP_ICON_BITMAP_DIMENSION = 256; private static final AtomicLong sNextId = new AtomicLong(1); @@ -47,6 +49,7 @@ public class CrossDeviceCall { private final Call mCall; @VisibleForTesting boolean mIsEnterprise; @VisibleForTesting boolean mIsOtt; + private final String mCallingAppPackageName; private String mCallingAppName; private byte[] mCallingAppIcon; private String mCallerDisplayName; @@ -59,7 +62,7 @@ public class CrossDeviceCall { CallAudioState callAudioState) { mId = sNextId.getAndIncrement(); mCall = call; - final String callingAppPackageName = call != null + mCallingAppPackageName = call != null ? call.getDetails().getAccountHandle().getComponentName().getPackageName() : null; mIsOtt = call != null && (call.getDetails().getCallCapabilities() & Call.Details.PROPERTY_SELF_MANAGED) @@ -69,13 +72,13 @@ public class CrossDeviceCall { == Call.Details.PROPERTY_ENTERPRISE_CALL; try { final ApplicationInfo applicationInfo = packageManager - .getApplicationInfo(callingAppPackageName, + .getApplicationInfo(mCallingAppPackageName, PackageManager.ApplicationInfoFlags.of(0)); mCallingAppName = packageManager.getApplicationLabel(applicationInfo).toString(); mCallingAppIcon = renderDrawableToByteArray( packageManager.getApplicationIcon(applicationInfo)); } catch (PackageManager.NameNotFoundException e) { - Slog.e(TAG, "Could not get application info for package " + callingAppPackageName, e); + Slog.e(TAG, "Could not get application info for package " + mCallingAppPackageName, e); } mIsMuted = callAudioState != null && callAudioState.isMuted(); if (call != null) { @@ -170,7 +173,8 @@ public class CrossDeviceCall { } } - private int convertStateToStatus(int callState) { + /** 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; @@ -178,20 +182,30 @@ public class CrossDeviceCall { return android.companion.Telecom.Call.ONGOING; case Call.STATE_RINGING: return android.companion.Telecom.Call.RINGING; - case Call.STATE_NEW: - case Call.STATE_DIALING: - case Call.STATE_DISCONNECTED: - case Call.STATE_SELECT_PHONE_ACCOUNT: - case Call.STATE_CONNECTING: - case Call.STATE_DISCONNECTING: - case Call.STATE_PULLING_CALL: - case Call.STATE_AUDIO_PROCESSING: - case Call.STATE_SIMULATED_RINGING: default: 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.UNKNOWN_STATUS: + default: + return Call.STATE_NEW; + } + } + public long getId() { return mId; } @@ -208,6 +222,10 @@ public class CrossDeviceCall { return mCallingAppIcon; } + public String getCallingAppPackageName() { + return mCallingAppPackageName; + } + /** * Get a human-readable "caller id" to display as the origin of the call. * diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java new file mode 100644 index 0000000000000..c5a9af7d909d9 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java @@ -0,0 +1,64 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import android.testing.AndroidTestingRunner; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidTestingRunner.class) +public class CallMetadataSyncDataTest { + + @Test + public void call_writeToParcel_fromParcel_reconstructsSuccessfully() { + final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call(); + final long id = 5; + final String callerId = "callerId"; + final byte[] appIcon = "appIcon".getBytes(); + final String appName = "appName"; + final String appIdentifier = "com.google.test"; + final int status = 1; + final int control1 = 2; + final int control2 = 3; + call.setId(id); + call.setCallerId(callerId); + call.setAppIcon(appIcon); + call.setAppName(appName); + call.setAppIdentifier(appIdentifier); + call.setStatus(status); + call.addControl(control1); + call.addControl(control2); + + Parcel parcel = Parcel.obtain(); + call.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + final CallMetadataSyncData.Call reconstructedCall = CallMetadataSyncData.Call.fromParcel( + parcel); + + assertThat(reconstructedCall.getId()).isEqualTo(id); + assertThat(reconstructedCall.getCallerId()).isEqualTo(callerId); + assertThat(reconstructedCall.getAppIcon()).isEqualTo(appIcon); + assertThat(reconstructedCall.getAppName()).isEqualTo(appName); + assertThat(reconstructedCall.getAppIdentifier()).isEqualTo(appIdentifier); + assertThat(reconstructedCall.getStatus()).isEqualTo(status); + assertThat(reconstructedCall.getControls()).containsExactly(control1, control2); + } +}