Handle changes in call metadata and trigger syncs

Bug: 266152297
Test: atest FrameworksServicesTests:com.android.server.companion.datatransfer

Change-Id: I818c9d1110124b58ecd3148da93a5630fd3d1186
This commit is contained in:
Christine Franks
2023-01-20 01:57:43 -08:00
parent 4bd93e8b10
commit 112d07753a
2 changed files with 239 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
/*
* 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;
import android.provider.DeviceConfig;
/**
* Feature flags for companion.
*/
public class CompanionDeviceConfig {
private static final String NAMESPACE_COMPANION = "companion";
/**
* Whether system data syncing for telecom-type data is enabled.
*/
public static final String ENABLE_CONTEXT_SYNC_TELECOM = "enable_context_sync_telecom";
/**
* Returns whether the given flag is currently enabled, with a default value of {@code true}.
*/
public static boolean isEnabled(String flag) {
return DeviceConfig.getBoolean(NAMESPACE_COMPANION, flag, /* defaultValue= */ true);
}
/**
* Returns whether the given flag is currently enabled.
*/
public static boolean isEnabled(String flag, boolean defaultValue) {
return DeviceConfig.getBoolean(NAMESPACE_COMPANION, flag, defaultValue);
}
}

View File

@@ -0,0 +1,193 @@
/*
* 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.app.admin.DevicePolicyManager;
import android.companion.AssociationInfo;
import android.companion.ContextSyncMessage;
import android.companion.Telecom;
import android.companion.Telecom.Call;
import android.content.Context;
import android.os.UserHandle;
import android.util.Pair;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Monitors connections and sending / receiving of synced data.
*/
public class CrossDeviceSyncController {
private static final String TAG = "CrossDeviceSyncController";
private static final int BYTE_ARRAY_SIZE = 64;
private final Context mContext;
private final Callback mCdmCallback;
private final Map<Integer, List<AssociationInfo>> mUserIdToAssociationInfo = new HashMap<>();
private final Map<Integer, Pair<InputStream, OutputStream>> mAssociationIdToStreams =
new HashMap<>();
private final Set<Integer> mBlocklist = new HashSet<>();
private CallMetadataSyncCallback mInCallServiceCallMetadataSyncCallback;
public CrossDeviceSyncController(Context context, Callback callback) {
mContext = context;
mCdmCallback = callback;
}
/** Registers the call metadata callback. */
public void registerCallMetadataSyncCallback(CallMetadataSyncCallback callback) {
mInCallServiceCallMetadataSyncCallback = callback;
}
/** Allow specific associated devices to enable / disable syncing. */
public void setSyncEnabled(AssociationInfo associationInfo, boolean enabled) {
if (enabled) {
if (mBlocklist.contains(associationInfo.getId())) {
mBlocklist.remove(associationInfo.getId());
openChannel(associationInfo);
}
} else {
if (!mBlocklist.contains(associationInfo.getId())) {
mBlocklist.add(associationInfo.getId());
closeChannel(associationInfo);
}
}
}
/**
* Opens channels to newly associated devices, and closes channels to newly disassociated
* devices.
*
* TODO(b/265466098): this needs to be limited to just connected devices
*/
public void onAssociationsChanged(int userId, List<AssociationInfo> newAssociationInfoList) {
final List<AssociationInfo> existingAssociationInfoList = mUserIdToAssociationInfo.get(
userId);
// Close channels to newly-disconnected devices.
for (AssociationInfo existingAssociationInfo : existingAssociationInfoList) {
if (!newAssociationInfoList.contains(existingAssociationInfo) && !mBlocklist.contains(
existingAssociationInfo.getId())) {
closeChannel(existingAssociationInfo);
}
}
// Open channels to newly-connected devices.
for (AssociationInfo newAssociationInfo : newAssociationInfoList) {
if (!existingAssociationInfoList.contains(newAssociationInfo) && !mBlocklist.contains(
newAssociationInfo.getId())) {
openChannel(newAssociationInfo);
}
}
mUserIdToAssociationInfo.put(userId, newAssociationInfoList);
}
private boolean isAdminBlocked(int userId) {
return mContext.getSystemService(DevicePolicyManager.class)
.getBluetoothContactSharingDisabled(UserHandle.of(userId));
}
/** Stop reading, close streams, and close secure channel. */
private void closeChannel(AssociationInfo associationInfo) {
// TODO(b/265466098): stop reading from secure channel
final Pair<InputStream, OutputStream> streams = mAssociationIdToStreams.get(
associationInfo.getId());
if (streams != null) {
try {
if (streams.first != null) {
streams.first.close();
}
if (streams.second != null) {
streams.second.close();
}
} catch (IOException e) {
Slog.e(TAG, "Could not close streams for association " + associationInfo.getId(),
e);
}
}
mCdmCallback.closeSecureChannel(associationInfo.getId());
}
/** Sync initial snapshot and start reading. */
private void openChannel(AssociationInfo associationInfo) {
final InputStream is = new ByteArrayInputStream(new byte[BYTE_ARRAY_SIZE]);
final OutputStream os = new ByteArrayOutputStream(BYTE_ARRAY_SIZE);
mAssociationIdToStreams.put(associationInfo.getId(), new Pair<>(is, os));
mCdmCallback.createSecureChannel(associationInfo.getId(), is, os);
// TODO(b/265466098): only requestSync for this specific association / connection?
mInCallServiceCallMetadataSyncCallback.requestCrossDeviceSync(associationInfo.getUserId());
// TODO(b/265466098): start reading from secure channel
}
/**
* Sync data to associated devices.
*
* @param userId The user whose data should be synced.
* @param calls The full list of current calls for all users.
*/
public void crossDeviceSync(int userId, Collection<CrossDeviceCall> calls) {
final boolean isAdminBlocked = isAdminBlocked(userId);
for (AssociationInfo associationInfo : mUserIdToAssociationInfo.get(userId)) {
final Pair<InputStream, OutputStream> streams = mAssociationIdToStreams.get(
associationInfo.getId());
final ProtoOutputStream pos = new ProtoOutputStream(streams.second);
final long telecomToken = pos.start(ContextSyncMessage.TELECOM);
for (CrossDeviceCall call : calls) {
final long callsToken = pos.start(Telecom.CALLS);
pos.write(Call.ID, call.getId());
final long originToken = pos.start(Call.ORIGIN);
pos.write(Call.Origin.CALLER_ID, call.getReadableCallerId(isAdminBlocked));
pos.write(Call.Origin.APP_ICON, call.getCallingAppIcon());
pos.write(Call.Origin.APP_NAME, call.getCallingAppName());
pos.end(originToken);
pos.write(Call.STATUS, call.getStatus());
for (int control : call.getControls()) {
pos.write(Call.CONTROLS_AVAILABLE, control);
}
pos.end(callsToken);
}
pos.end(telecomToken);
pos.flush();
}
}
/**
* Callback to be implemented by CompanionDeviceManagerService.
*/
public interface Callback {
/**
* Create a secure channel to send messages.
*/
void createSecureChannel(int associationId, InputStream input, OutputStream output);
/**
* Close the secure channel created previously.
*/
void closeSecureChannel(int associationId);
}
}