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