• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 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 package com.android.settingslib.media;
17 
18 import static android.media.MediaRoute2Info.TYPE_AUX_LINE;
19 import static android.media.MediaRoute2Info.TYPE_BLE_HEADSET;
20 import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
21 import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
22 import static android.media.MediaRoute2Info.TYPE_DOCK;
23 import static android.media.MediaRoute2Info.TYPE_GROUP;
24 import static android.media.MediaRoute2Info.TYPE_HDMI;
25 import static android.media.MediaRoute2Info.TYPE_HDMI_ARC;
26 import static android.media.MediaRoute2Info.TYPE_HDMI_EARC;
27 import static android.media.MediaRoute2Info.TYPE_HEARING_AID;
28 import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG;
29 import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL;
30 import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER;
31 import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER;
32 import static android.media.MediaRoute2Info.TYPE_REMOTE_TV;
33 import static android.media.MediaRoute2Info.TYPE_UNKNOWN;
34 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY;
35 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE;
36 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET;
37 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
38 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
39 import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION;
40 import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION_MANAGED;
41 import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED;
42 import static android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED;
43 import static android.media.RouteListingPreference.Item.SUBTEXT_CUSTOM;
44 import static android.media.RouteListingPreference.Item.SUBTEXT_DEVICE_LOW_POWER;
45 import static android.media.RouteListingPreference.Item.SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED;
46 import static android.media.RouteListingPreference.Item.SUBTEXT_ERROR_UNKNOWN;
47 import static android.media.RouteListingPreference.Item.SUBTEXT_NONE;
48 import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED;
49 import static android.media.RouteListingPreference.Item.SUBTEXT_TRACK_UNSUPPORTED;
50 import static android.media.RouteListingPreference.Item.SUBTEXT_UNAUTHORIZED;
51 
52 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
53 import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
54 
55 import android.annotation.NonNull;
56 import android.annotation.Nullable;
57 import android.annotation.SuppressLint;
58 import android.content.Context;
59 import android.graphics.drawable.Drawable;
60 import android.media.MediaRoute2Info;
61 import android.media.NearbyDevice;
62 import android.media.RouteListingPreference;
63 import android.os.Build;
64 import android.text.TextUtils;
65 import android.util.Log;
66 
67 import androidx.annotation.DoNotInline;
68 import androidx.annotation.IntDef;
69 import androidx.annotation.RequiresApi;
70 import androidx.annotation.VisibleForTesting;
71 
72 import com.android.settingslib.R;
73 
74 import java.lang.annotation.Retention;
75 import java.lang.annotation.RetentionPolicy;
76 import java.util.ArrayList;
77 import java.util.List;
78 
79 /**
80  * MediaDevice represents a media device(such like Bluetooth device, cast device and phone device).
81  */
82 public abstract class MediaDevice implements Comparable<MediaDevice> {
83     private static final String TAG = "MediaDevice";
84 
85     @Retention(RetentionPolicy.SOURCE)
86     @IntDef({MediaDeviceType.TYPE_UNKNOWN,
87             MediaDeviceType.TYPE_PHONE_DEVICE,
88             MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE,
89             MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE,
90             MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE,
91             MediaDeviceType.TYPE_BLUETOOTH_DEVICE,
92             MediaDeviceType.TYPE_CAST_DEVICE,
93             MediaDeviceType.TYPE_CAST_GROUP_DEVICE,
94             MediaDeviceType.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER})
95     public @interface MediaDeviceType {
96         int TYPE_UNKNOWN = 0;
97         int TYPE_PHONE_DEVICE = 1;
98         int TYPE_USB_C_AUDIO_DEVICE = 2;
99         int TYPE_3POINT5_MM_AUDIO_DEVICE = 3;
100         int TYPE_FAST_PAIR_BLUETOOTH_DEVICE = 4;
101         int TYPE_BLUETOOTH_DEVICE = 5;
102         int TYPE_CAST_DEVICE = 6;
103         int TYPE_CAST_GROUP_DEVICE = 7;
104         int TYPE_REMOTE_AUDIO_VIDEO_RECEIVER = 8;
105     }
106 
107     @Retention(RetentionPolicy.SOURCE)
108     @IntDef({SelectionBehavior.SELECTION_BEHAVIOR_NONE,
109             SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER,
110             SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP
111     })
112     public @interface SelectionBehavior {
113         int SELECTION_BEHAVIOR_NONE = 0;
114         int SELECTION_BEHAVIOR_TRANSFER = 1;
115         int SELECTION_BEHAVIOR_GO_TO_APP = 2;
116     }
117 
118     @VisibleForTesting
119     int mType;
120 
121     private int mConnectedRecord;
122     private int mState;
123     @NearbyDevice.RangeZone
124     private int mRangeZone = NearbyDevice.RANGE_UNKNOWN;
125 
126     protected final Context mContext;
127     protected final MediaRoute2Info mRouteInfo;
128     protected final RouteListingPreference.Item mItem;
129 
MediaDevice( @onNull Context context, @Nullable MediaRoute2Info info, @Nullable RouteListingPreference.Item item)130     MediaDevice(
131             @NonNull Context context,
132             @Nullable MediaRoute2Info info,
133             @Nullable RouteListingPreference.Item item) {
134         mContext = context;
135         mRouteInfo = info;
136         mItem = item;
137         setType(info);
138     }
139 
140     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
141     @SuppressWarnings("NewApi")
setType(MediaRoute2Info info)142     private void setType(MediaRoute2Info info) {
143         if (info == null) {
144             mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE;
145             return;
146         }
147         switch (info.getType()) {
148             case TYPE_GROUP:
149                 mType = MediaDeviceType.TYPE_CAST_GROUP_DEVICE;
150                 break;
151             case TYPE_BUILTIN_SPEAKER:
152                 mType = MediaDeviceType.TYPE_PHONE_DEVICE;
153                 break;
154             case TYPE_WIRED_HEADSET:
155             case TYPE_WIRED_HEADPHONES:
156             case TYPE_LINE_DIGITAL:
157             case TYPE_LINE_ANALOG:
158             case TYPE_AUX_LINE:
159                 mType = MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE;
160                 break;
161             case TYPE_USB_DEVICE:
162             case TYPE_USB_HEADSET:
163             case TYPE_USB_ACCESSORY:
164             case TYPE_DOCK:
165             case TYPE_HDMI:
166             case TYPE_HDMI_ARC:
167             case TYPE_HDMI_EARC:
168                 mType = MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE;
169                 break;
170             case TYPE_HEARING_AID:
171             case TYPE_BLUETOOTH_A2DP:
172             case TYPE_BLE_HEADSET:
173                 mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE;
174                 break;
175             case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER:
176                 mType = MediaDeviceType.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER;
177                 break;
178             case TYPE_UNKNOWN:
179             case TYPE_REMOTE_TV:
180             case TYPE_REMOTE_SPEAKER:
181             default:
182                 mType = MediaDeviceType.TYPE_CAST_DEVICE;
183                 break;
184         }
185     }
186 
initDeviceRecord()187     void initDeviceRecord() {
188         ConnectionRecordManager.getInstance().fetchLastSelectedDevice(mContext);
189         mConnectedRecord = ConnectionRecordManager.getInstance().fetchConnectionRecord(mContext,
190                 getId());
191     }
192 
getRangeZone()193     public @NearbyDevice.RangeZone int getRangeZone() {
194         return mRangeZone;
195     }
196 
setRangeZone(@earbyDevice.RangeZone int rangeZone)197     public void setRangeZone(@NearbyDevice.RangeZone int rangeZone) {
198         mRangeZone = rangeZone;
199     }
200 
201     /**
202      * Get name from MediaDevice.
203      *
204      * @return name of MediaDevice.
205      */
getName()206     public abstract String getName();
207 
208     /**
209      * Get summary from MediaDevice.
210      *
211      * @return summary of MediaDevice.
212      */
getSummary()213     public abstract String getSummary();
214 
215     /**
216      * Get summary from MediaDevice for TV with low batter states in a different color if
217      * applicable.
218      *
219      * @param lowBatteryColorRes Color resource for the part of the CharSequence that describes a
220      *                           low battery state.
221      */
getSummaryForTv(int lowBatteryColorRes)222     public CharSequence getSummaryForTv(int lowBatteryColorRes) {
223         return getSummary();
224     }
225 
226     /**
227      * Get icon of MediaDevice.
228      *
229      * @return drawable of icon.
230      */
getIcon()231     public abstract Drawable getIcon();
232 
233     /**
234      * Get icon of MediaDevice without background.
235      *
236      * @return drawable of icon
237      */
getIconWithoutBackground()238     public abstract Drawable getIconWithoutBackground();
239 
240     /**
241      * Get unique ID that represent MediaDevice
242      *
243      * @return unique id of MediaDevice
244      */
getId()245     public abstract String getId();
246 
247     /** Returns {@code true} if the device has a non-null {@link RouteListingPreference.Item}. */
hasRouteListingPreferenceItem()248     public boolean hasRouteListingPreferenceItem() {
249         return mItem != null;
250     }
251 
252     /**
253      * Get selection behavior of device
254      *
255      * @return selection behavior of device
256      */
257     @SelectionBehavior
getSelectionBehavior()258     public int getSelectionBehavior() {
259         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
260                 ? mItem.getSelectionBehavior() : SELECTION_BEHAVIOR_TRANSFER;
261     }
262 
263     /**
264      * Checks if device is has subtext
265      *
266      * @return true if device has subtext
267      */
hasSubtext()268     public boolean hasSubtext() {
269         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
270                 && mItem != null
271                 && mItem.getSubText() != SUBTEXT_NONE;
272     }
273 
274     /**
275      * Get subtext of device
276      *
277      * @return subtext of device
278      */
279     @RouteListingPreference.Item.SubText
getSubtext()280     public int getSubtext() {
281         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
282                 ? mItem.getSubText() : SUBTEXT_NONE;
283     }
284 
285     /**
286      * Returns subtext string for current route.
287      *
288      * @return subtext string for this route
289      */
getSubtextString()290     public String getSubtextString() {
291         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
292                 ? Api34Impl.composeSubtext(mItem, mContext) : null;
293     }
294 
295     /**
296      * Checks if device has ongoing shared session, which allow user to join
297      *
298      * @return true if device has ongoing session
299      */
hasOngoingSession()300     public boolean hasOngoingSession() {
301         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
302                 && Api34Impl.hasOngoingSession(mItem);
303     }
304 
305     /**
306      * Checks if device is the host for ongoing shared session, which allow user to adjust volume
307      *
308      * @return true if device is the host for ongoing shared session
309      */
isHostForOngoingSession()310     public boolean isHostForOngoingSession() {
311         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
312                 && Api34Impl.isHostForOngoingSession(mItem);
313     }
314 
315     /**
316      * Checks if device is suggested device from application
317      *
318      * @return true if device is suggested device
319      */
isSuggestedDevice()320     public boolean isSuggestedDevice() {
321         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
322                 && Api34Impl.isSuggestedDevice(mItem);
323     }
324 
setConnectedRecord()325     void setConnectedRecord() {
326         mConnectedRecord++;
327         ConnectionRecordManager.getInstance().setConnectionRecord(mContext, getId(),
328                 mConnectedRecord);
329     }
330 
331     /**
332      * According the MediaDevice type to check whether we are connected to this MediaDevice.
333      *
334      * @return Whether it is connected.
335      */
isConnected()336     public abstract boolean isConnected();
337 
338     /**
339      * Get max volume from MediaDevice.
340      *
341      * @return max volume.
342      */
getMaxVolume()343     public int getMaxVolume() {
344         if (mRouteInfo == null) {
345             Log.w(TAG, "Unable to get max volume. RouteInfo is empty");
346             return 0;
347         }
348         return mRouteInfo.getVolumeMax();
349     }
350 
351     /**
352      * Get current volume from MediaDevice.
353      *
354      * @return current volume.
355      */
getCurrentVolume()356     public int getCurrentVolume() {
357         if (mRouteInfo == null) {
358             Log.w(TAG, "Unable to get current volume. RouteInfo is empty");
359             return 0;
360         }
361         return mRouteInfo.getVolume();
362     }
363 
364     /**
365      * Get application package name.
366      *
367      * @return package name.
368      */
getClientPackageName()369     public String getClientPackageName() {
370         if (mRouteInfo == null) {
371             Log.w(TAG, "Unable to get client package name. RouteInfo is empty");
372             return null;
373         }
374         return mRouteInfo.getClientPackageName();
375     }
376 
377     /**
378      * Check if the device is Bluetooth LE Audio device.
379      *
380      * @return true if the RouteInfo equals TYPE_BLE_HEADSET.
381      */
382     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
383     @SuppressWarnings("NewApi")
isBLEDevice()384     public boolean isBLEDevice() {
385         return mRouteInfo.getType() == TYPE_BLE_HEADSET;
386     }
387 
388     /**
389      * Get application label from MediaDevice.
390      *
391      * @return application label.
392      */
getDeviceType()393     public int getDeviceType() {
394         return mType;
395     }
396 
397     /**
398      * Get the {@link MediaRoute2Info.Type} of the device.
399      */
getRouteType()400     public int getRouteType() {
401         if (mRouteInfo == null) {
402             return TYPE_UNKNOWN;
403         }
404         return mRouteInfo.getType();
405     }
406 
407     /**
408      * Checks if route's volume is fixed, if true, we should disable volume control for the device.
409      *
410      * @return route for this device is fixed.
411      */
412     @SuppressLint("NewApi")
isVolumeFixed()413     public boolean isVolumeFixed() {
414         if (mRouteInfo == null) {
415             Log.w(TAG, "RouteInfo is empty, regarded as volume fixed.");
416             return true;
417         }
418         return mRouteInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
419     }
420 
421     /**
422      * Set current device's state
423      */
setState(@ocalMediaManager.MediaDeviceState int state)424     public void setState(@LocalMediaManager.MediaDeviceState int state) {
425         mState = state;
426     }
427 
428     /**
429      * Get current device's state
430      *
431      * @return state of device
432      */
getState()433     public @LocalMediaManager.MediaDeviceState int getState() {
434         return mState;
435     }
436 
437     /**
438      * Rules:
439      * 1. If there is one of the connected devices identified as a carkit or fast pair device,
440      * the fast pair device will be always on the first of the device list and carkit will be
441      * second. Rule 2 and Rule 3 can’t overrule this rule.
442      * 2. For devices without any usage data yet
443      * WiFi device group sorted by alphabetical order + BT device group sorted by alphabetical
444      * order + phone speaker
445      * 3. For devices with usage record.
446      * The most recent used one + device group with usage info sorted by how many times the
447      * device has been used.
448      * 4. The order is followed below rule:
449      *    1. Phone
450      *    2. USB-C audio device
451      *    3. 3.5 mm audio device
452      *    4. Bluetooth device
453      *    5. Cast device
454      *    6. Cast group device
455      *
456      * So the device list will look like 5 slots ranked as below.
457      * Rule 4 + Rule 1 + the most recently used device + Rule 3 + Rule 2
458      * Any slot could be empty. And available device will belong to one of the slots.
459      *
460      * @return a negative integer, zero, or a positive integer
461      * as this object is less than, equal to, or greater than the specified object.
462      */
463     @Override
compareTo(MediaDevice another)464     public int compareTo(MediaDevice another) {
465         if (another == null) {
466             return -1;
467         }
468         // Check Bluetooth device is have same connection state
469         if (isConnected() ^ another.isConnected()) {
470             if (isConnected()) {
471                 return -1;
472             } else {
473                 return 1;
474             }
475         }
476 
477         if (getState() == STATE_SELECTED) {
478             return -1;
479         } else if (another.getState() == STATE_SELECTED) {
480             return 1;
481         }
482 
483         if (mType == another.mType) {
484             // Check device is muting expected device
485             if (isMutingExpectedDevice()) {
486                 return -1;
487             } else if (another.isMutingExpectedDevice()) {
488                 return 1;
489             }
490 
491             // Check fast pair device
492             if (isFastPairDevice()) {
493                 return -1;
494             } else if (another.isFastPairDevice()) {
495                 return 1;
496             }
497 
498             // Check carkit
499             if (isCarKitDevice()) {
500                 return -1;
501             } else if (another.isCarKitDevice()) {
502                 return 1;
503             }
504 
505             // Both devices have same connection status and type, compare the range zone
506             if (NearbyDevice.compareRangeZones(getRangeZone(), another.getRangeZone()) != 0) {
507                 return NearbyDevice.compareRangeZones(getRangeZone(), another.getRangeZone());
508             }
509 
510             // Set last used device at the first item
511             final String lastSelectedDevice = ConnectionRecordManager.getInstance()
512                     .getLastSelectedDevice();
513             if (TextUtils.equals(lastSelectedDevice, getId())) {
514                 return -1;
515             } else if (TextUtils.equals(lastSelectedDevice, another.getId())) {
516                 return 1;
517             }
518             // Sort by how many times the device has been used if there is usage record
519             if ((mConnectedRecord != another.mConnectedRecord)
520                     && (another.mConnectedRecord > 0 || mConnectedRecord > 0)) {
521                 return (another.mConnectedRecord - mConnectedRecord);
522             }
523 
524             // Both devices have never been used
525             // To devices with the same type, sort by alphabetical order
526             final String s1 = getName();
527             final String s2 = another.getName();
528             return s1.compareToIgnoreCase(s2);
529         } else {
530             // Both devices have never been used, the priority is:
531             // 1. Phone
532             // 2. USB-C audio device
533             // 3. 3.5 mm audio device
534             // 4. Bluetooth device
535             // 5. Cast device
536             // 6. Cast group device
537             return mType < another.mType ? -1 : 1;
538         }
539     }
540 
541     /**
542      * Gets the supported features of the route.
543      */
getFeatures()544     public List<String> getFeatures() {
545         if (mRouteInfo == null) {
546             Log.w(TAG, "Unable to get features. RouteInfo is empty");
547             return new ArrayList<>();
548         }
549         return mRouteInfo.getFeatures();
550     }
551 
552     /**
553      * Check if it is CarKit device
554      * @return true if it is CarKit device
555      */
isCarKitDevice()556     protected boolean isCarKitDevice() {
557         return false;
558     }
559 
560     /**
561      * Check if it is FastPair device
562      * @return {@code true} if it is FastPair device, otherwise return {@code false}
563      */
isFastPairDevice()564     protected boolean isFastPairDevice() {
565         return false;
566     }
567 
568     /**
569      * Check if it is muting expected device
570      * @return {@code true} if it is muting expected device, otherwise return {@code false}
571      */
isMutingExpectedDevice()572     public boolean isMutingExpectedDevice() {
573         return false;
574     }
575 
576     @Override
equals(Object obj)577     public boolean equals(Object obj) {
578         if (!(obj instanceof MediaDevice)) {
579             return false;
580         }
581         final MediaDevice otherDevice = (MediaDevice) obj;
582         return otherDevice.getId().equals(getId());
583     }
584 
585     @RequiresApi(34)
586     private static class Api34Impl {
587         @DoNotInline
isHostForOngoingSession(RouteListingPreference.Item item)588         static boolean isHostForOngoingSession(RouteListingPreference.Item item) {
589             int flags = item != null ? item.getFlags() : 0;
590             return (flags & FLAG_ONGOING_SESSION) != 0
591                     && (flags & FLAG_ONGOING_SESSION_MANAGED) != 0;
592         }
593 
594         @DoNotInline
isSuggestedDevice(RouteListingPreference.Item item)595         static boolean isSuggestedDevice(RouteListingPreference.Item item) {
596             return item != null && (item.getFlags() & FLAG_SUGGESTED) != 0;
597         }
598 
599         @DoNotInline
hasOngoingSession(RouteListingPreference.Item item)600         static boolean hasOngoingSession(RouteListingPreference.Item item) {
601             return item != null && (item.getFlags() & FLAG_ONGOING_SESSION) != 0;
602         }
603 
604         @DoNotInline
composeSubtext(RouteListingPreference.Item item, Context context)605         static String composeSubtext(RouteListingPreference.Item item, Context context) {
606             switch (item.getSubText()) {
607                 case SUBTEXT_ERROR_UNKNOWN:
608                     return context.getString(R.string.media_output_status_unknown_error);
609                 case SUBTEXT_SUBSCRIPTION_REQUIRED:
610                     return context.getString(R.string.media_output_status_require_premium);
611                 case SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED:
612                     return context.getString(R.string.media_output_status_not_support_downloads);
613                 case SUBTEXT_AD_ROUTING_DISALLOWED:
614                     return context.getString(R.string.media_output_status_try_after_ad);
615                 case SUBTEXT_DEVICE_LOW_POWER:
616                     return context.getString(R.string.media_output_status_device_in_low_power_mode);
617                 case SUBTEXT_UNAUTHORIZED:
618                     return context.getString(R.string.media_output_status_unauthorized);
619                 case SUBTEXT_TRACK_UNSUPPORTED:
620                     return context.getString(R.string.media_output_status_track_unsupported);
621                 case SUBTEXT_CUSTOM:
622                     return (String) item.getCustomSubtextMessage();
623             }
624             return "";
625         }
626     }
627 }
628