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

Commit 6c143511 authored by timhypeng's avatar timhypeng
Browse files

Add controller for Media operation

-Access LocalMediaManger to display avilable output devices information
-Access LocalMediaManger to do media operation, such as volume adjustment,
switching output device, grouping
-Access MediaController to show media content information
-Add MediaOutputControllerTest for unit test

Bug: 155822415
Test: atest MediaOutputControllerTest
Change-Id: I9eb6e3b0a6e584637aecb4132dbc2b138c6d1530
parent 729b60c0
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -1366,4 +1366,11 @@
    <dimen name="config_rounded_mask_size">@*android:dimen/rounded_corner_radius</dimen>
    <dimen name="config_rounded_mask_size_top">@*android:dimen/rounded_corner_radius_top</dimen>
    <dimen name="config_rounded_mask_size_bottom">@*android:dimen/rounded_corner_radius_bottom</dimen>

    <!-- Output switcher panel related dimensions -->
    <dimen name="media_output_dialog_padding_top">11dp</dimen>
    <dimen name="media_output_dialog_list_max_height">364dp</dimen>
    <dimen name="media_output_dialog_header_album_icon_size">52dp</dimen>
    <dimen name="media_output_dialog_header_back_icon_size">36dp</dimen>
    <dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
</resources>
+15 −0
Original line number Diff line number Diff line
@@ -2799,4 +2799,19 @@
    <string name="udfps_hbm_enable_command" translatable="false"></string>
    <!-- Device-specific payload for disabling the high-brightness mode -->
    <string name="udfps_hbm_disable_command" translatable="false"></string>

    <!-- Title for the media output group dialog with media related devices [CHAR LIMIT=50] -->
    <string name="media_output_dialog_add_output">Add outputs</string>
    <!-- Title for the media output slice with group devices [CHAR LIMIT=50] -->
    <string name="media_output_dialog_group">Group</string>
    <!-- Summary for media output group with only one device which is active [CHAR LIMIT=NONE] -->
    <string name="media_output_dialog_single_device">1 device selected</string>
    <!-- Summary for media output group with the active device count [CHAR LIMIT=NONE] -->
    <string name="media_output_dialog_multiple_devices"><xliff:g id="count" example="2">%1$d</xliff:g> devices selected</string>
    <!-- Summary for disconnected status [CHAR LIMIT=50] -->
    <string name="media_output_dialog_disconnected"><xliff:g id="device_name" example="My device">%1$s</xliff:g> (disconnected)</string>
    <!-- Summary for connecting error message [CHAR LIMIT=NONE] -->
    <string name="media_output_dialog_connect_failed">Couldn\'t connect. Try again.</string>
    <!-- Title for pairing item [CHAR LIMIT=60] -->
    <string name="media_output_dialog_pairing_new">Pair new device</string>
</resources>
+445 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.systemui.media.dialog;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.RoutingSessionInfo;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;

import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.Utils;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.InfoMediaManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.phone.ShadeController;

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

import javax.inject.Inject;

/**
 * Controller for media output dialog
 */
public class MediaOutputController implements LocalMediaManager.DeviceCallback{

    private static final String TAG = "MediaOutputController";
    private static final boolean DEBUG = false;

    private final String mPackageName;
    private final Context mContext;
    private final MediaSessionManager mMediaSessionManager;
    private final ShadeController mShadeController;
    private final ActivityStarter mActivityStarter;
    @VisibleForTesting
    final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();

    private MediaController mMediaController;
    @VisibleForTesting
    Callback mCallback;
    @VisibleForTesting
    LocalMediaManager mLocalMediaManager;

    @Inject
    public MediaOutputController(@NonNull Context context, String packageName,
            MediaSessionManager mediaSessionManager, LocalBluetoothManager
            lbm, ShadeController shadeController, ActivityStarter starter) {
        mContext = context;
        mPackageName = packageName;
        mMediaSessionManager = mediaSessionManager;
        mShadeController = shadeController;
        mActivityStarter = starter;
        InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
        mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
    }

    void start(@NonNull Callback cb) {
        mMediaDevices.clear();
        if (!TextUtils.isEmpty(mPackageName)) {
            for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
                if (TextUtils.equals(controller.getPackageName(), mPackageName)) {
                    mMediaController = controller;
                    mMediaController.unregisterCallback(mCb);
                    mMediaController.registerCallback(mCb);
                    break;
                }
            }
        }
        if (mMediaController == null) {
            if (DEBUG) {
                Log.d(TAG, "No media controller for " + mPackageName);
            }
        }
        if (mLocalMediaManager == null) {
            if (DEBUG) {
                Log.d(TAG, "No local media manager " + mPackageName);
            }
            return;
        }
        mCallback = cb;
        mLocalMediaManager.unregisterCallback(this);
        mLocalMediaManager.stopScan();
        mLocalMediaManager.registerCallback(this);
        mLocalMediaManager.startScan();
    }

    void stop() {
        if (mMediaController != null) {
            mMediaController.unregisterCallback(mCb);
        }
        if (mLocalMediaManager != null) {
            mLocalMediaManager.unregisterCallback(this);
            mLocalMediaManager.stopScan();
        }
        mMediaDevices.clear();
    }

    @Override
    public void onDeviceListUpdate(List<MediaDevice> devices) {
        buildMediaDevices(devices);
        mCallback.onRouteChanged();
    }

    @Override
    public void onSelectedDeviceStateChanged(MediaDevice device,
            @LocalMediaManager.MediaDeviceState int state) {
        mCallback.onRouteChanged();
    }

    @Override
    public void onDeviceAttributesChanged() {
        mCallback.onRouteChanged();
    }

    @Override
    public void onRequestFailed(int reason) {
        mCallback.onRouteChanged();
    }

    CharSequence getHeaderTitle() {
        if (mMediaController != null) {
            final MediaMetadata metadata = mMediaController.getMetadata();
            if (metadata != null) {
                return metadata.getDescription().getTitle();
            }
        }
        return mContext.getText(R.string.controls_media_title);
    }

    CharSequence getHeaderSubTitle() {
        if (mMediaController == null) {
            return null;
        }
        final MediaMetadata metadata = mMediaController.getMetadata();
        if (metadata == null) {
            return null;
        }
        return metadata.getDescription().getSubtitle();
    }

    IconCompat getHeaderIcon() {
        if (mMediaController == null) {
            return null;
        }
        final MediaMetadata metadata = mMediaController.getMetadata();
        if (metadata != null) {
            final Bitmap bitmap = metadata.getDescription().getIconBitmap();
            if (bitmap != null) {
                final Bitmap roundBitmap = Utils.convertCornerRadiusBitmap(mContext, bitmap,
                        (float) mContext.getResources().getDimensionPixelSize(
                                R.dimen.media_output_dialog_icon_corner_radius));
                return IconCompat.createWithBitmap(roundBitmap);
            }
        }
        if (DEBUG) {
            Log.d(TAG, "Media meta data does not contain icon information");
        }
        return getPackageIcon();
    }

    IconCompat getDeviceIconCompat(MediaDevice device) {
        Drawable drawable = device.getIcon();
        if (drawable == null) {
            if (DEBUG) {
                Log.d(TAG, "getDeviceIconCompat() device : " + device.getName()
                        + ", drawable is null");
            }
            // Use default Bluetooth device icon to handle getIcon() is null case.
            drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
        }
        return BluetoothUtils.createIconWithDrawable(drawable);
    }

    private IconCompat getPackageIcon() {
        if (TextUtils.isEmpty(mPackageName)) {
            return null;
        }
        try {
            final Drawable drawable = mContext.getPackageManager().getApplicationIcon(mPackageName);
            if (drawable instanceof BitmapDrawable) {
                return IconCompat.createWithBitmap(((BitmapDrawable) drawable).getBitmap());
            }
            final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                    drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            final Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return IconCompat.createWithBitmap(bitmap);
        } catch (PackageManager.NameNotFoundException e) {
            if (DEBUG) {
                Log.e(TAG, "Package is not found. Unable to get package icon.");
            }
        }
        return null;
    }

    private void buildMediaDevices(List<MediaDevice> devices) {
        // For the first time building list, to make sure the top device is the connected device.
        if (mMediaDevices.isEmpty()) {
            final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice();
            if (connectedMediaDevice == null) {
                if (DEBUG) {
                    Log.d(TAG, "No connected media device.");
                }
                mMediaDevices.addAll(devices);
                return;
            }
            for (MediaDevice device : devices) {
                if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
                    mMediaDevices.add(0, device);
                } else {
                    mMediaDevices.add(device);
                }
            }
            return;
        }
        // To keep the same list order
        final Collection<MediaDevice> targetMediaDevices = new ArrayList<>();
        for (MediaDevice originalDevice : mMediaDevices) {
            for (MediaDevice newDevice : devices) {
                if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
                    targetMediaDevices.add(newDevice);
                    break;
                }
            }
        }
        if (targetMediaDevices.size() != devices.size()) {
            devices.removeAll(targetMediaDevices);
            targetMediaDevices.addAll(devices);
        }
        mMediaDevices.clear();
        mMediaDevices.addAll(targetMediaDevices);
    }

    void connectDevice(MediaDevice device) {
        ThreadUtils.postOnBackgroundThread(() -> {
            mLocalMediaManager.connectDevice(device);
        });
    }

    Collection<MediaDevice> getMediaDevices() {
        return mMediaDevices;
    }

    MediaDevice getCurrentConnectedMediaDevice() {
        return mLocalMediaManager.getCurrentConnectedDevice();
    }

    private MediaDevice getMediaDeviceById(String id) {
        return mLocalMediaManager.getMediaDeviceById(new ArrayList<>(mMediaDevices), id);
    }

    boolean addDeviceToPlayMedia(MediaDevice device) {
        return mLocalMediaManager.addDeviceToPlayMedia(device);
    }

    boolean removeDeviceFromPlayMedia(MediaDevice device) {
        return mLocalMediaManager.removeDeviceFromPlayMedia(device);
    }

    List<MediaDevice> getSelectableMediaDevice() {
        return mLocalMediaManager.getSelectableMediaDevice();
    }

    List<MediaDevice> getSelectedMediaDevice() {
        return mLocalMediaManager.getSelectedMediaDevice();
    }

    List<MediaDevice> getDeselectableMediaDevice() {
        return mLocalMediaManager.getDeselectableMediaDevice();
    }

    boolean isDeviceIncluded(Collection<MediaDevice> deviceCollection, MediaDevice targetDevice) {
        for (MediaDevice device : deviceCollection) {
            if (TextUtils.equals(device.getId(), targetDevice.getId())) {
                return true;
            }
        }
        return false;
    }

    void adjustSessionVolume(String sessionId, int volume) {
        mLocalMediaManager.adjustSessionVolume(sessionId, volume);
    }

    void adjustSessionVolume(int volume) {
        mLocalMediaManager.adjustSessionVolume(volume);
    }

    int getSessionVolumeMax() {
        return mLocalMediaManager.getSessionVolumeMax();
    }

    int getSessionVolume() {
        return mLocalMediaManager.getSessionVolume();
    }

    CharSequence getSessionName() {
        return mLocalMediaManager.getSessionName();
    }

    void releaseSession() {
        mLocalMediaManager.releaseSession();
    }

    List<RoutingSessionInfo> getActiveRemoteMediaDevices() {
        final List<RoutingSessionInfo> sessionInfos = new ArrayList<>();
        for (RoutingSessionInfo info : mLocalMediaManager.getActiveMediaSession()) {
            if (!info.isSystemSession()) {
                sessionInfos.add(info);
            }
        }
        return sessionInfos;
    }

    void adjustVolume(MediaDevice device, int volume) {
        ThreadUtils.postOnBackgroundThread(() -> {
            device.requestSetVolume(volume);
        });
    }

    String getPackageName() {
        return mPackageName;
    }

    boolean hasAdjustVolumeUserRestriction() {
        if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
                mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) {
            return true;
        }
        final UserManager um = mContext.getSystemService(UserManager.class);
        return um.hasBaseUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME,
                UserHandle.of(UserHandle.myUserId()));
    }

    boolean isTransferring() {
        for (MediaDevice device : mMediaDevices) {
            if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
                return true;
            }
        }
        return false;
    }

    boolean isZeroMode() {
        if (mMediaDevices.size() == 1) {
            final MediaDevice device = mMediaDevices.iterator().next();
            // Add "pair new" only when local output device exists
            final int type = device.getDeviceType();
            if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE
                    || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
                    || type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) {
                return true;
            }
        }
        return false;
    }

    void launchBluetoothPairing() {
        mCallback.dismissDialog();
        final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
            mContext.sendBroadcast(new Intent()
                    .setAction(MediaOutputSliceConstants.ACTION_LAUNCH_BLUETOOTH_PAIRING)
                    .setPackage(MediaOutputSliceConstants.SETTINGS_PACKAGE_NAME));
            mShadeController.animateCollapsePanels();
            return true;
        };
        mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
    }

    private final MediaController.Callback mCb = new MediaController.Callback() {
        @Override
        public void onMetadataChanged(MediaMetadata metadata) {
            mCallback.onMediaChanged();
        }

        @Override
        public void onPlaybackStateChanged(PlaybackState playbackState) {
            final int state = playbackState.getState();
            if (state == PlaybackState.STATE_STOPPED || state == PlaybackState.STATE_PAUSED) {
                mCallback.onMediaStoppedOrPaused();
            }
        }
    };

    interface Callback {
        /**
         * Override to handle the media content updating.
         */
        void onMediaChanged();

        /**
         * Override to handle the media state updating.
         */
        void onMediaStoppedOrPaused();

        /**
         * Override to handle the device updating.
         */
        void onRouteChanged();

        /**
         * Override to dismiss dialog.
         */
        void dismissDialog();
    }
}
+348 −0

File added.

Preview size limit exceeded, changes collapsed.