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

Commit eef372d1 authored by timhypeng's avatar timhypeng
Browse files

Add Media Output Dialog for Output Switcher

-Add MediaOutputBaseDialog to provide common method for different media operations UI
-Add MediaOutputDialog for showing Bluetooth device
-Add resources for background image, style and layout
-Add MediaOutputBaseDialogTest for unit test

Bug: 155822415
Test: atest MediaOutputBaseDialogTest
Change-Id: I3086a4049f240870ca1ad870946d6848e500b561
parent 10ab47af
Loading
Loading
Loading
Loading
+23 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ 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.
  -->

<inset xmlns:android="http://schemas.android.com/apk/res/android">
    <shape android:shape="rectangle">
        <corners android:radius="8dp" />
        <solid android:color="?android:attr/colorBackground" />
    </shape>
</inset>
+136 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ 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.
  -->

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/media_output_dialog"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="94dp"
        android:gravity="start|center_vertical"
        android:paddingStart="16dp"
        android:orientation="horizontal">
        <ImageView
            android:id="@+id/header_icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingEnd="16dp"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:orientation="vertical">
            <TextView
                android:id="@+id/header_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:maxLines="1"
                android:textColor="?android:attr/textColorPrimary"
                android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
                android:textSize="20sp"/>

            <TextView
                android:id="@+id/header_subtitle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:maxLines="1"
                android:fontFamily="roboto-regular"
                android:textSize="14sp"/>

        </LinearLayout>
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="?android:attr/listDivider"/>

    <LinearLayout
        android:id="@+id/device_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="start|center_vertical"
        android:orientation="vertical">

        <View
            android:layout_width="match_parent"
            android:layout_height="12dp"/>

        <include
            layout="@layout/media_output_list_item"
            android:id="@+id/group_item_controller"
            android:visibility="gone"/>

        <View
            android:id="@+id/group_item_divider"
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="?android:attr/listDivider"
            android:visibility="gone"/>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/list_result"
            android:scrollbars="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:overScrollMode="never"/>

        <View
            android:id="@+id/list_bottom_padding"
            android:layout_width="match_parent"
            android:layout_height="12dp"/>
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="?android:attr/listDivider"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/stop"
            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
            android:layout_width="wrap_content"
            android:layout_height="64dp"
            android:text="@string/keyboard_key_media_stop"
            android:visibility="gone"/>

        <Space
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="match_parent"/>

        <Button
            android:id="@+id/done"
            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
            android:layout_width="wrap_content"
            android:layout_height="64dp"
            android:layout_marginEnd="0dp"
            android:text="@string/inline_done_button"/>
    </LinearLayout>
</LinearLayout>
 No newline at end of file
+4 −1
Original line number Diff line number Diff line
@@ -388,6 +388,10 @@
        <item name="android:windowIsFloating">true</item>
    </style>

    <style name="Theme.SystemUI.Dialog.MediaOutput">
        <item name="android:windowBackground">@drawable/media_output_dialog_background</item>
    </style>

    <style name="QSBorderlessButton">
        <item name="android:padding">12dp</item>
        <item name="android:background">@drawable/qs_btn_borderless_rect</item>
@@ -735,5 +739,4 @@
          * Title: headline, medium 20sp
          * Message: body, 16 sp -->
    <style name="Theme.ControlsRequestDialog" parent="@*android:style/Theme.DeviceDefault.Dialog.Alert"/>

</resources>
+220 −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 static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;

import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.android.settingslib.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;

/**
 * Base dialog for media output UI
 */
public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
        MediaOutputController.Callback {

    private static final String TAG = "MediaOutputDialog";

    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
    private final RecyclerView.LayoutManager mLayoutManager;

    final Context mContext;
    final MediaOutputController mMediaOutputController;

    @VisibleForTesting
    View mDialogView;
    private TextView mHeaderTitle;
    private TextView mHeaderSubtitle;
    private ImageView mHeaderIcon;
    private RecyclerView mDevicesRecyclerView;
    private LinearLayout mDeviceListLayout;
    private Button mDoneButton;
    private Button mStopButton;
    private View mListBottomPadding;
    private int mListMaxHeight;

    MediaOutputBaseAdapter mAdapter;
    FrameLayout mGroupItemController;
    View mGroupDivider;

    private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
        // Set max height for list
        if (mDeviceListLayout.getHeight() > mListMaxHeight) {
            ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
            params.height = mListMaxHeight;
            mDeviceListLayout.setLayoutParams(params);
        }
    };

    public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
        super(context, R.style.Theme_SystemUI_Dialog_MediaOutput);
        mContext = context;
        mMediaOutputController = mediaOutputController;
        mLayoutManager = new LinearLayoutManager(mContext);
        mListMaxHeight = context.getResources().getDimensionPixelSize(
                R.dimen.media_output_dialog_list_max_height);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null);
        final Window window = getWindow();
        final WindowManager.LayoutParams lp = window.getAttributes();
        lp.gravity = Gravity.BOTTOM;
        // Config insets to make sure the layout is above the navigation bar
        lp.setFitInsetsTypes(statusBars() | navigationBars());
        lp.setFitInsetsSides(WindowInsets.Side.all());
        lp.setFitInsetsIgnoringVisibility(true);
        window.setAttributes(lp);
        window.setContentView(mDialogView);
        window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);

        mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
        mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
        mHeaderIcon = mDialogView.requireViewById(R.id.header_icon);
        mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result);
        mGroupItemController = mDialogView.requireViewById(R.id.group_item_controller);
        mGroupDivider = mDialogView.requireViewById(R.id.group_item_divider);
        mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
        mDoneButton = mDialogView.requireViewById(R.id.done);
        mStopButton = mDialogView.requireViewById(R.id.stop);
        mListBottomPadding = mDialogView.requireViewById(R.id.list_bottom_padding);

        mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
                mDeviceListLayoutListener);
        // Init device list
        mDevicesRecyclerView.setLayoutManager(mLayoutManager);
        mDevicesRecyclerView.setAdapter(mAdapter);
        // Init bottom buttons
        mDoneButton.setOnClickListener(v -> dismiss());
        mStopButton.setOnClickListener(v -> {
            mMediaOutputController.releaseSession();
            dismiss();
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        mMediaOutputController.start(this);
    }

    @Override
    public void onStop() {
        super.onStop();
        mMediaOutputController.stop();
    }

    @VisibleForTesting
    void refresh() {
        // Update header icon
        final int iconRes = getHeaderIconRes();
        final IconCompat iconCompat = getHeaderIcon();
        if (iconRes != 0) {
            mHeaderIcon.setVisibility(View.VISIBLE);
            mHeaderIcon.setImageResource(iconRes);
        } else if (iconCompat != null) {
            mHeaderIcon.setVisibility(View.VISIBLE);
            mHeaderIcon.setImageIcon(iconCompat.toIcon(mContext));
        } else {
            mHeaderIcon.setVisibility(View.GONE);
        }
        if (mHeaderIcon.getVisibility() == View.VISIBLE) {
            final int size = getHeaderIconSize();
            mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size, size));
        }
        // Update title and subtitle
        mHeaderTitle.setText(getHeaderText());
        final CharSequence subTitle = getHeaderSubtitle();
        if (TextUtils.isEmpty(subTitle)) {
            mHeaderSubtitle.setVisibility(View.GONE);
            mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
        } else {
            mHeaderSubtitle.setVisibility(View.VISIBLE);
            mHeaderSubtitle.setText(subTitle);
            mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
        }
        if (!mAdapter.isDragging()) {
            mAdapter.notifyDataSetChanged();
        }
        // Add extra padding when device amount is less than 6
        if (mMediaOutputController.getMediaDevices().size() < 6) {
            mListBottomPadding.setVisibility(View.VISIBLE);
        } else {
            mListBottomPadding.setVisibility(View.GONE);
        }
    }

    abstract int getHeaderIconRes();

    abstract IconCompat getHeaderIcon();

    abstract int getHeaderIconSize();

    abstract CharSequence getHeaderText();

    abstract CharSequence getHeaderSubtitle();

    @Override
    public void onMediaChanged() {
        mMainThreadHandler.post(() -> refresh());
    }

    @Override
    public void onMediaStoppedOrPaused() {
        if (isShowing()) {
            dismiss();
        }
    }

    @Override
    public void onRouteChanged() {
        mMainThreadHandler.post(() -> refresh());
    }

    @Override
    public void dismissDialog() {
        dismiss();
    }
}
+77 −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.os.Bundle;
import android.view.View;
import android.view.WindowManager;

import androidx.core.graphics.drawable.IconCompat;

import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;

/**
 * Dialog for media output transferring.
 */
@SysUISingleton
public class MediaOutputDialog extends MediaOutputBaseDialog {

    MediaOutputDialog(Context context, boolean aboveStatusbar, MediaOutputController
            mediaOutputController) {
        super(context, mediaOutputController);
        mAdapter = new MediaOutputAdapter(mMediaOutputController);
        if (!aboveStatusbar) {
            getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
        }
        show();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mGroupItemController.setVisibility(View.GONE);
        mGroupDivider.setVisibility(View.GONE);
    }

    @Override
    int getHeaderIconRes() {
        return 0;
    }

    @Override
    IconCompat getHeaderIcon() {
        return mMediaOutputController.getHeaderIcon();
    }

    @Override
    int getHeaderIconSize() {
        return mContext.getResources().getDimensionPixelSize(
                R.dimen.media_output_dialog_header_album_icon_size);
    }

    @Override
    CharSequence getHeaderText() {
        return mMediaOutputController.getHeaderTitle();
    }

    @Override
    CharSequence getHeaderSubtitle() {
        return mMediaOutputController.getHeaderSubTitle();
    }
}
Loading