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