diff options
2 files changed, 239 insertions, 0 deletions
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceConfig.java b/services/companion/java/com/android/server/companion/CompanionDeviceConfig.java new file mode 100644 index 000000000000..05f2eea621cf --- /dev/null +++ b/services/companion/java/com/android/server/companion/CompanionDeviceConfig.java @@ -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); + } +} diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java new file mode 100644 index 000000000000..3d8fb7a8d5bf --- /dev/null +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java @@ -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); + } +} |