1 /* 2 * Copyright (C) 2019 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 com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI; 20 21 import android.app.PendingIntent; 22 import android.bluetooth.BluetoothAdapter; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.graphics.drawable.Drawable; 26 import android.net.Uri; 27 import android.telephony.TelephonyManager; 28 import android.text.TextUtils; 29 import android.util.Log; 30 31 import androidx.annotation.VisibleForTesting; 32 import androidx.core.graphics.drawable.IconCompat; 33 import androidx.slice.Slice; 34 import androidx.slice.builders.ListBuilder; 35 import androidx.slice.builders.SliceAction; 36 37 import com.android.settings.R; 38 import com.android.settings.Utils; 39 import com.android.settings.slices.CustomSliceable; 40 import com.android.settings.slices.SliceBackgroundWorker; 41 import com.android.settings.slices.SliceBroadcastReceiver; 42 import com.android.settingslib.media.MediaDevice; 43 44 import java.util.List; 45 46 /** 47 * Show the Media device that can be transfer the media. 48 */ 49 public class MediaOutputSlice implements CustomSliceable { 50 51 private static final String TAG = "MediaOutputSlice"; 52 private static final String MEDIA_DEVICE_ID = "media_device_id"; 53 54 public static final String MEDIA_PACKAGE_NAME = "media_package_name"; 55 56 private final Context mContext; 57 58 private MediaDeviceUpdateWorker mWorker; 59 private String mPackageName; 60 MediaOutputSlice(Context context)61 public MediaOutputSlice(Context context) { 62 mContext = context; 63 mPackageName = getUri().getQueryParameter(MEDIA_PACKAGE_NAME); 64 } 65 66 @VisibleForTesting init(String packageName, MediaDeviceUpdateWorker worker)67 void init(String packageName, MediaDeviceUpdateWorker worker) { 68 mPackageName = packageName; 69 mWorker = worker; 70 } 71 72 @Override getSlice()73 public Slice getSlice() { 74 // Reload theme for switching dark mode on/off 75 mContext.getTheme().applyStyle(R.style.Theme_Settings_Home, true /* force */); 76 77 final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY) 78 .setAccentColor(COLOR_NOT_TINTED); 79 80 if (!isVisible()) { 81 Log.d(TAG, "getSlice() is not visible"); 82 return listBuilder.build(); 83 } 84 85 final List<MediaDevice> devices = getMediaDevices(); 86 87 final MediaDevice connectedDevice = getWorker().getCurrentConnectedMediaDevice(); 88 if (connectedDevice != null) { 89 listBuilder.addRow(getActiveDeviceHeaderRow(connectedDevice)); 90 } 91 92 for (MediaDevice device : devices) { 93 if (connectedDevice == null 94 || !TextUtils.equals(connectedDevice.getId(), device.getId())) { 95 listBuilder.addRow(getMediaDeviceRow(device)); 96 } 97 } 98 99 return listBuilder.build(); 100 } 101 getActiveDeviceHeaderRow(MediaDevice device)102 private ListBuilder.RowBuilder getActiveDeviceHeaderRow(MediaDevice device) { 103 final String title = device.getName(); 104 final IconCompat icon = getDeviceIconCompat(device); 105 106 final PendingIntent broadcastAction = 107 getBroadcastIntent(mContext, device.getId(), device.hashCode()); 108 final SliceAction primarySliceAction = SliceAction.createDeeplink(broadcastAction, icon, 109 ListBuilder.ICON_IMAGE, title); 110 111 final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() 112 .setTitleItem(icon, ListBuilder.ICON_IMAGE) 113 .setTitle(title) 114 .setSubtitle(device.getSummary()) 115 .setPrimaryAction(primarySliceAction); 116 117 return rowBuilder; 118 } 119 getDeviceIconCompat(MediaDevice device)120 private IconCompat getDeviceIconCompat(MediaDevice device) { 121 Drawable drawable = device.getIcon(); 122 if (drawable == null) { 123 Log.d(TAG, "getDeviceIconCompat() device : " + device.getName() + ", drawable is null"); 124 // Use default Bluetooth device icon to handle getIcon() is null case. 125 drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp); 126 } 127 128 return Utils.createIconWithDrawable(drawable); 129 } 130 getWorker()131 private MediaDeviceUpdateWorker getWorker() { 132 if (mWorker == null) { 133 mWorker = (MediaDeviceUpdateWorker) SliceBackgroundWorker.getInstance(getUri()); 134 if (mWorker != null) { 135 mWorker.setPackageName(mPackageName); 136 } 137 } 138 return mWorker; 139 } 140 getMediaDevices()141 private List<MediaDevice> getMediaDevices() { 142 final List<MediaDevice> devices = getWorker().getMediaDevices(); 143 return devices; 144 } 145 getMediaDeviceRow(MediaDevice device)146 private ListBuilder.RowBuilder getMediaDeviceRow(MediaDevice device) { 147 final String title = device.getName(); 148 final PendingIntent broadcastAction = 149 getBroadcastIntent(mContext, device.getId(), device.hashCode()); 150 final IconCompat deviceIcon = getDeviceIconCompat(device); 151 152 final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() 153 .setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE) 154 .setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon, 155 ListBuilder.ICON_IMAGE, title)) 156 .setTitle(title) 157 .setSubtitle(device.getSummary()); 158 159 return rowBuilder; 160 } 161 getBroadcastIntent(Context context, String id, int requestCode)162 private PendingIntent getBroadcastIntent(Context context, String id, int requestCode) { 163 final Intent intent = new Intent(getUri().toString()); 164 intent.setClass(context, SliceBroadcastReceiver.class); 165 intent.putExtra(MEDIA_DEVICE_ID, id); 166 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 167 return PendingIntent.getBroadcast(context, requestCode /* requestCode */, intent, 168 PendingIntent.FLAG_CANCEL_CURRENT); 169 } 170 171 @Override getUri()172 public Uri getUri() { 173 return MEDIA_OUTPUT_SLICE_URI; 174 } 175 176 @Override onNotifyChange(Intent intent)177 public void onNotifyChange(Intent intent) { 178 final MediaDeviceUpdateWorker worker = getWorker(); 179 final String id = intent != null ? intent.getStringExtra(MEDIA_DEVICE_ID) : ""; 180 final MediaDevice device = worker.getMediaDeviceById(id); 181 if (device != null) { 182 Log.d(TAG, "onNotifyChange() device name : " + device.getName()); 183 worker.connectDevice(device); 184 } 185 } 186 187 @Override getIntent()188 public Intent getIntent() { 189 return null; 190 } 191 192 @Override getBackgroundWorkerClass()193 public Class getBackgroundWorkerClass() { 194 return MediaDeviceUpdateWorker.class; 195 } 196 isVisible()197 private boolean isVisible() { 198 // To decide Slice's visibility. 199 // Return true if 200 // 1. phone is not in ongoing call mode 201 // 2. worker is not null 202 // 3. Bluetooth is enabled 203 final TelephonyManager telephonyManager = 204 (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); 205 final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 206 207 return telephonyManager.getCallState() == TelephonyManager.CALL_STATE_IDLE 208 && adapter.isEnabled() 209 && getWorker() != null; 210 } 211 } 212