Snap for 10453938 from efd17e939480612943ec8ed4373c15258b0a02ff to mainline-odp-release
Change-Id: Id9ddeeedfd00b2f9a6dbedd80c4e4142cf07a779
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index cdae76d..030c955 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -67,6 +67,7 @@
<action android:name="android.media.browse.MediaBrowserService" />
<action android:name="android.car.intent.action.PLAY_BROADCASTRADIO" />
</intent-filter>
+ <meta-data android:name="androidx.car.app.launchable" android:value="false"/>
</service>
</application>
</manifest>
diff --git a/OWNERS b/OWNERS
index 679960d..f0d59d4 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,6 +1,7 @@
# Default code reviewers picked from top 3 or more developers.
# Please update this list if you find better candidates.
-twasilczyk@google.com
-sdai@google.com
+xuweilin@google.com
+oscarazu@google.com
ajchen@google.com
arnaudberry@google.com
+keunyoung@google.com
diff --git a/res/layout/radio_activity.xml b/res/layout/radio_activity.xml
index bd92a9a..cf2b4df 100644
--- a/res/layout/radio_activity.xml
+++ b/res/layout/radio_activity.xml
@@ -55,5 +55,6 @@
android:layout_marginEnd="@dimen/playback_card_margin_horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent" />
+ app:layout_constraintStart_toStartOf="parent"
+ style="@style/RadioPlaybackCard"/>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
new file mode 100644
index 0000000..66d082f
--- /dev/null
+++ b/res/values/overlayable.xml
@@ -0,0 +1,50 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- Copyright (C) 2022 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.-->
+<!--
+THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY.
+REGENERATE USING packages/apps/Car/libs/tools/rro/generate-overlayable.py
+-->
+<resources>
+ <overlayable name="CarRadio">
+ <policy type="system|product|signature">
+ <item type="color" name="accent_color"/>
+ <item type="color" name="band_selector_flat_text_color"/>
+ <item type="color" name="band_selector_flat_text_color_selected"/>
+ <item type="color" name="browse_fragment_list_color"/>
+ <item type="color" name="browse_item_text_color"/>
+ <item type="color" name="control_button_color"/>
+ <item type="color" name="control_button_disabled_color"/>
+ <item type="color" name="control_button_ripple_color"/>
+ <item type="color" name="manual_tuner_button_background_color"/>
+ <item type="color" name="manual_tuner_button_ripple_background_color"/>
+ <item type="color" name="manual_tuner_button_ripple_fill_color"/>
+ <item type="color" name="manual_tuner_button_text_color"/>
+ <item type="color" name="manual_tuner_digit_color"/>
+ <item type="color" name="manual_tuner_done_disabled_color"/>
+ <item type="color" name="pause_button_fill_color"/>
+ <item type="color" name="play_arrow_fill_color"/>
+ <item type="color" name="play_arrow_off_fill_color"/>
+ <item type="color" name="playback_button_ripple_background_color"/>
+ <item type="color" name="playback_button_ripple_fill_color"/>
+ <item type="color" name="playback_card_background_color"/>
+ <item type="color" name="playback_controls_play_button_color"/>
+ <item type="color" name="radio_am_fill_color"/>
+ <item type="color" name="radio_card_color"/>
+ <item type="color" name="radio_fm_fill_color"/>
+ <item type="dimen" name="playback_card_height"/>
+ <item type="dimen" name="playback_card_margin_bottom"/>
+ <item type="dimen" name="playback_card_margin_horizontal"/>
+ <item type="dimen" name="radio_activity_playback_width"/>
+ <item type="style" name="RadioPlaybackCard"/>
+ </policy>
+ </overlayable>
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 20001dc..ffef549 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -55,4 +55,7 @@
<item name="android:background">@drawable/radio_control_background</item>
<item name="android:textAppearance">?android:attr/textAppearanceLarge</item>
</style>
+
+ <style name="RadioPlaybackCard" />
+
</resources>
diff --git a/src/com/android/car/radio/RadioActivity.java b/src/com/android/car/radio/RadioActivity.java
index da8eb30..a9da2be 100644
--- a/src/com/android/car/radio/RadioActivity.java
+++ b/src/com/android/car/radio/RadioActivity.java
@@ -32,6 +32,7 @@
import com.android.car.media.common.source.MediaTrampolineHelper;
import com.android.car.radio.bands.ProgramType;
import com.android.car.radio.service.RadioAppService;
+import com.android.car.radio.service.RadioAppServiceWrapper;
import com.android.car.radio.util.Log;
import com.android.car.ui.baselayout.Insets;
import com.android.car.ui.baselayout.InsetsChangedListener;
@@ -39,7 +40,7 @@
import com.android.car.ui.toolbar.TabLayout;
import com.android.car.ui.toolbar.ToolbarController;
-import java.util.Arrays;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -60,9 +61,11 @@
private static final String EXTRA_RADIO_APP_FOREGROUND =
"android.intent.action.RADIO_APP_STATE";
+ private boolean mIsConnected;
private RadioController mRadioController;
private BandController mBandController = new BandController();
private ToolbarController mToolbar;
+ private ViewPager mViewPager;
private RadioPagerAdapter mRadioPagerAdapter;
private boolean mUseSourceLogoForAppSelector;
@@ -79,13 +82,16 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- Log.d(TAG, "Radio app main activity created");
+ Log.i(TAG, "Radio app main activity created");
setContentView(R.layout.radio_activity);
mMediaTrampoline = new MediaTrampolineHelper(this);
mRadioController = new RadioController(this);
+
+ mRadioController.getConnectionState().observe(this, this::onConnectionStateChanged);
+
mRadioController.getCurrentProgram().observe(this, info -> {
ProgramType programType = ProgramType.fromSelector(info.getSelector());
if (programType != null) {
@@ -96,8 +102,7 @@
mRadioPagerAdapter =
new RadioPagerAdapter(this, getSupportFragmentManager(), mRadioController);
- ViewPager viewPager = findViewById(R.id.viewpager);
- viewPager.setAdapter(mRadioPagerAdapter);
+ mViewPager = findViewById(R.id.viewpager);
mUseSourceLogoForAppSelector =
getResources().getBoolean(R.bool.use_media_source_logo_for_app_selector);
@@ -108,11 +113,18 @@
if (!mUseSourceLogoForAppSelector) {
mToolbar.setLogo(R.drawable.logo_fm_radio);
}
- mToolbar.registerOnTabSelectedListener(t ->
- viewPager.setCurrentItem(mToolbar.getTabPosition(t)));
+ mToolbar.registerOnTabSelectedListener(t -> {
+ if (mIsConnected) {
+ mViewPager.setCurrentItem(mToolbar.getTabPosition(t));
+ }
+ });
- updateMenuItems();
- setupTabsWithViewPager(viewPager);
+ mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ mToolbar.selectTab(position);
+ }
+ });
}
@Override
@@ -202,13 +214,16 @@
mBandController.setSupportedProgramTypes(supported);
}
- private void setupTabsWithViewPager(ViewPager viewPager) {
- viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
- @Override
- public void onPageSelected(int position) {
- mToolbar.selectTab(position);
- }
- });
+ private void onConnectionStateChanged(@RadioAppServiceWrapper.ConnectionState int state) {
+ if (state == RadioAppServiceWrapper.STATE_CONNECTED) {
+ mIsConnected = true;
+ mViewPager.setAdapter(mRadioPagerAdapter);
+ } else {
+ mIsConnected = false;
+ mViewPager.setAdapter(null);
+ }
+ Log.i(TAG, "onConnectionStateChanged connected: " + mIsConnected);
+ updateMenuItems();
updateTabs();
}
@@ -230,15 +245,22 @@
.setOnClickListener(m -> startActivity(appSelectorIntent))
.build();
- mToolbar.setMenuItems(Arrays.asList(bandSelectorMenuItem, appSelectorMenuItem));
+ ArrayList<MenuItem> menuItems = new ArrayList<>(2);
+ if (mIsConnected) {
+ menuItems.add(bandSelectorMenuItem);
+ }
+ menuItems.add(appSelectorMenuItem);
+ mToolbar.setMenuItems(menuItems);
}
private void updateTabs() {
mToolbar.clearAllTabs();
- for (int i = 0; i < mRadioPagerAdapter.getCount(); i++) {
- Drawable icon = mRadioPagerAdapter.getPageIcon(i);
- CharSequence title = mRadioPagerAdapter.getPageTitle(i);
- mToolbar.addTab(new TabLayout.Tab(icon, title));
+ if (mIsConnected) {
+ for (int i = 0; i < mRadioPagerAdapter.getCount(); i++) {
+ Drawable icon = mRadioPagerAdapter.getPageIcon(i);
+ CharSequence title = mRadioPagerAdapter.getPageTitle(i);
+ mToolbar.addTab(new TabLayout.Tab(icon, title));
+ }
}
}
}
diff --git a/src/com/android/car/radio/RadioController.java b/src/com/android/car/radio/RadioController.java
index 6244847..a034aaa 100644
--- a/src/com/android/car/radio/RadioController.java
+++ b/src/com/android/car/radio/RadioController.java
@@ -28,8 +28,10 @@
import com.android.car.broadcastradio.support.Program;
import com.android.car.broadcastradio.support.platform.ProgramInfoExt;
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
import com.android.car.radio.bands.ProgramType;
import com.android.car.radio.bands.RegionConfig;
+import com.android.car.radio.media.TunerSession;
import com.android.car.radio.service.RadioAppService;
import com.android.car.radio.service.RadioAppServiceWrapper;
import com.android.car.radio.service.RadioAppServiceWrapper.ConnectionState;
@@ -94,6 +96,14 @@
}
/**
+ * See {@link RadioAppServiceWrapper#getConnectionState}.
+ */
+ @NonNull
+ public LiveData<Integer> getConnectionState() {
+ return mAppService.getConnectionState();
+ }
+
+ /**
* See {@link RadioAppServiceWrapper#getPlaybackState}.
*/
@NonNull
@@ -169,7 +179,7 @@
mDisplayController.setChannel(sel);
mDisplayController.setStationName(
- ProgramInfoExt.getProgramName(info, ProgramInfoExt.NAME_NO_CHANNEL_FALLBACK));
+ ProgramSelectorExt.getDisplayName(sel, info.getChannel()));
if (meta.containsKey(RadioMetadata.METADATA_KEY_TITLE)
|| meta.containsKey(RadioMetadata.METADATA_KEY_ARTIST)) {
@@ -177,7 +187,8 @@
meta.getString(RadioMetadata.METADATA_KEY_TITLE),
meta.getString(RadioMetadata.METADATA_KEY_ARTIST));
} else {
- mDisplayController.setDetails(meta.getString(RadioMetadata.METADATA_KEY_RDS_RT));
+ mDisplayController.setDetails(ProgramInfoExt.getProgramName(info, /* flags= */ 0,
+ TunerSession.PROGRAM_NAME_ORDER));
}
mDisplayController.setCurrentIsFavorite(mRadioStorage.isFavorite(sel));
diff --git a/src/com/android/car/radio/bands/AMFMProgramType.java b/src/com/android/car/radio/bands/AMFMProgramType.java
index 4b9f202..46a4ff3 100644
--- a/src/com/android/car/radio/bands/AMFMProgramType.java
+++ b/src/com/android/car/radio/bands/AMFMProgramType.java
@@ -59,17 +59,19 @@
*/
Random rnd = new Random();
BandDescriptor band = bands.get(rnd.nextInt(bands.size()));
- int freq = rnd.nextInt(band.getUpperLimit() - band.getLowerLimit()) + band.getLowerLimit();
- freq /= band.getSpacing();
- freq *= band.getSpacing();
+ int freq = rnd.nextInt(band.getUpperLimit() - band.getLowerLimit()) / band.getSpacing()
+ * band.getSpacing() + band.getLowerLimit();
+ Log.i(TAG, "Tuning to default frequency: " + freq);
// tune to that frequency and seek forward, to find any station
tuner.tune(ProgramSelectorExt.createAmFmSelector(freq), succeeded -> {
if (!succeeded) {
result.onFinished(false);
+ Log.w(TAG, "Cannot tune to default frequency" + freq);
return;
}
tuner.seek(true, result);
+ Log.i(TAG, "Tuned to default frequency " + freq + " successfully");
});
}
diff --git a/src/com/android/car/radio/media/TunerSession.java b/src/com/android/car/radio/media/TunerSession.java
index 3d9c70c..438fd3f 100644
--- a/src/com/android/car/radio/media/TunerSession.java
+++ b/src/com/android/car/radio/media/TunerSession.java
@@ -19,6 +19,8 @@
import android.content.Context;
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.media.session.MediaController;
import android.media.session.MediaSession;
@@ -62,6 +64,20 @@
new PlaybackState.Builder();
@Nullable private ProgramInfo mCurrentProgram;
+ /**
+ * Custom order that puts RDS_RT ahead of RDS_PS.
+ * RDS_PS is often used to scroll data in RDS_RT in some regions, and for the interests of
+ * driver distration we want to prevent RDS_PS scrolling from updating UI so frequently.
+ */
+ public 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_RT,
+ RadioMetadata.METADATA_KEY_RDS_PS
+ };
+
public TunerSession(@NonNull Context context, @NonNull BrowseTree browseTree,
@NonNull RadioAppServiceWrapper appService, @Nullable ImageResolver imageResolver) {
mSession = new MediaSession(context, TAG);
@@ -107,7 +123,12 @@
synchronized (mLock) {
if (info == null) return;
boolean fav = mRadioStorage.isFavorite(info.getSelector());
- mSession.setMetadata(ProgramInfoExt.toMediaMetadata(info, fav, mImageResolver));
+ MediaMetadata currMetaData = mSession.getController().getMetadata();
+ MediaMetadata newMetaData = ProgramInfoExt.toMediaDisplayMetadata(info, fav,
+ mImageResolver, PROGRAM_NAME_ORDER);
+ if (!Objects.equals(currMetaData, newMetaData)) {
+ mSession.setMetadata(newMetaData);
+ }
}
}
diff --git a/src/com/android/car/radio/platform/ImageMemoryCache.java b/src/com/android/car/radio/platform/ImageMemoryCache.java
index 145e340..9e4bcda 100644
--- a/src/com/android/car/radio/platform/ImageMemoryCache.java
+++ b/src/com/android/car/radio/platform/ImageMemoryCache.java
@@ -18,27 +18,43 @@
import android.graphics.Bitmap;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.broadcastradio.support.platform.ImageResolver;
+import com.android.internal.annotations.GuardedBy;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
-public class ImageMemoryCache implements ImageResolver {
+/**
+ * Resolves metadata image cache for specific {@link RadioManagerExt}
+ */
+public final class ImageMemoryCache implements ImageResolver {
+ private final Object mLock = new Object();
+
private final RadioManagerExt mRadioManager;
+
+ @GuardedBy("mLock")
private final Map<Long, Bitmap> mCache;
- public ImageMemoryCache(@NonNull RadioManagerExt radioManager, int cacheSize) {
- mRadioManager = Objects.requireNonNull(radioManager);
+ public ImageMemoryCache(RadioManagerExt radioManager, int cacheSize) {
+ mRadioManager = Objects.requireNonNull(radioManager, "RadioManager cannot be null");
mCache = new CacheMap<>(cacheSize);
}
- public @Nullable Bitmap resolve(long globalId) {
- synchronized (mCache) {
- if (mCache.containsKey(globalId)) return mCache.get(globalId);
+ /**
+ * Gets metadata image cache
+ *
+ * @param globalId Metadata image id
+ * @return Metadata image
+ */
+ @Nullable
+ public Bitmap resolve(long globalId) {
+ synchronized (mLock) {
+ if (mCache.containsKey(globalId)) {
+ return mCache.get(globalId);
+ }
Bitmap bm = mRadioManager.getMetadataImage(globalId);
mCache.put(globalId, bm);
@@ -50,7 +66,9 @@
private final int mMaxSize;
public CacheMap(int maxSize) {
- if (maxSize < 0) throw new IllegalArgumentException("maxSize must not be negative");
+ if (maxSize < 0) {
+ throw new IllegalArgumentException("maxSize must not be negative");
+ }
mMaxSize = maxSize;
}
diff --git a/src/com/android/car/radio/platform/RadioManagerExt.java b/src/com/android/car/radio/platform/RadioManagerExt.java
index 01125f8..32d854e 100644
--- a/src/com/android/car/radio/platform/RadioManagerExt.java
+++ b/src/com/android/car/radio/platform/RadioManagerExt.java
@@ -23,16 +23,16 @@
import android.hardware.radio.RadioTuner;
import android.os.Handler;
import android.os.HandlerThread;
+import android.util.ArrayMap;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.broadcastradio.support.platform.RadioMetadataExt;
import com.android.car.radio.util.Log;
+import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -43,33 +43,46 @@
*
* They might eventually get pushed to the framework.
*/
-public class RadioManagerExt {
+public final class RadioManagerExt {
private static final String TAG = "BcRadioApp.mgrext";
// For now, we open first radio module only.
private static final int HARDCODED_MODULE_INDEX = 0;
+ private static final long SHIFT_FOR_MODULE_ID = 32;
+ private static final long MASK_FOR_LOCAL_ID = 0xFFFFFFFF;
+
+ // This won't be necessary when we push this code to the framework,
+ // as we really need only module references.
+ private static Map<Integer, RadioTuner> sSessions = new ArrayMap<>();
+
private final Object mLock = new Object();
private final Context mContext;
private final HandlerThread mCallbackHandlerThread = new HandlerThread("BcRadioApp.cbhandler");
- private final @NonNull RadioManager mRadioManager;
- private List<RadioManager.ModuleProperties> mModules;
- private @Nullable List<BandDescriptor> mAmFmRegionConfig;
+ private final RadioManager mRadioManager;
- public RadioManagerExt(@NonNull Context ctx) {
- mContext = Objects.requireNonNull(ctx);
- mRadioManager = (RadioManager)ctx.getSystemService(Context.RADIO_SERVICE);
+ @GuardedBy("mLock")
+ private List<RadioManager.ModuleProperties> mModules;
+
+ @GuardedBy("mLock")
+ @Nullable private List<BandDescriptor> mAmFmRegionConfig;
+
+ public RadioManagerExt(Context ctx) {
+ mContext = Objects.requireNonNull(ctx, "Context cannot be null");
+ mRadioManager = ctx.getSystemService(RadioManager.class);
Objects.requireNonNull(mRadioManager, "RadioManager could not be loaded");
mCallbackHandlerThread.start();
}
- /* Select only one region. HAL 2.x moves region selection responsibility from the app to the
- * Broadcast Radio service, so we won't implement region selection based on bands in the app.
- */
- private @Nullable List<BandDescriptor> reduceAmFmBands(@Nullable BandDescriptor[] bands) {
- if (bands == null || bands.length == 0) return null;
+ // Select only one region. HAL 2.x moves region selection responsibility from the app to the
+ // Broadcast Radio service, so we won't implement region selection based on bands in the app.
+ @Nullable
+ private List<BandDescriptor> reduceAmFmBands(@Nullable BandDescriptor[] bands) {
+ if (bands == null || bands.length == 0) {
+ return null;
+ }
int region = bands[0].getRegion();
Log.d(TAG, "Auto-selecting region " + region);
@@ -77,25 +90,26 @@
collect(Collectors.toList());
}
- private void initModules() {
- synchronized (mLock) {
- if (mModules != null) return;
-
- mModules = new ArrayList<>();
- int status = mRadioManager.listModules(mModules);
- if (status != RadioManager.STATUS_OK) {
- Log.w(TAG, "Couldn't get radio module list: " + status);
- return;
- }
-
- if (mModules.size() == 0) {
- Log.i(TAG, "No radio modules on this device");
- return;
- }
-
- RadioManager.ModuleProperties module = mModules.get(HARDCODED_MODULE_INDEX);
- mAmFmRegionConfig = reduceAmFmBands(module.getBands());
+ @GuardedBy("mLock")
+ private void initModulesLocked() {
+ if (mModules != null) {
+ return;
}
+
+ mModules = new ArrayList<>();
+ int status = mRadioManager.listModules(mModules);
+ if (status != RadioManager.STATUS_OK) {
+ Log.w(TAG, "Couldn't get radio module list: " + status);
+ return;
+ }
+
+ if (mModules.size() == 0) {
+ Log.i(TAG, "No radio modules on this device");
+ return;
+ }
+
+ RadioManager.ModuleProperties moduleProperties = mModules.get(HARDCODED_MODULE_INDEX);
+ mAmFmRegionConfig = reduceAmFmBands(moduleProperties.getBands());
}
/**
@@ -105,31 +119,38 @@
* @param handler The Handler on which the callbacks will be received,
* {@code null} for default handler.
*/
- public @Nullable RadioTunerExt openSession(RadioTuner.Callback callback, Handler handler) {
+ @Nullable
+ public RadioTunerExt openSession(RadioTuner.Callback callback, Handler handler) {
Log.i(TAG, "Opening broadcast radio session...");
- initModules();
- if (mModules.size() == 0) return null;
+ RadioManager.ModuleProperties moduleProperties;
+ synchronized (mLock) {
+ initModulesLocked();
+ if (mModules.size() == 0) {
+ return null;
+ }
+ moduleProperties = mModules.get(HARDCODED_MODULE_INDEX);
+ }
- /* We won't need custom default wrapper when we push these proposed extensions to the
- * framework; this is solely to avoid deadlock on onConfigurationChanged callback versus
- * waitForInitialization.
- */
+ // We won't need custom default wrapper when we push these proposed extensions to the
+ // framework; this is solely to avoid deadlock on onConfigurationChanged callback versus
+ // waitForInitialization.
Handler hwHandler = new Handler(mCallbackHandlerThread.getLooper());
- RadioManager.ModuleProperties module = mModules.get(HARDCODED_MODULE_INDEX);
TunerCallbackAdapterExt cbExt = new TunerCallbackAdapterExt(callback, handler);
RadioTuner tuner = mRadioManager.openTuner(
- module.getId(),
- null, // BandConfig - let the service automatically select one.
- true, // withAudio
+ moduleProperties.getId(),
+ /* config= */ null,
+ /* withAudio= */ true,
cbExt, hwHandler);
- mSessions.put(module.getId(), tuner);
- if (tuner == null) return null;
- RadioMetadataExt.setModuleId(module.getId());
+ sSessions.put(moduleProperties.getId(), tuner);
+ if (tuner == null) {
+ return null;
+ }
+ RadioMetadataExt.setModuleId(moduleProperties.getId());
- if (module.isInitializationRequired()) {
+ if (moduleProperties.isInitializationRequired()) {
if (!cbExt.waitForInitialization()) {
Log.w(TAG, "Timed out waiting for tuner initialization");
tuner.close();
@@ -140,23 +161,40 @@
return new RadioTunerExt(mContext, tuner, cbExt);
}
- public @Nullable List<BandDescriptor> getAmFmRegionConfig() {
- initModules();
- return mAmFmRegionConfig;
+ /**
+ * Gets AM/FM region configuration
+ *
+ * @return AM/FM region configuration
+ */
+ @Nullable
+ public List<BandDescriptor> getAmFmRegionConfig() {
+ List<BandDescriptor> amFmRegionConfig;
+ synchronized (mLock) {
+ initModulesLocked();
+ amFmRegionConfig = mAmFmRegionConfig;
+ }
+ return amFmRegionConfig;
}
- /* This won't be necessary when we push this code to the framework,
- * as we really need only module references. */
- private static Map<Integer, RadioTuner> mSessions = new HashMap<>();
+ /**
+ * Gets metadata image
+ *
+ * @param globalId Global id of the metadata image
+ * @return Metadata image
+ */
+ @Nullable
+ public Bitmap getMetadataImage(long globalId) {
+ if (globalId == 0) {
+ return null;
+ }
- public @Nullable Bitmap getMetadataImage(long globalId) {
- if (globalId == 0) return null;
+ int moduleId = (int) (globalId >>> SHIFT_FOR_MODULE_ID);
+ int localId = (int) (globalId & MASK_FOR_LOCAL_ID);
- int moduleId = (int)(globalId >>> 32);
- int localId = (int)(globalId & 0xFFFFFFFF);
-
- RadioTuner tuner = mSessions.get(moduleId);
- if (tuner == null) return null;
+ RadioTuner tuner = sSessions.get(moduleId);
+ if (tuner == null) {
+ return null;
+ }
return tuner.getMetadataImage(localId);
}
diff --git a/src/com/android/car/radio/platform/RadioTunerExt.java b/src/com/android/car/radio/platform/RadioTunerExt.java
index bee6e92..d081d5e 100644
--- a/src/com/android/car/radio/platform/RadioTunerExt.java
+++ b/src/com/android/car/radio/platform/RadioTunerExt.java
@@ -27,10 +27,10 @@
import android.media.HwAudioSource;
import android.text.TextUtils;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.radio.util.Log;
+import com.android.internal.annotations.GuardedBy;
import java.util.Objects;
import java.util.stream.Stream;
@@ -40,16 +40,19 @@
*
* They might eventually get pushed to the framework.
*/
-public class RadioTunerExt {
+public final class RadioTunerExt {
private static final String TAG = "BcRadioApp.tunerext";
private final Object mLock = new Object();
- private final Object mTuneLock = new Object();
private final RadioTuner mTuner;
+ @GuardedBy("mLock")
private HwAudioSource mHwAudioSource;
+ @GuardedBy("mLock")
@Nullable private ProgramSelector mOperationSelector; // null for seek operations
+
+ @GuardedBy("mLock")
@Nullable private TuneCallback mOperationResultCb;
/**
@@ -66,7 +69,7 @@
/**
* Chains other result callbacks.
*/
- default TuneCallback alsoCall(@NonNull TuneCallback other) {
+ default TuneCallback alsoCall(TuneCallback other) {
return succeeded -> {
onFinished(succeeded);
other.onFinished(succeeded);
@@ -74,13 +77,12 @@
}
}
- RadioTunerExt(@NonNull Context context, @NonNull RadioTuner tuner,
- @NonNull TunerCallbackAdapterExt cbExt) {
- mTuner = Objects.requireNonNull(tuner);
+ RadioTunerExt(Context context, RadioTuner tuner, TunerCallbackAdapterExt cbExt) {
+ mTuner = Objects.requireNonNull(tuner, "Tuner cannot be null");
cbExt.setTuneFailedCallback(this::onTuneFailed);
cbExt.setProgramInfoCallback(this::onProgramInfoChanged);
- final AudioDeviceInfo tunerDevice = findTunerDevice(context, null);
+ AudioDeviceInfo tunerDevice = findTunerDevice(context, /* address= */ null);
if (tunerDevice == null) {
Log.e(TAG, "No TUNER_DEVICE found on board");
} else {
@@ -94,11 +96,11 @@
}
public boolean setMuted(boolean muted) {
- if (mHwAudioSource == null) {
- Log.e(TAG, "No TUNER_DEVICE found on board");
- return false;
- }
synchronized (mLock) {
+ if (mHwAudioSource == null) {
+ Log.e(TAG, "No TUNER_DEVICE found on board when setting muted");
+ return false;
+ }
if (muted) {
mHwAudioSource.stop();
} else {
@@ -112,18 +114,16 @@
* See {@link RadioTuner#scan}.
*/
public void seek(boolean forward, @Nullable TuneCallback resultCb) {
- synchronized (mTuneLock) {
- synchronized (mLock) {
- markOperationFinishedLocked(false);
- mOperationResultCb = resultCb;
- }
+ synchronized (mLock) {
+ markOperationFinishedLocked(/* succeeded= */ false);
+ mOperationResultCb = resultCb;
+ }
- mTuner.cancel();
- int res = mTuner.scan(
- forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN, false);
- if (res != RadioManager.STATUS_OK) {
- throw new RuntimeException("Seek failed with result of " + res);
- }
+ int res = mTuner.scan(
+ forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN,
+ /* skipSubChannel= */ false);
+ if (res != RadioManager.STATUS_OK) {
+ throw new RuntimeException("Seek failed with result of " + res);
}
}
@@ -131,13 +131,14 @@
* See {@link RadioTuner#step}.
*/
public void step(boolean forward, @Nullable TuneCallback resultCb) {
- synchronized (mTuneLock) {
- markOperationFinishedLocked(false);
+ synchronized (mLock) {
+ markOperationFinishedLocked(/* succeeded= */ false);
mOperationResultCb = resultCb;
}
- mTuner.cancel();
+
int res =
- mTuner.step(forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN, false);
+ mTuner.step(forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN,
+ /* skipSubChannel= */ false);
if (res != RadioManager.STATUS_OK) {
throw new RuntimeException("Step failed with result of " + res);
}
@@ -146,17 +147,14 @@
/**
* See {@link RadioTuner#tune}.
*/
- public void tune(@NonNull ProgramSelector selector, @Nullable TuneCallback resultCb) {
- synchronized (mTuneLock) {
- synchronized (mLock) {
- markOperationFinishedLocked(false);
- mOperationSelector = selector;
- mOperationResultCb = resultCb;
- }
-
- mTuner.cancel();
- mTuner.tune(selector);
+ public void tune(ProgramSelector selector, @Nullable TuneCallback resultCb) {
+ synchronized (mLock) {
+ markOperationFinishedLocked(/* succeeded= */ false);
+ mOperationSelector = selector;
+ mOperationResultCb = resultCb;
}
+
+ mTuner.tune(selector);
}
/**
@@ -176,8 +174,11 @@
return null;
}
+ @GuardedBy("mLock")
private void markOperationFinishedLocked(boolean succeeded) {
- if (mOperationResultCb == null) return;
+ if (mOperationResultCb == null) {
+ return;
+ }
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Tune operation for " + mOperationSelector
@@ -196,32 +197,41 @@
}
}
- private boolean isMatching(@NonNull ProgramSelector currentOperation,
- @NonNull ProgramSelector event) {
+ private boolean isMatching(ProgramSelector currentOperation, ProgramSelector event) {
ProgramSelector.Identifier pri = currentOperation.getPrimaryId();
return Stream.of(event.getAllIds(pri.getType())).anyMatch(id -> pri.equals(id));
}
private void onProgramInfoChanged(RadioManager.ProgramInfo info) {
synchronized (mLock) {
- if (mOperationResultCb == null) return;
+ if (mOperationResultCb == null) {
+ return;
+ }
// if we're seeking, all program info chanes does match
if (mOperationSelector != null) {
- if (!isMatching(mOperationSelector, info.getSelector())) return;
+ if (!isMatching(mOperationSelector, info.getSelector())) {
+ return;
+ }
}
- markOperationFinishedLocked(true);
+ markOperationFinishedLocked(/* succeeded= */ true);
}
}
private void onTuneFailed(int result, @Nullable ProgramSelector selector) {
synchronized (mLock) {
- if (mOperationResultCb == null) return;
- // if we're seeking and got a failed tune (or vice versa), that's a mismatch
- if ((mOperationSelector == null) != (selector == null)) return;
- if (mOperationSelector != null) {
- if (!isMatching(mOperationSelector, selector)) return;
+ if (mOperationResultCb == null) {
+ return;
}
- markOperationFinishedLocked(false);
+ // if we're seeking and got a failed tune (or vice versa), that's a mismatch
+ if ((mOperationSelector == null) != (selector == null)) {
+ return;
+ }
+ if (mOperationSelector != null) {
+ if (!isMatching(mOperationSelector, selector)) {
+ return;
+ }
+ }
+ markOperationFinishedLocked(/* succeeded= */ false);
}
}
@@ -229,34 +239,33 @@
* See {@link RadioTuner#cancel}.
*/
public void cancel() {
- synchronized (mTuneLock) {
- synchronized (mLock) {
- markOperationFinishedLocked(false);
- }
+ synchronized (mLock) {
+ markOperationFinishedLocked(/* succeeded= */ false);
+ }
- int res = mTuner.cancel();
- if (res != RadioManager.STATUS_OK) {
- Log.e(TAG, "Cancel failed with result of " + res);
- }
+ int res = mTuner.cancel();
+ if (res != RadioManager.STATUS_OK) {
+ Log.e(TAG, "Cancel failed with result of " + res);
}
}
/**
* See {@link RadioTuner#getDynamicProgramList}.
*/
- public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
+ @Nullable
+ public ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
return mTuner.getDynamicProgramList(filter);
}
public void close() {
synchronized (mLock) {
- markOperationFinishedLocked(false);
+ markOperationFinishedLocked(/* succeeded= */ false);
+ if (mHwAudioSource != null) {
+ mHwAudioSource.stop();
+ mHwAudioSource = null;
+ }
}
- if (mHwAudioSource != null) {
- mHwAudioSource.stop();
- mHwAudioSource = null;
- }
mTuner.close();
}
}
diff --git a/src/com/android/car/radio/platform/TunerCallbackAdapterExt.java b/src/com/android/car/radio/platform/TunerCallbackAdapterExt.java
index cdf8748..d04991e 100644
--- a/src/com/android/car/radio/platform/TunerCallbackAdapterExt.java
+++ b/src/com/android/car/radio/platform/TunerCallbackAdapterExt.java
@@ -23,7 +23,6 @@
import android.os.Handler;
import android.os.Looper;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.radio.util.Log;
@@ -42,7 +41,7 @@
*
* They might eventually get pushed to the framework.
*/
-class TunerCallbackAdapterExt extends RadioTuner.Callback {
+final class TunerCallbackAdapterExt extends RadioTuner.Callback {
private static final String TAG = "BcRadioApp.tunerext";
private static final int INIT_TIMEOUT_MS = 10000; // 10s
@@ -67,8 +66,8 @@
void onProgramInfoChanged(RadioManager.ProgramInfo info);
}
- TunerCallbackAdapterExt(@NonNull RadioTuner.Callback callback, @Nullable Handler handler) {
- mCallback = Objects.requireNonNull(callback);
+ TunerCallbackAdapterExt(RadioTuner.Callback callback, @Nullable Handler handler) {
+ mCallback = Objects.requireNonNull(callback, "Callback cannot be null");
if (handler == null) {
mHandler = new Handler(Looper.getMainLooper());
} else {
@@ -175,7 +174,7 @@
mHandler.post(() -> mCallback.onProgramListChanged());
}
- public void onParametersUpdated(@NonNull Map<String, String> parameters) {
+ public void onParametersUpdated(Map<String, String> parameters) {
mHandler.post(() -> mCallback.onParametersUpdated(parameters));
}
}
diff --git a/src/com/android/car/radio/service/RadioAppService.java b/src/com/android/car/radio/service/RadioAppService.java
index e906bcd..89d0b69 100644
--- a/src/com/android/car/radio/service/RadioAppService.java
+++ b/src/com/android/car/radio/service/RadioAppService.java
@@ -102,6 +102,8 @@
private long mLastProgramListPush;
@GuardedBy("mLock")
private RegionConfig mRegionConfigCache;
+ @GuardedBy("mLock")
+ private boolean mCanUpdateCurrentProgram;
private SkipController mSkipController;
@@ -197,6 +199,19 @@
return mLifecycleRegistry;
}
+ @GuardedBy("mLock")
+ private void setCanUpdateCurrentProgramLocked() {
+ if (mCanUpdateCurrentProgram) {
+ return;
+ }
+ mCanUpdateCurrentProgram = true;
+ }
+
+ @GuardedBy("mLock")
+ private boolean canUpdateCurrentProgramLocked() {
+ return mCanUpdateCurrentProgram;
+ }
+
private void onPlaybackStateChanged(int newState) {
Log.d(TAG, "onPlaybackStateChanged new state [%d]", newState);
synchronized (mLock) {
@@ -243,6 +258,7 @@
} catch (IllegalArgumentException | UnsupportedOperationException e) {
Log.e(TAG, "Can't restore recently selected program: " + sel, e);
}
+ setCanUpdateCurrentProgramLocked();
return;
}
@@ -345,6 +361,7 @@
if (tuneCb == null) return;
mRadioTuner.tune(sel, tuneCb.alsoCall(
succ -> tryExec(() -> callback.onFinished(succ))));
+ setCanUpdateCurrentProgramLocked();
}
}
@@ -359,6 +376,7 @@
if (tuneCb == null) return;
mRadioTuner.seek(forward, tuneCb.alsoCall(
succ -> tryExec(() -> callback.onFinished(succ))));
+ setCanUpdateCurrentProgramLocked();
}
}
@@ -391,6 +409,7 @@
if (tuneCb == null) return;
mRadioTuner.step(forward, tuneCb.alsoCall(
succ -> tryExec(() -> callback.onFinished(succ))));
+ setCanUpdateCurrentProgramLocked();
}
}
@@ -445,6 +464,10 @@
Log.d(TAG, "Program info changed: %s", info);
synchronized (mLock) {
+ if (!canUpdateCurrentProgramLocked()) {
+ return;
+ }
+
mCurrentProgram = info;
/* Storing recently selected program might be limited to explicit tune calls only
diff --git a/src/com/android/car/radio/storage/RadioStorage.java b/src/com/android/car/radio/storage/RadioStorage.java
index 632d453..4211b78 100644
--- a/src/com/android/car/radio/storage/RadioStorage.java
+++ b/src/com/android/car/radio/storage/RadioStorage.java
@@ -21,7 +21,9 @@
import android.hardware.radio.ProgramSelector;
import android.net.Uri;
import android.os.AsyncTask;
+import android.util.ArrayMap;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
@@ -33,6 +35,7 @@
import com.android.car.radio.util.Log;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
/**
@@ -47,7 +50,10 @@
private static final String PREF_KEY_SKIP_MODE = "smartSeekMode";
- private static RadioStorage sInstance;
+ private static final Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static Map<Integer, RadioStorage> sInstances = new ArrayMap<>();
private final SharedPreferences mPrefs;
private final RadioDatabase mDatabase;
@@ -61,14 +67,20 @@
}
/**
- * Returns singleton instance of {@link RadioStorage}.
+ * Returns singleton instance of {@link RadioStorage} for the user in context.
+ *
+ * @param context Application context
*/
- public static @NonNull RadioStorage getInstance(Context context) {
- if (sInstance != null) return sInstance;
- synchronized (RadioStorage.class) {
- if (sInstance != null) return sInstance;
- sInstance = new RadioStorage(context.getApplicationContext());
- return sInstance;
+ @NonNull
+ public static RadioStorage getInstance(Context context) {
+ int userId = context.getUserId();
+ synchronized (sLock) {
+ if (sInstances.containsKey(userId)) {
+ return sInstances.get(userId);
+ }
+ RadioStorage newInstance = new RadioStorage(context.getApplicationContext());
+ sInstances.put(userId, newInstance);
+ return newInstance;
}
}
diff --git a/tools/generate-overlayable.sh b/tools/generate-overlayable.sh
new file mode 100755
index 0000000..7a2e140
--- /dev/null
+++ b/tools/generate-overlayable.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+# Copyright (C) 2021 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.
+# Run this script to regenerate the overlayable.xml file.
+if [[ -z "$ANDROID_BUILD_TOP" ]]; then
+ echo 'ANDROID_BUILD_TOP environment variable is empty; did you forget to run `lunch`?'
+ exit 1
+fi
+PROJECT_TOP=$ANDROID_BUILD_TOP/packages/apps/Car/Radio
+python3 $ANDROID_BUILD_TOP/packages/apps/Car/systemlibs/tools/rro/generate-overlayable.py \
+ -n CarRadio \
+ -r $PROJECT_TOP/res \
+ -e $PROJECT_TOP/res/values/overlayable.xml \
+ -o $PROJECT_TOP/res/values/overlayable.xml