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

Commit 4a297591 authored by Joe Antonetti's avatar Joe Antonetti Committed by Android (Google) Code Review
Browse files

Merge changes If8d91943,Id5ae1f35,I4130728e into main

* changes:
  Remove Devices from RemoteTaskStore when the transport is lost
  Send RemoteTaskAddedMessage when tasks are added
  Create ConnectedAssociationStore
parents ca89793f 78628b69
Loading
Loading
Loading
Loading
+87 −31
Original line number Diff line number Diff line
@@ -20,42 +20,51 @@ import static android.companion.CompanionDeviceManager.MESSAGE_TASK_CONTINUITY;

import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.companion.CompanionDeviceManager;
import android.app.TaskStackListener;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceManager;
import android.content.ComponentName;
import android.content.Context;
import android.os.RemoteException;
import android.util.Slog;

import com.android.server.companion.datatransfer.continuity.connectivity.ConnectedAssociationStore;
import com.android.server.companion.datatransfer.continuity.messages.ContinuityDeviceConnected;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskAddedMessage;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskInfo;
import com.android.server.companion.datatransfer.continuity.messages.TaskContinuityMessage;
import com.android.server.companion.datatransfer.continuity.messages.TaskContinuityMessageData;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Responsible for broadcasting recent tasks on the current device to the user's
 *
 * other devices via {@link CompanionDeviceManager}.
 */
class TaskBroadcaster {
class TaskBroadcaster
    extends TaskStackListener
    implements ConnectedAssociationStore.Observer {

    private static final String TAG = "TaskBroadcaster";

    private final Context mContext;
    private final ActivityTaskManager mActivityTaskManager;
    private final CompanionDeviceManager mCompanionDeviceManager;
    private final Set<Integer> mConnectedAssociationIds = new HashSet<>();

    private final Consumer<List<AssociationInfo>> mOnTransportsChangedListener =
        this::onTransportsChanged;
    private final ConnectedAssociationStore mConnectedAssociationStore;

    private boolean mIsBroadcasting = false;

    public TaskBroadcaster(Context context) {
    public TaskBroadcaster(
        Context context,
        ConnectedAssociationStore connectedAssociationStore) {

        mContext = context;
        mConnectedAssociationStore = connectedAssociationStore;

        mActivityTaskManager
            = context.getSystemService(ActivityTaskManager.class);
@@ -71,10 +80,9 @@ class TaskBroadcaster {
        }

        Slog.v(TAG, "Starting broadcasting");
        mCompanionDeviceManager.addOnTransportsChangedListener(
            mContext.getMainExecutor(),
            mOnTransportsChangedListener
        );
        mConnectedAssociationStore.addObserver(this);
        mActivityTaskManager.registerTaskStackListener(this);

        mIsBroadcasting = true;
    }

@@ -86,27 +94,42 @@ class TaskBroadcaster {

        Slog.v(TAG, "Stopping broadcasting");
        mIsBroadcasting = false;
        mCompanionDeviceManager.removeOnTransportsChangedListener(
            mOnTransportsChangedListener
        );
        mConnectedAssociationStore.removeObserver(this);
        mActivityTaskManager.unregisterTaskStackListener(this);
    }

    private void onTransportsChanged(List<AssociationInfo> associationInfos) {
        Set<Integer> removedAssociationIds
            = new HashSet<>(mConnectedAssociationIds);

        for (AssociationInfo associationInfo : associationInfos) {
            if (!mConnectedAssociationIds.contains(associationInfo.getId())) {
    @Override
    public void onTransportConnected(AssociationInfo associationInfo) {
        Slog.v(
            TAG,
            "Transport connected for association id: " + associationInfo.getId());
        sendDeviceConnectedMessage(associationInfo.getId());
            } else {
                removedAssociationIds.remove(associationInfo.getId());
    }

            mConnectedAssociationIds.add(associationInfo.getId());
    @Override
    public void onTransportDisconnected(int associationId) {
        Slog.v(
            TAG,
            "Transport disconnected for association id: " + associationId);
    }

        for (Integer removedAssociationId : removedAssociationIds) {
            mConnectedAssociationIds.remove(removedAssociationId);
    @Override
    public void onTaskCreated(
        int taskId,
        ComponentName componentName) throws RemoteException {

        Slog.v(TAG, "onTaskCreated: taskId=" + taskId);

        ActivityManager.RunningTaskInfo taskInfo = getRunningTask(taskId);

        if (taskInfo != null) {
            RemoteTaskInfo remoteTaskInfo = new RemoteTaskInfo(taskInfo);
            RemoteTaskAddedMessage taskAddedMessage
                = new RemoteTaskAddedMessage(remoteTaskInfo);

            sendMessageToAllConnectedAssociations(taskAddedMessage);
        } else {
            Slog.w(TAG, "Could not find RunningTaskInfo for taskId: " + taskId);
        }
    }

@@ -116,8 +139,7 @@ class TaskBroadcaster {
            "Sending device connected message for association id: "
                + associationId);

        List<ActivityManager.RunningTaskInfo> runningTasks
            = mActivityTaskManager.getTasks(Integer.MAX_VALUE, true);
        List<ActivityManager.RunningTaskInfo> runningTasks = getRunningTasks();

        int currentForegroundTaskId = -1;
        if (runningTasks.size() > 0) {
@@ -135,7 +157,9 @@ class TaskBroadcaster {
        sendMessage(associationId, deviceConnectedMessage);
    }

    private void sendMessage(int associationId, TaskContinuityMessageData data) {
    private void sendMessage(
        int associationId,
        TaskContinuityMessageData data) {

        Slog.v(
            TAG,
@@ -151,4 +175,36 @@ class TaskBroadcaster {
            message.toBytes(),
            new int[] {associationId});
    }

    private void sendMessageToAllConnectedAssociations(
        TaskContinuityMessageData data) {

        Collection<AssociationInfo> connectedAssociations
            = mConnectedAssociationStore.getConnectedAssociations();

        Slog.v(
            TAG,
            "Sending message to " + connectedAssociations.size() + " associations.");

        for (AssociationInfo associationInfo : connectedAssociations) {
            sendMessage(associationInfo.getId(), data);
        }
    }

    private ActivityManager.RunningTaskInfo getRunningTask(int taskId) {
        List<ActivityManager.RunningTaskInfo> runningTasks = getRunningTasks();
        if (runningTasks != null) {
            for (ActivityManager.RunningTaskInfo info : runningTasks) {
                if (info.taskId == taskId) {
                    return info;
                }
            }
        }

        return null;
    }

    private List<ActivityManager.RunningTaskInfo> getRunningTasks() {
        return mActivityTaskManager.getTasks(Integer.MAX_VALUE, true);
    }
}
 No newline at end of file
+11 −5
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.companion.datatransfer.continuity;

import android.companion.CompanionDeviceManager;
import android.companion.datatransfer.continuity.ITaskContinuityManager;
import android.content.Context;
import android.util.Slog;
@@ -25,6 +26,7 @@ import com.android.server.companion.datatransfer.continuity.messages.TaskContinu
import com.android.server.companion.datatransfer.continuity.tasks.RemoteTaskStore;

import com.android.server.SystemService;
import com.android.server.companion.datatransfer.continuity.connectivity.ConnectedAssociationStore;

/**
 * Service to handle task continuity features
@@ -38,14 +40,20 @@ public final class TaskContinuityManagerService extends SystemService {

    private TaskContinuityManagerServiceImpl mTaskContinuityManagerService;
    private TaskBroadcaster mTaskBroadcaster;
    private ConnectedAssociationStore mConnectedAssociationStore;
    private TaskContinuityMessageReceiver mTaskContinuityMessageReceiver;
    private RemoteTaskStore mRemoteTaskStore;

    public TaskContinuityManagerService(Context context) {
        super(context);
        mTaskBroadcaster = new TaskBroadcaster(context);
        mConnectedAssociationStore = new ConnectedAssociationStore(context);

        mTaskBroadcaster = new TaskBroadcaster(
            context,
            mConnectedAssociationStore);

        mTaskContinuityMessageReceiver = new TaskContinuityMessageReceiver(context);
        mRemoteTaskStore = new RemoteTaskStore();
        mRemoteTaskStore = new RemoteTaskStore(mConnectedAssociationStore);
    }

    @Override
@@ -68,10 +76,8 @@ public final class TaskContinuityManagerService extends SystemService {

        switch (taskContinuityMessage.getData()) {
            case ContinuityDeviceConnected continuityDeviceConnected:
                // TODO: joeantonetti - Extract a readable device name and pass it to the store.
                mRemoteTaskStore.registerDevice(
                mRemoteTaskStore.setTasks(
                    associationId,
                    String.format("device-%d", associationId),
                    continuityDeviceConnected.getRemoteTasks());
                break;
            default:
+111 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.continuity.connectivity;

import android.annotation.NonNull;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceManager;
import android.content.Context;
import android.util.Log;

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.Set;

public class ConnectedAssociationStore {

    private static final String TAG = "ConnectedAssociationStore";

    private final CompanionDeviceManager mCompanionDeviceManager;
    private final Map<Integer, AssociationInfo> mConnectedAssociations = new HashMap<>();
    private final List<Observer> mObservers = new ArrayList<>();

    public interface Observer {
        void onTransportConnected(AssociationInfo associationInfo);
        void onTransportDisconnected(int associationId);
    }

    public ConnectedAssociationStore(
            @NonNull Context context) {
        mCompanionDeviceManager = context
            .getSystemService(CompanionDeviceManager.class);

        mCompanionDeviceManager.addOnTransportsChangedListener(
                context.getMainExecutor(),
                this::onTransportsChanged);
   }

    public void addObserver(@NonNull Observer observer) {
        mObservers.add(observer);
    }

    public void removeObserver(@NonNull Observer observer) {
        mObservers.remove(observer);
    }

    public Collection<AssociationInfo> getConnectedAssociations() {
        return mConnectedAssociations.values();
    }

    public AssociationInfo getConnectedAssociationById(int associationId) {
        return mConnectedAssociations.get(associationId);
    }

    private void onTransportsChanged(List<AssociationInfo> associationInfos) {
        Set<Integer> removedAssociations
            = new HashSet<>(mConnectedAssociations.keySet());

        Set<AssociationInfo> addedAssociations = new HashSet<>();
        for (AssociationInfo associationInfo : associationInfos) {
            if (!mConnectedAssociations.containsKey(associationInfo.getId())) {
                addedAssociations.add(associationInfo);
            }

            if (removedAssociations.contains(associationInfo.getId())) {
                removedAssociations.remove(associationInfo.getId());
            }
        }

        for (Integer associationId : removedAssociations) {
            Log.i(
                TAG,
                "Transport disconnected for association: " + associationId);

            mConnectedAssociations.remove(associationId);

            for (Observer observer : mObservers) {
                observer.onTransportDisconnected(associationId);
            }
        }

        for (AssociationInfo associationInfo : addedAssociations) {
            Log.i(
                TAG,
                "Transport connected for association: " + associationInfo.getId());

            mConnectedAssociations.put(associationInfo.getId(), associationInfo);

            for (Observer observer : mObservers) {
                observer.onTransportConnected(associationInfo);
            }
        }
    }
}
 No newline at end of file
+10 −4
Original line number Diff line number Diff line
@@ -27,16 +27,14 @@ import java.util.List;
class RemoteDeviceTaskList {
    private final int mAssociationId;
    private final String mDeviceName;
    private List<RemoteTaskInfo> mTasks;
    private final List<RemoteTaskInfo> mTasks = new ArrayList<>();

    RemoteDeviceTaskList(
        int associationId,
        String deviceName,
        List<RemoteTaskInfo> tasks) {
        String deviceName) {

        mAssociationId = associationId;
        mDeviceName = deviceName;
        mTasks = new ArrayList<>(tasks);
    }

    /**
@@ -61,6 +59,14 @@ class RemoteDeviceTaskList {
        mTasks.add(taskInfo);
    }

    /**
     * Sets the list of tasks currently available on the remote device.
     */
    void setTasks(List<RemoteTaskInfo> tasks) {
        mTasks.clear();
        mTasks.addAll(tasks);
    }

    /**
     * Gets the most recently used task on this device, or null if there are no
     * tasks.
+69 −16
Original line number Diff line number Diff line
@@ -15,6 +15,10 @@
 */
package com.android.server.companion.datatransfer.continuity.tasks;

import android.companion.AssociationInfo;
import android.util.Slog;

import com.android.server.companion.datatransfer.continuity.connectivity.ConnectedAssociationStore;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskInfo;

import java.util.ArrayList;
@@ -22,28 +26,42 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RemoteTaskStore {
public class RemoteTaskStore implements ConnectedAssociationStore.Observer {

    private static final String TAG = "RemoteTaskStore";

    private final ConnectedAssociationStore mConnectedAssociationStore;
    private final Map<Integer, RemoteDeviceTaskList> mRemoteDeviceTaskLists
        = new HashMap<>();

    public RemoteTaskStore() {}
    public RemoteTaskStore(
        ConnectedAssociationStore connectedAssociationStore) {

        mConnectedAssociationStore = connectedAssociationStore;
        mConnectedAssociationStore.addObserver(this);
    }

    /**
     * Registers a device with the task store.
     * Sets the task list of the given association id to the given tasks.
     *
     * @param associationId The ID of the device.
     * @param deviceName The name of the device.
     * @param associationId The association id of the device.
     * @param tasks The list of tasks currently available on the device on first
     * connection.
     */
    public void registerDevice(
    public void setTasks(
        int associationId,
        String deviceName,
        List<RemoteTaskInfo> tasks) {
        synchronized (mRemoteDeviceTaskLists) {
            if (!mRemoteDeviceTaskLists.containsKey(associationId)) {
                Slog.e(
                    TAG,
                    "Attempted to set tasks for association: " + associationId + " which is not connected.");

        RemoteDeviceTaskList taskList
            = new RemoteDeviceTaskList(associationId, deviceName, tasks);
        mRemoteDeviceTaskLists.put(associationId, taskList);
                return;
            }

            mRemoteDeviceTaskLists.get(associationId).setTasks(tasks);
        }
    }

    /**
@@ -53,6 +71,7 @@ public class RemoteTaskStore {
     * store.
     */
    public List<RemoteTaskInfo> getMostRecentTasks() {
        synchronized (mRemoteDeviceTaskLists) {
            List<RemoteTaskInfo> mostRecentTasks = new ArrayList<>();
            for (RemoteDeviceTaskList taskList : mRemoteDeviceTaskLists.values()) {
                RemoteTaskInfo mostRecentTask = taskList.getMostRecentTask();
@@ -63,3 +82,37 @@ public class RemoteTaskStore {
            return mostRecentTasks;
        }
    }

    @Override
    public void onTransportConnected(AssociationInfo associationInfo) {
        synchronized (mRemoteDeviceTaskLists) {
            if (!mRemoteDeviceTaskLists.containsKey(associationInfo.getId())) {
                Slog.v(
                    TAG,
                    "Creating new RemoteDeviceTaskList for association: " + associationInfo.getId());

                RemoteDeviceTaskList taskList
                    = new RemoteDeviceTaskList(
                        associationInfo.getId(),
                        associationInfo.getDisplayName().toString());

                mRemoteDeviceTaskLists.put(associationInfo.getId(), taskList);
            } else {
                Slog.v(
                    TAG,
                    "Transport already connected for association: " + associationInfo.getId());
            }
        }
    }

    @Override
    public void onTransportDisconnected(int associationId) {
        synchronized (mRemoteDeviceTaskLists) {
            Slog.v(
                TAG,
                "Deleting RemoteDeviceTaskList for association: " + associationId);

            mRemoteDeviceTaskLists.remove(associationId);
        }
    }
}
 No newline at end of file
Loading