summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ClusterHomeSample/AndroidManifest.xml14
-rw-r--r--ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivity.java21
-rw-r--r--ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivityInterface.java26
-rw-r--r--ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeActivityLightMode.java84
-rw-r--r--ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java107
-rw-r--r--ClusterOsDouble/src/com/android/car/cluster/view/ClusterViewModel.java12
6 files changed, 240 insertions, 24 deletions
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 @@ public class ClusterHomeActivity extends Activity {
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.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED
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.PackageManager.ResolveInfoFlags;
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 @@ public final class ClusterHomeApplication extends Application {
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 @@ public final class ClusterHomeApplication extends Application {
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 @@ public final class ClusterHomeApplication extends Application {
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 @@ public final class ClusterHomeApplication extends Application {
(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 @@ public final class ClusterHomeApplication extends Application {
@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 @@ public final class ClusterHomeApplication extends Application {
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 @@ public final class ClusterHomeApplication extends Application {
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 androidx.lifecycle.Transformations;
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 @@ public class ClusterViewModel extends AndroidViewModel {
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 @@ public class ClusterViewModel extends AndroidViewModel {
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 class ClusterViewModel extends AndroidViewModel {
*/
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);
});
}