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; 25 import android.hardware.radio.RadioManager.ProgramInfo; 26 import android.hardware.radio.RadioMetadata; 27 import android.media.MediaMetadata; 28 import android.media.Rating; 29 import android.util.Log; 30 31 import java.lang.annotation.Retention; 32 import java.lang.annotation.RetentionPolicy; 33 import java.util.Objects; 34 35 /** 36 * Proposed extensions to android.hardware.radio.RadioManager.ProgramInfo. 37 * 38 * They might eventually get pushed to the framework. 39 */ 40 public class ProgramInfoExt { 41 private static final String TAG = "BcRadioApp.pinfoext"; 42 43 /** 44 * If there is no suitable program name, return null instead of doing 45 * a fallback to channel display name. 46 */ 47 public static final int NAME_NO_CHANNEL_FALLBACK = 1 << 16; 48 49 /** 50 * Flags to control how to fetch program name with {@link #getProgramName}. 51 * 52 * Lower 16 bits are reserved for {@link ProgramSelectorExt#NameFlag}. 53 */ 54 @IntDef(prefix = { "NAME_" }, flag = true, value = { 55 ProgramSelectorExt.NAME_NO_MODULATION, 56 ProgramSelectorExt.NAME_MODULATION_ONLY, 57 NAME_NO_CHANNEL_FALLBACK, 58 }) 59 @Retention(RetentionPolicy.SOURCE) 60 public @interface NameFlag {} 61 62 private static final char EN_DASH = '\u2013'; 63 private static final String TITLE_SEPARATOR = " " + EN_DASH + " "; 64 65 private static final String[] PROGRAM_NAME_ORDER = new String[] { 66 RadioMetadata.METADATA_KEY_PROGRAM_NAME, 67 RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME, 68 RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME, 69 RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME, 70 RadioMetadata.METADATA_KEY_RDS_PS, 71 }; 72 73 /** 74 * Returns program name suitable to display. 75 * 76 * <p>If there is no program name, it falls back to channel name. Flags related to 77 * the channel name display will be forwarded to the channel name generation method. 78 * 79 * @param info {@link ProgramInfo} to get name from 80 * @param flags Fallback method 81 * @param programNameOrder {@link RadioMetadata} metadata keys to pull from {@link ProgramInfo} 82 * for the program name 83 */ 84 @NonNull getProgramName(@onNull ProgramInfo info, @NameFlag int flags, @NonNull String[] programNameOrder)85 public static String getProgramName(@NonNull ProgramInfo info, @NameFlag int flags, 86 @NonNull String[] programNameOrder) { 87 Objects.requireNonNull(info, "info can not be null."); 88 Objects.requireNonNull(programNameOrder, "programNameOrder can not be null"); 89 90 RadioMetadata meta = info.getMetadata(); 91 if (meta != null) { 92 for (String key : programNameOrder) { 93 String value = meta.getString(key); 94 if (value != null) return value; 95 } 96 } 97 98 if ((flags & NAME_NO_CHANNEL_FALLBACK) != 0) return ""; 99 100 ProgramSelector sel = info.getSelector(); 101 102 // if it's AM/FM program, prefer to display currently used AF frequency 103 if (ProgramSelectorExt.isAmFmProgram(sel)) { 104 ProgramSelector.Identifier phy = info.getPhysicallyTunedTo(); 105 if (phy != null && phy.getType() == ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY) { 106 String chName = ProgramSelectorExt.formatAmFmFrequency(phy.getValue(), flags); 107 if (chName != null) return chName; 108 } 109 } 110 111 String selName = ProgramSelectorExt.getDisplayName(sel, flags); 112 if (selName != null) return selName; 113 114 Log.w(TAG, "ProgramInfo without a name nor channel name"); 115 return ""; 116 } 117 118 /** 119 * Returns program name suitable to display. 120 * 121 * <p>If there is no program name, it falls back to channel name. Flags related to 122 * the channel name display will be forwarded to the channel name generation method. 123 */ 124 @NonNull getProgramName(@onNull ProgramInfo info, @NameFlag int flags)125 public static String getProgramName(@NonNull ProgramInfo info, @NameFlag int flags) { 126 return getProgramName(info, flags, PROGRAM_NAME_ORDER); 127 } 128 129 /** 130 * Proposed reimplementation of {@link RadioManager#ProgramInfo#getMetadata}. 131 * 132 * As opposed to the original implementation, it never returns null. 133 */ getMetadata(@onNull ProgramInfo info)134 public static @NonNull RadioMetadata getMetadata(@NonNull ProgramInfo info) { 135 RadioMetadata meta = info.getMetadata(); 136 if (meta != null) return meta; 137 138 /* Creating new Metadata object on each get won't be necessary after we 139 * push this code to the framework. */ 140 return (new RadioMetadata.Builder()).build(); 141 } 142 143 /** 144 * Converts {@link ProgramInfo} to {@link MediaMetadata} for displaying. 145 * 146 * <p>This method is meant to be used for displaying the currently playing station in 147 * {@link MediaSession}, only a subset of keys populated in {@link ProgramInfo#toMediaMetadata} 148 * will be populated in this method. 149 * 150 * <ul> 151 * The following keys will be populated in the {@link MediaMetadata}: 152 * <li>{@link MediaMetadata#METADATA_KEY_DISPLAY_TITLE}</li> 153 * <li>{@link MediaMetadata#METADATA_KEY_DISPLAY_SUBTITLE}</li> 154 * <li>{@link MediaMetadata#METADATA_KEY_ALBUM_ART}</li> 155 * <li>{@link MediaMetadata#METADATA_KEY_USER_RATING}</li> 156 * <ul/> 157 * 158 * @param info {@link ProgramInfo} to convert 159 * @param isFavorite {@code true}, if a given program is a favorite 160 * @param imageResolver metadata images resolver/cache 161 * @param programNameOrder order of keys to look for program name in {@link ProgramInfo} 162 * @return {@link MediaMetadata} object 163 */ 164 @NonNull toMediaDisplayMetadata(@onNull ProgramInfo info, boolean isFavorite, @NonNull ImageResolver imageResolver, @NonNull String[] programNameOrder)165 public static MediaMetadata toMediaDisplayMetadata(@NonNull ProgramInfo info, 166 boolean isFavorite, @NonNull ImageResolver imageResolver, 167 @NonNull String[] programNameOrder) { 168 Objects.requireNonNull(info, "info can not be null."); 169 Objects.requireNonNull(imageResolver, "imageResolver can not be null."); 170 Objects.requireNonNull(programNameOrder, "programNameOrder can not be null."); 171 172 MediaMetadata.Builder bld = new MediaMetadata.Builder(); 173 174 ProgramSelector selector = 175 ProgramSelectorExt.createAmFmSelector(info.getLogicallyTunedTo().getValue()); 176 String displayTitle = ProgramSelectorExt.getDisplayName(selector, info.getChannel()); 177 bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, displayTitle); 178 String subtitle = getProgramName(info, /* flags= */ 0, programNameOrder); 179 bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); 180 181 Bitmap bm = resolveAlbumArtBitmap(info.getMetadata(), imageResolver); 182 if (bm != null) bld.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bm); 183 184 bld.putRating(MediaMetadata.METADATA_KEY_USER_RATING, Rating.newHeartRating(isFavorite)); 185 186 return bld.build(); 187 } 188 189 /** 190 * Converts {@link ProgramInfo} to {@link MediaMetadata}. 191 * 192 * <p>This method is meant to be used for currently playing station in {@link MediaSession}. 193 * 194 * <ul> 195 * The following keys will be populated in the {@link MediaMetadata}: 196 * <li>{@link MediaMetadata#METADATA_KEY_DISPLAY_TITLE}</li> 197 * <li>{@link MediaMetadata#METADATA_KEY_TITLE}</li> 198 * <li>{@link MediaMetadata#METADATA_KEY_ARTIST}</li> 199 * <li>{@link MediaMetadata#METADATA_KEY_ALBUM}</li> 200 * <li>{@link MediaMetadata#METADATA_KEY_DISPLAY_SUBTITLE}</li> 201 * <li>{@link MediaMetadata#METADATA_KEY_ALBUM_ART}</li> 202 * <li>{@link MediaMetadata#METADATA_KEY_USER_RATING}</li> 203 * <ul/> 204 * 205 * @param info {@link ProgramInfo} to convert 206 * @param isFavorite {@code true}, if a given program is a favorite 207 * @param imageResolver metadata images resolver/cache 208 * @return {@link MediaMetadata} object 209 */ toMediaMetadata(@onNull ProgramInfo info, boolean isFavorite, @Nullable ImageResolver imageResolver)210 public static @NonNull MediaMetadata toMediaMetadata(@NonNull ProgramInfo info, 211 boolean isFavorite, @Nullable ImageResolver imageResolver) { 212 MediaMetadata.Builder bld = new MediaMetadata.Builder(); 213 214 bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, getProgramName(info, 0)); 215 216 RadioMetadata meta = info.getMetadata(); 217 if (meta != null) { 218 String title = meta.getString(RadioMetadata.METADATA_KEY_TITLE); 219 if (title != null) { 220 bld.putString(MediaMetadata.METADATA_KEY_TITLE, title); 221 } 222 String artist = meta.getString(RadioMetadata.METADATA_KEY_ARTIST); 223 if (artist != null) { 224 bld.putString(MediaMetadata.METADATA_KEY_ARTIST, artist); 225 } 226 String album = meta.getString(RadioMetadata.METADATA_KEY_ALBUM); 227 if (album != null) { 228 bld.putString(MediaMetadata.METADATA_KEY_ALBUM, album); 229 } 230 if (title != null || artist != null) { 231 String subtitle; 232 if (title == null) { 233 subtitle = artist; 234 } else if (artist == null) { 235 subtitle = title; 236 } else { 237 subtitle = title + TITLE_SEPARATOR + artist; 238 } 239 bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); 240 } 241 242 Bitmap bm = resolveAlbumArtBitmap(meta, imageResolver); 243 if (bm != null) bld.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bm); 244 } 245 246 bld.putRating(MediaMetadata.METADATA_KEY_USER_RATING, Rating.newHeartRating(isFavorite)); 247 248 return bld.build(); 249 } 250 resolveAlbumArtBitmap(@onNull RadioMetadata meta, @NonNull ImageResolver imageResolver)251 private static Bitmap resolveAlbumArtBitmap(@NonNull RadioMetadata meta, 252 @NonNull ImageResolver imageResolver) { 253 long albumArtId = RadioMetadataExt.getGlobalBitmapId(meta, RadioMetadata.METADATA_KEY_ART); 254 if (albumArtId != 0 && imageResolver != null) { 255 return imageResolver.resolve(albumArtId); 256 } 257 return null; 258 } 259 } 260