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

Commit a73686fc authored by hughchen's avatar hughchen
Browse files

Implement LocalMediaManager for seamless transfer

Bug: 117129183
Test: Build pass
Change-Id: I388435f928d2be3519993ffcaf41588dc4422ea2
parent b89929aa
Loading
Loading
Loading
Loading
+261 −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.app.Notification;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.util.Log;

import com.android.settingslib.bluetooth.A2dpProfile;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.HearingAidProfile;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;

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

/**
 * BluetoothMediaManager provide interface to get Bluetooth device list.
 */
public class BluetoothMediaManager extends MediaManager implements BluetoothCallback {

    private static final String TAG = "BluetoothMediaManager";

    private final DeviceAttributeChangeCallback mCachedDeviceCallback =
            new DeviceAttributeChangeCallback();

    private LocalBluetoothManager mLocalBluetoothManager;
    private LocalBluetoothProfileManager mProfileManager;

    private MediaDevice mLastAddedDevice;
    private MediaDevice mLastRemovedDevice;

    BluetoothMediaManager(Context context, LocalBluetoothManager localBluetoothManager,
            Notification notification) {
        super(context, notification);

        mLocalBluetoothManager = localBluetoothManager;
        mProfileManager = mLocalBluetoothManager.getProfileManager();
    }

    @Override
    public void startScan() {
        mMediaDevices.clear();
        mLocalBluetoothManager.getEventManager().registerCallback(this);
        buildBluetoothDeviceList();
        dispatchDeviceListAdded();
    }

    private void buildBluetoothDeviceList() {
        addConnectedA2dpDevices();
        addConnectedHearingAidDevices();
    }

    private void addConnectedA2dpDevices() {
        final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
        if (a2dpProfile == null) {
            Log.w(TAG, "addConnectedA2dpDevices() a2dp profile is null!");
            return;
        }
        final List<BluetoothDevice> devices = a2dpProfile.getConnectedDevices();
        final CachedBluetoothDeviceManager cachedBluetoothDeviceManager =
                mLocalBluetoothManager.getCachedDeviceManager();

        for (BluetoothDevice device : devices) {
            final CachedBluetoothDevice cachedDevice =
                    cachedBluetoothDeviceManager.findDevice(device);

            if (cachedDevice == null) {
                Log.w(TAG, "Can't found CachedBluetoothDevice : " + device.getName());
                continue;
            }

            Log.d(TAG, "addConnectedA2dpDevices() device : " + cachedDevice.getName()
                    + ", is connected : " + cachedDevice.isConnected());

            if (cachedDevice.isConnected()) {
                addMediaDevice(cachedDevice);
            }
        }
    }

    private void addConnectedHearingAidDevices() {
        final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile();
        if (hapProfile == null) {
            Log.w(TAG, "addConnectedA2dpDevices() hap profile is null!");
            return;
        }
        final List<Long> devicesHiSyncIds = new ArrayList<>();
        final List<BluetoothDevice> devices = hapProfile.getConnectedDevices();
        final CachedBluetoothDeviceManager cachedBluetoothDeviceManager =
                mLocalBluetoothManager.getCachedDeviceManager();

        for (BluetoothDevice device : devices) {
            final CachedBluetoothDevice cachedDevice =
                    cachedBluetoothDeviceManager.findDevice(device);

            if (cachedDevice == null) {
                Log.w(TAG, "Can't found CachedBluetoothDevice : " + device.getName());
                continue;
            }

            Log.d(TAG, "addConnectedHearingAidDevices() device : " + cachedDevice.getName()
                    + ", is connected : " + cachedDevice.isConnected());
            final long hiSyncId = hapProfile.getHiSyncId(device);

            // device with same hiSyncId should not be shown in the UI.
            // So do not add it into connectedDevices.
            if (!devicesHiSyncIds.contains(hiSyncId) && cachedDevice.isConnected()) {
                devicesHiSyncIds.add(hiSyncId);
                addMediaDevice(cachedDevice);
            }
        }
    }

    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);
        }
    }

    @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
    public void onBluetoothStateChanged(int bluetoothState) {
        if (BluetoothAdapter.STATE_ON == bluetoothState) {
            buildBluetoothDeviceList();
            dispatchDeviceListAdded();
        } else if (BluetoothAdapter.STATE_OFF == bluetoothState) {
            final List<MediaDevice> removeDevicesList = new ArrayList<>();
            for (MediaDevice device : mMediaDevices) {
                if (device instanceof BluetoothMediaDevice) {
                    ((BluetoothMediaDevice) device).getCachedDevice()
                            .unregisterCallback(mCachedDeviceCallback);
                    removeDevicesList.add(device);
                }
            }
            mMediaDevices.removeAll(removeDevicesList);
            dispatchDeviceListRemoved(removeDevicesList);
        }
    }

    @Override
    public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
        if (isCachedDeviceConnected(cachedDevice)) {
            addMediaDevice(cachedDevice);
            dispatchDeviceAdded(cachedDevice);
        }
    }

    private boolean isCachedDeviceConnected(CachedBluetoothDevice cachedDevice) {
        final boolean isConnectedHearingAidDevice = cachedDevice.isConnectedHearingAidDevice();
        final boolean isConnectedA2dpDevice = cachedDevice.isConnectedA2dpDevice();
        Log.d(TAG, "isCachedDeviceConnected() cachedDevice : " + cachedDevice.getName()
                + ", is hearing aid connected : " + isConnectedHearingAidDevice
                + ", is a2dp connected : " + isConnectedA2dpDevice);

        return isConnectedHearingAidDevice || isConnectedA2dpDevice;
    }

    private void dispatchDeviceAdded(CachedBluetoothDevice cachedDevice) {
        if (mLastAddedDevice != null
                && MediaDeviceUtils.getId(cachedDevice) == mLastAddedDevice.getId()) {
            dispatchDeviceAdded(mLastAddedDevice);
        }
    }

    @Override
    public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
        if (!isCachedDeviceConnected(cachedDevice)) {
            removeMediaDevice(cachedDevice);
            dispatchDeviceRemoved(cachedDevice);
        }
    }

    private void removeMediaDevice(CachedBluetoothDevice cachedDevice) {
        final MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(cachedDevice));
        if (mediaDevice != null) {
            cachedDevice.unregisterCallback(mCachedDeviceCallback);
            mLastRemovedDevice = mediaDevice;
            mMediaDevices.remove(mediaDevice);
        }
    }

    void dispatchDeviceRemoved(CachedBluetoothDevice cachedDevice) {
        if (mLastRemovedDevice != null
                && MediaDeviceUtils.getId(cachedDevice) == mLastRemovedDevice.getId()) {
            dispatchDeviceRemoved(mLastRemovedDevice);
        }
    }

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

        if (isCachedDeviceConnected(cachedDevice)) {
            addMediaDevice(cachedDevice);
            dispatchDeviceAdded(cachedDevice);
        } else {
            removeMediaDevice(cachedDevice);
            dispatchDeviceRemoved(cachedDevice);
        }
    }

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

        if (isCachedDeviceConnected(cachedDevice)) {
            addMediaDevice(cachedDevice);
            dispatchDeviceAdded(cachedDevice);
        } else {
            removeMediaDevice(cachedDevice);
            dispatchDeviceRemoved(cachedDevice);
        }
    }

    class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback {
        @Override
        public void onDeviceAttributesChanged() {
            dispatchDeviceAttributesChanged();
        }
    }
}
+92 −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.app.Notification;
import android.content.Context;
import android.util.Log;

import androidx.mediarouter.media.MediaRouteSelector;
import androidx.mediarouter.media.MediaRouter;

/**
 * InfoMediaManager provide interface to get InfoMediaDevice list.
 */
public class InfoMediaManager extends MediaManager {

    private static final String TAG = "InfoMediaManager";

    private final MediaRouterCallback mMediaRouterCallback = new MediaRouterCallback();

    private MediaRouter mMediaRouter;
    private String mPackageName;

    InfoMediaManager(Context context, String packageName, Notification notification) {
        super(context, notification);

        mMediaRouter = MediaRouter.getInstance(context);
        mPackageName = packageName;
    }

    @Override
    public void startScan() {
        mMediaDevices.clear();
        startScanCastDevice();
    }

    private void startScanCastDevice() {
        final MediaRouteSelector selector = new MediaRouteSelector.Builder()
                .addControlCategory(getControlCategoryByPackageName(mPackageName))
                .build();

        mMediaRouter.addCallback(selector, mMediaRouterCallback,
                MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
    }

    private String getControlCategoryByPackageName(String packageName) {
        //TODO(b/117129183): Use package name to get ControlCategory.
        //Since api not ready, return fixed ControlCategory for prototype.
        return "com.google.android.gms.cast.CATEGORY_CAST/4F8B3483";
    }

    @Override
    public void stopScan() {
        mMediaRouter.removeCallback(mMediaRouterCallback);
    }

    class MediaRouterCallback extends MediaRouter.Callback {
        @Override
        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
            MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(route));
            if (mediaDevice == null) {
                mediaDevice = new InfoMediaDevice(mContext, route);
                Log.d(TAG, "onRouteAdded() route : " + route.getName());
                mMediaDevices.add(mediaDevice);
                dispatchDeviceAdded(mediaDevice);
            }
        }

        @Override
        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo route) {
            final MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(route));
            if (mediaDevice != null) {
                Log.d(TAG, "onRouteRemoved() route : " + route.getName());
                mMediaDevices.remove(mediaDevice);
                dispatchDeviceRemoved(mediaDevice);
            }
        }
    }
}
+234 −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.app.Notification;
import android.content.Context;
import android.util.Log;

import androidx.annotation.IntDef;

import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.LocalBluetoothManager;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

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

    private static final String TAG = "LocalMediaManager";

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({MediaDeviceState.STATE_CONNECTED,
            MediaDeviceState.STATE_CONNECTING,
            MediaDeviceState.STATE_DISCONNECTED})
    public @interface MediaDeviceState {
        int STATE_CONNECTED = 1;
        int STATE_CONNECTING = 2;
        int STATE_DISCONNECTED = 3;
    }

    private final Collection<DeviceCallback> mCallbacks = new ArrayList<>();
    private final MediaDeviceCallback mMediaDeviceCallback = new MediaDeviceCallback();

    private Context mContext;
    private List<MediaDevice> mMediaDevices = new ArrayList<>();
    private BluetoothMediaManager mBluetoothMediaManager;
    private InfoMediaManager mInfoMediaManager;

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

    /**
     * Register to start receiving callbacks for MediaDevice events.
     */
    public void registerCallback(DeviceCallback callback) {
        synchronized (mCallbacks) {
            mCallbacks.add(callback);
        }
    }

    /**
     * Unregister to stop receiving callbacks for MediaDevice events
     */
    public void unregisterCallback(DeviceCallback callback) {
        synchronized (mCallbacks) {
            mCallbacks.remove(callback);
        }
    }

    public LocalMediaManager(Context context, String packageName, Notification notification) {
        mContext = context;
        mLocalBluetoothManager =
                LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null);
        if (mLocalBluetoothManager == null) {
            Log.e(TAG, "Bluetooth is not supported on this device");
            return;
        }

        mBluetoothMediaManager =
                new BluetoothMediaManager(context, mLocalBluetoothManager, notification);
        mInfoMediaManager = new InfoMediaManager(context, packageName, notification);
    }

    /**
     * Connect the MediaDevice to transfer media
     * @param connectDevice the MediaDevice
     */
    public void connectDevice(MediaDevice connectDevice) {
        if (connectDevice == mLastConnectedDevice) {
            return;
        }

        if (mLastConnectedDevice != null) {
            mLastConnectedDevice.disconnect();
        }

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

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

    void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) {
        synchronized (mCallbacks) {
            for (DeviceCallback callback : mCallbacks) {
                callback.onSelectedDeviceStateChanged(device, state);
            }
        }
    }

    /**
     * Start scan connected MediaDevice
     */
    public void startScan() {
        mMediaDevices.clear();
        mBluetoothMediaManager.registerCallback(mMediaDeviceCallback);
        mInfoMediaManager.registerCallback(mMediaDeviceCallback);
        mBluetoothMediaManager.startScan();
        mInfoMediaManager.startScan();
    }

    private void addPhoneDeviceIfNecessary() {
        // add phone device to list if there have any Bluetooth device and cast device.
        if (mMediaDevices.size() > 0 && !mMediaDevices.contains(mPhoneDevice)) {
            if (mPhoneDevice == null) {
                mPhoneDevice = new PhoneMediaDevice(mContext, mLocalBluetoothManager);
            }
            mMediaDevices.add(mPhoneDevice);
        }
    }

    private void removePhoneMediaDeviceIfNecessary() {
        // if PhoneMediaDevice is the last item in the list, remove it.
        if (mMediaDevices.size() == 1 && mMediaDevices.contains(mPhoneDevice)) {
            mMediaDevices.clear();
        }
    }

    void dispatchDeviceListUpdate() {
        synchronized (mCallbacks) {
            for (DeviceCallback callback : mCallbacks) {
                callback.onDeviceListUpdate(new ArrayList<>(mMediaDevices));
            }
        }
    }

    /**
     * Stop scan MediaDevice
     */
    public void stopScan() {
        mBluetoothMediaManager.unregisterCallback(mMediaDeviceCallback);
        mInfoMediaManager.unregisterCallback(mMediaDeviceCallback);
        mBluetoothMediaManager.stopScan();
        mInfoMediaManager.stopScan();
    }

    class MediaDeviceCallback implements MediaManager.MediaDeviceCallback {
        @Override
        public void onDeviceAdded(MediaDevice device) {
            if (!mMediaDevices.contains(device)) {
                mMediaDevices.add(device);
                addPhoneDeviceIfNecessary();
                dispatchDeviceListUpdate();
            }
        }

        @Override
        public void onDeviceListAdded(List<MediaDevice> devices) {
            mMediaDevices.addAll(devices);
            addPhoneDeviceIfNecessary();
            dispatchDeviceListUpdate();
        }

        @Override
        public void onDeviceRemoved(MediaDevice device) {
            if (mMediaDevices.contains(device)) {
                mMediaDevices.remove(device);
                removePhoneMediaDeviceIfNecessary();
                dispatchDeviceListUpdate();
            }
        }

        @Override
        public void onDeviceListRemoved(List<MediaDevice> devices) {
            mMediaDevices.removeAll(devices);
            removePhoneMediaDeviceIfNecessary();
            dispatchDeviceListUpdate();
        }

        @Override
        public void onDeviceAttributesChanged() {
            dispatchDeviceListUpdate();
        }
    }


    /**
     * Callback for notifying device information updating
     */
    public interface DeviceCallback {
        /**
         * Callback for notifying device list updated.
         *
         * @param devices MediaDevice list
         */
        void onDeviceListUpdate(List<MediaDevice> devices);

        /**
         * Callback for notifying the connected device is changed.
         *
         * @param device the changed connected MediaDevice
         * @param state the current MediaDevice state, the possible values are:
         * {@link MediaDeviceState#STATE_CONNECTED},
         * {@link MediaDeviceState#STATE_CONNECTING},
         * {@link MediaDeviceState#STATE_DISCONNECTED}
         */
        void onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state);
    }
}
+157 −0

File added.

Preview size limit exceeded, changes collapsed.