Merge Android 14 QPR2 to AOSP main

Bug: 319669529
Merged-In: I633ebf3e5a829ad6255edc3b6696eaeb4cf78d57
Change-Id: I647481c7c452ebb5cfc758e55b9ae0bd3c487c42
diff --git a/ClusterHomeSample/AndroidManifest.xml b/ClusterHomeSample/AndroidManifest.xml
index b2ca15f..526280a 100644
--- a/ClusterHomeSample/AndroidManifest.xml
+++ b/ClusterHomeSample/AndroidManifest.xml
@@ -50,6 +50,20 @@
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
+        <activity android:name=".ClusterHomeActivityLightMode"
+                  android:exported="true"
+                  android:showForAllUsers="true"
+                  android:excludeFromRecents="true"
+                  android:screenOrientation="nosensor"
+                  android:launchMode="singleTask"
+                  android:configChanges="uiMode|mcc|mnc"
+                  android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+            <meta-data android:name="distractionOptimized" android:value="true"/>
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
         <activity android:name=".FakeFreeNavigationActivity"
                 android:exported="true"
                 android:showForAllUsers="true"
diff --git a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivity.java b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivity.java
index bc05beb..0cb640c 100644
--- a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivity.java
+++ b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivity.java
@@ -17,22 +17,20 @@
 package com.android.car.cluster.home;
 
 import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.IActivityManager;
 import android.car.Car;
 import android.car.cluster.ClusterActivityState;
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.RemoteException;
 import android.util.Log;
 import android.view.View;
-import android.view.WindowManager;
 
 /**
  * Skeleton Activity for Home UI in Cluster display.
  */
-public class ClusterHomeActivity extends Activity {
+public class ClusterHomeActivity extends Activity implements ClusterHomeActivityInterface {
+
     private static final String TAG = ClusterHomeActivity.class.getSimpleName();
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -40,7 +38,18 @@
         View view = getLayoutInflater().inflate(R.layout.cluster_home_activity, /* root= */ null);
         setContentView(view);
         logIntent(getIntent());
-   }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This activity is used for FULL mode only, thus always return {@code false}.
+     *    Use {@link ClusterHomeActivityLightMode} for the LIGHT mode.
+     */
+    @Override
+    public boolean isClusterInLightMode() {
+        return false;
+    }
 
     @Override
     protected void onNewIntent(Intent intent) {
diff --git a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivityInterface.java b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivityInterface.java
new file mode 100644
index 0000000..91f902b
--- /dev/null
+++ b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivityInterface.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.cluster.home;
+
+/** Interface for cluster activities. */
+interface ClusterHomeActivityInterface {
+
+    /**
+     * Returns true if the activity is designed to run in the LIGHT mode.
+     */
+    boolean isClusterInLightMode();
+}
diff --git a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivityLightMode.java b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivityLightMode.java
new file mode 100644
index 0000000..0240339
--- /dev/null
+++ b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivityLightMode.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.cluster.home;
+
+import android.car.Car;
+import android.car.cluster.ClusterHomeManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.TextView;
+
+public class ClusterHomeActivityLightMode extends ClusterHomeActivity {
+
+    private static final String TAG = ClusterHomeActivityLightMode.class.getSimpleName();
+    private static final long HEARTBEAT_INTERVAL_MS = 1000; // 1 second interval.
+
+    private ClusterHomeManager mClusterHomeManager;
+    private TextView mTextView;
+    private String mText;
+
+    private final Runnable mSendHeartbeatsRunnable = () -> sendHeartbeats();
+
+    /**
+     * Returns true if the activity is designed to run in the LIGHT mode.
+     *
+     * <p>This activity is used for LIGHT mode only, thus always return {@code true}.
+     *    Use {@link ClusterHomeActivity} for the FULL mode.
+     */
+    @Override
+    public boolean isClusterInLightMode() {
+        return true;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mTextView = findViewById(R.id.text);
+        mText = getResources().getString(R.string.cluster_home_text);
+
+        Car car = Car.createCar(getApplicationContext());
+        mClusterHomeManager = (ClusterHomeManager) car.getCarManager(ClusterHomeManager.class);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        mClusterHomeManager.startVisibilityMonitoring(this);
+        Log.i(TAG, "Visibility monitoring started");
+
+        // Clean up the handler queue.
+        getMainThreadHandler().removeCallbacks(mSendHeartbeatsRunnable);
+        // Start sending the heartbeats.
+        sendHeartbeats();
+    }
+
+    @Override
+    public void onStop() {
+        getMainThreadHandler().removeCallbacks(mSendHeartbeatsRunnable);
+        super.onStop();
+    }
+
+    private void sendHeartbeats() {
+        long nanoTime = System.nanoTime();
+        mClusterHomeManager.sendHeartbeat(nanoTime, /* appMetadata= */ null);
+        mTextView.setText(mText + "\nHeartbeat sent: " + nanoTime);
+
+        getMainThreadHandler().postDelayed(mSendHeartbeatsRunnable, HEARTBEAT_INTERVAL_MS);
+    }
+}
diff --git a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java
index 2e656a2..280e589 100644
--- a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java
+++ b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java
@@ -27,6 +27,7 @@
 import static android.content.Intent.ACTION_MAIN;
 import static android.hardware.input.InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
 
+import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
@@ -54,8 +55,10 @@
 import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
 import android.hardware.input.InputManager;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.ArraySet;
 import android.util.Log;
 import android.view.Display;
@@ -77,10 +80,11 @@
     private static final byte UI_AVAILABLE = 1;
 
     private PackageManager mPackageManager;
+    private UserManager mUserManager;
     private IActivityTaskManager mAtm;
     private InputManager mInputManager;
     private ClusterHomeManager mHomeManager;
-    private CarUserManager mUserManager;
+    private CarUserManager mCarUserManager;
     private CarInputManager mCarInputManager;
     private CarAppFocusManager mAppFocusManager;
     private ClusterState mClusterState;
@@ -93,6 +97,58 @@
     private int mLastLaunchedUiType = UI_TYPE_CLUSTER_NONE;
     private int mLastReportedUiType = UI_TYPE_CLUSTER_NONE;
 
+    private boolean mIsLightMode = false;
+    private boolean mIsInitialized = false;
+
+    // Note that we use this callback to detect which cluster service mode (either FULL or LIGHT),
+    // by looking at what cluster activity is being created. This is a hack to support both service
+    // modes with a single sample application. In actual production scenarios, only one service
+    // will be supported on a given device, thus there is no need for this callback mechanism.
+    private final ActivityLifecycleCallbacks mActivityLifecycleCallbacks =
+            new ActivityLifecycleCallbacks() {
+                @Override
+                public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
+                    // Set the mode based on the home activity class that is being created.
+                    if (activity instanceof ClusterHomeActivityInterface) {
+                        mIsLightMode =
+                                ((ClusterHomeActivityInterface) activity).isClusterInLightMode();
+                    }
+                    // Initialize before the first activity is created.
+                    if (!mIsInitialized) {
+                        mIsInitialized = true;
+                        initClusterHome();
+                    }
+                }
+
+                @Override
+                public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+                }
+
+                @Override
+                public void onActivityStarted(Activity activity) {
+                }
+
+                @Override
+                public void onActivityResumed(Activity activity) {
+                }
+
+                @Override
+                public void onActivityPaused(Activity activity) {
+                }
+
+                @Override
+                public void onActivityStopped(Activity activity) {
+                }
+
+                @Override
+                public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+                }
+
+                @Override
+                public void onActivityDestroyed(Activity activity) {
+                }
+            };
+
     @Override
     public void onCreate() {
         super.onCreate();
@@ -106,6 +162,7 @@
                 ComponentName.unflattenFromString(getString(R.string.config_clusterPhoneActivity)));
         mDefaultClusterActivitySize = mClusterActivities.size();
         mPackageManager = getApplicationContext().getPackageManager();
+        mUserManager = getApplicationContext().getSystemService(UserManager.class);
         mAtm = ActivityTaskManager.getService();
         try {
             mAtm.registerTaskStackListener(mTaskStackListener);
@@ -119,35 +176,46 @@
                 (car, ready) -> {
                     if (!ready) return;
                     mHomeManager = (ClusterHomeManager) car.getCarManager(Car.CLUSTER_HOME_SERVICE);
-                    mUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
+                    mCarUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
                     mCarInputManager = (CarInputManager) car.getCarManager(Car.CAR_INPUT_SERVICE);
                     mAppFocusManager = (CarAppFocusManager) car.getCarManager(
                             Car.APP_FOCUS_SERVICE);
-                    initClusterHome();
                 });
+
+        registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
     }
 
     private void initClusterHome() {
+        Log.i(TAG, "initClusterHome() in " + (mIsLightMode ? "LIGHT" : "FULL") + " mode");
         if (mHomeManager == null) {
             Log.e(TAG, "ClusterHome is null (ClusterHomeService may not be enabled), "
                     + "Stopping ClusterHomeSample.");
             return;
         }
-        mHomeManager.registerClusterStateListener(getMainExecutor(),mClusterHomeCalback);
+        // In the LIGHT mode, the HOME activity (DriverUI) takes care of everything, so we just
+        // stay as the UI_TYPE_HOME, and do not need any logic to switch activities to different
+        // types.
+        if (mIsLightMode) {
+            return;
+        }
+
+        mHomeManager.registerClusterStateListener(getMainExecutor(), mClusterHomeCallback);
         mClusterState = mHomeManager.getClusterState();
         if (!mClusterState.on) {
             mHomeManager.requestDisplay(UI_TYPE_HOME);
         }
         mUiAvailability = buildUiAvailability(ActivityManager.getCurrentUser());
         mHomeManager.reportState(mClusterState.uiType, UI_TYPE_CLUSTER_NONE, mUiAvailability);
-        mHomeManager.registerClusterStateListener(getMainExecutor(), mClusterHomeCalback);
 
         // Using the filter, only listens to the current user starting or unlocked events.
         UserLifecycleEventFilter filter = new UserLifecycleEventFilter.Builder()
                 .addUser(UserHandle.CURRENT)
                 .addEventType(USER_LIFECYCLE_EVENT_TYPE_STARTING)
                 .addEventType(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED).build();
-        mUserManager.addListener(getMainExecutor(), filter, mUserLifecycleListener);
+        mCarUserManager.addListener(getMainExecutor(), filter, mUserLifecycleListener);
+        if (mUserManager.isUserUnlocked(UserHandle.of(ActivityManager.getCurrentUser()))) {
+            mUserLifeCycleEvent = USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
+        }
 
         mAppFocusManager.addFocusListener(mAppFocusChangedListener,
                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
@@ -168,14 +236,17 @@
 
     @Override
     public void onTerminate() {
-        mCarInputManager.releaseInputEventCapture(DISPLAY_TYPE_INSTRUMENT_CLUSTER);
-        mUserManager.removeListener(mUserLifecycleListener);
-        mHomeManager.unregisterClusterStateListener(mClusterHomeCalback);
-        try {
-            mAtm.unregisterTaskStackListener(mTaskStackListener);
-        } catch (RemoteException e) {
-            Log.e(TAG, "remote exception from AM", e);
+        if (!mIsLightMode) {
+            mCarInputManager.releaseInputEventCapture(DISPLAY_TYPE_INSTRUMENT_CLUSTER);
+            mCarUserManager.removeListener(mUserLifecycleListener);
+            mHomeManager.unregisterClusterStateListener(mClusterHomeCallback);
+            try {
+                mAtm.unregisterTaskStackListener(mTaskStackListener);
+            } catch (RemoteException e) {
+                Log.e(TAG, "remote exception from AM", e);
+            }
         }
+        unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
         super.onTerminate();
     }
 
@@ -189,6 +260,14 @@
             Log.w(TAG, "Cluster display is not ready");
             return;
         }
+
+        // If this is the first activity to start, and the user is already unlocked,
+        // use UI_TYPE_START activity instead of UI_TYPE_HOME activity.
+        if (mLastLaunchedUiType == UI_TYPE_CLUSTER_NONE && uiType == UI_TYPE_HOME
+                && mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) {
+            Log.i(TAG, "Starting START UI instead of HOME UI, since user is already unlocked.");
+            uiType = UI_TYPE_START;
+        }
         mLastLaunchedUiType = uiType;
         ComponentName activity = mClusterActivities.get(uiType);
 
@@ -259,7 +338,7 @@
         return availability;
     }
 
-    private final ClusterStateListener mClusterHomeCalback = new ClusterStateListener() {
+    private final ClusterStateListener mClusterHomeCallback = new ClusterStateListener() {
         @Override
         public void onClusterStateChanged(
                 ClusterState state, @ClusterHomeManager.Config int changes) {
diff --git a/ClusterOsDouble/src/com/android/car/cluster/view/ClusterViewModel.java b/ClusterOsDouble/src/com/android/car/cluster/view/ClusterViewModel.java
index f43d2be..c094ee8 100644
--- a/ClusterOsDouble/src/com/android/car/cluster/view/ClusterViewModel.java
+++ b/ClusterOsDouble/src/com/android/car/cluster/view/ClusterViewModel.java
@@ -41,6 +41,7 @@
 import com.android.car.cluster.osdouble.R;
 import com.android.car.cluster.sensors.Sensor;
 import com.android.car.cluster.sensors.Sensors;
+import com.android.internal.annotations.GuardedBy;
 
 import java.text.DecimalFormat;
 import java.util.Map;
@@ -56,6 +57,9 @@
 
     private float mSpeedFactor;
     private float mDistanceFactor;
+    // mFuelCapacity is initialized once in registerCarPropertiesListener(), so doesn't need the
+    // lock.
+    private Float mFuelCapacity;
 
     public enum NavigationActivityState {
         /** No activity has been selected to be displayed on the navigation fragment yet */
@@ -106,6 +110,7 @@
     private void registerCarPropertiesListener() throws CarNotConnectedException {
         Sensors sensors = Sensors.getInstance();
         mCarPropertyManager = (CarPropertyManager) mCar.getCarManager(Car.PROPERTY_SERVICE);
+        mFuelCapacity = getSensorValue(Sensors.SENSOR_FUEL_CAPACITY);
         for (int propertyId : sensors.getPropertyIds()) {
             try {
                 mCarPropertyManager.registerCallback(mCarPropertyEventCallback,
@@ -230,17 +235,16 @@
      */
     public LiveData<Integer> getFuelLevel() {
         return Transformations.map(getSensor(Sensors.SENSOR_FUEL), (fuelValue) -> {
-            Float fuelCapacityValue = getSensorValue(Sensors.SENSOR_FUEL_CAPACITY);
-            if (fuelValue == null || fuelCapacityValue == null || fuelCapacityValue == 0) {
+            if (fuelValue == null || mFuelCapacity == null || mFuelCapacity == 0) {
                 return null;
             }
             if (fuelValue < 0.0f) {
                 return 0;
             }
-            if (fuelValue > fuelCapacityValue) {
+            if (fuelValue > mFuelCapacity) {
                 return 100;
             }
-            return Math.round(fuelValue / fuelCapacityValue * 100f);
+            return Math.round(fuelValue / mFuelCapacity * 100f);
         });
     }