diff options
author | Xin Li <delphij@google.com> | 2024-03-06 09:30:03 -0800 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2024-03-06 09:30:03 -0800 |
commit | 5662586ca7ee155ef12ce8abe5ee69bcc327a8e0 (patch) | |
tree | 82155eb26a16c1b4942666b3e272ae3204b2454d | |
parent | aff0de653ef688b406fb6f8c4673d5b9697787f5 (diff) | |
parent | 3b1ea331ea0ee8dd73b09a2e25ad89fc59f6f408 (diff) | |
download | Cluster-master.tar.gz |
Bug: 319669529
Merged-In: I633ebf3e5a829ad6255edc3b6696eaeb4cf78d57
Change-Id: I647481c7c452ebb5cfc758e55b9ae0bd3c487c42
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); }); } |