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

Commit fc16dc4b authored by Christine Franks's avatar Christine Franks Committed by Android (Google) Code Review
Browse files

Merge "Handle changes in call metadata and trigger syncs"

parents a32f22ec 112d0775
Loading
Loading
Loading
Loading
+46 −0
Original line number 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 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);
    }
}