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