diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:00:32 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:00:32 +0000 |
commit | 92473ff9d767db1718e05a3b884f5377d71871e9 (patch) | |
tree | f7872b14f3b3c12d604bad20ee49e0def4500ec3 | |
parent | 955fa10612e88e9a673746ef7f38134602e3c18f (diff) | |
parent | 8f99a063b20dd5a18da30d1f1b09568bf5624117 (diff) | |
download | Cluster-android14-mainline-media-release.tar.gz |
Snap for 10453563 from 8f99a063b20dd5a18da30d1f1b09568bf5624117 to mainline-media-releaseaml_med_341619000aml_med_341513600aml_med_341312300aml_med_341312020aml_med_341111000aml_med_341011000aml_med_340922010android14-mainline-media-release
Change-Id: Ib1e5676148404b42d3b61eb8d0c4fb3da044d2a9
12 files changed, 371 insertions, 139 deletions
diff --git a/ClusterHomeSample/AndroidManifest.xml b/ClusterHomeSample/AndroidManifest.xml index 1dc7eb5..b2ca15f 100644 --- a/ClusterHomeSample/AndroidManifest.xml +++ b/ClusterHomeSample/AndroidManifest.xml @@ -21,6 +21,8 @@ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/> <!-- for IActivityManager.registerTaskStackListener() --> <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/> + <!-- for PackageManager.queryIntentActivitiesAsUser() --> + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- for ClusterHomeManager --> <uses-permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"/> <!-- for CarInputManager.requestInputEventCapture() --> diff --git a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java index c85fa41..2e656a2 100644 --- a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java +++ b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java @@ -35,6 +35,8 @@ import android.app.IActivityTaskManager; import android.app.TaskInfo; import android.app.TaskStackListener; import android.car.Car; +import android.car.CarAppFocusManager; +import android.car.CarAppFocusManager.OnAppFocusChangedListener; import android.car.CarOccupantZoneManager; import android.car.cluster.ClusterActivityState; import android.car.cluster.ClusterHomeManager; @@ -46,40 +48,47 @@ import android.car.user.CarUserManager.UserLifecycleListener; import android.car.user.UserLifecycleEventFilter; import android.content.ComponentName; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.ResolveInfoFlags; +import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.hardware.input.InputManager; import android.os.RemoteException; import android.os.UserHandle; +import android.util.ArraySet; import android.util.Log; import android.view.Display; import android.view.KeyEvent; +import java.util.ArrayList; import java.util.List; public final class ClusterHomeApplication extends Application { public static final String TAG = "ClusterHome"; - private static final boolean DBG = false; + private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); private static final int UI_TYPE_HOME = UI_TYPE_CLUSTER_HOME; private static final int UI_TYPE_MAPS = UI_TYPE_HOME + 1; private static final int UI_TYPE_MUSIC = UI_TYPE_HOME + 2; private static final int UI_TYPE_PHONE = UI_TYPE_HOME + 3; private static final int UI_TYPE_START = UI_TYPE_MAPS; - private static final byte HOME_AVAILABILITY = 1; - private static final byte MAPS_AVAILABILITY = 1; - private static final byte PHONE_AVAILABILITY = 1; - private static final byte MUSIC_AVAILABILITY = 1; + private static final byte UI_UNAVAILABLE = 0; + private static final byte UI_AVAILABLE = 1; + private PackageManager mPackageManager; private IActivityTaskManager mAtm; private InputManager mInputManager; private ClusterHomeManager mHomeManager; private CarUserManager mUserManager; private CarInputManager mCarInputManager; + private CarAppFocusManager mAppFocusManager; private ClusterState mClusterState; private byte mUiAvailability[]; private int mUserLifeCycleEvent = USER_LIFECYCLE_EVENT_TYPE_STARTING; - private ComponentName[] mClusterActivities; + private ArrayList<ComponentName> mClusterActivities = new ArrayList<>(); + private int mDefaultClusterActivitySize = 0; private int mLastLaunchedUiType = UI_TYPE_CLUSTER_NONE; private int mLastReportedUiType = UI_TYPE_CLUSTER_NONE; @@ -87,15 +96,16 @@ public final class ClusterHomeApplication extends Application { @Override public void onCreate() { super.onCreate(); - mClusterActivities = new ComponentName[] { - new ComponentName(getApplicationContext(), ClusterHomeActivity.class), - ComponentName.unflattenFromString( - getString(R.string.config_clusterMapActivity)), - ComponentName.unflattenFromString( - getString(R.string.config_clusterMusicActivity)), - ComponentName.unflattenFromString( - getString(R.string.config_clusterPhoneActivity)), - }; + mClusterActivities.add(UI_TYPE_HOME, + new ComponentName(getApplicationContext(), ClusterHomeActivity.class)); + mClusterActivities.add(UI_TYPE_MAPS, + ComponentName.unflattenFromString(getString(R.string.config_clusterMapActivity))); + mClusterActivities.add(UI_TYPE_MUSIC, + ComponentName.unflattenFromString(getString(R.string.config_clusterMusicActivity))); + mClusterActivities.add(UI_TYPE_PHONE, + ComponentName.unflattenFromString(getString(R.string.config_clusterPhoneActivity))); + mDefaultClusterActivitySize = mClusterActivities.size(); + mPackageManager = getApplicationContext().getPackageManager(); mAtm = ActivityTaskManager.getService(); try { mAtm.registerTaskStackListener(mTaskStackListener); @@ -111,6 +121,8 @@ public final class ClusterHomeApplication extends Application { mHomeManager = (ClusterHomeManager) car.getCarManager(Car.CLUSTER_HOME_SERVICE); mUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE); mCarInputManager = (CarInputManager) car.getCarManager(Car.CAR_INPUT_SERVICE); + mAppFocusManager = (CarAppFocusManager) car.getCarManager( + Car.APP_FOCUS_SERVICE); initClusterHome(); }); } @@ -126,7 +138,7 @@ public final class ClusterHomeApplication extends Application { if (!mClusterState.on) { mHomeManager.requestDisplay(UI_TYPE_HOME); } - mUiAvailability = buildUiAvailability(); + mUiAvailability = buildUiAvailability(ActivityManager.getCurrentUser()); mHomeManager.reportState(mClusterState.uiType, UI_TYPE_CLUSTER_NONE, mUiAvailability); mHomeManager.registerClusterStateListener(getMainExecutor(), mClusterHomeCalback); @@ -137,6 +149,9 @@ public final class ClusterHomeApplication extends Application { .addEventType(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED).build(); mUserManager.addListener(getMainExecutor(), filter, mUserLifecycleListener); + mAppFocusManager.addFocusListener(mAppFocusChangedListener, + CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); + int r = mCarInputManager.requestInputEventCapture( DISPLAY_TYPE_INSTRUMENT_CLUSTER, new int[]{CarInputManager.INPUT_TYPE_ALL_INPUTS}, @@ -175,7 +190,7 @@ public final class ClusterHomeApplication extends Application { return; } mLastLaunchedUiType = uiType; - ComponentName activity = mClusterActivities[uiType]; + ComponentName activity = mClusterActivities.get(uiType); Intent intent = new Intent(ACTION_MAIN).setComponent(activity); if (mClusterState.bounds != null && mClusterState.insets != null) { @@ -195,17 +210,63 @@ public final class ClusterHomeApplication extends Application { mHomeManager.startFixedActivityModeAsUser(intent, options.toBundle(), userId); } - private byte[] buildUiAvailability() { - // TODO(b/183115088): populate uiAvailability based on the package availability - return new byte[] { - HOME_AVAILABILITY, MAPS_AVAILABILITY, PHONE_AVAILABILITY, MUSIC_AVAILABILITY - }; + private void add3PNavigationActivities(int currentUser) { + // Clean up the 3P Navigations from the previous user. + mClusterActivities.subList(mDefaultClusterActivitySize, mClusterActivities.size()).clear(); + + ArraySet<String> clusterPackages = new ArraySet<>(); + for (int i = mDefaultClusterActivitySize - 1; i >= 0; --i) { + clusterPackages.add(mClusterActivities.get(i).getPackageName()); + } + Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Car.CAR_CATEGORY_NAVIGATION); + List<ResolveInfo> resolveList = mPackageManager.queryIntentActivitiesAsUser( + intent, ResolveInfoFlags.of(PackageManager.GET_RESOLVED_FILTER), + UserHandle.of(currentUser)); + for (int i = resolveList.size() - 1; i >= 0; --i) { + ActivityInfo activityInfo = resolveList.get(i).activityInfo; + if (DBG) Log.d(TAG, "Found: " + activityInfo.packageName + "/" + activityInfo.name); + // Some package can have multiple navigation Activities, we choose the default one only. + if (clusterPackages.contains(activityInfo.packageName)) { + if (DBG) { + Log.d(TAG, "Skip this, because another Activity in the package is registered."); + }; + continue; + } + mClusterActivities.add(new ComponentName(activityInfo.packageName, activityInfo.name)); + } + mUiAvailability = buildUiAvailability(currentUser); + } + + private byte[] buildUiAvailability(int currentUser) { + byte[] availability = new byte[mClusterActivities.size()]; + Intent intent = new Intent(ACTION_MAIN); + for (int i = mClusterActivities.size() - 1; i >= 0; --i) { + ComponentName clusterActivity = mClusterActivities.get(i); + if (clusterActivity.getPackageName().equals(getPackageName())) { + // Assume that all Activities in ClusterHome are available. + availability[i] = UI_AVAILABLE; + continue; + } + intent.setComponent(clusterActivity); + ResolveInfo resolveInfo = mPackageManager.resolveActivityAsUser( + intent, PackageManager.MATCH_DEFAULT_ONLY, currentUser); + availability[i] = resolveInfo == null ? UI_UNAVAILABLE : UI_AVAILABLE; + if (DBG) { + Log.d(TAG, "availability=" + availability[i] + ", activity=" + clusterActivity + + ", userId=" + currentUser); + } + } + return availability; } private final ClusterStateListener mClusterHomeCalback = new ClusterStateListener() { @Override public void onClusterStateChanged( ClusterState state, @ClusterHomeManager.Config int changes) { + if (DBG) { + Log.d(TAG, "onClusterStateChanged: changes=" + Integer.toHexString(changes) + + ", state=" + clusterStateToString(state)); + } mClusterState = state; // We'll restart Activity when the display bounds or insets are changed, to let Activity // redraw itself to fit the changed attributes. @@ -255,8 +316,8 @@ public final class ClusterHomeApplication extends Application { } private int identifyTopTask(TaskInfo taskInfo) { - for (int i = mClusterActivities.length - 1; i >=0; --i) { - if (mClusterActivities[i].equals(taskInfo.topActivity)) { + for (int i = mClusterActivities.size() - 1; i >=0; --i) { + if (mClusterActivities.get(i).equals(taskInfo.topActivity)) { return i; } } @@ -269,9 +330,11 @@ public final class ClusterHomeApplication extends Application { mUserLifeCycleEvent = event.getEventType(); if (mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_STARTING) { startClusterActivity(UI_TYPE_HOME); - } else if (UI_TYPE_HOME != UI_TYPE_START - && mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) { - startClusterActivity(UI_TYPE_START); + } else if (mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) { + add3PNavigationActivities(event.getUserId()); + if (UI_TYPE_START != UI_TYPE_HOME) { + startClusterActivity(UI_TYPE_START); + } } }; @@ -287,11 +350,74 @@ public final class ClusterHomeApplication extends Application { if (DBG) Log.d(TAG, "onKeyEvent: " + keyEvent); if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MENU) { if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) return; - int nextUiType = (mLastLaunchedUiType + 1) % mUiAvailability.length; + int nextUiType; + do { + // Select the Cluster Activity within the preinstalled ones. + nextUiType = mLastLaunchedUiType + 1; + if (nextUiType >= mDefaultClusterActivitySize) nextUiType = 0; + } while (mUiAvailability[nextUiType] == UI_UNAVAILABLE); startClusterActivity(nextUiType); return; } // Use Android InputManager to forward KeyEvent. mInputManager.injectInputEvent(keyEvent, INJECT_INPUT_EVENT_MODE_ASYNC); } + + private OnAppFocusChangedListener mAppFocusChangedListener = new OnAppFocusChangedListener() { + @Override + public void onAppFocusChanged(int appType, boolean active) { + if (!active || appType != CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION) { + return; + } + int navigationUi = getFocusedNavigationUi(); + if (navigationUi != UI_TYPE_CLUSTER_NONE) { + startClusterActivity(navigationUi); + } + } + }; + + private int getFocusedNavigationUi() { + List<String> focusOwnerPackageNames = mAppFocusManager.getAppTypeOwner( + CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); + if (focusOwnerPackageNames == null || focusOwnerPackageNames.isEmpty()) { + Log.e(TAG, "Can't find the navigation owner"); + return UI_TYPE_CLUSTER_NONE; + } + for (int i = 0; i < focusOwnerPackageNames.size(); ++i) { + String focusOwnerPackage = focusOwnerPackageNames.get(i); + for (int j = mClusterActivities.size() - 1; j >= 0; --j) { + if (mUiAvailability[j] == UI_UNAVAILABLE) { + continue; + } + if (mClusterActivities.get(j).getPackageName().equals(focusOwnerPackage)) { + if (DBG) { + Log.d(TAG, "Found focused NavigationUI: " + j + + ", package=" + focusOwnerPackage); + } + return j; + } + } + } + Log.e(TAG, "Can't find the navigation UI for " + + String.join(", ", focusOwnerPackageNames) + "."); + return UI_TYPE_CLUSTER_NONE; + } + + private static String clusterStateToString(ClusterState state) { + StringBuilder sb = new StringBuilder("ClusterState["); + sb.append("on="); sb.append(state.on); + if (state.bounds != null) { + sb.append(", bounds="); sb.append(state.bounds); + } + if (state.insets != null) { + sb.append(", insets="); sb.append(state.insets); + } + if (state.insets != null) { + sb.append(", insets="); sb.append(state.insets); + } + sb.append(", uiType="); sb.append(state.uiType); + sb.append(", displayId="); sb.append(state.displayId); + sb.append(']'); + return sb.toString(); + } } diff --git a/ClusterOsDouble/AndroidManifest.xml b/ClusterOsDouble/AndroidManifest.xml index 5e8d334..e57b260 100644 --- a/ClusterOsDouble/AndroidManifest.xml +++ b/ClusterOsDouble/AndroidManifest.xml @@ -13,9 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. --> - <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.car.cluster.osdouble"> + package="com.android.car.cluster.osdouble" + android:sharedUserId="android.uid.system"> + <!-- system uid is required to access the local private display --> <!-- Required to show car sensor data --> <uses-permission android:name="android.car.permission.CAR_SPEED"/> diff --git a/ClusterOsDouble/res/values/config.xml b/ClusterOsDouble/res/values/config.xml index 7f1cb52..e85c206 100644 --- a/ClusterOsDouble/res/values/config.xml +++ b/ClusterOsDouble/res/values/config.xml @@ -18,5 +18,8 @@ <!-- Resources to configure based on each OEM's preference. --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <!-- DisplayPort to launch ClusterOsDouble --> - <integer name="config_clusterDisplayPort">1</integer> + <integer name="config_clusterDisplayPort">-1</integer> + <!-- Display uniqueId to launch ClusterOsDouble. --> + <!-- To use this values, set config_clusterDisplayPort to -1. --> + <string name="config_clusterDisplayUniqueId" /> </resources> diff --git a/ClusterOsDouble/res/values/overlayable.xml b/ClusterOsDouble/res/values/overlayable.xml index 465c39f..329ffee 100644 --- a/ClusterOsDouble/res/values/overlayable.xml +++ b/ClusterOsDouble/res/values/overlayable.xml @@ -19,6 +19,7 @@ <overlayable name="ClusterOsConfig"> <policy type="product|system|vendor"> <item type="integer" name="config_clusterDisplayPort" /> + <item type="string" name="config_clusterDisplayUniqueId" /> </policy> </overlayable> </resources> diff --git a/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleActivity.java b/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleActivity.java index 8ba7eb3..f86b001 100644 --- a/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleActivity.java +++ b/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleActivity.java @@ -20,23 +20,26 @@ import static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL; import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_HOME; import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_NONE; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; -import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED; import static com.android.car.cluster.osdouble.ClusterOsDoubleApplication.TAG; import android.car.Car; -import android.car.cluster.navigation.NavigationState.NavigationStateProto; import android.car.VehiclePropertyIds; +import android.car.cluster.navigation.NavigationState.NavigationStateProto; import android.car.hardware.CarPropertyValue; import android.car.hardware.property.CarPropertyManager; import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback; +import android.content.res.CompatibilityInfo; import android.graphics.Insets; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.os.Bundle; import android.util.ArrayMap; +import android.util.DisplayMetrics; +import android.util.IntArray; import android.util.Log; +import android.view.DisplayInfo; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; @@ -83,19 +86,23 @@ public class ClusterOsDoubleActivity extends ComponentActivity { private static final int VENDOR_CLUSTER_DISPLAY_STATE = toVendorId( VehiclePropertyIds.CLUSTER_DISPLAY_STATE); + // For the detail, please refer to vehicle/2.0/types.hal. + private static final int REPORT_STATE_MAIN_UI_INDEX = 9; + private static final int REPORT_STATE_UI_AVAILABILITY_INDEX = 11; + private DisplayManager mDisplayManager; private CarPropertyManager mPropertyManager; private SurfaceView mSurfaceView; private Rect mBounds; private Insets mInsets; - private VirtualDisplay mVirtualDisplay; + private static VirtualDisplay sVirtualDisplay; private ClusterViewModel mClusterViewModel; private final ArrayMap<Sensors.Gear, View> mGearsToIcon = new ArrayMap<>(); private final ArrayList<View> mUiToButton = new ArrayList<>(); int mCurrentUi = UI_TYPE_CLUSTER_HOME; - int mTotalUiSize; + private final IntArray mUiAvailability = new IntArray(); private NavStateController mNavStateController; @@ -143,15 +150,6 @@ public class ClusterOsDoubleActivity extends ComponentActivity { findViewById(R.id.navigation_state), imageResolver); } - @Override - protected void onDestroy() { - if (mVirtualDisplay != null) { - mVirtualDisplay.release(); - mVirtualDisplay = null; - } - super.onDestroy(); - } - private final SurfaceHolder.Callback mSurfaceViewCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { @@ -173,10 +171,23 @@ public class ClusterOsDoubleActivity extends ComponentActivity { // Adds some empty space in the boundary of the display to verify if mBounds works. mBounds.inset(/* dx= */ 12, /* dy= */ 12); mInsets = Insets.of(obscuredWidth, obscuredHeight, obscuredWidth, obscuredHeight); - if (mVirtualDisplay == null) { - mVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height); + if (sVirtualDisplay == null) { + sVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height); } else { - mVirtualDisplay.setSurface(holder.getSurface()); + DisplayInfo displayInfo = new DisplayInfo(); + DisplayMetrics boundsMetrics = new DisplayMetrics(); + boolean isDisplayValid = sVirtualDisplay.getDisplay().getDisplayInfo(displayInfo); + displayInfo.getLogicalMetrics(boundsMetrics, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, /* configuration= */ null); + if (isDisplayValid && boundsMetrics.widthPixels == width + && boundsMetrics.heightPixels == height) { + sVirtualDisplay.setSurface(holder.getSurface()); + } else { + // Display was resized, delete existing and create new display. + // TODO(b/254931119): Resize the display instead of replacing it. + sVirtualDisplay.release(); + sVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height); + } } } @@ -185,7 +196,7 @@ public class ClusterOsDoubleActivity extends ComponentActivity { Log.i(TAG, "surfaceDestroyed, holder: " + holder + ", detaching surface from" + " display, surface: " + holder.getSurface()); // detaching surface is similar to turning off the display - mVirtualDisplay.setSurface(null); + sVirtualDisplay.setSurface(null); } }; @@ -234,15 +245,17 @@ public class ClusterOsDoubleActivity extends ComponentActivity { if (values.length < 11) { throw new IllegalArgumentException("Insufficient size of CLUSTER_REPORT_STATE"); } - // mainUI is the 10th element, refer to vehicle/2.0/types.hal. - int mainUi = (Integer) values[9]; - if (mainUi >= 0 && mainUi < mTotalUiSize) { - selectUiButton(mainUi); + int mainUi = (Integer) values[REPORT_STATE_MAIN_UI_INDEX]; + int totalUiSize = values.length - REPORT_STATE_UI_AVAILABILITY_INDEX; + mUiAvailability.resize(totalUiSize); + for (int i = 0; i < totalUiSize; ++i) { + mUiAvailability.set(i, (Byte) values[i + REPORT_STATE_UI_AVAILABILITY_INDEX]); } + selectUiButton(mainUi); } private void selectUiButton(int mainUi) { - for (int i = 0; i < mTotalUiSize; ++i) { + for (int i = mUiToButton.size() - 1; i >= 0; --i) { View button = mUiToButton.get(i); button.setSelected(i == mainUi); } @@ -265,7 +278,6 @@ public class ClusterOsDoubleActivity extends ComponentActivity { sendDisplayState(); } - private static int toVendorId(int propId) { return (propId & ~MASK) | VENDOR; } @@ -300,7 +312,6 @@ public class ClusterOsDoubleActivity extends ComponentActivity { private void registerUi(View view) { int currentUi = mUiToButton.size(); mUiToButton.add(view); - mTotalUiSize = mUiToButton.size(); view.setOnTouchListener((v, event) -> { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.d(TAG, "onTouch: " + currentUi); @@ -329,7 +340,12 @@ public class ClusterOsDoubleActivity extends ComponentActivity { public boolean onKeyDown(int keyCode, KeyEvent event) { Log.d(TAG, "onKeyDown: " + keyCode); if (keyCode == KeyEvent.KEYCODE_MENU) { - switchUi((mCurrentUi + 1) % mTotalUiSize); + int nextUi = mCurrentUi; + do { + nextUi = nextUi + 1; + if (nextUi >= mUiToButton.size()) nextUi = 0; + } while (mUiAvailability.get(nextUi) == 0); + switchUi(nextUi); return true; } return super.onKeyDown(keyCode, event); diff --git a/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleApplication.java b/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleApplication.java index dd102c7..4b63a9a 100644 --- a/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleApplication.java +++ b/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleApplication.java @@ -18,12 +18,16 @@ package com.android.car.cluster.osdouble; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import android.annotation.NonNull; import android.app.ActivityOptions; import android.app.Application; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.hardware.display.DisplayManager; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; import android.util.Log; import android.view.Display; import android.view.DisplayAddress; @@ -34,45 +38,126 @@ import android.view.DisplayAddress; public class ClusterOsDoubleApplication extends Application { public static final String TAG = "ClusterOsDouble"; - private DisplayManager mDisplayManager; - @Override public void onCreate() { super.onCreate(); Context context = getApplicationContext(); - mDisplayManager = context.getSystemService(DisplayManager.class); int displayPort = context.getResources().getInteger(R.integer.config_clusterDisplayPort); - if (displayPort == 0) { - Log.e(TAG, "Invalid resource: config_clusterDisplayPort"); - // Won't throw the exception, if so, it'll restart the application continuously, - // because this is the persistent application. + String displayUniqueId = context.getResources().getString( + R.string.config_clusterDisplayUniqueId); + + if (displayPort <= 0 && TextUtils.isEmpty(displayUniqueId)) { + Log.e(TAG, "Cluster display isn't configured."); return; } - int displayId = findDisplay(displayPort); - if (displayId == Display.INVALID_DISPLAY) { - Log.e(TAG, "Can't find the display with portId: " + displayPort); - return; + + DisplayManager displayManager = context.getSystemService(DisplayManager.class); + ClusterDisplayMonitor clusterDisplayMonitor = new ClusterDisplayMonitor(context, + displayManager, displayPort, displayUniqueId); + clusterDisplayMonitor.start(new Handler(Looper.myLooper())); + } + + /** + * Monitors displays and starts the cluster activity when the correct display becomes available. + */ + private static class ClusterDisplayMonitor { + private final Context mContext; + private final DisplayManager mDisplayManager; + private final int mDisplayPort; + private final String mDisplayUniqueId; + + private final DisplayManager.DisplayListener mDisplayListener = + new DisplayManager.DisplayListener() { + @Override + public void onDisplayAdded(int displayId) { + int clusterDisplayId = findClusterDisplayId(); + if (clusterDisplayId == displayId) { + Log.d(TAG, "Display " + displayId + " was added. Starting cluster."); + onDisplayReadyForCluster(displayId); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + // No-op + } + + @Override + public void onDisplayChanged(int displayId) { + // No-op + } + }; + + public ClusterDisplayMonitor(Context context, DisplayManager displayManager, + int displayPort, String displayUniqueId) { + mContext = context; + mDisplayManager = displayManager; + mDisplayPort = displayPort; + mDisplayUniqueId = displayUniqueId; + } + + public void start(Handler handler) { + int clusterDisplayId = findClusterDisplayId(); + if (clusterDisplayId != Display.INVALID_DISPLAY) { + onDisplayReadyForCluster(clusterDisplayId); + } + // This listener will never get unregistered. This is only ok as long as this is a + // persistent app that is not expected to stop. + mDisplayManager.registerDisplayListener(mDisplayListener, handler); } - Intent intent = Intent.makeMainActivity( - ComponentName.createRelative(context, ClusterOsDoubleActivity.class.getName())); - intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + private void onDisplayReadyForCluster(int displayId) { + Intent intent = Intent.makeMainActivity( + ComponentName.createRelative(mContext, + ClusterOsDoubleActivity.class.getName())); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); - ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId); - context.startActivity(intent, options.toBundle()); - } + ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId); + mContext.startActivity(intent, options.toBundle()); + } - private int findDisplay(int displayPort) { - for (Display display : mDisplayManager.getDisplays()) { - DisplayAddress address = display.getAddress(); - if (!(address instanceof DisplayAddress.Physical)) { - continue; + private int findClusterDisplayId() { + int displayId = Display.INVALID_DISPLAY; + if (mDisplayPort > 0) { + displayId = findDisplayByPort(mDisplayPort); + if (displayId == Display.INVALID_DISPLAY) { + Log.e(TAG, "Can't find the display with portId: " + mDisplayPort); + } + } else if (!TextUtils.isEmpty(mDisplayUniqueId)) { + displayId = findDisplayIdByUniqueId(mDisplayUniqueId); + if (displayId == Display.INVALID_DISPLAY) { + Log.e(TAG, "Can't find the display with uniqueId: " + mDisplayUniqueId); + } + } else { + // This should not ever happen. + Log.wtf(TAG, "No valid cluster display configs found."); } - DisplayAddress.Physical physical = (DisplayAddress.Physical) address; - if (physical.getPort() == displayPort) { - return display.getDisplayId(); + + return displayId; + } + + private int findDisplayIdByUniqueId(@NonNull String displayUniqueId) { + for (Display display : mDisplayManager.getDisplays()) { + if (displayUniqueId.equals(display.getUniqueId())) { + return display.getDisplayId(); + } } + return Display.INVALID_DISPLAY; + } + + private int findDisplayByPort(int displayPort) { + for (Display display : mDisplayManager.getDisplays()) { + DisplayAddress address = display.getAddress(); + if (!(address instanceof DisplayAddress.Physical)) { + continue; + } + DisplayAddress.Physical physical = (DisplayAddress.Physical) address; + if (physical.getPort() == displayPort) { + return display.getDisplayId(); + } + } + return Display.INVALID_DISPLAY; } - return Display.INVALID_DISPLAY; } + } diff --git a/DirectRenderingCluster/AndroidManifest.xml b/DirectRenderingCluster/AndroidManifest.xml index 6f1ce2b..c4eaa87 100644 --- a/DirectRenderingCluster/AndroidManifest.xml +++ b/DirectRenderingCluster/AndroidManifest.xml @@ -20,7 +20,7 @@ android:process="android.car.cluster" android:sharedUserId="android.uid.cluster"> - <uses-sdk android:targetSdkVersion="25" android:minSdkVersion="29"/> + <uses-sdk android:targetSdkVersion="30"/> <protected-broadcast android:name="android.car.cluster.NAVIGATION_STATE_UPDATE"/> @@ -42,8 +42,11 @@ <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 use CarOccupantZoneManager.getDisplayIdForDriver() --> + <uses-permission android:name="android.car.permission.ACCESS_PRIVATE_DISPLAY_ID"/> <!-- Required to watch activities running on the cluster --> <uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/> + <!-- Required to show car sensor data --> <uses-permission android:name="android.car.permission.CAR_ENERGY"/> <uses-permission android:name="android.car.permission.CAR_POWERTRAIN"/> @@ -53,7 +56,11 @@ <!-- Required to query packages in Android 11+ --> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> + <!-- Required to use PhoneStateListener.onCallStateChanged() --> + <uses-permission android:name="android.permission.READ_CALL_LOG" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> + <application android:label="@string/app_name" android:icon="@mipmap/ic_launcher" diff --git a/DirectRenderingCluster/src/android/car/cluster/ClusterDisplayProvider.java b/DirectRenderingCluster/src/android/car/cluster/ClusterDisplayProvider.java index 4050099..5949a50 100644 --- a/DirectRenderingCluster/src/android/car/cluster/ClusterDisplayProvider.java +++ b/DirectRenderingCluster/src/android/car/cluster/ClusterDisplayProvider.java @@ -18,7 +18,6 @@ package android.car.cluster; import android.car.Car; import android.car.CarOccupantZoneManager; -import android.car.CarOccupantZoneManager.OccupantZoneInfo; import android.content.Context; import android.hardware.display.DisplayManager.DisplayListener; import android.util.Log; @@ -26,8 +25,6 @@ import android.view.Display; import com.android.internal.util.Preconditions; -import java.util.List; - /** * This class provides a display for instrument cluster renderer. * <p> @@ -37,7 +34,7 @@ import java.util.List; */ public class ClusterDisplayProvider { private static final String TAG = "Cluster.DisplayProvider"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final DisplayListener mListener; private final Car mCar; @@ -66,35 +63,30 @@ public class ClusterDisplayProvider { Preconditions.checkArgument( occupantZoneManager != null,"Can't get CarOccupantZoneManager"); mOccupantZoneManager = occupantZoneManager; - checkClusterDisplayAdded(); + checkClusterDisplayChanged(); mOccupantZoneManager.registerOccupantZoneConfigChangeListener( new ClusterDisplayChangeListener()); } - private void checkClusterDisplayAdded() { - Display clusterDisplay = getClusterDisplay(); - if (clusterDisplay != null) { - Log.i(TAG, String.format("Found display: %s (id: %d, owner: %s)", - clusterDisplay.getName(), clusterDisplay.getDisplayId(), - clusterDisplay.getOwnerPackageName())); - mClusterDisplayId = clusterDisplay.getDisplayId(); - mListener.onDisplayAdded(clusterDisplay.getDisplayId()); + private void checkClusterDisplayChanged() { + int clusterDisplayId = getClusterDisplayId(); + if (clusterDisplayId == mClusterDisplayId) { + return; + } + if (mClusterDisplayId != Display.INVALID_DISPLAY) { + Log.i(TAG, "Cluster display is removed"); + mListener.onDisplayRemoved(mClusterDisplayId); + } + mClusterDisplayId = clusterDisplayId; + if (clusterDisplayId != Display.INVALID_DISPLAY) { + Log.i(TAG, "Found cluster displayId=" + clusterDisplayId); + mListener.onDisplayAdded(clusterDisplayId); } } - private Display getClusterDisplay() { - List<OccupantZoneInfo> zones = mOccupantZoneManager.getAllOccupantZones(); - int zones_size = zones.size(); - for (int i = 0; i < zones_size; ++i) { - OccupantZoneInfo zone = zones.get(i); - // Assumes that a Car has only one driver. - if (zone.occupantType == CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER) { - return mOccupantZoneManager.getDisplayForOccupant( - zone, CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER); - } - } - Log.e(TAG, "Can't find the OccupantZoneInfo for driver"); - return null; + private int getClusterDisplayId() { + return mOccupantZoneManager.getDisplayIdForDriver( + CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER); } private final class ClusterDisplayChangeListener implements @@ -105,15 +97,7 @@ public class ClusterDisplayProvider { if ((changeFlags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) == 0) { return; } - if (mClusterDisplayId == Display.INVALID_DISPLAY) { - checkClusterDisplayAdded(); - } else { - Display clusterDisplay = getClusterDisplay(); - if (clusterDisplay == null) { - mListener.onDisplayRemoved(mClusterDisplayId); - mClusterDisplayId = Display.INVALID_DISPLAY; - } - } + checkClusterDisplayChanged(); } } diff --git a/DirectRenderingCluster/src/android/car/cluster/ClusterViewModel.java b/DirectRenderingCluster/src/android/car/cluster/ClusterViewModel.java index 7412ac6..d02bab3 100644 --- a/DirectRenderingCluster/src/android/car/cluster/ClusterViewModel.java +++ b/DirectRenderingCluster/src/android/car/cluster/ClusterViewModel.java @@ -15,12 +15,15 @@ */ package android.car.cluster; +import static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL; + import android.annotation.Nullable; import android.app.Application; import android.car.Car; import android.car.CarAppFocusManager; import android.car.CarNotConnectedException; import android.car.VehicleAreaType; +import android.car.VehiclePropertyIds; import android.car.cluster.sensors.Sensor; import android.car.cluster.sensors.Sensors; import android.car.hardware.CarPropertyValue; @@ -131,7 +134,7 @@ public class ClusterViewModel extends AndroidViewModel { } for (Sensor<?> sensorId : Sensors.getInstance() .getSensorsForPropertyId(value.getPropertyId())) { - if (sensorId.mAreaId == Sensors.GLOBAL_AREA_ID + if (sensorId.mAreaId == VEHICLE_AREA_TYPE_GLOBAL || (sensorId.mAreaId & value.getAreaId()) != 0) { setSensorValue(sensorId, value); } @@ -217,19 +220,23 @@ public class ClusterViewModel extends AndroidViewModel { * Returns the current value of the sensor, directly from the VHAL. * * @param sensor sensor to read - * @param <V> VHAL data type * @param <T> data type of such sensor */ @Nullable public <T> T getSensorValue(@NonNull Sensor<T> sensor) { - try { - CarPropertyValue<?> value = mCarPropertyManager - .getProperty(sensor.mPropertyId, sensor.mAreaId); - return sensor.mAdapter.apply(value); - } catch (CarNotConnectedException ex) { - Log.e(TAG, "We got disconnected from Car Service", ex); + if (mCarPropertyManager == null) { + Log.e(TAG, "CarPropertyManager reference is null, car service is disconnected."); + return null; + } + CarPropertyValue<?> carPropertyValue = mCarPropertyManager.getProperty(sensor.mPropertyId, + sensor.mAreaId); + if (carPropertyValue == null) { + Log.w(TAG, "Property ID: " + VehiclePropertyIds.toString(sensor.mPropertyId) + + " Area ID: 0x" + Integer.toHexString(sensor.mAreaId) + + " returned null from CarPropertyManager#getProperty()"); return null; } + return sensor.mAdapter.apply(carPropertyValue); } /** diff --git a/DirectRenderingCluster/src/android/car/cluster/MusicFragment.java b/DirectRenderingCluster/src/android/car/cluster/MusicFragment.java index 873fef3..ac17371 100644 --- a/DirectRenderingCluster/src/android/car/cluster/MusicFragment.java +++ b/DirectRenderingCluster/src/android/car/cluster/MusicFragment.java @@ -85,6 +85,7 @@ public class MusicFragment extends Fragment { new MetadataController( getViewLifecycleOwner(), playbackViewModel, + null, title, subtitle, null, @@ -95,7 +96,8 @@ public class MusicFragment extends Fragment { seekBar, albumIcon, null, - new Size(artSize, artSize) + new Size(artSize, artSize), + null ); return view; diff --git a/DirectRenderingCluster/src/android/car/cluster/sensors/Sensors.java b/DirectRenderingCluster/src/android/car/cluster/sensors/Sensors.java index 25f6572..65dea7e 100644 --- a/DirectRenderingCluster/src/android/car/cluster/sensors/Sensors.java +++ b/DirectRenderingCluster/src/android/car/cluster/sensors/Sensors.java @@ -15,6 +15,8 @@ */ package android.car.cluster.sensors; +import static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL; + import android.car.VehiclePropertyIds; import android.car.VehiclePropertyType; import android.car.hardware.CarPropertyValue; @@ -31,9 +33,6 @@ import java.util.function.Function; * The collection of all sensors supported by this application. */ public class Sensors { - /** Area identifier used for sensors corresponding to global VHAL properties */ - public static final int GLOBAL_AREA_ID = -1; - private static Sensors sInstance; private static List<Sensor<?>> sSensors = new ArrayList<>(); private Map<Integer, List<Sensor<?>>> mSensorsByPropertyId = new HashMap<>(); @@ -48,35 +47,34 @@ public class Sensors { /** Fuel of the car, measured in millimeters */ public static final Sensor<Float> SENSOR_FUEL = registerSensor( - "Fuel", VehiclePropertyIds.FUEL_LEVEL, GLOBAL_AREA_ID, VehiclePropertyType.FLOAT, + "Fuel", VehiclePropertyIds.FUEL_LEVEL, VEHICLE_AREA_TYPE_GLOBAL, + VehiclePropertyType.FLOAT, value -> (Float) value.getValue()); /** Fuel capacity of the car, measured in millimeters */ public static final Sensor<Float> SENSOR_FUEL_CAPACITY = registerSensor( - "Fuel Capacity", VehiclePropertyIds.INFO_FUEL_CAPACITY, GLOBAL_AREA_ID, + "Fuel Capacity", VehiclePropertyIds.INFO_FUEL_CAPACITY, VEHICLE_AREA_TYPE_GLOBAL, VehiclePropertyType.FLOAT, value -> (Float) value.getValue()); /** RPMs */ public static final Sensor<Float> SENSOR_RPM = registerSensor( - "RPM", VehiclePropertyIds.ENGINE_RPM, GLOBAL_AREA_ID, + "RPM", VehiclePropertyIds.ENGINE_RPM, VEHICLE_AREA_TYPE_GLOBAL, VehiclePropertyType.FLOAT, value -> (Float) value.getValue()); /** Fuel range in meters */ public static final Sensor<Float> SENSOR_FUEL_RANGE = registerSensor( - "Fuel Range", VehiclePropertyIds.RANGE_REMAINING, GLOBAL_AREA_ID, + "Fuel Range", VehiclePropertyIds.RANGE_REMAINING, VEHICLE_AREA_TYPE_GLOBAL, VehiclePropertyType.FLOAT, value -> (Float) value.getValue()); /** Speed in meters per second */ public static final Sensor<Float> SENSOR_SPEED = registerSensor( - "Speed", VehiclePropertyIds.PERF_VEHICLE_SPEED, GLOBAL_AREA_ID, + "Speed", VehiclePropertyIds.PERF_VEHICLE_SPEED, VEHICLE_AREA_TYPE_GLOBAL, VehiclePropertyType.FLOAT, value -> (Float) value.getValue()); /** Current gear of the car */ public static final Sensor<Gear> SENSOR_GEAR = registerSensor( - "Gear", VehiclePropertyIds.GEAR_SELECTION, GLOBAL_AREA_ID, VehiclePropertyType.INT32, + "Gear", VehiclePropertyIds.GEAR_SELECTION, VEHICLE_AREA_TYPE_GLOBAL, + VehiclePropertyType.INT32, value -> { - if (value == null) { - return null; - } Integer gear = (Integer) value.getValue(); if ((gear & CarSensorEvent.GEAR_REVERSE) != 0) { return Gear.REVERSE; |