1 /** 2 * Copyright (C) 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 17 package com.android.car.broadcastradio.support.platform; 18 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.graphics.Bitmap; 23 import android.hardware.radio.ProgramSelector; 24 import android.hardware.radio.RadioManager.ProgramInfo; 25 import android.hardware.radio.RadioMetadata; 26 import android.media.MediaMetadata; 27 import android.media.Rating; 28 import android.util.Log; 29 30 import java.lang.annotation.Retention; 31 import java.lang.annotation.RetentionPolicy; 32 33 /** 34 * Proposed extensions to android.hardware.radio.RadioManager.ProgramInfo. 35 * 36 * They might eventually get pushed to the framework. 37 */ 38 public class ProgramInfoExt { 39 private static final String TAG = "BcRadioApp.pinfoext"; 40 41 /** 42 * If there is no suitable program name, return null instead of doing 43 * a fallback to channel display name. 44 */ 45 public static final int NAME_NO_CHANNEL_FALLBACK = 1 << 16; 46 47 /** 48 * Flags to control how to fetch program name with {@link #getProgramName}. 49 * 50 * Lower 16 bits are reserved for {@link ProgramSelectorExt#NameFlag}. 51 */ 52 @IntDef(prefix = { "NAME_" }, flag = true, value = { 53 ProgramSelectorExt.NAME_NO_MODULATION, 54 ProgramSelectorExt.NAME_MODULATION_ONLY, 55 NAME_NO_CHANNEL_FALLBACK, 56 }) 57 @Retention(RetentionPolicy.SOURCE) 58 public @interface NameFlag {} 59 60 private static final char EN_DASH = '\u2013'; 61 private static final String TITLE_SEPARATOR = " " + EN_DASH + " "; 62 63 private static final String[] PROGRAM_NAME_ORDER = new String[] { 64 RadioMetadata.METADATA_KEY_PROGRAM_NAME, 65 RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME, 66 RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME, 67 RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME, 68 RadioMetadata.METADATA_KEY_RDS_PS, 69 }; 70 71 /** 72 * Returns program name suitable to display. 73 * 74 * If there is no program name, it falls back to channel name. Flags related to 75 * the channel name display will be forwarded to the channel name generation method. 76 */ getProgramName(@onNull ProgramInfo info, @NameFlag int flags)77 public static @NonNull String getProgramName(@NonNull ProgramInfo info, @NameFlag int flags) { 78 RadioMetadata meta = info.getMetadata(); 79 if (meta != null) { 80 for (String key : PROGRAM_NAME_ORDER) { 81 String value = meta.getString(key); 82 if (value != null) return value; 83 } 84 } 85 86 if ((flags & NAME_NO_CHANNEL_FALLBACK) != 0) return ""; 87 88 ProgramSelector sel = info.getSelector(); 89 90 // if it's AM/FM program, prefer to display currently used AF frequency 91 if (ProgramSelectorExt.isAmFmProgram(sel)) { 92 ProgramSelector.Identifier phy = info.getPhysicallyTunedTo(); 93 if (phy != null && phy.getType() == ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY) { 94 String chName = ProgramSelectorExt.formatAmFmFrequency(phy.getValue(), flags); 95 if (chName != null) return chName; 96 } 97 } 98 99 String selName = ProgramSelectorExt.getDisplayName(sel, flags); 100 if (selName != null) return selName; 101 102 Log.w(TAG, "ProgramInfo without a name nor channel name"); 103 return ""; 104 } 105 106 /** 107 * Proposed reimplementation of {@link RadioManager#ProgramInfo#getMetadata}. 108 * 109 * As opposed to the original implementation, it never returns null. 110 */ getMetadata(@onNull ProgramInfo info)111 public static @NonNull RadioMetadata getMetadata(@NonNull ProgramInfo info) { 112 RadioMetadata meta = info.getMetadata(); 113 if (meta != null) return meta; 114 115 /* Creating new Metadata object on each get won't be necessary after we 116 * push this code to the framework. */ 117 return (new RadioMetadata.Builder()).build(); 118 } 119 120 /** 121 * Converts {@ProgramInfo} to {@MediaMetadata}. 122 * 123 * This method is meant to be used for currently playing station in {@link MediaSession}. 124 * 125 * @param info {@link ProgramInfo} to convert 126 * @param isFavorite true, if a given program is a favorite 127 * @param imageResolver metadata images resolver/cache 128 * @return {@link MediaMetadata} object 129 */ toMediaMetadata(@onNull ProgramInfo info, boolean isFavorite, @Nullable ImageResolver imageResolver)130 public static @NonNull MediaMetadata toMediaMetadata(@NonNull ProgramInfo info, 131 boolean isFavorite, @Nullable ImageResolver imageResolver) { 132 MediaMetadata.Builder bld = new MediaMetadata.Builder(); 133 134 bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, getProgramName(info, 0)); 135 136 RadioMetadata meta = info.getMetadata(); 137 if (meta != null) { 138 String title = meta.getString(RadioMetadata.METADATA_KEY_TITLE); 139 if (title != null) { 140 bld.putString(MediaMetadata.METADATA_KEY_TITLE, title); 141 } 142 String artist = meta.getString(RadioMetadata.METADATA_KEY_ARTIST); 143 if (artist != null) { 144 bld.putString(MediaMetadata.METADATA_KEY_ARTIST, artist); 145 } 146 String album = meta.getString(RadioMetadata.METADATA_KEY_ALBUM); 147 if (album != null) { 148 bld.putString(MediaMetadata.METADATA_KEY_ALBUM, album); 149 } 150 if (title != null || artist != null) { 151 String subtitle; 152 if (title == null) { 153 subtitle = artist; 154 } else if (artist == null) { 155 subtitle = title; 156 } else { 157 subtitle = title + TITLE_SEPARATOR + artist; 158 } 159 bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); 160 } 161 long albumArtId = RadioMetadataExt.getGlobalBitmapId(meta, 162 RadioMetadata.METADATA_KEY_ART); 163 if (albumArtId != 0 && imageResolver != null) { 164 Bitmap bm = imageResolver.resolve(albumArtId); 165 if (bm != null) bld.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bm); 166 } 167 } 168 169 bld.putRating(MediaMetadata.METADATA_KEY_USER_RATING, Rating.newHeartRating(isFavorite)); 170 171 return bld.build(); 172 } 173 } 174