diff options
author | Tomasz Wasilczyk <twasilczyk@google.com> | 2018-05-03 12:56:04 -0700 |
---|---|---|
committer | Tomasz Wasilczyk <twasilczyk@google.com> | 2018-05-07 11:10:54 -0700 |
commit | f11da2ad7168c9f76518bac3bb63f9da9698109b (patch) | |
tree | 671b18652a4ce43475841c8a678f5aae060bc164 | |
parent | d1e574cb3fb8cac41085cc6daf4be528f3d66466 (diff) | |
download | systemlibs-f11da2ad7168c9f76518bac3bb63f9da9698109b.tar.gz |
Extract car-broadcastradio-support lib from the radio app.
Bug: 75970985
Test: full build, flash and run radio app
Change-Id: I5e1e6a9c392235a8eb30d5207d10d6adb0c063ee
Merged-In: I5e1e6a9c392235a8eb30d5207d10d6adb0c063ee
11 files changed, 1388 insertions, 0 deletions
diff --git a/car-broadcastradio-support/Android.mk b/car-broadcastradio-support/Android.mk new file mode 100644 index 0000000..3184f72 --- /dev/null +++ b/car-broadcastradio-support/Android.mk @@ -0,0 +1,36 @@ +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := car-broadcastradio-support + +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/src +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res + +LOCAL_MODULE_TAGS := optional +LOCAL_PRIVILEGED_MODULE := true + +LOCAL_PROGUARD_ENABLED := disabled +LOCAL_USE_AAPT2 := true + +LOCAL_STATIC_ANDROID_LIBRARIES := \ + android-support-v4 + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/car-broadcastradio-support/AndroidManifest.xml b/car-broadcastradio-support/AndroidManifest.xml new file mode 100644 index 0000000..e592ee1 --- /dev/null +++ b/car-broadcastradio-support/AndroidManifest.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.car.broadcastradio.support"> + <uses-sdk + android:minSdkVersion="28" + android:targetSdkVersion='28'/> +</manifest> diff --git a/car-broadcastradio-support/OWNERS b/car-broadcastradio-support/OWNERS new file mode 100644 index 0000000..136b607 --- /dev/null +++ b/car-broadcastradio-support/OWNERS @@ -0,0 +1,3 @@ +# Automotive team +egranata@google.com +twasilczyk@google.com diff --git a/car-broadcastradio-support/res/values/strings.xml b/car-broadcastradio-support/res/values/strings.xml new file mode 100644 index 0000000..229d701 --- /dev/null +++ b/car-broadcastradio-support/res/values/strings.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Text to denote the AM radio band. --> + <string name="radio_am_text">AM</string> + + <!-- Text to denote the FM radio band. --> + <string name="radio_fm_text">FM</string> + + <!-- Text to denote the list of programs (stations). --> + <string name="program_list_text">Stations</string> + + <!-- Text to denote the list of favorite programs (stations). --> + <string name="favorites_list_text">Favorites</string> +</resources> diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.aidl b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.aidl new file mode 100644 index 0000000..99c9a93 --- /dev/null +++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.aidl @@ -0,0 +1,18 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.car.broadcastradio.support; + +parcelable Program; diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.java new file mode 100644 index 0000000..4d1de73 --- /dev/null +++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.java @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.broadcastradio.support; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Bitmap; +import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioManager.ProgramInfo; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.car.broadcastradio.support.platform.ProgramInfoExt; + +import java.util.Objects; + +/** + * Holds storable information about a Program. + * + * Contrary to {@link android.hardware.radio.RadioManager.ProgramInfo}, it doesn't hold runtime + * information, like artist or signal quality. + */ +public final class Program implements Parcelable { + private final @NonNull ProgramSelector mSelector; + private final @NonNull String mName; + + public Program(@NonNull ProgramSelector selector, @NonNull String name) { + mSelector = Objects.requireNonNull(selector); + mName = Objects.requireNonNull(name); + } + + public @NonNull ProgramSelector getSelector() { + return mSelector; + } + + public @NonNull String getName() { + return mName; + } + + /** @hide */ + public @Nullable Bitmap getIcon() { + // TODO(b/75970985): implement saving icons + return null; + } + + @Override + public String toString() { + return "Program(\"" + mName + "\", " + mSelector + ")"; + } + + @Override + public int hashCode() { + return mSelector.hashCode(); + } + + /** + * Two programs are considered equal if their selectors are equal. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Program)) return false; + Program other = (Program) obj; + return mSelector.equals(other.mSelector); + } + + /** + * Builds a new {@link Program} object from {@link ProgramInfo}. + */ + public static @NonNull Program fromProgramInfo(@NonNull ProgramInfo info) { + return new Program(info.getSelector(), ProgramInfoExt.getProgramName(info, 0)); + } + + private Program(Parcel in) { + mSelector = Objects.requireNonNull(in.readTypedObject(ProgramSelector.CREATOR)); + mName = Objects.requireNonNull(in.readString()); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedObject(mSelector, 0); + dest.writeString(mName); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<Program> CREATOR = new Parcelable.Creator<Program>() { + public Program createFromParcel(Parcel in) { + return new Program(in); + } + + public Program[] newArray(int size) { + return new Program[size]; + } + }; +} diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/media/BrowseTree.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/media/BrowseTree.java new file mode 100644 index 0000000..ebfd8ba --- /dev/null +++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/media/BrowseTree.java @@ -0,0 +1,446 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.broadcastradio.support.media; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringRes; +import android.graphics.Bitmap; +import android.hardware.radio.ProgramList; +import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioManager; +import android.hardware.radio.RadioManager.BandDescriptor; +import android.hardware.radio.RadioMetadata; +import android.os.Bundle; +import android.support.v4.media.MediaBrowserCompat.MediaItem; +import android.support.v4.media.MediaBrowserServiceCompat; +import android.support.v4.media.MediaBrowserServiceCompat.BrowserRoot; +import android.support.v4.media.MediaBrowserServiceCompat.Result; +import android.support.v4.media.MediaDescriptionCompat; +import android.util.Log; + +import com.android.car.broadcastradio.support.Program; +import com.android.car.broadcastradio.support.R; +import com.android.car.broadcastradio.support.platform.ImageResolver; +import com.android.car.broadcastradio.support.platform.ProgramInfoExt; +import com.android.car.broadcastradio.support.platform.ProgramSelectorExt; +import com.android.car.broadcastradio.support.platform.RadioMetadataExt; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Implementation of MediaBrowserService logic regarding browser tree. + */ +public class BrowseTree { + private static final String TAG = "BcRadioApp.BrowseTree"; + + /** + * Used as a long extra field to indicate the Broadcast Radio folder type of the media item. + * The value should be one of the following: + * <ul> + * <li>{@link #BCRADIO_FOLDER_TYPE_PROGRAMS}</li> + * <li>{@link #BCRADIO_FOLDER_TYPE_FAVORITES}</li> + * <li>{@link #BCRADIO_FOLDER_TYPE_BAND}</li> + * </ul> + * + * @see android.media.MediaDescription#getExtras() + */ + public static final String EXTRA_BCRADIO_FOLDER_TYPE = + "android.media.extra.EXTRA_BCRADIO_FOLDER_TYPE"; + + /** + * The type of folder that contains a list of Broadcast Radio programs available + * to tune at the moment. + */ + public static final long BCRADIO_FOLDER_TYPE_PROGRAMS = 1; + + /** + * The type of folder that contains a list of Broadcast Radio programs added + * to favorites (not necessarily available to tune at the moment). + * + * If this folder has {@link android.media.browse.MediaBrowser.MediaItem#FLAG_PLAYABLE} flag + * set, it can be used to play some program from the favorite list (selection depends on the + * radio app implementation). + */ + public static final long BCRADIO_FOLDER_TYPE_FAVORITES = 2; + + /** + * The type of folder that contains the list of all Broadcast Radio channels + * (frequency values valid in the current region) for a given band. + * Each band (like AM, FM) has its own, separate folder. + * These lists include all channels, whether or not some program is tunable through it. + * + * If this folder has {@link android.media.browse.MediaBrowser.MediaItem#FLAG_PLAYABLE} flag + * set, it can be used to tune to some channel within a given band (selection depends on the + * radio app implementation). + */ + public static final long BCRADIO_FOLDER_TYPE_BAND = 3; + + private static final String NODE_ROOT = "root_id"; + private static final String NODE_PROGRAMS = "programs_id"; + private static final String NODE_FAVORITES = "favorites_id"; + + private static final String NODEPREFIX_BAND = "band:"; + private static final String NODEPREFIX_AMFMCHANNEL = "amfm:"; + private static final String NODEPREFIX_PROGRAM = "program:"; + + private final BrowserRoot mRoot = new BrowserRoot(NODE_ROOT, null); + + private final Object mLock = new Object(); + private final @NonNull MediaBrowserServiceCompat mBrowserService; + private final @Nullable ImageResolver mImageResolver; + + private List<MediaItem> mRootChildren; + + private final AmFmChannelList mAmChannels = new AmFmChannelList( + NODEPREFIX_BAND + "am", R.string.radio_am_text); + private final AmFmChannelList mFmChannels = new AmFmChannelList( + NODEPREFIX_BAND + "fm", R.string.radio_fm_text); + + private final ProgramList.OnCompleteListener mProgramListCompleteListener = + this::onProgramListUpdated; + @Nullable private ProgramList mProgramList; + @Nullable private List<RadioManager.ProgramInfo> mProgramListSnapshot; + @Nullable private List<MediaItem> mProgramListCache; + private final List<Runnable> mProgramListTasks = new ArrayList<>(); + private final Map<String, ProgramSelector> mProgramSelectors = new HashMap<>(); + + @Nullable Set<Program> mFavorites; + @Nullable private List<MediaItem> mFavoritesCache; + + public BrowseTree(@NonNull MediaBrowserServiceCompat browserService, + @Nullable ImageResolver imageResolver) { + mBrowserService = Objects.requireNonNull(browserService); + mImageResolver = imageResolver; + } + + public BrowserRoot getRoot() { + return mRoot; + } + + private static MediaItem createChild(MediaDescriptionCompat.Builder descBuilder, + String mediaId, String title, ProgramSelector sel, Bitmap icon) { + MediaDescriptionCompat desc = descBuilder + .setMediaId(mediaId) + .setMediaUri(ProgramSelectorExt.toUri(sel)) + .setTitle(title) + .setIconBitmap(icon) + .build(); + return new MediaItem(desc, MediaItem.FLAG_PLAYABLE); + } + + private static MediaItem createFolder(MediaDescriptionCompat.Builder descBuilder, + String mediaId, String title, boolean isPlayable, long folderType) { + Bundle extras = new Bundle(); + extras.putLong(EXTRA_BCRADIO_FOLDER_TYPE, folderType); + + MediaDescriptionCompat desc = descBuilder + .setMediaId(mediaId).setTitle(title).setExtras(extras).build(); + + int flags = MediaItem.FLAG_BROWSABLE; + if (isPlayable) flags |= MediaItem.FLAG_PLAYABLE; + return new MediaItem(desc, flags); + } + + /** + * Sets AM/FM region configuration. + * + * This method is meant to be called shortly after initialization, if AM/FM is supported. + */ + public void setAmFmRegionConfig(@Nullable List<BandDescriptor> amFmBands) { + List<BandDescriptor> amBands = new ArrayList<>(); + List<BandDescriptor> fmBands = new ArrayList<>(); + + if (amFmBands != null) { + for (BandDescriptor band : amFmBands) { + final int freq = band.getLowerLimit(); + if (ProgramSelectorExt.isAmFrequency(freq)) { + amBands.add(band); + } else if (ProgramSelectorExt.isFmFrequency(freq)) { + fmBands.add(band); + } + } + } + + synchronized (mLock) { + mAmChannels.setBands(amBands); + mFmChannels.setBands(fmBands); + mRootChildren = null; + mBrowserService.notifyChildrenChanged(NODE_ROOT); + } + } + + private void onProgramListUpdated() { + synchronized (mLock) { + mProgramListSnapshot = mProgramList.toList(); + mProgramListCache = null; + mBrowserService.notifyChildrenChanged(NODE_PROGRAMS); + + for (Runnable task : mProgramListTasks) { + task.run(); + } + mProgramListTasks.clear(); + } + } + + /** + * Binds program list. + * + * This method is meant to be called shortly after opening a new tuner session. + */ + public void setProgramList(@Nullable ProgramList programList) { + synchronized (mLock) { + if (mProgramList != null) { + mProgramList.removeOnCompleteListener(mProgramListCompleteListener); + } + mProgramList = programList; + if (programList != null) { + mProgramList.addOnCompleteListener(mProgramListCompleteListener); + } + mBrowserService.notifyChildrenChanged(NODE_ROOT); + } + } + + private List<MediaItem> getPrograms() { + synchronized (mLock) { + if (mProgramListSnapshot == null) { + Log.w(TAG, "There is no snapshot of the program list"); + return null; + } + + if (mProgramListCache != null) return mProgramListCache; + mProgramListCache = new ArrayList<>(); + + MediaDescriptionCompat.Builder dbld = new MediaDescriptionCompat.Builder(); + + for (RadioManager.ProgramInfo program : mProgramListSnapshot) { + ProgramSelector sel = program.getSelector(); + String mediaId = selectorToMediaId(sel); + mProgramSelectors.put(mediaId, sel); + + Bitmap icon = null; + RadioMetadata meta = program.getMetadata(); + if (meta != null && mImageResolver != null) { + long id = RadioMetadataExt.getGlobalBitmapId(meta, + RadioMetadata.METADATA_KEY_ICON); + if (id != 0) icon = mImageResolver.resolve(id); + } + + mProgramListCache.add(createChild(dbld, mediaId, + ProgramInfoExt.getProgramName(program, 0), program.getSelector(), icon)); + } + + if (mProgramListCache.size() == 0) { + Log.v(TAG, "Program list is empty"); + } + return mProgramListCache; + } + } + + private void sendPrograms(final Result<List<MediaItem>> result) { + synchronized (mLock) { + if (mProgramListSnapshot != null) { + result.sendResult(getPrograms()); + } else { + Log.d(TAG, "Program list is not ready yet"); + result.detach(); + mProgramListTasks.add(() -> result.sendResult(getPrograms())); + } + } + } + + /** + * Updates favorites list. + */ + public void setFavorites(@Nullable Set<Program> favorites) { + synchronized (mLock) { + boolean rootChanged = (mFavorites == null) != (favorites == null); + mFavorites = favorites; + mFavoritesCache = null; + mBrowserService.notifyChildrenChanged(NODE_FAVORITES); + if (rootChanged) mBrowserService.notifyChildrenChanged(NODE_ROOT); + } + } + + /** @hide */ + public boolean isFavorite(@NonNull ProgramSelector selector) { + synchronized (mLock) { + if (mFavorites == null) return false; + return mFavorites.contains(new Program(selector, "")); + } + } + + private List<MediaItem> getFavorites() { + synchronized (mLock) { + if (mFavorites == null) return null; + if (mFavoritesCache != null) return mFavoritesCache; + mFavoritesCache = new ArrayList<>(); + + MediaDescriptionCompat.Builder dbld = new MediaDescriptionCompat.Builder(); + + for (Program fav : mFavorites) { + ProgramSelector sel = fav.getSelector(); + String mediaId = selectorToMediaId(sel); + mProgramSelectors.putIfAbsent(mediaId, sel); // prefer program list entries + mFavoritesCache.add(createChild(dbld, mediaId, fav.getName(), sel, fav.getIcon())); + } + + return mFavoritesCache; + } + } + + private List<MediaItem> getRootChildren() { + synchronized (mLock) { + if (mRootChildren != null) return mRootChildren; + mRootChildren = new ArrayList<>(); + + MediaDescriptionCompat.Builder dbld = new MediaDescriptionCompat.Builder(); + if (mProgramList != null) { + mRootChildren.add(createFolder(dbld, NODE_PROGRAMS, + mBrowserService.getString(R.string.program_list_text), + false, BCRADIO_FOLDER_TYPE_PROGRAMS)); + } + if (mFavorites != null) { + mRootChildren.add(createFolder(dbld, NODE_FAVORITES, + mBrowserService.getString(R.string.favorites_list_text), + true, BCRADIO_FOLDER_TYPE_FAVORITES)); + } + + MediaItem amRoot = mAmChannels.getBandRoot(); + if (amRoot != null) mRootChildren.add(amRoot); + MediaItem fmRoot = mFmChannels.getBandRoot(); + if (fmRoot != null) mRootChildren.add(fmRoot); + + return mRootChildren; + } + } + + private class AmFmChannelList { + public final @NonNull String mMediaId; + private final @StringRes int mBandName; + private @Nullable List<BandDescriptor> mBands; + private @Nullable List<MediaItem> mChannels; + + private AmFmChannelList(@NonNull String mediaId, @StringRes int bandName) { + mMediaId = Objects.requireNonNull(mediaId); + mBandName = bandName; + } + + public void setBands(List<BandDescriptor> bands) { + synchronized (mLock) { + mBands = bands; + mChannels = null; + mBrowserService.notifyChildrenChanged(mMediaId); + } + } + + private boolean isEmpty() { + if (mBands == null) { + Log.w(TAG, "AM/FM configuration not set"); + return true; + } + return mBands.isEmpty(); + } + + public @Nullable MediaItem getBandRoot() { + if (isEmpty()) return null; + return createFolder(new MediaDescriptionCompat.Builder(), mMediaId, + mBrowserService.getString(mBandName), true, BCRADIO_FOLDER_TYPE_BAND); + } + + public List<MediaItem> getChannels() { + synchronized (mLock) { + if (mChannels != null) return mChannels; + if (isEmpty()) return null; + mChannels = new ArrayList<>(); + + MediaDescriptionCompat.Builder dbld = new MediaDescriptionCompat.Builder(); + + for (BandDescriptor band : mBands) { + final int lowerLimit = band.getLowerLimit(); + final int upperLimit = band.getUpperLimit(); + final int spacing = band.getSpacing(); + for (int ch = lowerLimit; ch <= upperLimit; ch += spacing) { + ProgramSelector sel = ProgramSelectorExt.createAmFmSelector(ch); + mChannels.add(createChild(dbld, NODEPREFIX_AMFMCHANNEL + ch, + ProgramSelectorExt.getDisplayName(sel, 0), sel, null)); + } + } + + return mChannels; + } + } + } + + /** + * Loads subtree children. + * + * This method is meant to be used in MediaBrowserService's onLoadChildren callback. + */ + public void loadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { + if (parentMediaId == null || result == null) return; + + if (NODE_ROOT.equals(parentMediaId)) { + result.sendResult(getRootChildren()); + } else if (NODE_PROGRAMS.equals(parentMediaId)) { + sendPrograms(result); + } else if (NODE_FAVORITES.equals(parentMediaId)) { + result.sendResult(getFavorites()); + } else if (parentMediaId.equals(mAmChannels.mMediaId)) { + result.sendResult(mAmChannels.getChannels()); + } else if (parentMediaId.equals(mFmChannels.mMediaId)) { + result.sendResult(mFmChannels.getChannels()); + } else { + Log.w(TAG, "Invalid parent media ID: " + parentMediaId); + result.sendResult(null); + } + } + + private static @NonNull String selectorToMediaId(@NonNull ProgramSelector sel) { + ProgramSelector.Identifier id = sel.getPrimaryId(); + return NODEPREFIX_PROGRAM + id.getType() + '/' + id.getValue(); + } + + /** + * Resolves mediaId to a tunable {@link ProgramSelector}. + * + * This method is meant to be used in MediaSession's onPlayFromMediaId callback. + */ + public @Nullable ProgramSelector parseMediaId(@Nullable String mediaId) { + if (mediaId == null) return null; + + if (mediaId.startsWith(NODEPREFIX_AMFMCHANNEL)) { + String freqStr = mediaId.substring(NODEPREFIX_AMFMCHANNEL.length()); + int freqInt; + try { + freqInt = Integer.parseInt(freqStr); + } catch (NumberFormatException ex) { + Log.e(TAG, "Invalid frequency", ex); + return null; + } + return ProgramSelectorExt.createAmFmSelector(freqInt); + } else if (mediaId.startsWith(NODEPREFIX_PROGRAM)) { + return mProgramSelectors.get(mediaId); + } + return null; + } +} diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ImageResolver.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ImageResolver.java new file mode 100644 index 0000000..5538a58 --- /dev/null +++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ImageResolver.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.broadcastradio.support.platform; + +import android.annotation.Nullable; +import android.graphics.Bitmap; + +/** + * Resolves metadata images. + */ +public interface ImageResolver { + /** + * Resolve a given metadata image global id to a bitmap. + * + * @param globalId metadata image id + * @return A bitmap, or null if it was not available or invalid + */ + @Nullable Bitmap resolve(long globalId); +} diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java new file mode 100644 index 0000000..ce3d014 --- /dev/null +++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java @@ -0,0 +1,173 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.broadcastradio.support.platform; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Bitmap; +import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioManager.ProgramInfo; +import android.hardware.radio.RadioMetadata; +import android.media.MediaMetadata; +import android.media.Rating; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Proposed extensions to android.hardware.radio.RadioManager.ProgramInfo. + * + * They might eventually get pushed to the framework. + */ +public class ProgramInfoExt { + private static final String TAG = "BcRadioApp.pinfoext"; + + /** + * If there is no suitable program name, return null instead of doing + * a fallback to channel display name. + */ + public static final int NAME_NO_CHANNEL_FALLBACK = 1 << 16; + + /** + * Flags to control how to fetch program name with {@link #getProgramName}. + * + * Lower 16 bits are reserved for {@link ProgramSelectorExt#NameFlag}. + */ + @IntDef(prefix = { "NAME_" }, flag = true, value = { + ProgramSelectorExt.NAME_NO_MODULATION, + ProgramSelectorExt.NAME_MODULATION_ONLY, + NAME_NO_CHANNEL_FALLBACK, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NameFlag {} + + private static final char EN_DASH = '\u2013'; + private static final String TITLE_SEPARATOR = " " + EN_DASH + " "; + + private static final String[] PROGRAM_NAME_ORDER = new String[] { + RadioMetadata.METADATA_KEY_PROGRAM_NAME, + RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME, + RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME, + RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME, + RadioMetadata.METADATA_KEY_RDS_PS, + }; + + /** + * Returns program name suitable to display. + * + * If there is no program name, it falls back to channel name. Flags related to + * the channel name display will be forwarded to the channel name generation method. + */ + public static @NonNull String getProgramName(@NonNull ProgramInfo info, @NameFlag int flags) { + RadioMetadata meta = info.getMetadata(); + if (meta != null) { + for (String key : PROGRAM_NAME_ORDER) { + String value = meta.getString(key); + if (value != null) return value; + } + } + + if ((flags & NAME_NO_CHANNEL_FALLBACK) != 0) return ""; + + ProgramSelector sel = info.getSelector(); + + // if it's AM/FM program, prefer to display currently used AF frequency + if (ProgramSelectorExt.isAmFmProgram(sel)) { + ProgramSelector.Identifier phy = info.getPhysicallyTunedTo(); + if (phy != null && phy.getType() == ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY) { + String chName = ProgramSelectorExt.formatAmFmFrequency(phy.getValue(), flags); + if (chName != null) return chName; + } + } + + String selName = ProgramSelectorExt.getDisplayName(sel, flags); + if (selName != null) return selName; + + Log.w(TAG, "ProgramInfo without a name nor channel name"); + return ""; + } + + /** + * Proposed reimplementation of {@link RadioManager#ProgramInfo#getMetadata}. + * + * As opposed to the original implementation, it never returns null. + */ + public static @NonNull RadioMetadata getMetadata(@NonNull ProgramInfo info) { + RadioMetadata meta = info.getMetadata(); + if (meta != null) return meta; + + /* Creating new Metadata object on each get won't be necessary after we + * push this code to the framework. */ + return (new RadioMetadata.Builder()).build(); + } + + /** + * Converts {@ProgramInfo} to {@MediaMetadata}. + * + * This method is meant to be used for currently playing station in {@link MediaSession}. + * + * @param info {@link ProgramInfo} to convert + * @param isFavorite true, if a given program is a favorite + * @param imageResolver metadata images resolver/cache + * @return {@link MediaMetadata} object + */ + public static @NonNull MediaMetadata toMediaMetadata(@NonNull ProgramInfo info, + boolean isFavorite, @Nullable ImageResolver imageResolver) { + MediaMetadata.Builder bld = new MediaMetadata.Builder(); + + bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, getProgramName(info, 0)); + + RadioMetadata meta = info.getMetadata(); + if (meta != null) { + String title = meta.getString(RadioMetadata.METADATA_KEY_TITLE); + if (title != null) { + bld.putString(MediaMetadata.METADATA_KEY_TITLE, title); + } + String artist = meta.getString(RadioMetadata.METADATA_KEY_ARTIST); + if (artist != null) { + bld.putString(MediaMetadata.METADATA_KEY_ARTIST, artist); + } + String album = meta.getString(RadioMetadata.METADATA_KEY_ALBUM); + if (album != null) { + bld.putString(MediaMetadata.METADATA_KEY_ALBUM, album); + } + if (title != null || artist != null) { + String subtitle; + if (title == null) { + subtitle = artist; + } else if (artist == null) { + subtitle = title; + } else { + subtitle = title + TITLE_SEPARATOR + artist; + } + bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); + } + long albumArtId = RadioMetadataExt.getGlobalBitmapId(meta, + RadioMetadata.METADATA_KEY_ART); + if (albumArtId != 0 && imageResolver != null) { + Bitmap bm = imageResolver.resolve(albumArtId); + if (bm != null) bld.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bm); + } + } + + bld.putRating(MediaMetadata.METADATA_KEY_USER_RATING, Rating.newHeartRating(isFavorite)); + + return bld.build(); + } +} diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java new file mode 100644 index 0000000..6d07437 --- /dev/null +++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java @@ -0,0 +1,457 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.broadcastradio.support.platform; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.radio.ProgramSelector; +import android.hardware.radio.ProgramSelector.Identifier; +import android.hardware.radio.RadioManager; +import android.net.Uri; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +/** + * Proposed extensions to android.hardware.radio.ProgramSelector. + * + * They might eventually get pushed to the framework. + */ +public class ProgramSelectorExt { + private static final String TAG = "BcRadioApp.pselext"; + + /** + * If this is AM/FM channel (or any other technology using different modulations), + * don't return modulation part. + */ + public static final int NAME_NO_MODULATION = 1 << 0; + + /** + * Return only modulation part of channel name. + * + * If this is not a radio technology using modulation, return nothing + * (unless combined with other _ONLY flags in the future). + * + * If this returns non-null string, it's guaranteed that {@link #NAME_NO_MODULATION} + * will return the complement of channel name. + */ + public static final int NAME_MODULATION_ONLY = 1 << 1; + + /** + * Flags to control how channel values are converted to string with {@link #getDisplayName}. + * + * Upper 16 bits are reserved for {@link ProgramInfoExt#NameFlag}. + */ + @IntDef(prefix = { "NAME_" }, flag = true, value = { + NAME_NO_MODULATION, + NAME_MODULATION_ONLY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NameFlag {} + + private static final String URI_SCHEME_BROADCASTRADIO = "broadcastradio"; + private static final String URI_AUTHORITY_PROGRAM = "program"; + private static final String URI_VENDOR_PREFIX = "VENDOR_"; + private static final String URI_HEX_PREFIX = "0x"; + + private static final DecimalFormat FORMAT_FM = new DecimalFormat("###.#"); + + private static final Map<Integer, String> ID_TO_URI = new HashMap<>(); + private static final Map<String, Integer> URI_TO_ID = new HashMap<>(); + + /** + * New proposed constructor for {@link ProgramSelector}. + * + * As opposed to the current platform API, this one matches more closely simplified HAL 2.0. + * + * @param primaryId primary program identifier. + * @param secondaryIds list of secondary program identifiers. + */ + public static @NonNull ProgramSelector newProgramSelector(@NonNull Identifier primaryId, + @Nullable Identifier[] secondaryIds) { + return new ProgramSelector( + identifierToProgramType(primaryId), + primaryId, secondaryIds, null); + } + + // when pushed to the framework, remove similar code from HAL 2.0 service + private static @ProgramSelector.ProgramType int identifierToProgramType( + @NonNull Identifier primaryId) { + int idType = primaryId.getType(); + switch (idType) { + case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY: + if (isAmFrequency(primaryId.getValue())) { + return ProgramSelector.PROGRAM_TYPE_AM; + } else { + return ProgramSelector.PROGRAM_TYPE_FM; + } + case ProgramSelector.IDENTIFIER_TYPE_RDS_PI: + return ProgramSelector.PROGRAM_TYPE_FM; + case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT: + if (isAmFrequency(IdentifierExt.asHdPrimary(primaryId).getFrequency())) { + return ProgramSelector.PROGRAM_TYPE_AM_HD; + } else { + return ProgramSelector.PROGRAM_TYPE_FM_HD; + } + case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC: + case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE: + case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID: + case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY: + return ProgramSelector.PROGRAM_TYPE_DAB; + case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID: + case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY: + return ProgramSelector.PROGRAM_TYPE_DRMO; + case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID: + case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL: + return ProgramSelector.PROGRAM_TYPE_SXM; + } + if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START + && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) { + return idType; + } + return ProgramSelector.PROGRAM_TYPE_INVALID; + } + + /** + * Checks, if a given AM frequency is roughly valid and in correct unit. + * + * It does not check the range precisely: it may provide false positives, but not false + * negatives. In particular, it may be way off for certain regions. + * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz. + * It also can be used to check if a given frequency is likely to be used + * with AM or FM modulation. + * + * @param frequencyKhz the frequency in kHz. + * @return true, if the frequency is rougly valid. + */ + public static boolean isAmFrequency(long frequencyKhz) { + return frequencyKhz > 150 && frequencyKhz <= 30000; + } + + /** + * Checks, if a given FM frequency is roughly valid and in correct unit. + * + * It does not check the range precisely: it may provide false positives, but not false + * negatives. In particular, it may be way off for certain regions. + * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz. + * It also can be used to check if a given frequency is likely to be used + * with AM or FM modulation. + * + * @param frequencyKhz the frequency in kHz. + * @return true, if the frequency is rougly valid. + */ + public static boolean isFmFrequency(long frequencyKhz) { + return frequencyKhz > 60000 && frequencyKhz < 110000; + } + + /** + * Provides human-readable representation of AM/FM frequency. + * + * @param frequencyKhz the frequency in kHz. + * @param flags flags that affect display format + * @return human-readable formatted frequency + */ + public static @Nullable String formatAmFmFrequency(long frequencyKhz, @NameFlag int flags) { + String channel; + String modulation; + + if (isAmFrequency(frequencyKhz)) { + channel = Long.toString(frequencyKhz); + modulation = "AM"; + } else if (isFmFrequency(frequencyKhz)) { + channel = FORMAT_FM.format(frequencyKhz / 1000f); + modulation = "FM"; + } else { + Log.w(TAG, "AM/FM frequency out of range: " + frequencyKhz); + return null; + } + + if ((flags & NAME_MODULATION_ONLY) != 0) return modulation; + if ((flags & NAME_NO_MODULATION) != 0) return channel; + return channel + ' ' + modulation; + } + + /** + * Builds new ProgramSelector for AM/FM frequency. + * + * @param frequencyKhz the frequency in kHz. + * @return new ProgramSelector object representing given frequency. + * @throws IllegalArgumentException if provided frequency is out of bounds. + */ + public static @NonNull ProgramSelector createAmFmSelector(int frequencyKhz) { + return ProgramSelector.createAmFmSelector(RadioManager.BAND_INVALID, frequencyKhz); + } + + /** + * Checks, if {@link ProgramSelector} contains an id of a given type. + * + * @param sel selector being checked + * @param type identifier type to check for + * @return true, if sel contains any identifier of a given type + */ + public static boolean hasId(@NonNull ProgramSelector sel, + @ProgramSelector.IdentifierType int type) { + try { + sel.getFirstId(type); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * Checks, if {@link ProgramSelector} is a AM/FM program. + * + * @return true, if the primary identifier of a selector belongs to one of the following + * technologies: + * - Analogue AM/FM + * - FM RDS + * - HD Radio AM/FM + */ + public static boolean isAmFmProgram(@NonNull ProgramSelector sel) { + int priType = sel.getPrimaryId().getType(); + return priType == ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY + || priType == ProgramSelector.IDENTIFIER_TYPE_RDS_PI + || priType == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT; + } + + /** + * Returns a channel name that can be displayed to the user. + * + * It's implemented only for radio technologies where the channel is meant + * to be presented to the user. + * + * @param sel the program selector + * @return Channel name or null, if radio technology doesn't present channel names to the user. + */ + public static @Nullable String getDisplayName(@NonNull ProgramSelector sel, + @NameFlag int flags) { + if (isAmFmProgram(sel)) { + if (!hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) return null; + long freq = sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY); + return formatAmFmFrequency(freq, flags); + } + + if ((flags & NAME_MODULATION_ONLY) != 0) return null; + + if (sel.getPrimaryId().getType() == ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID) { + if (!hasId(sel, ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL)) return null; + return Long.toString(sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL)); + } + + return null; + } + + static { + BiConsumer<Integer, String> add = (idType, name) -> { + ID_TO_URI.put(idType, name); + URI_TO_ID.put(name, idType); + }; + + add.accept(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, "AMFM_FREQUENCY"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, "RDS_PI"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, "HD_STATION_ID_EXT"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, "HD_STATION_NAME"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, "DAB_SID_EXT"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, "DAB_ENSEMBLE"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, "DAB_SCID"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, "DAB_FREQUENCY"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, "DRMO_SERVICE_ID"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, "DRMO_FREQUENCY"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, "SXM_SERVICE_ID"); + add.accept(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, "SXM_CHANNEL"); + } + + private static @Nullable String typeToUri(int identifierType) { + if (identifierType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_START + && identifierType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_END) { + int idx = identifierType - ProgramSelector.IDENTIFIER_TYPE_VENDOR_START; + return URI_VENDOR_PREFIX + idx; + } + return ID_TO_URI.get(identifierType); + } + + private static int uriToType(@Nullable String typeUri) { + if (typeUri == null) return ProgramSelector.IDENTIFIER_TYPE_INVALID; + if (typeUri.startsWith(URI_VENDOR_PREFIX)) { + int idx; + try { + idx = Integer.parseInt(typeUri.substring(URI_VENDOR_PREFIX.length())); + } catch (NumberFormatException ex) { + return ProgramSelector.IDENTIFIER_TYPE_INVALID; + } + if (idx > ProgramSelector.IDENTIFIER_TYPE_VENDOR_END + - ProgramSelector.IDENTIFIER_TYPE_VENDOR_START) { + return ProgramSelector.IDENTIFIER_TYPE_INVALID; + } + return ProgramSelector.IDENTIFIER_TYPE_VENDOR_START + idx; + } + return URI_TO_ID.get(typeUri); + } + + private static @NonNull String valueToUri(@NonNull Identifier id) { + long val = id.getValue(); + switch (id.getType()) { + case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY: + case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY: + case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY: + case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL: + return Long.toString(val); + default: + return URI_HEX_PREFIX + Long.toHexString(val); + } + } + + private static @Nullable Long uriToValue(@Nullable String valUri) { + if (valUri == null) return null; + try { + if (valUri.startsWith(URI_HEX_PREFIX)) { + return Long.parseLong(valUri.substring(URI_HEX_PREFIX.length()), 16); + } else { + return Long.parseLong(valUri, 10); + } + } catch (NumberFormatException ex) { + return null; + } + } + + /** + * Serialize {@link ProgramSelector} to URI. + * + * @param sel selector to serialize + * @return serialized form of selector + */ + public static @Nullable Uri toUri(@NonNull ProgramSelector sel) { + Identifier pri = sel.getPrimaryId(); + String priType = typeToUri(pri.getType()); + // unsupported primary identifier, might be from future HAL revision + if (priType == null) return null; + + Uri.Builder uri = new Uri.Builder() + .scheme(URI_SCHEME_BROADCASTRADIO) + .authority(URI_AUTHORITY_PROGRAM) + .appendPath(priType) + .appendPath(valueToUri(pri)); + + for (Identifier sec : sel.getSecondaryIds()) { + String secType = typeToUri(sec.getType()); + if (secType == null) continue; // skip unsupported secondary identifier + uri.appendQueryParameter(secType, valueToUri(sec)); + } + return uri.build(); + } + + /** + * Parse serialized {@link ProgramSelector}. + * + * @param uri URI-zed form of ProgramSelector + * @return de-serialized object or null, if couldn't parse + */ + public static @Nullable ProgramSelector fromUri(@Nullable Uri uri) { + if (uri == null) return null; + + if (!URI_SCHEME_BROADCASTRADIO.equals(uri.getScheme())) return null; + if (!URI_AUTHORITY_PROGRAM.equals(uri.getAuthority())) { + Log.w(TAG, "Unknown URI authority part (might be a future, unsupported version): " + + uri.getAuthority()); + return null; + } + + BiFunction<String, String, Identifier> parseComponents = (typeStr, valueStr) -> { + int type = uriToType(typeStr); + Long value = uriToValue(valueStr); + if (type == ProgramSelector.IDENTIFIER_TYPE_INVALID || value == null) return null; + return new Identifier(type, value); + }; + + List<String> priUri = uri.getPathSegments(); + if (priUri.size() != 2) return null; + Identifier pri = parseComponents.apply(priUri.get(0), priUri.get(1)); + if (pri == null) return null; + + String query = uri.getQuery(); + List<Identifier> secIds = new ArrayList<>(); + if (query != null) { + for (String secPair : query.split("&")) { + String[] secStr = secPair.split("="); + if (secStr.length != 2) continue; + Identifier sec = parseComponents.apply(secStr[0], secStr[1]); + if (sec != null) secIds.add(sec); + } + } + + return newProgramSelector(pri, secIds.toArray(new Identifier[secIds.size()])); + } + + /** + * Proposed extensions to android.hardware.radio.ProgramSelector.Identifier. + * + * They might eventually get pushed to the framework. + */ + public static class IdentifierExt { + /** + * Decode {@link ProgramSelector#IDENTIFIER_TYPE_HD_STATION_ID_EXT} value. + * + * @param id identifier to decode + * @return value decoder + */ + public static @Nullable HdPrimary asHdPrimary(@NonNull Identifier id) { + if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT) { + return new HdPrimary(id.getValue()); + } + return null; + } + + /** + * Decoder of {@link ProgramSelector#IDENTIFIER_TYPE_HD_STATION_ID_EXT} value. + * + * When pushed to the framework, it will be non-static class referring + * to the original value. + */ + public static class HdPrimary { + /* For mValue format (bit shifts and bit masks), please refer to + * HD_STATION_ID_EXT from broadcastradio HAL 2.0. + */ + private final long mValue; + + private HdPrimary(long value) { + mValue = value; + } + + public long getStationId() { + return mValue & 0xFFFFFFFF; + } + + public int getSubchannel() { + return (int) ((mValue >>> 32) & 0xF); + } + + public int getFrequency() { + return (int) ((mValue >>> (32 + 4)) & 0x3FFFF); + } + } + } +} diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/RadioMetadataExt.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/RadioMetadataExt.java new file mode 100644 index 0000000..e7b6f3b --- /dev/null +++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/RadioMetadataExt.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.broadcastradio.support.platform; + +import android.annotation.NonNull; +import android.hardware.radio.RadioMetadata; + +/** + * Proposed extensions to android.hardware.radio.RadioMetadata. + * + * They might eventually get pushed to the framework. + */ +public class RadioMetadataExt { + private static int sModuleId; + + /** + * A hack to inject module ID for getGlobalBitmapId. When pushed to the framework, + * it will be set with RadioMetadata object creation or just separate int field. + * @hide + */ + public static void setModuleId(int id) { + sModuleId = id; + } + + /** + * Proposed redefinition of {@link RadioMetadata#getBitmapId}. + * + * {@link RadioMetadata#getBitmapId} isn't part of the system API yet, so we can skip + * deprecation here and jump straight to the correct solution. + */ + public static long getGlobalBitmapId(@NonNull RadioMetadata meta, @NonNull String key) { + int localId = meta.getBitmapId(key); + if (localId == 0) return 0; + + /* When generating global bitmap ID, we want them to remain stable between sessions + * (radio app might cache images to disk between sessions). + * + * Local IDs are already stable, but module ID is not guaranteed to be stable (i.e. some + * module might be not available at each boot, due to HW failure). + * + * When we push this to the framework, we will need persistence mechanism at the radio + * service to permanently match modules to their IDs. + */ + return ((long) sModuleId << 32) | localId; + } +} |