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

Commit a8b826c0 authored by timhypeng's avatar timhypeng Committed by tim peng
Browse files

Implement naive rule of device order on seamless transfer

- Add ConnectionRecordManager for device usage record operation
- Add test cases to verify naive rules

Bug: 119065243
Test: make -j50 RunSettingsLibRoboTests
Change-Id: Idf1c0ed575d1feeb3f1eb93b41cd209c3a510bfa
parent 60e88ecd
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 */
package com.android.settingslib.media;

import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;

@@ -33,6 +35,13 @@ public class BluetoothMediaDevice extends MediaDevice {
    BluetoothMediaDevice(Context context, CachedBluetoothDevice device) {
        super(context, MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
        mCachedDevice = device;
        initDeviceRecord();
        buildConnectedState(device);
    }

    private void buildConnectedState(CachedBluetoothDevice device) {
        mIsConnected = device.isActiveDevice(BluetoothProfile.A2DP)
                || device.isActiveDevice(BluetoothProfile.HEARING_AID);
    }

    @Override
@@ -51,10 +60,16 @@ public class BluetoothMediaDevice extends MediaDevice {
        return MediaDeviceUtils.getId(mCachedDevice);
    }

    @Override
    public void notifyConnectedChanged() {
        buildConnectedState(mCachedDevice);
    }

    @Override
    public void connect() {
        //TODO(b/117129183): add callback to notify LocalMediaManager connection state.
        mIsConnected = mCachedDevice.setActive();
        super.connect();
        Log.d(TAG, "connect() device : " + getName() + ", is selected : " + mIsConnected);
    }

@@ -70,4 +85,18 @@ public class BluetoothMediaDevice extends MediaDevice {
    public CachedBluetoothDevice getCachedDevice() {
        return mCachedDevice;
    }

    @Override
    protected boolean isCarKitDevice() {
        final BluetoothClass bluetoothClass = mCachedDevice.getDevice().getBluetoothClass();
        if (bluetoothClass != null) {
            switch (bluetoothClass.getDeviceClass()) {
                // Both are common CarKit class
                case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
                case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
                    return true;
            }
        }
        return false;
    }
}
+50 −22
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.settingslib.media;
import android.app.Notification;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;

@@ -30,6 +31,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
@@ -39,8 +41,8 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall

    private static final String TAG = "BluetoothMediaManager";

    private final DeviceAttributeChangeCallback mCachedDeviceCallback =
            new DeviceAttributeChangeCallback();
    private final DeviceProfileNotReadyObserverCallback mObserverCallback =
            new DeviceProfileNotReadyObserverCallback();

    private LocalBluetoothManager mLocalBluetoothManager;
    private LocalBluetoothProfileManager mProfileManager;
@@ -48,6 +50,10 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
    private MediaDevice mLastAddedDevice;
    private MediaDevice mLastRemovedDevice;

    private boolean mIsA2dpProfileReady = false;
    private boolean mIsHearingAidProfileReady = false;
    private Collection<CachedBluetoothDevice> mCachedDevices;

    BluetoothMediaManager(Context context, LocalBluetoothManager localBluetoothManager,
            Notification notification) {
        super(context, notification);
@@ -62,6 +68,18 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
        mLocalBluetoothManager.getEventManager().registerCallback(this);
        buildBluetoothDeviceList();
        dispatchDeviceListAdded();

        // The profile may not ready when calling startScan().
        // Device status are all disconnected since profiles are not ready to connected.
        // In this case, we observe all devices in CachedDeviceManager.
        // When one of these device is connected to profile, will call buildBluetoothDeviceList()
        // again to find the connected devices.
        if (!mIsA2dpProfileReady && !mIsHearingAidProfileReady) {
            mCachedDevices = mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy();
            for (CachedBluetoothDevice device : mCachedDevices) {
                device.registerCallback(mObserverCallback);
            }
        }
    }

    private void buildBluetoothDeviceList() {
@@ -75,6 +93,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
            Log.w(TAG, "addConnectedA2dpDevices() a2dp profile is null!");
            return;
        }

        final List<BluetoothDevice> devices = a2dpProfile.getConnectedDevices();
        final CachedBluetoothDeviceManager cachedBluetoothDeviceManager =
                mLocalBluetoothManager.getCachedDeviceManager();
@@ -95,6 +114,8 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
                addMediaDevice(cachedDevice);
            }
        }

        mIsA2dpProfileReady = a2dpProfile.isProfileReady();
    }

    private void addConnectedHearingAidDevices() {
@@ -103,6 +124,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
            Log.w(TAG, "addConnectedA2dpDevices() hap profile is null!");
            return;
        }

        final List<Long> devicesHiSyncIds = new ArrayList<>();
        final List<BluetoothDevice> devices = hapProfile.getConnectedDevices();
        final CachedBluetoothDeviceManager cachedBluetoothDeviceManager =
@@ -128,13 +150,14 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
                addMediaDevice(cachedDevice);
            }
        }

        mIsHearingAidProfileReady = hapProfile.isProfileReady();
    }

    private void addMediaDevice(CachedBluetoothDevice cachedDevice) {
        MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(cachedDevice));
        if (mediaDevice == null) {
            mediaDevice = new BluetoothMediaDevice(mContext, cachedDevice);
            cachedDevice.registerCallback(mCachedDeviceCallback);
            mLastAddedDevice = mediaDevice;
            mMediaDevices.add(mediaDevice);
        }
@@ -143,16 +166,6 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
    @Override
    public void stopScan() {
        mLocalBluetoothManager.getEventManager().unregisterCallback(this);
        unregisterCachedDeviceCallback();
    }

    private void unregisterCachedDeviceCallback() {
        for (MediaDevice device : mMediaDevices) {
            if (device instanceof BluetoothMediaDevice) {
                ((BluetoothMediaDevice) device).getCachedDevice()
                        .unregisterCallback(mCachedDeviceCallback);
            }
        }
    }

    @Override
@@ -164,8 +177,6 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
            final List<MediaDevice> removeDevicesList = new ArrayList<>();
            for (MediaDevice device : mMediaDevices) {
                if (device instanceof BluetoothMediaDevice) {
                    ((BluetoothMediaDevice) device).getCachedDevice()
                            .unregisterCallback(mCachedDeviceCallback);
                    removeDevicesList.add(device);
                }
            }
@@ -185,7 +196,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
    private boolean isCachedDeviceConnected(CachedBluetoothDevice cachedDevice) {
        final boolean isConnectedHearingAidDevice = cachedDevice.isConnectedHearingAidDevice();
        final boolean isConnectedA2dpDevice = cachedDevice.isConnectedA2dpDevice();
        Log.d(TAG, "isCachedDeviceConnected() cachedDevice : " + cachedDevice.getName()
        Log.d(TAG, "isCachedDeviceConnected() cachedDevice : " + cachedDevice
                + ", is hearing aid connected : " + isConnectedHearingAidDevice
                + ", is a2dp connected : " + isConnectedA2dpDevice);

@@ -210,7 +221,6 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
    private void removeMediaDevice(CachedBluetoothDevice cachedDevice) {
        final MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(cachedDevice));
        if (mediaDevice != null) {
            cachedDevice.unregisterCallback(mCachedDeviceCallback);
            mLastRemovedDevice = mediaDevice;
            mMediaDevices.remove(mediaDevice);
        }
@@ -226,7 +236,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
    @Override
    public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state,
            int bluetoothProfile) {
        Log.d(TAG, "onProfileConnectionStateChanged() device: " + cachedDevice.getName()
        Log.d(TAG, "onProfileConnectionStateChanged() device: " + cachedDevice
                + ", state: " + state + ", bluetoothProfile: " + bluetoothProfile);

        if (isCachedDeviceConnected(cachedDevice)) {
@@ -240,8 +250,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall

    @Override
    public void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
        Log.d(TAG, "onAclConnectionStateChanged() device: " + cachedDevice.getName()
                + ", state: " + state);
        Log.d(TAG, "onAclConnectionStateChanged() device: " + cachedDevice + ", state: " + state);

        if (isCachedDeviceConnected(cachedDevice)) {
            addMediaDevice(cachedDevice);
@@ -252,10 +261,29 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall
        }
    }

    class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback {
    @Override
    public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
        Log.d(TAG, "onActiveDeviceChanged : device : "
                + activeDevice + ", profile : " + bluetoothProfile);
        if (BluetoothProfile.HEARING_AID == bluetoothProfile
                || BluetoothProfile.A2DP == bluetoothProfile) {
            final String id = activeDevice == null
                    ? PhoneMediaDevice.ID : MediaDeviceUtils.getId(activeDevice);
            dispatchActiveDeviceChanged(id);
        }
    }

    class DeviceProfileNotReadyObserverCallback implements CachedBluetoothDevice.Callback {

        @Override
        public void onDeviceAttributesChanged() {
            dispatchDeviceAttributesChanged();
            if (!mIsA2dpProfileReady && !mIsHearingAidProfileReady) {
                for (CachedBluetoothDevice device : mCachedDevices) {
                    device.unregisterCallback(mObserverCallback);
                }
                buildBluetoothDeviceList();
                dispatchDeviceListAdded();
            }
        }
    }
}
+89 −0
Original line number Diff line number Diff line
/*
 * Copyright 2018 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.settingslib.media;

import android.content.Context;
import android.content.SharedPreferences;

/**
 * ConnectionRecordManager represents the sharedPreferences operation on device usage record
 */
public class ConnectionRecordManager {
    private static final Object sInstanceSync = new Object();
    private static final String KEY_LAST_SELECTED_DEVICE = "last_selected_device";
    private static final String SHARED_PREFERENCES_NAME = "seamless_transfer_record";
    private static final String TAG = "ConnectionRecordManager";
    private static ConnectionRecordManager sInstance;

    private String mLastSelectedDevice;

    /**
     * Get an {@code ConnectionRecordManager} instance (create one if necessary).
     */
    public static ConnectionRecordManager getInstance() {
        synchronized (sInstanceSync) {
            if (sInstance == null) {
                sInstance = new ConnectionRecordManager();
            }
        }
        return sInstance;
    }

    private SharedPreferences getSharedPreferences(Context context) {
        return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
    }

    /**
     * Get connection record from sharedPreferences
     *
     * @param id a unique device Id
     * @return the the usage result
     */
    public synchronized int fetchConnectionRecord(Context context, String id) {
        return getSharedPreferences(context).getInt(id, 0);
    }

    /**
     * Get the last selected device from sharedPreferences
     */
    public synchronized void fetchLastSelectedDevice(Context context) {
        mLastSelectedDevice = getSharedPreferences(context).getString(KEY_LAST_SELECTED_DEVICE,
                null);
    }

    /**
     * Set device usage time and last selected device in sharedPreference
     *
     * @param id a unique device Id
     * @param record usage times
     */
    public synchronized void setConnectionRecord(Context context, String id, int record) {
        final SharedPreferences.Editor editor = getSharedPreferences(context).edit();
        // Update used times
        mLastSelectedDevice = id;
        editor.putInt(mLastSelectedDevice, record);
        // Update last used device
        editor.putString(KEY_LAST_SELECTED_DEVICE, mLastSelectedDevice);
        editor.apply();
    }

    /**
     * @return the last selected device
     */
    public synchronized String getLastSelectedDevice() {
        return mLastSelectedDevice;
    }
}
+11 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@
package com.android.settingslib.media;

import android.content.Context;
import android.widget.Toast;

import androidx.mediarouter.media.MediaRouter;

@@ -33,6 +34,7 @@ public class InfoMediaDevice extends MediaDevice {
    InfoMediaDevice(Context context, MediaRouter.RouteInfo info) {
        super(context, MediaDeviceType.TYPE_CAST_DEVICE);
        mRouteInfo = info;
        initDeviceRecord();
    }

    @Override
@@ -51,15 +53,23 @@ public class InfoMediaDevice extends MediaDevice {
        return MediaDeviceUtils.getId(mRouteInfo);
    }

    @Override
    public void notifyConnectedChanged() {
        //TODO(b/117129183): check mIsConnected state
    }

    @Override
    public void connect() {
        //TODO(b/117129183): use MediaController2 to transfer media
        mIsConnected = true;
        super.connect();
        //mIsConnected = true;
        Toast.makeText(mContext, "This is cast device !", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void disconnect() {
        //TODO(b/117129183): disconnected last select device
        mIsConnected = false;
        //mIsConnected = false;
    }
}
+53 −14
Original line number Diff line number Diff line
@@ -28,15 +28,20 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * LocalMediaManager provide interface to get MediaDevice list and transfer media to MediaDevice.
 */
public class LocalMediaManager implements BluetoothCallback {

    private static final Comparator<MediaDevice> COMPARATOR = Comparator.naturalOrder();
    private static final String TAG = "LocalMediaManager";

    public static final String NOTIFICATION_EXTRA = "notification_extra";
    public static final String NOTIFICATION_PACKAGE_NAME = "notification_package_name";

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({MediaDeviceState.STATE_CONNECTED,
            MediaDeviceState.STATE_CONNECTING,
@@ -56,7 +61,6 @@ public class LocalMediaManager implements BluetoothCallback {
    private InfoMediaManager mInfoMediaManager;

    private LocalBluetoothManager mLocalBluetoothManager;
    private MediaDevice mLastConnectedDevice;
    private MediaDevice mPhoneDevice;

    /**
@@ -96,29 +100,43 @@ public class LocalMediaManager implements BluetoothCallback {
     * @param connectDevice the MediaDevice
     */
    public void connectDevice(MediaDevice connectDevice) {
        if (connectDevice == mLastConnectedDevice) {
        final MediaDevice currentDevice = getCurrentConnectedDevice();
        final MediaDevice device =
                MediaDeviceUtils.findMediaDevice(mMediaDevices, connectDevice.getId());
        if (device != null && currentDevice != null
                && device.getId().equals(currentDevice.getId())) {
            return;
        }

        if (mLastConnectedDevice != null) {
            mLastConnectedDevice.disconnect();
        //TODO(b/117129183): For demo, will remove check connectDevice is InfoMediaDevice.
        if (currentDevice != null && !(connectDevice instanceof InfoMediaDevice)) {
            currentDevice.disconnect();
        }

        connectDevice.connect();
        if (connectDevice.isConnected()) {
            mLastConnectedDevice = connectDevice;
        }
        device.connect();

        final int state = connectDevice.isConnected()
        final int state = device.isConnected()
                ? MediaDeviceState.STATE_CONNECTED
                : MediaDeviceState.STATE_DISCONNECTED;
        dispatchSelectedDeviceStateChanged(connectDevice, state);
        dispatchSelectedDeviceStateChanged(mMediaDevices, device, state);
    }

    void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) {
    private MediaDevice getCurrentConnectedDevice() {
        for (MediaDevice device : mMediaDevices) {
            if (device.isConnected()) {
                return device;
            }
        }
        Log.w(TAG, "getCurrentConnectedDevice() cannot find current connected device !");
        return null;
    }

    void dispatchSelectedDeviceStateChanged(List<MediaDevice> mMediaDevices, MediaDevice device,
            @MediaDeviceState int state) {
        synchronized (mCallbacks) {
            for (DeviceCallback callback : mCallbacks) {
                callback.onSelectedDeviceStateChanged(device, state);
                callback.onSelectedDeviceStateChanged(new ArrayList<>(mMediaDevices), device,
                        state);
            }
        }
    }
@@ -153,6 +171,7 @@ public class LocalMediaManager implements BluetoothCallback {

    void dispatchDeviceListUpdate() {
        synchronized (mCallbacks) {
            Collections.sort(mMediaDevices, COMPARATOR);
            for (DeviceCallback callback : mCallbacks) {
                callback.onDeviceListUpdate(new ArrayList<>(mMediaDevices));
            }
@@ -206,6 +225,25 @@ public class LocalMediaManager implements BluetoothCallback {
        public void onDeviceAttributesChanged() {
            dispatchDeviceListUpdate();
        }

        @Override
        public void onActiveDeviceChanged(String id) {
            final MediaDevice currentDevice = getCurrentConnectedDevice();
            final MediaDevice connectDevice = MediaDeviceUtils.findMediaDevice(mMediaDevices, id);

            if (connectDevice != null && currentDevice != null
                    && connectDevice.getId().equals(currentDevice.getId())) {
                return;
            }
            if (currentDevice != null) {
                currentDevice.notifyConnectedChanged();
            }
            if (connectDevice != null) {
                connectDevice.notifyConnectedChanged();
            }

            dispatchDeviceListUpdate();
        }
    }


@@ -229,6 +267,7 @@ public class LocalMediaManager implements BluetoothCallback {
         * {@link MediaDeviceState#STATE_CONNECTING},
         * {@link MediaDeviceState#STATE_DISCONNECTED}
         */
        void onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state);
        void onSelectedDeviceStateChanged(List<MediaDevice> devices, MediaDevice device,
                @MediaDeviceState int state);
    }
}
Loading