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