Update output chooser for calls

- Hide media routes while in call
- Change the title while in call
- Add a title while not in call, too

Bug: 63096355
Test: manual
Change-Id: Ib7bcd314efb010fe903327dd4d9bdbfe521c1f73
This commit is contained in:
Julia Reynolds
2017-12-20 09:53:59 -05:00
parent 4f9750c42a
commit 5daa47270b
9 changed files with 295 additions and 56 deletions

View File

@@ -14,7 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- extends FrameLayout -->
<!-- extends LinearLayout -->
<com.android.systemui.volume.OutputChooserLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:sysui="http://schemas.android.com/apk/res-auto"
@@ -23,8 +23,17 @@
android:minHeight="320dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.QS.DetailHeader"
android:layout_marginBottom="20dp" />
<com.android.systemui.qs.AutoSizingList
android:id="@android:id/list"
android:layout_width="match_parent"
@@ -42,9 +51,10 @@
android:orientation="vertical">
<TextView
android:id="@android:id/title"
android:id="@+id/empty_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textDirection="locale"
android:layout_marginTop="20dp"
android:textAppearance="@style/TextAppearance.QS.DetailEmpty"/>
</LinearLayout>

View File

@@ -30,6 +30,7 @@
android:layout_height="@dimen/qs_detail_item_icon_size"
android:layout_marginStart="@dimen/qs_detail_item_icon_marginStart"
android:layout_marginEnd="@dimen/qs_detail_item_icon_marginEnd"
android:background="?android:selectableItemBackgroundBorderless"
android:tint="?android:attr/textColorPrimary"/>
<LinearLayout

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 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.systemui.volume;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import java.util.List;
/**
* Wrapper for final class MediaRouter, for testing.
*/
public class MediaRouterWrapper {
private final MediaRouter mRouter;
public MediaRouterWrapper(MediaRouter router)
{
mRouter = router;
}
public void addCallback(MediaRouteSelector selector, MediaRouter.Callback callback, int flags) {
mRouter.addCallback(selector, callback, flags);
}
public void removeCallback(MediaRouter.Callback callback) {
mRouter.removeCallback(callback);
}
public void unselect(int reason) {
mRouter.unselect(reason);
}
public List<MediaRouter.RouteInfo> getRoutes() {
return mRouter.getRoutes();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2015 The Android Open Source Project
* Copyright (C) 2017 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.
@@ -40,6 +40,7 @@ import android.os.SystemClock;
import android.support.v7.media.MediaControlIntent;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.telecom.TelecomManager;
import android.util.Log;
import android.util.Pair;
@@ -54,7 +55,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -65,15 +65,17 @@ public class OutputChooserDialog extends SystemUIDialog
private static final int MAX_DEVICES = 10;
private static final long UPDATE_DELAY_MS = 300L;
static final int MSG_UPDATE_ITEMS = 1;
private static final int MSG_UPDATE_ITEMS = 1;
private final Context mContext;
private final BluetoothController mController;
private final WifiManager mWifiManager;
private final BluetoothController mBluetoothController;
private WifiManager mWifiManager;
private OutputChooserLayout mView;
private final MediaRouter mRouter;
private final MediaRouterWrapper mRouter;
private final MediaRouterCallback mRouterCallback;
private long mLastUpdateTime;
private boolean mIsInCall;
protected boolean isAttached;
private final MediaRouteSelector mRouteSelector;
private Drawable mDefaultIcon;
@@ -81,12 +83,14 @@ public class OutputChooserDialog extends SystemUIDialog
private Drawable mSpeakerIcon;
private Drawable mSpeakerGroupIcon;
public OutputChooserDialog(Context context) {
public OutputChooserDialog(Context context, MediaRouterWrapper router) {
super(context);
mContext = context;
mController = Dependency.get(BluetoothController.class);
mBluetoothController = Dependency.get(BluetoothController.class);
mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
mRouter = MediaRouter.getInstance(context);
TelecomManager tm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
mIsInCall = tm.isInCall();
mRouter = router;
mRouterCallback = new MediaRouterCallback();
mRouteSelector = new MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
@@ -96,27 +100,38 @@ public class OutputChooserDialog extends SystemUIDialog
context.registerReceiver(mReceiver, filter);
}
protected void setIsInCall(boolean inCall) {
mIsInCall = inCall;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.output_chooser);
setCanceledOnTouchOutside(true);
setOnDismissListener(this::onDismiss);
setTitle(R.string.output_title);
mView = findViewById(R.id.output_chooser);
mView.setCallback(this);
if (mIsInCall) {
mView.setTitle(R.string.output_calls_title);
} else {
mView.setTitle(R.string.output_title);
}
mDefaultIcon = mContext.getDrawable(R.drawable.ic_cast);
mTvIcon = mContext.getDrawable(R.drawable.ic_tv);
mSpeakerIcon = mContext.getDrawable(R.drawable.ic_speaker);
mSpeakerGroupIcon = mContext.getDrawable(R.drawable.ic_speaker_group);
final boolean wifiOff = !mWifiManager.isWifiEnabled();
final boolean btOff = !mController.isBluetoothEnabled();
if (wifiOff || btOff) {
final boolean btOff = !mBluetoothController.isBluetoothEnabled();
if (wifiOff && btOff) {
mView.setEmptyState(getDisabledServicesMessage(wifiOff, btOff));
}
// time out after 5 seconds
mView.postDelayed(() -> updateItems(true), 5000);
}
protected void cleanUp() {}
@@ -131,15 +146,19 @@ public class OutputChooserDialog extends SystemUIDialog
public void onAttachedToWindow() {
super.onAttachedToWindow();
mRouter.addCallback(mRouteSelector, mRouterCallback,
MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
mController.addCallback(mCallback);
if (!mIsInCall) {
mRouter.addCallback(mRouteSelector, mRouterCallback,
MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
}
mBluetoothController.addCallback(mCallback);
isAttached = true;
}
@Override
public void onDetachedFromWindow() {
isAttached = false;
mRouter.removeCallback(mRouterCallback);
mController.removeCallback(mCallback);
mBluetoothController.removeCallback(mCallback);
super.onDetachedFromWindow();
}
@@ -154,9 +173,8 @@ public class OutputChooserDialog extends SystemUIDialog
if (item == null || item.tag == null) return;
if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) {
final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag;
if (device != null && device.getMaxConnectionState()
== BluetoothProfile.STATE_DISCONNECTED) {
mController.connect(device);
if (device.getMaxConnectionState() == BluetoothProfile.STATE_DISCONNECTED) {
mBluetoothController.connect(device);
}
} else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) {
final MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.tag;
@@ -171,18 +189,16 @@ public class OutputChooserDialog extends SystemUIDialog
if (item == null || item.tag == null) return;
if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) {
final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag;
if (device != null) {
mController.disconnect(device);
}
mBluetoothController.disconnect(device);
} else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) {
mRouter.unselect(UNSELECT_REASON_DISCONNECTED);
}
}
private void updateItems() {
private void updateItems(boolean timeout) {
if (SystemClock.uptimeMillis() - mLastUpdateTime < UPDATE_DELAY_MS) {
mHandler.removeMessages(MSG_UPDATE_ITEMS);
mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ITEMS),
mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ITEMS, timeout),
mLastUpdateTime + UPDATE_DELAY_MS);
return;
}
@@ -194,14 +210,16 @@ public class OutputChooserDialog extends SystemUIDialog
addBluetoothDevices(items);
// Add remote displays
addRemoteDisplayRoutes(items);
if (!mIsInCall) {
addRemoteDisplayRoutes(items);
}
Collections.sort(items, ItemComparator.sInstance);
items.sort(ItemComparator.sInstance);
if (items.size() == 0) {
if (items.size() == 0 && timeout) {
String emptyMessage = mContext.getString(R.string.output_none_found);
final boolean wifiOff = !mWifiManager.isWifiEnabled();
final boolean btOff = !mController.isBluetoothEnabled();
final boolean btOff = !mBluetoothController.isBluetoothEnabled();
if (wifiOff || btOff) {
emptyMessage = getDisabledServicesMessage(wifiOff, btOff);
}
@@ -219,12 +237,12 @@ public class OutputChooserDialog extends SystemUIDialog
}
private void addBluetoothDevices(List<OutputChooserLayout.Item> items) {
final Collection<CachedBluetoothDevice> devices = mController.getDevices();
final Collection<CachedBluetoothDevice> devices = mBluetoothController.getDevices();
if (devices != null) {
int connectedDevices = 0;
int count = 0;
for (CachedBluetoothDevice device : devices) {
if (mController.getBondState(device) == BluetoothDevice.BOND_NONE) continue;
if (mBluetoothController.getBondState(device) == BluetoothDevice.BOND_NONE) continue;
final int majorClass = device.getBtClass().getMajorDeviceClass();
if (majorClass != BluetoothClass.Device.Major.AUDIO_VIDEO
&& majorClass != BluetoothClass.Device.Major.UNCATEGORIZED) {
@@ -328,22 +346,22 @@ public class OutputChooserDialog extends SystemUIDialog
private final class MediaRouterCallback extends MediaRouter.Callback {
@Override
public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
updateItems();
updateItems(false);
}
@Override
public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
updateItems();
updateItems(false);
}
@Override
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
updateItems();
updateItems(false);
}
@Override
public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
dismiss();
updateItems(false);
}
}
@@ -361,12 +379,12 @@ public class OutputChooserDialog extends SystemUIDialog
private final BluetoothController.Callback mCallback = new BluetoothController.Callback() {
@Override
public void onBluetoothStateChange(boolean enabled) {
updateItems();
updateItems(false);
}
@Override
public void onBluetoothDevicesChanged() {
updateItems();
updateItems(false);
}
};
@@ -393,7 +411,7 @@ public class OutputChooserDialog extends SystemUIDialog
public void handleMessage(Message message) {
switch (message.what) {
case MSG_UPDATE_ITEMS:
updateItems();
updateItems((Boolean) message.obj);
break;
}
}

View File

@@ -29,8 +29,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.systemui.FontSizeUtils;
@@ -40,11 +40,10 @@ import com.android.systemui.qs.AutoSizingList;
/**
* Limited height list of devices.
*/
public class OutputChooserLayout extends FrameLayout {
public class OutputChooserLayout extends LinearLayout {
private static final String TAG = "OutputChooserLayout";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final int mQsDetailIconOverlaySize;
private final Context mContext;
private final H mHandler = new H();
private final Adapter mAdapter = new Adapter();
@@ -55,6 +54,7 @@ public class OutputChooserLayout extends FrameLayout {
private AutoSizingList mItemList;
private View mEmpty;
private TextView mEmptyText;
private TextView mTitle;
private Item[] mItems;
@@ -62,8 +62,6 @@ public class OutputChooserLayout extends FrameLayout {
super(context, attrs);
mContext = context;
mTag = TAG;
mQsDetailIconOverlaySize = (int) getResources().getDimension(
R.dimen.qs_detail_icon_overlay_size);
}
@Override
@@ -74,7 +72,8 @@ public class OutputChooserLayout extends FrameLayout {
mItemList.setAdapter(mAdapter);
mEmpty = findViewById(android.R.id.empty);
mEmpty.setVisibility(GONE);
mEmptyText = mEmpty.findViewById(android.R.id.title);
mEmptyText = mEmpty.findViewById(R.id.empty_text);
mTitle = findViewById(R.id.title);
}
@Override
@@ -84,17 +83,21 @@ public class OutputChooserLayout extends FrameLayout {
int count = mItemList.getChildCount();
for (int i = 0; i < count; i++) {
View item = mItemList.getChildAt(i);
FontSizeUtils.updateFontSize(item, android.R.id.title,
FontSizeUtils.updateFontSize(item, R.id.empty_text,
R.dimen.qs_detail_item_primary_text_size);
FontSizeUtils.updateFontSize(item, android.R.id.summary,
R.dimen.qs_detail_item_secondary_text_size);
FontSizeUtils.updateFontSize(item, android.R.id.title,
R.dimen.qs_detail_header_text_size);
}
}
public void setTitle(int title) {
mTitle.setText(title);
}
public void setEmptyState(String text) {
mEmpty.post(() -> {
mEmptyText.setText(text);
});
mEmptyText.setText(text);
}
@Override
@@ -176,11 +179,6 @@ public class OutputChooserLayout extends FrameLayout {
} else {
iv.setImageResource(item.iconResId);
}
iv.getOverlay().clear();
if (item.overlay != null) {
item.overlay.setBounds(0, 0, mQsDetailIconOverlaySize, mQsDetailIconOverlaySize);
iv.getOverlay().add(item.overlay);
}
final TextView title = view.findViewById(android.R.id.title);
title.setText(item.line1);
final TextView summary = view.findViewById(android.R.id.summary);

View File

@@ -47,6 +47,7 @@ import android.os.Message;
import android.os.SystemClock;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.support.v7.media.MediaRouter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseBooleanArray;
@@ -858,7 +859,8 @@ public class VolumeDialogImpl implements VolumeDialog {
if (mOutputChooserDialog != null) {
return;
}
mOutputChooserDialog = new OutputChooserDialog(mContext) {
mOutputChooserDialog = new OutputChooserDialog(mContext,
new MediaRouterWrapper(MediaRouter.getInstance(mContext))) {
@Override
protected void cleanUp() {
synchronized (mOutputChooserLock) {

View File

@@ -46,6 +46,7 @@
<uses-permission android:name="android.permission.STATUS_BAR" />
<uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" />
<uses-permission android:name="android.permission.REAL_GET_TASKS" />
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
<application>
<uses-library android:name="android.test.runner" />

View File

@@ -83,7 +83,7 @@ public abstract class SysuiTestCase {
return null;
}
public Context getContext() {
public SysuiTestableContext getContext() {
return mContext;
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 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.systemui.volume;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothProfile;
import android.net.wifi.WifiManager;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v7.media.MediaRouter;
import android.telecom.TelecomManager;
import android.widget.TextView;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.policy.BluetoothController;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class OutputChooserDialogTest extends SysuiTestCase {
OutputChooserDialog mDialog;
@Mock
private BluetoothController mController;
@Mock
private WifiManager mWifiManager;
@Mock
private TelecomManager mTelecomManager;
@Mock
private MediaRouterWrapper mRouter;
@Before
@UiThreadTest
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
mController = mDependency.injectMockDependency(BluetoothController.class);
when(mWifiManager.isWifiEnabled()).thenReturn(true);
getContext().addMockSystemService(WifiManager.class, mWifiManager);
getContext().addMockSystemService(TelecomManager.class, mTelecomManager);
mDialog = new OutputChooserDialog(getContext(), mRouter);
}
@After
@UiThreadTest
public void tearDown() throws Exception {
mDialog.dismiss();
}
private void showDialog() {
mDialog.show();
}
@Test
@UiThreadTest
public void testClickMediaRouterItemConnectsMedia() {
showDialog();
OutputChooserLayout.Item item = new OutputChooserLayout.Item();
item.deviceType = OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER;
MediaRouter.RouteInfo info = mock(MediaRouter.RouteInfo.class);
when(info.isEnabled()).thenReturn(true);
item.tag = info;
mDialog.onDetailItemClick(item);
verify(info, times(1)).select();
verify(mController, never()).connect(any());
}
@Test
@UiThreadTest
public void testClickBtItemConnectsBt() {
showDialog();
OutputChooserLayout.Item item = new OutputChooserLayout.Item();
item.deviceType = OutputChooserLayout.Item.DEVICE_TYPE_BT;
CachedBluetoothDevice btDevice = mock(CachedBluetoothDevice.class);
when(btDevice.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_DISCONNECTED);
item.tag = btDevice;
mDialog.onDetailItemClick(item);
verify(mController, times(1)).connect(any());
}
@Test
@UiThreadTest
public void testTitleNotInCall() {
showDialog();
assertTrue(((TextView) mDialog.findViewById(R.id.title))
.getText().toString().contains("Media"));
}
@Test
@UiThreadTest
public void testTitleInCall() {
mDialog.setIsInCall(true);
showDialog();
assertTrue(((TextView) mDialog.findViewById(R.id.title))
.getText().toString().contains("Phone"));
}
@Test
@UiThreadTest
public void testNoMediaScanIfInCall() {
mDialog.setIsInCall(true);
mDialog.onAttachedToWindow();
verify(mRouter, never()).addCallback(any(), any(), anyInt());
}
@Test
@UiThreadTest
public void testMediaScanIfNotInCall() {
mDialog.setIsInCall(false);
mDialog.onAttachedToWindow();
verify(mRouter, times(1)).addCallback(any(), any(), anyInt());
}
}