1/**
2 * Copyright (C) 2017 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.server.broadcastradio;
18
19import android.annotation.NonNull;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.hardware.radio.ITuner;
23import android.hardware.radio.ITunerCallback;
24import android.hardware.radio.ProgramSelector;
25import android.hardware.radio.RadioManager;
26import android.os.IBinder;
27import android.os.RemoteException;
28import android.util.Slog;
29
30import java.util.List;
31import java.util.Map;
32
33class Tuner extends ITuner.Stub {
34    private static final String TAG = "BroadcastRadioService.Tuner";
35
36    /**
37     * This field is used by native code, do not access or modify.
38     */
39    private final long mNativeContext;
40
41    private final Object mLock = new Object();
42    @NonNull private final TunerCallback mTunerCallback;
43    @NonNull private final ITunerCallback mClientCallback;
44    @NonNull private final IBinder.DeathRecipient mDeathRecipient;
45
46    private boolean mIsClosed = false;
47    private boolean mIsMuted = false;
48    private int mRegion;  // TODO(b/62710330): find better solution to handle regions
49    private final boolean mWithAudio;
50
51    Tuner(@NonNull ITunerCallback clientCallback, int halRev,
52            int region, boolean withAudio, int band) {
53        mClientCallback = clientCallback;
54        mTunerCallback = new TunerCallback(this, clientCallback, halRev);
55        mRegion = region;
56        mWithAudio = withAudio;
57        mNativeContext = nativeInit(halRev, withAudio, band);
58        mDeathRecipient = this::close;
59        try {
60            mClientCallback.asBinder().linkToDeath(mDeathRecipient, 0);
61        } catch (RemoteException ex) {
62            close();
63        }
64    }
65
66    @Override
67    protected void finalize() throws Throwable {
68        nativeFinalize(mNativeContext);
69        super.finalize();
70    }
71
72    private native long nativeInit(int halRev, boolean withAudio, int band);
73    private native void nativeFinalize(long nativeContext);
74    private native void nativeClose(long nativeContext);
75
76    private native void nativeSetConfiguration(long nativeContext,
77            @NonNull RadioManager.BandConfig config);
78    private native RadioManager.BandConfig nativeGetConfiguration(long nativeContext, int region);
79
80    private native void nativeSetMuted(long nativeContext, boolean mute);
81
82    private native void nativeStep(long nativeContext, boolean directionDown, boolean skipSubChannel);
83    private native void nativeScan(long nativeContext, boolean directionDown, boolean skipSubChannel);
84    private native void nativeTune(long nativeContext, @NonNull ProgramSelector selector);
85    private native void nativeCancel(long nativeContext);
86
87    private native void nativeCancelAnnouncement(long nativeContext);
88
89    private native RadioManager.ProgramInfo nativeGetProgramInformation(long nativeContext);
90    private native boolean nativeStartBackgroundScan(long nativeContext);
91    private native List<RadioManager.ProgramInfo> nativeGetProgramList(long nativeContext,
92            Map<String, String> vendorFilter);
93
94    private native byte[] nativeGetImage(long nativeContext, int id);
95
96    private native boolean nativeIsAnalogForced(long nativeContext);
97    private native void nativeSetAnalogForced(long nativeContext, boolean isForced);
98
99    private native boolean nativeIsAntennaConnected(long nativeContext);
100
101    @Override
102    public void close() {
103        synchronized (mLock) {
104            if (mIsClosed) return;
105            mIsClosed = true;
106            mTunerCallback.detach();
107            mClientCallback.asBinder().unlinkToDeath(mDeathRecipient, 0);
108            nativeClose(mNativeContext);
109        }
110    }
111
112    @Override
113    public boolean isClosed() {
114        return mIsClosed;
115    }
116
117    private void checkNotClosedLocked() {
118        if (mIsClosed) {
119            throw new IllegalStateException("Tuner is closed, no further operations are allowed");
120        }
121    }
122
123    @Override
124    public void setConfiguration(RadioManager.BandConfig config) {
125        if (config == null) {
126            throw new IllegalArgumentException("The argument must not be a null pointer");
127        }
128        synchronized (mLock) {
129            checkNotClosedLocked();
130            nativeSetConfiguration(mNativeContext, config);
131            mRegion = config.getRegion();
132        }
133    }
134
135    @Override
136    public RadioManager.BandConfig getConfiguration() {
137        synchronized (mLock) {
138            checkNotClosedLocked();
139            return nativeGetConfiguration(mNativeContext, mRegion);
140        }
141    }
142
143    @Override
144    public void setMuted(boolean mute) {
145        if (!mWithAudio) {
146            throw new IllegalStateException("Can't operate on mute - no audio requested");
147        }
148        synchronized (mLock) {
149            checkNotClosedLocked();
150            if (mIsMuted == mute) return;
151            mIsMuted = mute;
152
153            nativeSetMuted(mNativeContext, mute);
154        }
155    }
156
157    @Override
158    public boolean isMuted() {
159        if (!mWithAudio) {
160            Slog.w(TAG, "Tuner did not request audio, pretending it was muted");
161            return true;
162        }
163        synchronized (mLock) {
164            checkNotClosedLocked();
165            return mIsMuted;
166        }
167    }
168
169    @Override
170    public void step(boolean directionDown, boolean skipSubChannel) {
171        synchronized (mLock) {
172            checkNotClosedLocked();
173            nativeStep(mNativeContext, directionDown, skipSubChannel);
174        }
175    }
176
177    @Override
178    public void scan(boolean directionDown, boolean skipSubChannel) {
179        synchronized (mLock) {
180            checkNotClosedLocked();
181            nativeScan(mNativeContext, directionDown, skipSubChannel);
182        }
183    }
184
185    @Override
186    public void tune(ProgramSelector selector) {
187        if (selector == null) {
188            throw new IllegalArgumentException("The argument must not be a null pointer");
189        }
190        Slog.i(TAG, "Tuning to " + selector);
191        synchronized (mLock) {
192            checkNotClosedLocked();
193            nativeTune(mNativeContext, selector);
194        }
195    }
196
197    @Override
198    public void cancel() {
199        synchronized (mLock) {
200            checkNotClosedLocked();
201            nativeCancel(mNativeContext);
202        }
203    }
204
205    @Override
206    public void cancelAnnouncement() {
207        synchronized (mLock) {
208            checkNotClosedLocked();
209            nativeCancelAnnouncement(mNativeContext);
210        }
211    }
212
213    @Override
214    public RadioManager.ProgramInfo getProgramInformation() {
215        synchronized (mLock) {
216            checkNotClosedLocked();
217            return nativeGetProgramInformation(mNativeContext);
218        }
219    }
220
221    @Override
222    public Bitmap getImage(int id) {
223        if (id == 0) {
224            throw new IllegalArgumentException("Image ID is missing");
225        }
226
227        byte[] rawImage;
228        synchronized (mLock) {
229            rawImage = nativeGetImage(mNativeContext, id);
230        }
231        if (rawImage == null || rawImage.length == 0) {
232            return null;
233        }
234
235        return BitmapFactory.decodeByteArray(rawImage, 0, rawImage.length);
236    }
237
238    @Override
239    public boolean startBackgroundScan() {
240        synchronized (mLock) {
241            checkNotClosedLocked();
242            return nativeStartBackgroundScan(mNativeContext);
243        }
244    }
245
246    @Override
247    public List<RadioManager.ProgramInfo> getProgramList(Map vendorFilter) {
248        Map<String, String> sFilter = vendorFilter;
249        synchronized (mLock) {
250            checkNotClosedLocked();
251            List<RadioManager.ProgramInfo> list = nativeGetProgramList(mNativeContext, sFilter);
252            if (list == null) {
253                throw new IllegalStateException("Program list is not ready");
254            }
255            return list;
256        }
257    }
258
259    @Override
260    public boolean isAnalogForced() {
261        synchronized (mLock) {
262            checkNotClosedLocked();
263            return nativeIsAnalogForced(mNativeContext);
264        }
265    }
266
267    @Override
268    public void setAnalogForced(boolean isForced) {
269        synchronized (mLock) {
270            checkNotClosedLocked();
271            nativeSetAnalogForced(mNativeContext, isForced);
272        }
273    }
274
275    @Override
276    public boolean isAntennaConnected() {
277        synchronized (mLock) {
278            checkNotClosedLocked();
279            return nativeIsAntennaConnected(mNativeContext);
280        }
281    }
282}
283