More multi-user fixes on SampleClusterService

Bug: 79884417
Test: Ran on device
Change-Id: Idedb6d708c99a7e170b3310e39954329dd8b5085
diff --git a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
index 0ec6d72..ad54893 100644
--- a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
+++ b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
@@ -79,6 +79,7 @@
         try {
             mService.startClusterActivity(intent);
         } catch (RemoteException e) {
+            Log.e(TAG, "Unable to launch activity (" + intent + ")", e);
             throw new CarNotConnectedException(e);
         }
     }
diff --git a/car-lib/src/android/car/navigation/CarNavigationStatusManager.java b/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
index df0dee9..6d4a561 100644
--- a/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
+++ b/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
@@ -55,6 +55,7 @@
         try {
             mService.onEvent(eventType, bundle);
         } catch (IllegalStateException e) {
+            Log.e(TAG, "Illegal state sending event " + eventType, e);
             CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
         } catch (RemoteException e) {
             handleCarServiceRemoteExceptionAndThrow(e);
@@ -63,7 +64,9 @@
 
     /** @hide */
     @Override
-    public void onCarDisconnected() {}
+    public void onCarDisconnected() {
+        Log.e(TAG, "Car service disconnected");
+    }
 
     /** Returns navigation features of instrument cluster */
     public CarNavigationInstrumentCluster getInstrumentClusterInfo()
@@ -78,12 +81,7 @@
 
     private void handleCarServiceRemoteExceptionAndThrow(RemoteException e)
             throws CarNotConnectedException {
-        handleCarServiceRemoteException(e);
-        throw new CarNotConnectedException();
-    }
-
-    private void handleCarServiceRemoteException(RemoteException e) {
-        Log.w(TAG, "RemoteException from car service:" + e.getMessage());
-        // nothing to do for now
+        Log.e(TAG, "RemoteException from car service:" + e);
+        throw new CarNotConnectedException(e);
     }
 }
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index ab3ea62..91b8c1a 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -19,6 +19,7 @@
 
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.ActivityOptions;
 import android.car.Car;
 import android.car.CarAppFocusManager;
 import android.car.cluster.CarInstrumentClusterManager;
@@ -42,6 +43,7 @@
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
@@ -323,6 +325,8 @@
 
         resolveList = checkPermission(resolveList, Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER);
         if (resolveList.isEmpty()) {
+            Log.w(TAG, String.format("intent didn't have permission %s: %s",
+                    Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER, intent));
             return;
         }
 
@@ -348,7 +352,10 @@
         // Virtual display could be private and not available to calling process.
         final long token = Binder.clearCallingIdentity();
         try {
-            mContext.startActivity(intent, opts.launchOptions);
+            mContext.startActivityAsUser(intent, opts.launchOptions, UserHandle.CURRENT);
+            Log.i(TAG, String.format("activity launched: %s (options: %s, displayId: %d)",
+                    opts.launchOptions, intent, new ActivityOptions(opts.launchOptions)
+                            .getLaunchDisplayId()));
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -422,8 +429,11 @@
             Set<String> registeredCategories = mActivityInfoByCategory.keySet();
 
             for (ResolveInfo resolveInfo : resolveList) {
+                if (resolveInfo.filter == null) {
+                    continue;
+                }
                 for (String category : registeredCategories) {
-                    if (resolveInfo.filter != null && resolveInfo.filter.hasCategory(category)) {
+                    if (resolveInfo.filter.hasCategory(category)) {
                         ClusterActivityInfo categoryInfo = mActivityInfoByCategory.get(category);
                         return new Pair<>(resolveInfo, categoryInfo);
                     }
diff --git a/tests/DirectRenderingClusterSample/AndroidManifest.xml b/tests/DirectRenderingClusterSample/AndroidManifest.xml
index ad59fdf..9ec4eb5 100644
--- a/tests/DirectRenderingClusterSample/AndroidManifest.xml
+++ b/tests/DirectRenderingClusterSample/AndroidManifest.xml
@@ -35,6 +35,8 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
     <!-- Required by 'startActivityAsUser' -->
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
+    <!-- Required to detect the current user in the device -->
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
 
     <application android:label="@string/app_name"
                  android:icon="@mipmap/ic_launcher"
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
index b8a8016..3e31615 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
@@ -105,11 +105,15 @@
                     mActivity.get().onKeyEvent(data.getParcelable(MSG_KEY_KEY_EVENT));
                     break;
                 case MSG_ON_NAVIGATION_STATE_CHANGED:
-                    data.setClassLoader(ParcelUtils.class.getClassLoader());
-                    NavigationState navState = NavigationState
-                            .fromParcelable(data.getParcelable(
-                                    SampleClusterServiceImpl.NAV_STATE_BUNDLE_KEY));
-                    mActivity.get().onNavigationStateChange(navState);
+                    if (data == null) {
+                        mActivity.get().onNavigationStateChange(null);
+                    } else {
+                        data.setClassLoader(ParcelUtils.class.getClassLoader());
+                        NavigationState navState = NavigationState
+                                .fromParcelable(data.getParcelable(
+                                        SampleClusterServiceImpl.NAV_STATE_BUNDLE_KEY));
+                        mActivity.get().onNavigationStateChange(navState);
+                    }
                     break;
                 default:
                     super.handleMessage(msg);
@@ -148,9 +152,9 @@
         Log.d(TAG, "onDestroy");
         if (mService != null) {
             sendServiceMessage(MSG_UNREGISTER_CLIENT, null, mServiceCallbacks);
-            unbindService(mServiceConnection);
             mService = null;
         }
+        unbindService(mServiceConnection);
     }
 
     private void onKeyEvent(KeyEvent event) {
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
index 30b3b49..b45a806 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
@@ -15,6 +15,7 @@
  */
 package android.car.cluster.sample;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.util.Log;
@@ -55,14 +56,14 @@
     /**
      * Updates views to reflect the provided navigation state
      */
-    public void update(NavigationState state) {
+    public void update(@Nullable NavigationState state) {
         Log.i(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));
     }
 
-    private Drawable getManeuverIcon(Maneuver maneuver) {
+    private Drawable getManeuverIcon(@Nullable Maneuver maneuver) {
         if (maneuver == null) {
             return null;
         }
@@ -179,11 +180,11 @@
         return null;
     }
 
-    private Step getImmediateStep(NavigationState state) {
-        return state.getSteps().size() > 0 ? state.getSteps().get(0) : null;
+    private Step getImmediateStep(@Nullable NavigationState state) {
+        return state != null && state.getSteps().size() > 0 ? state.getSteps().get(0) : null;
     }
 
-    private String formatDistance(Distance distance) {
+    private String formatDistance(@Nullable Distance distance) {
         if (distance == null || distance.getDisplayUnit() == Distance.Unit.UNKNOWN) {
             return null;
         }
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
index 5066162..a9c76a3 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
@@ -180,13 +180,12 @@
         encoder.setCallback(new MediaCodec.Callback() {
             @Override
             public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
-                Log.i(TAG, "onInputBufferAvailable, index: " + index);
+                // Nothing to do
             }
 
             @Override
             public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index,
                     @NonNull BufferInfo info) {
-                Log.i(TAG, "onOutputBufferAvailable, index: " + index);
                 mCounter.outputBuffers++;
                 doOutputBufferAvailable(index, info);
             }
@@ -247,8 +246,9 @@
 
     private void sendFrame(byte[] buf, int len) {
         try {
-            mOutputStream.write(buf, 0, len);
-            Log.i(TAG, "Bytes written: " + len);
+            if (mOutputStream != null) {
+                mOutputStream.write(buf, 0, len);
+            }
         } catch (IOException e) {
             mCounter.clientsDisconnected++;
             mOutputStream = null;
@@ -326,7 +326,6 @@
 
                 case MSG_RESUBMIT_FRAME:
                     if (mServerSocket != null && mOutputStream != null) {
-                        Log.i(TAG, "Resending the last frame again. Buffer: " + mLastFrameLength);
                         sendFrame(mBuffer, mLastFrameLength);
                     }
                     // We will keep sending last frame every second as a heartbeat.
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java
index cd337e5..e8a2a12 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java
@@ -15,17 +15,23 @@
  */
 package android.car.cluster.sample;
 
+import static android.content.Intent.ACTION_USER_SWITCHED;
+import static android.content.Intent.ACTION_USER_UNLOCKED;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 
 import static java.lang.Integer.parseInt;
 
+import android.app.ActivityManager;
 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.BroadcastReceiver;
+import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.os.Bundle;
@@ -59,6 +65,8 @@
 public class SampleClusterServiceImpl extends InstrumentClusterRenderingService {
     private static final String TAG = "Cluster.SampleService";
 
+    private static final int NO_DISPLAY = -1;
+
     static final String LOCAL_BINDING_ACTION = "local";
     static final String NAV_STATE_BUNDLE_KEY = "navstate";
     static final int NAV_STATE_EVENT_ID = 1;
@@ -75,6 +83,52 @@
 
     private List<Messenger> mClients = new ArrayList<>();
     private ClusterDisplayProvider mDisplayProvider;
+    private int mDisplayId = NO_DISPLAY;
+    private UserReceiver mUserReceiver;
+
+    private final DisplayListener mDisplayListener = new DisplayListener() {
+        @Override
+        public void onDisplayAdded(int displayId) {
+            Log.i(TAG, "Cluster display found, displayId: " + displayId);
+            mDisplayId = displayId;
+            tryLaunchActivity();
+        }
+
+        @Override
+        public void onDisplayRemoved(int displayId) {
+            Log.w(TAG, "Cluster display has been removed");
+        }
+
+        @Override
+        public void onDisplayChanged(int displayId) {
+
+        }
+    };
+
+    private static class UserReceiver extends BroadcastReceiver {
+        private WeakReference<SampleClusterServiceImpl> mService;
+
+        UserReceiver(SampleClusterServiceImpl service) {
+            mService = new WeakReference<>(service);
+        }
+
+        public void register(Context context) {
+            IntentFilter intentFilter =  new IntentFilter(ACTION_USER_UNLOCKED);
+            intentFilter.addAction(ACTION_USER_SWITCHED);
+            context.registerReceiver(this, intentFilter);
+        }
+
+        public void unregister(Context context) {
+            context.unregisterReceiver(this);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            SampleClusterServiceImpl service = mService.get();
+            Log.d(TAG, "Broadcast received: " + intent);
+            service.tryLaunchActivity();
+        }
+    }
 
     private static class MessageHandler extends Handler {
         private final WeakReference<SampleClusterServiceImpl> mService;
@@ -88,19 +142,22 @@
             Log.d(TAG, "handleMessage: " + msg.what);
             try {
                 switch (msg.what) {
-                    case MSG_SET_ACTIVITY_LAUNCH_OPTIONS:
-                        mService.get().setClusterActivityLaunchOptions(
-                                msg.getData().getString(MSG_KEY_CATEGORY),
-                                ActivityOptions.fromBundle(
-                                        msg.getData().getBundle(MSG_KEY_ACTIVITY_OPTIONS)
-                                ));
+                    case MSG_SET_ACTIVITY_LAUNCH_OPTIONS: {
+                        ActivityOptions options = ActivityOptions.fromBundle(
+                                msg.getData().getBundle(MSG_KEY_ACTIVITY_OPTIONS));
+                        String category = msg.getData().getString(MSG_KEY_CATEGORY);
+                        mService.get().setClusterActivityLaunchOptions(category, options);
+                        Log.d(TAG, String.format("activity options set: %s = %s (displayeId: %d)",
+                                category, options, options.getLaunchDisplayId()));
                         break;
-                    case MSG_SET_ACTIVITY_STATE:
-                        mService.get().setClusterActivityState(
-                                msg.getData().getString(MSG_KEY_CATEGORY),
-                                msg.getData().getBundle(MSG_KEY_ACTIVITY_STATE)
-                        );
+                    }
+                    case MSG_SET_ACTIVITY_STATE: {
+                        Bundle state = msg.getData().getBundle(MSG_KEY_ACTIVITY_STATE);
+                        String category = msg.getData().getString(MSG_KEY_CATEGORY);
+                        mService.get().setClusterActivityState(category, state);
+                        Log.d(TAG, String.format("activity state set: %s = %s", category, state));
                         break;
+                    }
                     case MSG_REGISTER_CLIENT:
                         mService.get().mClients.add(msg.replyTo);
                         break;
@@ -128,34 +185,26 @@
     public void onCreate() {
         super.onCreate();
         Log.d(TAG, "onCreate");
-
-        mDisplayProvider = new ClusterDisplayProvider(this,
-                new DisplayListener() {
-                    @Override
-                    public void onDisplayAdded(int displayId) {
-                        Log.i(TAG, "Cluster display found, displayId: " + displayId);
-                        doClusterDisplayConnected(displayId);
-                    }
-
-                    @Override
-                    public void onDisplayRemoved(int displayId) {
-                        Log.w(TAG, "Cluster display has been removed");
-                    }
-
-                    @Override
-                    public void onDisplayChanged(int displayId) {
-
-                    }
-                });
+        mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener);
+        mUserReceiver = new UserReceiver(this);
+        mUserReceiver.register(this);
     }
 
-    private void doClusterDisplayConnected(int displayId) {
+    private void tryLaunchActivity() {
+        int userHandle = ActivityManager.getCurrentUser();
+        if (userHandle == UserHandle.USER_SYSTEM || mDisplayId == NO_DISPLAY) {
+            Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)", userHandle,
+                    mDisplayId));
+            // Not ready to launch yet.
+            return;
+        }
         ActivityOptions options = ActivityOptions.makeBasic();
-        options.setLaunchDisplayId(displayId);
+        options.setLaunchDisplayId(mDisplayId);
         Intent intent = new Intent(this, MainClusterActivity.class);
         intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
-        startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT);
-        Log.d(TAG, "launching main activity: " + intent);
+        startActivityAsUser(intent, options.toBundle(), UserHandle.of(userHandle));
+        Log.i(TAG, String.format("launching main activity: %s (user: %d, display: %d)", intent,
+                userHandle, mDisplayId));
     }
 
     @Override
@@ -191,6 +240,7 @@
     public void onDestroy() {
         super.onDestroy();
         Log.w(TAG, "onDestroy");
+        mUserReceiver.unregister(this);
     }
 
     @Override
@@ -206,24 +256,31 @@
 
             @Override
             public void onEvent(int eventType, Bundle bundle) {
-                StringBuilder bundleSummary = new StringBuilder();
-                if (eventType == NAV_STATE_EVENT_ID) {
-                    bundle.setClassLoader(ParcelUtils.class.getClassLoader());
-                    NavigationState navState = NavigationState
-                            .fromParcelable(bundle.getParcelable(NAV_STATE_BUNDLE_KEY));
-                    bundleSummary.append(navState.toString());
+                try {
+                    StringBuilder bundleSummary = new StringBuilder();
+                    if (eventType == NAV_STATE_EVENT_ID) {
+                        bundle.setClassLoader(ParcelUtils.class.getClassLoader());
+                        NavigationState navState = NavigationState
+                                .fromParcelable(bundle.getParcelable(NAV_STATE_BUNDLE_KEY));
+                        bundleSummary.append(navState.toString());
 
-                    // Update clients
-                    broadcastClientMessage(MSG_ON_NAVIGATION_STATE_CHANGED, bundle);
-                } else {
-                    for (String key : bundle.keySet()) {
-                        bundleSummary.append(key);
-                        bundleSummary.append("=");
-                        bundleSummary.append(bundle.get(key));
-                        bundleSummary.append(" ");
+                        // Update clients
+                        broadcastClientMessage(MSG_ON_NAVIGATION_STATE_CHANGED, bundle);
+                    } else {
+                        for (String key : bundle.keySet()) {
+                            bundleSummary.append(key);
+                            bundleSummary.append("=");
+                            bundleSummary.append(bundle.get(key));
+                            bundleSummary.append(" ");
+                        }
                     }
+                    Log.d(TAG, "onEvent(" + eventType + ", " + bundleSummary + ")");
+                } catch (Exception e) {
+                    Log.e(TAG, "Error parsing event data (" + eventType + ", " + bundle + ")", e);
+                    bundle.putParcelable(NAV_STATE_BUNDLE_KEY, new NavigationState.Builder().build()
+                            .toParcelable());
+                    broadcastClientMessage(MSG_ON_NAVIGATION_STATE_CHANGED, bundle);
                 }
-                Log.d(TAG, "onEvent(" + eventType + ", " + bundleSummary + ")");
             }
         };