Monitoring the launched navigation app
Bug: 119258294
Test: Manual on device
Change-Id: I04cc2f1d4b606b2596a44151ba600fd855bf6141
(cherry picked from commit 72c5b124aa655ff7547cc890dfb9c4f270d4c507)
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index beb4427..360f6b4 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -122,6 +122,7 @@
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "onServiceDisconnected, name: " + name);
+ mContext.unbindService(this);
mRendererBound = false;
synchronized (mSync) {
diff --git a/tests/DirectRenderingClusterSample/Android.mk b/tests/DirectRenderingClusterSample/Android.mk
index 62918d5..4408158 100644
--- a/tests/DirectRenderingClusterSample/Android.mk
+++ b/tests/DirectRenderingClusterSample/Android.mk
@@ -37,6 +37,8 @@
LOCAL_JAVA_LIBRARIES += android.car
LOCAL_STATIC_ANDROID_LIBRARIES += \
androidx.legacy_legacy-support-v4 \
- androidx.car_car-cluster
+ androidx-constraintlayout_constraintlayout \
+ androidx.car_car-cluster \
+ car-arch-common
include $(BUILD_PACKAGE)
diff --git a/tests/DirectRenderingClusterSample/AndroidManifest.xml b/tests/DirectRenderingClusterSample/AndroidManifest.xml
index a5f117c..4a44d73 100644
--- a/tests/DirectRenderingClusterSample/AndroidManifest.xml
+++ b/tests/DirectRenderingClusterSample/AndroidManifest.xml
@@ -39,6 +39,8 @@
<uses-permission android:name="android.permission.MANAGE_USERS" />
<!-- Required to launch navigation apps -->
<uses-permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"/>
+ <!-- Required to watch activities running on the cluster -->
+ <uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/>
<application android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
diff --git a/tests/DirectRenderingClusterSample/res/drawable/gradient_bottom.xml b/tests/DirectRenderingClusterSample/res/drawable/gradient_bottom.xml
new file mode 100644
index 0000000..ddafcf9
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/gradient_bottom.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:startColor="#00000000"
+ android:endColor="#FF000000"
+ android:angle="270"
+ android:dither="true"
+ />
+</shape>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/drawable/gradient_top.xml b/tests/DirectRenderingClusterSample/res/drawable/gradient_top.xml
new file mode 100644
index 0000000..35c8497
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/gradient_top.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:startColor="#00000000"
+ android:endColor="#FF000000"
+ android:angle="90"
+ android:dither="true"
+ />
+</shape>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml b/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
index c0fb4b3..865a3b5 100644
--- a/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
+++ b/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
@@ -1,22 +1,47 @@
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
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"
+ <SurfaceView
+ android:id="@+id/nav_surface"
android:layout_width="match_parent"
- android:layout_height="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"/>
+ <ProgressBar
+ android:id="@+id/progress_bar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/message"/>
- </FrameLayout>
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/select_nav_app"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/progress_bar"/>
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="30dp"
+ android:src="@drawable/gradient_top"
+ app:layout_constraintTop_toTopOf="parent"/>
-</FrameLayout>
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="30dp"
+ android:src="@drawable/gradient_bottom"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/values/strings.xml b/tests/DirectRenderingClusterSample/res/values/strings.xml
index 778ecf6..e86cf60 100644
--- a/tests/DirectRenderingClusterSample/res/values/strings.xml
+++ b/tests/DirectRenderingClusterSample/res/values/strings.xml
@@ -3,4 +3,7 @@
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
+
+ <!-- Message to show when a navigation app hasn't been selected yet. [CHAR LIMIT=100]-->
+ <string name="select_nav_app">Select a navigation app on the main unit.</string>
</resources>
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
new file mode 100644
index 0000000..8b69e65
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2018 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.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManager.StackInfo;
+import android.app.IActivityManager;
+import android.app.IProcessObserver;
+import android.app.TaskStackListener;
+import android.content.ComponentName;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Top activity monitor, allows listeners to be notified when a new activity comes to the foreground
+ * on a particular device.
+ */
+public class ActivityMonitor {
+ private static final String TAG = "Cluster.ActivityMonitor";
+
+ /**
+ * Listener of activity changes
+ */
+ public interface ActivityListener {
+ /**
+ * Invoked when a new activity becomes the top activity on a particular display.
+ */
+ void onTopActivityChanged(int displayId, @Nullable ComponentName activity);
+ }
+
+ private IActivityManager mActivityManager;
+ // Listeners of top activity changes, indexed by the displayId they are interested on.
+ private final Map<Integer, Set<ActivityListener>> mListeners = new HashMap<>();
+ private final IProcessObserver.Stub mProcessObserver = new IProcessObserver.Stub() {
+ @Override
+ public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
+ notifyTopActivities();
+ }
+
+ @Override
+ public void onProcessDied(int pid, int uid) {
+ notifyTopActivities();
+ }
+ };
+ private final TaskStackListener mTaskStackListener = new TaskStackListener() {
+ @Override
+ public void onTaskStackChanged() {
+ Log.i(TAG, "onTaskStackChanged");
+ notifyTopActivities();
+ }
+ };
+
+ /**
+ * Registers a new listener to receive activity updates on a particular display
+ *
+ * @param displayId identifier of the display to monitor
+ * @param listener listener to be notified
+ */
+ public void addListener(int displayId, ActivityListener listener) {
+ mListeners.computeIfAbsent(displayId, k -> new HashSet<>()).add(listener);
+ }
+
+ /**
+ * Unregisters a listener previously registered with {@link #addListener(int, ActivityListener)}
+ */
+ public void removeListener(int displayId, ActivityListener listener) {
+ mListeners.computeIfAbsent(displayId, k -> new HashSet<>()).remove(listener);
+ }
+
+ /**
+ * Starts monitoring activity changes. {@link #stop()} should be invoked to release resources.
+ */
+ public void start() {
+ mActivityManager = ActivityManager.getService();
+ // Monitoring both listeners are necessary as there are cases where one listener cannot
+ // monitor activity change.
+ try {
+ mActivityManager.registerProcessObserver(mProcessObserver);
+ mActivityManager.registerTaskStackListener(mTaskStackListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Cannot register activity monitoring", e);
+ throw new RuntimeException(e);
+ }
+ notifyTopActivities();
+ }
+
+ /**
+ * Stops monitoring activity changes. Should be invoked when this monitor is not longer used.
+ */
+ public void stop() {
+ if (mActivityManager == null) {
+ return;
+ }
+ try {
+ mActivityManager.unregisterProcessObserver(mProcessObserver);
+ mActivityManager.unregisterTaskStackListener(mTaskStackListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Cannot unregister activity monitoring. Ignoring", e);
+ }
+ mActivityManager = null;
+ }
+
+ private void notifyTopActivities() {
+ try {
+ List<StackInfo> infos = mActivityManager.getAllStackInfos();
+ for (StackInfo info : infos) {
+ Set<ActivityListener> listeners = mListeners.get(info.displayId);
+ if (listeners != null && !listeners.isEmpty()) {
+ for (ActivityListener listener : listeners) {
+ listener.onTopActivityChanged(info.displayId, info.topActivity);
+ }
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Cannot getTasks", e);
+ }
+ }
+}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
index 66d2d58..74125c3 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
@@ -64,6 +64,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
/**
* Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
@@ -82,10 +83,14 @@
static final int MSG_ON_KEY_EVENT = 3;
static final int MSG_REGISTER_CLIENT = 4;
static final int MSG_UNREGISTER_CLIENT = 5;
+ static final int MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED = 6;
static final String MSG_KEY_CATEGORY = "category";
static final String MSG_KEY_ACTIVITY_DISPLAY_ID = "activity_display_id";
static final String MSG_KEY_ACTIVITY_STATE = "activity_state";
static final String MSG_KEY_KEY_EVENT = "key_event";
+ static final String MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME = "free_navigation_activity_name";
+ static final String MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE =
+ "free_navigation_activity_visible";
private static final int NAVIGATION_ACTIVITY_MAX_RETRIES = 10;
private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
@@ -95,8 +100,12 @@
private int mDisplayId = NO_DISPLAY;
private UserReceiver mUserReceiver;
private final Handler mHandler = new Handler();
+ private final IBinder mLocalBinder = new Messenger(new MessageHandler(this)).getBinder();
private final Runnable mRetryLaunchNavigationActivity = this::tryLaunchNavigationActivity;
private int mNavigationDisplayId = NO_DISPLAY;
+ private ComponentName mFreeNavigationActivity;
+ private ActivityMonitor mActivityMonitor = new ActivityMonitor();
+ private boolean mFreeNavigationActivityVisible;
private final DisplayListener mDisplayListener = new DisplayListener() {
@Override
@@ -174,6 +183,7 @@
}
case MSG_REGISTER_CLIENT:
mService.get().mClients.add(msg.replyTo);
+ mService.get().notifyFreeNavigationActivityChange();
break;
case MSG_UNREGISTER_CLIENT:
mService.get().mClients.remove(msg.replyTo);
@@ -187,11 +197,22 @@
}
}
+ private ActivityMonitor.ActivityListener mNavigationActivityMonitor = (displayId, activity) -> {
+ if (displayId != mNavigationDisplayId) {
+ return;
+ }
+ boolean activityVisible = activity != null && activity.equals(mFreeNavigationActivity);
+ if (activityVisible != mFreeNavigationActivityVisible) {
+ mFreeNavigationActivityVisible = activityVisible;
+ notifyFreeNavigationActivityChange();
+ }
+ };
+
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind, intent: " + intent);
- return (LOCAL_BINDING_ACTION.equals(intent.getAction()))
- ? new Messenger(new MessageHandler(this)).getBinder()
+ return LOCAL_BINDING_ACTION.equals(intent.getAction())
+ ? mLocalBinder
: super.onBind(intent);
}
@@ -202,6 +223,7 @@
mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener);
mUserReceiver = new UserReceiver(this);
mUserReceiver.register(this);
+ mActivityMonitor.start();
}
private void launchMainActivity() {
@@ -247,6 +269,7 @@
super.onDestroy();
Log.w(TAG, "onDestroy");
mUserReceiver.unregister(this);
+ mActivityMonitor.stop();
}
@Override
@@ -299,11 +322,8 @@
if (args != null && args.length > 0) {
execShellCommand(args);
} else {
-
- if (args == null || args.length == 0) {
- writer.println("* dump " + getClass().getCanonicalName() + " *");
- writer.println("DisplayProvider: " + mDisplayProvider);
- }
+ writer.println("* dump " + getClass().getCanonicalName() + " *");
+ writer.println("DisplayProvider: " + mDisplayProvider);
}
}
@@ -388,6 +408,8 @@
}
private void startNavigationActivity(int displayId) {
+ mActivityMonitor.removeListener(mNavigationDisplayId, mNavigationActivityMonitor);
+ mActivityMonitor.addListener(displayId, mNavigationActivityMonitor);
mNavigationDisplayId = displayId;
tryLaunchNavigationActivity();
}
@@ -410,12 +432,23 @@
}
mHandler.removeCallbacks(mRetryLaunchNavigationActivity);
- Intent intent = getNavigationActivityIntent();
+ ComponentName navigationActivity = getNavigationActivity();
+ if (!Objects.equals(navigationActivity, mFreeNavigationActivity)) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Navigation activity change detected: " + navigationActivity);
+ }
+ mFreeNavigationActivity = navigationActivity;
+ notifyFreeNavigationActivityChange();
+ }
try {
- if (intent == null) {
+ if (navigationActivity == null) {
throw new ActivityNotFoundException();
}
+ Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(CATEGORY_NAVIGATION)
+ .setPackage(navigationActivity.getPackageName())
+ .setComponent(navigationActivity)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.d(TAG, "Launching: " + intent + " on display: " + mNavigationDisplayId);
Bundle activityOptions = ActivityOptions.makeBasic()
.setLaunchDisplayId(mNavigationDisplayId)
@@ -427,22 +460,29 @@
mHandler.postDelayed(mRetryLaunchNavigationActivity,
NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS);
} catch (Exception ex) {
- Log.e(TAG, "Unable to start navigation activity: " + intent, ex);
+ Log.e(TAG, "Unable to start navigation activity: " + navigationActivity, ex);
}
}
+ private void notifyFreeNavigationActivityChange() {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME, mFreeNavigationActivity);
+ bundle.putBoolean(MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE, mFreeNavigationActivityVisible);
+ broadcastClientMessage(MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED, bundle);
+ }
+
/**
* Returns a default navigation activity to show in the cluster.
* In the current implementation we search for an activity with the
* {@link CarInstrumentClusterManager#CATEGORY_NAVIGATION} category from the same navigation app
* selected from CarLauncher (see CarLauncher#getMapsIntent()).
- * Alternatively, other implementations could
+ * Alternatively, other implementations could:
* <ul>
* <li>Read this package from a resource (having a OEM default activity to show)
* <li>Let the user select one from settings.
* </ul>
*/
- private Intent getNavigationActivityIntent() {
+ private ComponentName getNavigationActivity() {
PackageManager pm = getPackageManager();
int userId = ActivityManager.getCurrentUser();
@@ -464,11 +504,8 @@
if (candidate.activityInfo.packageName.equals(navigationApp.activityInfo
.packageName)) {
Log.d(TAG, "Found activity: " + candidate);
- intent.setPackage(navigationApp.activityInfo.packageName);
- intent.setComponent(new ComponentName(candidate.activityInfo.packageName,
- candidate.activityInfo.name));
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- return intent;
+ return new ComponentName(candidate.activityInfo.packageName,
+ candidate.activityInfo.name);
}
}
} else {
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
new file mode 100644
index 0000000..55c9e54
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2018 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.app.Application;
+import android.content.ComponentName;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import java.util.Objects;
+
+/**
+ * {@link AndroidViewModel} for cluster information.
+ */
+public class ClusterViewModel extends AndroidViewModel {
+ /**
+ * Reference to a component (e.g.: an activity) and whether such component is visible or not.
+ */
+ public static class ComponentVisibility {
+ /**
+ * Application component name
+ */
+ public final ComponentName mComponent;
+ /**
+ * Whether the component is currently visible to the user or not.
+ */
+ public final boolean mIsVisible;
+
+ /**
+ * Creates a new component visibility reference
+ */
+ private ComponentVisibility(ComponentName component, boolean isVisible) {
+ mComponent = component;
+ mIsVisible = isVisible;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ComponentVisibility that = (ComponentVisibility) o;
+ return mIsVisible == that.mIsVisible
+ && Objects.equals(mComponent, that.mComponent);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mComponent, mIsVisible);
+ }
+ }
+
+ private final MutableLiveData<ComponentVisibility> mFreeNavigationActivity =
+ new MutableLiveData<>(new ComponentVisibility(null, false));
+ private final MutableLiveData<Boolean> mNavigationFocus = new MutableLiveData<>(false);
+
+ /**
+ * New {@link ClusterViewModel} instance
+ */
+ public ClusterViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ /**
+ * Returns a {@link LiveData} providing the activity selected to be displayed on the cluster
+ * when navigation focus is not granted (a.k.a.: free navigation). It also indicates whether
+ * such activity is currently visible to the user or not.
+ */
+ public LiveData<ComponentVisibility> getFreeNavigationActivity() {
+ return mFreeNavigationActivity;
+ }
+
+ /**
+ * Returns a {@link LiveData} indicating whether navigation focus is currently being granted
+ * or not. This indicates whether a navigation application is currently providing driving
+ * directions. Instrument cluster can use this signal to show/hide turn-by-turn
+ * directions UI, and hide/show the free navigation activity
+ * (see {@link #getFreeNavigationActivity()}).
+ */
+ public LiveData<Boolean> getNavigationFocus() {
+ return mNavigationFocus;
+ }
+
+ /**
+ * Sets the activity selected to be displayed on the cluster when no driving directions are
+ * being provided, and whether such activity is currently visible to the user or not
+ */
+ public void setFreeNavigationActivity(ComponentName application, boolean isVisible) {
+ ComponentVisibility newValue = new ComponentVisibility(application, isVisible);
+ if (!Objects.equals(mFreeNavigationActivity.getValue(), newValue)) {
+ mFreeNavigationActivity.setValue(new ComponentVisibility(application, isVisible));
+ }
+ }
+
+ /**
+ * Sets whether navigation focus is currently being granted or not.
+ */
+ public void setNavigationFocus(boolean navigationFocus) {
+ if (mNavigationFocus.getValue() == null || mNavigationFocus.getValue() != navigationFocus) {
+ mNavigationFocus.setValue(navigationFocus);
+ }
+ }
+}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
index 7464552..2a2ac73 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
@@ -20,11 +20,18 @@
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_DISPLAY_ID;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_STATE;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_CATEGORY;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl
+ .MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_KEY_EVENT;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl
+ .MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_KEY_EVENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_NAVIGATION_STATE_CHANGED;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl
+ .MSG_ON_NAVIGATION_STATE_CHANGED;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_REGISTER_CLIENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl
+ .MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_UNREGISTER_CLIENT;
import android.car.Car;
@@ -55,6 +62,7 @@
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.lifecycle.ViewModelProviders;
import androidx.versionedparcelable.ParcelUtils;
import androidx.viewpager.widget.ViewPager;
@@ -69,6 +77,7 @@
private ViewPager mPager;
private NavStateController mNavStateController;
+ private ClusterViewModel mClusterViewModel;
private HashMap<Button, Facet<?>> mButtonToFacet = new HashMap<>();
private SparseArray<Facet<?>> mOrderToFacet = new SparseArray<>();
@@ -132,9 +141,9 @@
Log.e(TAG, "onServiceConnected: unable to obtain CarAppFocusManager");
return;
}
- mCarAppFocusManager.addFocusListener((appType, active) -> {
- onNavigationFocusChanged(active);
- }, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+ mCarAppFocusManager.addFocusListener(
+ (appType, active) -> mClusterViewModel.setNavigationFocus(active),
+ CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
} catch (CarNotConnectedException e) {
Log.e(TAG, "onServiceConnected: error obtaining manager", e);
}
@@ -159,7 +168,10 @@
Bundle data = msg.getData();
switch (msg.what) {
case MSG_ON_KEY_EVENT:
- mActivity.get().onKeyEvent(data.getParcelable(MSG_KEY_KEY_EVENT));
+ KeyEvent event = data.getParcelable(MSG_KEY_KEY_EVENT);
+ if (event != null) {
+ mActivity.get().onKeyEvent(event);
+ }
break;
case MSG_ON_NAVIGATION_STATE_CHANGED:
if (data == null) {
@@ -172,6 +184,13 @@
mActivity.get().onNavigationStateChange(navState);
}
break;
+ case MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED:
+ ComponentName activity = data.getParcelable(
+ MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME);
+ boolean isVisible = data.getBoolean(MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE);
+ mActivity.get().mClusterViewModel.setFreeNavigationActivity(activity,
+ isVisible);
+ break;
default:
super.handleMessage(msg);
}
@@ -201,6 +220,10 @@
mOrderToFacet.get(0).button.requestFocus();
mNavStateController = new NavStateController(findViewById(R.id.navigation_state));
+ mClusterViewModel = ViewModelProviders.of(this).get(ClusterViewModel.class);
+ mClusterViewModel.getNavigationFocus().observe(this, active ->
+ mNavStateController.setActive(active));
+
mCar = Car.createCar(this, mCarServiceConnection);
mCar.connect();
}
@@ -235,12 +258,6 @@
}
}
- private void onNavigationFocusChanged(boolean active) {
- if (mNavStateController != null) {
- mNavStateController.setActive(active);
- }
- }
-
public void updateNavDisplay(VirtualDisplay virtualDisplay) {
if (mService == null) {
// Service is not bound yet. Hold the information and notify when the service is bound.
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
index 44b6268..f31c090 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
@@ -58,7 +58,9 @@
* Updates views to reflect the provided navigation state
*/
public void update(@Nullable NavigationState state) {
- Log.i(TAG, "Updating nav state: " + state);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Updating nav state: " + state);
+ }
Step step = getImmediateStep(state);
mManeuver.setImageDrawable(getManeuverIcon(step != null ? step.getManeuver() : null));
mDistance.setText(formatDistance(step != null ? step.getDistance() : null));
@@ -69,7 +71,9 @@
* a navigation application has focus.
*/
public void setActive(boolean active) {
- Log.i(TAG, "Navigation status active: " + active);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Navigation status active: " + active);
+ }
if (!active) {
mManeuver.setImageDrawable(null);
mDistance.setText(null);
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
index 1b61658..407e13a 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
@@ -31,8 +31,12 @@
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelProviders;
public class NavigationFragment extends Fragment {
private static final String TAG = "Cluster.NavFragment";
@@ -41,6 +45,10 @@
private DisplayManager mDisplayManager;
private Rect mUnobscuredBounds;
private MainClusterActivity mMainClusterActivity;
+ private ClusterViewModel mViewModel;
+ private ProgressBar mProgressBar;
+ private TextView mMessage;
+
// Static because we want to keep alive this virtual display when navigating through
// ViewPager (this fragment gets dynamically destroyed and created)
@@ -109,6 +117,9 @@
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.i(TAG, "onCreateView");
+ ViewModelProvider provider = ViewModelProviders.of(requireActivity());
+ mViewModel = provider.get(ClusterViewModel.class);
+
mDisplayManager = getActivity().getSystemService(DisplayManager.class);
mDisplayManager.registerDisplayListener(mDisplayListener, new Handler());
@@ -145,6 +156,14 @@
mVirtualDisplay.setSurface(null);
}
});
+ mProgressBar = root.findViewById(R.id.progress_bar);
+ mMessage = root.findViewById(R.id.message);
+
+ mViewModel.getFreeNavigationActivity().observe(this, app -> {
+ mProgressBar.setVisibility(app.mComponent != null && !app.mIsVisible ? View.VISIBLE
+ : View.GONE);
+ mMessage.setVisibility(app.mComponent == null ? View.VISIBLE : View.GONE);
+ });
return root;
}