1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.media;
18 
19 import static android.app.slice.Slice.EXTRA_RANGE_VALUE;
20 import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
21 
22 import static com.android.settings.slices.CustomSliceRegistry.REMOTE_MEDIA_SLICE_URI;
23 
24 import android.app.PendingIntent;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.graphics.Bitmap;
28 import android.media.MediaRouter2Manager;
29 import android.media.RoutingSessionInfo;
30 import android.net.Uri;
31 import android.text.SpannableString;
32 import android.text.TextUtils;
33 import android.text.style.ForegroundColorSpan;
34 import android.util.Log;
35 
36 import androidx.annotation.VisibleForTesting;
37 import androidx.core.graphics.drawable.IconCompat;
38 import androidx.slice.Slice;
39 import androidx.slice.builders.ListBuilder;
40 import androidx.slice.builders.ListBuilder.InputRangeBuilder;
41 import androidx.slice.builders.SliceAction;
42 
43 import com.android.settings.R;
44 import com.android.settings.SubSettings;
45 import com.android.settings.Utils;
46 import com.android.settings.notification.SoundSettings;
47 import com.android.settings.slices.CustomSliceable;
48 import com.android.settings.slices.SliceBackgroundWorker;
49 import com.android.settings.slices.SliceBroadcastReceiver;
50 import com.android.settings.slices.SliceBuilderUtils;
51 import com.android.settingslib.media.MediaOutputConstants;
52 
53 import java.util.List;
54 
55 /**
56  * Display the Remote Media device information.
57  */
58 public class RemoteMediaSlice implements CustomSliceable {
59 
60     private static final String TAG = "RemoteMediaSlice";
61     private static final String MEDIA_ID = "media_id";
62     private static final String ACTION_LAUNCH_DIALOG = "action_launch_dialog";
63     private static final String SESSION_INFO = "RoutingSessionInfo";
64     private static final String CUSTOMIZED_ACTION = "customized_action";
65 
66     private final Context mContext;
67 
68     private MediaDeviceUpdateWorker mWorker;
69 
70     @VisibleForTesting
71     MediaRouter2Manager mRouterManager;
72 
RemoteMediaSlice(Context context)73     public RemoteMediaSlice(Context context) {
74         mContext = context;
75     }
76 
77     @Override
onNotifyChange(Intent intent)78     public void onNotifyChange(Intent intent) {
79         final int newPosition = intent.getIntExtra(EXTRA_RANGE_VALUE, -1);
80         final String id = intent.getStringExtra(MEDIA_ID);
81         if (!TextUtils.isEmpty(id)) {
82             getWorker().adjustSessionVolume(id, newPosition);
83             return;
84         }
85         if (TextUtils.equals(ACTION_LAUNCH_DIALOG, intent.getStringExtra(CUSTOMIZED_ACTION))) {
86             // Launch Media Output Dialog
87             final RoutingSessionInfo info = intent.getParcelableExtra(SESSION_INFO);
88             mContext.sendBroadcast(new Intent()
89                     .setPackage(MediaOutputConstants.SYSTEMUI_PACKAGE_NAME)
90                     .setAction(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG)
91                     .putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME,
92                             info.getClientPackageName()));
93             // Dismiss volume panel
94             mContext.sendBroadcast(new Intent()
95                     .setPackage(MediaOutputConstants.SETTINGS_PACKAGE_NAME)
96                     .setAction(MediaOutputConstants.ACTION_CLOSE_PANEL));
97         }
98     }
99 
100     @Override
getSlice()101     public Slice getSlice() {
102         final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY)
103                 .setAccentColor(COLOR_NOT_TINTED);
104         if (getWorker() == null) {
105             Log.e(TAG, "Unable to get the slice worker.");
106             return listBuilder.build();
107         }
108         if (mRouterManager == null) {
109             mRouterManager = MediaRouter2Manager.getInstance(mContext);
110         }
111         // Only displaying remote devices
112         final List<RoutingSessionInfo> infos = getWorker().getActiveRemoteMediaDevice();
113         if (infos.isEmpty()) {
114             Log.d(TAG, "No active remote media device");
115             return listBuilder.build();
116         }
117         final CharSequence castVolume = mContext.getText(R.string.remote_media_volume_option_title);
118         final IconCompat icon = IconCompat.createWithResource(mContext,
119                 R.drawable.ic_volume_remote);
120         // To create an empty icon to indent the row
121         final IconCompat emptyIcon = createEmptyIcon();
122         for (RoutingSessionInfo info : infos) {
123             final int maxVolume = info.getVolumeMax();
124             if (maxVolume <= 0) {
125                 Log.d(TAG, "Unable to add Slice. " + info.getName() + ": max volume is "
126                         + maxVolume);
127                 continue;
128             }
129             if (!getWorker().shouldEnableVolumeSeekBar(info)) {
130                 // There is no disable state. We hide it directly.
131                 Log.d(TAG, "Unable to add Slice. " + info.getName() + ": This is a group session");
132                 continue;
133             }
134 
135             final CharSequence appName = Utils.getApplicationLabel(
136                     mContext, info.getClientPackageName());
137             final CharSequence outputTitle = mContext.getString(R.string.media_output_label_title,
138                     appName);
139             listBuilder.addInputRange(new InputRangeBuilder()
140                     .setTitleItem(icon, ListBuilder.ICON_IMAGE)
141                     .setTitle(castVolume)
142                     .setInputAction(getSliderInputAction(info.getId().hashCode(), info.getId()))
143                     .setPrimaryAction(getSoundSettingAction(castVolume, icon, info.getId()))
144                     .setMax(maxVolume)
145                     .setValue(info.getVolume()));
146 
147             final boolean isMediaOutputDisabled =
148                     getWorker().shouldDisableMediaOutput(info.getClientPackageName());
149             final SpannableString spannableTitle = new SpannableString(
150                     TextUtils.isEmpty(appName) ? "" : appName);
151             spannableTitle.setSpan(new ForegroundColorSpan(
152                             Utils.getColorAttrDefaultColor(
153                                     mContext, android.R.attr.textColorSecondary)), 0,
154                     spannableTitle.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
155             listBuilder.addRow(new ListBuilder.RowBuilder()
156                     .setTitle(isMediaOutputDisabled ? spannableTitle : outputTitle)
157                     .setSubtitle(info.getName())
158                     .setTitleItem(emptyIcon, ListBuilder.ICON_IMAGE)
159                     .setPrimaryAction(getMediaOutputDialogAction(info, isMediaOutputDisabled)));
160         }
161         return listBuilder.build();
162     }
163 
createEmptyIcon()164     private IconCompat createEmptyIcon() {
165         final Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
166         return IconCompat.createWithBitmap(bitmap);
167     }
168 
getSliderInputAction(int requestCode, String id)169     private PendingIntent getSliderInputAction(int requestCode, String id) {
170         final Intent intent = new Intent(getUri().toString())
171                 .setData(getUri())
172                 .putExtra(MEDIA_ID, id)
173                 .setClass(mContext, SliceBroadcastReceiver.class);
174         return PendingIntent.getBroadcast(mContext, requestCode, intent,
175                 PendingIntent.FLAG_MUTABLE);
176     }
177 
getSoundSettingAction(CharSequence actionTitle, IconCompat icon, String id)178     private SliceAction getSoundSettingAction(CharSequence actionTitle, IconCompat icon,
179             String id) {
180         final Uri contentUri = new Uri.Builder().appendPath(id).build();
181         final Intent intent = SliceBuilderUtils.buildSearchResultPageIntent(mContext,
182                 SoundSettings.class.getName(),
183                 id,
184                 mContext.getText(R.string.sound_settings).toString(),
185                 0 /* sourceMetricsCategory */,
186                 R.string.menu_key_sound);
187         intent.setClassName(mContext.getPackageName(), SubSettings.class.getName());
188         intent.setData(contentUri);
189         final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent,
190                 PendingIntent.FLAG_IMMUTABLE);
191         final SliceAction primarySliceAction = SliceAction.createDeeplink(pendingIntent, icon,
192                 ListBuilder.ICON_IMAGE, actionTitle);
193         return primarySliceAction;
194     }
195 
getMediaOutputDialogAction(RoutingSessionInfo info, boolean isMediaOutputDisabled)196     private SliceAction getMediaOutputDialogAction(RoutingSessionInfo info,
197             boolean isMediaOutputDisabled) {
198         final Intent intent = new Intent(getUri().toString())
199                 .setData(getUri())
200                 .setClass(mContext, SliceBroadcastReceiver.class)
201                 .putExtra(CUSTOMIZED_ACTION, isMediaOutputDisabled ? "" : ACTION_LAUNCH_DIALOG)
202                 .putExtra(SESSION_INFO, info)
203                 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
204         final PendingIntent primaryBroadcastIntent = PendingIntent.getBroadcast(mContext,
205                 info.hashCode(), intent,
206                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
207         final SliceAction primarySliceAction = SliceAction.createDeeplink(
208                 primaryBroadcastIntent,
209                 IconCompat.createWithResource(mContext, R.drawable.ic_volume_remote),
210                 ListBuilder.ICON_IMAGE,
211                 mContext.getString(R.string.media_output_label_title,
212                         Utils.getApplicationLabel(mContext, info.getClientPackageName())));
213         return primarySliceAction;
214     }
215 
216     @Override
getUri()217     public Uri getUri() {
218         return REMOTE_MEDIA_SLICE_URI;
219     }
220 
221     @Override
getIntent()222     public Intent getIntent() {
223         return null;
224     }
225 
226     @Override
getSliceHighlightMenuRes()227     public int getSliceHighlightMenuRes() {
228         return R.string.menu_key_connected_devices;
229     }
230 
231     @Override
getBackgroundWorkerClass()232     public Class getBackgroundWorkerClass() {
233         return MediaDeviceUpdateWorker.class;
234     }
235 
getWorker()236     private MediaDeviceUpdateWorker getWorker() {
237         if (mWorker == null) {
238             mWorker = SliceBackgroundWorker.getInstance(getUri());
239         }
240         return mWorker;
241     }
242 }
243