diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 6acb6cd0a63..8362fd60515 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2983,6 +2983,17 @@ + + + + + + + %1$d notification channels. Tap to manage all. + + Switch output + + Currently playing on %1$s diff --git a/src/com/android/settings/media/MediaDeviceUpdateWorker.java b/src/com/android/settings/media/MediaDeviceUpdateWorker.java new file mode 100644 index 00000000000..7416018f6cd --- /dev/null +++ b/src/com/android/settings/media/MediaDeviceUpdateWorker.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2019 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.settings.media; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; + +import java.util.ArrayList; +import java.util.List; + +/** + * SliceBackgroundWorker for get MediaDevice list and handle MediaDevice state change event. + */ +public class MediaDeviceUpdateWorker extends SliceBackgroundWorker + implements LocalMediaManager.DeviceCallback { + + private final Context mContext; + private final List mMediaDevices = new ArrayList<>(); + + private String mPackageName; + + @VisibleForTesting + LocalMediaManager mLocalMediaManager; + + public MediaDeviceUpdateWorker(Context context, Uri uri) { + super(context, uri); + mContext = context; + } + + public void setPackageName(String packageName) { + mPackageName = packageName; + } + + @Override + protected void onSlicePinned() { + mMediaDevices.clear(); + if (mLocalMediaManager == null) { + mLocalMediaManager = new LocalMediaManager(mContext, mPackageName, null); + } + + mLocalMediaManager.registerCallback(this); + mLocalMediaManager.startScan(); + } + + @Override + protected void onSliceUnpinned() { + mLocalMediaManager.unregisterCallback(this); + mLocalMediaManager.stopScan(); + } + + @Override + public void close() { + + } + + @Override + public void onDeviceListUpdate(List devices) { + buildMediaDevices(devices); + notifySliceChange(); + } + + private void buildMediaDevices(List devices) { + mMediaDevices.clear(); + mMediaDevices.addAll(devices); + } + + @Override + public void onSelectedDeviceStateChanged(MediaDevice device, int state) { + notifySliceChange(); + } + + public List getMediaDevices() { + return new ArrayList<>(mMediaDevices); + } + + public void connectDevice(MediaDevice device) { + mLocalMediaManager.connectDevice(device); + } + + public MediaDevice getMediaDeviceById(String id) { + return mLocalMediaManager.getMediaDeviceById(mMediaDevices, id); + } + + public MediaDevice getCurrentConnectedMediaDevice() { + return mLocalMediaManager.getCurrentConnectedDevice(); + } +} diff --git a/src/com/android/settings/media/MediaOutputSlice.java b/src/com/android/settings/media/MediaOutputSlice.java new file mode 100644 index 00000000000..5c5eb8823c1 --- /dev/null +++ b/src/com/android/settings/media/MediaOutputSlice.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2019 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.settings.media; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI; + +import android.annotation.ColorInt; +import android.app.PendingIntent; +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.Drawable; +import android.net.Uri; +import android.os.UserHandle; +import android.util.IconDrawableFactory; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settings.slices.SliceBroadcastReceiver; +import com.android.settingslib.media.MediaDevice; + +import java.util.List; + +/** + * Show the Media device that can be transfer the media. + */ +public class MediaOutputSlice implements CustomSliceable { + + private static final String TAG = "MediaOutputSlice"; + private static final String MEDIA_DEVICE_ID = "media_device_id"; + + public static final String MEDIA_PACKAGE_NAME = "media_package_name"; + + private final Context mContext; + + private MediaDeviceUpdateWorker mWorker; + private String mPackageName; + private IconDrawableFactory mIconDrawableFactory; + + public MediaOutputSlice(Context context) { + mContext = context; + mPackageName = getUri().getQueryParameter(MEDIA_PACKAGE_NAME); + mIconDrawableFactory = IconDrawableFactory.newInstance(mContext); + } + + @VisibleForTesting + void init(String packageName, MediaDeviceUpdateWorker worker, IconDrawableFactory factory) { + mPackageName = packageName; + mWorker = worker; + mIconDrawableFactory = factory; + } + + @Override + public Slice getSlice() { + final PackageManager pm = mContext.getPackageManager(); + + final List devices = getMediaDevices(); + final CharSequence title = Utils.getApplicationLabel(mContext, mPackageName); + final CharSequence summary = + mContext.getString(R.string.media_output_panel_summary_of_playing_device, + getConnectedDeviceName()); + + final Drawable drawable = + Utils.getBadgedIcon(mIconDrawableFactory, pm, mPackageName, UserHandle.myUserId()); + final IconCompat icon = IconCompat.createWithBitmap(getBitmapFromDrawable(drawable)); + + @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext); + final SliceAction primarySliceAction = SliceAction.createDeeplink(getPrimaryAction(), icon, + ListBuilder.ICON_IMAGE, title); + + final ListBuilder listBuilder = new ListBuilder(mContext, MEDIA_OUTPUT_SLICE_URI, + ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(new ListBuilder.RowBuilder() + .setTitleItem(icon, ListBuilder.ICON_IMAGE) + .setTitle(title) + .setSubtitle(summary) + .setPrimaryAction(primarySliceAction)); + + for (MediaDevice device : devices) { + listBuilder.addRow(getMediaDeviceRow(device)); + } + + return listBuilder.build(); + } + + private MediaDeviceUpdateWorker getWorker() { + if (mWorker == null) { + mWorker = (MediaDeviceUpdateWorker) SliceBackgroundWorker.getInstance(getUri()); + mWorker.setPackageName(mPackageName); + } + return mWorker; + } + + private List getMediaDevices() { + List devices = getWorker().getMediaDevices(); + return devices; + } + + private String getConnectedDeviceName() { + final MediaDevice device = getWorker().getCurrentConnectedMediaDevice(); + return device != null ? device.getName() : ""; + } + + private PendingIntent getPrimaryAction() { + final PackageManager pm = mContext.getPackageManager(); + final Intent launchIntent = pm.getLaunchIntentForPackage(mPackageName); + final Intent intent = launchIntent; + return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); + } + + private Bitmap getBitmapFromDrawable(Drawable drawable) { + 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 bitmap; + } + + private ListBuilder.RowBuilder getMediaDeviceRow(MediaDevice device) { + final String title = device.getName(); + final PendingIntent broadcastAction = + getBroadcastIntent(mContext, device.getId(), device.hashCode()); + final IconCompat deviceIcon = IconCompat.createWithResource(mContext, device.getIcon()); + final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() + .setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE) + .setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon, + ListBuilder.ICON_IMAGE, title)) + .setTitle(title); + + return rowBuilder; + } + + private PendingIntent getBroadcastIntent(Context context, String id, int requestCode) { + final Intent intent = new Intent(getUri().toString()); + intent.setClass(context, SliceBroadcastReceiver.class); + intent.putExtra(MEDIA_DEVICE_ID, id); + return PendingIntent.getBroadcast(context, requestCode /* requestCode */, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + } + + @Override + public Uri getUri() { + return MEDIA_OUTPUT_SLICE_URI; + } + + @Override + public void onNotifyChange(Intent intent) { + final MediaDeviceUpdateWorker worker = getWorker(); + final String id = intent != null ? intent.getStringExtra(MEDIA_DEVICE_ID) : ""; + final MediaDevice device = worker.getMediaDeviceById(id); + if (device != null) { + Log.d(TAG, "onNotifyChange() device name : " + device.getName()); + worker.connectDevice(device); + } + } + + @Override + public Intent getIntent() { + return null; + } + + @Override + public Class getBackgroundWorkerClass() { + return MediaDeviceUpdateWorker.class; + } +} diff --git a/src/com/android/settings/panel/MediaOutputPanel.java b/src/com/android/settings/panel/MediaOutputPanel.java new file mode 100644 index 00000000000..f7639d94a78 --- /dev/null +++ b/src/com/android/settings/panel/MediaOutputPanel.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 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.settings.panel; + +import static com.android.settings.media.MediaOutputSlice.MEDIA_PACKAGE_NAME; +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the Media output Panel. + * + *

+ * Displays Media output item + *

+ */ +public class MediaOutputPanel implements PanelContent { + + private final Context mContext; + private final String mPackageName; + + public static MediaOutputPanel create(Context context, String packageName) { + return new MediaOutputPanel(context, packageName); + } + + private MediaOutputPanel(Context context, String packageName) { + mContext = context.getApplicationContext(); + mPackageName = packageName; + } + + @Override + public CharSequence getTitle() { + return mContext.getText(R.string.media_output_panel_title); + } + + @Override + public List getSlices() { + final List uris = new ArrayList<>(); + MEDIA_OUTPUT_SLICE_URI = + MEDIA_OUTPUT_SLICE_URI + .buildUpon() + .clearQuery() + .appendQueryParameter(MEDIA_PACKAGE_NAME, mPackageName) + .build(); + uris.add(MEDIA_OUTPUT_SLICE_URI); + return uris; + } + + @Override + public Intent getSeeMoreIntent() { + return null; + } +} diff --git a/src/com/android/settings/panel/PanelFeatureProvider.java b/src/com/android/settings/panel/PanelFeatureProvider.java index 7d6c5581d25..5af5ac8979b 100644 --- a/src/com/android/settings/panel/PanelFeatureProvider.java +++ b/src/com/android/settings/panel/PanelFeatureProvider.java @@ -21,7 +21,7 @@ import android.content.Context; public interface PanelFeatureProvider { /** - * Returns {@link PanelContent} as specified by the {@param panelType}. + * Returns {@link PanelContent} as specified by the {@code panelType} and {@code packageName}. */ - PanelContent getPanel(Context context, String panelType); + PanelContent getPanel(Context context, String panelType, String packageName); } diff --git a/src/com/android/settings/panel/PanelFeatureProviderImpl.java b/src/com/android/settings/panel/PanelFeatureProviderImpl.java index b4c37bf39a1..c3d611dc515 100644 --- a/src/com/android/settings/panel/PanelFeatureProviderImpl.java +++ b/src/com/android/settings/panel/PanelFeatureProviderImpl.java @@ -16,13 +16,15 @@ package com.android.settings.panel; +import static com.android.settingslib.media.MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT; + import android.content.Context; import android.provider.Settings; public class PanelFeatureProviderImpl implements PanelFeatureProvider { @Override - public PanelContent getPanel(Context context, String panelType) { + public PanelContent getPanel(Context context, String panelType, String packageName) { switch (panelType) { case Settings.Panel.ACTION_INTERNET_CONNECTIVITY: return InternetConnectivityPanel.create(context); @@ -30,6 +32,8 @@ public class PanelFeatureProviderImpl implements PanelFeatureProvider { return VolumePanel.create(context); case Settings.Panel.ACTION_NFC: return NfcPanel.create(context); + case ACTION_MEDIA_OUTPUT: + return MediaOutputPanel.create(context, packageName); } throw new IllegalStateException("No matching panel for: " + panelType); diff --git a/src/com/android/settings/panel/PanelFragment.java b/src/com/android/settings/panel/PanelFragment.java index 3d302fc011b..db0bf0e85ac 100644 --- a/src/com/android/settings/panel/PanelFragment.java +++ b/src/com/android/settings/panel/PanelFragment.java @@ -70,10 +70,12 @@ public class PanelFragment extends Fragment { final Bundle arguments = getArguments(); final String panelType = arguments.getString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT); + final String packageName = + arguments.getString(SettingsPanelActivity.KEY_PANEL_PACKAGE_NAME); final PanelContent panel = FeatureFactory.getFactory(activity) .getPanelFeatureProvider() - .getPanel(activity, panelType); + .getPanel(activity, panelType, packageName); mAdapter = new PanelSlicesAdapter(this, panel.getSlices()); @@ -86,6 +88,11 @@ public class PanelFragment extends Fragment { mSeeMoreButton.setOnClickListener(getSeeMoreListener(panel.getSeeMoreIntent())); mDoneButton.setOnClickListener(mDoneButtonListener); + //If getSeeMoreIntent() is null, hide the mSeeMoreButton. + if (panel.getSeeMoreIntent() == null) { + mSeeMoreButton.setVisibility(View.GONE); + } + return view; } diff --git a/src/com/android/settings/panel/SettingsPanelActivity.java b/src/com/android/settings/panel/SettingsPanelActivity.java index 02e14e80aaa..4cf535ede11 100644 --- a/src/com/android/settings/panel/SettingsPanelActivity.java +++ b/src/com/android/settings/panel/SettingsPanelActivity.java @@ -16,8 +16,12 @@ package com.android.settings.panel; +import static com.android.settingslib.media.MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT; +import static com.android.settingslib.media.MediaOutputSliceConstants.EXTRA_PACKAGE_NAME; + import android.content.Intent; import android.os.Bundle; +import android.text.TextUtils; import android.util.Log; import android.view.Gravity; import android.view.Window; @@ -28,6 +32,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; +import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; /** @@ -37,10 +42,14 @@ public class SettingsPanelActivity extends FragmentActivity { private final String TAG = "panel_activity"; + @VisibleForTesting + final Bundle mBundle = new Bundle(); + /** * Key specifying which Panel the app is requesting. */ public static final String KEY_PANEL_TYPE_ARGUMENT = "PANEL_TYPE_ARGUMENT"; + public static final String KEY_PANEL_PACKAGE_NAME = "PANEL_PACKAGE_NAME"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -53,6 +62,16 @@ public class SettingsPanelActivity extends FragmentActivity { return; } + final String packageName = + callingIntent.getStringExtra(EXTRA_PACKAGE_NAME); + + if (TextUtils.equals(ACTION_MEDIA_OUTPUT, callingIntent.getAction()) + && TextUtils.isEmpty(packageName)) { + Log.e(TAG, "Null package name, closing Panel Activity"); + finish(); + return; + } + setContentView(R.layout.settings_panel); // Move the window to the bottom of screen, and make it take up the entire screen width. @@ -61,11 +80,11 @@ public class SettingsPanelActivity extends FragmentActivity { window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); - final Bundle bundle = new Bundle(); - bundle.putString(KEY_PANEL_TYPE_ARGUMENT, callingIntent.getAction()); + mBundle.putString(KEY_PANEL_TYPE_ARGUMENT, callingIntent.getAction()); + mBundle.putString(KEY_PANEL_PACKAGE_NAME, packageName); final PanelFragment panelFragment = new PanelFragment(); - panelFragment.setArguments(bundle); + panelFragment.setArguments(mBundle); final FragmentManager fragmentManager = getSupportFragmentManager(); final Fragment fragment = fragmentManager.findFragmentById(R.id.main_content); diff --git a/src/com/android/settings/slices/CustomSliceManager.java b/src/com/android/settings/slices/CustomSliceManager.java index 8d07276fab7..3786c5c2ed9 100644 --- a/src/com/android/settings/slices/CustomSliceManager.java +++ b/src/com/android/settings/slices/CustomSliceManager.java @@ -18,6 +18,7 @@ package com.android.settings.slices; import android.content.Context; import android.net.Uri; +import android.text.TextUtils; import android.util.ArrayMap; import androidx.annotation.VisibleForTesting; @@ -33,6 +34,7 @@ import com.android.settings.homepage.contextualcards.slices.BluetoothDevicesSlic import com.android.settings.homepage.contextualcards.slices.LowStorageSlice; import com.android.settings.homepage.contextualcards.slices.NotificationChannelSlice; import com.android.settings.location.LocationSlice; +import com.android.settings.media.MediaOutputSlice; import com.android.settings.wifi.slice.ContextualWifiSlice; import com.android.settings.wifi.slice.WifiSlice; @@ -65,20 +67,25 @@ public class CustomSliceManager { * the only thing that should be needed to create the object. */ public CustomSliceable getSliceableFromUri(Uri uri) { - if (mSliceableCache.containsKey(uri)) { - return mSliceableCache.get(uri); + final Uri newUri = removeParameterFromUri(uri); + if (mSliceableCache.containsKey(newUri)) { + return mSliceableCache.get(newUri); } - final Class clazz = mUriMap.get(uri); + final Class clazz = mUriMap.get(newUri); if (clazz == null) { throw new IllegalArgumentException("No Slice found for uri: " + uri); } final CustomSliceable sliceable = CustomSliceable.createInstance(mContext, clazz); - mSliceableCache.put(uri, sliceable); + mSliceableCache.put(newUri, sliceable); return sliceable; } + private Uri removeParameterFromUri(Uri uri) { + return uri != null ? uri.buildUpon().clearQuery().build() : null; + } + /** * Return a {@link CustomSliceable} associated to the Action. *

@@ -94,7 +101,7 @@ public class CustomSliceManager { * {@link CustomSliceManager}. */ public boolean isValidUri(Uri uri) { - return mUriMap.containsKey(uri); + return mUriMap.containsKey(removeParameterFromUri(uri)); } /** @@ -120,5 +127,6 @@ public class CustomSliceManager { NotificationChannelSlice.class); mUriMap.put(CustomSliceRegistry.STORAGE_SLICE_URI, StorageSlice.class); mUriMap.put(CustomSliceRegistry.WIFI_SLICE_URI, WifiSlice.class); + mUriMap.put(CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI, MediaOutputSlice.class); } } diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java index 3a213df6742..66e85c0612d 100644 --- a/src/com/android/settings/slices/CustomSliceRegistry.java +++ b/src/com/android/settings/slices/CustomSliceRegistry.java @@ -27,6 +27,7 @@ import android.provider.SettingsSlicesContract; import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; import com.android.settings.wifi.calling.WifiCallingSliceHelper; +import com.android.settingslib.media.MediaOutputSliceConstants; /** * A registry of custom slice Uris. @@ -255,4 +256,14 @@ public class CustomSliceRegistry { .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(ZEN_MODE_KEY) .build(); + + /** + * Backing Uri for the Media output Slice. + */ + public static Uri MEDIA_OUTPUT_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(MediaOutputSliceConstants.KEY_MEDIA_OUTPUT) + .build(); } diff --git a/tests/robotests/src/com/android/settings/media/MediaDeviceUpdateWorkerTest.java b/tests/robotests/src/com/android/settings/media/MediaDeviceUpdateWorkerTest.java new file mode 100644 index 00000000000..28bf4e9053b --- /dev/null +++ b/tests/robotests/src/com/android/settings/media/MediaDeviceUpdateWorkerTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2019 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.settings.media; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; + +import com.android.settingslib.media.MediaDevice; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class MediaDeviceUpdateWorkerTest { + + private static final Uri URI = Uri.parse("content://com.android.settings.slices/test"); + private static final String TEST_DEVICE_1_ID = "test_device_1_id"; + private static final String TEST_DEVICE_2_ID = "test_device_2_id"; + private static final String TEST_DEVICE_3_ID = "test_device_3_id"; + + private final List mMediaDevices = new ArrayList<>(); + + private MediaDeviceUpdateWorker mMediaDeviceUpdateWorker; + private ContentResolver mResolver; + private Context mContext; + private MediaDevice mMediaDevice1; + private MediaDevice mMediaDevice2; + + @Before + public void setUp() { + mContext = spy(RuntimeEnvironment.application); + mMediaDeviceUpdateWorker = new MediaDeviceUpdateWorker(mContext, URI); + mResolver = mock(ContentResolver.class); + + mMediaDevice1 = mock(MediaDevice.class); + when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_1_ID); + mMediaDevice2 = mock(MediaDevice.class); + when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID); + mMediaDevices.add(mMediaDevice1); + mMediaDevices.add(mMediaDevice2); + + doReturn(mResolver).when(mContext).getContentResolver(); + } + + @Test + public void onDeviceListUpdate_shouldNotifyChange() { + mMediaDeviceUpdateWorker.onDeviceListUpdate(mMediaDevices); + + verify(mResolver).notifyChange(URI, null); + } + + @Test + public void onSelectedDeviceStateChanged_shouldNotifyChange() { + mMediaDeviceUpdateWorker.onSelectedDeviceStateChanged(null, 0); + + verify(mResolver).notifyChange(URI, null); + } + + @Test + public void onDeviceListUpdate_sameDeviceList_shouldBeEqual() { + mMediaDeviceUpdateWorker.onDeviceListUpdate(mMediaDevices); + + final List newDevices = new ArrayList<>(); + newDevices.add(mMediaDevice1); + newDevices.add(mMediaDevice2); + + mMediaDeviceUpdateWorker.onDeviceListUpdate(newDevices); + final List devices = mMediaDeviceUpdateWorker.getMediaDevices(); + + assertThat(devices.get(0).getId()).isEqualTo(newDevices.get(0).getId()); + assertThat(devices.get(1).getId()).isEqualTo(newDevices.get(1).getId()); + } + + @Test + public void onDeviceListUpdate_add1DeviceToDeviceList_shouldBeEqual() { + mMediaDeviceUpdateWorker.onDeviceListUpdate(mMediaDevices); + + final List newDevices = new ArrayList<>(); + final MediaDevice device3 = mock(MediaDevice.class); + when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_3_ID); + newDevices.add(mMediaDevice1); + newDevices.add(mMediaDevice2); + newDevices.add(device3); + + mMediaDeviceUpdateWorker.onDeviceListUpdate(newDevices); + final List devices = mMediaDeviceUpdateWorker.getMediaDevices(); + + assertThat(devices.size()).isEqualTo(newDevices.size()); + } + + @Test + public void onDeviceListUpdate_less1DeviceToDeviceList_shouldBeEqual() { + mMediaDeviceUpdateWorker.onDeviceListUpdate(mMediaDevices); + + final List newDevices = new ArrayList<>(); + newDevices.add(mMediaDevice1); + + mMediaDeviceUpdateWorker.onDeviceListUpdate(newDevices); + final List devices = mMediaDeviceUpdateWorker.getMediaDevices(); + + assertThat(devices.size()).isEqualTo(newDevices.size()); + } +} diff --git a/tests/robotests/src/com/android/settings/media/MediaOutputSliceTest.java b/tests/robotests/src/com/android/settings/media/MediaOutputSliceTest.java new file mode 100644 index 00000000000..daaba90bfa6 --- /dev/null +++ b/tests/robotests/src/com/android/settings/media/MediaOutputSliceTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2019 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.settings.media; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.util.IconDrawableFactory; + +import androidx.slice.Slice; +import androidx.slice.SliceMetadata; +import androidx.slice.SliceProvider; +import androidx.slice.core.SliceAction; +import androidx.slice.widget.SliceLiveData; + +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class MediaOutputSliceTest { + + private static final String TEST_PACKAGE_NAME = "com.fake.android.music"; + private static final String TEST_LABEL = "Test app"; + private static final String TEST_DEVICE_1_ID = "test_device_1_id"; + + @Mock + private PackageManager mPackageManager; + @Mock + private ApplicationInfo mApplicationInfo; + @Mock + private ApplicationInfo mApplicationInfo2; + @Mock + private LocalMediaManager mLocalMediaManager; + @Mock + private IconDrawableFactory mIconDrawableFactory; + @Mock + private Drawable mTestDrawable; + + private final List mDevices = new ArrayList<>(); + + private Context mContext; + private MediaOutputSlice mMediaOutputSlice; + private MediaDeviceUpdateWorker mMediaDeviceUpdateWorker; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.getApplicationInfo(eq(TEST_PACKAGE_NAME), anyInt())) + .thenReturn(mApplicationInfo); + when(mPackageManager.getApplicationInfoAsUser(eq(TEST_PACKAGE_NAME), anyInt(), anyInt())) + .thenReturn(mApplicationInfo2); + when(mApplicationInfo.loadLabel(mPackageManager)).thenReturn(TEST_LABEL); + when(mIconDrawableFactory.getBadgedIcon(mApplicationInfo2, UserHandle.myUserId())) + .thenReturn(mTestDrawable); + when(mTestDrawable.getIntrinsicWidth()).thenReturn(100); + when(mTestDrawable.getIntrinsicHeight()).thenReturn(100); + + // Set-up specs for SliceMetadata. + SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); + + mMediaOutputSlice = new MediaOutputSlice(mContext); + mMediaDeviceUpdateWorker = new MediaDeviceUpdateWorker(mContext, MEDIA_OUTPUT_SLICE_URI); + mMediaDeviceUpdateWorker.setPackageName(TEST_PACKAGE_NAME); + mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices); + mMediaDeviceUpdateWorker.mLocalMediaManager = mLocalMediaManager; + mMediaOutputSlice.init(TEST_PACKAGE_NAME, mMediaDeviceUpdateWorker, mIconDrawableFactory); + } + + @Test + public void getSlice_shouldHaveAppTitle() { + final Slice mediaSlice = mMediaOutputSlice.getSlice(); + final SliceMetadata metadata = SliceMetadata.from(mContext, mediaSlice); + + final SliceAction primaryAction = metadata.getPrimaryAction(); + assertThat(primaryAction.getTitle().toString()).isEqualTo(TEST_LABEL); + } + + @Test + public void onNotifyChange_foundMediaDevice_connect() { + mDevices.clear(); + final MediaDevice device = mock(MediaDevice.class); + when(device.getId()).thenReturn(TEST_DEVICE_1_ID); + when(mLocalMediaManager.getMediaDeviceById(mDevices, TEST_DEVICE_1_ID)).thenReturn(device); + mDevices.add(device); + + mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices); + + final Intent intent = new Intent(); + intent.putExtra("media_device_id", TEST_DEVICE_1_ID); + + mMediaOutputSlice.onNotifyChange(intent); + + verify(mLocalMediaManager).connectDevice(device); + } + + @Test + public void onNotifyChange_notFoundMediaDevice_doNothing() { + mDevices.clear(); + final MediaDevice device = mock(MediaDevice.class); + when(device.getId()).thenReturn(TEST_DEVICE_1_ID); + when(mLocalMediaManager.getMediaDeviceById(mDevices, TEST_DEVICE_1_ID)).thenReturn(device); + mDevices.add(device); + + mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices); + + final Intent intent = new Intent(); + intent.putExtra("media_device_id", "fake_123"); + + mMediaOutputSlice.onNotifyChange(intent); + + verify(mLocalMediaManager, never()).connectDevice(device); + } +} diff --git a/tests/robotests/src/com/android/settings/panel/MediaOutputPanelTest.java b/tests/robotests/src/com/android/settings/panel/MediaOutputPanelTest.java new file mode 100644 index 00000000000..b4110376226 --- /dev/null +++ b/tests/robotests/src/com/android/settings/panel/MediaOutputPanelTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2019 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.settings.panel; + +import static com.android.settings.media.MediaOutputSlice.MEDIA_PACKAGE_NAME; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; + +import com.android.settings.slices.CustomSliceRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class MediaOutputPanelTest { + + private static final String TEST_PACKAGENAME = "com.test.packagename"; + + private MediaOutputPanel mPanel; + + @Before + public void setUp() { + mPanel = MediaOutputPanel.create(RuntimeEnvironment.application, TEST_PACKAGENAME); + } + + @Test + public void getSlices_containsNecessarySlices() { + final List uris = mPanel.getSlices(); + + assertThat(uris).containsExactly(CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI); + } + + @Test + public void getSlices_verifyPackageName_isEqual() { + final List uris = mPanel.getSlices(); + + assertThat(uris.get(0).getQueryParameter(MEDIA_PACKAGE_NAME)).isEqualTo(TEST_PACKAGENAME); + } + + @Test + public void getSeeMoreIntent_isNull() { + assertThat(mPanel.getSeeMoreIntent()).isNull(); + } +} diff --git a/tests/robotests/src/com/android/settings/panel/PanelFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/panel/PanelFeatureProviderImplTest.java index 99d5d6cea55..ae57a778b61 100644 --- a/tests/robotests/src/com/android/settings/panel/PanelFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/panel/PanelFeatureProviderImplTest.java @@ -17,6 +17,8 @@ package com.android.settings.panel; +import static com.android.settingslib.media.MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT; + import static com.google.common.truth.Truth.assertThat; import android.content.Context; @@ -32,6 +34,8 @@ import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class PanelFeatureProviderImplTest { + private static final String TEST_PACKAGENAME = "com.test.packagename"; + private Context mContext; private PanelFeatureProviderImpl mProvider; @@ -44,7 +48,7 @@ public class PanelFeatureProviderImplTest { @Test public void getPanel_internetConnectivityKey_returnsCorrectPanel() { final PanelContent panel = mProvider.getPanel(mContext, - Settings.Panel.ACTION_INTERNET_CONNECTIVITY); + Settings.Panel.ACTION_INTERNET_CONNECTIVITY, TEST_PACKAGENAME); assertThat(panel).isInstanceOf(InternetConnectivityPanel.class); } @@ -52,8 +56,16 @@ public class PanelFeatureProviderImplTest { @Test public void getPanel_volume_returnsCorrectPanel() { final PanelContent panel = mProvider.getPanel(mContext, - Settings.Panel.ACTION_VOLUME); + Settings.Panel.ACTION_VOLUME, TEST_PACKAGENAME); assertThat(panel).isInstanceOf(VolumePanel.class); } + + @Test + public void getPanel_mediaOutputKey_returnsCorrectPanel() { + final PanelContent panel = mProvider.getPanel(mContext, + ACTION_MEDIA_OUTPUT, TEST_PACKAGENAME); + + assertThat(panel).isInstanceOf(MediaOutputPanel.class); + } } diff --git a/tests/robotests/src/com/android/settings/panel/PanelFragmentTest.java b/tests/robotests/src/com/android/settings/panel/PanelFragmentTest.java index 73318f2ac11..389c31e621c 100644 --- a/tests/robotests/src/com/android/settings/panel/PanelFragmentTest.java +++ b/tests/robotests/src/com/android/settings/panel/PanelFragmentTest.java @@ -59,7 +59,7 @@ public class PanelFragmentTest { mFakeFeatureFactory = FakeFeatureFactory.setupForTest(); mFakeFeatureFactory.panelFeatureProvider = mPanelFeatureProvider; mFakePanelContent = new FakePanelContent(); - doReturn(mFakePanelContent).when(mPanelFeatureProvider).getPanel(any(), any()); + doReturn(mFakePanelContent).when(mPanelFeatureProvider).getPanel(any(), any(), any()); ActivityController activityController = Robolectric.buildActivity(FakeSettingsPanelActivity.class); diff --git a/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java b/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java index 7648d236720..abefa674623 100644 --- a/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java +++ b/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java @@ -58,7 +58,7 @@ public class PanelSlicesAdapterTest { mFakeFeatureFactory = FakeFeatureFactory.setupForTest(); mFakeFeatureFactory.panelFeatureProvider = mPanelFeatureProvider; mFakePanelContent = new FakePanelContent(); - doReturn(mFakePanelContent).when(mPanelFeatureProvider).getPanel(any(), any()); + doReturn(mFakePanelContent).when(mPanelFeatureProvider).getPanel(any(), any(), any()); ActivityController activityController = Robolectric.buildActivity(FakeSettingsPanelActivity.class); diff --git a/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java b/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java new file mode 100644 index 00000000000..359cf5d7bc3 --- /dev/null +++ b/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 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.settings.panel; + +import static com.android.settings.panel.SettingsPanelActivity.KEY_PANEL_PACKAGE_NAME; +import static com.android.settings.panel.SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Intent; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class SettingsPanelActivityTest { + + @Test + public void startMediaOutputSlice_withPackageName_bundleShouldHaveValue() { + final Intent intent = new Intent() + .setAction("com.android.settings.panel.action.MEDIA_OUTPUT") + .putExtra("com.android.settings.panel.extra.PACKAGE_NAME", + "com.google.android.music"); + + final SettingsPanelActivity activity = + Robolectric.buildActivity(SettingsPanelActivity.class, intent).create().get(); + + assertThat(activity.mBundle.getString(KEY_PANEL_PACKAGE_NAME)) + .isEqualTo("com.google.android.music"); + assertThat(activity.mBundle.getString(KEY_PANEL_TYPE_ARGUMENT)) + .isEqualTo("com.android.settings.panel.action.MEDIA_OUTPUT"); + } + + @Test + public void startMediaOutputSlice_withoutPackageName_bundleShouldNotHaveValue() { + final Intent intent = new Intent() + .setAction("com.android.settings.panel.action.MEDIA_OUTPUT"); + + final SettingsPanelActivity activity = + Robolectric.buildActivity(SettingsPanelActivity.class, intent).create().get(); + + assertThat(activity.mBundle.containsKey(KEY_PANEL_PACKAGE_NAME)).isFalse(); + assertThat(activity.mBundle.containsKey(KEY_PANEL_TYPE_ARGUMENT)).isFalse(); + } +}