• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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