1/**
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.car.radio.platform;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.graphics.Bitmap;
23import android.hardware.radio.RadioManager;
24import android.hardware.radio.RadioManager.BandDescriptor;
25import android.hardware.radio.RadioTuner;
26import android.os.Handler;
27import android.os.HandlerThread;
28import android.util.Log;
29
30import com.android.car.broadcastradio.support.platform.RadioMetadataExt;
31
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.HashMap;
35import java.util.List;
36import java.util.Map;
37import java.util.Objects;
38import java.util.stream.Collectors;
39
40/**
41 * Proposed extensions to android.hardware.radio.RadioManager.
42 *
43 * They might eventually get pushed to the framework.
44 */
45public class RadioManagerExt {
46    private static final String TAG = "BcRadioApp.mgrext";
47
48    // For now, we open first radio module only.
49    private static final int HARDCODED_MODULE_INDEX = 0;
50
51    private final Object mLock = new Object();
52
53    private final HandlerThread mCallbackHandlerThread = new HandlerThread("BcRadioApp.cbhandler");
54
55    private final @NonNull RadioManager mRadioManager;
56    private final RadioTunerExt mRadioTunerExt;
57    private List<RadioManager.ModuleProperties> mModules;
58    private @Nullable List<BandDescriptor> mAmFmRegionConfig;
59
60    public RadioManagerExt(@NonNull Context ctx) {
61        mRadioManager = (RadioManager)ctx.getSystemService(Context.RADIO_SERVICE);
62        Objects.requireNonNull(mRadioManager, "RadioManager could not be loaded");
63        mRadioTunerExt = new RadioTunerExt(ctx);
64        mCallbackHandlerThread.start();
65    }
66
67    public RadioTunerExt getRadioTunerExt() {
68        return mRadioTunerExt;
69    }
70
71    /* Select only one region. HAL 2.x moves region selection responsibility from the app to the
72     * Broadcast Radio service, so we won't implement region selection based on bands in the app.
73     */
74    private @Nullable List<BandDescriptor> reduceAmFmBands(@Nullable BandDescriptor[] bands) {
75        if (bands == null || bands.length == 0) return null;
76        int region = bands[0].getRegion();
77        Log.d(TAG, "Auto-selecting region " + region);
78
79        return Arrays.stream(bands).filter(band -> band.getRegion() == region).
80                collect(Collectors.toList());
81    }
82
83    private void initModules() {
84        synchronized (mLock) {
85            if (mModules != null) return;
86
87            mModules = new ArrayList<>();
88            int status = mRadioManager.listModules(mModules);
89            if (status != RadioManager.STATUS_OK) {
90                Log.w(TAG, "Couldn't get radio module list: " + status);
91                return;
92            }
93
94            if (mModules.size() == 0) {
95                Log.i(TAG, "No radio modules on this device");
96                return;
97            }
98
99            RadioManager.ModuleProperties module = mModules.get(HARDCODED_MODULE_INDEX);
100            mAmFmRegionConfig = reduceAmFmBands(module.getBands());
101        }
102    }
103
104    public @Nullable RadioTuner openSession(RadioTuner.Callback callback, Handler handler) {
105        Log.i(TAG, "Opening broadcast radio session...");
106
107        initModules();
108        if (mModules.size() == 0) return null;
109
110        /* We won't need custom default wrapper when we push these proposed extensions to the
111         * framework; this is solely to avoid deadlock on onConfigurationChanged callback versus
112         * waitForInitialization.
113         */
114        Handler hwHandler = new Handler(mCallbackHandlerThread.getLooper());
115
116        RadioManager.ModuleProperties module = mModules.get(HARDCODED_MODULE_INDEX);
117        TunerCallbackAdapterExt cbExt = new TunerCallbackAdapterExt(callback, handler);
118
119        RadioTuner tuner = mRadioManager.openTuner(
120                module.getId(),
121                null,  // BandConfig - let the service automatically select one.
122                true,  // withAudio
123                cbExt, hwHandler);
124        mSessions.put(module.getId(), tuner);
125        if (tuner == null) return null;
126        RadioMetadataExt.setModuleId(module.getId());
127
128        if (module.isInitializationRequired()) {
129            if (!cbExt.waitForInitialization()) {
130                Log.w(TAG, "Timed out waiting for tuner initialization");
131                tuner.close();
132                return null;
133            }
134        }
135
136        return tuner;
137    }
138
139    public @Nullable List<BandDescriptor> getAmFmRegionConfig() {
140        initModules();
141        return mAmFmRegionConfig;
142    }
143
144    /* This won't be necessary when we push this code to the framework,
145     * as we really need only module references. */
146    private static Map<Integer, RadioTuner> mSessions = new HashMap<>();
147
148    public @Nullable Bitmap getMetadataImage(long globalId) {
149        if (globalId == 0) return null;
150
151        int moduleId = (int)(globalId >>> 32);
152        int localId = (int)(globalId & 0xFFFFFFFF);
153
154        RadioTuner tuner = mSessions.get(moduleId);
155        if (tuner == null) return null;
156
157        return tuner.getMetadataImage(localId);
158    }
159}
160