1 /*
2  * Copyright (C) 2015 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 android.hardware.radio;
17 
18 import android.annotation.FlaggedApi;
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SystemApi;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.os.Bundle;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.util.ArrayMap;
28 import android.util.Log;
29 import android.util.SparseArray;
30 
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.List;
34 import java.util.Objects;
35 import java.util.Set;
36 
37 /**
38  * Contains meta data about a radio program such as station name, song title, artist etc...
39  * @hide
40  */
41 @SystemApi
42 public final class RadioMetadata implements Parcelable {
43     private static final String TAG = "BroadcastRadio.metadata";
44 
45     /**
46      * The RDS Program Information.
47      */
48     public static final String METADATA_KEY_RDS_PI = "android.hardware.radio.metadata.RDS_PI";
49 
50     /**
51      * The RDS Program Service.
52      */
53     public static final String METADATA_KEY_RDS_PS = "android.hardware.radio.metadata.RDS_PS";
54 
55     /**
56      * The RDS PTY.
57      */
58     public static final String METADATA_KEY_RDS_PTY = "android.hardware.radio.metadata.RDS_PTY";
59 
60     /**
61      * The RBDS PTY.
62      */
63     public static final String METADATA_KEY_RBDS_PTY = "android.hardware.radio.metadata.RBDS_PTY";
64 
65     /**
66      * The RBDS Radio Text.
67      */
68     public static final String METADATA_KEY_RDS_RT = "android.hardware.radio.metadata.RDS_RT";
69 
70     /**
71      * The song title.
72      */
73     public static final String METADATA_KEY_TITLE = "android.hardware.radio.metadata.TITLE";
74 
75     /**
76      * The artist name.
77      */
78     public static final String METADATA_KEY_ARTIST = "android.hardware.radio.metadata.ARTIST";
79 
80     /**
81      * The album name.
82      */
83     public static final String METADATA_KEY_ALBUM = "android.hardware.radio.metadata.ALBUM";
84 
85     /**
86      * The music genre.
87      */
88     public static final String METADATA_KEY_GENRE = "android.hardware.radio.metadata.GENRE";
89 
90     /**
91      * The radio station icon {@link Bitmap}.
92      */
93     public static final String METADATA_KEY_ICON = "android.hardware.radio.metadata.ICON";
94 
95     /**
96      * The artwork for the song/album {@link Bitmap}.
97      */
98     public static final String METADATA_KEY_ART = "android.hardware.radio.metadata.ART";
99 
100     /**
101      * The clock.
102      */
103     public static final String METADATA_KEY_CLOCK = "android.hardware.radio.metadata.CLOCK";
104 
105     /**
106      * Technology-independent program name (station name).
107      */
108     public static final String METADATA_KEY_PROGRAM_NAME =
109             "android.hardware.radio.metadata.PROGRAM_NAME";
110 
111     /**
112      * DAB ensemble name.
113      */
114     public static final String METADATA_KEY_DAB_ENSEMBLE_NAME =
115             "android.hardware.radio.metadata.DAB_ENSEMBLE_NAME";
116 
117     /**
118      * DAB ensemble name - short version (up to 8 characters).
119      */
120     public static final String METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT =
121             "android.hardware.radio.metadata.DAB_ENSEMBLE_NAME_SHORT";
122 
123     /**
124      * DAB service name.
125      */
126     public static final String METADATA_KEY_DAB_SERVICE_NAME =
127             "android.hardware.radio.metadata.DAB_SERVICE_NAME";
128 
129     /**
130      * DAB service name - short version (up to 8 characters).
131      */
132     public static final String METADATA_KEY_DAB_SERVICE_NAME_SHORT =
133             "android.hardware.radio.metadata.DAB_SERVICE_NAME_SHORT";
134 
135     /**
136      * DAB component name.
137      */
138     public static final String METADATA_KEY_DAB_COMPONENT_NAME =
139             "android.hardware.radio.metadata.DAB_COMPONENT_NAME";
140 
141     /**
142      * DAB component name.
143      */
144     public static final String METADATA_KEY_DAB_COMPONENT_NAME_SHORT =
145             "android.hardware.radio.metadata.DAB_COMPONENT_NAME_SHORT";
146 
147     /**
148      * Short context description of comment
149      *
150      * <p>Comment could relate to the current audio program content, or it might
151      * be unrelated information that the station chooses to send. It is composed
152      * of short content description and actual text (see NRSC-G200-A and id3v2.3.0
153      * for more info).
154      */
155     @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
156     public static final String METADATA_KEY_COMMENT_SHORT_DESCRIPTION =
157             "android.hardware.radio.metadata.COMMENT_SHORT_DESCRIPTION";
158 
159     /**
160      * Actual text of comment
161      *
162      * @see #METADATA_KEY_COMMENT_SHORT_DESCRIPTION
163      */
164     @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
165     public static final String METADATA_KEY_COMMENT_ACTUAL_TEXT =
166             "android.hardware.radio.metadata.COMMENT_ACTUAL_TEXT";
167 
168     /**
169      * Commercial
170      *
171      * <p>Commercial is application specific and generally used to facilitate the
172      * sale of products and services (see NRSC-G200-A and id3v2.3.0 for more info).
173      */
174     @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
175     public static final String METADATA_KEY_COMMERCIAL =
176             "android.hardware.radio.metadata.COMMERCIAL";
177 
178     /**
179      * Array of Unique File Identifiers
180      *
181      * <p>Unique File Identifier (UFID) can be used to transmit an alphanumeric
182      * identifier of the current content, or of an advertised product or
183      * service (see NRSC-G200-A and id3v2.3.0 for more info).
184      */
185     @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
186     public static final String METADATA_KEY_UFIDS = "android.hardware.radio.metadata.UFIDS";
187 
188     /**
189      * HD short station name or HD universal short station name
190      *
191      * <p>It can be up to 12 characters (see SY_IDD_1020s for more info).
192      */
193     @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
194     public static final String METADATA_KEY_HD_STATION_NAME_SHORT =
195             "android.hardware.radio.metadata.HD_STATION_NAME_SHORT";
196 
197     /**
198      * HD long station name, HD station slogan or HD station message
199      *
200      * <p>(see SY_IDD_1020s for more info)
201      */
202     @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
203     public static final String METADATA_KEY_HD_STATION_NAME_LONG =
204             "android.hardware.radio.metadata.HD_STATION_NAME_LONG";
205 
206     /**
207      * Bit mask of all HD Radio subchannels available
208      *
209      * <p>Bit {@link ProgramSelector#SUB_CHANNEL_HD_1} from LSB represents the
210      * availability of HD-1 subchannel (main program service, MPS). Bits
211      * {@link ProgramSelector#SUB_CHANNEL_HD_2} to {@link ProgramSelector#SUB_CHANNEL_HD_8}
212      * from LSB represent HD-2 to HD-8 subchannel (supplemental program services, SPS)
213      * respectively.
214      */
215     @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
216     public static final String METADATA_KEY_HD_SUBCHANNELS_AVAILABLE =
217             "android.hardware.radio.metadata.HD_SUBCHANNELS_AVAILABLE";
218 
219     private static final int METADATA_TYPE_INVALID = -1;
220     private static final int METADATA_TYPE_INT = 0;
221     private static final int METADATA_TYPE_TEXT = 1;
222     private static final int METADATA_TYPE_BITMAP = 2;
223     private static final int METADATA_TYPE_CLOCK = 3;
224     private static final int METADATA_TYPE_TEXT_ARRAY = 4;
225 
226     private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
227 
228     static {
229         METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PI, METADATA_TYPE_INT)230         METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PI, METADATA_TYPE_INT);
METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PS, METADATA_TYPE_TEXT)231         METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PS, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PTY, METADATA_TYPE_INT)232         METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PTY, METADATA_TYPE_INT);
METADATA_KEYS_TYPE.put(METADATA_KEY_RBDS_PTY, METADATA_TYPE_INT)233         METADATA_KEYS_TYPE.put(METADATA_KEY_RBDS_PTY, METADATA_TYPE_INT);
METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_RT, METADATA_TYPE_TEXT)234         METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_RT, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT)235         METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT)236         METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT)237         METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT)238         METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_ICON, METADATA_TYPE_BITMAP)239         METADATA_KEYS_TYPE.put(METADATA_KEY_ICON, METADATA_TYPE_BITMAP);
METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP)240         METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
METADATA_KEYS_TYPE.put(METADATA_KEY_CLOCK, METADATA_TYPE_CLOCK)241         METADATA_KEYS_TYPE.put(METADATA_KEY_CLOCK, METADATA_TYPE_CLOCK);
METADATA_KEYS_TYPE.put(METADATA_KEY_PROGRAM_NAME, METADATA_TYPE_TEXT)242         METADATA_KEYS_TYPE.put(METADATA_KEY_PROGRAM_NAME, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_ENSEMBLE_NAME, METADATA_TYPE_TEXT)243         METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_ENSEMBLE_NAME, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT, METADATA_TYPE_TEXT)244         METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_SERVICE_NAME, METADATA_TYPE_TEXT)245         METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_SERVICE_NAME, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_SERVICE_NAME_SHORT, METADATA_TYPE_TEXT)246         METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_SERVICE_NAME_SHORT, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_COMPONENT_NAME, METADATA_TYPE_TEXT)247         METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_COMPONENT_NAME, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_COMPONENT_NAME_SHORT, METADATA_TYPE_TEXT)248         METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_COMPONENT_NAME_SHORT, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_COMMENT_SHORT_DESCRIPTION, METADATA_TYPE_TEXT)249         METADATA_KEYS_TYPE.put(METADATA_KEY_COMMENT_SHORT_DESCRIPTION, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_COMMENT_ACTUAL_TEXT, METADATA_TYPE_TEXT)250         METADATA_KEYS_TYPE.put(METADATA_KEY_COMMENT_ACTUAL_TEXT, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_COMMERCIAL, METADATA_TYPE_TEXT)251         METADATA_KEYS_TYPE.put(METADATA_KEY_COMMERCIAL, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_UFIDS, METADATA_TYPE_TEXT_ARRAY)252         METADATA_KEYS_TYPE.put(METADATA_KEY_UFIDS, METADATA_TYPE_TEXT_ARRAY);
METADATA_KEYS_TYPE.put(METADATA_KEY_HD_STATION_NAME_SHORT, METADATA_TYPE_TEXT)253         METADATA_KEYS_TYPE.put(METADATA_KEY_HD_STATION_NAME_SHORT, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_HD_STATION_NAME_LONG, METADATA_TYPE_TEXT)254         METADATA_KEYS_TYPE.put(METADATA_KEY_HD_STATION_NAME_LONG, METADATA_TYPE_TEXT);
METADATA_KEYS_TYPE.put(METADATA_KEY_HD_SUBCHANNELS_AVAILABLE, METADATA_TYPE_INT)255         METADATA_KEYS_TYPE.put(METADATA_KEY_HD_SUBCHANNELS_AVAILABLE, METADATA_TYPE_INT);
256     }
257 
258     // keep in sync with: system/media/radio/include/system/radio_metadata.h
259     private static final int NATIVE_KEY_INVALID     = -1;
260     private static final int NATIVE_KEY_RDS_PI      = 0;
261     private static final int NATIVE_KEY_RDS_PS      = 1;
262     private static final int NATIVE_KEY_RDS_PTY     = 2;
263     private static final int NATIVE_KEY_RBDS_PTY    = 3;
264     private static final int NATIVE_KEY_RDS_RT      = 4;
265     private static final int NATIVE_KEY_TITLE       = 5;
266     private static final int NATIVE_KEY_ARTIST      = 6;
267     private static final int NATIVE_KEY_ALBUM       = 7;
268     private static final int NATIVE_KEY_GENRE       = 8;
269     private static final int NATIVE_KEY_ICON        = 9;
270     private static final int NATIVE_KEY_ART         = 10;
271     private static final int NATIVE_KEY_CLOCK       = 11;
272 
273     private static final SparseArray<String> NATIVE_KEY_MAPPING;
274 
275     static {
276         NATIVE_KEY_MAPPING = new SparseArray<String>();
NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PI, METADATA_KEY_RDS_PI)277         NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PI, METADATA_KEY_RDS_PI);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PS, METADATA_KEY_RDS_PS)278         NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PS, METADATA_KEY_RDS_PS);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PTY, METADATA_KEY_RDS_PTY)279         NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PTY, METADATA_KEY_RDS_PTY);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_RBDS_PTY, METADATA_KEY_RBDS_PTY)280         NATIVE_KEY_MAPPING.put(NATIVE_KEY_RBDS_PTY, METADATA_KEY_RBDS_PTY);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_RT, METADATA_KEY_RDS_RT)281         NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_RT, METADATA_KEY_RDS_RT);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_TITLE, METADATA_KEY_TITLE)282         NATIVE_KEY_MAPPING.put(NATIVE_KEY_TITLE, METADATA_KEY_TITLE);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_ARTIST, METADATA_KEY_ARTIST)283         NATIVE_KEY_MAPPING.put(NATIVE_KEY_ARTIST, METADATA_KEY_ARTIST);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_ALBUM, METADATA_KEY_ALBUM)284         NATIVE_KEY_MAPPING.put(NATIVE_KEY_ALBUM, METADATA_KEY_ALBUM);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_GENRE, METADATA_KEY_GENRE)285         NATIVE_KEY_MAPPING.put(NATIVE_KEY_GENRE, METADATA_KEY_GENRE);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_ICON, METADATA_KEY_ICON)286         NATIVE_KEY_MAPPING.put(NATIVE_KEY_ICON, METADATA_KEY_ICON);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_ART, METADATA_KEY_ART)287         NATIVE_KEY_MAPPING.put(NATIVE_KEY_ART, METADATA_KEY_ART);
NATIVE_KEY_MAPPING.put(NATIVE_KEY_CLOCK, METADATA_KEY_CLOCK)288         NATIVE_KEY_MAPPING.put(NATIVE_KEY_CLOCK, METADATA_KEY_CLOCK);
289     }
290 
291     /**
292      * Provides a Clock that can be used to describe time as provided by the Radio.
293      *
294      * <p>The clock time is defined by the seconds since epoch at the UTC + 0 timezone
295      * and timezone offset from UTC + 0 represented in number of minutes.
296      *
297      * @hide
298      */
299     @SystemApi
300     public static final class Clock implements Parcelable {
301         private final long mUtcEpochSeconds;
302         private final int mTimezoneOffsetMinutes;
303 
describeContents()304         public int describeContents() {
305             return 0;
306         }
307 
writeToParcel(Parcel out, int flags)308         public void writeToParcel(Parcel out, int flags) {
309             out.writeLong(mUtcEpochSeconds);
310             out.writeInt(mTimezoneOffsetMinutes);
311         }
312 
313         public static final @android.annotation.NonNull Parcelable.Creator<Clock> CREATOR
314                 = new Parcelable.Creator<Clock>() {
315             public Clock createFromParcel(Parcel in) {
316                 return new Clock(in);
317             }
318 
319             public Clock[] newArray(int size) {
320                 return new Clock[size];
321             }
322         };
323 
Clock(long utcEpochSeconds, int timezoneOffsetMinutes)324         public Clock(long utcEpochSeconds, int timezoneOffsetMinutes) {
325             mUtcEpochSeconds = utcEpochSeconds;
326             mTimezoneOffsetMinutes = timezoneOffsetMinutes;
327         }
328 
Clock(Parcel in)329         private Clock(Parcel in) {
330             mUtcEpochSeconds = in.readLong();
331             mTimezoneOffsetMinutes = in.readInt();
332         }
333 
getUtcEpochSeconds()334         public long getUtcEpochSeconds() {
335             return mUtcEpochSeconds;
336         }
337 
getTimezoneOffsetMinutes()338         public int getTimezoneOffsetMinutes() {
339             return mTimezoneOffsetMinutes;
340         }
341     }
342 
343     private final Bundle mBundle;
344 
345     // Lazily computed hash code based upon the contents of mBundle.
346     private Integer mHashCode;
347 
348     @Override
hashCode()349     public int hashCode() {
350         if (mHashCode == null) {
351             List<String> keys = new ArrayList<String>(mBundle.keySet());
352             keys.sort(null);
353             Object[] objs = new Object[2 * keys.size()];
354             for (int i = 0; i < keys.size(); i++) {
355                 objs[2 * i] = keys.get(i);
356                 objs[2 * i + 1] = mBundle.get(keys.get(i));
357             }
358             mHashCode = Arrays.hashCode(objs);
359         }
360         return mHashCode;
361     }
362 
363     @Override
equals(@ullable Object obj)364     public boolean equals(@Nullable Object obj) {
365         if (this == obj) return true;
366         if (!(obj instanceof RadioMetadata)) return false;
367         Bundle otherBundle = ((RadioMetadata) obj).mBundle;
368         if (!mBundle.keySet().equals(otherBundle.keySet())) {
369             return false;
370         }
371         for (String key : mBundle.keySet()) {
372             if (Flags.hdRadioImproved() && Objects.equals(METADATA_KEYS_TYPE.get(key),
373                     METADATA_TYPE_TEXT_ARRAY)) {
374                 if (!Arrays.equals(mBundle.getStringArray(key), otherBundle.getStringArray(key))) {
375                     return false;
376                 }
377             } else if (!Objects.equals(mBundle.get(key), otherBundle.get(key))) {
378                 return false;
379             }
380         }
381         return true;
382     }
383 
RadioMetadata()384     RadioMetadata() {
385         mBundle = new Bundle();
386     }
387 
RadioMetadata(Bundle bundle)388     private RadioMetadata(Bundle bundle) {
389         mBundle = new Bundle(bundle);
390     }
391 
RadioMetadata(Parcel in)392     private RadioMetadata(Parcel in) {
393         mBundle = in.readBundle();
394     }
395 
396     @NonNull
397     @Override
toString()398     public String toString() {
399         StringBuilder sb = new StringBuilder("RadioMetadata[");
400 
401         final String removePrefix = "android.hardware.radio.metadata";
402 
403         boolean first = true;
404         for (String key : mBundle.keySet()) {
405             if (first) first = false;
406             else sb.append(", ");
407 
408             String keyDisp = key;
409             if (key.startsWith(removePrefix)) keyDisp = key.substring(removePrefix.length());
410 
411             sb.append(keyDisp);
412             sb.append('=');
413             if (Flags.hdRadioImproved() && Objects.equals(METADATA_KEYS_TYPE.get(key),
414                     METADATA_TYPE_TEXT_ARRAY)) {
415                 String[] stringArrayValue = mBundle.getStringArray(key);
416                 sb.append('[');
417                 for (int i = 0; i < stringArrayValue.length; i++) {
418                     if (i != 0) {
419                         sb.append(',');
420                     }
421                     sb.append(stringArrayValue[i]);
422                 }
423                 sb.append(']');
424             } else {
425                 sb.append(mBundle.get(key));
426             }
427 
428         }
429 
430         sb.append("]");
431         return sb.toString();
432     }
433 
434     /**
435      * Returns {@code true} if the given key is contained in the meta data
436      *
437      * @param key a String key
438      * @return {@code true} if the key exists in this meta data, {@code false} otherwise
439      */
containsKey(String key)440     public boolean containsKey(String key) {
441         return mBundle.containsKey(key);
442     }
443 
444     /**
445      * Returns the text value associated with the given key as a String, or null
446      * if the key is not found in the meta data.
447      *
448      * @param key The key the value is stored under
449      * @return a String value, or null
450      */
getString(String key)451     public String getString(String key) {
452         return mBundle.getString(key);
453     }
454 
putInt(Bundle bundle, String key, int value)455     private static void putInt(Bundle bundle, String key, int value) {
456         int type = METADATA_KEYS_TYPE.getOrDefault(key, METADATA_TYPE_INVALID);
457         if (type != METADATA_TYPE_INT && type != METADATA_TYPE_BITMAP) {
458             throw new IllegalArgumentException("The " + key + " key cannot be used to put an int");
459         }
460         bundle.putInt(key, value);
461     }
462 
463     /**
464      * Returns the value associated with the given key,
465      * or 0 if the key is not found in the meta data.
466      *
467      * @param key The key the value is stored under
468      * @return an int value
469      */
getInt(String key)470     public int getInt(String key) {
471         return mBundle.getInt(key, 0);
472     }
473 
474     /**
475      * Returns a {@link Bitmap} for the given key or null if the key is not found in the meta data.
476      *
477      * @param key The key the value is stored under
478      * @return a {@link Bitmap} or null
479      * @deprecated Use getBitmapId(String) instead
480      */
481     @Deprecated
getBitmap(String key)482     public Bitmap getBitmap(String key) {
483         Bitmap bmp = null;
484         try {
485             bmp = mBundle.getParcelable(key, android.graphics.Bitmap.class);
486         } catch (Exception e) {
487             // ignore, value was not a bitmap
488             Log.w(TAG, "Failed to retrieve a key as Bitmap.", e);
489         }
490         return bmp;
491     }
492 
493     /**
494      * Retrieves an identifier for a bitmap.
495      *
496      * <p>The format of an identifier is opaque to the application,
497      * with a special case of value 0 being invalid.
498      * An identifier for a given image-tuner pair is unique, so an application
499      * may cache images and determine if there is a necessity to fetch them
500      * again - if identifier changes, it means the image has changed.
501      *
502      * <p>Only bitmap keys may be used with this method:
503      * <ul>
504      *     <li>{@link #METADATA_KEY_ICON}</li>
505      *     <li>{@link #METADATA_KEY_ART}</li>
506      * </ul>
507      *
508      * @param key The key the value is stored under.
509      * @return a bitmap identifier or 0 if it's missing.
510      * @throws NullPointerException if metadata key is {@code null}
511      * @throws IllegalArgumentException if the metadata with the key is not found in
512      * metadata or the key is not of bitmap-key type
513      */
514     @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
getBitmapId(@onNull String key)515     public int getBitmapId(@NonNull String key) {
516         Objects.requireNonNull(key, "Metadata key can not be null");
517         if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) {
518             throw new IllegalArgumentException("Failed to retrieve key " + key + " as bitmap key");
519         }
520         return getInt(key);
521     }
522 
getClock(String key)523     public Clock getClock(String key) {
524         Clock clock = null;
525         try {
526             clock = mBundle.getParcelable(key, android.hardware.radio.RadioMetadata.Clock.class);
527         } catch (Exception e) {
528             // ignore, value was not a clock.
529             Log.w(TAG, "Failed to retrieve a key as Clock.", e);
530         }
531         return clock;
532     }
533 
534     /**
535      * Gets the string array value associated with the given key as a string
536      * array.
537      *
538      * <p>Only string array keys may be used with this method:
539      * <ul>
540      *     <li>{@link #METADATA_KEY_UFIDS}</li>
541      * </ul>
542      *
543      * @param key The key the value is stored under
544      * @return String array of the given string-array-type key
545      * @throws NullPointerException if metadata key is {@code null}
546      * @throws IllegalArgumentException if the metadata with the key is not found in
547      * metadata or the key is not of string-array type
548      */
549     @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
550     @NonNull
getStringArray(@onNull String key)551     public String[] getStringArray(@NonNull String key) {
552         Objects.requireNonNull(key, "Metadata key can not be null");
553         if (!Objects.equals(METADATA_KEYS_TYPE.get(key), METADATA_TYPE_TEXT_ARRAY)) {
554             throw new IllegalArgumentException("Failed to retrieve key " + key
555                     + " as string array");
556         }
557         String[] stringArrayValue = mBundle.getStringArray(key);
558         if (stringArrayValue == null) {
559             throw new IllegalArgumentException("Key " + key + " is not found in metadata");
560         }
561         return stringArrayValue;
562     }
563 
564     @Override
describeContents()565     public int describeContents() {
566         return 0;
567     }
568 
569     @Override
writeToParcel(Parcel dest, int flags)570     public void writeToParcel(Parcel dest, int flags) {
571         dest.writeBundle(mBundle);
572     }
573 
574     /**
575      * Returns the number of fields in this meta data.
576      *
577      * @return the number of fields in the meta data.
578      */
size()579     public int size() {
580         return mBundle.size();
581     }
582 
583     /**
584      * Returns a Set containing the Strings used as keys in this meta data.
585      *
586      * @return a Set of String keys
587      */
keySet()588     public Set<String> keySet() {
589         return mBundle.keySet();
590     }
591 
592     /**
593      * Helper for getting the String key used by {@link RadioMetadata} from the
594      * corrsponding native integer key.
595      *
596      * @param nativeKey The key used by the editor
597      * @return the key used by this class or null if no mapping exists
598      * @hide
599      */
getKeyFromNativeKey(int nativeKey)600     public static String getKeyFromNativeKey(int nativeKey) {
601         return NATIVE_KEY_MAPPING.get(nativeKey, null);
602     }
603 
604     public static final @android.annotation.NonNull Parcelable.Creator<RadioMetadata> CREATOR =
605             new Parcelable.Creator<RadioMetadata>() {
606                 @Override
607                 public RadioMetadata createFromParcel(Parcel in) {
608                     return new RadioMetadata(in);
609                 }
610 
611                 @Override
612                 public RadioMetadata[] newArray(int size) {
613                     return new RadioMetadata[size];
614                 }
615             };
616 
617     /**
618      * Use to build RadioMetadata objects.
619      */
620     public static final class Builder {
621         private final Bundle mBundle;
622 
623         /**
624          * Create an empty Builder. Any field that should be included in the
625          * {@link RadioMetadata} must be added.
626          */
Builder()627         public Builder() {
628             mBundle = new Bundle();
629         }
630 
631         /**
632          * Create a Builder using a {@link RadioMetadata} instance to set the
633          * initial values. All fields in the source meta data will be included in
634          * the new meta data. Fields can be overwritten by adding the same key.
635          *
636          * @param source
637          */
Builder(RadioMetadata source)638         public Builder(RadioMetadata source) {
639             mBundle = new Bundle(source.mBundle);
640         }
641 
642         /**
643          * Create a Builder using a {@link RadioMetadata} instance to set
644          * initial values, but replace bitmaps with a scaled down copy if they
645          * are larger than maxBitmapSize.
646          *
647          * @param source The original meta data to copy.
648          * @param maxBitmapSize The maximum height/width for bitmaps contained
649          *            in the meta data.
650          * @hide
651          */
Builder(RadioMetadata source, int maxBitmapSize)652         public Builder(RadioMetadata source, int maxBitmapSize) {
653             this(source);
654             for (String key : mBundle.keySet()) {
655                 Object value = mBundle.get(key);
656                 if (value != null && value instanceof Bitmap) {
657                     Bitmap bmp = (Bitmap) value;
658                     if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) {
659                         putBitmap(key, scaleBitmap(bmp, maxBitmapSize));
660                     }
661                 }
662             }
663         }
664 
665         /**
666          * Put a String value into the meta data. Custom keys may be used, but if
667          * the METADATA_KEYs defined in this class are used they may only be one
668          * of the following:
669          * <ul>
670          *     <li>{@link #METADATA_KEY_RDS_PS}</li>
671          *     <li>{@link #METADATA_KEY_RDS_RT}</li>
672          *     <li>{@link #METADATA_KEY_TITLE}</li>
673          *     <li>{@link #METADATA_KEY_ARTIST}</li>
674          *     <li>{@link #METADATA_KEY_ALBUM}</li>
675          *     <li>{@link #METADATA_KEY_GENRE}</li>
676          *     <li>{@link #METADATA_KEY_COMMENT_SHORT_DESCRIPTION}</li>
677          *     <li>{@link #METADATA_KEY_COMMENT_ACTUAL_TEXT}</li>
678          *     <li>{@link #METADATA_KEY_COMMERCIAL}</li>
679          *     <li>{@link #METADATA_KEY_HD_STATION_NAME_SHORT}</li>
680          *     <li>{@link #METADATA_KEY_HD_STATION_NAME_LONG}</li>
681          * </ul>
682          *
683          * @param key The key for referencing this value
684          * @param value The String value to store
685          * @return the same Builder instance
686          */
putString(String key, String value)687         public Builder putString(String key, String value) {
688             if (!METADATA_KEYS_TYPE.containsKey(key) ||
689                     METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
690                 throw new IllegalArgumentException("The " + key
691                         + " key cannot be used to put a String");
692             }
693             mBundle.putString(key, value);
694             return this;
695         }
696 
697         /**
698          * Put an int value into the meta data. Custom keys may be used, but if
699          * the METADATA_KEYs defined in this class are used they may only be one
700          * of the following:
701          * <ul>
702          *     <li>{@link #METADATA_KEY_RDS_PI}</li>
703          *     <li>{@link #METADATA_KEY_RDS_PTY}</li>
704          *     <li>{@link #METADATA_KEY_RBDS_PTY}</li>
705          *     <li>{@link #METADATA_KEY_HD_SUBCHANNELS_AVAILABLE}</li>
706          * </ul>
707          * or any bitmap represented by its identifier.
708          *
709          * @param key The key for referencing this value
710          * @param value The int value to store
711          * @return the same Builder instance
712          */
putInt(String key, int value)713         public Builder putInt(String key, int value) {
714             RadioMetadata.putInt(mBundle, key, value);
715             return this;
716         }
717 
718         /**
719          * Put a {@link Bitmap} into the meta data. Custom keys may be used, but
720          * if the METADATA_KEYs defined in this class are used they may only be
721          * one of the following:
722          * <ul>
723          *     <li>{@link #METADATA_KEY_ICON}</li>
724          *     <li>{@link #METADATA_KEY_ART}</li>
725          * </ul>
726          * <p>
727          *
728          * @param key The key for referencing this value
729          * @param value The Bitmap to store
730          * @return the same Builder instance
731          */
putBitmap(String key, Bitmap value)732         public Builder putBitmap(String key, Bitmap value) {
733             if (!METADATA_KEYS_TYPE.containsKey(key) ||
734                     METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
735                 throw new IllegalArgumentException("The " + key
736                         + " key cannot be used to put a Bitmap");
737             }
738             mBundle.putParcelable(key, value);
739             return this;
740         }
741 
742         /**
743          * Put a {@link RadioMetadata.Clock} into the meta data. Custom keys may be used, but if the
744          * METADATA_KEYs defined in this class are used they may only be one of the following:
745          * <ul>
746          * <li>{@link #METADATA_KEY_CLOCK}</li>
747          * </ul>
748          *
749          * @param utcSecondsSinceEpoch Number of seconds since epoch for UTC + 0 timezone.
750          * @param timezoneOffsetMinutes Offset of timezone from UTC + 0 in minutes.
751          * @return the same Builder instance.
752          */
putClock(String key, long utcSecondsSinceEpoch, int timezoneOffsetMinutes)753         public Builder putClock(String key, long utcSecondsSinceEpoch, int timezoneOffsetMinutes) {
754             if (!METADATA_KEYS_TYPE.containsKey(key) ||
755                     METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_CLOCK) {
756                 throw new IllegalArgumentException("The " + key
757                     + " key cannot be used to put a RadioMetadata.Clock.");
758             }
759             mBundle.putParcelable(key, new Clock(utcSecondsSinceEpoch, timezoneOffsetMinutes));
760             return this;
761         }
762 
763         /**
764          * Put a String array into the meta data. Custom keys may be used, but if
765          * the METADATA_KEYs defined in this class are used they may only be one
766          * of the following:
767          * <ul>
768          *     <li>{@link #METADATA_KEY_UFIDS}</li>
769          * </ul>
770          *
771          * @param key The key for referencing this value
772          * @param value The String value to store
773          * @return the same Builder instance
774          * @throws NullPointerException if key or value is null
775          * @throws IllegalArgumentException if the key is not string-array-type key
776          */
777         @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
778         @NonNull
putStringArray(@onNull String key, @NonNull String[] value)779         public Builder putStringArray(@NonNull String key, @NonNull String[] value) {
780             Objects.requireNonNull(key, "Key can not be null");
781             Objects.requireNonNull(value, "Value can not be null");
782             if (!METADATA_KEYS_TYPE.containsKey(key)
783                     || !Objects.equals(METADATA_KEYS_TYPE.get(key), METADATA_TYPE_TEXT_ARRAY)) {
784                 throw new IllegalArgumentException("The " + key
785                         + " key cannot be used to put a RadioMetadata String Array.");
786             }
787             mBundle.putStringArray(key, value);
788             return this;
789         }
790 
791 
792         /**
793          * Creates a {@link RadioMetadata} instance with the specified fields.
794          *
795          * @return a new {@link RadioMetadata} object
796          */
build()797         public RadioMetadata build() {
798             return new RadioMetadata(mBundle);
799         }
800 
scaleBitmap(Bitmap bmp, int maxSize)801         private Bitmap scaleBitmap(Bitmap bmp, int maxSize) {
802             float maxSizeF = maxSize;
803             float widthScale = maxSizeF / bmp.getWidth();
804             float heightScale = maxSizeF / bmp.getHeight();
805             float scale = Math.min(widthScale, heightScale);
806             int height = (int) (bmp.getHeight() * scale);
807             int width = (int) (bmp.getWidth() * scale);
808             return Bitmap.createScaledBitmap(bmp, width, height, true);
809         }
810     }
811 
putIntFromNative(int nativeKey, int value)812     int putIntFromNative(int nativeKey, int value) {
813         String key = getKeyFromNativeKey(nativeKey);
814         try {
815             putInt(mBundle, key, value);
816             // Invalidate mHashCode to force it to be recomputed.
817             mHashCode = null;
818             return 0;
819         } catch (IllegalArgumentException ex) {
820             return -1;
821         }
822     }
823 
putStringFromNative(int nativeKey, String value)824     int putStringFromNative(int nativeKey, String value) {
825         String key = getKeyFromNativeKey(nativeKey);
826         if (!METADATA_KEYS_TYPE.containsKey(key) ||
827                 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
828             return -1;
829         }
830         mBundle.putString(key, value);
831         // Invalidate mHashCode to force it to be recomputed.
832         mHashCode = null;
833         return 0;
834     }
835 
putBitmapFromNative(int nativeKey, byte[] value)836     int putBitmapFromNative(int nativeKey, byte[] value) {
837         String key = getKeyFromNativeKey(nativeKey);
838         if (!METADATA_KEYS_TYPE.containsKey(key) ||
839                 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
840             return -1;
841         }
842         Bitmap bmp = null;
843         try {
844             bmp = BitmapFactory.decodeByteArray(value, 0, value.length);
845             if (bmp != null) {
846                 mBundle.putParcelable(key, bmp);
847                 // Invalidate mHashCode to force it to be recomputed.
848                 mHashCode = null;
849                 return 0;
850             }
851         } catch (Exception e) {
852         }
853         return -1;
854     }
855 
putClockFromNative(int nativeKey, long utcEpochSeconds, int timezoneOffsetInMinutes)856     int putClockFromNative(int nativeKey, long utcEpochSeconds, int timezoneOffsetInMinutes) {
857         String key = getKeyFromNativeKey(nativeKey);
858         if (!METADATA_KEYS_TYPE.containsKey(key) ||
859                 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_CLOCK) {
860               return -1;
861         }
862         mBundle.putParcelable(key, new RadioMetadata.Clock(
863             utcEpochSeconds, timezoneOffsetInMinutes));
864         // Invalidate mHashCode to force it to be recomputed.
865         mHashCode = null;
866         return 0;
867     }
868 }
869