aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPavel Maltsev <pavelm@google.com>2017-07-16 19:48:57 -0700
committerPavel Maltsev <pavelm@google.com>2017-07-19 19:23:12 -0700
commit905968cf95d4c8608d6d9351b5dd10fe994a1220 (patch)
treea05c210fae4e5139f12ce852d7066e17fd2abc32
parent909546fc1aea53bb86fb1a91d11a11bd2cd4befd (diff)
downloadCar-905968cf95d4c8608d6d9351b5dd10fe994a1220.tar.gz
Allow activities in instrument cluster
- Added new CarInstrumentClusterManager to start activity in the cluster and subscribe to cluster specific events - Cluster vendor implementation (InstrumentClusterRenderingService) was extended, now it can notify Car Service with ActivityOptions that holds info how to launch activity in the cluster for specific category, also it can send additional info such as unobscured bounds (ClusterActivityState) - added DirectRenderingClusterSample which is an example of vendor implementation that utilizes new features - added FakeClusterNavigationActivity in Kitchensink which is a dummy nav activity that can be run in the cluster, it has appropriate permissions and activity category in manifest Test: kitchensink Bug: b/37500371 Change-Id: Ic4b3709a3b7e1310dbd1c499990eea64479b3333
-rw-r--r--car-lib/api/system-current.txt13
-rw-r--r--car-lib/src/android/car/Car.java28
-rw-r--r--car-lib/src/android/car/cluster/CarInstrumentClusterManager.java232
-rw-r--r--car-lib/src/android/car/cluster/ClusterActivityState.java91
-rw-r--r--car-lib/src/android/car/cluster/IInstrumentClusterManagerCallback.aidl35
-rw-r--r--car-lib/src/android/car/cluster/IInstrumentClusterManagerService.aidl32
-rw-r--r--car-lib/src/android/car/cluster/renderer/DisplayConfiguration.java96
-rw-r--r--car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl5
-rw-r--r--car-lib/src/android/car/cluster/renderer/IInstrumentClusterCallback.aidl54
-rw-r--r--car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java111
-rw-r--r--car_product/build/car.mk4
-rw-r--r--service/AndroidManifest.xml11
-rw-r--r--service/res/values/config.xml2
-rw-r--r--service/res/values/strings.xml5
-rw-r--r--service/src/com/android/car/ICarImpl.java11
-rw-r--r--service/src/com/android/car/cluster/InstrumentClusterService.java286
-rw-r--r--tests/DirectRenderingClusterSample/Android.mk35
-rw-r--r--tests/DirectRenderingClusterSample/AndroidManifest.xml51
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_car_info.pngbin0 -> 694 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_music.pngbin0 -> 365 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_nav.pngbin0 -> 519 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_phone.pngbin0 -> 689 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_car_info.pngbin0 -> 454 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_music.pngbin0 -> 271 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_nav.pngbin0 -> 314 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_phone.pngbin0 -> 450 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_car_info.pngbin0 -> 800 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_music.pngbin0 -> 466 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_nav.pngbin0 -> 499 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_phone.pngbin0 -> 837 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_car_info.pngbin0 -> 1308 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_music.pngbin0 -> 727 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_nav.pngbin0 -> 903 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_phone.pngbin0 -> 1355 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable/btn_car_info.xml10
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable/btn_music.xml10
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable/btn_nav.xml10
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable/btn_phone.xml10
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable/car_top_view.pngbin0 -> 22694 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/drawable/focused_button_shape.xml13
-rw-r--r--tests/DirectRenderingClusterSample/res/layout/activity_main.xml69
-rw-r--r--tests/DirectRenderingClusterSample/res/layout/fragment_car_info.xml46
-rw-r--r--tests/DirectRenderingClusterSample/res/layout/fragment_music.xml14
-rw-r--r--tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml22
-rw-r--r--tests/DirectRenderingClusterSample/res/layout/fragment_phone.xml13
-rw-r--r--tests/DirectRenderingClusterSample/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3418 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2206 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4842 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 7718 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 10486 bytes
-rw-r--r--tests/DirectRenderingClusterSample/res/values-w820dp/dimens.xml6
-rw-r--r--tests/DirectRenderingClusterSample/res/values/colors.xml7
-rw-r--r--tests/DirectRenderingClusterSample/res/values/dimens.xml5
-rw-r--r--tests/DirectRenderingClusterSample/res/values/strings.xml6
-rw-r--r--tests/DirectRenderingClusterSample/res/values/styles.xml3
-rw-r--r--tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CarInfoFragment.java44
-rw-r--r--tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java221
-rw-r--r--tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MusicFragment.java39
-rw-r--r--tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java163
-rw-r--r--tests/DirectRenderingClusterSample/src/android/car/cluster/sample/PhoneFragment.java42
-rw-r--r--tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java259
-rw-r--r--tests/EmbeddedKitchenSinkApp/AndroidManifest.xml18
-rw-r--r--tests/EmbeddedKitchenSinkApp/res/layout/fake_cluster_navigation_activity.xml29
-rw-r--r--tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml17
-rw-r--r--tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml15
-rw-r--r--tests/EmbeddedKitchenSinkApp/res/values/strings.xml7
-rw-r--r--tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/FakeClusterNavigationActivity.java103
-rw-r--r--tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java113
68 files changed, 2243 insertions, 173 deletions
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index ee1afdedfb..14fc5abace 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -278,18 +278,6 @@ package android.car.app.menu {
package android.car.cluster.renderer {
- public class DisplayConfiguration implements android.os.Parcelable {
- ctor public DisplayConfiguration(android.os.Parcel);
- ctor public DisplayConfiguration(android.graphics.Rect);
- ctor public DisplayConfiguration(android.graphics.Rect, android.graphics.Rect);
- method public int describeContents();
- method public android.graphics.Rect getPrimaryRegion();
- method public android.graphics.Rect getSecondaryRegion();
- method public boolean hasSecondaryRegion();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<android.car.cluster.renderer.DisplayConfiguration> CREATOR;
- }
-
public abstract class InstrumentClusterRenderer {
ctor public InstrumentClusterRenderer();
method protected abstract android.car.cluster.renderer.NavigationRenderer createNavigationRenderer();
@@ -305,6 +293,7 @@ package android.car.cluster.renderer {
method protected abstract android.car.cluster.renderer.NavigationRenderer getNavigationRenderer();
method public android.os.IBinder onBind(android.content.Intent);
method protected void onKeyEvent(android.view.KeyEvent);
+ method public void setClusterActivityState(java.lang.String, android.os.Bundle) throws android.car.CarNotConnectedException;
}
public abstract class NavigationRenderer {
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index edd4d7ceff..706f7aa7d2 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -20,6 +20,7 @@ import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.car.annotation.FutureFeature;
+import android.car.cluster.CarInstrumentClusterManager;
import android.car.content.pm.CarPackageManager;
import android.car.hardware.CarDiagnosticManager;
import android.car.hardware.CarSensorManager;
@@ -44,7 +45,6 @@ import android.os.UserHandle;
import android.util.Log;
import com.android.car.internal.FeatureConfiguration;
-import com.android.car.internal.FeatureUtil;
import com.android.internal.annotations.GuardedBy;
import java.lang.annotation.Retention;
@@ -85,6 +85,11 @@ public final class Car {
* @hide
*/
public static final String CAR_NAVIGATION_SERVICE = "car_navigation_service";
+ /**
+ * Service name for {@link CarInstrumentClusterManager}
+ * @hide
+ */
+ public static final String CAR_INSTRUMENT_CLUSTER_SERVICE = "cluster_service";
/**
* @hide
@@ -170,6 +175,24 @@ public final class Car {
"android.car.permission.CAR_NAVIGATION_MANAGER";
/**
+ * Permission necessary to start activities in the instrument cluster through
+ * {@link CarInstrumentClusterManager}
+ *
+ * @hide
+ */
+ public static final String PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL =
+ "android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL";
+
+ /**
+ * Application must have this permission in order to be launched in the instrument cluster
+ * display.
+ *
+ * @hide
+ */
+ public static final String PERMISSION_CAR_DISPLAY_IN_CLUSTER =
+ "android.car.permission.CAR_DISPLAY_IN_CLUSTER";
+
+ /**
* Permission necessary to access car specific communication channel.
* @hide
*/
@@ -600,6 +623,9 @@ public final class Car {
case VENDOR_EXTENSION_SERVICE:
manager = new CarVendorExtensionManager(binder, mEventHandler);
break;
+ case CAR_INSTRUMENT_CLUSTER_SERVICE:
+ manager = new CarInstrumentClusterManager(binder, mEventHandler);
+ break;
case TEST_SERVICE:
/* CarTestManager exist in static library. So instead of constructing it here,
* only pass binder wrapper so that CarTestManager can be constructed outside. */
diff --git a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
new file mode 100644
index 0000000000..758ce9e9b1
--- /dev/null
+++ b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
@@ -0,0 +1,232 @@
+/*
+ * 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.car.cluster;
+
+import android.car.CarManagerBase;
+import android.car.CarNotConnectedException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * API to work with instrument cluster.
+ *
+ * @hide
+ */
+public class CarInstrumentClusterManager implements CarManagerBase {
+ private static final String TAG = CarInstrumentClusterManager.class.getSimpleName();
+
+ /** @hide */
+ public static final String CATEGORY_NAVIGATION = "android.car.cluster.NAVIGATION";
+
+ /**
+ * When activity in the cluster is launched it will receive {@link ClusterActivityState} in the
+ * intent's extra thus activity will know information about unobscured area, etc. upon activity
+ * creation.
+ *
+ * @hide
+ */
+ public static final String KEY_EXTRA_ACTIVITY_STATE =
+ "android.car.cluster.ClusterActivityState";
+
+ private final EventHandler mHandler;
+ private final Map<String, Set<Callback>> mCallbacksByCategory = new HashMap<>(0);
+ private final Object mLock = new Object();
+ private final Map<String, Bundle> mActivityStatesByCategory = new HashMap<>(0);
+
+ private final IInstrumentClusterManagerService mService;
+
+ private ClusterManagerCallback mServiceToManagerCallback;
+
+ /**
+ * Starts activity in the instrument cluster.
+ *
+ * @hide
+ */
+ public void startActivity(Intent intent) throws CarNotConnectedException {
+ try {
+ mService.startClusterActivity(intent);
+ } catch (RemoteException e) {
+ throw new CarNotConnectedException(e);
+ }
+ }
+
+ /**
+ * Caller of this method will receive immediate callback with the most recent state if state
+ * exists for given category.
+ *
+ * @param category category of the activity in the cluster,
+ * see {@link #CATEGORY_NAVIGATION}
+ * @param callback instance of {@link Callback} class to receive events.
+ *
+ * @hide
+ */
+ public void registerCallback(String category, Callback callback)
+ throws CarNotConnectedException {
+ Log.i(TAG, "registerCallback, category: " + category + ", callback: " + callback);
+ ClusterManagerCallback callbackToCarService = null;
+ synchronized (mLock) {
+ Set<Callback> callbacks = mCallbacksByCategory.get(category);
+ if (callbacks == null) {
+ callbacks = new HashSet<>(1);
+ mCallbacksByCategory.put(category, callbacks);
+ }
+ if (!callbacks.add(callback)) {
+ Log.w(TAG, "registerCallback: already registered");
+ return; // already registered
+ }
+
+ if (mActivityStatesByCategory.containsKey(category)) {
+ Log.i(TAG, "registerCallback: sending activity state...");
+ callback.onClusterActivityStateChanged(
+ category, mActivityStatesByCategory.get(category));
+ }
+
+ if (mServiceToManagerCallback == null) {
+ Log.i(TAG, "registerCallback: registering callback with car service...");
+ mServiceToManagerCallback = new ClusterManagerCallback();
+ callbackToCarService = mServiceToManagerCallback;
+ }
+ }
+ try {
+ mService.registerCallback(callbackToCarService);
+ Log.i(TAG, "registerCallback: done");
+ } catch (RemoteException e) {
+ throw new CarNotConnectedException(e);
+ }
+ }
+
+ /**
+ * Unregisters given callback for all activity categories.
+ *
+ * @param callback previously registered callback
+ *
+ * @hide
+ */
+ public void unregisterCallback(Callback callback) throws CarNotConnectedException {
+ List<String> keysToRemove = new ArrayList<>(1);
+ synchronized (mLock) {
+ for (Map.Entry<String, Set<Callback>> entry : mCallbacksByCategory.entrySet()) {
+ Set<Callback> callbacks = entry.getValue();
+ if (callbacks.remove(callback) && callbacks.isEmpty()) {
+ keysToRemove.add(entry.getKey());
+ }
+
+ }
+
+ for (String key: keysToRemove) {
+ mCallbacksByCategory.remove(key);
+ }
+
+ if (mCallbacksByCategory.isEmpty()) {
+ try {
+ mService.unregisterCallback(mServiceToManagerCallback);
+ } catch (RemoteException e) {
+ throw new CarNotConnectedException(e);
+ }
+ mServiceToManagerCallback = null;
+ }
+ }
+ }
+
+ /** @hide */
+ public CarInstrumentClusterManager(IBinder service, Handler handler) {
+ mService = IInstrumentClusterManagerService.Stub.asInterface(service);
+
+ mHandler = new EventHandler(handler.getLooper());
+ }
+
+ /** @hide */
+ public interface Callback {
+
+ /**
+ * Notify client that activity state was changed.
+ *
+ * @param category cluster activity category, see {@link #CATEGORY_NAVIGATION}
+ * @param clusterActivityState see {@link ClusterActivityState} how to read this bundle.
+ */
+ void onClusterActivityStateChanged(String category, Bundle clusterActivityState);
+ }
+
+ /** @hide */
+ @Override
+ public void onCarDisconnected() {
+ }
+
+ private class EventHandler extends Handler {
+
+ final static int MSG_ACTIVITY_STATE = 1;
+
+ EventHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ Log.i(TAG, "handleMessage, message: " + msg);
+ switch (msg.what) {
+ case MSG_ACTIVITY_STATE:
+ Pair<String, Bundle> info = (Pair<String, Bundle>) msg.obj;
+ String category = info.first;
+ Bundle state = info.second;
+ List<CarInstrumentClusterManager.Callback> callbacks = null;
+ synchronized (mLock) {
+ if (mCallbacksByCategory.containsKey(category)) {
+ callbacks = new ArrayList<>(mCallbacksByCategory.get(category));
+ }
+ }
+ Log.i(TAG, "handleMessage, callbacks: " + callbacks);
+ if (callbacks != null) {
+ for (CarInstrumentClusterManager.Callback cb : callbacks) {
+ cb.onClusterActivityStateChanged(category, state);
+ }
+ }
+ break;
+ default:
+ Log.e(TAG, "Unexpected message: " + msg.what);
+ }
+ }
+ }
+
+ private class ClusterManagerCallback extends IInstrumentClusterManagerCallback.Stub {
+
+ @Override
+ public void setClusterActivityState(String category, Bundle clusterActivityState)
+ throws RemoteException {
+ Log.i(TAG, "setClusterActivityState, category: " + category);
+ synchronized (mLock) {
+ mActivityStatesByCategory.put(category, clusterActivityState);
+ }
+
+ mHandler.sendMessage(mHandler.obtainMessage(EventHandler.MSG_ACTIVITY_STATE,
+ new Pair<>(category, clusterActivityState)));
+ }
+ }
+} \ No newline at end of file
diff --git a/car-lib/src/android/car/cluster/ClusterActivityState.java b/car-lib/src/android/car/cluster/ClusterActivityState.java
new file mode 100644
index 0000000000..9a6223c1b9
--- /dev/null
+++ b/car-lib/src/android/car/cluster/ClusterActivityState.java
@@ -0,0 +1,91 @@
+/*
+ * 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.car.cluster;
+
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Bundle;
+
+/**
+ * Helper class that represents activity state in the cluster and can be serialized / deserialized
+ * to/from bundle.
+ * @hide
+ */
+public class ClusterActivityState {
+ private static final String KEY_VISIBLE = "android.car:activityState.visible";
+ private static final String KEY_UNOBSCURED_BOUNDS = "android.car:activityState.unobscured";
+ private static final String KEY_EXTRAS = "android.car:activityState.extras";
+
+ private boolean mVisible = true;
+ private Rect mUnobscuredBounds;
+ private Bundle mExtras;
+
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ @Nullable public Rect getUnobscuredBounds() {
+ return mUnobscuredBounds;
+ }
+
+ public ClusterActivityState setVisible(boolean visible) {
+ mVisible = visible;
+ return this;
+ }
+
+ public ClusterActivityState setUnobscuredBounds(Rect unobscuredBounds) {
+ mUnobscuredBounds = unobscuredBounds;
+ return this;
+ }
+
+ public ClusterActivityState setExtras(Bundle bundle) {
+ mExtras = bundle;
+ return this;
+ }
+
+ /** Use factory methods instead. */
+ private ClusterActivityState() {}
+
+ public static ClusterActivityState create(boolean visible, Rect unobscuredBounds) {
+ return new ClusterActivityState()
+ .setVisible(visible)
+ .setUnobscuredBounds(unobscuredBounds);
+ }
+
+ public static ClusterActivityState fromBundle(Bundle bundle) {
+ return new ClusterActivityState()
+ .setVisible(bundle.getBoolean(KEY_VISIBLE, true))
+ .setUnobscuredBounds((Rect) bundle.getParcelable(KEY_UNOBSCURED_BOUNDS))
+ .setExtras(bundle.getBundle(KEY_EXTRAS));
+ }
+
+ public Bundle toBundle() {
+ Bundle b = new Bundle();
+ b.putBoolean(KEY_VISIBLE, mVisible);
+ b.putParcelable(KEY_UNOBSCURED_BOUNDS, mUnobscuredBounds);
+ b.putBundle(KEY_EXTRAS, mExtras);
+ return b;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + " {"
+ + "visible: " + mVisible + ", "
+ + "unobscuredBounds: " + mUnobscuredBounds
+ + " }";
+ }
+}
diff --git a/car-lib/src/android/car/cluster/IInstrumentClusterManagerCallback.aidl b/car-lib/src/android/car/cluster/IInstrumentClusterManagerCallback.aidl
new file mode 100644
index 0000000000..91a497d433
--- /dev/null
+++ b/car-lib/src/android/car/cluster/IInstrumentClusterManagerCallback.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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.car.cluster;
+
+import android.os.Bundle;
+
+/**
+ * Interface from Car Service to {@link android.car.cluster.CarInstrumentClusterManager}
+ * @hide
+ */
+interface IInstrumentClusterManagerCallback {
+ /**
+ * Notifies manager about changes in the cluster activity state.
+ *
+ * @param category cluster activity category to which this state applies,
+ * see {@link android.car.cluster.CarInstrumentClusterManager} for details.
+ * @param clusterActivityState is a {@link Bundle} object,
+ * see {@link android.car.cluster.ClusterActivityState} for how to construct the bundle.
+ * @hide
+ */
+ oneway void setClusterActivityState(String category, in Bundle clusterActivityState);
+}
diff --git a/car-lib/src/android/car/cluster/IInstrumentClusterManagerService.aidl b/car-lib/src/android/car/cluster/IInstrumentClusterManagerService.aidl
new file mode 100644
index 0000000000..aaaeaeec18
--- /dev/null
+++ b/car-lib/src/android/car/cluster/IInstrumentClusterManagerService.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.car.cluster;
+
+import android.content.Intent;
+
+import android.car.cluster.IInstrumentClusterManagerCallback;
+
+/**
+ * API to communicate between {@link CarInstrumentClusterManager} and Car Service.
+ *
+ * @hide
+ */
+interface IInstrumentClusterManagerService {
+ oneway void startClusterActivity(in Intent intent);
+
+ oneway void registerCallback(in IInstrumentClusterManagerCallback callback);
+ oneway void unregisterCallback(in IInstrumentClusterManagerCallback callback);
+}
diff --git a/car-lib/src/android/car/cluster/renderer/DisplayConfiguration.java b/car-lib/src/android/car/cluster/renderer/DisplayConfiguration.java
deleted file mode 100644
index 409c8174f7..0000000000
--- a/car-lib/src/android/car/cluster/renderer/DisplayConfiguration.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2016 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.car.cluster.renderer;
-
-import android.annotation.Nullable;
-import android.annotation.SystemApi;
-import android.graphics.Rect;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * TODO: need to properly define this class and make it immutable. bug: 32060601
- *
- * @hide
- */
-@SystemApi
-public class DisplayConfiguration implements Parcelable {
- private final Rect mPrimaryRegion;
-
- @Nullable
- private final Rect mSecondaryRegion;
-
- public static final Parcelable.Creator<DisplayConfiguration> CREATOR =
- new Parcelable.Creator<DisplayConfiguration>() {
-
- public DisplayConfiguration createFromParcel(Parcel in) {
- return new DisplayConfiguration(in);
- }
-
- public DisplayConfiguration[] newArray(int size) {
- return new DisplayConfiguration[size];
- }
- };
-
-
- public DisplayConfiguration(Parcel in) {
- mPrimaryRegion = in.readTypedObject(Rect.CREATOR);
- mSecondaryRegion = in.readTypedObject(Rect.CREATOR);
- }
-
- public DisplayConfiguration(Rect primaryRegion) {
- this(primaryRegion, null);
- }
-
- public DisplayConfiguration(Rect primaryRegion, @Nullable Rect secondaryRegion) {
- mPrimaryRegion = primaryRegion;
- mSecondaryRegion = secondaryRegion;
- }
-
-
- /** Region that will be fully visible in instrument cluster */
- public Rect getPrimaryRegion() {
- return mPrimaryRegion;
- }
-
- /**
- * The region that includes primary region + may include some additional space that might
- * be partially visible in the instrument cluster. It is useful to fade-out primary
- * rendering for example or adding background image.
- */
- @Nullable
- public Rect getSecondaryRegion() {
- return mSecondaryRegion;
- }
-
- /** Returns true if secondary region is strongly greater then primary region */
- public boolean hasSecondaryRegion() {
- return mSecondaryRegion != null
- && mSecondaryRegion.width() > mPrimaryRegion.width()
- && mSecondaryRegion.height() > mPrimaryRegion.height();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeTypedObject(mPrimaryRegion, 0);
- dest.writeTypedObject(mSecondaryRegion, 0);
- }
-}
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
index 9fb4b56587..3458975872 100644
--- a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
@@ -15,6 +15,7 @@
*/
package android.car.cluster.renderer;
+import android.car.cluster.renderer.IInstrumentClusterCallback;
import android.car.cluster.renderer.IInstrumentClusterNavigation;
import android.view.KeyEvent;
@@ -28,8 +29,8 @@ interface IInstrumentCluster {
IInstrumentClusterNavigation getNavigationService();
/** Supplies Instrument Cluster Renderer with current owner of Navigation app context */
- void setNavigationContextOwner(int uid, int pid);
+ oneway void setNavigationContextOwner(int uid, int pid);
/** Called when key event that was addressed to instrument cluster display has been received. */
- void onKeyEvent(in KeyEvent keyEvent);
+ oneway void onKeyEvent(in KeyEvent keyEvent);
}
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentClusterCallback.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterCallback.aidl
new file mode 100644
index 0000000000..996dc9e205
--- /dev/null
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterCallback.aidl
@@ -0,0 +1,54 @@
+/*
+ * 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.car.cluster.renderer;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+
+/**
+ * This interface defines the communication channel between the cluster vendor implementation and
+ * Car Service.
+ *
+ * @hide
+ */
+interface IInstrumentClusterCallback {
+ /**
+ * Notify Car Service how to launch an activity for particular category.
+ *
+ * @param category cluster activity category,
+ * see {@link android.car.cluster.CarInstrumentClusterManager} for details.
+ * @param activityOptions this bundle will be converted to {@link android.app.ActivityOptions}
+ * and used when starting an activity. It may contain information such as virtual display
+ * id or activity stack id where to start cluster activity.
+ *
+ * @hide
+ */
+ void setClusterActivityLaunchOptions(String category, in Bundle activityOptions);
+
+ /**
+ * Activities launched on virtual display will be in onPause state most of the time, so they
+ * can't really know whether they visible on the screen or not. We need to propagate this
+ * information along with unobscured bounds (and possible other info) from instrument cluster
+ * vendor implementation to activity.
+ *
+ * @param category cluster activity category to which this state applies,
+ * see {@link android.car.cluster.CarInstrumentClusterManager} for details.
+ * @param clusterActivityState is a {@link Bundle} object,
+ * see {@link android.car.cluster.ClusterActivityState} for how to construct the bundle.
+ * @hide
+ */
+ void setClusterActivityState(String category, in Bundle clusterActivityState);
+}
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index 068b1e5219..6afe6c4c1b 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -18,8 +18,10 @@ package android.car.cluster.renderer;
import android.annotation.CallSuper;
import android.annotation.MainThread;
import android.annotation.SystemApi;
+import android.app.ActivityOptions;
import android.app.Service;
import android.car.CarLibLog;
+import android.car.CarNotConnectedException;
import android.car.navigation.CarNavigationInstrumentCluster;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -33,6 +35,8 @@ import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
@@ -60,6 +64,14 @@ public abstract class InstrumentClusterRenderingService extends Service {
private RendererBinder mRendererBinder;
+ /** @hide */
+ public static final String EXTRA_KEY_CALLBACK_SERVICE =
+ "android.car.cluster.IInstrumentClusterCallback";
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private IInstrumentClusterCallback mCallback;
+
@Override
@CallSuper
public IBinder onBind(Intent intent) {
@@ -67,6 +79,15 @@ public abstract class InstrumentClusterRenderingService extends Service {
Log.d(TAG, "onBind, intent: " + intent);
}
+ if (intent.getExtras().containsKey(EXTRA_KEY_CALLBACK_SERVICE)) {
+ IBinder callbackBinder = intent.getExtras().getBinder(EXTRA_KEY_CALLBACK_SERVICE);
+ synchronized (mLock) {
+ mCallback = IInstrumentClusterCallback.Stub.asInterface(callbackBinder);
+ }
+ } else {
+ Log.w(TAG, "onBind, no callback in extra!");
+ }
+
if (mRendererBinder == null) {
mRendererBinder = new RendererBinder(getNavigationRenderer());
}
@@ -83,6 +104,55 @@ public abstract class InstrumentClusterRenderingService extends Service {
protected void onKeyEvent(KeyEvent keyEvent) {
}
+ /**
+ *
+ * Sets configuration for activities that should be launched directly in the instrument
+ * cluster.
+ *
+ * @param category category of cluster activity
+ * @param activityOptions contains information of how to start cluster activity (on what display
+ * or activity stack.
+ *
+ * @hide
+ */
+ public void setClusterActivityLaunchOptions(String category,
+ ActivityOptions activityOptions) throws CarNotConnectedException {
+ IInstrumentClusterCallback cb;
+ synchronized (mLock) {
+ cb = mCallback;
+ }
+ if (cb == null) throw new CarNotConnectedException();
+ try {
+ cb.setClusterActivityLaunchOptions(category, activityOptions.toBundle());
+ } catch (RemoteException e) {
+ throw new CarNotConnectedException(e);
+ }
+ }
+
+ /**
+ *
+ * @param category cluster activity category,
+ * see {@link android.car.cluster.CarInstrumentClusterManager}
+ * @param state pass information about activity state,
+ * see {@link android.car.cluster.ClusterActivityState}
+ * @return true if information was sent to Car Service
+ * @throws CarNotConnectedException
+ */
+ public void setClusterActivityState(String category, Bundle state)
+ throws CarNotConnectedException {
+ IInstrumentClusterCallback cb;
+ synchronized (mLock) {
+ cb = mCallback;
+ }
+ if (cb == null) throw new CarNotConnectedException();
+ try {
+ cb.setClusterActivityState(category, state);
+ } catch (RemoteException e) {
+ throw new CarNotConnectedException(e);
+ }
+ }
+
+
@Override
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
writer.println("**" + getClass().getSimpleName() + "**");
@@ -90,12 +160,19 @@ public abstract class InstrumentClusterRenderingService extends Service {
if (mRendererBinder != null) {
writer.println("navigation renderer: " + mRendererBinder.mNavigationRenderer);
String owner = "none";
- if (mRendererBinder.mNavContextOwner != null) {
- owner = "[uid: " + mRendererBinder.mNavContextOwner.first
- + ", pid: " + mRendererBinder.mNavContextOwner.second + "]";
+ synchronized (mLock) {
+ if (mRendererBinder.mNavContextOwner != null) {
+ owner = "[uid: " + mRendererBinder.mNavContextOwner.first
+ + ", pid: " + mRendererBinder.mNavContextOwner.second + "]";
+ }
}
writer.println("navigation focus owner: " + owner);
}
+ IInstrumentClusterCallback cb;
+ synchronized (mLock) {
+ cb = mCallback;
+ }
+ writer.println("callback: " + cb);
}
private class RendererBinder extends IInstrumentCluster.Stub {
@@ -103,8 +180,10 @@ public abstract class InstrumentClusterRenderingService extends Service {
private final NavigationRenderer mNavigationRenderer;
private final UiHandler mUiHandler;
- private volatile NavigationBinder mNavigationBinder;
- private volatile Pair<Integer, Integer> mNavContextOwner;
+ @GuardedBy("mLock")
+ private NavigationBinder mNavigationBinder;
+ @GuardedBy("mLock")
+ private Pair<Integer, Integer> mNavContextOwner;
RendererBinder(NavigationRenderer navigationRenderer) {
mNavigationRenderer = navigationRenderer;
@@ -113,21 +192,25 @@ public abstract class InstrumentClusterRenderingService extends Service {
@Override
public IInstrumentClusterNavigation getNavigationService() throws RemoteException {
- if (mNavigationBinder == null) {
- mNavigationBinder = new NavigationBinder(mNavigationRenderer);
- if (mNavContextOwner != null) {
- mNavigationBinder.setNavigationContextOwner(
- mNavContextOwner.first, mNavContextOwner.second);
+ synchronized (mLock) {
+ if (mNavigationBinder == null) {
+ mNavigationBinder = new NavigationBinder(mNavigationRenderer);
+ if (mNavContextOwner != null) {
+ mNavigationBinder.setNavigationContextOwner(
+ mNavContextOwner.first, mNavContextOwner.second);
+ }
}
+ return mNavigationBinder;
}
- return mNavigationBinder;
}
@Override
public void setNavigationContextOwner(int uid, int pid) throws RemoteException {
- mNavContextOwner = new Pair<>(uid, pid);
- if (mNavigationBinder != null) {
- mNavigationBinder.setNavigationContextOwner(uid, pid);
+ synchronized (mLock) {
+ mNavContextOwner = new Pair<>(uid, pid);
+ if (mNavigationBinder != null) {
+ mNavigationBinder.setNavigationContextOwner(uid, pid);
+ }
}
}
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index 5cfc5c2f68..c492210990 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -33,7 +33,9 @@ PRODUCT_PACKAGES += \
PRODUCT_PACKAGES += \
EmbeddedKitchenSinkApp \
VmsPublisherClientSample \
- VmsSubscriberClientSample
+ VmsSubscriberClientSample \
+ android.car.cluster.loggingrenderer \
+ DirectRenderingClusterSample \
PRODUCT_COPY_FILES := \
frameworks/av/media/libeffects/data/audio_effects.conf:system/etc/audio_effects.conf \
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 5fa6072af2..111f998e8e 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -143,6 +143,17 @@
android:label="@string/car_permission_label_bind_input_service"
android:description="@string/car_permission_desc_bind_input_service"/>
+ <permission
+ android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"
+ android:protectionLevel="system|signature"
+ android:label="@string/car_permission_car_display_in_cluster"
+ android:description="@string/car_permission_desc_car_display_in_cluster" />
+
+ <permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"
+ android:protectionLevel="system|signature"
+ android:label="@string/car_permission_car_cluster_control"
+ android:description="@string/car_permission_desc_car_cluster_control" />
+
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.DEVICE_POWER" />
<uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS" />
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index fd4a8414c9..61cef594a7 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -58,7 +58,7 @@
<string name="inputService">android.car.input.service/.DefaultInputService</string>
- <string name="instrumentClusterRendererService">android.car.cluster.loggingrenderer/.LoggingClusterRenderingService</string>
+ <string name="instrumentClusterRendererService">android.car.cluster.sample/.SampleClusterServiceImpl</string>
<!-- Whether to enable Avtivity blocking for safety. When Activity blocking is enabled,
only whitelisted safe Activities will be allowed while car is not parked. -->
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index e17b53367d..6cfff9a8bd 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -81,6 +81,11 @@
cluster [CHAR LIMIT=NONE] -->
<string name="car_permission_desc_car_navigation_manager">Report navigation data to instrument
cluster</string>
+ <string name="car_permission_car_display_in_cluster">Direct rendering to instrument cluster</string>
+ <string name="car_permission_desc_car_display_in_cluster">Allow an application to declare
+ activities to be displayed in the instrument cluster</string>
+ <string name="car_permission_car_cluster_control">Instrument cluster control</string>
+ <string name="car_permission_desc_car_cluster_control">Launch apps in the instrument cluster</string>
<string name="car_permission_label_bind_instrument_cluster_rendering">Instrument Cluster Rendering</string>
<string name="car_permission_desc_bind_instrument_cluster_rendering">Receive instrument cluster data</string>
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 8da9bfb692..f58302d3e4 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -24,13 +24,10 @@ import android.car.cluster.renderer.IInstrumentClusterNavigation;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.automotive.vehicle.V2_0.IVehicle;
-import android.hardware.automotive.vehicle.V2_0.VehicleAreaDoor;
-import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.util.Log;
-
import com.android.car.cluster.InstrumentClusterService;
import com.android.car.hal.VehicleHal;
import com.android.car.internal.FeatureConfiguration;
@@ -38,7 +35,6 @@ import com.android.car.internal.FeatureUtil;
import com.android.car.pm.CarPackageManagerService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.car.ICarServiceHelper;
-
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -216,6 +212,9 @@ public class ICarImpl extends ICar.Stub {
IInstrumentClusterNavigation navService =
mInstrumentClusterService.getNavigationService();
return navService == null ? null : navService.asBinder();
+ case Car.CAR_INSTRUMENT_CLUSTER_SERVICE:
+ assertClusterManagerPermission(mContext);
+ return mInstrumentClusterService.getManagerService();
case Car.PROJECTION_SERVICE:
assertProjectionPermission(mContext);
return mCarProjectionService;
@@ -273,6 +272,10 @@ public class ICarImpl extends ICar.Stub {
assertPermission(context, Car.PERMISSION_CAR_NAVIGATION_MANAGER);
}
+ public static void assertClusterManagerPermission(Context context) {
+ assertPermission(context, Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
+ }
+
public static void assertHvacPermission(Context context) {
assertPermission(context, Car.PERMISSION_CAR_HVAC);
}
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index 8e0507bcbb..eef177c420 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -15,19 +15,34 @@
*/
package com.android.car.cluster;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.car.Car;
import android.car.CarAppFocusManager;
+import android.car.cluster.CarInstrumentClusterManager;
+import android.car.cluster.IInstrumentClusterManagerCallback;
+import android.car.cluster.IInstrumentClusterManagerService;
import android.car.cluster.renderer.IInstrumentCluster;
+import android.car.cluster.renderer.IInstrumentClusterCallback;
import android.car.cluster.renderer.IInstrumentClusterNavigation;
+import android.car.cluster.renderer.InstrumentClusterRenderingService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.Bundle;
import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.Process;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import android.view.KeyEvent;
import com.android.car.AppFocusService;
@@ -40,6 +55,11 @@ import com.android.car.R;
import com.android.internal.annotations.GuardedBy;
import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* Service responsible for interaction with car's instrument cluster.
@@ -54,14 +74,23 @@ public class InstrumentClusterService implements CarServiceBase,
private static final Boolean DBG = false;
private final Context mContext;
+
private final AppFocusService mAppFocusService;
private final CarInputService mCarInputService;
+ private final PackageManager mPackageManager;
private final Object mSync = new Object();
+ private final ClusterServiceCallback mClusterCallback = new ClusterServiceCallback();
+ private final ClusterManagerService mClusterManagerService = new ClusterManagerService();
+
@GuardedBy("mSync")
private ContextOwner mNavContextOwner;
@GuardedBy("mSync")
private IInstrumentCluster mRendererService;
+ @GuardedBy("mSync")
+ private final HashMap<String, ClusterActivityInfo> mActivityInfoByCategory = new HashMap<>();
+ @GuardedBy("mSync")
+ private final HashMap<IBinder, ManagerCallbackInfo> mManagerCallbacks = new HashMap<>();
private boolean mRendererBound = false;
@@ -85,7 +114,9 @@ public class InstrumentClusterService implements CarServiceBase,
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "onServiceDisconnected, name: " + name);
- mRendererService = null;
+ synchronized (mSync) {
+ mRendererService = null;
+ }
// Try to rebind with instrument cluster.
mRendererBound = bindInstrumentClusterRendererService();
}
@@ -96,6 +127,7 @@ public class InstrumentClusterService implements CarServiceBase,
mContext = context;
mAppFocusService = appFocusService;
mCarInputService = carInputService;
+ mPackageManager = mContext.getPackageManager();
}
@Override
@@ -188,6 +220,11 @@ public class InstrumentClusterService implements CarServiceBase,
Intent intent = new Intent();
intent.setComponent(ComponentName.unflattenFromString(rendererService));
+ Bundle extras = new Bundle();
+ extras.putBinder(
+ InstrumentClusterRenderingService.EXTRA_KEY_CALLBACK_SERVICE,
+ mClusterCallback);
+ intent.putExtras(extras);
return mContext.bindService(intent, mRendererServiceConnection, Context.BIND_AUTO_CREATE);
}
@@ -206,6 +243,10 @@ public class InstrumentClusterService implements CarServiceBase,
}
}
+ public IInstrumentClusterManagerService.Stub getManagerService() {
+ return mClusterManagerService;
+ }
+
@Override
public boolean onKeyEvent(KeyEvent event) {
if (DBG) {
@@ -236,4 +277,247 @@ public class InstrumentClusterService implements CarServiceBase,
this.pid = pid;
}
}
+
+ private static class ClusterActivityInfo {
+ Bundle launchOptions; // ActivityOptions
+ Bundle state; // ClusterActivityState
+ }
+
+ private void enforcePermission(String permission) {
+ int callingUid = Binder.getCallingUid();
+ int callingPid = Binder.getCallingPid();
+ if (Binder.getCallingUid() == Process.myUid()) {
+ if (mContext.checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) {
+ throw new SecurityException("Permission " + permission + " is not granted to "
+ + "client {uid: " + callingUid + ", pid: " + callingPid + "}");
+ }
+ }
+ }
+
+ private void enforceClusterControlPermission() {
+ enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
+ }
+
+ private void doStartClusterActivity(Intent intent) {
+ enforceClusterControlPermission();
+
+ // Category from given intent should match category from cluster vendor implementation.
+ List<ResolveInfo> resolveList = mPackageManager.queryIntentActivities(intent,
+ PackageManager.GET_RESOLVED_FILTER);
+ if (resolveList == null || resolveList.isEmpty()) {
+ Log.w(TAG, "Failed to resolve an intent: " + intent);
+ return;
+ }
+
+ resolveList = checkPermission(resolveList, Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER);
+ if (resolveList.isEmpty()) {
+ return;
+ }
+
+ // TODO(b/63861009): we may have multiple navigation apps that eligible to be launched in
+ // the cluster. We need to resolve intent that may have multiple activity candidates, right
+ // now we pickup the first one that matches registered category (resolveList is sorted
+ // priority).
+ Pair<ResolveInfo, ClusterActivityInfo> attributedResolveInfo =
+ findClusterActivityOptions(resolveList);
+ if (attributedResolveInfo == null) {
+ Log.w(TAG, "Unable to start an activity with intent: " + intent + " in the cluster: "
+ + "category intent didn't match with any categories from vendor "
+ + "implementation");
+ return;
+ }
+ ClusterActivityInfo opts = attributedResolveInfo.second;
+
+ // Intent was already checked for permission and resolved, make it explicit.
+ intent.setComponent(attributedResolveInfo.first.getComponentInfo().getComponentName());
+
+ intent.putExtra(CarInstrumentClusterManager.KEY_EXTRA_ACTIVITY_STATE, opts.state);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Virtual display could be private and not available to calling process.
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mContext.startActivity(intent, opts.launchOptions);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private List<ResolveInfo> checkPermission(List<ResolveInfo> resolveList,
+ String permission) {
+ List<ResolveInfo> permittedResolveList = new ArrayList<>(resolveList.size());
+ for (ResolveInfo info : resolveList) {
+ String pkgName = info.getComponentInfo().packageName;
+ if (mPackageManager.checkPermission(permission, pkgName) == PERMISSION_GRANTED) {
+ permittedResolveList.add(info);
+ } else {
+ Log.w(TAG, "Permission " + permission + " not granted for "
+ + info.getComponentInfo());
+ }
+
+ }
+ return permittedResolveList;
+ }
+
+ private void doRegisterManagerCallback(IInstrumentClusterManagerCallback callback)
+ throws RemoteException {
+ enforceClusterControlPermission();
+ IBinder binder = callback.asBinder();
+
+ List<Pair<String, Bundle>> knownActivityStates = null;
+ ManagerCallbackDeathRecipient deathRecipient = new ManagerCallbackDeathRecipient(binder);
+ synchronized (mSync) {
+ if (mManagerCallbacks.containsKey(binder)) {
+ Log.w(TAG, "Manager callback already registered for binder: " + binder);
+ return;
+ }
+ mManagerCallbacks.put(binder, new ManagerCallbackInfo(callback, deathRecipient));
+ if (!mActivityInfoByCategory.isEmpty()) {
+ knownActivityStates = new ArrayList<>(mActivityInfoByCategory.size());
+ for (Map.Entry<String, ClusterActivityInfo> it : mActivityInfoByCategory.entrySet()) {
+ knownActivityStates.add(new Pair<>(it.getKey(), it.getValue().state));
+ }
+ }
+ }
+ binder.linkToDeath(deathRecipient, 0);
+
+ // Notify manager immediately with known states.
+ if (knownActivityStates != null) {
+ for (Pair<String, Bundle> it : knownActivityStates) {
+ callback.setClusterActivityState(it.first, it.second);
+ }
+ }
+ }
+
+ private void doUnregisterManagerCallback(IBinder binder) throws RemoteException {
+ enforceClusterControlPermission();
+ ManagerCallbackInfo info;
+ synchronized (mSync) {
+ info = mManagerCallbacks.get(binder);
+ if (info == null) {
+ Log.w(TAG, "Unable to unregister manager callback binder: " + binder + " because "
+ + "it wasn't previously registered.");
+ return;
+ }
+ mManagerCallbacks.remove(binder);
+ }
+ binder.unlinkToDeath(info.deathRecipient, 0);
+ }
+
+ @Nullable
+ private Pair<ResolveInfo, ClusterActivityInfo> findClusterActivityOptions(
+ List<ResolveInfo> resolveList) {
+ synchronized (mSync) {
+ Set<String> registeredCategories = mActivityInfoByCategory.keySet();
+
+ for (ResolveInfo resolveInfo : resolveList) {
+ for (String category : registeredCategories) {
+ if (resolveInfo.filter != null && resolveInfo.filter.hasCategory(category)) {
+ ClusterActivityInfo categoryInfo = mActivityInfoByCategory.get(category);
+ return new Pair<>(resolveInfo, categoryInfo);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private class ManagerCallbackDeathRecipient implements DeathRecipient {
+ private final IBinder mBinder;
+
+ ManagerCallbackDeathRecipient(IBinder binder) {
+ mBinder = binder;
+ }
+
+ @Override
+ public void binderDied() {
+ try {
+ doUnregisterManagerCallback(mBinder);
+ } catch (RemoteException e) {
+ // Ignore, shutdown route.
+ }
+ }
+ }
+
+ private class ClusterManagerService extends IInstrumentClusterManagerService.Stub {
+
+ @Override
+ public void startClusterActivity(Intent intent) throws RemoteException {
+ doStartClusterActivity(intent);
+ }
+
+ @Override
+ public void registerCallback(IInstrumentClusterManagerCallback callback)
+ throws RemoteException {
+ doRegisterManagerCallback(callback);
+ }
+
+ @Override
+ public void unregisterCallback(IInstrumentClusterManagerCallback callback)
+ throws RemoteException {
+ doUnregisterManagerCallback(callback.asBinder());
+ }
+ }
+
+ private ClusterActivityInfo getOrCreateActivityInfoLocked(String category) {
+ return mActivityInfoByCategory.computeIfAbsent(category, k -> new ClusterActivityInfo());
+ }
+
+ /** This is communication channel from vendor cluster implementation to Car Service. */
+ private class ClusterServiceCallback extends IInstrumentClusterCallback.Stub {
+
+ @Override
+ public void setClusterActivityLaunchOptions(String category, Bundle activityOptions)
+ throws RemoteException {
+ doSetActivityLaunchOptions(category, activityOptions);
+ }
+
+ @Override
+ public void setClusterActivityState(String category, Bundle clusterActivityState)
+ throws RemoteException {
+ doSetClusterActivityState(category, clusterActivityState);
+ }
+ }
+
+ /** Called from cluster vendor implementation */
+ private void doSetActivityLaunchOptions(String category, Bundle activityOptions) {
+ if (DBG) {
+ Log.d(TAG, "doSetActivityLaunchOptions, category: " + category
+ + ", options: " + activityOptions);
+ }
+ synchronized (mSync) {
+ ClusterActivityInfo info = getOrCreateActivityInfoLocked(category);
+ info.launchOptions = activityOptions;
+ }
+ }
+
+ /** Called from cluster vendor implementation */
+ private void doSetClusterActivityState(String category, Bundle clusterActivityState)
+ throws RemoteException {
+ if (DBG) {
+ Log.d(TAG, "doSetClusterActivityState, category: " + category
+ + ", state: " + clusterActivityState);
+ }
+
+ List<ManagerCallbackInfo> managerCallbacks;
+ synchronized (mSync) {
+ ClusterActivityInfo info = getOrCreateActivityInfoLocked(category);
+ info.state = clusterActivityState;
+ managerCallbacks = new ArrayList<>(mManagerCallbacks.values());
+ }
+
+ for (ManagerCallbackInfo cbInfo : managerCallbacks) {
+ cbInfo.callback.setClusterActivityState(category, clusterActivityState);
+ }
+ }
+
+ private static class ManagerCallbackInfo {
+ final IInstrumentClusterManagerCallback callback;
+ final ManagerCallbackDeathRecipient deathRecipient;
+
+ ManagerCallbackInfo(IInstrumentClusterManagerCallback callback,
+ ManagerCallbackDeathRecipient deathRecipient) {
+ this.callback = callback;
+ this.deathRecipient = deathRecipient;
+ }
+ }
}
diff --git a/tests/DirectRenderingClusterSample/Android.mk b/tests/DirectRenderingClusterSample/Android.mk
new file mode 100644
index 0000000000..7fb6e8e269
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2015 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_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := DirectRenderingClusterSample
+
+# Each update should be signed by OEMs
+LOCAL_CERTIFICATE := platform
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_JAVA_LIBRARIES += android.car
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4
+
+include $(BUILD_PACKAGE)
diff --git a/tests/DirectRenderingClusterSample/AndroidManifest.xml b/tests/DirectRenderingClusterSample/AndroidManifest.xml
new file mode 100644
index 0000000000..fc41ef0365
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/AndroidManifest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="android.car.cluster.sample"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk android:targetSdkVersion="25" android:minSdkVersion="25"/>
+
+ <!-- We set TYPE_SYSTEM_ALERT window flag to presentation in order
+ to show it outside of activity context -->
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+ <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
+ tools:ignore="ProtectedPermissions"/>
+ <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS"/>
+ <uses-permission android:name="android.permission.INJECT_EVENTS"/>
+
+ <application android:label="@string/app_name"
+ android:icon="@mipmap/ic_launcher"
+ android:directBootAware="true"
+ android:persistent="true">
+ <service android:name=".SampleClusterServiceImpl"
+ android:exported="false"
+ android:permission="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"/>
+
+ <activity android:name=".MainClusterActivity"
+ android:exported="false"
+ android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
+ </application>
+</manifest>
diff --git a/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_car_info.png b/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_car_info.png
new file mode 100644
index 0000000000..adb07d64b7
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_car_info.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_music.png b/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_music.png
new file mode 100644
index 0000000000..f1b2533be7
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_music.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_nav.png b/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_nav.png
new file mode 100644
index 0000000000..8036b895a5
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_nav.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_phone.png b/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_phone.png
new file mode 100644
index 0000000000..2daaa8eb82
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-hdpi/ic_phone.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_car_info.png b/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_car_info.png
new file mode 100644
index 0000000000..9c45d3e0ca
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_car_info.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_music.png b/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_music.png
new file mode 100644
index 0000000000..fb0671af42
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_music.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_nav.png b/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_nav.png
new file mode 100644
index 0000000000..4844c89b08
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_nav.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_phone.png b/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_phone.png
new file mode 100644
index 0000000000..ef50db6fa4
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-mdpi/ic_phone.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_car_info.png b/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_car_info.png
new file mode 100644
index 0000000000..2db56450b8
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_car_info.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_music.png b/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_music.png
new file mode 100644
index 0000000000..6154079eee
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_music.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_nav.png b/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_nav.png
new file mode 100644
index 0000000000..f94db1e83f
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_nav.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_phone.png b/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_phone.png
new file mode 100644
index 0000000000..ca0be39a9d
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-xhdpi/ic_phone.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_car_info.png b/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_car_info.png
new file mode 100644
index 0000000000..f00be305f0
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_car_info.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_music.png b/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_music.png
new file mode 100644
index 0000000000..8a875ad403
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_music.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_nav.png b/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_nav.png
new file mode 100644
index 0000000000..7b2b514525
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_nav.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_phone.png b/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_phone.png
new file mode 100644
index 0000000000..fce6fb7b10
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable-xxhdpi/ic_phone.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable/btn_car_info.xml b/tests/DirectRenderingClusterSample/res/drawable/btn_car_info.xml
new file mode 100644
index 0000000000..437ad41f4f
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/btn_car_info.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" >
+ <layer-list>
+ <item android:drawable="@drawable/ic_car_info"/>
+ <item android:drawable="@drawable/focused_button_shape"/>
+ </layer-list>
+ </item>
+ <item android:drawable="@drawable/ic_car_info"/>
+</selector> \ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/drawable/btn_music.xml b/tests/DirectRenderingClusterSample/res/drawable/btn_music.xml
new file mode 100644
index 0000000000..65e01b685b
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/btn_music.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" >
+ <layer-list>
+ <item android:drawable="@drawable/ic_music"/>
+ <item android:drawable="@drawable/focused_button_shape"/>
+ </layer-list>
+ </item>
+ <item android:drawable="@drawable/ic_music"/>
+</selector> \ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/drawable/btn_nav.xml b/tests/DirectRenderingClusterSample/res/drawable/btn_nav.xml
new file mode 100644
index 0000000000..9f9c6bcc88
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/btn_nav.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" >
+ <layer-list>
+ <item android:drawable="@drawable/ic_nav"/>
+ <item android:drawable="@drawable/focused_button_shape"/>
+ </layer-list>
+ </item>
+ <item android:drawable="@drawable/ic_nav"/>
+</selector> \ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/drawable/btn_phone.xml b/tests/DirectRenderingClusterSample/res/drawable/btn_phone.xml
new file mode 100644
index 0000000000..2a6e249f1d
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/btn_phone.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" >
+ <layer-list>
+ <item android:drawable="@drawable/ic_phone"/>
+ <item android:drawable="@drawable/focused_button_shape"/>
+ </layer-list>
+ </item>
+ <item android:drawable="@drawable/ic_phone"/>
+</selector> \ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/drawable/car_top_view.png b/tests/DirectRenderingClusterSample/res/drawable/car_top_view.png
new file mode 100644
index 0000000000..b19ee1222c
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/car_top_view.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/drawable/focused_button_shape.xml b/tests/DirectRenderingClusterSample/res/drawable/focused_button_shape.xml
new file mode 100644
index 0000000000..b84ef30a75
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/focused_button_shape.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <stroke
+ android:width="2dp"
+ android:color="#333333" />
+ <corners
+ android:radius="7dp" />
+ <padding
+ android:left="5dp"
+ android:top="5dp"
+ android:right="5dp"
+ android:bottom="5dp" />
+</shape> \ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/layout/activity_main.xml b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
new file mode 100644
index 0000000000..7e1ef9f712
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/activity_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/background_dark"
+ tools:context=".MainClusterActivity"
+ android:windowIsFloating="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <android.support.v4.view.ViewPager
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pager"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ />
+
+ <LinearLayout
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/btn_nav"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:background="@drawable/btn_nav"
+ android:layout_margin="10dp"
+ android:focusableInTouchMode="true" />
+ <Button
+ android:id="@+id/btn_phone"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_margin="10dp"
+ android:background="@drawable/btn_phone"
+ android:focusableInTouchMode="true" />
+ <Button
+ android:id="@+id/btn_music"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_margin="10dp"
+ android:background="@drawable/btn_music"
+ android:focusableInTouchMode="true" />
+ <Button
+ android:id="@+id/btn_car_info"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_margin="10dp"
+ android:background="@drawable/btn_car_info"
+ android:focusableInTouchMode="true" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/text_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:background="@android:color/background_light"
+ android:textSize="30sp"
+ />
+</RelativeLayout>
diff --git a/tests/DirectRenderingClusterSample/res/layout/fragment_car_info.xml b/tests/DirectRenderingClusterSample/res/layout/fragment_car_info.xml
new file mode 100644
index 0000000000..3a1f5ab8f2
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/layout/fragment_car_info.xml
@@ -0,0 +1,46 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="com.google.experiments.client.pavelm.fakeclusterux.CarInfoFragment">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="32 psi"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginLeft="-80dp"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="33 psi"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginLeft="80dp"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="33 psi"
+ android:layout_gravity="center_horizontal|bottom"
+ android:layout_marginLeft="80dp"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="31 psi"
+ android:layout_gravity="center_horizontal|bottom"
+ android:layout_marginLeft="-80dp"
+ />
+
+ <ImageView
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:src="@drawable/car_top_view"
+ android:padding="30dp"
+ android:scaleType="fitCenter" />
+
+</FrameLayout>
diff --git a/tests/DirectRenderingClusterSample/res/layout/fragment_music.xml b/tests/DirectRenderingClusterSample/res/layout/fragment_music.xml
new file mode 100644
index 0000000000..1a11b3fb69
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/layout/fragment_music.xml
@@ -0,0 +1,14 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="com.google.experiments.client.pavelm.fakeclusterux.MusicFragment"
+ android:background="@color/colorPrimaryDark">
+
+ <!-- TODO: Update blank fragment layout -->
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/hello_blank_fragment" />
+
+</FrameLayout>
diff --git a/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml b/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
new file mode 100644
index 0000000000..c0fb4b302d
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
@@ -0,0 +1,22 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/darkBlue"
+ tools:context=".NavigationFragment">
+
+ <FrameLayout
+ android:id="@+id/nav_frame_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <SurfaceView
+ android:id="@+id/nav_surface"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="20dp"/>
+
+ </FrameLayout>
+
+
+</FrameLayout>
diff --git a/tests/DirectRenderingClusterSample/res/layout/fragment_phone.xml b/tests/DirectRenderingClusterSample/res/layout/fragment_phone.xml
new file mode 100644
index 0000000000..42fe24ad72
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/layout/fragment_phone.xml
@@ -0,0 +1,13 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="com.google.experiments.client.pavelm.fakeclusterux.PhoneFragment">
+
+ <!-- TODO: Update blank fragment layout -->
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/hello_blank_fragment" />
+
+</FrameLayout>
diff --git a/tests/DirectRenderingClusterSample/res/mipmap-hdpi/ic_launcher.png b/tests/DirectRenderingClusterSample/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..cde69bccce
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/mipmap-mdpi/ic_launcher.png b/tests/DirectRenderingClusterSample/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..c133a0cbd3
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/mipmap-xhdpi/ic_launcher.png b/tests/DirectRenderingClusterSample/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..bfa42f0e7b
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/mipmap-xxhdpi/ic_launcher.png b/tests/DirectRenderingClusterSample/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..324e72cdd7
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/mipmap-xxxhdpi/ic_launcher.png b/tests/DirectRenderingClusterSample/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..aee44e1384
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DirectRenderingClusterSample/res/values-w820dp/dimens.xml b/tests/DirectRenderingClusterSample/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000000..63fc816444
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+ <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/tests/DirectRenderingClusterSample/res/values/colors.xml b/tests/DirectRenderingClusterSample/res/values/colors.xml
new file mode 100644
index 0000000000..a71c0d5127
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/values/colors.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+ <color name="darkBlue">#2b2b77</color>
+</resources>
diff --git a/tests/DirectRenderingClusterSample/res/values/dimens.xml b/tests/DirectRenderingClusterSample/res/values/dimens.xml
new file mode 100644
index 0000000000..47c8224673
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/tests/DirectRenderingClusterSample/res/values/strings.xml b/tests/DirectRenderingClusterSample/res/values/strings.xml
new file mode 100644
index 0000000000..778ecf6640
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/values/strings.xml
@@ -0,0 +1,6 @@
+<resources>
+ <string name="app_name">Fake Cluster Implementation</string>
+
+ <!-- TODO: Remove or change this placeholder text -->
+ <string name="hello_blank_fragment">Hello blank fragment</string>
+</resources>
diff --git a/tests/DirectRenderingClusterSample/res/values/styles.xml b/tests/DirectRenderingClusterSample/res/values/styles.xml
new file mode 100644
index 0000000000..f11f7450a8
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/values/styles.xml
@@ -0,0 +1,3 @@
+<resources>
+
+</resources>
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CarInfoFragment.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CarInfoFragment.java
new file mode 100644
index 0000000000..d1e7112645
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CarInfoFragment.java
@@ -0,0 +1,44 @@
+/*
+ * 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.car.cluster.sample;
+
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+
+/**
+ * A simple {@link Fragment} subclass.
+ */
+public class CarInfoFragment extends Fragment {
+
+
+ public CarInfoFragment() {
+ // Required empty public constructor
+ }
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_car_info, container, false);
+ }
+
+}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
new file mode 100644
index 0000000000..1c3f7af9d5
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
@@ -0,0 +1,221 @@
+/*
+ * 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.car.cluster.sample;
+
+import static android.car.cluster.sample.SampleClusterServiceImpl.LOCAL_BINDING_ACTION;
+
+import android.car.cluster.sample.SampleClusterServiceImpl.Listener;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+
+public class MainClusterActivity extends FragmentActivity
+ implements Listener {
+ private static final String TAG = MainClusterActivity.class.getSimpleName();
+
+ private Button mNavButton;
+ private Button mPhoneButton;
+ private Button mCarInfoButton;
+ private Button mMusicButton;
+ private TextView mTextOverlay;
+ private ViewPager mPager;
+
+ private SampleClusterServiceImpl mService;
+
+ private final Handler mHandler = new Handler();
+
+ private HashMap<Button, Facet<?>> mButtonToFacet = new HashMap<>();
+ private SparseArray<Facet<?>> mOrderToFacet = new SparseArray<>();
+
+ private final View.OnFocusChangeListener mFacetButtonFocusListener =
+ new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ mPager.setCurrentItem(mButtonToFacet.get(v).order);
+ }
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ Intent intent = new Intent(this, SampleClusterServiceImpl.class);
+ intent.setAction(LOCAL_BINDING_ACTION);
+ bindService(intent,
+ new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service);
+ mService = ((SampleClusterServiceImpl.LocalBinder) service)
+ .getService();
+ mService.registerListener(MainClusterActivity.this);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.i(TAG, "onServiceDisconnected, name: " + name);
+ mService = null;
+ }
+ }, BIND_AUTO_CREATE);
+
+ mNavButton = findViewById(R.id.btn_nav);
+ mPhoneButton = findViewById(R.id.btn_phone);
+ mCarInfoButton = findViewById(R.id.btn_car_info);
+ mMusicButton = findViewById(R.id.btn_music);
+ mTextOverlay = findViewById(R.id.text_overlay);
+
+ registerFacets(
+ new Facet<>(mNavButton, 0, NavigationFragment.class),
+ new Facet<>(mPhoneButton, 1, PhoneFragment.class),
+ new Facet<>(mMusicButton, 2, MusicFragment.class),
+ new Facet<>(mCarInfoButton, 3, CarInfoFragment.class));
+
+ mPager = (ViewPager) findViewById(R.id.pager);
+ mPager.setAdapter(new ClusterPageAdapter(getSupportFragmentManager()));
+
+ mNavButton.requestFocus();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mService != null) {
+ mService.unregisterListener();
+ }
+ }
+
+ @Override
+ public void onShowToast(String text) {
+ if (mTextOverlay.getVisibility() == View.VISIBLE) {
+ if (!TextUtils.isEmpty(mTextOverlay.getText())) {
+ mTextOverlay.setText(mTextOverlay.getText() + "\n" + text);
+ } else {
+ mTextOverlay.setText(text);
+ }
+ }
+
+ mTextOverlay.setVisibility(View.VISIBLE);
+
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.postDelayed(() -> {
+ mTextOverlay.setVisibility(View.GONE);
+ mTextOverlay.setText("");
+ }, 3000);
+ }
+
+ @Override
+ public void onKeyEvent(KeyEvent event) {
+ Log.i(TAG, "onKeyEvent, event: " + event);
+ dispatchKeyEvent(event); // TODO: dispatch event doesn't work for some reason.
+
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ int nextItem = (mPager.getCurrentItem() + 1) % mButtonToFacet.size();
+ mOrderToFacet.get(nextItem).button.requestFocus();
+ } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) {
+ int nextItem = (mPager.getCurrentItem() - 1);
+ if (nextItem < 0) nextItem = mButtonToFacet.size() - 1;
+ mOrderToFacet.get(nextItem).button.requestFocus();
+ }
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ boolean consumed = super.dispatchKeyEvent(event);
+ Log.i(TAG, "dispatchKeyEvent, event: " + event + ", consumed: " + consumed);
+ return consumed;
+ }
+
+ public class ClusterPageAdapter extends FragmentPagerAdapter {
+ public ClusterPageAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public int getCount() {
+ return mButtonToFacet.size();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ return mOrderToFacet.get(position).getOrCreateFragment();
+ }
+ }
+
+ private void registerFacets(Facet<?>... facets) {
+ for (Facet<?> f : facets) {
+ registerFacet(f);
+ }
+ }
+
+ private <T> void registerFacet(Facet<T> facet) {
+ mOrderToFacet.append(facet.order, facet);
+ mButtonToFacet.put(facet.button, facet);
+
+ facet.button.setOnFocusChangeListener(mFacetButtonFocusListener);
+ }
+
+ private static class Facet<T> {
+ Button button;
+ Class<T> clazz;
+ int order;
+
+ Facet(Button button, int order, Class<T> clazz) {
+ this.button = button;
+ this.order = order;
+ this.clazz = clazz;
+ }
+
+ private Fragment mFragment;
+
+ Fragment getOrCreateFragment() {
+ if (mFragment == null) {
+ try {
+ mFragment = (Fragment) clazz.getConstructors()[0].newInstance();
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return mFragment;
+ }
+ }
+
+ SampleClusterServiceImpl getService() {
+ return mService;
+ }
+}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MusicFragment.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MusicFragment.java
new file mode 100644
index 0000000000..2c8206544b
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MusicFragment.java
@@ -0,0 +1,39 @@
+/*
+ * 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.car.cluster.sample;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A simple {@link Fragment} subclass.
+ */
+public class MusicFragment extends Fragment {
+
+ public MusicFragment() {
+ // Required empty public constructor
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_music, container, false);
+ }
+}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
new file mode 100644
index 0000000000..0ffac1ee94
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
@@ -0,0 +1,163 @@
+/*
+ * 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.car.cluster.sample;
+
+import static android.car.cluster.CarInstrumentClusterManager.CATEGORY_NAVIGATION;
+
+import android.app.ActivityOptions;
+import android.car.CarNotConnectedException;
+import android.car.cluster.ClusterActivityState;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.hardware.display.VirtualDisplay;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceHolder.Callback;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class NavigationFragment extends Fragment {
+ private final static String TAG = "Cluster.NavigationFragment";
+
+ private SurfaceView mSurfaceView;
+ private DisplayManager mDisplayManager;
+ private Rect mUnobscuredBounds;
+
+ // Static because we want to keep alive this virtual display when navigating through
+ // ViewPager (this fragment gets dynamically destroyed and created)
+ private static VirtualDisplay mVirtualDisplay;
+ private static int mRegisteredNavDisplayId = Display.INVALID_DISPLAY;
+
+ public NavigationFragment() {
+ // Required empty public constructor
+ }
+
+ private final DisplayListener mDisplayListener = new DisplayListener() {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ int navDisplayId = getVirtualDisplayId();
+ Log.i(TAG, "onDisplayAdded, displayId: " + displayId
+ + ", navigation display id: " + navDisplayId);
+
+ if (navDisplayId == displayId) {
+ try {
+ getService().setClusterActivityLaunchOptions(
+ CATEGORY_NAVIGATION,
+ ActivityOptions.makeBasic()
+ .setLaunchDisplayId(displayId));
+ mRegisteredNavDisplayId = displayId;
+
+ getService().setClusterActivityState(
+ CATEGORY_NAVIGATION,
+ ClusterActivityState.create(true, mUnobscuredBounds).toBundle());
+ } catch (CarNotConnectedException e) {
+ throw new IllegalStateException(
+ "Failed to report nav activity cluster launch options", e);
+ }
+ }
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ if (mRegisteredNavDisplayId == displayId) {
+ try {
+ mRegisteredNavDisplayId = Display.INVALID_DISPLAY;
+ getService().setClusterActivityLaunchOptions(
+ CATEGORY_NAVIGATION, null);
+ } catch (CarNotConnectedException e) {
+ // This can happen only during shutdown, ignore.
+ }
+ }
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {}
+ };
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ Log.i(TAG, "onCreateView");
+ mDisplayManager = getActivity().getSystemService(DisplayManager.class);
+ mDisplayManager.registerDisplayListener(mDisplayListener, new Handler());
+
+ // Inflate the layout for this fragment
+ View root = inflater.inflate(R.layout.fragment_navigation, container, false);
+
+ mSurfaceView = root.findViewById(R.id.nav_surface);
+ mSurfaceView.getHolder().addCallback(new Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ Log.i(TAG, "surfaceCreated, holder: " + holder);
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Log.i(TAG, "surfaceChanged, holder: " + holder + ", size:" + width + "x" + height
+ + ", format:" + format);
+
+ //Create dummy unobscured area to report to navigation activity.
+ mUnobscuredBounds = new Rect(40, 0, width - 80, height - 40);
+
+ if (mVirtualDisplay == null) {
+ mVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height);
+ } else {
+ mVirtualDisplay.setSurface(holder.getSurface());
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ Log.i(TAG, "surfaceDestroyed, holder: " + holder + ", detaching surface from"
+ + " display, surface: " + holder.getSurface());
+ // detaching surface is similar to turning off the display
+ mVirtualDisplay.setSurface(null);
+ }
+ });
+
+ return root;
+ }
+
+ private VirtualDisplay createVirtualDisplay(Surface surface, int width, int height) {
+ Log.i(TAG, "createVirtualDisplay, surface: " + surface + ", width: " + width
+ + "x" + height);
+ return mDisplayManager.createVirtualDisplay("Cluster-App-VD", width, height, 160, surface,
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.i(TAG, "onDestroy");
+ }
+
+ private SampleClusterServiceImpl getService() {
+ return ((MainClusterActivity) getActivity()).getService();
+ }
+
+ private int getVirtualDisplayId() {
+ return (mVirtualDisplay != null && mVirtualDisplay.getDisplay() != null)
+ ? mVirtualDisplay.getDisplay().getDisplayId() : Display.INVALID_DISPLAY;
+ }
+}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/PhoneFragment.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/PhoneFragment.java
new file mode 100644
index 0000000000..9930ec532c
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/PhoneFragment.java
@@ -0,0 +1,42 @@
+/*
+ * 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.car.cluster.sample;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+
+/**
+ * A simple {@link Fragment} subclass.
+ */
+public class PhoneFragment extends Fragment {
+
+
+ public PhoneFragment() {
+ // Required empty public constructor
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_phone, container, false);
+ }
+
+}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java
new file mode 100644
index 0000000000..14577080a5
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java
@@ -0,0 +1,259 @@
+/*
+ * 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.car.cluster.sample;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static java.lang.Integer.parseInt;
+
+import android.app.ActivityOptions;
+import android.car.CarNotConnectedException;
+import android.car.cluster.ClusterActivityState;
+import android.car.cluster.renderer.InstrumentClusterRenderingService;
+import android.car.cluster.renderer.NavigationRenderer;
+import android.car.navigation.CarNavigationInstrumentCluster;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.util.Log;
+import android.view.Display;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+/**
+ * Dummy implementation of {@link SampleClusterServiceImpl} to log all interaction.
+ */
+public class SampleClusterServiceImpl extends InstrumentClusterRenderingService {
+
+ private static final String TAG = SampleClusterServiceImpl.class.getSimpleName();
+
+ private Listener mListener;
+ private final Binder mLocalBinder = new LocalBinder();
+ static final String LOCAL_BINDING_ACTION = "local";
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.i(TAG, "onBind, intent: " + intent);
+ return (LOCAL_BINDING_ACTION.equals(intent.getAction()))
+ ? mLocalBinder : super.onBind(intent);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.i(TAG, "onCreate");
+
+ Display clusterDisplay = getInstrumentClusterDisplay(this);
+ if (clusterDisplay == null) {
+ Log.e(TAG, "Unable to find instrument cluster display");
+ return;
+ }
+
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchDisplayId(clusterDisplay.getDisplayId());
+ Intent intent = new Intent(this, MainClusterActivity.class);
+ intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent, options.toBundle());
+ }
+
+ @Override
+ protected void onKeyEvent(KeyEvent keyEvent) {
+ Log.i(TAG, "onKeyEvent, keyEvent: " + keyEvent + ", listener: " + mListener);
+ if (mListener != null) {
+ mListener.onKeyEvent(keyEvent);
+ }
+ }
+
+ void registerListener(Listener listener) {
+ mListener = listener;
+ }
+
+ void unregisterListener() {
+ mListener = null;
+ }
+
+ @Override
+ protected NavigationRenderer getNavigationRenderer() {
+ NavigationRenderer navigationRenderer = new NavigationRenderer() {
+ @Override
+ public CarNavigationInstrumentCluster getNavigationProperties() {
+ Log.i(TAG, "getNavigationProperties");
+ CarNavigationInstrumentCluster config =
+ CarNavigationInstrumentCluster.createCluster(1000);
+ Log.i(TAG, "getNavigationProperties, returns: " + config);
+ return config;
+ }
+
+ @Override
+ public void onStartNavigation() {
+ Log.i(TAG, "onStartNavigation");
+ }
+
+ @Override
+ public void onStopNavigation() {
+ Log.i(TAG, "onStopNavigation");
+ }
+
+ @Override
+ public void onNextTurnChanged(int event, CharSequence eventName, int turnAngle,
+ int turnNumber, Bitmap image, int turnSide) {
+ Log.i(TAG, "event: " + event + ", eventName: " + eventName +
+ ", turnAngle: " + turnAngle + ", turnNumber: " + turnNumber +
+ ", image: " + image + ", turnSide: " + turnSide);
+ mListener.onShowToast("Next turn: " + eventName);
+ }
+
+ @Override
+ public void onNextTurnDistanceChanged(int distanceMeters, int timeSeconds,
+ int displayDistanceMillis, int displayDistanceUnit) {
+ Log.i(TAG, "onNextTurnDistanceChanged, distanceMeters: " + distanceMeters
+ + ", timeSeconds: " + timeSeconds
+ + ", displayDistanceMillis: " + displayDistanceMillis
+ + ", displayDistanceUnit: " + displayDistanceUnit);
+ mListener.onShowToast("Next turn distance: " + distanceMeters + " meters.");
+ }
+ };
+
+ Log.i(TAG, "createNavigationRenderer, returns: " + navigationRenderer);
+ return navigationRenderer;
+ }
+
+ class LocalBinder extends Binder {
+ SampleClusterServiceImpl getService() {
+ // Return this instance of LocalService so clients can call public methods
+ return SampleClusterServiceImpl.this;
+ }
+ }
+
+ interface Listener {
+ void onKeyEvent(KeyEvent event);
+ void onShowToast(String text);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (args != null && args.length > 0) {
+ execShellCommand(args);
+ }
+ }
+
+ private void doKeyEvent(int keyCode) {
+ Log.i(TAG, "doKeyEvent, keyCode: " + keyCode);
+ long downTime = SystemClock.uptimeMillis();
+ long eventTime = SystemClock.uptimeMillis();
+ KeyEvent event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_DOWN);
+ onKeyEvent(event);
+
+ eventTime = SystemClock.uptimeMillis();
+ event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_UP);
+ onKeyEvent(event);
+ }
+
+ private KeyEvent obtainKeyEvent(int keyCode, long downTime, long eventTime, int action) {
+ int scanCode = 0;
+ if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+ scanCode = 108;
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ scanCode = 106;
+ }
+ return KeyEvent.obtain(
+ downTime,
+ eventTime,
+ action,
+ keyCode,
+ 0 /* repeat */,
+ 0 /* meta state */,
+ 0 /* deviceId*/,
+ scanCode /* scancode */,
+ KeyEvent.FLAG_FROM_SYSTEM /* flags */,
+ InputDevice.SOURCE_KEYBOARD,
+ null /* characters */);
+ }
+
+ private void execShellCommand(String[] args) {
+ Log.i(TAG, "execShellCommand, args: " + Arrays.toString(args));
+
+ String command = args[0];
+
+ switch (command) {
+ case "injectKey": {
+ if (args.length > 1) {
+ doKeyEvent(parseInt(args[1]));
+ } else {
+ Log.i(TAG, "Not enough arguments");
+ }
+ break;
+ }
+ case "destroyOverlayDisplay": {
+ Settings.Global.putString(getContentResolver(),
+ Global.OVERLAY_DISPLAY_DEVICES, "");
+ break;
+ }
+
+ case "createOverlayDisplay": {
+ if (args.length > 1) {
+ Settings.Global.putString(getContentResolver(),
+ Global.OVERLAY_DISPLAY_DEVICES, args[1]);
+ } else {
+ Log.i(TAG, "Not enough arguments, expected 2");
+ }
+ break;
+ }
+
+ case "setUnobscuredArea": {
+ if (args.length > 5) {
+ Rect unobscuredArea = new Rect(parseInt(args[2]), parseInt(args[3]),
+ parseInt(args[4]), parseInt(args[5]));
+ try {
+ setClusterActivityState(args[1],
+ ClusterActivityState.create(true, unobscuredArea).toBundle());
+ } catch (CarNotConnectedException e) {
+ Log.i(TAG, "Failed to set activity state.", e);
+ }
+ } else {
+ Log.i(TAG, "wrong format, expected: category left top right bottom");
+ }
+ }
+ }
+ }
+
+ private static Display getInstrumentClusterDisplay(Context context) {
+ DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+ Display[] displays = displayManager.getDisplays();
+
+ Log.d(TAG, "There are currently " + displays.length + " displays connected.");
+ for (Display display : displays) {
+ Log.d(TAG, " " + display);
+ }
+
+ if (displays.length > 1) {
+ // TODO: assuming that secondary display is instrument cluster. Put this into settings?
+ return displays[1];
+ }
+ return null;
+ }
+
+}
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index dc10d2b824..2f7279802b 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -18,8 +18,8 @@
package="com.google.android.car.kitchensink"
android:sharedUserId="android.uid.system">
<uses-sdk
- android:minSdkVersion="22"
- android:targetSdkVersion='23'/>
+ android:minSdkVersion="24"
+ android:targetSdkVersion='25'/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
@@ -32,6 +32,7 @@
<uses-permission android:name="android.car.permission.CAR_NAVIGATION_MANAGER"/>
<uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
<uses-permission android:name="android.car.permission.VEHICLE_DYNAMICS_STATE"/>
+ <uses-permission android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.MANAGE_USB" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
@@ -89,6 +90,19 @@
android:launchMode="singleTop">
</activity>
+ <activity android:name=".cluster.FakeClusterNavigationActivity"
+ android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+ android:launchMode="singleInstance"
+ android:resizeableActivity="true"
+ android:allowEmbedded="true"
+ android:permission="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.car.cluster.NAVIGATION"/>
+ </intent-filter>
+ </activity>
+
<receiver android:name=".bluetooth.MapReceiver"
android:permission="android.permission.READ_SMS">
<intent-filter>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/fake_cluster_navigation_activity.xml b/tests/EmbeddedKitchenSinkApp/res/layout/fake_cluster_navigation_activity.xml
new file mode 100644
index 0000000000..11f0a6b862
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/fake_cluster_navigation_activity.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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/app_bg">
+
+ <ImageView
+ android:id="@+id/unobscuredArea"
+ android:alpha="0.25"
+ android:background="@android:color/white"
+ android:layout_height="0dp"
+ android:layout_width="0dp"/>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
index 71c7fca195..cb785a9790 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
@@ -17,26 +17,25 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_marginTop="160dp"
+ android:layout_marginTop="40dp"
android:layout_marginStart="40dp">
<LinearLayout
android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="@string/cluster_start" android:id="@+id/cluster_start_button"
- android:layout_column="0" android:textSize="32sp"/>
+ android:text="@string/cluster_start" android:id="@+id/cluster_start_button"/>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="@string/cluster_turn_left" android:id="@+id/cluster_turn_left_button"
- android:layout_column="0" android:textSize="32sp"/>
+ android:text="@string/cluster_turn_left" android:id="@+id/cluster_turn_left_button"/>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="@string/cluster_stop" android:id="@+id/cluster_stop_button"
- android:layout_column="0" android:textSize="32sp"/>
+ android:text="@string/cluster_stop" android:id="@+id/cluster_stop_button"/>
+ <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="@string/cluster_start_activity" android:id="@+id/cluster_start_activity"/>
</LinearLayout>
</LinearLayout>
</LinearLayout> \ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml b/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml
index 75545088ed..49497a2b0c 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml
@@ -1,4 +1,19 @@
<?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.
+-->
+
<!-- We use this container to place kitchen app fragments. It insets the fragment contents -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/kitchen_content"
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 72e08fa1eb..75fbceb158 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -113,10 +113,11 @@
<string name="open_kb_button">Hide/Show Input</string>
<!-- instrument cluster -->
- <string name="cluster_start">Start Nav</string>
- <string name="cluster_turn_left">Turn left</string>
- <string name="cluster_stop">Stop Nav</string>
+ <string name="cluster_start">Start metadata</string>
+ <string name="cluster_turn_left">Send turn-by-turn</string>
+ <string name="cluster_stop">Stop metadata</string>
<string name="cluster_nav_app_context_loss">Navigation app context lost!</string>
+ <string name="cluster_start_activity">Start Nav Activity</string>
<!-- input test -->
<string name="volume_up">Volume +</string>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/FakeClusterNavigationActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/FakeClusterNavigationActivity.java
new file mode 100644
index 0000000000..964d8128ee
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/FakeClusterNavigationActivity.java
@@ -0,0 +1,103 @@
+/*
+ * 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.google.android.car.kitchensink.cluster;
+
+import android.app.Activity;
+import android.car.cluster.CarInstrumentClusterManager;
+import android.car.cluster.ClusterActivityState;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.car.Car;
+import android.support.car.CarConnectionCallback;
+import android.support.car.CarNotConnectedException;
+import android.util.Log;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import com.google.android.car.kitchensink.R;
+
+/**
+ * Fake navigation activity for instrument cluster.
+ */
+public class FakeClusterNavigationActivity
+ extends Activity
+ implements CarInstrumentClusterManager.Callback {
+
+ private final static String TAG = FakeClusterNavigationActivity.class.getSimpleName();
+
+ private Car mCarApi;
+ private CarInstrumentClusterManager mClusterManager;
+ private ImageView mUnobscuredArea;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.i(TAG, "onCreate");
+ setContentView(R.layout.fake_cluster_navigation_activity);
+ mUnobscuredArea = findViewById(R.id.unobscuredArea);
+
+ mCarApi = Car.createCar(this /* context */, new CarConnectionCallback() {
+
+ @Override
+ public void onConnected(Car car) {
+ onCarConnected(car);
+ }
+
+ @Override
+ public void onDisconnected(Car car) {
+ onCarDisconnected(car);
+ }
+ });
+ Log.i(TAG, "Connecting to car api...");
+ mCarApi.connect();
+ }
+
+
+ @Override
+ public void onClusterActivityStateChanged(String category, Bundle clusterActivityState) {
+ ClusterActivityState state = ClusterActivityState.fromBundle(clusterActivityState);
+ Log.i(TAG, "onClusterActivityStateChanged, category: " + category + ", state: " + state);
+
+ Rect unobscured = state.getUnobscuredBounds();
+ RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
+ unobscured.width(), unobscured.height());
+ lp.setMargins(unobscured.left, unobscured.top, 0, 0);
+ mUnobscuredArea.setLayoutParams(lp);
+ }
+
+ private void onCarConnected(Car car) {
+ Log.i(TAG, "onCarConnected, car: " + car);
+ try {
+ mClusterManager = (CarInstrumentClusterManager) car.getCarManager(
+ android.car.Car.CAR_INSTRUMENT_CLUSTER_SERVICE);
+ } catch (CarNotConnectedException e) {
+ throw new IllegalStateException(e);
+ }
+
+ try {
+ Log.i(TAG, "registering callback...");
+ mClusterManager.registerCallback(CarInstrumentClusterManager.CATEGORY_NAVIGATION, this);
+ Log.i(TAG, "callback registered");
+ } catch (android.car.CarNotConnectedException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private void onCarDisconnected(Car car) {
+
+ }
+} \ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
index 28e0a5d3e7..cfae45f775 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
@@ -16,18 +16,22 @@
package com.google.android.car.kitchensink.cluster;
import android.app.AlertDialog;
+import android.car.cluster.CarInstrumentClusterManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.car.Car;
import android.support.car.CarAppFocusManager;
-import android.support.car.CarNotConnectedException;
import android.support.car.CarConnectionCallback;
+import android.support.car.CarNotConnectedException;
import android.support.car.navigation.CarNavigationStatusManager;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Toast;
import com.google.android.car.kitchensink.R;
@@ -37,30 +41,30 @@ import com.google.android.car.kitchensink.R;
public class InstrumentClusterFragment extends Fragment {
private static final String TAG = InstrumentClusterFragment.class.getSimpleName();
+ private static final int DISPLAY_IN_CLUSTER_PERMISSION_REQUEST = 1;
+
private CarNavigationStatusManager mCarNavigationStatusManager;
private CarAppFocusManager mCarAppFocusManager;
private Car mCarApi;
- private final CarConnectionCallback mCarConnectionCallback =
- new CarConnectionCallback() {
- @Override
- public void onConnected(Car car) {
- Log.d(TAG, "Connected to Car Service");
- try {
- mCarNavigationStatusManager = (CarNavigationStatusManager) mCarApi.getCarManager(
- android.car.Car.CAR_NAVIGATION_SERVICE);
- mCarAppFocusManager =
- (CarAppFocusManager) mCarApi.getCarManager(Car.APP_FOCUS_SERVICE);
- } catch (CarNotConnectedException e) {
- Log.e(TAG, "Car is not connected!", e);
- }
+ private final CarConnectionCallback mCarConnectionCallback = new CarConnectionCallback() {
+ @Override
+ public void onConnected(Car car) {
+ Log.d(TAG, "Connected to Car Service");
+ try {
+ mCarNavigationStatusManager =
+ mCarApi.getCarManager(CarNavigationStatusManager.class);
+ mCarAppFocusManager = mCarApi.getCarManager(CarAppFocusManager.class);
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Car is not connected!", e);
}
+ }
- @Override
- public void onDisconnected(Car car) {
- Log.d(TAG, "Disconnect from Car Service");
- }
- };
+ @Override
+ public void onDisconnected(Car car) {
+ Log.d(TAG, "Disconnect from Car Service");
+ }
+ };
private void initCarApi() {
if (mCarApi != null && mCarApi.isConnected()) {
@@ -80,6 +84,7 @@ public class InstrumentClusterFragment extends Fragment {
view.findViewById(R.id.cluster_start_button).setOnClickListener(v -> initCluster());
view.findViewById(R.id.cluster_turn_left_button).setOnClickListener(v -> turnLeft());
+ view.findViewById(R.id.cluster_start_activity).setOnClickListener(v -> startNavActivity());
return view;
}
@@ -91,6 +96,31 @@ public class InstrumentClusterFragment extends Fragment {
super.onCreate(savedInstanceState);
}
+ private void startNavActivity() {
+ CarInstrumentClusterManager clusterManager;
+ try {
+ clusterManager = (CarInstrumentClusterManager) mCarApi.getCarManager(
+ android.car.Car.CAR_INSTRUMENT_CLUSTER_SERVICE);
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Failed to get CarInstrumentClusterManager", e);
+ Toast.makeText(getContext(), "Failed to get CarInstrumentClusterManager",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ // Implicit intent
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(CarInstrumentClusterManager.CATEGORY_NAVIGATION);
+ try {
+ clusterManager.startActivity(intent);
+ } catch (android.car.CarNotConnectedException e) {
+ Log.e(TAG, "Failed to startActivity in cluster", e);
+ Toast.makeText(getContext(), "Failed to start activity in cluster",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ }
+
private void turnLeft() {
try {
mCarNavigationStatusManager
@@ -100,18 +130,20 @@ public class InstrumentClusterFragment extends Fragment {
CarNavigationStatusManager.DISTANCE_METERS);
} catch (CarNotConnectedException e) {
e.printStackTrace();
- initCarApi(); // This might happen due to inst cluster renderer crash.
}
}
private void initCluster() {
try {
- mCarAppFocusManager.addFocusListener(new CarAppFocusManager.OnAppFocusChangedListener() {
- @Override
- public void onAppFocusChanged(CarAppFocusManager manager, int appType, boolean active) {
- Log.d(TAG, "onAppFocusChanged, appType: " + appType + " active: " + active);
- }
- }, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+ mCarAppFocusManager
+ .addFocusListener(new CarAppFocusManager.OnAppFocusChangedListener() {
+ @Override
+ public void onAppFocusChanged(CarAppFocusManager manager, int appType,
+ boolean active) {
+ Log.d(TAG, "onAppFocusChanged, appType: " + appType + " active: "
+ + active);
+ }
+ }, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
} catch (CarNotConnectedException e) {
Log.e(TAG, "Failed to register focus listener", e);
}
@@ -126,6 +158,7 @@ public class InstrumentClusterFragment extends Fragment {
.setMessage(R.string.cluster_nav_app_context_loss)
.show();
}
+
@Override
public void onAppFocusOwnershipGranted(CarAppFocusManager manager, int focus) {
Log.w(TAG, "onAppFocusOwnershipGranted, focus: " + focus);
@@ -155,7 +188,33 @@ public class InstrumentClusterFragment extends Fragment {
.sendNavigationStatus(CarNavigationStatusManager.STATUS_ACTIVE);
} catch (CarNotConnectedException e) {
Log.e(TAG, "Failed to set navigation status, reconnecting to the car", e);
- initCarApi(); // This might happen due to inst cluster renderer crash.
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Log.i(TAG, "onResume!");
+ if (getActivity().checkSelfPermission(android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.i(TAG, "Requesting: " + android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER);
+
+ requestPermissions(new String[] {android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER},
+ DISPLAY_IN_CLUSTER_PERMISSION_REQUEST);
+ } else {
+ Log.i(TAG, "All required permissions granted");
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ int[] grantResults) {
+ if (DISPLAY_IN_CLUSTER_PERMISSION_REQUEST == requestCode) {
+ for (int i = 0; i < permissions.length; i++) {
+ boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
+ Log.i(TAG, "onRequestPermissionsResult, requestCode: " + requestCode
+ + ", permission: " + permissions[i] + ", granted: " + granted);
+ }
}
}
}