Initial implementation of Java-based broadcast radio service.

It provides only limited amount of service, without actual interaction
with HAL.

Added config.enable_java_radio switch to use Java-based service instead
of native. Added FEATURE_RADIO to PackageManager.

Bug: b/36863239
Test: Instrumentation, manual (Kitchen Sink)

Change-Id: I01139d326893c0a437c60cc35d6e5b005da35231
diff --git a/Android.mk b/Android.mk
index cf4a7aa..d3415b3 100644
--- a/Android.mk
+++ b/Android.mk
@@ -216,6 +216,8 @@
 	core/java/android/hardware/location/IGeofenceHardwareMonitorCallback.aidl \
 	core/java/android/hardware/location/IContextHubCallback.aidl \
 	core/java/android/hardware/location/IContextHubService.aidl \
+	core/java/android/hardware/radio/IRadioService.aidl \
+	core/java/android/hardware/radio/ITuner.aidl \
 	core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl \
 	core/java/android/hardware/usb/IUsbManager.aidl \
 	core/java/android/net/ICaptivePortal.aidl \
@@ -674,6 +676,7 @@
 	frameworks/base/core/java/android/print/PrinterInfo.aidl \
 	frameworks/base/core/java/android/print/PrintJobId.aidl \
 	frameworks/base/core/java/android/printservice/recommendation/RecommendationInfo.aidl \
+	frameworks/base/core/java/android/hardware/radio/RadioManager.aidl \
 	frameworks/base/core/java/android/hardware/usb/UsbDevice.aidl \
 	frameworks/base/core/java/android/hardware/usb/UsbInterface.aidl \
 	frameworks/base/core/java/android/hardware/usb/UsbEndpoint.aidl \
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index c2c40df..abc50ae 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -784,7 +784,7 @@
         registerService(Context.RADIO_SERVICE, RadioManager.class,
                 new CachedServiceFetcher<RadioManager>() {
             @Override
-            public RadioManager createService(ContextImpl ctx) {
+            public RadioManager createService(ContextImpl ctx) throws ServiceNotFoundException {
                 return new RadioManager(ctx);
             }});
 
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 4f0ff61..7ba2e96 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1829,6 +1829,14 @@
     public static final String FEATURE_VULKAN_HARDWARE_VERSION = "android.hardware.vulkan.version";
 
     /**
+     * The device includes broadcast radio tuner.
+     *
+     * @hide FutureFeature
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_RADIO = "android.hardware.radio";
+
+    /**
      * Feature for {@link #getSystemAvailableFeatures} and
      * {@link #hasSystemFeature}: The device includes an accelerometer.
      */
diff --git a/core/java/android/hardware/radio/IRadioService.aidl b/core/java/android/hardware/radio/IRadioService.aidl
new file mode 100644
index 0000000..3c83114
--- /dev/null
+++ b/core/java/android/hardware/radio/IRadioService.aidl
@@ -0,0 +1,28 @@
+/**
+ * Copyright (C) 2017 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 android.hardware.radio;
+
+import android.hardware.radio.ITuner;
+
+/**
+ * API to the broadcast radio service.
+ *
+ * {@hide}
+ */
+interface IRadioService {
+    ITuner openTuner();
+}
diff --git a/core/java/android/hardware/radio/ITuner.aidl b/core/java/android/hardware/radio/ITuner.aidl
new file mode 100644
index 0000000..73f6dc2
--- /dev/null
+++ b/core/java/android/hardware/radio/ITuner.aidl
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) 2017 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 android.hardware.radio;
+
+import android.hardware.radio.RadioManager;
+
+/** {@hide} */
+interface ITuner {
+    int getProgramInformation(out RadioManager.ProgramInfo[] infoOut);
+}
diff --git a/core/java/android/hardware/radio/RadioManager.aidl b/core/java/android/hardware/radio/RadioManager.aidl
new file mode 100644
index 0000000..d79ae4f
--- /dev/null
+++ b/core/java/android/hardware/radio/RadioManager.aidl
@@ -0,0 +1,20 @@
+/**
+ * Copyright (C) 2017 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 android.hardware.radio;
+
+/** @hide */
+parcelable RadioManager.ProgramInfo;
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index ac2bd95..99412de 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -17,12 +17,19 @@
 package android.hardware.radio;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.content.Context;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.os.SystemProperties;
 import android.text.TextUtils;
+import android.util.Log;
+
 import java.util.List;
 import java.util.Arrays;
 
@@ -35,6 +42,7 @@
  */
 @SystemApi
 public class RadioManager {
+    private static final String TAG = "RadioManager";
 
     /** Method return status: successful operation */
     public static final int STATUS_OK = 0;
@@ -1434,23 +1442,40 @@
     public RadioTuner openTuner(int moduleId, BandConfig config, boolean withAudio,
             RadioTuner.Callback callback, Handler handler) {
         if (callback == null) {
-            return null;
+            throw new IllegalArgumentException("callback must not be empty");
         }
-        RadioModule module = new RadioModule(moduleId, config, withAudio, callback, handler);
-        if (module != null) {
-            if (!module.initCheck()) {
-                module = null;
+
+        if (mService != null) {
+            ITuner tuner;
+            try {
+                tuner = mService.openTuner();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
             }
+            return new TunerAdapter(tuner);
         }
+
+        RadioModule module = new RadioModule(moduleId, config, withAudio, callback, handler);
+        if (!module.initCheck()) {
+            Log.e(TAG, "Failed to open tuner");
+            module = null;
+        }
+
         return (RadioTuner)module;
     }
 
-    private final Context mContext;
+    @NonNull private final Context mContext;
+    // TODO(b/36863239): NonNull when transitioned from native service
+    @Nullable private final IRadioService mService;
 
     /**
      * @hide
      */
-    public RadioManager(Context context) {
+    public RadioManager(@NonNull Context context) throws ServiceNotFoundException {
         mContext = context;
+
+        boolean isServiceJava = SystemProperties.getBoolean("config.enable_java_radio", false);
+        mService = isServiceJava ? IRadioService.Stub.asInterface(
+                ServiceManager.getServiceOrThrow(Context.RADIO_SERVICE)) : null;
     }
 }
diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java
new file mode 100644
index 0000000..1822e07b
--- /dev/null
+++ b/core/java/android/hardware/radio/TunerAdapter.java
@@ -0,0 +1,143 @@
+/**
+ * Copyright (C) 2017 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 android.hardware.radio;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements the RadioTuner interface by forwarding calls to radio service.
+ */
+class TunerAdapter extends RadioTuner {
+    private static final String TAG = "radio.TunerAdapter";
+
+    @NonNull private final ITuner mTuner;
+
+    TunerAdapter(ITuner tuner) {
+        if (tuner == null) {
+            throw new NullPointerException();
+        }
+        mTuner = tuner;
+    }
+
+    @Override
+    public void close() {
+        // TODO(b/36863239): forward to mTuner
+        Log.w(TAG, "Close call not implemented");
+    }
+
+    @Override
+    public int setConfiguration(RadioManager.BandConfig config) {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public int getConfiguration(RadioManager.BandConfig[] config) {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public int setMute(boolean mute) {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public boolean getMute() {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public int step(int direction, boolean skipSubChannel) {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public int scan(int direction, boolean skipSubChannel) {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public int tune(int channel, int subChannel) {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public int cancel() {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public int getProgramInformation(RadioManager.ProgramInfo[] info) {
+        if (info == null || info.length != 1) {
+            throw new IllegalArgumentException("The argument must be an array of length 1");
+        }
+        try {
+            return mTuner.getProgramInformation(info);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    public boolean startBackgroundScan() {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public @NonNull List<RadioManager.ProgramInfo> getProgramList(@Nullable String filter) {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public boolean isAnalogForced() {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public void setAnalogForced(boolean isForced) {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public boolean isAntennaConnected() {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public boolean hasControl() {
+        // TODO(b/36863239): forward to mTuner
+        throw new RuntimeException("Not implemented");
+    }
+}
diff --git a/services/core/java/com/android/server/radio/RadioService.java b/services/core/java/com/android/server/radio/RadioService.java
new file mode 100644
index 0000000..327e98f
--- /dev/null
+++ b/services/core/java/com/android/server/radio/RadioService.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (C) 2017 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.server.radio;
+
+import android.content.Context;
+import android.hardware.radio.IRadioService;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.RadioManager;
+import android.util.Slog;
+
+import com.android.server.SystemService;
+
+public class RadioService extends SystemService {
+    // TODO(b/36863239): rename to RadioService when native service goes away
+    private static final String TAG = "RadioServiceJava";
+
+    public RadioService(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(Context.RADIO_SERVICE, new RadioServiceImpl());
+        Slog.v(TAG, "RadioService started");
+    }
+
+    private static class RadioServiceImpl extends IRadioService.Stub {
+        @Override
+        public ITuner openTuner() {
+            Slog.d(TAG, "openTuner()");
+            return new TunerImpl();
+        }
+    }
+
+    private static class TunerImpl extends ITuner.Stub {
+        @Override
+        public int getProgramInformation(RadioManager.ProgramInfo[] infoOut) {
+            Slog.d(TAG, "getProgramInformation()");
+            return RadioManager.STATUS_INVALID_OPERATION;
+        }
+    }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index f74512a..ad25ce0 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -96,6 +96,7 @@
 import com.android.server.policy.PhoneWindowManager;
 import com.android.server.power.PowerManagerService;
 import com.android.server.power.ShutdownThread;
+import com.android.server.radio.RadioService;
 import com.android.server.restrictions.RestrictionsManagerService;
 import com.android.server.retaildemo.RetailDemoModeService;
 import com.android.server.security.KeyAttestationApplicationIdProviderService;
@@ -711,6 +712,8 @@
         boolean disableVrManager = SystemProperties.getBoolean("config.disable_vrmanager", false);
         boolean disableCameraService = SystemProperties.getBoolean("config.disable_cameraservice",
                 false);
+        // TODO(b/36863239): Remove when transitioned from native service.
+        boolean enableRadioService = SystemProperties.getBoolean("config.enable_java_radio", false);
 
         boolean isEmulator = SystemProperties.get("ro.kernel.qemu").equals("1");
 
@@ -1211,6 +1214,13 @@
             mSystemServiceManager.startService(AudioService.Lifecycle.class);
             traceEnd();
 
+            if (enableRadioService &&
+                    mPackageManager.hasSystemFeature(PackageManager.FEATURE_RADIO)) {
+                traceBeginAndSlog("StartRadioService");
+                mSystemServiceManager.startService(RadioService.class);
+                traceEnd();
+            }
+
             if (!disableNonCoreServices) {
                 traceBeginAndSlog("StartDockObserver");
                 mSystemServiceManager.startService(DockObserver.class);
diff --git a/tests/radio/Android.mk b/tests/radio/Android.mk
new file mode 100644
index 0000000..46bf9cc
--- /dev/null
+++ b/tests/radio/Android.mk
@@ -0,0 +1,33 @@
+# Copyright (C) 2017 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_PACKAGE_NAME := RadioTests
+
+LOCAL_MODULE_TAGS := tests
+# TODO(b/13282254): uncomment when b/13282254 is fixed
+# LOCAL_SDK_VERSION := current
+
+LOCAL_STATIC_JAVA_LIBRARIES := compatibility-device-util android-support-test
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_DEX_PREOPT := false
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
diff --git a/tests/radio/AndroidManifest.xml b/tests/radio/AndroidManifest.xml
new file mode 100644
index 0000000..150edbf
--- /dev/null
+++ b/tests/radio/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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="android.hardware.radio.tests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.hardware.radio.tests"
+        android:label="Tests for broadcast radio API" >
+    </instrumentation>
+</manifest>
diff --git a/tests/radio/src/android/hardware/radio/tests/RadioTest.java b/tests/radio/src/android/hardware/radio/tests/RadioTest.java
new file mode 100644
index 0000000..47fbe74
--- /dev/null
+++ b/tests/radio/src/android/hardware/radio/tests/RadioTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 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 android.hardware.radio.tests;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioTuner;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+/**
+ * A test for broadcast radio API.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RadioTest {
+
+    public final Context mContext = InstrumentationRegistry.getContext();
+
+    private RadioManager mRadioManager;
+    private RadioTuner mRadioTuner;
+    private final List<RadioManager.ModuleProperties> mModules = new ArrayList<>();
+
+    @Before
+    public void setup() {
+        // check if radio is supported and skip the test if it's not
+        PackageManager packageManager = mContext.getPackageManager();
+        boolean isRadioSupported = packageManager.hasSystemFeature(PackageManager.FEATURE_RADIO);
+        assumeTrue(isRadioSupported);
+
+        mRadioManager = (RadioManager)mContext.getSystemService(Context.RADIO_SERVICE);
+        assertNotNull(mRadioManager);
+
+        int status = mRadioManager.listModules(mModules);
+        assertEquals(RadioManager.STATUS_OK, status);
+        assertFalse(mModules.isEmpty());
+    }
+
+    @After
+    public void tearDown() {
+        mRadioManager = null;
+        mModules.clear();
+        if (mRadioTuner != null) {
+            mRadioTuner.close();
+            mRadioTuner = null;
+        }
+    }
+
+    private void openTuner(RadioTuner.Callback callback) {
+        assertNull(mRadioTuner);
+
+        // find FM band and build its config
+        RadioManager.ModuleProperties module = mModules.get(0);
+        RadioManager.FmBandDescriptor fmBandDescriptor = null;
+        for (RadioManager.BandDescriptor band : module.getBands()) {
+            if (band.getType() == RadioManager.BAND_FM) {
+                fmBandDescriptor = (RadioManager.FmBandDescriptor)band;
+                break;
+            }
+        }
+        assertNotNull(fmBandDescriptor);
+        RadioManager.BandConfig fmBandConfig =
+            new RadioManager.FmBandConfig.Builder(fmBandDescriptor).build();
+
+        mRadioTuner = mRadioManager.openTuner(module.getId(), fmBandConfig, true, callback, null);
+        assertNotNull(mRadioTuner);
+    }
+
+    @Test
+    public void testOpenTuner() {
+        openTuner(new RadioTuner.Callback() {});
+    }
+}