Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 112d0775 authored by Christine Franks's avatar Christine Franks
Browse files

Handle changes in call metadata and trigger syncs

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

Change-Id: I818c9d1110124b58ecd3148da93a5630fd3d1186
parent 4bd93e8b
Loading
Loading
Loading
Loading
+46 −0
Original line number Original line Diff line number Diff line
/*
 * 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);
    }
}
+193 −0
Original line number Original line Diff line number Diff line
/*
 * 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);
    }
}